replace artifact heuristics/regexes with protocol messages (#8996)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -60,6 +60,7 @@ export interface Session {
|
||||
personaId?: string;
|
||||
modelId?: string;
|
||||
modelName?: string;
|
||||
workingDir?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
archivedAt?: string;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user