Merge branch 'main' of github.com:getpaseo/paseo
This commit is contained in:
@@ -177,7 +177,7 @@ export function ToolCallDetailsContent({
|
|||||||
if (plainInputText !== null) {
|
if (plainInputText !== null) {
|
||||||
sections.push(
|
sections.push(
|
||||||
<View key="unknown-plain-text" style={styles.plainTextSection}>
|
<View key="unknown-plain-text" style={styles.plainTextSection}>
|
||||||
<Text selectable style={styles.scrollText}>{plainInputText}</Text>
|
<Text selectable style={styles.plainText}>{plainInputText}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -298,6 +298,13 @@ const styles = StyleSheet.create((theme) => {
|
|||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
padding: theme.spacing[3],
|
padding: theme.spacing[3],
|
||||||
},
|
},
|
||||||
|
plainText: {
|
||||||
|
fontFamily: Fonts.sans,
|
||||||
|
fontSize: theme.fontSize.base,
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
lineHeight: 22,
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
import { Bot, Brain, Eye, Pencil, Search, SquareTerminal, Wrench } from "lucide-react-native";
|
import {
|
||||||
|
Bot,
|
||||||
|
Brain,
|
||||||
|
Eye,
|
||||||
|
MicVocal,
|
||||||
|
Pencil,
|
||||||
|
Search,
|
||||||
|
SquareTerminal,
|
||||||
|
Wrench,
|
||||||
|
} from "lucide-react-native";
|
||||||
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
|
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
|
||||||
|
|
||||||
export type ToolCallIconComponent = ComponentType<{ size?: number; color?: string }>;
|
export type ToolCallIconComponent = ComponentType<{ size?: number; color?: string }>;
|
||||||
@@ -20,6 +29,9 @@ export function resolveToolCallIcon(toolName: string, detail?: ToolCallDetail):
|
|||||||
if (lowerName === "thinking" && (!detail || detail.type === "unknown")) {
|
if (lowerName === "thinking" && (!detail || detail.type === "unknown")) {
|
||||||
return Brain;
|
return Brain;
|
||||||
}
|
}
|
||||||
|
if (lowerName === "speak") {
|
||||||
|
return MicVocal;
|
||||||
|
}
|
||||||
|
|
||||||
if (detail) {
|
if (detail) {
|
||||||
return TOOL_DETAIL_ICONS[detail.type];
|
return TOOL_DETAIL_ICONS[detail.type];
|
||||||
|
|||||||
@@ -1300,16 +1300,21 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
this.activeSidechains.set(parentToolUseId, toolName);
|
this.activeSidechains.set(parentToolUseId, toolName);
|
||||||
|
|
||||||
|
const toolCall = mapClaudeRunningToolCall({
|
||||||
|
name: "Task",
|
||||||
|
callId: parentToolUseId,
|
||||||
|
input: null,
|
||||||
|
output: null,
|
||||||
|
metadata: { subAgentActivity: toolName },
|
||||||
|
});
|
||||||
|
if (!toolCall) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: "timeline",
|
type: "timeline",
|
||||||
item: mapClaudeRunningToolCall({
|
item: toolCall,
|
||||||
name: "Task",
|
|
||||||
callId: parentToolUseId,
|
|
||||||
input: null,
|
|
||||||
output: null,
|
|
||||||
metadata: { subAgentActivity: toolName },
|
|
||||||
}),
|
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -1497,14 +1502,14 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
if (toolName === "ExitPlanMode" && typeof input.plan === "string") {
|
if (toolName === "ExitPlanMode" && typeof input.plan === "string") {
|
||||||
metadata.planText = input.plan;
|
metadata.planText = input.plan;
|
||||||
}
|
}
|
||||||
const detail =
|
const toolDetail =
|
||||||
kind === "tool"
|
kind === "tool"
|
||||||
? mapClaudeRunningToolCall({
|
? mapClaudeRunningToolCall({
|
||||||
name: toolName,
|
name: toolName,
|
||||||
callId: options.toolUseID ?? requestId,
|
callId: options.toolUseID ?? requestId,
|
||||||
input,
|
input,
|
||||||
output: null,
|
output: null,
|
||||||
}).detail
|
})?.detail
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const request: AgentPermissionRequest = {
|
const request: AgentPermissionRequest = {
|
||||||
@@ -1513,7 +1518,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
name: toolName,
|
name: toolName,
|
||||||
kind,
|
kind,
|
||||||
input,
|
input,
|
||||||
detail,
|
detail: toolDetail,
|
||||||
suggestions: options.suggestions?.map((suggestion) => ({ ...suggestion })),
|
suggestions: options.suggestions?.map((suggestion) => ({ ...suggestion })),
|
||||||
metadata: Object.keys(metadata).length ? metadata : undefined,
|
metadata: Object.keys(metadata).length ? metadata : undefined,
|
||||||
};
|
};
|
||||||
@@ -1591,9 +1596,12 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private pushToolCall(
|
private pushToolCall(
|
||||||
item: Extract<AgentTimelineItem, { type: "tool_call" }>,
|
item: Extract<AgentTimelineItem, { type: "tool_call" }> | null,
|
||||||
target?: AgentTimelineItem[]
|
target?: AgentTimelineItem[]
|
||||||
) {
|
) {
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (target) {
|
if (target) {
|
||||||
target.push(item);
|
target.push(item);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -19,15 +19,58 @@ import {
|
|||||||
toolDetailBranchByName,
|
toolDetailBranchByName,
|
||||||
} from "../tool-call-detail-primitives.js";
|
} from "../tool-call-detail-primitives.js";
|
||||||
|
|
||||||
const ClaudeKnownToolDetailSchema = z.union([
|
const ClaudeToolEnvelopeSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
input: z.unknown().nullable(),
|
||||||
|
output: z.unknown().nullable(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
const ClaudeSpeakToolDetailSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.literal("speak"),
|
||||||
|
input: z
|
||||||
|
.union([
|
||||||
|
z.string().transform((text) => ({ text })),
|
||||||
|
z.object({ text: z.string() }).passthrough(),
|
||||||
|
])
|
||||||
|
.nullable(),
|
||||||
|
output: z.unknown().nullable(),
|
||||||
|
})
|
||||||
|
.transform(({ input }) => {
|
||||||
|
const text = input?.text?.trim() ?? "";
|
||||||
|
if (!text) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "unknown",
|
||||||
|
input: text,
|
||||||
|
output: null,
|
||||||
|
} satisfies ToolCallDetail;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ClaudeToolDetailPass2Schema = z.union([
|
||||||
toolDetailBranchByName("Bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
toolDetailBranchByName("Bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
||||||
toolDetailBranchByName("bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
toolDetailBranchByName("bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
||||||
toolDetailBranchByName("shell", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
toolDetailBranchByName("shell", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
||||||
toolDetailBranchByName("exec_command", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
toolDetailBranchByName("exec_command", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
||||||
toolDetailBranchByName("Read", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail),
|
toolDetailBranchByName("Read", ToolReadInputSchema, z.unknown(), (input, output) => {
|
||||||
toolDetailBranchByName("read", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail),
|
const parsedOutput = ToolReadOutputSchema.safeParse(output);
|
||||||
toolDetailBranchByName("read_file", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail),
|
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
|
||||||
toolDetailBranchByName("view_file", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail),
|
}),
|
||||||
|
toolDetailBranchByName("read", ToolReadInputSchema, z.unknown(), (input, output) => {
|
||||||
|
const parsedOutput = ToolReadOutputSchema.safeParse(output);
|
||||||
|
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
|
||||||
|
}),
|
||||||
|
toolDetailBranchByName("read_file", ToolReadInputSchema, z.unknown(), (input, output) => {
|
||||||
|
const parsedOutput = ToolReadOutputSchema.safeParse(output);
|
||||||
|
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
|
||||||
|
}),
|
||||||
|
toolDetailBranchByName("view_file", ToolReadInputSchema, z.unknown(), (input, output) => {
|
||||||
|
const parsedOutput = ToolReadOutputSchema.safeParse(output);
|
||||||
|
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
|
||||||
|
}),
|
||||||
toolDetailBranchByName("Write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
toolDetailBranchByName("Write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
||||||
toolDetailBranchByName("write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
toolDetailBranchByName("write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
||||||
toolDetailBranchByName("write_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
toolDetailBranchByName("write_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
||||||
@@ -38,7 +81,12 @@ const ClaudeKnownToolDetailSchema = z.union([
|
|||||||
toolDetailBranchByName("edit", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail),
|
toolDetailBranchByName("edit", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail),
|
||||||
toolDetailBranchByName("apply_patch", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail),
|
toolDetailBranchByName("apply_patch", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail),
|
||||||
toolDetailBranchByName("apply_diff", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail),
|
toolDetailBranchByName("apply_diff", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail),
|
||||||
toolDetailBranchByName("str_replace_editor", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail),
|
toolDetailBranchByName(
|
||||||
|
"str_replace_editor",
|
||||||
|
ToolEditInputSchema,
|
||||||
|
ToolEditOutputSchema,
|
||||||
|
toEditToolDetail
|
||||||
|
),
|
||||||
toolDetailBranchByName("WebSearch", ToolSearchInputSchema, z.unknown(), (input) =>
|
toolDetailBranchByName("WebSearch", ToolSearchInputSchema, z.unknown(), (input) =>
|
||||||
toSearchToolDetail(input)
|
toSearchToolDetail(input)
|
||||||
),
|
),
|
||||||
@@ -48,6 +96,7 @@ const ClaudeKnownToolDetailSchema = z.union([
|
|||||||
toolDetailBranchByName("search", ToolSearchInputSchema, z.unknown(), (input) =>
|
toolDetailBranchByName("search", ToolSearchInputSchema, z.unknown(), (input) =>
|
||||||
toSearchToolDetail(input)
|
toSearchToolDetail(input)
|
||||||
),
|
),
|
||||||
|
ClaudeSpeakToolDetailSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function deriveClaudeToolDetail(
|
export function deriveClaudeToolDetail(
|
||||||
@@ -55,17 +104,27 @@ export function deriveClaudeToolDetail(
|
|||||||
input: unknown,
|
input: unknown,
|
||||||
output: unknown
|
output: unknown
|
||||||
): ToolCallDetail {
|
): ToolCallDetail {
|
||||||
const parsed = ClaudeKnownToolDetailSchema.safeParse({
|
const pass1 = ClaudeToolEnvelopeSchema.safeParse({
|
||||||
name,
|
name,
|
||||||
input,
|
|
||||||
output,
|
|
||||||
});
|
|
||||||
if (parsed.success && parsed.data) {
|
|
||||||
return parsed.data;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: "unknown",
|
|
||||||
input: input ?? null,
|
input: input ?? null,
|
||||||
output: output ?? null,
|
output: output ?? null,
|
||||||
|
});
|
||||||
|
if (!pass1.success) {
|
||||||
|
return {
|
||||||
|
type: "unknown",
|
||||||
|
input: input ?? null,
|
||||||
|
output: output ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass2 = ClaudeToolDetailPass2Schema.safeParse(pass1.data);
|
||||||
|
if (pass2.success && pass2.data) {
|
||||||
|
return pass2.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "unknown",
|
||||||
|
input: pass1.data.input,
|
||||||
|
output: pass1.data.output,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,24 @@ import {
|
|||||||
mapClaudeRunningToolCall,
|
mapClaudeRunningToolCall,
|
||||||
} from "./tool-call-mapper.js";
|
} from "./tool-call-mapper.js";
|
||||||
|
|
||||||
|
function expectMapped<T>(item: T | null): T {
|
||||||
|
expect(item).toBeTruthy();
|
||||||
|
if (!item) {
|
||||||
|
throw new Error("Expected mapped tool call");
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
describe("claude tool-call mapper", () => {
|
describe("claude tool-call mapper", () => {
|
||||||
it("maps running shell calls with canonical fields", () => {
|
it("maps running shell calls with canonical fields", () => {
|
||||||
const item = mapClaudeRunningToolCall({
|
const item = expectMapped(
|
||||||
callId: "claude-call-1",
|
mapClaudeRunningToolCall({
|
||||||
name: "Bash",
|
callId: "claude-call-1",
|
||||||
input: { command: "pwd", cwd: "/tmp/repo" },
|
name: "Bash",
|
||||||
output: null,
|
input: { command: "pwd", cwd: "/tmp/repo" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.type).toBe("tool_call");
|
expect(item.type).toBe("tool_call");
|
||||||
expect(item.status).toBe("running");
|
expect(item.status).toBe("running");
|
||||||
@@ -27,45 +37,53 @@ describe("claude tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps running known tool variants with detail for early summaries", () => {
|
it("maps running known tool variants with detail for early summaries", () => {
|
||||||
const readItem = mapClaudeRunningToolCall({
|
const readItem = expectMapped(
|
||||||
callId: "claude-running-read",
|
mapClaudeRunningToolCall({
|
||||||
name: "read_file",
|
callId: "claude-running-read",
|
||||||
input: { file_path: "README.md" },
|
name: "read_file",
|
||||||
output: null,
|
input: { file_path: "README.md" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(readItem.detail).toEqual({
|
expect(readItem.detail).toEqual({
|
||||||
type: "read",
|
type: "read",
|
||||||
filePath: "README.md",
|
filePath: "README.md",
|
||||||
});
|
});
|
||||||
|
|
||||||
const writeItem = mapClaudeRunningToolCall({
|
const writeItem = expectMapped(
|
||||||
callId: "claude-running-write",
|
mapClaudeRunningToolCall({
|
||||||
name: "write_file",
|
callId: "claude-running-write",
|
||||||
input: { file_path: "src/new.ts" },
|
name: "write_file",
|
||||||
output: null,
|
input: { file_path: "src/new.ts" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(writeItem.detail).toEqual({
|
expect(writeItem.detail).toEqual({
|
||||||
type: "write",
|
type: "write",
|
||||||
filePath: "src/new.ts",
|
filePath: "src/new.ts",
|
||||||
});
|
});
|
||||||
|
|
||||||
const editItem = mapClaudeRunningToolCall({
|
const editItem = expectMapped(
|
||||||
callId: "claude-running-edit",
|
mapClaudeRunningToolCall({
|
||||||
name: "apply_patch",
|
callId: "claude-running-edit",
|
||||||
input: { file_path: "src/index.ts" },
|
name: "apply_patch",
|
||||||
output: null,
|
input: { file_path: "src/index.ts" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(editItem.detail).toEqual({
|
expect(editItem.detail).toEqual({
|
||||||
type: "edit",
|
type: "edit",
|
||||||
filePath: "src/index.ts",
|
filePath: "src/index.ts",
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchItem = mapClaudeRunningToolCall({
|
const searchItem = expectMapped(
|
||||||
callId: "claude-running-search",
|
mapClaudeRunningToolCall({
|
||||||
name: "web_search",
|
callId: "claude-running-search",
|
||||||
input: { query: "tool call mapping" },
|
name: "web_search",
|
||||||
output: null,
|
input: { query: "tool call mapping" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(searchItem.detail).toEqual({
|
expect(searchItem.detail).toEqual({
|
||||||
type: "search",
|
type: "search",
|
||||||
query: "tool call mapping",
|
query: "tool call mapping",
|
||||||
@@ -73,12 +91,14 @@ describe("claude tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps completed read calls with detail enrichment", () => {
|
it("maps completed read calls with detail enrichment", () => {
|
||||||
const item = mapClaudeCompletedToolCall({
|
const item = expectMapped(
|
||||||
callId: "claude-call-2",
|
mapClaudeCompletedToolCall({
|
||||||
name: "read_file",
|
callId: "claude-call-2",
|
||||||
input: { file_path: "README.md" },
|
name: "read_file",
|
||||||
output: { content: "hello" },
|
input: { file_path: "README.md" },
|
||||||
});
|
output: { content: "hello" },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.status).toBe("completed");
|
expect(item.status).toBe("completed");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
@@ -91,33 +111,37 @@ describe("claude tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("preserves read content from array/object output variants", () => {
|
it("preserves read content from array/object output variants", () => {
|
||||||
const arrayContent = mapClaudeCompletedToolCall({
|
const arrayContent = expectMapped(
|
||||||
callId: "claude-read-array",
|
mapClaudeCompletedToolCall({
|
||||||
name: "read_file",
|
callId: "claude-read-array",
|
||||||
input: { file_path: "README.md" },
|
name: "read_file",
|
||||||
output: {
|
input: { file_path: "README.md" },
|
||||||
content: [
|
output: {
|
||||||
{ type: "output_text", text: "alpha" },
|
content: [
|
||||||
{ type: "output_text", content: "beta" },
|
{ type: "output_text", text: "alpha" },
|
||||||
],
|
{ type: "output_text", content: "beta" },
|
||||||
},
|
],
|
||||||
});
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(arrayContent.detail?.type).toBe("read");
|
expect(arrayContent.detail?.type).toBe("read");
|
||||||
if (arrayContent.detail?.type === "read") {
|
if (arrayContent.detail?.type === "read") {
|
||||||
expect(arrayContent.detail.content).toBe("alpha\nbeta");
|
expect(arrayContent.detail.content).toBe("alpha\nbeta");
|
||||||
}
|
}
|
||||||
|
|
||||||
const objectContent = mapClaudeCompletedToolCall({
|
const objectContent = expectMapped(
|
||||||
callId: "claude-read-object",
|
mapClaudeCompletedToolCall({
|
||||||
name: "read_file",
|
callId: "claude-read-object",
|
||||||
input: { file_path: "README.md" },
|
name: "read_file",
|
||||||
output: {
|
input: { file_path: "README.md" },
|
||||||
structured_content: {
|
output: {
|
||||||
content: { type: "output_text", text: "gamma" },
|
structured_content: {
|
||||||
|
content: { type: "output_text", text: "gamma" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
});
|
);
|
||||||
|
|
||||||
expect(objectContent.detail?.type).toBe("read");
|
expect(objectContent.detail?.type).toBe("read");
|
||||||
if (objectContent.detail?.type === "read") {
|
if (objectContent.detail?.type === "read") {
|
||||||
@@ -126,13 +150,15 @@ describe("claude tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps failed calls with required error", () => {
|
it("maps failed calls with required error", () => {
|
||||||
const item = mapClaudeFailedToolCall({
|
const item = expectMapped(
|
||||||
callId: "claude-call-3",
|
mapClaudeFailedToolCall({
|
||||||
name: "shell",
|
callId: "claude-call-3",
|
||||||
input: { command: "false" },
|
name: "shell",
|
||||||
output: null,
|
input: { command: "false" },
|
||||||
error: { message: "Command failed" },
|
output: null,
|
||||||
});
|
error: { message: "Command failed" },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.status).toBe("failed");
|
expect(item.status).toBe("failed");
|
||||||
expect(item.error).toEqual({ message: "Command failed" });
|
expect(item.error).toEqual({ message: "Command failed" });
|
||||||
@@ -140,35 +166,41 @@ describe("claude tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps write/edit/search known shapes with distinct detail types", () => {
|
it("maps write/edit/search known shapes with distinct detail types", () => {
|
||||||
const writeItem = mapClaudeCompletedToolCall({
|
const writeItem = expectMapped(
|
||||||
callId: "claude-write-1",
|
mapClaudeCompletedToolCall({
|
||||||
name: "write_file",
|
callId: "claude-write-1",
|
||||||
input: { file_path: "src/new.ts", content: "export const x = 1;" },
|
name: "write_file",
|
||||||
output: null,
|
input: { file_path: "src/new.ts", content: "export const x = 1;" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(writeItem.detail?.type).toBe("write");
|
expect(writeItem.detail?.type).toBe("write");
|
||||||
if (writeItem.detail?.type === "write") {
|
if (writeItem.detail?.type === "write") {
|
||||||
expect(writeItem.detail.filePath).toBe("src/new.ts");
|
expect(writeItem.detail.filePath).toBe("src/new.ts");
|
||||||
}
|
}
|
||||||
|
|
||||||
const editItem = mapClaudeCompletedToolCall({
|
const editItem = expectMapped(
|
||||||
callId: "claude-edit-1",
|
mapClaudeCompletedToolCall({
|
||||||
name: "apply_patch",
|
callId: "claude-edit-1",
|
||||||
input: { file_path: "src/index.ts", patch: "@@\\n-old\\n+new\\n" },
|
name: "apply_patch",
|
||||||
output: null,
|
input: { file_path: "src/index.ts", patch: "@@\\n-old\\n+new\\n" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(editItem.detail?.type).toBe("edit");
|
expect(editItem.detail?.type).toBe("edit");
|
||||||
if (editItem.detail?.type === "edit") {
|
if (editItem.detail?.type === "edit") {
|
||||||
expect(editItem.detail.filePath).toBe("src/index.ts");
|
expect(editItem.detail.filePath).toBe("src/index.ts");
|
||||||
expect(editItem.detail.unifiedDiff).toContain("@@");
|
expect(editItem.detail.unifiedDiff).toContain("@@");
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchItem = mapClaudeCompletedToolCall({
|
const searchItem = expectMapped(
|
||||||
callId: "claude-search-1",
|
mapClaudeCompletedToolCall({
|
||||||
name: "web_search",
|
callId: "claude-search-1",
|
||||||
input: { query: "tool call mapping" },
|
name: "web_search",
|
||||||
output: null,
|
input: { query: "tool call mapping" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(searchItem.detail).toEqual({
|
expect(searchItem.detail).toEqual({
|
||||||
type: "search",
|
type: "search",
|
||||||
query: "tool call mapping",
|
query: "tool call mapping",
|
||||||
@@ -176,12 +208,14 @@ describe("claude tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps unknown tools to unknown detail with raw payloads", () => {
|
it("maps unknown tools to unknown detail with raw payloads", () => {
|
||||||
const item = mapClaudeCompletedToolCall({
|
const item = expectMapped(
|
||||||
callId: "claude-call-4",
|
mapClaudeCompletedToolCall({
|
||||||
name: "my_custom_tool",
|
callId: "claude-call-4",
|
||||||
input: { foo: "bar" },
|
name: "my_custom_tool",
|
||||||
output: { ok: true },
|
input: { foo: "bar" },
|
||||||
});
|
output: { ok: true },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.status).toBe("completed");
|
expect(item.status).toBe("completed");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
@@ -191,4 +225,33 @@ describe("claude tool-call mapper", () => {
|
|||||||
output: { ok: true },
|
output: { ok: true },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes claude speak tool names through schema transforms", () => {
|
||||||
|
const item = expectMapped(
|
||||||
|
mapClaudeCompletedToolCall({
|
||||||
|
callId: "claude-speak-1",
|
||||||
|
name: "mcp__paseo__speak",
|
||||||
|
input: { text: "Voice response from Claude." },
|
||||||
|
output: { ok: true },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(item.name).toBe("speak");
|
||||||
|
expect(item.detail).toEqual({
|
||||||
|
type: "unknown",
|
||||||
|
input: "Voice response from Claude.",
|
||||||
|
output: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops tool calls when callId is missing", () => {
|
||||||
|
const item = mapClaudeCompletedToolCall({
|
||||||
|
callId: null,
|
||||||
|
name: "read_file",
|
||||||
|
input: { file_path: "README.md" },
|
||||||
|
output: { content: "hello" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(item).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { 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";
|
import { deriveClaudeToolDetail } from "./tool-call-detail-parser.js";
|
||||||
|
|
||||||
type MapperParams = {
|
type MapperParams = {
|
||||||
@@ -12,87 +11,170 @@ type MapperParams = {
|
|||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClaudeMapperParamsSchema = z
|
const ClaudeToolCallStatusSchema = z.enum([
|
||||||
|
"running",
|
||||||
|
"completed",
|
||||||
|
"failed",
|
||||||
|
"canceled",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ClaudeRawToolCallSchema = z
|
||||||
.object({
|
.object({
|
||||||
callId: z.string().optional().nullable(),
|
callId: z.string().optional().nullable(),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
input: z.unknown().optional(),
|
input: z.unknown().optional(),
|
||||||
output: z.unknown().optional(),
|
output: z.unknown().optional(),
|
||||||
metadata: z.record(z.unknown()).optional(),
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
error: z.unknown().nullable().optional(),
|
||||||
|
status: ClaudeToolCallStatusSchema,
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
const ClaudeFailedMapperParamsSchema = ClaudeMapperParamsSchema.extend({
|
const ClaudeToolCallPass1Schema = ClaudeRawToolCallSchema.transform((raw) => ({
|
||||||
error: z.unknown(),
|
callId:
|
||||||
|
typeof raw.callId === "string" && raw.callId.trim().length > 0
|
||||||
|
? raw.callId
|
||||||
|
: null,
|
||||||
|
name: raw.name.trim(),
|
||||||
|
input: raw.input ?? null,
|
||||||
|
output: raw.output ?? null,
|
||||||
|
metadata: raw.metadata,
|
||||||
|
error: raw.error ?? null,
|
||||||
|
status: raw.status,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ClaudeToolCallPass2BaseSchema = z.object({
|
||||||
|
callId: z.string().min(1),
|
||||||
|
name: z.string().min(1),
|
||||||
|
input: z.unknown().nullable(),
|
||||||
|
output: z.unknown().nullable(),
|
||||||
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
error: z.unknown().nullable(),
|
||||||
|
status: ClaudeToolCallStatusSchema,
|
||||||
|
toolKind: z.enum(["speak", "other"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
function coerceCallId(callId: string | null | undefined, name: string, input: unknown): string {
|
const ClaudeToolCallPass2InputSchema = ClaudeToolCallPass2BaseSchema.omit({
|
||||||
return coerceToolCallId({
|
toolKind: true,
|
||||||
providerPrefix: "claude",
|
});
|
||||||
rawCallId: callId,
|
|
||||||
toolName: name,
|
const ClaudeToolCallPass2EnvelopeSchema = z.union([
|
||||||
input,
|
ClaudeToolCallPass2InputSchema.extend({
|
||||||
|
name: z.literal("mcp__paseo__speak"),
|
||||||
|
}).transform((normalized) => ({
|
||||||
|
...normalized,
|
||||||
|
name: normalized.name.trim(),
|
||||||
|
toolKind: "speak" as const,
|
||||||
|
})),
|
||||||
|
ClaudeToolCallPass2InputSchema.transform((normalized) => ({
|
||||||
|
...normalized,
|
||||||
|
name: normalized.name.trim(),
|
||||||
|
toolKind: "other" as const,
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ClaudeToolCallPass2Schema = z.discriminatedUnion("toolKind", [
|
||||||
|
ClaudeToolCallPass2BaseSchema.extend({
|
||||||
|
toolKind: z.literal("speak"),
|
||||||
|
name: z.literal("mcp__paseo__speak"),
|
||||||
|
}).transform((normalized): ToolCallTimelineItem => {
|
||||||
|
const name = "speak" as const;
|
||||||
|
const detail = deriveClaudeToolDetail(name, normalized.input, normalized.output);
|
||||||
|
if (normalized.status === "failed") {
|
||||||
|
return {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: normalized.callId,
|
||||||
|
name,
|
||||||
|
detail,
|
||||||
|
status: "failed",
|
||||||
|
error: normalized.error ?? { message: "Tool call failed" },
|
||||||
|
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: normalized.callId,
|
||||||
|
name,
|
||||||
|
detail,
|
||||||
|
status: normalized.status,
|
||||||
|
error: null,
|
||||||
|
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
ClaudeToolCallPass2BaseSchema.extend({
|
||||||
|
toolKind: z.literal("other"),
|
||||||
|
}).transform((normalized): ToolCallTimelineItem => {
|
||||||
|
const detail = deriveClaudeToolDetail(normalized.name, normalized.input, normalized.output);
|
||||||
|
if (normalized.status === "failed") {
|
||||||
|
return {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: normalized.callId,
|
||||||
|
name: normalized.name,
|
||||||
|
detail,
|
||||||
|
status: "failed",
|
||||||
|
error: normalized.error ?? { message: "Tool call failed" },
|
||||||
|
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: normalized.callId,
|
||||||
|
name: normalized.name,
|
||||||
|
detail,
|
||||||
|
status: normalized.status,
|
||||||
|
error: null,
|
||||||
|
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
function mapClaudeToolCall(
|
||||||
|
params: MapperParams,
|
||||||
|
status: z.infer<typeof ClaudeToolCallStatusSchema>,
|
||||||
|
error: unknown | null
|
||||||
|
): ToolCallTimelineItem | null {
|
||||||
|
const pass1 = ClaudeToolCallPass1Schema.safeParse({
|
||||||
|
...params,
|
||||||
|
status,
|
||||||
|
error,
|
||||||
});
|
});
|
||||||
|
if (!pass1.success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass2Envelope = ClaudeToolCallPass2EnvelopeSchema.safeParse(pass1.data);
|
||||||
|
if (!pass2Envelope.success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass2 = ClaudeToolCallPass2Schema.safeParse(pass2Envelope.data);
|
||||||
|
if (!pass2.success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pass2.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildBase(params: MapperParams): {
|
export function mapClaudeRunningToolCall(
|
||||||
callId: string;
|
params: MapperParams
|
||||||
name: string;
|
): ToolCallTimelineItem | null {
|
||||||
detail: Extract<ToolCallTimelineItem, { type: "tool_call" }>["detail"];
|
return mapClaudeToolCall(params, "running", null);
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
} {
|
|
||||||
const parsedParams = ClaudeMapperParamsSchema.parse(params);
|
|
||||||
const input = parsedParams.input ?? null;
|
|
||||||
const output = parsedParams.output ?? null;
|
|
||||||
const detail = deriveClaudeToolDetail(parsedParams.name, input, output);
|
|
||||||
|
|
||||||
return {
|
|
||||||
callId: coerceCallId(parsedParams.callId, parsedParams.name, input),
|
|
||||||
name: parsedParams.name,
|
|
||||||
detail,
|
|
||||||
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapClaudeRunningToolCall(params: MapperParams): ToolCallTimelineItem {
|
export function mapClaudeCompletedToolCall(
|
||||||
const base = buildBase(params);
|
params: MapperParams
|
||||||
return {
|
): ToolCallTimelineItem | null {
|
||||||
type: "tool_call",
|
return mapClaudeToolCall(params, "completed", null);
|
||||||
...base,
|
|
||||||
status: "running",
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapClaudeCompletedToolCall(params: MapperParams): ToolCallTimelineItem {
|
|
||||||
const base = buildBase(params);
|
|
||||||
return {
|
|
||||||
type: "tool_call",
|
|
||||||
...base,
|
|
||||||
status: "completed",
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapClaudeFailedToolCall(
|
export function mapClaudeFailedToolCall(
|
||||||
params: MapperParams & { error: unknown }
|
params: MapperParams & { error: unknown }
|
||||||
): ToolCallTimelineItem {
|
): ToolCallTimelineItem | null {
|
||||||
const parsedParams = ClaudeFailedMapperParamsSchema.parse(params);
|
return mapClaudeToolCall(params, "failed", params.error);
|
||||||
const base = buildBase(parsedParams);
|
|
||||||
return {
|
|
||||||
type: "tool_call",
|
|
||||||
...base,
|
|
||||||
status: "failed",
|
|
||||||
error: parsedParams.error,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapClaudeCanceledToolCall(params: MapperParams): ToolCallTimelineItem {
|
export function mapClaudeCanceledToolCall(
|
||||||
const base = buildBase(params);
|
params: MapperParams
|
||||||
return {
|
): ToolCallTimelineItem | null {
|
||||||
type: "tool_call",
|
return mapClaudeToolCall(params, "canceled", null);
|
||||||
...base,
|
|
||||||
status: "canceled",
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -982,6 +982,9 @@ function mapCodexExecNotificationToToolCall(params: {
|
|||||||
: null,
|
: null,
|
||||||
cwd: params.cwd ?? null,
|
cwd: params.cwd ?? null,
|
||||||
});
|
});
|
||||||
|
if (!mapped) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return params.running ? toRunningToolCall(mapped) : mapped;
|
return params.running ? toRunningToolCall(mapped) : mapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -993,7 +996,7 @@ function mapCodexPatchNotificationToToolCall(params: {
|
|||||||
stderr?: string | null;
|
stderr?: string | null;
|
||||||
success?: boolean | null;
|
success?: boolean | null;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
}): ToolCallTimelineItem {
|
}): ToolCallTimelineItem | null {
|
||||||
const files = parseCodexPatchChanges(params.changes);
|
const files = parseCodexPatchChanges(params.changes);
|
||||||
const firstPath = files[0]?.path;
|
const firstPath = files[0]?.path;
|
||||||
const firstPatchText = files
|
const firstPatchText = files
|
||||||
@@ -1038,6 +1041,9 @@ function mapCodexPatchNotificationToToolCall(params: {
|
|||||||
: { message: params.stderr?.trim() || "Patch apply failed" },
|
: { message: params.stderr?.trim() || "Patch apply failed" },
|
||||||
cwd: params.cwd ?? null,
|
cwd: params.cwd ?? null,
|
||||||
});
|
});
|
||||||
|
if (!mapped) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return params.running ? toRunningToolCall(mapped) : mapped;
|
return params.running ? toRunningToolCall(mapped) : mapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2565,11 +2571,13 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
cwd: this.config.cwd ?? null,
|
cwd: this.config.cwd ?? null,
|
||||||
running: true,
|
running: true,
|
||||||
});
|
});
|
||||||
this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_started", {
|
if (timelineItem) {
|
||||||
callId: parsed.callId,
|
this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_started", {
|
||||||
changes: parsed.changes,
|
callId: parsed.callId,
|
||||||
});
|
changes: parsed.changes,
|
||||||
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
});
|
||||||
|
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2587,12 +2595,14 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
success: parsed.success,
|
success: parsed.success,
|
||||||
running: false,
|
running: false,
|
||||||
});
|
});
|
||||||
this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_completed", {
|
if (timelineItem) {
|
||||||
callId: parsed.callId,
|
this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_completed", {
|
||||||
changes: parsed.changes,
|
callId: parsed.callId,
|
||||||
stdout: parsed.stdout,
|
changes: parsed.changes,
|
||||||
});
|
stdout: parsed.stdout,
|
||||||
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
});
|
||||||
|
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -503,14 +503,15 @@ export async function parseRolloutFile(
|
|||||||
record.kind === "timeline"
|
record.kind === "timeline"
|
||||||
? [record.item]
|
? [record.item]
|
||||||
: record.kind === "call"
|
: record.kind === "call"
|
||||||
? [
|
? (() => {
|
||||||
mapCodexRolloutToolCall({
|
const mapped = mapCodexRolloutToolCall({
|
||||||
callId: record.callId ?? null,
|
callId: record.callId ?? null,
|
||||||
name: record.name,
|
name: record.name,
|
||||||
input: record.input ?? null,
|
input: record.input ?? null,
|
||||||
output: record.callId ? outputsByCallId.get(record.callId) ?? null : null,
|
output: record.callId ? outputsByCallId.get(record.callId) ?? null : null,
|
||||||
}),
|
});
|
||||||
]
|
return mapped ? [mapped] : [];
|
||||||
|
})()
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
return dedupeMirroredTextTimelineItems(timeline);
|
return dedupeMirroredTextTimelineItems(timeline);
|
||||||
|
|||||||
@@ -24,23 +24,43 @@ export type CodexToolDetailContext = {
|
|||||||
cwd?: string | null;
|
cwd?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CODEX_BUILTIN_TOOL_NAMES = new Set([
|
const CodexToolEnvelopeSchema = z
|
||||||
"shell",
|
.object({
|
||||||
"bash",
|
name: z.string().min(1),
|
||||||
"exec",
|
input: z.unknown().nullable(),
|
||||||
"exec_command",
|
output: z.unknown().nullable(),
|
||||||
"command",
|
cwd: z.string().nullable().optional(),
|
||||||
"read",
|
})
|
||||||
"read_file",
|
.passthrough();
|
||||||
"write",
|
|
||||||
"write_file",
|
const CodexSpeakToolDetailSchema = z
|
||||||
"create_file",
|
.object({
|
||||||
"edit",
|
name: z.literal("speak"),
|
||||||
"apply_patch",
|
input: z
|
||||||
"apply_diff",
|
.union([
|
||||||
"web_search",
|
z.string().transform((text) => ({ text })),
|
||||||
"search",
|
z.object({ text: z.string() }).passthrough(),
|
||||||
]);
|
])
|
||||||
|
.nullable(),
|
||||||
|
output: z.unknown().nullable(),
|
||||||
|
cwd: z.string().nullable().optional(),
|
||||||
|
})
|
||||||
|
.transform(({ input }) => {
|
||||||
|
const text = input?.text?.trim() ?? "";
|
||||||
|
if (!text) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "unknown",
|
||||||
|
input: text,
|
||||||
|
output: null,
|
||||||
|
} satisfies ToolCallDetail;
|
||||||
|
});
|
||||||
|
|
||||||
|
const CodexLooseEditOutputSchema = z.unknown().transform((value) => {
|
||||||
|
const parsed = ToolEditOutputSchema.safeParse(value);
|
||||||
|
return parsed.success ? parsed.data : null;
|
||||||
|
});
|
||||||
|
|
||||||
export function normalizeCodexFilePath(
|
export function normalizeCodexFilePath(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
@@ -60,7 +80,7 @@ function normalizePathForCwd(cwd: string | null): (filePath: string) => string |
|
|||||||
return (filePath) => normalizeCodexFilePath(filePath, cwd);
|
return (filePath) => normalizeCodexFilePath(filePath, cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodexKnownToolDetailSchema = z.union([
|
const CodexToolDetailPass2Schema = z.union([
|
||||||
toolDetailBranchByNameWithCwd("Bash", ToolShellInputSchema, ToolShellOutputSchema, (input, output) =>
|
toolDetailBranchByNameWithCwd("Bash", ToolShellInputSchema, ToolShellOutputSchema, (input, output) =>
|
||||||
toShellToolDetail(input, output)
|
toShellToolDetail(input, output)
|
||||||
),
|
),
|
||||||
@@ -82,14 +102,22 @@ const CodexKnownToolDetailSchema = z.union([
|
|||||||
toolDetailBranchByNameWithCwd("command", ToolShellInputSchema, ToolShellOutputSchema, (input, output) =>
|
toolDetailBranchByNameWithCwd("command", ToolShellInputSchema, ToolShellOutputSchema, (input, output) =>
|
||||||
toShellToolDetail(input, output)
|
toShellToolDetail(input, output)
|
||||||
),
|
),
|
||||||
toolDetailBranchByNameWithCwd("read", ToolReadInputSchema, ToolReadOutputWithPathSchema, (input, output, cwd) =>
|
toolDetailBranchByNameWithCwd("read", ToolReadInputSchema, z.unknown(), (input, output, cwd) => {
|
||||||
toReadToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
const parsedOutput = ToolReadOutputWithPathSchema.safeParse(output);
|
||||||
),
|
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null, {
|
||||||
|
normalizePath: normalizePathForCwd(cwd),
|
||||||
|
});
|
||||||
|
}),
|
||||||
toolDetailBranchByNameWithCwd(
|
toolDetailBranchByNameWithCwd(
|
||||||
"read_file",
|
"read_file",
|
||||||
ToolReadInputSchema,
|
ToolReadInputSchema,
|
||||||
ToolReadOutputWithPathSchema,
|
z.unknown(),
|
||||||
(input, output, cwd) => toReadToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
(input, output, cwd) => {
|
||||||
|
const parsedOutput = ToolReadOutputWithPathSchema.safeParse(output);
|
||||||
|
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null, {
|
||||||
|
normalizePath: normalizePathForCwd(cwd),
|
||||||
|
});
|
||||||
|
}
|
||||||
),
|
),
|
||||||
toolDetailBranchByNameWithCwd("write", ToolWriteInputSchema, ToolWriteOutputSchema, (input, output, cwd) =>
|
toolDetailBranchByNameWithCwd("write", ToolWriteInputSchema, ToolWriteOutputSchema, (input, output, cwd) =>
|
||||||
toWriteToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
toWriteToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
||||||
@@ -106,19 +134,19 @@ const CodexKnownToolDetailSchema = z.union([
|
|||||||
ToolWriteOutputSchema,
|
ToolWriteOutputSchema,
|
||||||
(input, output, cwd) => toWriteToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
(input, output, cwd) => toWriteToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
||||||
),
|
),
|
||||||
toolDetailBranchByNameWithCwd("edit", ToolEditInputSchema, ToolEditOutputSchema, (input, output, cwd) =>
|
toolDetailBranchByNameWithCwd("edit", ToolEditInputSchema, CodexLooseEditOutputSchema, (input, output, cwd) =>
|
||||||
toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
||||||
),
|
),
|
||||||
toolDetailBranchByNameWithCwd(
|
toolDetailBranchByNameWithCwd(
|
||||||
"apply_patch",
|
"apply_patch",
|
||||||
ToolEditInputSchema,
|
ToolEditInputSchema,
|
||||||
ToolEditOutputSchema,
|
CodexLooseEditOutputSchema,
|
||||||
(input, output, cwd) => toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
(input, output, cwd) => toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
||||||
),
|
),
|
||||||
toolDetailBranchByNameWithCwd(
|
toolDetailBranchByNameWithCwd(
|
||||||
"apply_diff",
|
"apply_diff",
|
||||||
ToolEditInputSchema,
|
ToolEditInputSchema,
|
||||||
ToolEditOutputSchema,
|
CodexLooseEditOutputSchema,
|
||||||
(input, output, cwd) => toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
(input, output, cwd) => toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
|
||||||
),
|
),
|
||||||
toolDetailBranchByNameWithCwd("search", ToolSearchInputSchema, z.unknown(), (input) =>
|
toolDetailBranchByNameWithCwd("search", ToolSearchInputSchema, z.unknown(), (input) =>
|
||||||
@@ -127,6 +155,7 @@ const CodexKnownToolDetailSchema = z.union([
|
|||||||
toolDetailBranchByNameWithCwd("web_search", ToolSearchInputSchema, z.unknown(), (input) =>
|
toolDetailBranchByNameWithCwd("web_search", ToolSearchInputSchema, z.unknown(), (input) =>
|
||||||
toSearchToolDetail(input)
|
toSearchToolDetail(input)
|
||||||
),
|
),
|
||||||
|
CodexSpeakToolDetailSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function deriveCodexToolDetail(params: {
|
export function deriveCodexToolDetail(params: {
|
||||||
@@ -135,18 +164,28 @@ export function deriveCodexToolDetail(params: {
|
|||||||
output: unknown;
|
output: unknown;
|
||||||
cwd?: string | null;
|
cwd?: string | null;
|
||||||
}): ToolCallDetail {
|
}): ToolCallDetail {
|
||||||
const parsed = CodexKnownToolDetailSchema.safeParse({
|
const pass1 = CodexToolEnvelopeSchema.safeParse({
|
||||||
name: params.name,
|
name: params.name,
|
||||||
input: params.input,
|
|
||||||
output: params.output,
|
|
||||||
cwd: params.cwd ?? null,
|
|
||||||
});
|
|
||||||
if (parsed.success && parsed.data) {
|
|
||||||
return parsed.data;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: "unknown",
|
|
||||||
input: params.input ?? null,
|
input: params.input ?? null,
|
||||||
output: params.output ?? null,
|
output: params.output ?? null,
|
||||||
|
cwd: params.cwd ?? null,
|
||||||
|
});
|
||||||
|
if (!pass1.success) {
|
||||||
|
return {
|
||||||
|
type: "unknown",
|
||||||
|
input: params.input ?? null,
|
||||||
|
output: params.output ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass2 = CodexToolDetailPass2Schema.safeParse(pass1.data);
|
||||||
|
if (pass2.success && pass2.data) {
|
||||||
|
return pass2.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "unknown",
|
||||||
|
input: pass1.data.input,
|
||||||
|
output: pass1.data.output,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ import {
|
|||||||
mapCodexToolCallFromThreadItem,
|
mapCodexToolCallFromThreadItem,
|
||||||
} from "./tool-call-mapper.js";
|
} from "./tool-call-mapper.js";
|
||||||
|
|
||||||
|
function expectMapped<T>(item: T | null): T {
|
||||||
|
expect(item).toBeTruthy();
|
||||||
|
if (!item) {
|
||||||
|
throw new Error("Expected mapped tool call");
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
describe("codex tool-call mapper", () => {
|
describe("codex tool-call mapper", () => {
|
||||||
it("maps commandExecution start into running canonical call", () => {
|
it("maps commandExecution start into running canonical call", () => {
|
||||||
const item = mapCodexToolCallFromThreadItem({
|
const item = mapCodexToolCallFromThreadItem({
|
||||||
@@ -301,12 +309,14 @@ describe("codex tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps unknown tools to unknown detail with raw payloads", () => {
|
it("maps unknown tools to unknown detail with raw payloads", () => {
|
||||||
const item = mapCodexRolloutToolCall({
|
const item = expectMapped(
|
||||||
callId: "codex-call-4",
|
mapCodexRolloutToolCall({
|
||||||
name: "my_custom_tool",
|
callId: "codex-call-4",
|
||||||
input: { foo: "bar" },
|
name: "my_custom_tool",
|
||||||
output: { ok: true },
|
input: { foo: "bar" },
|
||||||
});
|
output: { ok: true },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.status).toBe("completed");
|
expect(item.status).toBe("completed");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
@@ -327,13 +337,15 @@ describe("codex tool-call mapper", () => {
|
|||||||
"+new",
|
"+new",
|
||||||
"*** End Patch",
|
"*** End Patch",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
const item = mapCodexRolloutToolCall({
|
const item = expectMapped(
|
||||||
callId: "codex-call-apply",
|
mapCodexRolloutToolCall({
|
||||||
name: "apply_patch",
|
callId: "codex-call-apply",
|
||||||
input: patch,
|
name: "apply_patch",
|
||||||
output: '{"output":"Success. Updated the following files:\\nM src/index.ts\\n"}',
|
input: patch,
|
||||||
cwd: "/tmp/repo",
|
output: '{"output":"Success. Updated the following files:\\nM src/index.ts\\n"}',
|
||||||
});
|
cwd: "/tmp/repo",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.status).toBe("completed");
|
expect(item.status).toBe("completed");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
@@ -359,16 +371,18 @@ describe("codex tool-call mapper", () => {
|
|||||||
"*** End Patch",
|
"*** End Patch",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
const item = mapCodexRolloutToolCall({
|
const item = expectMapped(
|
||||||
callId: "codex-call-apply-object",
|
mapCodexRolloutToolCall({
|
||||||
name: "apply_patch",
|
callId: "codex-call-apply-object",
|
||||||
input: {
|
name: "apply_patch",
|
||||||
path: "/tmp/repo/src/object.ts",
|
input: {
|
||||||
content: patch,
|
path: "/tmp/repo/src/object.ts",
|
||||||
},
|
content: patch,
|
||||||
output: null,
|
},
|
||||||
cwd: "/tmp/repo",
|
output: null,
|
||||||
});
|
cwd: "/tmp/repo",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.detail.type).toBe("edit");
|
expect(item.detail.type).toBe("edit");
|
||||||
if (item.detail.type === "edit") {
|
if (item.detail.type === "edit") {
|
||||||
@@ -412,58 +426,6 @@ describe("codex tool-call mapper", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps fileChange patch alias fields into edit unified diff detail", () => {
|
|
||||||
const item = mapCodexToolCallFromThreadItem(
|
|
||||||
{
|
|
||||||
type: "fileChange",
|
|
||||||
id: "codex-file-change-patch-alias",
|
|
||||||
status: "completed",
|
|
||||||
changes: [
|
|
||||||
{
|
|
||||||
path: "/tmp/repo/src/from-patch-alias.ts",
|
|
||||||
kind: "modify",
|
|
||||||
patch: "@@\n-oldAlias\n+newAlias\n",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ cwd: "/tmp/repo" }
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(item?.detail?.type).toBe("edit");
|
|
||||||
if (item?.detail?.type === "edit") {
|
|
||||||
expect(item.detail.filePath).toBe("src/from-patch-alias.ts");
|
|
||||||
expect(item.detail.unifiedDiff).toContain("-oldAlias");
|
|
||||||
expect(item.detail.unifiedDiff).toContain("+newAlias");
|
|
||||||
expect(item.detail.newString).toBeUndefined();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maps fileChange unifiedDiff alias fields into edit unified diff detail", () => {
|
|
||||||
const item = mapCodexToolCallFromThreadItem(
|
|
||||||
{
|
|
||||||
type: "fileChange",
|
|
||||||
id: "codex-file-change-unified-diff-alias",
|
|
||||||
status: "completed",
|
|
||||||
changes: [
|
|
||||||
{
|
|
||||||
path: "/tmp/repo/src/from-unified-diff-alias.ts",
|
|
||||||
kind: "modify",
|
|
||||||
unified_diff: "@@\n-beforeAlias\n+afterAlias\n",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ cwd: "/tmp/repo" }
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(item?.detail?.type).toBe("edit");
|
|
||||||
if (item?.detail?.type === "edit") {
|
|
||||||
expect(item.detail.filePath).toBe("src/from-unified-diff-alias.ts");
|
|
||||||
expect(item.detail.unifiedDiff).toContain("-beforeAlias");
|
|
||||||
expect(item.detail.unifiedDiff).toContain("+afterAlias");
|
|
||||||
expect(item.detail.newString).toBeUndefined();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maps path-only fileChange payloads to unknown detail instead of empty edit detail", () => {
|
it("maps path-only fileChange payloads to unknown detail instead of empty edit detail", () => {
|
||||||
const item = mapCodexToolCallFromThreadItem(
|
const item = mapCodexToolCallFromThreadItem(
|
||||||
{
|
{
|
||||||
@@ -484,17 +446,80 @@ describe("codex tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps path-only apply_patch rollout payloads to unknown detail instead of empty edit detail", () => {
|
it("maps path-only apply_patch rollout payloads to unknown detail instead of empty edit detail", () => {
|
||||||
const item = mapCodexRolloutToolCall({
|
const item = expectMapped(
|
||||||
callId: "codex-call-apply-path-only",
|
mapCodexRolloutToolCall({
|
||||||
name: "apply_patch",
|
callId: "codex-call-apply-path-only",
|
||||||
input: { path: "/tmp/repo/src/path-only-rollout.ts" },
|
name: "apply_patch",
|
||||||
output: { files: [{ path: "/tmp/repo/src/path-only-rollout.ts", kind: "modify" }] },
|
input: { path: "/tmp/repo/src/path-only-rollout.ts" },
|
||||||
cwd: "/tmp/repo",
|
output: { files: [{ path: "/tmp/repo/src/path-only-rollout.ts", kind: "modify" }] },
|
||||||
});
|
cwd: "/tmp/repo",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.detail.type).toBe("unknown");
|
expect(item.detail.type).toBe("unknown");
|
||||||
if (item.detail.type === "unknown") {
|
if (item.detail.type === "unknown") {
|
||||||
expect(item.detail.input).toEqual({ path: "/tmp/repo/src/path-only-rollout.ts" });
|
expect(item.detail.input).toEqual({ path: "/tmp/repo/src/path-only-rollout.ts" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes codex paseo speak mcp calls and extracts spoken text", () => {
|
||||||
|
const item = mapCodexToolCallFromThreadItem({
|
||||||
|
type: "mcpToolCall",
|
||||||
|
id: "codex-speak-thread-1",
|
||||||
|
status: "completed",
|
||||||
|
server: "paseo",
|
||||||
|
tool: "speak",
|
||||||
|
arguments: { text: "Voice response from Codex." },
|
||||||
|
result: { ok: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(item).toBeTruthy();
|
||||||
|
expect(item?.name).toBe("speak");
|
||||||
|
expect(item?.detail).toEqual({
|
||||||
|
type: "unknown",
|
||||||
|
input: "Voice response from Codex.",
|
||||||
|
output: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes codex paseo speak rollout names and extracts spoken text", () => {
|
||||||
|
const item = expectMapped(
|
||||||
|
mapCodexRolloutToolCall({
|
||||||
|
callId: "codex-speak-rollout-1",
|
||||||
|
name: "paseo.speak",
|
||||||
|
input: { text: "Rollout speech text." },
|
||||||
|
output: { ok: true },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(item.name).toBe("speak");
|
||||||
|
expect(item.detail).toEqual({
|
||||||
|
type: "unknown",
|
||||||
|
input: "Rollout speech text.",
|
||||||
|
output: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops rollout tool calls when callId is missing", () => {
|
||||||
|
const item = mapCodexRolloutToolCall({
|
||||||
|
callId: null,
|
||||||
|
name: "read_file",
|
||||||
|
input: { path: "/tmp/repo/README.md" },
|
||||||
|
output: { content: "hello" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(item).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops thread mcp tool calls when id is missing", () => {
|
||||||
|
const item = mapCodexToolCallFromThreadItem({
|
||||||
|
type: "mcpToolCall",
|
||||||
|
status: "completed",
|
||||||
|
tool: "read_file",
|
||||||
|
arguments: { path: "/tmp/repo/README.md" },
|
||||||
|
result: { content: "hello" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(item).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import { execSync, spawn, type ChildProcess } from "node:child_process";
|
|||||||
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client";
|
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client";
|
||||||
import net from "node:net";
|
import net from "node:net";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AgentCapabilityFlags,
|
AgentCapabilityFlags,
|
||||||
@@ -69,6 +70,84 @@ type OpenCodeMcpConfig =
|
|||||||
|
|
||||||
const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"] as const;
|
const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"] as const;
|
||||||
|
|
||||||
|
const OpencodeToolStateSchema = z
|
||||||
|
.object({
|
||||||
|
status: z.string().optional(),
|
||||||
|
input: z.unknown().optional(),
|
||||||
|
output: z.unknown().optional(),
|
||||||
|
error: z.unknown().optional(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
const OpencodeToolPartBaseSchema = z
|
||||||
|
.object({
|
||||||
|
tool: z.string().trim().min(1),
|
||||||
|
state: OpencodeToolStateSchema.optional(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
const OpencodeToolPartWithCallIdSchema = OpencodeToolPartBaseSchema.extend({
|
||||||
|
callID: z.string().trim().min(1),
|
||||||
|
id: z.string().optional(),
|
||||||
|
}).transform((part) => ({
|
||||||
|
toolName: part.tool,
|
||||||
|
callId: part.callID,
|
||||||
|
status: part.state?.status,
|
||||||
|
input: part.state?.input,
|
||||||
|
output: part.state?.output,
|
||||||
|
error: part.state?.error,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const OpencodeToolPartWithIdSchema = OpencodeToolPartBaseSchema.extend({
|
||||||
|
id: z.string().trim().min(1),
|
||||||
|
callID: z.string().optional(),
|
||||||
|
}).transform((part) => ({
|
||||||
|
toolName: part.tool,
|
||||||
|
callId: part.id,
|
||||||
|
status: part.state?.status,
|
||||||
|
input: part.state?.input,
|
||||||
|
output: part.state?.output,
|
||||||
|
error: part.state?.error,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const OpencodeToolPartWithoutIdSchema = OpencodeToolPartBaseSchema.extend({
|
||||||
|
id: z.string().optional(),
|
||||||
|
callID: z.string().optional(),
|
||||||
|
}).transform((part) => ({
|
||||||
|
toolName: part.tool,
|
||||||
|
callId: undefined,
|
||||||
|
status: part.state?.status,
|
||||||
|
input: part.state?.input,
|
||||||
|
output: part.state?.output,
|
||||||
|
error: part.state?.error,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const OpencodeToolPartSchema = z.union([
|
||||||
|
OpencodeToolPartWithCallIdSchema,
|
||||||
|
OpencodeToolPartWithIdSchema,
|
||||||
|
OpencodeToolPartWithoutIdSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const OpencodeToolPartTimelineEnvelopeSchema = OpencodeToolPartSchema.transform((part) => ({
|
||||||
|
toolName: part.toolName,
|
||||||
|
callId: part.callId,
|
||||||
|
status: part.status,
|
||||||
|
input: part.input,
|
||||||
|
output: part.output,
|
||||||
|
error: part.error,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const OpencodeToolPartToTimelineItemSchema = OpencodeToolPartTimelineEnvelopeSchema.transform((part) =>
|
||||||
|
mapOpencodeToolCall({
|
||||||
|
toolName: part.toolName,
|
||||||
|
callId: part.callId,
|
||||||
|
status: part.status,
|
||||||
|
input: part.input,
|
||||||
|
output: part.output,
|
||||||
|
error: part.error,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
function resolveOpenCodeBinary(): string {
|
function resolveOpenCodeBinary(): string {
|
||||||
try {
|
try {
|
||||||
const opencodePath = execSync("which opencode", { encoding: "utf8" }).trim();
|
const opencodePath = execSync("which opencode", { encoding: "utf8" }).trim();
|
||||||
@@ -650,33 +729,15 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (partType === "tool") {
|
} else if (partType === "tool") {
|
||||||
const toolPart = part as {
|
const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
|
||||||
id?: string;
|
if (parsedToolPart.success) {
|
||||||
tool?: string;
|
if (parsedToolPart.data) {
|
||||||
callID?: string;
|
yield {
|
||||||
state?: {
|
type: "timeline",
|
||||||
status?: string;
|
provider: "opencode",
|
||||||
input?: AgentMetadata;
|
item: parsedToolPart.data,
|
||||||
output?: string;
|
};
|
||||||
error?: string;
|
}
|
||||||
};
|
|
||||||
};
|
|
||||||
const toolName = toolPart.tool;
|
|
||||||
const state = toolPart.state;
|
|
||||||
|
|
||||||
if (toolName) {
|
|
||||||
yield {
|
|
||||||
type: "timeline",
|
|
||||||
provider: "opencode",
|
|
||||||
item: mapOpencodeToolCall({
|
|
||||||
toolName,
|
|
||||||
callId: toolPart.callID ?? toolPart.id,
|
|
||||||
status: state?.status,
|
|
||||||
input: state?.input,
|
|
||||||
output: state?.output,
|
|
||||||
error: state?.error,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -931,26 +992,15 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
} else if (partType === "tool") {
|
} else if (partType === "tool") {
|
||||||
// Tool parts: { tool: string, state: { status, input, output?, error? } }
|
// Tool parts: { tool: string, state: { status, input, output?, error? } }
|
||||||
const toolName = part.tool as string | undefined;
|
const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
|
||||||
const state = part.state as AgentMetadata | undefined;
|
if (parsedToolPart.success) {
|
||||||
const status = state?.status as string | undefined;
|
if (parsedToolPart.data) {
|
||||||
const input = state?.input as AgentMetadata | undefined;
|
events.push({
|
||||||
const output = state?.output as string | undefined;
|
type: "timeline",
|
||||||
const error = state?.error as string | undefined;
|
provider: "opencode",
|
||||||
|
item: parsedToolPart.data,
|
||||||
if (toolName) {
|
});
|
||||||
events.push({
|
}
|
||||||
type: "timeline",
|
|
||||||
provider: "opencode",
|
|
||||||
item: mapOpencodeToolCall({
|
|
||||||
toolName,
|
|
||||||
callId: (part.callID as string | undefined) ?? (part.id as string | undefined),
|
|
||||||
status,
|
|
||||||
input,
|
|
||||||
output,
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (partType === "step-finish") {
|
} else if (partType === "step-finish") {
|
||||||
// Extract usage from step-finish parts
|
// Extract usage from step-finish parts
|
||||||
|
|||||||
@@ -23,8 +23,14 @@ const OpencodeKnownToolDetailSchema = z.union([
|
|||||||
toolDetailBranchByToolName("shell", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
toolDetailBranchByToolName("shell", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
||||||
toolDetailBranchByToolName("bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
toolDetailBranchByToolName("bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
||||||
toolDetailBranchByToolName("exec_command", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
toolDetailBranchByToolName("exec_command", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
|
||||||
toolDetailBranchByToolName("read", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail),
|
toolDetailBranchByToolName("read", ToolReadInputSchema, z.unknown(), (input, output) => {
|
||||||
toolDetailBranchByToolName("read_file", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail),
|
const parsedOutput = ToolReadOutputSchema.safeParse(output);
|
||||||
|
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
|
||||||
|
}),
|
||||||
|
toolDetailBranchByToolName("read_file", ToolReadInputSchema, z.unknown(), (input, output) => {
|
||||||
|
const parsedOutput = ToolReadOutputSchema.safeParse(output);
|
||||||
|
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
|
||||||
|
}),
|
||||||
toolDetailBranchByToolName("write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
toolDetailBranchByToolName("write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
||||||
toolDetailBranchByToolName("write_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
toolDetailBranchByToolName("write_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
||||||
toolDetailBranchByToolName("create_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
toolDetailBranchByToolName("create_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
|
||||||
|
|||||||
@@ -2,15 +2,25 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import { mapOpencodeToolCall } from "./tool-call-mapper.js";
|
import { mapOpencodeToolCall } from "./tool-call-mapper.js";
|
||||||
|
|
||||||
|
function expectMapped<T>(item: T | null): T {
|
||||||
|
expect(item).toBeTruthy();
|
||||||
|
if (!item) {
|
||||||
|
throw new Error("Expected mapped tool call");
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
describe("opencode tool-call mapper", () => {
|
describe("opencode tool-call mapper", () => {
|
||||||
it("maps running shell calls", () => {
|
it("maps running shell calls", () => {
|
||||||
const item = mapOpencodeToolCall({
|
const item = expectMapped(
|
||||||
toolName: "shell",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-call-1",
|
toolName: "shell",
|
||||||
status: "running",
|
callId: "opencode-call-1",
|
||||||
input: { command: "pwd", cwd: "/tmp/repo" },
|
status: "running",
|
||||||
output: null,
|
input: { command: "pwd", cwd: "/tmp/repo" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.status).toBe("running");
|
expect(item.status).toBe("running");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
@@ -22,49 +32,57 @@ describe("opencode tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps running known tool variants with detail for early summaries", () => {
|
it("maps running known tool variants with detail for early summaries", () => {
|
||||||
const readItem = mapOpencodeToolCall({
|
const readItem = expectMapped(
|
||||||
toolName: "read_file",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-running-read",
|
toolName: "read_file",
|
||||||
status: "running",
|
callId: "opencode-running-read",
|
||||||
input: { file_path: "README.md" },
|
status: "running",
|
||||||
output: null,
|
input: { file_path: "README.md" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(readItem.detail).toEqual({
|
expect(readItem.detail).toEqual({
|
||||||
type: "read",
|
type: "read",
|
||||||
filePath: "README.md",
|
filePath: "README.md",
|
||||||
});
|
});
|
||||||
|
|
||||||
const writeItem = mapOpencodeToolCall({
|
const writeItem = expectMapped(
|
||||||
toolName: "write_file",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-running-write",
|
toolName: "write_file",
|
||||||
status: "running",
|
callId: "opencode-running-write",
|
||||||
input: { file_path: "src/new.ts" },
|
status: "running",
|
||||||
output: null,
|
input: { file_path: "src/new.ts" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(writeItem.detail).toEqual({
|
expect(writeItem.detail).toEqual({
|
||||||
type: "write",
|
type: "write",
|
||||||
filePath: "src/new.ts",
|
filePath: "src/new.ts",
|
||||||
});
|
});
|
||||||
|
|
||||||
const editItem = mapOpencodeToolCall({
|
const editItem = expectMapped(
|
||||||
toolName: "apply_patch",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-running-edit",
|
toolName: "apply_patch",
|
||||||
status: "running",
|
callId: "opencode-running-edit",
|
||||||
input: { file_path: "src/index.ts" },
|
status: "running",
|
||||||
output: null,
|
input: { file_path: "src/index.ts" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(editItem.detail).toEqual({
|
expect(editItem.detail).toEqual({
|
||||||
type: "edit",
|
type: "edit",
|
||||||
filePath: "src/index.ts",
|
filePath: "src/index.ts",
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchItem = mapOpencodeToolCall({
|
const searchItem = expectMapped(
|
||||||
toolName: "web_search",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-running-search",
|
toolName: "web_search",
|
||||||
status: "running",
|
callId: "opencode-running-search",
|
||||||
input: { query: "opencode mapper" },
|
status: "running",
|
||||||
output: null,
|
input: { query: "opencode mapper" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(searchItem.detail).toEqual({
|
expect(searchItem.detail).toEqual({
|
||||||
type: "search",
|
type: "search",
|
||||||
query: "opencode mapper",
|
query: "opencode mapper",
|
||||||
@@ -72,13 +90,15 @@ describe("opencode tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps completed read calls", () => {
|
it("maps completed read calls", () => {
|
||||||
const item = mapOpencodeToolCall({
|
const item = expectMapped(
|
||||||
toolName: "read_file",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-call-2",
|
toolName: "read_file",
|
||||||
status: "complete",
|
callId: "opencode-call-2",
|
||||||
input: { file_path: "README.md" },
|
status: "complete",
|
||||||
output: { content: "hello" },
|
input: { file_path: "README.md" },
|
||||||
});
|
output: { content: "hello" },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.status).toBe("completed");
|
expect(item.status).toBe("completed");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
@@ -91,35 +111,39 @@ describe("opencode tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("preserves read content from array/object output variants", () => {
|
it("preserves read content from array/object output variants", () => {
|
||||||
const arrayContent = mapOpencodeToolCall({
|
const arrayContent = expectMapped(
|
||||||
toolName: "read_file",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-read-array",
|
toolName: "read_file",
|
||||||
status: "completed",
|
callId: "opencode-read-array",
|
||||||
input: { file_path: "README.md" },
|
status: "completed",
|
||||||
output: {
|
input: { file_path: "README.md" },
|
||||||
content: [
|
output: {
|
||||||
{ type: "output_text", text: "alpha" },
|
content: [
|
||||||
{ type: "output_text", output: "beta" },
|
{ type: "output_text", text: "alpha" },
|
||||||
],
|
{ type: "output_text", output: "beta" },
|
||||||
},
|
],
|
||||||
});
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(arrayContent.detail?.type).toBe("read");
|
expect(arrayContent.detail?.type).toBe("read");
|
||||||
if (arrayContent.detail?.type === "read") {
|
if (arrayContent.detail?.type === "read") {
|
||||||
expect(arrayContent.detail.content).toBe("alpha\nbeta");
|
expect(arrayContent.detail.content).toBe("alpha\nbeta");
|
||||||
}
|
}
|
||||||
|
|
||||||
const objectContent = mapOpencodeToolCall({
|
const objectContent = expectMapped(
|
||||||
toolName: "read_file",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-read-object",
|
toolName: "read_file",
|
||||||
status: "completed",
|
callId: "opencode-read-object",
|
||||||
input: { file_path: "README.md" },
|
status: "completed",
|
||||||
output: {
|
input: { file_path: "README.md" },
|
||||||
data: {
|
output: {
|
||||||
content: { type: "output_text", text: "gamma" },
|
data: {
|
||||||
|
content: { type: "output_text", text: "gamma" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
});
|
);
|
||||||
|
|
||||||
expect(objectContent.detail?.type).toBe("read");
|
expect(objectContent.detail?.type).toBe("read");
|
||||||
if (objectContent.detail?.type === "read") {
|
if (objectContent.detail?.type === "read") {
|
||||||
@@ -128,14 +152,16 @@ describe("opencode tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps failed calls with required error", () => {
|
it("maps failed calls with required error", () => {
|
||||||
const item = mapOpencodeToolCall({
|
const item = expectMapped(
|
||||||
toolName: "shell",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-call-3",
|
toolName: "shell",
|
||||||
status: "error",
|
callId: "opencode-call-3",
|
||||||
input: { command: "false" },
|
status: "error",
|
||||||
output: null,
|
input: { command: "false" },
|
||||||
error: "command failed",
|
output: null,
|
||||||
});
|
error: "command failed",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.status).toBe("failed");
|
expect(item.status).toBe("failed");
|
||||||
expect(item.error).toBe("command failed");
|
expect(item.error).toBe("command failed");
|
||||||
@@ -143,38 +169,44 @@ describe("opencode tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps write/edit/search known variants into canonical detail", () => {
|
it("maps write/edit/search known variants into canonical detail", () => {
|
||||||
const writeItem = mapOpencodeToolCall({
|
const writeItem = expectMapped(
|
||||||
toolName: "write_file",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-write-1",
|
toolName: "write_file",
|
||||||
status: "completed",
|
callId: "opencode-write-1",
|
||||||
input: { file_path: "src/new.ts", content: "const x = 1;" },
|
status: "completed",
|
||||||
output: null,
|
input: { file_path: "src/new.ts", content: "const x = 1;" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(writeItem.detail?.type).toBe("write");
|
expect(writeItem.detail?.type).toBe("write");
|
||||||
if (writeItem.detail?.type === "write") {
|
if (writeItem.detail?.type === "write") {
|
||||||
expect(writeItem.detail.filePath).toBe("src/new.ts");
|
expect(writeItem.detail.filePath).toBe("src/new.ts");
|
||||||
}
|
}
|
||||||
|
|
||||||
const editItem = mapOpencodeToolCall({
|
const editItem = expectMapped(
|
||||||
toolName: "apply_patch",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-edit-1",
|
toolName: "apply_patch",
|
||||||
status: "completed",
|
callId: "opencode-edit-1",
|
||||||
input: { file_path: "src/index.ts", diff: "@@\\n-old\\n+new\\n" },
|
status: "completed",
|
||||||
output: null,
|
input: { file_path: "src/index.ts", diff: "@@\\n-old\\n+new\\n" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(editItem.detail?.type).toBe("edit");
|
expect(editItem.detail?.type).toBe("edit");
|
||||||
if (editItem.detail?.type === "edit") {
|
if (editItem.detail?.type === "edit") {
|
||||||
expect(editItem.detail.filePath).toBe("src/index.ts");
|
expect(editItem.detail.filePath).toBe("src/index.ts");
|
||||||
expect(editItem.detail.unifiedDiff).toContain("@@");
|
expect(editItem.detail.unifiedDiff).toContain("@@");
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchItem = mapOpencodeToolCall({
|
const searchItem = expectMapped(
|
||||||
toolName: "web_search",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-search-1",
|
toolName: "web_search",
|
||||||
status: "completed",
|
callId: "opencode-search-1",
|
||||||
input: { query: "opencode mapper" },
|
status: "completed",
|
||||||
output: null,
|
input: { query: "opencode mapper" },
|
||||||
});
|
output: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(searchItem.detail).toEqual({
|
expect(searchItem.detail).toEqual({
|
||||||
type: "search",
|
type: "search",
|
||||||
query: "opencode mapper",
|
query: "opencode mapper",
|
||||||
@@ -182,13 +214,15 @@ describe("opencode tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps unknown tools to unknown detail with raw payloads", () => {
|
it("maps unknown tools to unknown detail with raw payloads", () => {
|
||||||
const item = mapOpencodeToolCall({
|
const item = expectMapped(
|
||||||
toolName: "my_custom_tool",
|
mapOpencodeToolCall({
|
||||||
callId: "opencode-call-4",
|
toolName: "my_custom_tool",
|
||||||
status: "completed",
|
callId: "opencode-call-4",
|
||||||
input: { foo: "bar" },
|
status: "completed",
|
||||||
output: { ok: true },
|
input: { foo: "bar" },
|
||||||
});
|
output: { ok: true },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(item.status).toBe("completed");
|
expect(item.status).toBe("completed");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
@@ -198,4 +232,35 @@ describe("opencode tool-call mapper", () => {
|
|||||||
output: { ok: true },
|
output: { ok: true },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not apply cross-provider speak normalization in opencode mapper", () => {
|
||||||
|
const item = expectMapped(
|
||||||
|
mapOpencodeToolCall({
|
||||||
|
toolName: "paseo_voice.speak",
|
||||||
|
callId: "opencode-call-voice-1",
|
||||||
|
status: "completed",
|
||||||
|
input: { text: "Voice response from OpenCode." },
|
||||||
|
output: { ok: true },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(item.name).toBe("paseo_voice.speak");
|
||||||
|
expect(item.detail).toEqual({
|
||||||
|
type: "unknown",
|
||||||
|
input: { text: "Voice response from OpenCode." },
|
||||||
|
output: { ok: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops tool calls when callId is missing", () => {
|
||||||
|
const item = mapOpencodeToolCall({
|
||||||
|
toolName: "read_file",
|
||||||
|
callId: null,
|
||||||
|
status: "completed",
|
||||||
|
input: { file_path: "README.md" },
|
||||||
|
output: { content: "hello" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(item).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { ToolCallTimelineItem } from "../../agent-sdk-types.js";
|
import type { ToolCallTimelineItem } from "../../agent-sdk-types.js";
|
||||||
import { coerceToolCallId } from "../tool-call-mapper-utils.js";
|
|
||||||
import { deriveOpencodeToolDetail } from "./tool-call-detail-parser.js";
|
import { deriveOpencodeToolDetail } from "./tool-call-detail-parser.js";
|
||||||
|
|
||||||
type OpencodeToolCallParams = {
|
type OpencodeToolCallParams = {
|
||||||
@@ -18,7 +17,14 @@ const FAILED_STATUSES = new Set(["error", "failed", "failure"]);
|
|||||||
const CANCELED_STATUSES = new Set(["canceled", "cancelled", "aborted", "interrupted"]);
|
const CANCELED_STATUSES = new Set(["canceled", "cancelled", "aborted", "interrupted"]);
|
||||||
const COMPLETED_STATUSES = new Set(["complete", "completed", "success", "succeeded", "done"]);
|
const COMPLETED_STATUSES = new Set(["complete", "completed", "success", "succeeded", "done"]);
|
||||||
|
|
||||||
const OpencodeToolCallParamsSchema = z
|
const OpencodeToolCallStatusSchema = z.enum([
|
||||||
|
"running",
|
||||||
|
"completed",
|
||||||
|
"failed",
|
||||||
|
"canceled",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const OpencodeRawToolCallSchema = z
|
||||||
.object({
|
.object({
|
||||||
toolName: z.string().min(1),
|
toolName: z.string().min(1),
|
||||||
callId: z.string().optional().nullable(),
|
callId: z.string().optional().nullable(),
|
||||||
@@ -26,74 +32,163 @@ const OpencodeToolCallParamsSchema = z
|
|||||||
input: z.unknown().optional(),
|
input: z.unknown().optional(),
|
||||||
output: z.unknown().optional(),
|
output: z.unknown().optional(),
|
||||||
error: z.unknown().optional(),
|
error: z.unknown().optional(),
|
||||||
metadata: z.record(z.unknown()).optional(),
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
function coerceCallId(callId: string | null | undefined, toolName: string, input: unknown): string {
|
const OpencodeNormalizedToolCallPass1Schema = OpencodeRawToolCallSchema.transform((raw) => {
|
||||||
return coerceToolCallId({
|
const input = raw.input ?? null;
|
||||||
providerPrefix: "opencode",
|
const output = raw.output ?? null;
|
||||||
rawCallId: callId,
|
const error = raw.error ?? null;
|
||||||
toolName,
|
const callId =
|
||||||
input,
|
typeof raw.callId === "string" && raw.callId.trim().length > 0
|
||||||
});
|
? raw.callId.trim()
|
||||||
}
|
: null;
|
||||||
|
let status: z.infer<typeof OpencodeToolCallStatusSchema>;
|
||||||
|
|
||||||
function resolveStatus(
|
if (error !== null) {
|
||||||
rawStatus: unknown,
|
status = "failed";
|
||||||
error: unknown,
|
} else if (typeof raw.status === "string") {
|
||||||
output: unknown
|
const normalized = raw.status.trim().toLowerCase();
|
||||||
): ToolCallTimelineItem["status"] {
|
if (FAILED_STATUSES.has(normalized)) {
|
||||||
if (error !== null && error !== undefined) {
|
status = "failed";
|
||||||
return "failed";
|
} else if (CANCELED_STATUSES.has(normalized)) {
|
||||||
}
|
status = "canceled";
|
||||||
|
} else if (COMPLETED_STATUSES.has(normalized)) {
|
||||||
if (typeof rawStatus === "string") {
|
status = "completed";
|
||||||
const normalized = rawStatus.trim().toLowerCase();
|
} else {
|
||||||
if (normalized.length > 0) {
|
status = "running";
|
||||||
if (FAILED_STATUSES.has(normalized)) {
|
|
||||||
return "failed";
|
|
||||||
}
|
|
||||||
if (CANCELED_STATUSES.has(normalized)) {
|
|
||||||
return "canceled";
|
|
||||||
}
|
|
||||||
if (COMPLETED_STATUSES.has(normalized)) {
|
|
||||||
return "completed";
|
|
||||||
}
|
|
||||||
return "running";
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
status = output !== null ? "completed" : "running";
|
||||||
return output !== null && output !== undefined ? "completed" : "running";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTimelineItem {
|
|
||||||
const parsedParams = OpencodeToolCallParamsSchema.parse(params);
|
|
||||||
const input = parsedParams.input ?? null;
|
|
||||||
const output = parsedParams.output ?? null;
|
|
||||||
const status = resolveStatus(parsedParams.status, parsedParams.error, output);
|
|
||||||
const callId = coerceCallId(parsedParams.callId, parsedParams.toolName, input);
|
|
||||||
const detail = deriveOpencodeToolDetail(parsedParams.toolName, input, output);
|
|
||||||
|
|
||||||
if (status === "failed") {
|
|
||||||
return {
|
|
||||||
type: "tool_call",
|
|
||||||
callId,
|
|
||||||
name: parsedParams.toolName,
|
|
||||||
status: "failed",
|
|
||||||
detail,
|
|
||||||
error: parsedParams.error ?? { message: "Tool call failed" },
|
|
||||||
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "tool_call",
|
|
||||||
callId,
|
callId,
|
||||||
name: parsedParams.toolName,
|
name: raw.toolName.trim(),
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
metadata: raw.metadata,
|
||||||
status,
|
status,
|
||||||
detail,
|
|
||||||
error: null,
|
|
||||||
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
|
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const OpencodeKnownToolNameSchema = z.union([
|
||||||
|
z.literal("shell"),
|
||||||
|
z.literal("bash"),
|
||||||
|
z.literal("exec_command"),
|
||||||
|
z.literal("read"),
|
||||||
|
z.literal("read_file"),
|
||||||
|
z.literal("write"),
|
||||||
|
z.literal("write_file"),
|
||||||
|
z.literal("create_file"),
|
||||||
|
z.literal("edit"),
|
||||||
|
z.literal("apply_patch"),
|
||||||
|
z.literal("apply_diff"),
|
||||||
|
z.literal("search"),
|
||||||
|
z.literal("web_search"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const OpencodeToolCallPass2BaseSchema = z.object({
|
||||||
|
callId: z.string().min(1),
|
||||||
|
name: z.string().min(1),
|
||||||
|
input: z.unknown().nullable(),
|
||||||
|
output: z.unknown().nullable(),
|
||||||
|
error: z.unknown().nullable(),
|
||||||
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
status: OpencodeToolCallStatusSchema,
|
||||||
|
toolKind: z.enum(["known", "other"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const OpencodeToolCallPass2InputSchema = OpencodeToolCallPass2BaseSchema.omit({
|
||||||
|
toolKind: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const OpencodeToolCallPass2EnvelopeSchema = z.union([
|
||||||
|
OpencodeToolCallPass2InputSchema.extend({
|
||||||
|
name: OpencodeKnownToolNameSchema,
|
||||||
|
}).transform((normalized) => ({
|
||||||
|
...normalized,
|
||||||
|
name: normalized.name.trim(),
|
||||||
|
toolKind: "known" as const,
|
||||||
|
})),
|
||||||
|
OpencodeToolCallPass2InputSchema.transform((normalized) => ({
|
||||||
|
...normalized,
|
||||||
|
name: normalized.name.trim(),
|
||||||
|
toolKind: "other" as const,
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const OpencodeToolCallPass2Schema = z.discriminatedUnion("toolKind", [
|
||||||
|
OpencodeToolCallPass2BaseSchema.extend({
|
||||||
|
toolKind: z.literal("known"),
|
||||||
|
name: OpencodeKnownToolNameSchema,
|
||||||
|
}).transform((normalized): ToolCallTimelineItem => {
|
||||||
|
const detail = deriveOpencodeToolDetail(normalized.name, normalized.input, normalized.output);
|
||||||
|
if (normalized.status === "failed") {
|
||||||
|
return {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: normalized.callId,
|
||||||
|
name: normalized.name,
|
||||||
|
status: "failed",
|
||||||
|
detail,
|
||||||
|
error: normalized.error ?? { message: "Tool call failed" },
|
||||||
|
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: normalized.callId,
|
||||||
|
name: normalized.name,
|
||||||
|
status: normalized.status,
|
||||||
|
detail,
|
||||||
|
error: null,
|
||||||
|
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
OpencodeToolCallPass2BaseSchema.extend({
|
||||||
|
toolKind: z.literal("other"),
|
||||||
|
}).transform((normalized): ToolCallTimelineItem => {
|
||||||
|
const detail = deriveOpencodeToolDetail(normalized.name, normalized.input, normalized.output);
|
||||||
|
if (normalized.status === "failed") {
|
||||||
|
return {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: normalized.callId,
|
||||||
|
name: normalized.name,
|
||||||
|
status: "failed",
|
||||||
|
detail,
|
||||||
|
error: normalized.error ?? { message: "Tool call failed" },
|
||||||
|
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: normalized.callId,
|
||||||
|
name: normalized.name,
|
||||||
|
status: normalized.status,
|
||||||
|
detail,
|
||||||
|
error: null,
|
||||||
|
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTimelineItem | null {
|
||||||
|
const pass1 = OpencodeNormalizedToolCallPass1Schema.safeParse(params);
|
||||||
|
if (!pass1.success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass2Envelope = OpencodeToolCallPass2EnvelopeSchema.safeParse(pass1.data);
|
||||||
|
if (!pass2Envelope.success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass2 = OpencodeToolCallPass2Schema.safeParse(pass2Envelope.data);
|
||||||
|
if (!pass2.success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pass2.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import type { ToolCallDetail } from "../agent-sdk-types.js";
|
import type { ToolCallDetail } from "../agent-sdk-types.js";
|
||||||
import {
|
import {
|
||||||
commandFromValue,
|
|
||||||
extractCodexShellOutput,
|
extractCodexShellOutput,
|
||||||
flattenReadContent as flattenToolReadContent,
|
flattenReadContent as flattenToolReadContent,
|
||||||
nonEmptyString,
|
nonEmptyString,
|
||||||
@@ -29,9 +28,19 @@ export const ToolShellInputSchema = z
|
|||||||
.passthrough(),
|
.passthrough(),
|
||||||
])
|
])
|
||||||
.transform((value) => {
|
.transform((value) => {
|
||||||
const commandValue = "command" in value ? value.command : value.cmd;
|
const parsedCommand = CommandValueSchema.safeParse(
|
||||||
|
"command" in value ? value.command : value.cmd
|
||||||
|
);
|
||||||
|
const command = parsedCommand.success
|
||||||
|
? typeof parsedCommand.data === "string"
|
||||||
|
? nonEmptyString(parsedCommand.data)
|
||||||
|
: parsedCommand.data
|
||||||
|
.map((token) => token.trim())
|
||||||
|
.filter((token) => token.length > 0)
|
||||||
|
.join(" ") || undefined
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
command: commandFromValue(commandValue),
|
command,
|
||||||
cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory),
|
cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -314,17 +323,12 @@ type ToolReadOutputValue = {
|
|||||||
content?: string;
|
content?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ToolReadOutputSchema: z.ZodType<
|
export const ToolReadOutputSchema = ToolReadOutputContentSchema;
|
||||||
ToolReadOutputValue,
|
|
||||||
z.ZodTypeDef,
|
|
||||||
unknown
|
|
||||||
> = ToolReadOutputContentSchema;
|
|
||||||
|
|
||||||
export const ToolReadOutputWithPathSchema: z.ZodType<
|
export const ToolReadOutputWithPathSchema = z.union([
|
||||||
ToolReadOutputValue,
|
ToolReadOutputContentSchema,
|
||||||
z.ZodTypeDef,
|
ToolReadOutputPathSchema,
|
||||||
unknown
|
]);
|
||||||
> = z.union([ToolReadOutputContentSchema, ToolReadOutputPathSchema]);
|
|
||||||
|
|
||||||
export const ToolWriteContentSchema = z
|
export const ToolWriteContentSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -508,8 +512,8 @@ export const ToolSearchInputSchema = z.union([
|
|||||||
export type ParsedToolShellInput = z.infer<typeof ToolShellInputSchema>;
|
export type ParsedToolShellInput = z.infer<typeof ToolShellInputSchema>;
|
||||||
export type ParsedToolShellOutput = z.infer<typeof ToolShellOutputSchema>;
|
export type ParsedToolShellOutput = z.infer<typeof ToolShellOutputSchema>;
|
||||||
export type ParsedToolReadInput = z.infer<typeof ToolReadInputSchema>;
|
export type ParsedToolReadInput = z.infer<typeof ToolReadInputSchema>;
|
||||||
export type ParsedToolReadOutput = z.infer<typeof ToolReadOutputSchema>;
|
export type ParsedToolReadOutput = ToolReadOutputValue;
|
||||||
export type ParsedToolReadOutputWithPath = z.infer<typeof ToolReadOutputWithPathSchema>;
|
export type ParsedToolReadOutputWithPath = ToolReadOutputValue;
|
||||||
export type ParsedToolWriteInput = z.infer<typeof ToolWriteInputSchema>;
|
export type ParsedToolWriteInput = z.infer<typeof ToolWriteInputSchema>;
|
||||||
export type ParsedToolWriteOutput = z.infer<typeof ToolWriteOutputSchema>;
|
export type ParsedToolWriteOutput = z.infer<typeof ToolWriteOutputSchema>;
|
||||||
export type ParsedToolEditInput = z.infer<typeof ToolEditInputSchema>;
|
export type ParsedToolEditInput = z.infer<typeof ToolEditInputSchema>;
|
||||||
@@ -649,7 +653,13 @@ export function toolDetailBranchByName<
|
|||||||
input: inputSchema.nullable(),
|
input: inputSchema.nullable(),
|
||||||
output: outputSchema.nullable(),
|
output: outputSchema.nullable(),
|
||||||
})
|
})
|
||||||
.transform(({ input, output }) => mapper(input, output));
|
.transform((value) => {
|
||||||
|
const parsed = value as unknown as {
|
||||||
|
input: z.infer<InputSchema> | null;
|
||||||
|
output: z.infer<OutputSchema> | null;
|
||||||
|
};
|
||||||
|
return mapper(parsed.input, parsed.output);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toolDetailBranchByToolName<
|
export function toolDetailBranchByToolName<
|
||||||
@@ -671,7 +681,13 @@ export function toolDetailBranchByToolName<
|
|||||||
input: inputSchema.nullable(),
|
input: inputSchema.nullable(),
|
||||||
output: outputSchema.nullable(),
|
output: outputSchema.nullable(),
|
||||||
})
|
})
|
||||||
.transform(({ input, output }) => mapper(input, output));
|
.transform((value) => {
|
||||||
|
const parsed = value as unknown as {
|
||||||
|
input: z.infer<InputSchema> | null;
|
||||||
|
output: z.infer<OutputSchema> | null;
|
||||||
|
};
|
||||||
|
return mapper(parsed.input, parsed.output);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toolDetailBranchByNameWithCwd<
|
export function toolDetailBranchByNameWithCwd<
|
||||||
@@ -695,5 +711,12 @@ export function toolDetailBranchByNameWithCwd<
|
|||||||
output: outputSchema.nullable(),
|
output: outputSchema.nullable(),
|
||||||
cwd: z.string().optional().nullable(),
|
cwd: z.string().optional().nullable(),
|
||||||
})
|
})
|
||||||
.transform(({ input, output, cwd }) => mapper(input, output, cwd ?? null));
|
.transform((value) => {
|
||||||
|
const parsed = value as unknown as {
|
||||||
|
input: z.infer<InputSchema> | null;
|
||||||
|
output: z.infer<OutputSchema> | null;
|
||||||
|
cwd?: string | null;
|
||||||
|
};
|
||||||
|
return mapper(parsed.input, parsed.output, parsed.cwd ?? null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,19 +8,6 @@ export function nonEmptyString(value: unknown): string | undefined {
|
|||||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function commandFromValue(value: unknown): string | undefined {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return nonEmptyString(value);
|
|
||||||
}
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const tokens = value.filter(
|
|
||||||
(token): token is string => typeof token === "string" && token.length > 0
|
|
||||||
);
|
|
||||||
return tokens.length > 0 ? tokens.join(" ") : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CODEX_SHELL_ENVELOPE_HEADER_LINES = new Set([
|
const CODEX_SHELL_ENVELOPE_HEADER_LINES = new Set([
|
||||||
"chunk id:",
|
"chunk id:",
|
||||||
"wall time:",
|
"wall time:",
|
||||||
|
|||||||
Reference in New Issue
Block a user