@@ -9,7 +9,13 @@ import {
99 classifyPiProcessFailure ,
1010 defaultPiProcessRunner ,
1111 piProcessFailureMessage ,
12+ splitPiCommand ,
1213} from './pi-process.js' ;
14+ import {
15+ extractAzureResourceName ,
16+ resolveCliProvider ,
17+ resolveEnvKeyName ,
18+ } from './pi-provider-aliases.js' ;
1319import { extractPiTextContent , toFiniteNumber } from './pi-utils.js' ;
1420import { normalizeInputFiles } from './preread.js' ;
1521import type { PiRpcResolvedConfig } from './targets.js' ;
@@ -49,7 +55,7 @@ export class PiRpcProvider implements Provider {
4955
5056 const startedAt = Date . now ( ) ;
5157 const cwd = this . resolveCwd ( request . cwd ) ;
52- const command = ensureRpcMode ( this . config . command ) ;
58+ const command = this . buildCommand ( ) ;
5359 const inputFiles = normalizeInputFiles ( request . inputFiles ) ;
5460 const rpcRequest = buildRpcRequest ( {
5561 request,
@@ -61,9 +67,11 @@ export class PiRpcProvider implements Provider {
6167 command,
6268 cwd,
6369 timeoutMs : this . config . timeoutMs ,
64- env : buildPiRuntimeEnv ( { runtime : this . config . runtime , targetName : this . targetName } ) ,
70+ env : this . buildEnv ( ) ,
6571 signal : request . signal ,
6672 stdin : `${ JSON . stringify ( rpcRequest ) } \n` ,
73+ stdinEnd : 'manual' ,
74+ completeOnStdout : ( stdout ) => hasRpcAgentEnd ( stdout , rpcRequest . id ) ,
6775 } ) ;
6876
6977 if ( result . timedOut || result . exitCode !== 0 || result . signal || result . spawnErrorCode ) {
@@ -100,6 +108,18 @@ export class PiRpcProvider implements Provider {
100108 } ) ;
101109 }
102110
111+ const resultError = rpcResultError ( parsed . result ) ;
112+ if ( resultError ) {
113+ return this . buildRpcTaskFailureResponse ( {
114+ result,
115+ command,
116+ cwd,
117+ startedAt,
118+ message : resultError ,
119+ events : parsed . events ,
120+ } ) ;
121+ }
122+
103123 const output = messagesFromRpcResult ( parsed . result ) ;
104124 const tokenUsage = tokenUsageFromRpcResult ( parsed . result ) ;
105125 const endedAt = Date . now ( ) ;
@@ -150,6 +170,90 @@ export class PiRpcProvider implements Provider {
150170 return process . cwd ( ) ;
151171 }
152172
173+ private buildCommand ( ) : readonly string [ ] {
174+ const args : string [ ] = [ ] ;
175+ if ( this . config . subprovider ) {
176+ args . push ( '--provider' , resolveCliProvider ( this . config . subprovider ) ) ;
177+ }
178+ if ( this . config . model ) {
179+ args . push ( '--model' , this . config . model ) ;
180+ }
181+ if ( this . config . apiKey && this . config . subprovider ?. toLowerCase ( ) !== 'azure' ) {
182+ args . push ( '--api-key' , this . config . apiKey ) ;
183+ }
184+ if ( this . config . systemPrompt ) {
185+ args . push ( '--system-prompt' , this . config . systemPrompt ) ;
186+ }
187+
188+ if ( ! hasModeFlag ( this . config . command ) ) {
189+ args . push ( '--mode' , 'rpc' ) ;
190+ }
191+ args . push ( '--no-session' ) ;
192+
193+ if ( this . config . tools ) {
194+ args . push ( '--tools' , this . config . tools ) ;
195+ }
196+ if ( this . config . thinking ) {
197+ args . push ( '--thinking' , this . config . thinking ) ;
198+ }
199+
200+ return splitPiCommand ( this . config . command , args ) ;
201+ }
202+
203+ private buildEnv ( ) : NodeJS . ProcessEnv {
204+ const env = buildPiRuntimeEnv ( {
205+ runtime : this . config . runtime ,
206+ targetName : this . targetName ,
207+ } ) ;
208+
209+ const provider = this . config . subprovider ?. toLowerCase ( ) ?? 'google' ;
210+ if ( provider === 'azure' ) {
211+ if ( this . config . apiKey ) {
212+ env . AZURE_OPENAI_API_KEY = this . config . apiKey ;
213+ }
214+ if ( this . config . baseUrl ) {
215+ if ( / ^ h t t p s ? : \/ \/ / . test ( this . config . baseUrl ) ) {
216+ env . AZURE_OPENAI_BASE_URL = this . config . baseUrl ;
217+ } else {
218+ env . AZURE_OPENAI_RESOURCE_NAME = extractAzureResourceName ( this . config . baseUrl ) ;
219+ }
220+ }
221+ } else if ( this . config . apiKey ) {
222+ const envKey = resolveEnvKeyName ( provider ) ;
223+ if ( envKey ) {
224+ env [ envKey ] = this . config . apiKey ;
225+ }
226+ }
227+
228+ if ( this . config . subprovider ) {
229+ const resolvedProvider = resolveCliProvider ( this . config . subprovider ) ;
230+ const providerOwnPrefixes : Record < string , readonly string [ ] > = {
231+ openrouter : [ 'OPENROUTER_' ] ,
232+ anthropic : [ 'ANTHROPIC_' ] ,
233+ openai : [ 'OPENAI_' ] ,
234+ 'azure-openai-responses' : [ 'AZURE_OPENAI_' ] ,
235+ google : [ 'GEMINI_' , 'GOOGLE_GENERATIVE_AI_' ] ,
236+ gemini : [ 'GEMINI_' , 'GOOGLE_GENERATIVE_AI_' ] ,
237+ groq : [ 'GROQ_' ] ,
238+ xai : [ 'XAI_' ] ,
239+ } ;
240+ const ownPrefixes = providerOwnPrefixes [ resolvedProvider ] ?? [ ] ;
241+ const allOtherPrefixes = Object . entries ( providerOwnPrefixes )
242+ . filter ( ( [ key ] ) => key !== resolvedProvider )
243+ . flatMap ( ( [ , prefixes ] ) => prefixes ) ;
244+ for ( const key of Object . keys ( env ) ) {
245+ if (
246+ allOtherPrefixes . some ( ( prefix ) => key . startsWith ( prefix ) ) &&
247+ ! ownPrefixes . some ( ( prefix ) => key . startsWith ( prefix ) )
248+ ) {
249+ delete env [ key ] ;
250+ }
251+ }
252+ }
253+
254+ return env ;
255+ }
256+
153257 private buildProcessErrorResponse ( params : {
154258 readonly result : PiProcessRunResult ;
155259 readonly command : readonly string [ ] ;
@@ -253,10 +357,10 @@ export class PiRpcProvider implements Provider {
253357}
254358
255359type RpcRequest = {
256- readonly jsonrpc : '2.0' ;
257360 readonly id : string ;
258- readonly method : 'run' ;
259- readonly params : Record < string , unknown > ;
361+ readonly type : 'prompt' ;
362+ readonly message : string ;
363+ readonly streamingBehavior ?: 'followUp' ;
260364} ;
261365
262366type ParsedRpcOutput = {
@@ -270,37 +374,18 @@ function buildRpcRequest(params: {
270374 readonly config : PiRpcResolvedConfig ;
271375 readonly inputFiles ?: readonly string [ ] ;
272376} ) : RpcRequest {
273- const rpcParams : Record < string , unknown > = {
274- prompt : params . request . question ,
275- system_prompt : params . config . systemPrompt ?? params . request . systemPrompt ,
276- model : params . config . model ,
277- subprovider : params . config . subprovider ,
278- tools : params . config . tools ,
279- thinking : params . config . thinking ,
280- input_files : params . inputFiles ,
281- metadata : params . request . metadata ,
282- } ;
283- for ( const key of Object . keys ( rpcParams ) ) {
284- if ( rpcParams [ key ] === undefined ) {
285- delete rpcParams [ key ] ;
286- }
287- }
377+ const prefix = params . request . systemPrompt ? `${ params . request . systemPrompt } \n\n` : '' ;
378+ const suffix =
379+ params . inputFiles && params . inputFiles . length > 0
380+ ? `\n\nInput files:\n${ params . inputFiles . map ( ( file ) => `@${ file } ` ) . join ( '\n' ) } `
381+ : '' ;
288382 return {
289- jsonrpc : '2.0' ,
290383 id : randomUUID ( ) ,
291- method : 'run ' ,
292- params : rpcParams ,
384+ type : 'prompt ' ,
385+ message : ` ${ prefix } ${ params . request . question } ${ suffix } ` ,
293386 } ;
294387}
295388
296- function ensureRpcMode ( command : readonly string [ ] ) : readonly string [ ] {
297- const hasMode = command . some (
298- ( arg , index ) =>
299- arg === '--mode' || arg . startsWith ( '--mode=' ) || command [ index - 1 ] === '--mode' ,
300- ) ;
301- return hasMode ? command : [ ...command , '--mode' , 'rpc' ] ;
302- }
303-
304389function parseRpcOutput ( stdout : string , requestId : string ) : ParsedRpcOutput {
305390 const lines = stdout
306391 . split ( / \r ? \n / )
@@ -326,6 +411,17 @@ function parseRpcOutput(stdout: string, requestId: string): ParsedRpcOutput {
326411 throw new Error ( `invalid JSON protocol message: ${ formatError ( parseError ) } ` ) ;
327412 }
328413
414+ if ( message . id === requestId && message . type === 'response' ) {
415+ if ( message . success === false ) {
416+ error = rpcErrorMessage ( message . error ?? message . message ) ;
417+ }
418+ continue ;
419+ }
420+ if ( message . type === 'agent_end' ) {
421+ result = message ;
422+ events . push ( message ) ;
423+ continue ;
424+ }
329425 if ( message . id === requestId && Object . prototype . hasOwnProperty . call ( message , 'result' ) ) {
330426 result = message . result ;
331427 continue ;
@@ -354,6 +450,32 @@ function parseRpcOutput(stdout: string, requestId: string): ParsedRpcOutput {
354450 return { result, events } ;
355451}
356452
453+ function hasRpcAgentEnd ( stdout : string , requestId : string ) : boolean {
454+ for ( const line of stdout . split ( '\n' ) ) {
455+ const trimmed = line . trim ( ) ;
456+ if ( ! trimmed ) continue ;
457+ try {
458+ const parsed = JSON . parse ( trimmed ) as Record < string , unknown > ;
459+ if ( parsed . type === 'agent_end' ) {
460+ return true ;
461+ }
462+ if ( parsed . id === requestId && parsed . type === 'response' && parsed . success === false ) {
463+ return true ;
464+ }
465+ } catch {
466+ // Keep waiting; parseRpcOutput will report malformed lines after exit.
467+ }
468+ }
469+ return false ;
470+ }
471+
472+ function hasModeFlag ( command : readonly string [ ] ) : boolean {
473+ return command . some (
474+ ( arg , index ) =>
475+ arg === '--mode' || arg . startsWith ( '--mode=' ) || command [ index - 1 ] === '--mode' ,
476+ ) ;
477+ }
478+
357479function messagesFromRpcResult ( result : unknown ) : readonly Message [ ] {
358480 if ( result && typeof result === 'object' ) {
359481 const record = result as Record < string , unknown > ;
@@ -374,6 +496,30 @@ function messagesFromRpcResult(result: unknown): readonly Message[] {
374496 return [ { role : 'assistant' , content : JSON . stringify ( result ) } ] ;
375497}
376498
499+ function rpcResultError ( result : unknown ) : string | undefined {
500+ if ( ! result || typeof result !== 'object' ) {
501+ return undefined ;
502+ }
503+ const record = result as Record < string , unknown > ;
504+ if ( ! Array . isArray ( record . messages ) ) {
505+ return undefined ;
506+ }
507+ const errored = [ ...record . messages ] . reverse ( ) . find ( ( message ) => {
508+ if ( ! message || typeof message !== 'object' ) return false ;
509+ const msg = message as Record < string , unknown > ;
510+ return msg . role === 'assistant' && ( msg . stopReason === 'error' || msg . stop_reason === 'error' ) ;
511+ } ) ;
512+ if ( ! errored || typeof errored !== 'object' ) {
513+ return undefined ;
514+ }
515+ const msg = errored as Record < string , unknown > ;
516+ return (
517+ stringField ( msg , 'errorMessage' ) ??
518+ stringField ( msg , 'error_message' ) ??
519+ 'Pi RPC assistant message ended with stopReason=error'
520+ ) ;
521+ }
522+
377523function convertRpcMessage ( value : unknown ) : Message | undefined {
378524 if ( ! value || typeof value !== 'object' || Array . isArray ( value ) ) {
379525 return undefined ;
@@ -403,7 +549,7 @@ function tokenUsageFromRpcResult(result: unknown): ProviderTokenUsage | undefine
403549 | Record < string , unknown >
404550 | undefined ;
405551 if ( ! usage || typeof usage !== 'object' ) {
406- return undefined ;
552+ return tokenUsageFromMessages ( record . messages ) ;
407553 }
408554 const input = toFiniteNumber ( usage . input ?? usage . input_tokens ?? usage . inputTokens ) ;
409555 const output = toFiniteNumber ( usage . output ?? usage . output_tokens ?? usage . outputTokens ) ;
@@ -420,6 +566,49 @@ function tokenUsageFromRpcResult(result: unknown): ProviderTokenUsage | undefine
420566 } ;
421567}
422568
569+ function tokenUsageFromMessages ( messages : unknown ) : ProviderTokenUsage | undefined {
570+ if ( ! Array . isArray ( messages ) ) {
571+ return undefined ;
572+ }
573+
574+ let totalInput = 0 ;
575+ let totalOutput = 0 ;
576+ let totalCached = 0 ;
577+ let totalReasoning = 0 ;
578+ let found = false ;
579+
580+ for ( const message of messages ) {
581+ if ( ! message || typeof message !== 'object' ) {
582+ continue ;
583+ }
584+ const usage = ( message as Record < string , unknown > ) . usage ;
585+ if ( ! usage || typeof usage !== 'object' ) {
586+ continue ;
587+ }
588+ found = true ;
589+ const record = usage as Record < string , unknown > ;
590+ totalInput += toFiniteNumber ( record . input ) ?? 0 ;
591+ totalOutput += toFiniteNumber ( record . output ) ?? 0 ;
592+ totalCached +=
593+ toFiniteNumber ( record . cacheRead ) ??
594+ toFiniteNumber ( record . cache_read ) ??
595+ toFiniteNumber ( record . cached ) ??
596+ 0 ;
597+ totalReasoning +=
598+ toFiniteNumber ( record . reasoning ) ?? toFiniteNumber ( record . reasoning_tokens ) ?? 0 ;
599+ }
600+
601+ if ( ! found ) {
602+ return undefined ;
603+ }
604+ return {
605+ input : totalInput ,
606+ output : totalOutput ,
607+ ...( totalCached > 0 ? { cached : totalCached } : { } ) ,
608+ ...( totalReasoning > 0 ? { reasoning : totalReasoning } : { } ) ,
609+ } ;
610+ }
611+
423612function stringField ( record : Record < string , unknown > , key : string ) : string | undefined {
424613 const value = record [ key ] ;
425614 return typeof value === 'string' ? value : undefined ;
@@ -443,6 +632,7 @@ function formatError(error: unknown): string {
443632}
444633
445634export const _internal = {
446- ensureRpcMode,
635+ hasModeFlag,
636+ hasRpcAgentEnd,
447637 parseRpcOutput,
448638} ;
0 commit comments