Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions packages/core/src/agents/codex/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,94 @@ describe('codexParser', () => {
});
});

it("gives an mcp_tool_call's arguments as its input, keeping the result off it", () => {
// Shape from a real CLI run: the result can be far larger than the call.
const args = { project_id: 'p1', query: 'select 1' };
const toolResult = {
content: [{ type: 'text', text: '[{"?column?":1}]' }],
structured_content: null,
};
const stream = JSON.stringify({
type: 'item.completed',
item: {
id: 'item_7',
type: 'mcp_tool_call',
server: 'supabase-mcp',
tool: 'execute_sql',
arguments: args,
result: toolResult,
error: null,
status: 'completed',
},
});

const adapted = adaptTranscript(codexParser.parseTranscript(stream).events);
expect(adapted.transcript[0]).toEqual({
type: 'tool_call',
name: 'execute_sql',
input: args,
output: toolResult,
error: undefined,
});
expect(adapted.toolCalls[0].body).toEqual(args);
expect(adapted.toolCalls[0].result).toEqual(toolResult);
});

it('gives an mcp_tool_call with no argument object an empty input', () => {
const stream = JSON.stringify({
type: 'item.completed',
item: {
id: 'item_8',
type: 'mcp_tool_call',
server: 'supabase-mcp',
tool: 'list_tables',
arguments: null,
result: { content: [] },
status: 'completed',
},
});
const adapted = adaptTranscript(codexParser.parseTranscript(stream).events);
expect(adapted.toolCalls[0].body).toEqual({});
});

it.each([
{
failure: 'an error the MCP tool returned',
result: { content: [{ type: 'text', text: 'column does not exist' }] },
error: null,
},
{
failure: 'an error Codex raised with no tool result',
result: null,
error: { message: 'column does not exist' },
},
])(
'surfaces $failure as the failed call error, not as input',
({ result, error }) => {
const stream = JSON.stringify({
type: 'item.completed',
item: {
id: 'item_9',
type: 'mcp_tool_call',
server: 'supabase-mcp',
tool: 'execute_sql',
arguments: { query: 'select child_table' },
result,
error,
status: 'failed',
},
});
const adapted = adaptTranscript(
codexParser.parseTranscript(stream).events
);
expect(adapted.toolCalls[0].body).toEqual({
query: 'select child_table',
});
expect(adapted.toolCalls[0].result).toBeUndefined();
expect(adapted.toolCalls[0].error).toContain('column does not exist');
}
);

it("keeps a web_search item's action, which says what the hosted tool did", () => {
const url = 'https://supabase.com/changelog.md';
const stream = JSON.stringify({
Expand Down
18 changes: 13 additions & 5 deletions packages/core/src/agents/codex/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
*
* Observed `item.type`s: `agent_message` {text}, `reasoning` {text},
* `command_execution` {command, aggregated_output, exit_code, status},
* `file_change` {changes:[{path,kind}], status}. MCP / web-search items are
* handled best-effort. Each tool item yields a paired tool_call + tool_result
* (correlated by the item id) so the adapter can attach the output.
* `file_change` {changes:[{path,kind}], status},
* `mcp_tool_call` {server, tool, arguments, result, error, status}. Web-search
* items are handled best-effort. Each tool item yields a paired tool_call +
* tool_result (correlated by the item id) so the adapter can attach the output.
* A tool_call carries only the call's arguments; its output rides on the
* tool_result, the same split every other agent's parser makes.
*
* NB: this is the `--json` event schema, NOT the `~/.codex/sessions` rollout
* format (event_msg/response_item) that older parsers targeted.
Expand Down Expand Up @@ -178,11 +181,16 @@ function itemToEvents(item: Record<string, unknown>): TranscriptEvent[] {
// bare tool name; `item.server` names the MCP server when present.
const bare = str(item.tool) ?? str(item.name) ?? 'mcp_tool_call';
const server = str(item.server);
const args = isRecord(item.arguments) ? item.arguments : {};
return toolCallPair(
id,
bare,
item,
item.result ?? item.output,
args,
// A failure Codex raised itself has no result, only `error: {message}`;
// carry that message as the output so the failure stays visible.
item.result ??
item.output ??
(isRecord(item.error) ? str(item.error.message) : undefined),
statusSuccess(item.status),
{},
server
Expand Down
40 changes: 0 additions & 40 deletions packages/core/src/docs-results.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,46 +99,6 @@ describe('buildDocsResult', () => {
expect(result.calls[0].hasContent).toBe(false);
});

it('finds the query nested under body.arguments (real Codex mcp_tool_call shape)', () => {
const result = buildDocsResult([
toolCall(
// Codex's parser sets originalName to the MCP tool's own name
// (item.tool), not the "mcp_tool_call" item type, and reports the
// whole raw item as `body`, args nested under `body.arguments`.
'search_docs',
{
id: 'item_9',
type: 'mcp_tool_call',
server: 'supabase-mcp',
tool: 'search_docs',
arguments: {
graphql_query:
'{ searchDocs(query: "rls") { nodes { title href } } }',
},
},
{
result: {
searchDocs: {
nodes: [
{
title: 'Row Level Security',
href: 'https://supabase.com/docs/guides/database/postgres/row-level-security',
},
],
},
},
}
),
]);

expect(result.calls[0].query).toBe(
'{ searchDocs(query: "rls") { nodes { title href } } }'
);
expect(result.calls[0].pages.map((p) => p.url)).toEqual([
'https://supabase.com/docs/guides/database/postgres/row-level-security',
]);
});

it('unwraps a content-array result whose text field is a JSON-encoded string (real Claude Code shape)', () => {
const result = buildDocsResult([
toolCall(
Expand Down
17 changes: 2 additions & 15 deletions packages/core/src/docs-results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,6 @@ const STDOUT_REDIRECT_PATTERN = /(?:^|[^2])>/;
// legally follow one (`|`, `>`, quotes, backslash-escapes).
const URL_IN_COMMAND_PATTERN = /https?:\/\/[^\s'"`\\;|&>()]+/g;

/** The search_docs query arg, flat on `body` or nested under `body.arguments` (Codex's shape). */
function extractGraphqlQuery(
body: Record<string, unknown>
): string | undefined {
if (typeof body.graphql_query === 'string') return body.graphql_query;
const args = body.arguments;
if (args && typeof args === 'object' && 'graphql_query' in args) {
const value = (args as Record<string, unknown>).graphql_query;
if (typeof value === 'string') return value;
}
return undefined;
}

/**
* True for the apex supabase.com host. Docs, changelog, and blog all live
* there. Subdomains like `api.` and `mcp.` are service endpoints, so they
Expand Down Expand Up @@ -242,8 +229,8 @@ export function buildDocsResult(toolCalls: ToolCallRecord[]): DocsResult {
const { tool, body, result } = call;

if (tool.toolName === 'search_docs') {
const graphqlQuery = extractGraphqlQuery(body);
if (!graphqlQuery) continue;
const graphqlQuery = body.graphql_query;
if (typeof graphqlQuery !== 'string' || !graphqlQuery) continue;
calls.push({
source: 'search_docs',
query: graphqlQuery,
Expand Down