mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-11 23:10:29 +00:00
acp: enrich streaming updates for ide clients (#41442)
Merged via squash.
Prepared head SHA: 0764368e80
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/node pending drain followup: keep `hasMore` true when the deferred baseline status item still needs delivery, and avoid allocating empty pending-work state for drain-only nodes with no queued work. (#41429) Thanks @mbelinky.
|
||||
- ACP/bridge mode: reject unsupported per-session MCP server setup and propagate rejected session-mode changes so IDE clients see explicit bridge limitations instead of silent success. (#41424) Thanks @mbelinky.
|
||||
- ACP/session UX: replay stored user and assistant text on `loadSession`, expose Gateway-backed session controls and metadata, and emit approximate session usage updates so IDE clients restore context more faithfully. (#41425) Thanks @mbelinky.
|
||||
- ACP/tool streaming: enrich `tool_call` and `tool_call_update` events with best-effort text content and file-location hints so IDE clients can follow bridge tool activity more naturally. (#41442) Thanks @mbelinky.
|
||||
|
||||
## 2026.3.8
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ session with predictable session mapping and basic streaming updates.
|
||||
| Prompt content (`text`, embedded `resource`, images) | Partial | Text/resources are flattened into chat input; images become Gateway attachments. |
|
||||
| Session modes | Partial | `session/set_mode` is supported and the bridge exposes initial Gateway-backed session controls for thought level, tool verbosity, reasoning, usage detail, and elevated actions. Broader ACP-native mode/config surfaces are still out of scope. |
|
||||
| Session info and usage updates | Partial | The bridge emits `session_info_update` and best-effort `usage_update` notifications from cached Gateway session snapshots. Usage is approximate and only sent when Gateway token totals are marked fresh. |
|
||||
| Tool streaming | Partial | Tool start and result updates are forwarded, but without richer editor metadata such as file locations or structured diff-native output. |
|
||||
| Tool streaming | Partial | `tool_call` / `tool_call_update` events include raw I/O, text content, and best-effort file locations when Gateway tool args/results expose them. Embedded terminals and richer diff-native output are still not exposed. |
|
||||
| Per-session MCP servers (`mcpServers`) | Unsupported | Bridge mode rejects per-session MCP server requests. Configure MCP on the OpenClaw gateway or agent instead. |
|
||||
| Client filesystem methods (`fs/read_text_file`, `fs/write_text_file`) | Unsupported | The bridge does not call ACP client filesystem methods. |
|
||||
| Client terminal methods (`terminal/*`) | Unsupported | The bridge does not create ACP client terminals or stream terminal ids through tool calls. |
|
||||
@@ -58,8 +58,9 @@ session with predictable session mapping and basic streaming updates.
|
||||
snapshots, not live ACP-native runtime accounting. Usage is approximate,
|
||||
carries no cost data, and is only emitted when the Gateway marks total token
|
||||
data as fresh.
|
||||
- Tool follow-along data is still intentionally narrow in bridge mode. The
|
||||
bridge does not yet emit ACP terminals, file locations, or structured diffs.
|
||||
- Tool follow-along data is best-effort. The bridge can surface file paths that
|
||||
appear in known tool args/results, but it does not yet emit ACP terminals or
|
||||
structured file diffs.
|
||||
|
||||
## How can I use this
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ updates.
|
||||
| Prompt content (`text`, embedded `resource`, images) | Partial | Text/resources are flattened into chat input; images become Gateway attachments. |
|
||||
| Session modes | Partial | `session/set_mode` is supported and the bridge exposes initial Gateway-backed session controls for thought level, tool verbosity, reasoning, usage detail, and elevated actions. Broader ACP-native mode/config surfaces are still out of scope. |
|
||||
| Session info and usage updates | Partial | The bridge emits `session_info_update` and best-effort `usage_update` notifications from cached Gateway session snapshots. Usage is approximate and only sent when Gateway token totals are marked fresh. |
|
||||
| Tool streaming | Partial | Tool start and result updates are forwarded, but without richer editor metadata such as file locations or structured diff-native output. |
|
||||
| Tool streaming | Partial | `tool_call` / `tool_call_update` events include raw I/O, text content, and best-effort file locations when Gateway tool args/results expose them. Embedded terminals and richer diff-native output are still not exposed. |
|
||||
| Per-session MCP servers (`mcpServers`) | Unsupported | Bridge mode rejects per-session MCP server requests. Configure MCP on the OpenClaw gateway or agent instead. |
|
||||
| Client filesystem methods (`fs/read_text_file`, `fs/write_text_file`) | Unsupported | The bridge does not call ACP client filesystem methods. |
|
||||
| Client terminal methods (`terminal/*`) | Unsupported | The bridge does not create ACP client terminals or stream terminal ids through tool calls. |
|
||||
@@ -52,8 +52,9 @@ updates.
|
||||
snapshots, not live ACP-native runtime accounting. Usage is approximate,
|
||||
carries no cost data, and is only emitted when the Gateway marks total token
|
||||
data as fresh.
|
||||
- Tool follow-along data is still intentionally narrow in bridge mode. The
|
||||
bridge does not yet emit ACP terminals, file locations, or structured diffs.
|
||||
- Tool follow-along data is best-effort. The bridge can surface file paths that
|
||||
appear in known tool args/results, but it does not yet emit ACP terminals or
|
||||
structured file diffs.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { ContentBlock, ImageContent, ToolKind } from "@agentclientprotocol/sdk";
|
||||
import type {
|
||||
ContentBlock,
|
||||
ImageContent,
|
||||
ToolCallContent,
|
||||
ToolCallLocation,
|
||||
ToolKind,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
|
||||
export type GatewayAttachment = {
|
||||
type: string;
|
||||
@@ -6,6 +12,39 @@ export type GatewayAttachment = {
|
||||
content: string;
|
||||
};
|
||||
|
||||
const TOOL_LOCATION_PATH_KEYS = [
|
||||
"path",
|
||||
"filePath",
|
||||
"file_path",
|
||||
"targetPath",
|
||||
"target_path",
|
||||
"targetFile",
|
||||
"target_file",
|
||||
"sourcePath",
|
||||
"source_path",
|
||||
"destinationPath",
|
||||
"destination_path",
|
||||
"oldPath",
|
||||
"old_path",
|
||||
"newPath",
|
||||
"new_path",
|
||||
"outputPath",
|
||||
"output_path",
|
||||
"inputPath",
|
||||
"input_path",
|
||||
] as const;
|
||||
|
||||
const TOOL_LOCATION_LINE_KEYS = [
|
||||
"line",
|
||||
"lineNumber",
|
||||
"line_number",
|
||||
"startLine",
|
||||
"start_line",
|
||||
] as const;
|
||||
const TOOL_RESULT_PATH_MARKER_RE = /^(?:FILE|MEDIA):(.+)$/gm;
|
||||
const TOOL_LOCATION_MAX_DEPTH = 4;
|
||||
const TOOL_LOCATION_MAX_NODES = 100;
|
||||
|
||||
const INLINE_CONTROL_ESCAPE_MAP: Readonly<Record<string, string>> = {
|
||||
"\0": "\\0",
|
||||
"\r": "\\r",
|
||||
@@ -56,6 +95,150 @@ function escapeResourceTitle(value: string): string {
|
||||
return escapeInlineControlChars(value).replace(/[()[\]]/g, (char) => `\\${char}`);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function normalizeToolLocationPath(value: string): string | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (
|
||||
!trimmed ||
|
||||
trimmed.length > 4096 ||
|
||||
trimmed.includes("\u0000") ||
|
||||
trimmed.includes("\r") ||
|
||||
trimmed.includes("\n")
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
return undefined;
|
||||
}
|
||||
if (/^file:\/\//i.test(trimmed)) {
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
return decodeURIComponent(parsed.pathname || "") || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function normalizeToolLocationLine(value: unknown): number | undefined {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const line = Math.floor(value);
|
||||
return line > 0 ? line : undefined;
|
||||
}
|
||||
|
||||
function extractToolLocationLine(record: Record<string, unknown>): number | undefined {
|
||||
for (const key of TOOL_LOCATION_LINE_KEYS) {
|
||||
const line = normalizeToolLocationLine(record[key]);
|
||||
if (line !== undefined) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function addToolLocation(
|
||||
locations: Map<string, ToolCallLocation>,
|
||||
rawPath: string,
|
||||
line?: number,
|
||||
): void {
|
||||
const path = normalizeToolLocationPath(rawPath);
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
for (const [existingKey, existing] of locations.entries()) {
|
||||
if (existing.path !== path) {
|
||||
continue;
|
||||
}
|
||||
if (line === undefined || existing.line === line) {
|
||||
return;
|
||||
}
|
||||
if (existing.line === undefined) {
|
||||
locations.delete(existingKey);
|
||||
}
|
||||
}
|
||||
const locationKey = `${path}:${line ?? ""}`;
|
||||
if (locations.has(locationKey)) {
|
||||
return;
|
||||
}
|
||||
locations.set(locationKey, line ? { path, line } : { path });
|
||||
}
|
||||
|
||||
function collectLocationsFromTextMarkers(
|
||||
text: string,
|
||||
locations: Map<string, ToolCallLocation>,
|
||||
): void {
|
||||
for (const match of text.matchAll(TOOL_RESULT_PATH_MARKER_RE)) {
|
||||
const candidate = match[1]?.trim();
|
||||
if (candidate) {
|
||||
addToolLocation(locations, candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectToolLocations(
|
||||
value: unknown,
|
||||
locations: Map<string, ToolCallLocation>,
|
||||
state: { visited: number; depth: number },
|
||||
): void {
|
||||
if (state.visited >= TOOL_LOCATION_MAX_NODES || state.depth > TOOL_LOCATION_MAX_DEPTH) {
|
||||
return;
|
||||
}
|
||||
state.visited += 1;
|
||||
|
||||
if (typeof value === "string") {
|
||||
collectLocationsFromTextMarkers(value, locations);
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
collectToolLocations(item, locations, { visited: state.visited, depth: state.depth + 1 });
|
||||
state.visited += 1;
|
||||
if (state.visited >= TOOL_LOCATION_MAX_NODES) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
const line = extractToolLocationLine(record);
|
||||
for (const key of TOOL_LOCATION_PATH_KEYS) {
|
||||
const rawPath = record[key];
|
||||
if (typeof rawPath === "string") {
|
||||
addToolLocation(locations, rawPath, line);
|
||||
}
|
||||
}
|
||||
|
||||
const content = Array.isArray(record.content) ? record.content : undefined;
|
||||
if (content) {
|
||||
for (const block of content) {
|
||||
const entry = asRecord(block);
|
||||
if (entry?.type === "text" && typeof entry.text === "string") {
|
||||
collectLocationsFromTextMarkers(entry.text, locations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const nested of Object.values(record)) {
|
||||
collectToolLocations(nested, locations, { visited: state.visited, depth: state.depth + 1 });
|
||||
state.visited += 1;
|
||||
if (state.visited >= TOOL_LOCATION_MAX_NODES) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function extractTextFromPrompt(prompt: ContentBlock[], maxBytes?: number): string {
|
||||
const parts: string[] = [];
|
||||
// Track accumulated byte count per block to catch oversized prompts before full concatenation
|
||||
@@ -152,3 +335,74 @@ export function inferToolKind(name?: string): ToolKind {
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
|
||||
export function extractToolCallContent(value: unknown): ToolCallContent[] | undefined {
|
||||
if (typeof value === "string") {
|
||||
return value.trim()
|
||||
? [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: value,
|
||||
},
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const record = asRecord(value);
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const contents: ToolCallContent[] = [];
|
||||
const blocks = Array.isArray(record.content) ? record.content : [];
|
||||
for (const block of blocks) {
|
||||
const entry = asRecord(block);
|
||||
if (entry?.type === "text" && typeof entry.text === "string" && entry.text.trim()) {
|
||||
contents.push({
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: entry.text,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (contents.length > 0) {
|
||||
return contents;
|
||||
}
|
||||
|
||||
const fallbackText =
|
||||
typeof record.text === "string"
|
||||
? record.text
|
||||
: typeof record.message === "string"
|
||||
? record.message
|
||||
: typeof record.error === "string"
|
||||
? record.error
|
||||
: undefined;
|
||||
|
||||
if (!fallbackText?.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: fallbackText,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function extractToolCallLocations(...values: unknown[]): ToolCallLocation[] | undefined {
|
||||
const locations = new Map<string, ToolCallLocation>();
|
||||
for (const value of values) {
|
||||
collectToolLocations(value, locations, { visited: 0, depth: 0 });
|
||||
}
|
||||
return locations.size > 0 ? [...locations.values()] : undefined;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,34 @@ function createSetSessionConfigOptionRequest(
|
||||
} as unknown as SetSessionConfigOptionRequest;
|
||||
}
|
||||
|
||||
function createToolEvent(params: {
|
||||
sessionKey: string;
|
||||
phase: "start" | "update" | "result";
|
||||
toolCallId: string;
|
||||
name: string;
|
||||
args?: Record<string, unknown>;
|
||||
partialResult?: unknown;
|
||||
result?: unknown;
|
||||
isError?: boolean;
|
||||
}): EventFrame {
|
||||
return {
|
||||
event: "agent",
|
||||
payload: {
|
||||
sessionKey: params.sessionKey,
|
||||
stream: "tool",
|
||||
data: {
|
||||
phase: params.phase,
|
||||
toolCallId: params.toolCallId,
|
||||
name: params.name,
|
||||
args: params.args,
|
||||
partialResult: params.partialResult,
|
||||
result: params.result,
|
||||
isError: params.isError,
|
||||
},
|
||||
},
|
||||
} as unknown as EventFrame;
|
||||
}
|
||||
|
||||
function createChatFinalEvent(sessionKey: string): EventFrame {
|
||||
return {
|
||||
event: "chat",
|
||||
@@ -561,6 +589,117 @@ describe("acp setSessionConfigOption bridge behavior", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("acp tool streaming bridge behavior", () => {
|
||||
it("maps Gateway tool partial output and file locations into ACP tool updates", async () => {
|
||||
const sessionStore = createInMemorySessionStore();
|
||||
const connection = createAcpConnection();
|
||||
const sessionUpdate = connection.__sessionUpdateMock;
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "chat.send") {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
return { ok: true };
|
||||
}) as GatewayClient["request"];
|
||||
const agent = new AcpGatewayAgent(connection, createAcpGateway(request), {
|
||||
sessionStore,
|
||||
});
|
||||
|
||||
await agent.loadSession(createLoadSessionRequest("tool-session"));
|
||||
sessionUpdate.mockClear();
|
||||
|
||||
const promptPromise = agent.prompt(createPromptRequest("tool-session", "Inspect app.ts"));
|
||||
|
||||
await agent.handleGatewayEvent(
|
||||
createToolEvent({
|
||||
sessionKey: "tool-session",
|
||||
phase: "start",
|
||||
toolCallId: "tool-1",
|
||||
name: "read",
|
||||
args: { path: "src/app.ts", line: 12 },
|
||||
}),
|
||||
);
|
||||
await agent.handleGatewayEvent(
|
||||
createToolEvent({
|
||||
sessionKey: "tool-session",
|
||||
phase: "update",
|
||||
toolCallId: "tool-1",
|
||||
name: "read",
|
||||
partialResult: {
|
||||
content: [{ type: "text", text: "partial output" }],
|
||||
details: { path: "src/app.ts" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
await agent.handleGatewayEvent(
|
||||
createToolEvent({
|
||||
sessionKey: "tool-session",
|
||||
phase: "result",
|
||||
toolCallId: "tool-1",
|
||||
name: "read",
|
||||
result: {
|
||||
content: [{ type: "text", text: "FILE:src/app.ts" }],
|
||||
details: { path: "src/app.ts" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
await agent.handleGatewayEvent(createChatFinalEvent("tool-session"));
|
||||
await promptPromise;
|
||||
|
||||
expect(sessionUpdate).toHaveBeenCalledWith({
|
||||
sessionId: "tool-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call",
|
||||
toolCallId: "tool-1",
|
||||
title: "read: path: src/app.ts, line: 12",
|
||||
status: "in_progress",
|
||||
rawInput: { path: "src/app.ts", line: 12 },
|
||||
kind: "read",
|
||||
locations: [{ path: "src/app.ts", line: 12 }],
|
||||
},
|
||||
});
|
||||
expect(sessionUpdate).toHaveBeenCalledWith({
|
||||
sessionId: "tool-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "tool-1",
|
||||
status: "in_progress",
|
||||
rawOutput: {
|
||||
content: [{ type: "text", text: "partial output" }],
|
||||
details: { path: "src/app.ts" },
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: "partial output" },
|
||||
},
|
||||
],
|
||||
locations: [{ path: "src/app.ts", line: 12 }],
|
||||
},
|
||||
});
|
||||
expect(sessionUpdate).toHaveBeenCalledWith({
|
||||
sessionId: "tool-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "tool-1",
|
||||
status: "completed",
|
||||
rawOutput: {
|
||||
content: [{ type: "text", text: "FILE:src/app.ts" }],
|
||||
details: { path: "src/app.ts" },
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: { type: "text", text: "FILE:src/app.ts" },
|
||||
},
|
||||
],
|
||||
locations: [{ path: "src/app.ts", line: 12 }],
|
||||
},
|
||||
});
|
||||
|
||||
sessionStore.clearAllSessionsForTest();
|
||||
});
|
||||
});
|
||||
|
||||
describe("acp session metadata and usage updates", () => {
|
||||
it("emits a fresh usage snapshot after prompt completion when gateway totals are available", async () => {
|
||||
const sessionStore = createInMemorySessionStore();
|
||||
|
||||
@@ -23,6 +23,8 @@ import type {
|
||||
SetSessionModeRequest,
|
||||
SetSessionModeResponse,
|
||||
StopReason,
|
||||
ToolCallLocation,
|
||||
ToolKind,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
|
||||
import { listThinkingLevels } from "../auto-reply/thinking.js";
|
||||
@@ -37,8 +39,11 @@ import { shortenHomePath } from "../utils.js";
|
||||
import { getAvailableCommands } from "./commands.js";
|
||||
import {
|
||||
extractAttachmentsFromPrompt,
|
||||
extractToolCallContent,
|
||||
extractToolCallLocations,
|
||||
extractTextFromPrompt,
|
||||
formatToolTitle,
|
||||
inferToolKind,
|
||||
} from "./event-mapper.js";
|
||||
import { readBool, readNumber, readString } from "./meta.js";
|
||||
import { parseSessionMeta, resetSessionIfNeeded, resolveSessionKey } from "./session-mapper.js";
|
||||
@@ -62,7 +67,14 @@ type PendingPrompt = {
|
||||
reject: (err: Error) => void;
|
||||
sentTextLength?: number;
|
||||
sentText?: string;
|
||||
toolCalls?: Set<string>;
|
||||
toolCalls?: Map<string, PendingToolCall>;
|
||||
};
|
||||
|
||||
type PendingToolCall = {
|
||||
kind: ToolKind;
|
||||
locations?: ToolCallLocation[];
|
||||
rawInput?: Record<string, unknown>;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type AcpGatewayAgentOptions = AcpServerOptions & {
|
||||
@@ -681,21 +693,48 @@ export class AcpGatewayAgent implements Agent {
|
||||
|
||||
if (phase === "start") {
|
||||
if (!pending.toolCalls) {
|
||||
pending.toolCalls = new Set();
|
||||
pending.toolCalls = new Map();
|
||||
}
|
||||
if (pending.toolCalls.has(toolCallId)) {
|
||||
return;
|
||||
}
|
||||
pending.toolCalls.add(toolCallId);
|
||||
const args = data.args as Record<string, unknown> | undefined;
|
||||
const title = formatToolTitle(name, args);
|
||||
const kind = inferToolKind(name);
|
||||
const locations = extractToolCallLocations(args);
|
||||
pending.toolCalls.set(toolCallId, {
|
||||
title,
|
||||
kind,
|
||||
rawInput: args,
|
||||
locations,
|
||||
});
|
||||
await this.connection.sessionUpdate({
|
||||
sessionId: pending.sessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call",
|
||||
toolCallId,
|
||||
title: formatToolTitle(name, args),
|
||||
title,
|
||||
status: "in_progress",
|
||||
rawInput: args,
|
||||
kind,
|
||||
locations,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (phase === "update") {
|
||||
const toolState = pending.toolCalls?.get(toolCallId);
|
||||
const partialResult = data.partialResult;
|
||||
await this.connection.sessionUpdate({
|
||||
sessionId: pending.sessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId,
|
||||
status: "in_progress",
|
||||
rawOutput: partialResult,
|
||||
content: extractToolCallContent(partialResult),
|
||||
locations: extractToolCallLocations(toolState?.locations, partialResult),
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -703,6 +742,7 @@ export class AcpGatewayAgent implements Agent {
|
||||
|
||||
if (phase === "result") {
|
||||
const isError = Boolean(data.isError);
|
||||
const toolState = pending.toolCalls?.get(toolCallId);
|
||||
pending.toolCalls?.delete(toolCallId);
|
||||
await this.connection.sessionUpdate({
|
||||
sessionId: pending.sessionId,
|
||||
@@ -711,6 +751,8 @@ export class AcpGatewayAgent implements Agent {
|
||||
toolCallId,
|
||||
status: isError ? "failed" : "completed",
|
||||
rawOutput: data.result,
|
||||
content: extractToolCallContent(data.result),
|
||||
locations: extractToolCallLocations(toolState?.locations, data.result),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user