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