@@ -16,6 +16,13 @@ export const T_ACPArgs = Symbol("scorpio:T_ACPArgs");
1616export const T_ACPEnv = Symbol ( "scorpio:T_ACPEnv" ) ;
1717export const T_ACPWorkPath = Symbol ( "scorpio:T_ACPWorkPath" ) ;
1818
19+ interface ACPStreamState {
20+ callback : IAgentCallback ;
21+ text : string ;
22+ thinkId : string ;
23+ usageReported : boolean ;
24+ }
25+
1926export abstract class ACPAgentServiceBase extends AgentServiceBase {
2027 protected command : string ;
2128 protected args : string [ ] ;
@@ -27,10 +34,7 @@ export abstract class ACPAgentServiceBase extends AgentServiceBase {
2734 protected sessionId : string | null = null ;
2835 protected initialized = false ;
2936
30- private _callback : IAgentCallback = { } ;
31- private _text = "" ;
32- private _thinkId = "" ;
33- private _usageReported = false ;
37+ private readonly activeStreams = new Map < string , ACPStreamState > ( ) ;
3438
3539 constructor (
3640 command : string ,
@@ -60,26 +64,30 @@ export abstract class ACPAgentServiceBase extends AgentServiceBase {
6064 const prompt = await this . preparePrompt ( query ) ;
6165 await this . saverService . pushMessage ( { role : MessageRole . Human , content : query } ) ;
6266
63- this . _callback = callback ;
64- this . _text = "" ;
65- this . _thinkId = uuidv4 ( ) ;
66- this . _usageReported = false ;
67+ const sessionId = this . sessionId ! ;
68+ const state : ACPStreamState = {
69+ callback,
70+ text : "" ,
71+ thinkId : uuidv4 ( ) ,
72+ usageReported : false ,
73+ } ;
74+ this . activeStreams . set ( sessionId , state ) ;
6775
6876 const onCancel = ( ) => {
69- if ( this . sessionId && this . connection ) this . connection . cancel ( { sessionId : this . sessionId } ) ;
77+ if ( this . connection ) this . connection . cancel ( { sessionId } ) ;
7078 } ;
7179 signal ?. addEventListener ( "abort" , onCancel , { once : true } ) ;
7280
7381 try {
74- const response = await this . connection ! . prompt ( { sessionId : this . sessionId ! , prompt } ) ;
75- return await this . collectResponse ( response , callback ) ;
82+ const response = await this . connection ! . prompt ( { sessionId, prompt } ) ;
83+ return await this . collectResponse ( response , state ) ;
7684 } catch ( e ) {
7785 this . onStreamError ( e ) ;
78- await this . recordException ( e , this . _thinkId ? { thinkId : this . _thinkId } : undefined ) ;
86+ await this . recordException ( e , { thinkId : state . thinkId } ) ;
7987 throw e ;
8088 } finally {
8189 signal ?. removeEventListener ( "abort" , onCancel ) ;
82- this . _callback = { } ;
90+ this . activeStreams . delete ( sessionId ) ;
8391 await this . onStreamFinally ( ) ;
8492 }
8593 }
@@ -90,12 +98,7 @@ export abstract class ACPAgentServiceBase extends AgentServiceBase {
9098 // ── dispose ──────────────────────────────────────────────────────────
9199
92100 async forceDispose ( ) {
93- if ( this . sessionId && this . connection ) {
94- await this . connection . closeSession ( { sessionId : this . sessionId } ) . catch ( e => {
95- this . logger ?. debug ( `[ACP] closeSession ignored: ${ e ?. message ?? e } ` ) ;
96- } ) ;
97- this . sessionId = null ;
98- }
101+ await this . closeCurrentSession ( ) ;
99102 if ( this . childProcess ) {
100103 this . childProcess . kill ( ) ;
101104 this . childProcess = null ;
@@ -170,32 +173,49 @@ export abstract class ACPAgentServiceBase extends AgentServiceBase {
170173
171174 protected abstract preparePrompt ( query : MessageContent ) : Promise < schema . ContentBlock [ ] > ;
172175
173- private async collectResponse ( response : { usage ?: schema . Usage | null } , callback : IAgentCallback ) : Promise < ChatMessage [ ] > {
176+ protected async buildPrompt ( query : MessageContent , includeHistory : boolean ) : Promise < schema . ContentBlock [ ] > {
177+ const blocks : schema . ContentBlock [ ] = [ ] ;
178+ if ( includeHistory ) {
179+ const history = await this . saverService . getMessages ( ) ;
180+ if ( history . length > 0 ) blocks . push ( { type : "text" , text : this . formatHistory ( history ) } ) ;
181+ }
182+ blocks . push ( ...this . toContentBlocks ( query ) ) ;
183+ return blocks ;
184+ }
185+
186+ protected async closeCurrentSession ( ) : Promise < void > {
187+ const sessionId = this . sessionId ;
188+ if ( ! sessionId || ! this . connection ) return ;
189+
190+ await this . connection . closeSession ( { sessionId } ) . catch ( e => {
191+ this . logger ?. debug ( `[ACP] closeSession ignored: ${ e ?. message ?? e } ` ) ;
192+ } ) ;
193+ if ( this . sessionId === sessionId ) this . sessionId = null ;
194+ }
195+
196+ private async collectResponse ( response : { usage ?: schema . Usage | null } , state : ACPStreamState ) : Promise < ChatMessage [ ] > {
174197 const messages : ChatMessage [ ] = [ ] ;
175- if ( this . _text . trim ( ) ) {
198+ if ( state . text . trim ( ) ) {
176199 const msg : ChatMessage = {
177200 role : MessageRole . AI ,
178- content : this . _text . trim ( ) ,
179- additional_kwargs : { thinkId : this . _thinkId } ,
201+ content : state . text . trim ( ) ,
202+ additional_kwargs : { thinkId : state . thinkId } ,
180203 } ;
181204 messages . push ( msg ) ;
182- await callback . onMessage ?.( msg ) ;
183- await this . saverService . pushMessage ( msg , { thinkId : this . _thinkId } ) ;
205+ await state . callback . onMessage ?.( msg ) ;
206+ await this . saverService . pushMessage ( msg , { thinkId : state . thinkId } ) ;
184207 }
185- if ( response . usage && ! this . _usageReported ) {
186- await callback . onUsage ?.( {
187- input_tokens : response . usage . inputTokens ?? 0 ,
188- output_tokens : response . usage . outputTokens ?? 0 ,
189- total_tokens : ( response . usage . inputTokens ?? 0 ) + ( response . usage . outputTokens ?? 0 ) ,
190- } ) ;
208+ if ( response . usage && ! state . usageReported ) {
209+ await state . callback . onUsage ?.( this . toTokenUsage ( response . usage ) ) ;
191210 }
192211 return messages ;
193212 }
194213
195214 // ── ACP session handlers ─────────────────────────────────────────────
196215
197216 private async handlePermission ( params : schema . RequestPermissionRequest ) {
198- if ( ! this . _callback . executeTool ) {
217+ const callback = this . activeStreams . get ( params . sessionId ) ?. callback ;
218+ if ( ! callback ?. executeTool ) {
199219 const opt = params . options . find ( o => o . kind === "allow_once" ) ?? params . options [ 0 ] ;
200220 return { outcome : { outcome : "selected" as const , optionId : opt . optionId } } ;
201221 }
@@ -205,7 +225,7 @@ export abstract class ACPAgentServiceBase extends AgentServiceBase {
205225 name : params . toolCall . title ?? "unknown" ,
206226 args : ( params . toolCall . rawInput as Record < string , any > ) ?? { } ,
207227 } ;
208- const approval = await this . _callback . executeTool ( toolCall ) ;
228+ const approval = await callback . executeTool ( toolCall ) ;
209229
210230 if ( approval === ToolApproval . Deny ) {
211231 const reject = params . options . find ( o => o . kind === "reject_once" ) ;
@@ -222,50 +242,47 @@ export abstract class ACPAgentServiceBase extends AgentServiceBase {
222242 }
223243
224244 private async handleSessionUpdate ( params : schema . SessionNotification ) {
245+ const state = this . activeStreams . get ( params . sessionId ) ;
246+ if ( ! state ) return ;
247+
225248 const update = params . update ;
226249 switch ( update . sessionUpdate ) {
227250 case "agent_message_chunk" : {
228251 if ( update . content . type === "text" ) {
229- this . _text += ( update . content as schema . TextContent ) . text ?? "" ;
230- await this . _callback . onStreamMessage ?.( { role : MessageRole . AI , content : this . _text } ) ;
252+ state . text += ( update . content as schema . TextContent ) . text ?? "" ;
253+ await state . callback . onStreamMessage ?.( { role : MessageRole . AI , content : state . text } ) ;
231254 }
232255 break ;
233256 }
234257 case "tool_call" : {
235- const tc = update as schema . ToolCall & { sessionUpdate : string } ;
236- await this . flushThinkText ( ) ;
237- await this . saverService . pushThinkMessage ( this . _thinkId , {
258+ await this . flushThinkText ( state ) ;
259+ await this . saverService . pushThinkMessage ( state . thinkId , {
238260 role : MessageRole . AI ,
239- content : `[Tool: ${ tc . title } ]` ,
240- tool_calls : [ { id : tc . toolCallId , name : tc . title , args : ( tc . rawInput as Record < string , any > ) ?? { } } ] ,
261+ content : `[Tool: ${ update . title } ]` ,
262+ tool_calls : [ { id : update . toolCallId , name : update . title , args : ( update . rawInput as Record < string , any > ) ?? { } } ] ,
241263 } ) ;
242- await this . _callback . onStreamMessage ?.( { role : MessageRole . AI , content : `[Tool: ${ tc . title } ]` } ) ;
264+ await state . callback . onStreamMessage ?.( { role : MessageRole . AI , content : `[Tool: ${ update . title } ]` } ) ;
243265 break ;
244266 }
245267 case "tool_call_update" : {
246- const tu = update as schema . ToolCallUpdate & { sessionUpdate : string } ;
247- if ( tu . status === "completed" || tu . status === "failed" ) {
248- const raw = tu . rawOutput ;
268+ if ( update . status === "completed" || update . status === "failed" ) {
269+ const raw = update . rawOutput ;
249270 const output = raw != null ? ( typeof raw === "string" ? raw : JSON . stringify ( raw ) ) : "" ;
250- await this . saverService . pushThinkMessage ( this . _thinkId , {
271+ await this . saverService . pushThinkMessage ( state . thinkId , {
251272 role : MessageRole . Tool ,
252- tool_call_id : tu . toolCallId ,
273+ tool_call_id : update . toolCallId ,
253274 content : output ,
254- status : tu . status === "failed" ? "error" : "success" ,
275+ status : update . status === "failed" ? "error" : "success" ,
255276 } ) ;
256- await this . _callback . onStreamMessage ?.( { role : MessageRole . Tool , content : output , tool_call_id : tu . toolCallId } ) ;
277+ await state . callback . onStreamMessage ?.( { role : MessageRole . Tool , content : output , tool_call_id : update . toolCallId } ) ;
257278 }
258279 break ;
259280 }
260281 case "usage_update" : {
261- const u = update as any ;
262- if ( u . usage ) {
263- this . _usageReported = true ;
264- await this . _callback . onUsage ?.( {
265- input_tokens : u . usage . inputTokens ?? 0 ,
266- output_tokens : u . usage . outputTokens ?? 0 ,
267- total_tokens : ( u . usage . inputTokens ?? 0 ) + ( u . usage . outputTokens ?? 0 ) ,
268- } ) ;
282+ const usage = ( update as { usage ?: schema . Usage | null } ) . usage ;
283+ if ( usage ) {
284+ state . usageReported = true ;
285+ await state . callback . onUsage ?.( this . toTokenUsage ( usage ) ) ;
269286 }
270287 break ;
271288 }
@@ -274,11 +291,21 @@ export abstract class ACPAgentServiceBase extends AgentServiceBase {
274291
275292 // ── utilities ────────────────────────────────────────────────────────
276293
277- private async flushThinkText ( ) : Promise < void > {
278- const pending = this . _text . trim ( ) ;
294+ private async flushThinkText ( state : ACPStreamState ) : Promise < void > {
295+ const pending = state . text . trim ( ) ;
279296 if ( ! pending ) return ;
280- await this . saverService . pushThinkMessage ( this . _thinkId , { role : MessageRole . AI , content : pending } ) ;
281- this . _text = "" ;
297+ await this . saverService . pushThinkMessage ( state . thinkId , { role : MessageRole . AI , content : pending } ) ;
298+ state . text = "" ;
299+ }
300+
301+ private toTokenUsage ( usage : schema . Usage ) {
302+ const input_tokens = usage . inputTokens ?? 0 ;
303+ const output_tokens = usage . outputTokens ?? 0 ;
304+ return {
305+ input_tokens,
306+ output_tokens,
307+ total_tokens : input_tokens + output_tokens ,
308+ } ;
282309 }
283310
284311 protected formatHistory ( messages : ChatMessage [ ] ) : string {
0 commit comments