refactor tool-call detail contract for unknown tools
This commit is contained in:
@@ -106,23 +106,23 @@ function MergedToolCallItem({ item }: { item: MergedToolCall }) {
|
|||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<View style={stylesheet.toolContent}>
|
<View style={stylesheet.toolContent}>
|
||||||
{item.rawInput && (
|
{item.input && (
|
||||||
<View style={stylesheet.section}>
|
<View style={stylesheet.section}>
|
||||||
<Text style={stylesheet.sectionTitle}>Input:</Text>
|
<Text style={stylesheet.sectionTitle}>Input:</Text>
|
||||||
<Text style={stylesheet.code}>
|
<Text style={stylesheet.code}>
|
||||||
{JSON.stringify(item.rawInput, null, 2)}
|
{JSON.stringify(item.input, null, 2)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{item.rawOutput && (
|
{item.output && (
|
||||||
<View style={stylesheet.section}>
|
<View style={stylesheet.section}>
|
||||||
<Text style={stylesheet.sectionTitle}>Output:</Text>
|
<Text style={stylesheet.sectionTitle}>Output:</Text>
|
||||||
<Text style={stylesheet.code}>
|
<Text style={stylesheet.code}>
|
||||||
{JSON.stringify(item.rawOutput, null, 2)}
|
{JSON.stringify(item.output, null, 2)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{!item.rawInput && !item.rawOutput && (
|
{!item.input && !item.output && (
|
||||||
<Text style={stylesheet.emptyText}>
|
<Text style={stylesheet.emptyText}>
|
||||||
No details available
|
No details available
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1101,8 +1101,8 @@ function PermissionRequestCard({
|
|||||||
<ToolCallDetailsContent
|
<ToolCallDetailsContent
|
||||||
detail={{
|
detail={{
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: request.input ?? null,
|
input: request.input ?? null,
|
||||||
rawOutput: null,
|
output: null,
|
||||||
}}
|
}}
|
||||||
maxHeight={200}
|
maxHeight={200}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1198,8 +1198,8 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
if (args !== undefined || result !== undefined) {
|
if (args !== undefined || result !== undefined) {
|
||||||
return {
|
return {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: args ?? null,
|
input: args ?? null,
|
||||||
rawOutput: result ?? null,
|
output: result ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -1208,8 +1208,8 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
const displayDetail =
|
const displayDetail =
|
||||||
effectiveDetail ?? {
|
effectiveDetail ?? {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: null,
|
input: null,
|
||||||
rawOutput: null,
|
output: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayModel = useMemo(
|
const displayModel = useMemo(
|
||||||
@@ -1235,8 +1235,8 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
Boolean(error) ||
|
Boolean(error) ||
|
||||||
(effectiveDetail
|
(effectiveDetail
|
||||||
? effectiveDetail.type !== "unknown" ||
|
? effectiveDetail.type !== "unknown" ||
|
||||||
effectiveDetail.rawInput !== null ||
|
effectiveDetail.input !== null ||
|
||||||
effectiveDetail.rawOutput !== null
|
effectiveDetail.output !== null
|
||||||
: false);
|
: false);
|
||||||
|
|
||||||
const handleToggle = useCallback(() => {
|
const handleToggle = useCallback(() => {
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ export function ToolCallDetailsContent({
|
|||||||
);
|
);
|
||||||
} else if (detail?.type === "unknown") {
|
} else if (detail?.type === "unknown") {
|
||||||
const sectionsFromTopLevel = [
|
const sectionsFromTopLevel = [
|
||||||
{ title: "Input", value: detail.rawInput },
|
{ title: "Input", value: detail.input },
|
||||||
{ title: "Output", value: detail.rawOutput },
|
{ title: "Output", value: detail.output },
|
||||||
].filter((entry) => entry.value !== null && entry.value !== undefined);
|
].filter((entry) => entry.value !== null && entry.value !== undefined);
|
||||||
|
|
||||||
for (const section of sectionsFromTopLevel) {
|
for (const section of sectionsFromTopLevel) {
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export interface ToolCall {
|
|||||||
title: string;
|
title: string;
|
||||||
status?: 'pending' | 'in_progress' | 'completed' | 'failed';
|
status?: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||||
toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
|
toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
|
||||||
rawInput?: Record<string, unknown>;
|
input?: Record<string, unknown>;
|
||||||
rawOutput?: Record<string, unknown>;
|
output?: Record<string, unknown>;
|
||||||
content?: unknown[];
|
content?: unknown[];
|
||||||
locations?: unknown[];
|
locations?: unknown[];
|
||||||
}
|
}
|
||||||
@@ -53,8 +53,8 @@ export interface ToolCallUpdate {
|
|||||||
title?: string | null;
|
title?: string | null;
|
||||||
status?: 'pending' | 'in_progress' | 'completed' | 'failed' | null;
|
status?: 'pending' | 'in_progress' | 'completed' | 'failed' | null;
|
||||||
toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other' | null;
|
toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other' | null;
|
||||||
rawInput?: Record<string, unknown>;
|
input?: Record<string, unknown>;
|
||||||
rawOutput?: Record<string, unknown>;
|
output?: Record<string, unknown>;
|
||||||
content?: unknown[] | null;
|
content?: unknown[] | null;
|
||||||
locations?: unknown[] | null;
|
locations?: unknown[] | null;
|
||||||
}
|
}
|
||||||
@@ -109,8 +109,8 @@ export interface MergedToolCall {
|
|||||||
title: string;
|
title: string;
|
||||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||||
toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
|
toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
|
||||||
rawInput?: Record<string, unknown>;
|
input?: Record<string, unknown>;
|
||||||
rawOutput?: Record<string, unknown>;
|
output?: Record<string, unknown>;
|
||||||
content?: unknown[];
|
content?: unknown[];
|
||||||
locations?: unknown[];
|
locations?: unknown[];
|
||||||
startTimestamp: Date;
|
startTimestamp: Date;
|
||||||
@@ -137,8 +137,8 @@ export function groupActivities(activities: AgentActivity[]): Array<GroupedTextM
|
|||||||
title: string;
|
title: string;
|
||||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||||
toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
|
toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
|
||||||
rawInput?: Record<string, unknown>;
|
input?: Record<string, unknown>;
|
||||||
rawOutput?: Record<string, unknown>;
|
output?: Record<string, unknown>;
|
||||||
content?: unknown[];
|
content?: unknown[];
|
||||||
locations?: unknown[];
|
locations?: unknown[];
|
||||||
startTimestamp: Date;
|
startTimestamp: Date;
|
||||||
@@ -210,8 +210,8 @@ export function groupActivities(activities: AgentActivity[]): Array<GroupedTextM
|
|||||||
title: update.title,
|
title: update.title,
|
||||||
status: update.status || 'pending',
|
status: update.status || 'pending',
|
||||||
toolKind: update.toolKind,
|
toolKind: update.toolKind,
|
||||||
rawInput: update.rawInput,
|
input: update.input,
|
||||||
rawOutput: update.rawOutput,
|
output: update.output,
|
||||||
content: update.content,
|
content: update.content,
|
||||||
locations: update.locations,
|
locations: update.locations,
|
||||||
startTimestamp: activity.timestamp,
|
startTimestamp: activity.timestamp,
|
||||||
@@ -223,8 +223,8 @@ export function groupActivities(activities: AgentActivity[]): Array<GroupedTextM
|
|||||||
existing.title = update.title;
|
existing.title = update.title;
|
||||||
if (update.status) existing.status = update.status;
|
if (update.status) existing.status = update.status;
|
||||||
if (update.toolKind) existing.toolKind = update.toolKind;
|
if (update.toolKind) existing.toolKind = update.toolKind;
|
||||||
if (update.rawInput) existing.rawInput = update.rawInput;
|
if (update.input) existing.input = update.input;
|
||||||
if (update.rawOutput) existing.rawOutput = update.rawOutput;
|
if (update.output) existing.output = update.output;
|
||||||
if (update.content) existing.content = update.content;
|
if (update.content) existing.content = update.content;
|
||||||
if (update.locations) existing.locations = update.locations;
|
if (update.locations) existing.locations = update.locations;
|
||||||
existing.endTimestamp = activity.timestamp;
|
existing.endTimestamp = activity.timestamp;
|
||||||
@@ -236,8 +236,8 @@ export function groupActivities(activities: AgentActivity[]): Array<GroupedTextM
|
|||||||
if (update.title) existing.title = update.title;
|
if (update.title) existing.title = update.title;
|
||||||
if (update.status) existing.status = update.status;
|
if (update.status) existing.status = update.status;
|
||||||
if (update.toolKind) existing.toolKind = update.toolKind;
|
if (update.toolKind) existing.toolKind = update.toolKind;
|
||||||
if (update.rawInput) existing.rawInput = { ...existing.rawInput, ...update.rawInput };
|
if (update.input) existing.input = { ...existing.input, ...update.input };
|
||||||
if (update.rawOutput) existing.rawOutput = { ...existing.rawOutput, ...update.rawOutput };
|
if (update.output) existing.output = { ...existing.output, ...update.output };
|
||||||
if (update.content) existing.content = update.content;
|
if (update.content) existing.content = update.content;
|
||||||
if (update.locations) existing.locations = update.locations;
|
if (update.locations) existing.locations = update.locations;
|
||||||
existing.endTimestamp = activity.timestamp;
|
existing.endTimestamp = activity.timestamp;
|
||||||
@@ -251,8 +251,8 @@ export function groupActivities(activities: AgentActivity[]): Array<GroupedTextM
|
|||||||
title: update.title || 'Tool Call',
|
title: update.title || 'Tool Call',
|
||||||
status: update.status || 'pending',
|
status: update.status || 'pending',
|
||||||
toolKind: update.toolKind || undefined,
|
toolKind: update.toolKind || undefined,
|
||||||
rawInput: update.rawInput,
|
input: update.input,
|
||||||
rawOutput: update.rawOutput,
|
output: update.output,
|
||||||
content: update.content || undefined,
|
content: update.content || undefined,
|
||||||
locations: update.locations || undefined,
|
locations: update.locations || undefined,
|
||||||
startTimestamp: activity.timestamp,
|
startTimestamp: activity.timestamp,
|
||||||
@@ -280,8 +280,8 @@ export function groupActivities(activities: AgentActivity[]): Array<GroupedTextM
|
|||||||
title: toolCall.title,
|
title: toolCall.title,
|
||||||
status: toolCall.status,
|
status: toolCall.status,
|
||||||
toolKind: toolCall.toolKind,
|
toolKind: toolCall.toolKind,
|
||||||
rawInput: toolCall.rawInput,
|
input: toolCall.input,
|
||||||
rawOutput: toolCall.rawOutput,
|
output: toolCall.output,
|
||||||
content: toolCall.content,
|
content: toolCall.content,
|
||||||
locations: toolCall.locations,
|
locations: toolCall.locations,
|
||||||
startTimestamp: toolCall.startTimestamp,
|
startTimestamp: toolCall.startTimestamp,
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ const toolCallEvent = (): AgentStreamEventPayload => ({
|
|||||||
status: "running",
|
status: "running",
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: { command: "echo hi" },
|
input: { command: "echo hi" },
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -215,8 +215,8 @@ function buildToolEvent({
|
|||||||
}): AgentStreamEventPayload {
|
}): AgentStreamEventPayload {
|
||||||
const canonicalDetail: ToolCallDetail = detail ?? {
|
const canonicalDetail: ToolCallDetail = detail ?? {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: input,
|
input: input,
|
||||||
rawOutput: output,
|
output: output,
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseItem = {
|
const baseItem = {
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ function canonicalToolTimeline(params: {
|
|||||||
}): AgentStreamEventPayload {
|
}): AgentStreamEventPayload {
|
||||||
const detail: ToolCallDetail = params.detail ?? {
|
const detail: ToolCallDetail = params.detail ?? {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: params.input ?? null,
|
input: params.input ?? null,
|
||||||
rawOutput: params.output ?? null,
|
output: params.output ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseItem = {
|
const baseItem = {
|
||||||
@@ -150,8 +150,8 @@ describe("stream reducer canonical tool calls", () => {
|
|||||||
assert.strictEqual(tools[0].payload.data.status, "completed");
|
assert.strictEqual(tools[0].payload.data.status, "completed");
|
||||||
assert.deepStrictEqual(tools[0].payload.data.detail, {
|
assert.deepStrictEqual(tools[0].payload.data.detail, {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: { command: "pwd" },
|
input: { command: "pwd" },
|
||||||
rawOutput: {
|
output: {
|
||||||
output: "/tmp/repo\n",
|
output: "/tmp/repo\n",
|
||||||
exitCode: 0,
|
exitCode: 0,
|
||||||
},
|
},
|
||||||
@@ -282,8 +282,8 @@ describe("stream reducer canonical tool calls", () => {
|
|||||||
assert.ok(tool);
|
assert.ok(tool);
|
||||||
assert.deepStrictEqual(tool.payload.data.detail, {
|
assert.deepStrictEqual(tool.payload.data.detail, {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: { path: "README.md" },
|
input: { path: "README.md" },
|
||||||
rawOutput: { content: "hello" },
|
output: { content: "hello" },
|
||||||
});
|
});
|
||||||
assert.strictEqual(tool.payload.data.status, "completed");
|
assert.strictEqual(tool.payload.data.status, "completed");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -353,20 +353,20 @@ function mergeToolCallDetail(existing: ToolCallDetail, incoming: ToolCallDetail)
|
|||||||
if (existing.type === "unknown" && incoming.type === "unknown") {
|
if (existing.type === "unknown" && incoming.type === "unknown") {
|
||||||
return {
|
return {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: mergeUnknownValue(existing.rawInput, incoming.rawInput),
|
input: mergeUnknownValue(existing.input, incoming.input),
|
||||||
rawOutput: mergeUnknownValue(existing.rawOutput, incoming.rawOutput),
|
output: mergeUnknownValue(existing.output, incoming.output),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existing.type === incoming.type) {
|
if (existing.type === incoming.type) {
|
||||||
return { ...existing, ...incoming };
|
return { ...existing, ...incoming } as ToolCallDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
return incoming;
|
return incoming;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rawInputFromDetail(detail: ToolCallDetail): unknown | null {
|
function inputFromUnknownDetail(detail: ToolCallDetail): unknown | null {
|
||||||
return detail.type === "unknown" ? detail.rawInput : null;
|
return detail.type === "unknown" ? detail.input : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeAgentToolCallStatus(
|
function mergeAgentToolCallStatus(
|
||||||
@@ -552,7 +552,7 @@ export function reduceStreamUpdate(
|
|||||||
// as Tasks when possible and otherwise hide it to avoid a stuck loading tool call.
|
// as Tasks when possible and otherwise hide it to avoid a stuck loading tool call.
|
||||||
const tasks = extractTaskEntriesFromToolCall(
|
const tasks = extractTaskEntriesFromToolCall(
|
||||||
item.name,
|
item.name,
|
||||||
rawInputFromDetail(item.detail)
|
inputFromUnknownDetail(item.detail)
|
||||||
);
|
);
|
||||||
if (tasks) {
|
if (tasks) {
|
||||||
nextState = appendTodoList(
|
nextState = appendTodoList(
|
||||||
@@ -570,7 +570,7 @@ export function reduceStreamUpdate(
|
|||||||
|
|
||||||
const tasks = extractTaskEntriesFromToolCall(
|
const tasks = extractTaskEntriesFromToolCall(
|
||||||
item.name,
|
item.name,
|
||||||
rawInputFromDetail(item.detail)
|
inputFromUnknownDetail(item.detail)
|
||||||
);
|
);
|
||||||
if (tasks) {
|
if (tasks) {
|
||||||
nextState = appendTodoList(
|
nextState = appendTodoList(
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ describe("tool-call-display", () => {
|
|||||||
error: null,
|
error: null,
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: null,
|
input: null,
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
subAgentActivity: "Running tests",
|
subAgentActivity: "Running tests",
|
||||||
@@ -66,8 +66,8 @@ describe("tool-call-display", () => {
|
|||||||
error: null,
|
error: null,
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: null,
|
input: null,
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,8 +83,8 @@ describe("tool-call-display", () => {
|
|||||||
error: null,
|
error: null,
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: { command: "npm run test" },
|
input: { command: "npm run test" },
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,8 +100,8 @@ describe("tool-call-display", () => {
|
|||||||
error: { message: "boom" },
|
error: { message: "boom" },
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: { command: "false" },
|
input: { command: "false" },
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -344,8 +344,8 @@ describe("DaemonClient", () => {
|
|||||||
status: "inProgress",
|
status: "inProgress",
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: { command: "pwd" },
|
input: { command: "pwd" },
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ function toolCallItem(params: {
|
|||||||
const detail =
|
const detail =
|
||||||
params.detail ?? {
|
params.detail ?? {
|
||||||
type: "unknown" as const,
|
type: "unknown" as const,
|
||||||
rawInput: params.input ?? null,
|
input: params.input ?? null,
|
||||||
rawOutput: params.output ?? null,
|
output: params.output ?? null,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
type: "tool_call",
|
type: "tool_call",
|
||||||
|
|||||||
@@ -71,18 +71,18 @@ function mergeToolDetail(existing: ToolCallDetail, incoming: ToolCallDetail): To
|
|||||||
if (existing.type === "unknown" && incoming.type === "unknown") {
|
if (existing.type === "unknown" && incoming.type === "unknown") {
|
||||||
return {
|
return {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: mergeUnknownValue(existing.rawInput, incoming.rawInput),
|
input: mergeUnknownValue(existing.input, incoming.input),
|
||||||
rawOutput: mergeUnknownValue(existing.rawOutput, incoming.rawOutput),
|
output: mergeUnknownValue(existing.output, incoming.output),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (existing.type === incoming.type) {
|
if (existing.type === incoming.type) {
|
||||||
return { ...existing, ...incoming };
|
return { ...existing, ...incoming } as ToolCallDetail;
|
||||||
}
|
}
|
||||||
return incoming;
|
return incoming;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rawInputFromDetail(detail: ToolCallDetail): unknown {
|
function inputFromUnknownDetail(detail: ToolCallDetail): unknown {
|
||||||
return detail.type === "unknown" ? detail.rawInput : null;
|
return detail.type === "unknown" ? detail.input : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -203,7 +203,7 @@ export function curateAgentActivity(
|
|||||||
break;
|
break;
|
||||||
case "tool_call": {
|
case "tool_call": {
|
||||||
flushBuffers(lines, buffers);
|
flushBuffers(lines, buffers);
|
||||||
const inputJson = formatToolInputJson(rawInputFromDetail(item.detail));
|
const inputJson = formatToolInputJson(inputFromUnknownDetail(item.detail));
|
||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ export type ToolCallDetail =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "unknown";
|
type: "unknown";
|
||||||
rawInput: unknown | null;
|
input: unknown | null;
|
||||||
rawOutput: unknown | null;
|
output: unknown | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ToolCallBase = {
|
type ToolCallBase = {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ function extractToolCommand(detail: unknown): string | null {
|
|||||||
return detail.command;
|
return detail.command;
|
||||||
}
|
}
|
||||||
if (detail.type === "unknown") {
|
if (detail.type === "unknown") {
|
||||||
return extractCommandText(detail.rawInput);
|
return extractCommandText(detail.input);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -245,7 +245,6 @@ async function startAgentMcpServer(): Promise<AgentMcpServerHandle> {
|
|||||||
let hydrateStreamState: (updates: unknown[]) => unknown = () => {
|
let hydrateStreamState: (updates: unknown[]) => unknown = () => {
|
||||||
throw new Error("hydrateStreamState not initialized");
|
throw new Error("hydrateStreamState not initialized");
|
||||||
};
|
};
|
||||||
let isAgentToolCallItem: (item: unknown) => boolean = () => false;
|
|
||||||
let agentMcpServer: AgentMcpServerHandle;
|
let agentMcpServer: AgentMcpServerHandle;
|
||||||
let restoreClaudeConfigDir: (() => void) | null = null;
|
let restoreClaudeConfigDir: (() => void) | null = null;
|
||||||
const buildConfig = (
|
const buildConfig = (
|
||||||
@@ -271,7 +270,6 @@ async function startAgentMcpServer(): Promise<AgentMcpServerHandle> {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const stream = await import("../../../../../app/src/types/stream.js");
|
const stream = await import("../../../../../app/src/types/stream.js");
|
||||||
hydrateStreamState = stream.hydrateStreamState as typeof hydrateStreamState;
|
hydrateStreamState = stream.hydrateStreamState as typeof hydrateStreamState;
|
||||||
isAgentToolCallItem = stream.isAgentToolCallItem as typeof isAgentToolCallItem;
|
|
||||||
});
|
});
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
agentMcpServer = await startAgentMcpServer();
|
agentMcpServer = await startAgentMcpServer();
|
||||||
@@ -450,8 +448,8 @@ async function startAgentMcpServer(): Promise<AgentMcpServerHandle> {
|
|||||||
}
|
}
|
||||||
if (item.detail.type === "unknown") {
|
if (item.detail.type === "unknown") {
|
||||||
return (
|
return (
|
||||||
rawContainsText(item.detail.rawInput, "tool-test.txt") ||
|
rawContainsText(item.detail.input, "tool-test.txt") ||
|
||||||
rawContainsText(item.detail.rawOutput, "tool-test.txt")
|
rawContainsText(item.detail.output, "tool-test.txt")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return rawContainsText(item.detail, "tool-test.txt");
|
return rawContainsText(item.detail, "tool-test.txt");
|
||||||
@@ -1330,10 +1328,18 @@ function stateIncludesUserMessage(state: StreamItem[], marker: string): boolean
|
|||||||
}
|
}
|
||||||
|
|
||||||
function extractAgentToolSnapshots(state: StreamItem[]): ToolSnapshot[] {
|
function extractAgentToolSnapshots(state: StreamItem[]): ToolSnapshot[] {
|
||||||
return state.filter(isAgentToolCallItem).map((item) => ({
|
return state
|
||||||
key: buildToolSnapshotKey(item.payload.data, item.id),
|
.filter(
|
||||||
data: item.payload.data,
|
(item): item is { kind: "tool_call"; id: string; payload: { source: "agent"; data: AgentToolCallData } } =>
|
||||||
}));
|
Boolean(item) &&
|
||||||
|
item.kind === "tool_call" &&
|
||||||
|
item.payload?.source === "agent" &&
|
||||||
|
item.payload?.data
|
||||||
|
)
|
||||||
|
.map((item) => ({
|
||||||
|
key: buildToolSnapshotKey(item.payload.data, item.id),
|
||||||
|
data: item.payload.data,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildToolSnapshotKey(data: AgentToolCallData, fallbackId: string): string {
|
function buildToolSnapshotKey(data: AgentToolCallData, fallbackId: string): string {
|
||||||
|
|||||||
@@ -54,14 +54,18 @@ export function deriveClaudeToolDetail(
|
|||||||
name: string,
|
name: string,
|
||||||
input: unknown,
|
input: unknown,
|
||||||
output: unknown
|
output: unknown
|
||||||
): ToolCallDetail | undefined {
|
): ToolCallDetail {
|
||||||
const parsed = ClaudeKnownToolDetailSchema.safeParse({
|
const parsed = ClaudeKnownToolDetailSchema.safeParse({
|
||||||
name,
|
name,
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
});
|
});
|
||||||
if (!parsed.success) {
|
if (parsed.success && parsed.data) {
|
||||||
return undefined;
|
return parsed.data;
|
||||||
}
|
}
|
||||||
return parsed.data;
|
return {
|
||||||
|
type: "unknown",
|
||||||
|
input: input ?? null,
|
||||||
|
output: output ?? null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,8 +187,8 @@ describe("claude tool-call mapper", () => {
|
|||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
expect(item.detail).toEqual({
|
expect(item.detail).toEqual({
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: { foo: "bar" },
|
input: { foo: "bar" },
|
||||||
rawOutput: { ok: true },
|
output: { ok: true },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.js";
|
import type { ToolCallTimelineItem } from "../../agent-sdk-types.js";
|
||||||
import { coerceToolCallId } from "../tool-call-mapper-utils.js";
|
import { coerceToolCallId } from "../tool-call-mapper-utils.js";
|
||||||
import { deriveClaudeToolDetail } from "./tool-call-detail-parser.js";
|
import { deriveClaudeToolDetail } from "./tool-call-detail-parser.js";
|
||||||
|
|
||||||
@@ -38,9 +38,7 @@ function coerceCallId(callId: string | null | undefined, name: string, input: un
|
|||||||
function buildBase(params: MapperParams): {
|
function buildBase(params: MapperParams): {
|
||||||
callId: string;
|
callId: string;
|
||||||
name: string;
|
name: string;
|
||||||
input: unknown | null;
|
detail: Extract<ToolCallTimelineItem, { type: "tool_call" }>["detail"];
|
||||||
output: unknown | null;
|
|
||||||
detail?: ToolCallDetail;
|
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
} {
|
} {
|
||||||
const parsedParams = ClaudeMapperParamsSchema.parse(params);
|
const parsedParams = ClaudeMapperParamsSchema.parse(params);
|
||||||
@@ -51,9 +49,7 @@ function buildBase(params: MapperParams): {
|
|||||||
return {
|
return {
|
||||||
callId: coerceCallId(parsedParams.callId, parsedParams.name, input),
|
callId: coerceCallId(parsedParams.callId, parsedParams.name, input),
|
||||||
name: parsedParams.name,
|
name: parsedParams.name,
|
||||||
input,
|
detail,
|
||||||
output,
|
|
||||||
...(detail ? { detail } : {}),
|
|
||||||
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
|
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,35 +48,43 @@ function useTempCodexSessionDir(): () => void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasShellCommand(item: AgentTimelineItem, commandFragment: string): boolean {
|
function hasShellCommand(item: AgentTimelineItem, commandFragment: string): boolean {
|
||||||
if (item.type !== "tool_call" || item.name !== "shell") return false;
|
if (item.type !== "tool_call") return false;
|
||||||
if (item.detail.type === "shell") {
|
if (item.detail.type === "shell") {
|
||||||
return item.detail.command.includes(commandFragment);
|
return item.detail.command.includes(commandFragment);
|
||||||
}
|
}
|
||||||
const unknownInput =
|
const unknownInput =
|
||||||
item.detail.type === "unknown" && typeof item.detail.rawInput === "object" && item.detail.rawInput
|
item.detail.type === "unknown" && typeof item.detail.input === "object" && item.detail.input
|
||||||
? (item.detail.rawInput as { command?: string })
|
? (item.detail.input as { command?: string | string[]; cmd?: string | string[] })
|
||||||
: undefined;
|
: undefined;
|
||||||
const command = unknownInput?.command ?? "";
|
const commandValue = unknownInput?.command ?? unknownInput?.cmd;
|
||||||
|
const command =
|
||||||
|
typeof commandValue === "string"
|
||||||
|
? commandValue
|
||||||
|
: Array.isArray(commandValue)
|
||||||
|
? commandValue.filter((value): value is string => typeof value === "string").join(" ")
|
||||||
|
: "";
|
||||||
return command.includes(commandFragment);
|
return command.includes(commandFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasApplyPatchFile(item: AgentTimelineItem, fileName: string): boolean {
|
function hasApplyPatchFile(item: AgentTimelineItem, fileName: string): boolean {
|
||||||
if (item.type !== "tool_call" || item.name !== "apply_patch") return false;
|
if (item.type !== "tool_call") return false;
|
||||||
if (item.detail.type === "edit") {
|
if (item.detail.type === "edit") {
|
||||||
return item.detail.filePath === fileName || (item.detail.unifiedDiff?.includes(fileName) ?? false);
|
return item.detail.filePath === fileName || (item.detail.unifiedDiff?.includes(fileName) ?? false);
|
||||||
}
|
}
|
||||||
const unknownInput =
|
const unknownInput =
|
||||||
item.detail.type === "unknown" && typeof item.detail.rawInput === "object" && item.detail.rawInput
|
item.detail.type === "unknown" && typeof item.detail.input === "object" && item.detail.input
|
||||||
? (item.detail.rawInput as { files?: Array<{ path?: string }> })
|
? (item.detail.input as { path?: string; file_path?: string; filePath?: string; files?: Array<{ path?: string }> })
|
||||||
: undefined;
|
: undefined;
|
||||||
const unknownOutput =
|
const unknownOutput =
|
||||||
item.detail.type === "unknown" && typeof item.detail.rawOutput === "object" && item.detail.rawOutput
|
item.detail.type === "unknown" && typeof item.detail.output === "object" && item.detail.output
|
||||||
? (item.detail.rawOutput as { files?: Array<{ path?: string; patch?: string }>; diff?: string })
|
? (item.detail.output as { path?: string; file_path?: string; filePath?: string; files?: Array<{ path?: string; patch?: string }>; diff?: string })
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const inputPath = unknownInput?.path ?? unknownInput?.file_path ?? unknownInput?.filePath;
|
||||||
|
const outputPath = unknownOutput?.path ?? unknownOutput?.file_path ?? unknownOutput?.filePath;
|
||||||
const inInput = (unknownInput?.files ?? []).some((file) => file?.path === fileName);
|
const inInput = (unknownInput?.files ?? []).some((file) => file?.path === fileName);
|
||||||
const inOutput = (unknownOutput?.files ?? []).some((file) => file?.path === fileName);
|
const inOutput = (unknownOutput?.files ?? []).some((file) => file?.path === fileName);
|
||||||
const inDiff = typeof unknownOutput?.diff === "string" && unknownOutput.diff.includes(fileName);
|
const inDiff = typeof unknownOutput?.diff === "string" && unknownOutput.diff.includes(fileName);
|
||||||
return inInput || inOutput || inDiff;
|
return inInput || inOutput || inDiff || inputPath === fileName || outputPath === fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForFileToContainText(
|
async function waitForFileToContainText(
|
||||||
@@ -363,7 +371,7 @@ describe("Codex app-server provider (integration)", () => {
|
|||||||
if (call.name !== "paseo_test.echo") {
|
if (call.name !== "paseo_test.echo") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const key = String(call.callId ?? `${call.name}:${JSON.stringify(call.input ?? {})}`);
|
const key = String(call.callId ?? `${call.name}:${JSON.stringify(call.detail)}`);
|
||||||
const existing = distinctMcpCalls.get(key);
|
const existing = distinctMcpCalls.get(key);
|
||||||
if (!existing || call.status === "completed") {
|
if (!existing || call.status === "completed") {
|
||||||
distinctMcpCalls.set(key, call);
|
distinctMcpCalls.set(key, call);
|
||||||
@@ -382,8 +390,9 @@ describe("Codex app-server provider (integration)", () => {
|
|||||||
expect(toolNames.some((name) => name.toLowerCase().includes("shell"))).toBe(false);
|
expect(toolNames.some((name) => name.toLowerCase().includes("shell"))).toBe(false);
|
||||||
|
|
||||||
// Hard assertion: roundtrip token must be present in the MCP tool I/O.
|
// Hard assertion: roundtrip token must be present in the MCP tool I/O.
|
||||||
expect(JSON.stringify(mcpToolCall.input ?? {})).toContain(token);
|
const mcpDetail = mcpToolCall.detail.type === "unknown" ? mcpToolCall.detail : null;
|
||||||
expect(JSON.stringify(mcpToolCall.output ?? {})).toContain(`ECHO:${token}`);
|
expect(JSON.stringify(mcpDetail?.input ?? {})).toContain(token);
|
||||||
|
expect(JSON.stringify(mcpDetail?.output ?? {})).toContain(`ECHO:${token}`);
|
||||||
expect(result.finalText).toContain(`ECHO:${token}`);
|
expect(result.finalText).toContain(`ECHO:${token}`);
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
@@ -496,7 +505,7 @@ describe("Codex app-server provider (integration)", () => {
|
|||||||
}
|
}
|
||||||
expect(sawPermission || timelineItems.length > 0).toBe(true);
|
expect(sawPermission || timelineItems.length > 0).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
timelineItems.some((item) => item.type === "tool_call" && item.name === "shell")
|
timelineItems.some((item) => hasShellCommand(item, "printf"))
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(existsSync(filePath)).toBe(true);
|
expect(existsSync(filePath)).toBe(true);
|
||||||
expect(readFileSync(filePath, "utf8")).toContain("ok");
|
expect(readFileSync(filePath, "utf8")).toContain("ok");
|
||||||
@@ -629,12 +638,10 @@ describe("Codex app-server provider (integration)", () => {
|
|||||||
expect(shellItem?.type).toBe("tool_call");
|
expect(shellItem?.type).toBe("tool_call");
|
||||||
expect(patchItem?.type).toBe("tool_call");
|
expect(patchItem?.type).toBe("tool_call");
|
||||||
if (shellItem?.type === "tool_call") {
|
if (shellItem?.type === "tool_call") {
|
||||||
expect(shellItem.name).toBe("shell");
|
expect(hasShellCommand(shellItem, "printf")).toBe(true);
|
||||||
expect(shellItem.input).toBeTruthy();
|
|
||||||
}
|
}
|
||||||
if (patchItem?.type === "tool_call") {
|
if (patchItem?.type === "tool_call") {
|
||||||
expect(patchItem.name).toBe("apply_patch");
|
expect(hasApplyPatchFile(patchItem, "patch.txt")).toBe(true);
|
||||||
expect(patchItem.input || patchItem.output).toBeTruthy();
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
@@ -698,7 +705,6 @@ describe("Codex app-server provider (integration)", () => {
|
|||||||
if (
|
if (
|
||||||
event.type === "timeline" &&
|
event.type === "timeline" &&
|
||||||
event.item.type === "tool_call" &&
|
event.item.type === "tool_call" &&
|
||||||
event.item.name === "shell" &&
|
|
||||||
hasShellCommand(event.item, "sleep 60")
|
hasShellCommand(event.item, "sleep 60")
|
||||||
) {
|
) {
|
||||||
sawSleepCommand = true;
|
sawSleepCommand = true;
|
||||||
|
|||||||
@@ -134,15 +134,19 @@ export function deriveCodexToolDetail(params: {
|
|||||||
input: unknown;
|
input: unknown;
|
||||||
output: unknown;
|
output: unknown;
|
||||||
cwd?: string | null;
|
cwd?: string | null;
|
||||||
}): ToolCallDetail | undefined {
|
}): ToolCallDetail {
|
||||||
const parsed = CodexKnownToolDetailSchema.safeParse({
|
const parsed = CodexKnownToolDetailSchema.safeParse({
|
||||||
name: params.name,
|
name: params.name,
|
||||||
input: params.input,
|
input: params.input,
|
||||||
output: params.output,
|
output: params.output,
|
||||||
cwd: params.cwd ?? null,
|
cwd: params.cwd ?? null,
|
||||||
});
|
});
|
||||||
if (!parsed.success) {
|
if (parsed.success && parsed.data) {
|
||||||
return undefined;
|
return parsed.data;
|
||||||
}
|
}
|
||||||
return parsed.data;
|
return {
|
||||||
|
type: "unknown",
|
||||||
|
input: params.input ?? null,
|
||||||
|
output: params.output ?? null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,8 +235,8 @@ describe("codex tool-call mapper", () => {
|
|||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
expect(item.detail).toEqual({
|
expect(item.detail).toEqual({
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: { foo: "bar" },
|
input: { foo: "bar" },
|
||||||
rawOutput: { ok: true },
|
output: { ok: true },
|
||||||
});
|
});
|
||||||
expect(item.callId).toBe("codex-call-4");
|
expect(item.callId).toBe("codex-call-4");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -140,10 +140,8 @@ function buildToolCall(params: {
|
|||||||
callId: string;
|
callId: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: ToolCallTimelineItem["status"];
|
status: ToolCallTimelineItem["status"];
|
||||||
input: unknown | null;
|
|
||||||
output: unknown | null;
|
|
||||||
error: unknown | null;
|
error: unknown | null;
|
||||||
detail?: ToolCallDetail;
|
detail: ToolCallDetail;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
}): ToolCallTimelineItem {
|
}): ToolCallTimelineItem {
|
||||||
if (params.status === "failed") {
|
if (params.status === "failed") {
|
||||||
@@ -152,10 +150,8 @@ function buildToolCall(params: {
|
|||||||
callId: params.callId,
|
callId: params.callId,
|
||||||
name: params.name,
|
name: params.name,
|
||||||
status: "failed",
|
status: "failed",
|
||||||
input: params.input,
|
|
||||||
output: params.output,
|
|
||||||
error: params.error ?? { message: "Tool call failed" },
|
error: params.error ?? { message: "Tool call failed" },
|
||||||
...(params.detail ? { detail: params.detail } : {}),
|
detail: params.detail,
|
||||||
...(params.metadata ? { metadata: params.metadata } : {}),
|
...(params.metadata ? { metadata: params.metadata } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -165,10 +161,8 @@ function buildToolCall(params: {
|
|||||||
callId: params.callId,
|
callId: params.callId,
|
||||||
name: params.name,
|
name: params.name,
|
||||||
status: params.status,
|
status: params.status,
|
||||||
input: params.input,
|
|
||||||
output: params.output,
|
|
||||||
error: null,
|
error: null,
|
||||||
...(params.detail ? { detail: params.detail } : {}),
|
detail: params.detail,
|
||||||
...(params.metadata ? { metadata: params.metadata } : {}),
|
...(params.metadata ? { metadata: params.metadata } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -221,7 +215,11 @@ function mapCommandExecutionItem(
|
|||||||
...(item.aggregatedOutput ? { output: item.aggregatedOutput } : {}),
|
...(item.aggregatedOutput ? { output: item.aggregatedOutput } : {}),
|
||||||
...(item.exitCode !== undefined ? { exitCode: item.exitCode } : {}),
|
...(item.exitCode !== undefined ? { exitCode: item.exitCode } : {}),
|
||||||
}
|
}
|
||||||
: undefined;
|
: {
|
||||||
|
type: "unknown" as const,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
};
|
||||||
|
|
||||||
const name = "shell";
|
const name = "shell";
|
||||||
const callId = coerceCallId(item.id, name, input);
|
const callId = coerceCallId(item.id, name, input);
|
||||||
@@ -232,10 +230,8 @@ function mapCommandExecutionItem(
|
|||||||
callId,
|
callId,
|
||||||
name,
|
name,
|
||||||
status,
|
status,
|
||||||
input,
|
|
||||||
output,
|
|
||||||
error,
|
error,
|
||||||
...(detail ? { detail } : {}),
|
detail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +286,11 @@ function mapFileChangeItem(
|
|||||||
filePath: firstFile.path,
|
filePath: firstFile.path,
|
||||||
...(firstFile.diff !== undefined ? { unifiedDiff: truncateDiffText(firstFile.diff) } : {}),
|
...(firstFile.diff !== undefined ? { unifiedDiff: truncateDiffText(firstFile.diff) } : {}),
|
||||||
}
|
}
|
||||||
: undefined;
|
: {
|
||||||
|
type: "unknown" as const,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
};
|
||||||
|
|
||||||
const name = "apply_patch";
|
const name = "apply_patch";
|
||||||
const callId = coerceCallId(item.id, name, input);
|
const callId = coerceCallId(item.id, name, input);
|
||||||
@@ -301,10 +301,8 @@ function mapFileChangeItem(
|
|||||||
callId,
|
callId,
|
||||||
name,
|
name,
|
||||||
status,
|
status,
|
||||||
input,
|
|
||||||
output,
|
|
||||||
error,
|
error,
|
||||||
...(detail ? { detail } : {}),
|
detail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,10 +328,8 @@ function mapMcpToolCallItem(
|
|||||||
callId,
|
callId,
|
||||||
name,
|
name,
|
||||||
status,
|
status,
|
||||||
input,
|
|
||||||
output,
|
|
||||||
error,
|
error,
|
||||||
...(detail ? { detail } : {}),
|
detail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,16 +345,18 @@ function mapWebSearchItem(item: z.infer<typeof CodexWebSearchItemSchema>): ToolC
|
|||||||
type: "search" as const,
|
type: "search" as const,
|
||||||
query: item.query,
|
query: item.query,
|
||||||
}
|
}
|
||||||
: undefined;
|
: {
|
||||||
|
type: "unknown" as const,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
};
|
||||||
|
|
||||||
return buildToolCall({
|
return buildToolCall({
|
||||||
callId,
|
callId,
|
||||||
name,
|
name,
|
||||||
status,
|
status,
|
||||||
input,
|
|
||||||
output,
|
|
||||||
error,
|
error,
|
||||||
...(detail ? { detail } : {}),
|
detail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,9 +418,7 @@ export function mapCodexRolloutToolCall(params: {
|
|||||||
callId,
|
callId,
|
||||||
name: parsed.name,
|
name: parsed.name,
|
||||||
status,
|
status,
|
||||||
input,
|
|
||||||
output,
|
|
||||||
error,
|
error,
|
||||||
...(detail ? { detail } : {}),
|
detail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,14 +43,18 @@ export function deriveOpencodeToolDetail(
|
|||||||
toolName: string,
|
toolName: string,
|
||||||
input: unknown,
|
input: unknown,
|
||||||
output: unknown
|
output: unknown
|
||||||
): ToolCallDetail | undefined {
|
): ToolCallDetail {
|
||||||
const parsed = OpencodeKnownToolDetailSchema.safeParse({
|
const parsed = OpencodeKnownToolDetailSchema.safeParse({
|
||||||
toolName,
|
toolName,
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
});
|
});
|
||||||
if (!parsed.success) {
|
if (parsed.success && parsed.data) {
|
||||||
return undefined;
|
return parsed.data;
|
||||||
}
|
}
|
||||||
return parsed.data;
|
return {
|
||||||
|
type: "unknown",
|
||||||
|
input: input ?? null,
|
||||||
|
output: output ?? null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,8 +194,8 @@ describe("opencode tool-call mapper", () => {
|
|||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
expect(item.detail).toEqual({
|
expect(item.detail).toEqual({
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: { foo: "bar" },
|
input: { foo: "bar" },
|
||||||
rawOutput: { ok: true },
|
output: { ok: true },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -81,10 +81,8 @@ export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTim
|
|||||||
callId,
|
callId,
|
||||||
name: parsedParams.toolName,
|
name: parsedParams.toolName,
|
||||||
status: "failed",
|
status: "failed",
|
||||||
input,
|
detail,
|
||||||
output,
|
|
||||||
error: parsedParams.error ?? { message: "Tool call failed" },
|
error: parsedParams.error ?? { message: "Tool call failed" },
|
||||||
...(detail ? { detail } : {}),
|
|
||||||
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
|
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -94,10 +92,8 @@ export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTim
|
|||||||
callId,
|
callId,
|
||||||
name: parsedParams.toolName,
|
name: parsedParams.toolName,
|
||||||
status,
|
status,
|
||||||
input,
|
detail,
|
||||||
output,
|
|
||||||
error: null,
|
error: null,
|
||||||
...(detail ? { detail } : {}),
|
|
||||||
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
|
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,36 @@ describe("serializeAgentStreamEvent", () => {
|
|||||||
expect(serialized.item.error).toBeNull();
|
expect(serialized.item.error).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("passes unknown-detail tool_call payloads through unchanged", () => {
|
||||||
|
const event: AgentStreamEvent = {
|
||||||
|
type: "timeline",
|
||||||
|
provider: "codex",
|
||||||
|
item: {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "call_unknown",
|
||||||
|
name: "paseo_voice.speak",
|
||||||
|
status: "completed",
|
||||||
|
detail: {
|
||||||
|
type: "unknown",
|
||||||
|
input: { text: "hello" },
|
||||||
|
output: { ok: true },
|
||||||
|
},
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const serialized = serializeAgentStreamEvent(event);
|
||||||
|
expect(serialized).not.toBeNull();
|
||||||
|
if (!serialized || serialized.type !== "timeline" || serialized.item.type !== "tool_call") {
|
||||||
|
throw new Error("Expected timeline.tool_call event");
|
||||||
|
}
|
||||||
|
expect(serialized.item.detail).toEqual({
|
||||||
|
type: "unknown",
|
||||||
|
input: { text: "hello" },
|
||||||
|
output: { ok: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("drops invalid legacy tool_call items", () => {
|
test("drops invalid legacy tool_call items", () => {
|
||||||
const event = {
|
const event = {
|
||||||
type: "timeline",
|
type: "timeline",
|
||||||
@@ -61,8 +91,8 @@ describe("serializeAgentStreamEvent", () => {
|
|||||||
status: "inProgress",
|
status: "inProgress",
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: { command: "pwd" },
|
input: { command: "pwd" },
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} satisfies unknown;
|
} satisfies unknown;
|
||||||
|
|||||||
@@ -2092,11 +2092,11 @@ export class Session {
|
|||||||
status: "running",
|
status: "running",
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: {
|
input: {
|
||||||
worktreePath: worktree.worktreePath,
|
worktreePath: worktree.worktreePath,
|
||||||
branchName: worktree.branchName,
|
branchName: worktree.branchName,
|
||||||
},
|
},
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
@@ -2117,11 +2117,11 @@ export class Session {
|
|||||||
status: "completed",
|
status: "completed",
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: {
|
input: {
|
||||||
worktreePath: worktree.worktreePath,
|
worktreePath: worktree.worktreePath,
|
||||||
branchName: worktree.branchName,
|
branchName: worktree.branchName,
|
||||||
},
|
},
|
||||||
rawOutput: {
|
output: {
|
||||||
worktreePath: worktree.worktreePath,
|
worktreePath: worktree.worktreePath,
|
||||||
commands: results.map((result) => ({
|
commands: results.map((result) => ({
|
||||||
command: result.command,
|
command: result.command,
|
||||||
@@ -2145,11 +2145,11 @@ export class Session {
|
|||||||
status: "failed",
|
status: "failed",
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: {
|
input: {
|
||||||
worktreePath: worktree.worktreePath,
|
worktreePath: worktree.worktreePath,
|
||||||
branchName: worktree.branchName,
|
branchName: worktree.branchName,
|
||||||
},
|
},
|
||||||
rawOutput: {
|
output: {
|
||||||
worktreePath: worktree.worktreePath,
|
worktreePath: worktree.worktreePath,
|
||||||
commands: results.map((result) => ({
|
commands: results.map((result) => ({
|
||||||
command: result.command,
|
command: result.command,
|
||||||
|
|||||||
@@ -248,8 +248,8 @@ class FakeAgentSession implements AgentSession {
|
|||||||
status: "running",
|
status: "running",
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: tool.input ?? null,
|
input: tool.input ?? null,
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
@@ -332,8 +332,8 @@ class FakeAgentSession implements AgentSession {
|
|||||||
status: "completed",
|
status: "completed",
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: tool.input ?? null,
|
input: tool.input ?? null,
|
||||||
rawOutput: toolOutput ?? { ok: true },
|
output: toolOutput ?? { ok: true },
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,8 +64,17 @@ describe("shared messages tool_call schema", () => {
|
|||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const withTopLevelInputOutput = AgentTimelineItemPayloadSchema.safeParse({
|
||||||
|
...canonicalBase(),
|
||||||
|
status: "running",
|
||||||
|
error: null,
|
||||||
|
input: { command: "pwd" },
|
||||||
|
output: { exitCode: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
expect(missingCallId.success).toBe(false);
|
expect(missingCallId.success).toBe(false);
|
||||||
expect(unknownStatus.success).toBe(false);
|
expect(unknownStatus.success).toBe(false);
|
||||||
|
expect(withTopLevelInputOutput.success).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects legacy status/error combinations without normalization", () => {
|
it("rejects legacy status/error combinations without normalization", () => {
|
||||||
|
|||||||
@@ -190,8 +190,8 @@ const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail> = z.discriminatedUn
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
type: z.literal("unknown"),
|
type: z.literal("unknown"),
|
||||||
rawInput: UnknownValueSchema,
|
input: UnknownValueSchema,
|
||||||
rawOutput: UnknownValueSchema,
|
output: UnknownValueSchema,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ const ToolCallBasePayloadSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
detail: ToolCallDetailPayloadSchema,
|
detail: ToolCallDetailPayloadSchema,
|
||||||
metadata: z.record(z.unknown()).optional(),
|
metadata: z.record(z.unknown()).optional(),
|
||||||
});
|
}).strict();
|
||||||
|
|
||||||
const ToolCallRunningPayloadSchema = ToolCallBasePayloadSchema.extend({
|
const ToolCallRunningPayloadSchema = ToolCallBasePayloadSchema.extend({
|
||||||
status: z.literal("running"),
|
status: z.literal("running"),
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ describe("shared tool-call display mapping", () => {
|
|||||||
error: null,
|
error: null,
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: { command: "npm test" },
|
input: { command: "npm test" },
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,8 +45,8 @@ describe("shared tool-call display mapping", () => {
|
|||||||
error: null,
|
error: null,
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: null,
|
input: null,
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
subAgentActivity: "Running tests",
|
subAgentActivity: "Running tests",
|
||||||
@@ -66,8 +66,8 @@ describe("shared tool-call display mapping", () => {
|
|||||||
error: { message: "boom" },
|
error: { message: "boom" },
|
||||||
detail: {
|
detail: {
|
||||||
type: "unknown",
|
type: "unknown",
|
||||||
rawInput: null,
|
input: null,
|
||||||
rawOutput: null,
|
output: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user