Skip to content

Commit ad540fe

Browse files
author
linyuan.yang
committed
acp
1 parent 9350fe9 commit ad540fe

3 files changed

Lines changed: 91 additions & 80 deletions

File tree

packages/scorpio.ai/src/Agents/ACP/ACPAgentServiceBase.ts

Lines changed: 87 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ export const T_ACPArgs = Symbol("scorpio:T_ACPArgs");
1616
export const T_ACPEnv = Symbol("scorpio:T_ACPEnv");
1717
export 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+
1926
export 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 {

packages/scorpio.ai/src/Agents/ACP/PersistentACPAgentService.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,9 @@ export class PersistentACPAgentService extends ACPAgentServiceBase {
3434
}
3535

3636
protected override async preparePrompt(query: MessageContent): Promise<schema.ContentBlock[]> {
37-
const isFirst = this.sessionFirstPrompt;
37+
const includeHistory = this.sessionFirstPrompt;
3838
this.sessionFirstPrompt = false;
39-
40-
const blocks: schema.ContentBlock[] = [];
41-
if (isFirst) {
42-
const history = await this.saverService.getMessages();
43-
if (history.length > 0) blocks.push({ type: "text", text: this.formatHistory(history) });
44-
}
45-
blocks.push(...this.toContentBlocks(query));
46-
return blocks;
39+
return this.buildPrompt(query, includeHistory);
4740
}
4841

4942
protected override onProcessExit(_code: number | null): void {

packages/scorpio.ai/src/Agents/ACP/TransientACPAgentService.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,10 @@ export class TransientACPAgentService extends ACPAgentServiceBase {
2020
}
2121

2222
protected override async preparePrompt(query: MessageContent): Promise<schema.ContentBlock[]> {
23-
const blocks: schema.ContentBlock[] = [];
24-
const history = await this.saverService.getMessages();
25-
if (history.length > 0) blocks.push({ type: "text", text: this.formatHistory(history) });
26-
blocks.push(...this.toContentBlocks(query));
27-
return blocks;
23+
return this.buildPrompt(query, true);
2824
}
2925

3026
protected override async onStreamFinally(): Promise<void> {
31-
if (this.sessionId) {
32-
await this.connection!.closeSession({ sessionId: this.sessionId }).catch(e => {
33-
this.logger?.debug(`[ACP] closeSession ignored: ${e?.message ?? e}`);
34-
});
35-
this.sessionId = null;
36-
}
27+
await this.closeCurrentSession();
3728
}
3829
}

0 commit comments

Comments
 (0)