replace artifact heuristics/regexes with protocol messages (#8996)

This commit is contained in:
Jack Amadeo
2026-05-04 15:00:22 -04:00
committed by GitHub
parent 4a443feae8
commit ebe3315bdd
26 changed files with 484 additions and 2208 deletions
@@ -151,6 +151,44 @@ describe("acpNotificationHandler", () => {
).toBe("assistant-1");
});
it("preserves ACP tool kind and locations on tool requests", async () => {
registerSession("local-session", "goose-session", "goose", "/Users/test");
setActiveMessageId("goose-session", "assistant-1");
await handleSessionNotification({
sessionId: "goose-session",
update: {
sessionUpdate: "tool_call",
toolCallId: "tool-1",
title: "write_file",
kind: "edit",
locations: [{ path: "/tmp/report.md", line: 7 }],
rawInput: { path: "/tmp/report.md" },
},
} as never);
await handleSessionNotification({
sessionId: "goose-session",
update: {
sessionUpdate: "tool_call_update",
toolCallId: "tool-1",
status: "completed",
locations: [{ path: "/tmp/report.md", line: 9 }],
},
} as never);
const [message] =
useChatStore.getState().messagesBySession["local-session"];
expect(message.content[0]).toMatchObject({
type: "toolRequest",
id: "tool-1",
arguments: { path: "/tmp/report.md" },
toolKind: "edit",
locations: [{ path: "/tmp/report.md", line: 9 }],
status: "completed",
});
});
it("preserves structured tool output when ACP provides rawOutput", async () => {
registerSession(
"local-session",
+3
View File
@@ -21,6 +21,7 @@ export interface AcpSessionInfo {
archivedAt: string | null;
userSetName: boolean;
messageCount: number;
workingDir: string | null;
projectId?: string | null;
providerId: string | null;
modelId: string | null;
@@ -74,6 +75,7 @@ export async function listSessions(): Promise<AcpSessionInfo[]> {
archivedAt: (info._meta?.archivedAt as string) ?? null,
userSetName: info._meta?.userSetName === true,
messageCount: (info._meta?.messageCount as number) ?? 0,
workingDir: info.cwd ?? null,
projectId: (info._meta?.projectId as string) ?? null,
providerId: (info._meta?.providerId as string) ?? null,
modelId: (info._meta?.modelId as string) ?? null,
@@ -108,6 +110,7 @@ export async function forkSession(sessionId: string): Promise<AcpSessionInfo> {
archivedAt: (response._meta?.archivedAt as string) ?? null,
userSetName: response._meta?.userSetName === true,
messageCount: (response._meta?.messageCount as number) ?? 0,
workingDir: null,
projectId: (response._meta?.projectId as string) ?? null,
providerId: (response._meta?.providerId as string) ?? null,
modelId: (response._meta?.modelId as string) ?? null,
@@ -9,7 +9,9 @@ import {
findLatestUnpairedToolRequest,
} from "@/features/chat/hooks/replayBuffer";
import type {
ToolCallLocation,
ToolCallStatus,
ToolKind,
ToolRequestContent,
ToolResponseContent,
} from "@/shared/types/messages";
@@ -59,6 +61,52 @@ const pendingUsageUpdates = new Map<
const toolCallStatusFromUpdate = (status: string): ToolCallStatus =>
status === "failed" ? "error" : "completed";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function rawInputToArguments(rawInput: unknown): Record<string, unknown> {
return isRecord(rawInput) ? rawInput : {};
}
function toolKindFromUpdate(update: SessionUpdate): ToolKind | undefined {
const record: Record<string, unknown> = update;
const value = record.kind;
return typeof value === "string" ? (value as ToolKind) : undefined;
}
function locationsFromUpdate(
update: SessionUpdate,
): ToolCallLocation[] | undefined {
const record: Record<string, unknown> = update;
const value = record.locations;
if (!Array.isArray(value)) return undefined;
return value
.filter(
(location): location is { path: string; line?: number | null } =>
isRecord(location) && typeof location.path === "string",
)
.map((location) => ({
path: location.path,
...(typeof location.line === "number" || location.line === null
? { line: location.line }
: {}),
}));
}
function toolCallUpdatePatch(
update: SessionUpdate,
): Partial<ToolRequestContent> {
const toolKind = toolKindFromUpdate(update);
const locations = locationsFromUpdate(update);
return {
...(toolKind ? { toolKind } : {}),
...(locations ? { locations } : {}),
};
}
subscribeToSessionRegistration((localSessionId, gooseSessionId) => {
const pendingUsage = pendingUsageUpdates.get(gooseSessionId);
if (!pendingUsage) {
@@ -203,8 +251,9 @@ function handleReplay(
id: update.toolCallId,
name: update.title,
...identity,
arguments: {},
arguments: rawInputToArguments(update.rawInput),
status: "executing",
...toolCallUpdatePatch(update),
startedAt: created ?? Date.now(),
});
break;
@@ -231,7 +280,12 @@ function handleReplay(
if (created !== undefined && !existingMsg && msg === replayMsg) {
msg.created = created;
}
if (update.title || Object.keys(identity).length > 0) {
const patch = toolCallUpdatePatch(update);
if (
update.title ||
Object.keys(identity).length > 0 ||
Object.keys(patch).length > 0
) {
const tc = msg.content.find(
(c) => c.type === "toolRequest" && c.id === update.toolCallId,
);
@@ -239,6 +293,7 @@ function handleReplay(
Object.assign(tc as ToolRequestContent, {
...(update.title ? { name: update.title } : {}),
...identity,
...patch,
});
}
}
@@ -253,6 +308,7 @@ function handleReplay(
msg.content[idx] = {
...tc,
...identity,
...toolCallUpdatePatch(update),
status: toolCallStatus,
} as ToolRequestContent;
}
@@ -327,8 +383,9 @@ function handleLive(
id: update.toolCallId,
name: update.title,
...identity,
arguments: {},
arguments: rawInputToArguments(update.rawInput),
status: "executing",
...toolCallUpdatePatch(update),
startedAt: Date.now(),
};
store.setStreamingMessageId(sessionId, messageId);
@@ -340,7 +397,12 @@ function handleLive(
const messageId = ensureLiveAssistantMessage(sessionId, gooseSessionId);
const identity = getToolCallIdentity(update);
if (update.title || Object.keys(identity).length > 0) {
const patch = toolCallUpdatePatch(update);
if (
update.title ||
Object.keys(identity).length > 0 ||
Object.keys(patch).length > 0
) {
store.updateMessage(sessionId, messageId, (msg) => ({
...msg,
content: msg.content.map((c) =>
@@ -349,6 +411,7 @@ function handleLive(
...c,
...(update.title ? { name: update.title } : {}),
...identity,
...patch,
}
: c,
),
@@ -371,6 +434,7 @@ function handleLive(
? {
...block,
...identity,
...toolCallUpdatePatch(update),
status: toolCallStatus,
}
: block,
+1
View File
@@ -60,6 +60,7 @@ export interface Session {
personaId?: string;
modelId?: string;
modelName?: string;
workingDir?: string | null;
createdAt: string;
updatedAt: string;
archivedAt?: string;
+19
View File
@@ -68,6 +68,23 @@ export type ToolCallStatus =
| "error"
| "stopped";
export type ToolKind =
| "read"
| "edit"
| "delete"
| "move"
| "search"
| "execute"
| "think"
| "fetch"
| "switch_mode"
| "other";
export interface ToolCallLocation {
path: string;
line?: number | null;
}
export type MessageCompletionStatus =
| "inProgress"
| "completed"
@@ -82,6 +99,8 @@ export interface ToolRequestContent {
extensionName?: string;
arguments: Record<string, unknown>;
status: ToolCallStatus;
toolKind?: ToolKind;
locations?: ToolCallLocation[];
/** Epoch ms when the tool call started executing (set on event receipt). */
startedAt?: number;
annotations?: ContentAnnotations;