From d2beb784571588b146552e3469acb0ec34ef1da9 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Mon, 12 Jan 2026 23:54:53 -0800 Subject: [PATCH] feat: structured output --- ...2480f80fa29b850af461dce619c0b2f-audit.json | 15 ----- logs/mcp-puppeteer-2025-10-07.log | 48 ---------------- packages/opencode/src/session/prompt.ts | 19 +++---- .../structured-output-integration.test.ts | 55 +++++++++++-------- .../test/session/structured-output.test.ts | 9 ++- 5 files changed, 47 insertions(+), 99 deletions(-) delete mode 100644 logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json delete mode 100644 logs/mcp-puppeteer-2025-10-07.log diff --git a/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json b/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json deleted file mode 100644 index 41cb01a2b8..0000000000 --- a/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "keep": { - "days": true, - "amount": 14 - }, - "auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json", - "files": [ - { - "date": 1759827172859, - "name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log", - "hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16" - } - ], - "hashType": "sha256" -} diff --git a/logs/mcp-puppeteer-2025-10-07.log b/logs/mcp-puppeteer-2025-10-07.log deleted file mode 100644 index 7705356966..0000000000 --- a/logs/mcp-puppeteer-2025-10-07.log +++ /dev/null @@ -1,48 +0,0 @@ -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.879"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.880"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.191"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.192"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.267"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.268"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.276"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.277"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.838"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.839"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.499"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.500"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"} -{"arguments":{"url":"https://google.com"},"level":"debug","message":"Tool call received","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150","tool":"puppeteer_navigate"} -{"0":"n","1":"p","2":"x","level":"info","message":"Launching browser with config:","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.488"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.489"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.815"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.816"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.934"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.935"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.154"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.155"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.426"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.427"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.715"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.716"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.063"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.064"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.567"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.568"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.937"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.938"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.120"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.121"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.490"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.491"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.524"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.525"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.126"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.127"} -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.175"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.176"} diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 1614384e27..2779aa4ee6 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -576,21 +576,21 @@ export namespace SessionPrompt { toolChoice: outputFormat.type === "json_schema" ? "required" : undefined, }) - // Handle structured output logic - // (outputFormat already set above before process call) + // If structured output was captured, save it and exit immediately + // This takes priority because the StructuredOutput tool was called successfully + if (structuredOutput !== undefined) { + processor.message.structured_output = structuredOutput + processor.message.finish = processor.message.finish ?? "stop" + await Session.updateMessage(processor.message) + break + } // Check if model finished (finish reason is not "tool-calls" or "unknown") const modelFinished = processor.message.finish && !["tool-calls", "unknown"].includes(processor.message.finish) if (modelFinished && !processor.message.error) { - // Check if structured output was captured successfully - if (structuredOutput !== undefined) { - // Store structured output on the final assistant message - processor.message.structured_output = structuredOutput - await Session.updateMessage(processor.message) - break - } else if (outputFormat.type === "json_schema") { + if (outputFormat.type === "json_schema") { // Model stopped without calling StructuredOutput tool processor.message.error = new MessageV2.StructuredOutputError({ message: "Model did not produce structured output", @@ -794,7 +794,6 @@ export namespace SessionPrompt { inputSchema: jsonSchema(toolSchema as any), async execute(args) { // AI SDK validates args against inputSchema before calling execute() - // So args is guaranteed to match the schema at this point input.onSuccess(args) return { output: "Structured output captured successfully.", diff --git a/packages/opencode/test/session/structured-output-integration.test.ts b/packages/opencode/test/session/structured-output-integration.test.ts index 4c2b23f3de..5b7bb84afe 100644 --- a/packages/opencode/test/session/structured-output-integration.test.ts +++ b/packages/opencode/test/session/structured-output-integration.test.ts @@ -43,18 +43,22 @@ describe("StructuredOutput Integration", () => { }, required: ["answer"], }, + retryCount: 0, }, }) - // Verify structured output was captured - expect(result.info.structured_output).toBeDefined() - expect(typeof result.info.structured_output).toBe("object") + // Verify structured output was captured (only on assistant messages) + expect(result.info.role).toBe("assistant") + if (result.info.role === "assistant") { + expect(result.info.structured_output).toBeDefined() + expect(typeof result.info.structured_output).toBe("object") - const output = result.info.structured_output as any - expect(output.answer).toBe(4) + const output = result.info.structured_output as any + expect(output.answer).toBe(4) - // Verify no error was set - expect(result.info.error).toBeUndefined() + // Verify no error was set + expect(result.info.error).toBeUndefined() + } // Clean up // Note: Not removing session to avoid race with background SessionSummary.summarize @@ -93,24 +97,28 @@ describe("StructuredOutput Integration", () => { }, required: ["company"], }, + retryCount: 0, }, }) - // Verify structured output was captured - expect(result.info.structured_output).toBeDefined() - const output = result.info.structured_output as any + // Verify structured output was captured (only on assistant messages) + expect(result.info.role).toBe("assistant") + if (result.info.role === "assistant") { + expect(result.info.structured_output).toBeDefined() + const output = result.info.structured_output as any - expect(output.company).toBeDefined() - expect(output.company.name).toBe("Anthropic") - expect(typeof output.company.founded).toBe("number") + expect(output.company).toBeDefined() + expect(output.company.name).toBe("Anthropic") + expect(typeof output.company.founded).toBe("number") - if (output.products) { - expect(Array.isArray(output.products)).toBe(true) + if (output.products) { + expect(Array.isArray(output.products)).toBe(true) + } + + // Verify no error was set + expect(result.info.error).toBeUndefined() } - // Verify no error was set - expect(result.info.error).toBeUndefined() - // Clean up // Note: Not removing session to avoid race with background SessionSummary.summarize }) @@ -133,15 +141,16 @@ describe("StructuredOutput Integration", () => { }, }) - // Verify no structured output (text mode) - expect(result.info.structured_output).toBeUndefined() + // Verify no structured output (text mode) and no error + expect(result.info.role).toBe("assistant") + if (result.info.role === "assistant") { + expect(result.info.structured_output).toBeUndefined() + expect(result.info.error).toBeUndefined() + } // Verify we got a response with parts expect(result.parts.length).toBeGreaterThan(0) - // Verify no error was set - expect(result.info.error).toBeUndefined() - // Clean up // Note: Not removing session to avoid race with background SessionSummary.summarize }) diff --git a/packages/opencode/test/session/structured-output.test.ts b/packages/opencode/test/session/structured-output.test.ts index d36f36d819..d965302b65 100644 --- a/packages/opencode/test/session/structured-output.test.ts +++ b/packages/opencode/test/session/structured-output.test.ts @@ -162,7 +162,8 @@ describe("structured-output.createStructuredOutputTool", () => { onSuccess: () => {}, }) - expect(tool.id).toBe("StructuredOutput") + // AI SDK tool type doesn't expose id, but we set it internally + expect((tool as any).id).toBe("StructuredOutput") }) test("creates tool with description", () => { @@ -223,8 +224,9 @@ describe("structured-output.createStructuredOutputTool", () => { }, }) + expect(tool.execute).toBeDefined() const testArgs = { name: "Test Company" } - const result = await tool.execute(testArgs, { + const result = await tool.execute!(testArgs, { toolCallId: "test-call-id", messages: [], abortSignal: undefined as any, @@ -241,7 +243,8 @@ describe("structured-output.createStructuredOutputTool", () => { onSuccess: () => {}, }) - const modelOutput = tool.toModelOutput({ + expect(tool.toModelOutput).toBeDefined() + const modelOutput = tool.toModelOutput!({ output: "Test output", title: "Test", metadata: { valid: true },