Skip to content

Commit a9caa27

Browse files
committed
feat(claude-adapter): support native structured outputs
User requested native structured-output support for the Claude adapter. Upgraded @anthropic-ai/claude-agent-sdk to 0.1.46 (and tightened the peer range) so the SDK exposes the json_schema outputFormat feature, then wired RunOpts.outputSchema into that API. The adapter now asks Claude for structured_output payloads, returns them via RunResult.json, and falls back to the legacy JSON-parsing prompt if native output is missing. Verified the TypeScript build with pnpm --filter @headless-coder-sdk/claude-adapter build.
1 parent 048fba0 commit a9caa27

File tree

5 files changed

+53
-26
lines changed

5 files changed

+53
-26
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"acp:e2e": "npm run e2e --workspace packages/acp-server"
1616
},
1717
"devDependencies": {
18-
"@anthropic-ai/claude-agent-sdk": "^0.1.30",
18+
"@anthropic-ai/claude-agent-sdk": "^0.1.46",
1919
"@types/node": "^20.12.7",
2020
"tsup": "^8.5.0",
2121
"tsx": "^4.19.1",

packages/claude-adapter/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"build": "tsup --config tsup.config.ts"
1919
},
2020
"peerDependencies": {
21-
"@anthropic-ai/claude-agent-sdk": "*",
21+
"@anthropic-ai/claude-agent-sdk": ">=0.1.46",
2222
"@headless-coder-sdk/core": "^0.18.0"
2323
},
2424
"devDependencies": {

packages/claude-adapter/src/index.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,21 @@ function applyOutputSchemaPrompt(input: PromptInput, schema?: object): PromptInp
8383
];
8484
}
8585

86+
function shouldUseNativeStructuredOutput(schema?: object): boolean {
87+
return !!schema;
88+
}
89+
90+
function extractNativeStructuredOutput(result: any): unknown | undefined {
91+
if (!result) return undefined;
92+
if (Object.prototype.hasOwnProperty.call(result, 'structured_output')) {
93+
return (result as any).structured_output;
94+
}
95+
if (Object.prototype.hasOwnProperty.call(result, 'structuredOutput')) {
96+
return (result as any).structuredOutput;
97+
}
98+
return undefined;
99+
}
100+
86101
function extractJsonPayload(text: string | undefined): unknown | undefined {
87102
if (!text) return undefined;
88103
const fenced = text.match(/```json\s*([\s\S]+?)```/i);
@@ -176,11 +191,18 @@ export class ClaudeAdapter implements HeadlessCoder {
176191
* Returns:
177192
* Options ready for the Claude Agent SDK.
178193
*/
179-
private buildOptions(state: ClaudeThreadState, runOpts?: RunOpts): Options {
194+
private buildOptions(state: ClaudeThreadState, runOpts?: RunOpts, useNativeStructuredOutput?: boolean): Options {
180195
const startOpts = state.opts ?? {};
181196
const resumeId = state.resume ? state.sessionId : undefined;
182197
const permissionMode: PermissionMode | undefined =
183198
(startOpts.permissionMode as PermissionMode | undefined) ?? (startOpts.yolo ? 'bypassPermissions' : undefined);
199+
const outputFormat =
200+
useNativeStructuredOutput && runOpts?.outputSchema
201+
? {
202+
type: 'json_schema' as const,
203+
schema: runOpts.outputSchema as Record<string, unknown>,
204+
}
205+
: undefined;
184206
return {
185207
cwd: startOpts.workingDirectory,
186208
allowedTools: startOpts.allowedTools,
@@ -192,6 +214,7 @@ export class ClaudeAdapter implements HeadlessCoder {
192214
model: startOpts.model,
193215
permissionMode,
194216
permissionPromptToolName: startOpts.permissionPromptToolName,
217+
outputFormat,
195218
};
196219
}
197220

@@ -213,9 +236,10 @@ export class ClaudeAdapter implements HeadlessCoder {
213236
ensureNodeRuntime('run Claude');
214237
const state = thread.internal as ClaudeThreadState;
215238
this.assertIdle(state);
216-
const structuredPrompt = applyOutputSchemaPrompt(toPrompt(input), runOpts?.outputSchema);
217-
const prompt = toPrompt(structuredPrompt);
218-
const options = this.buildOptions(state, runOpts);
239+
const useNativeStructuredOutput = shouldUseNativeStructuredOutput(runOpts?.outputSchema);
240+
const promptInput = useNativeStructuredOutput ? input : applyOutputSchemaPrompt(input, runOpts?.outputSchema);
241+
const prompt = toPrompt(promptInput);
242+
const options = this.buildOptions(state, runOpts, useNativeStructuredOutput);
219243
const generator = query({ prompt, options });
220244
const active = this.registerRun(state, generator, runOpts?.signal);
221245
let lastAssistant = '';
@@ -255,7 +279,9 @@ export class ClaudeAdapter implements HeadlessCoder {
255279
if (finalResult && claudeResultIndicatesError(finalResult)) {
256280
throw new Error(buildClaudeResultErrorMessage(finalResult));
257281
}
258-
const structured = runOpts?.outputSchema ? extractJsonPayload(lastAssistant) : undefined;
282+
const structured = runOpts?.outputSchema
283+
? extractNativeStructuredOutput(finalResult) ?? extractJsonPayload(lastAssistant)
284+
: undefined;
259285
return { threadId: state.sessionId, text: lastAssistant, raw: finalResult, json: structured };
260286
}
261287

@@ -281,9 +307,10 @@ export class ClaudeAdapter implements HeadlessCoder {
281307
ensureNodeRuntime('stream Claude events');
282308
const state = thread.internal as ClaudeThreadState;
283309
this.assertIdle(state);
284-
const structuredPrompt = applyOutputSchemaPrompt(toPrompt(input), runOpts?.outputSchema);
285-
const prompt = toPrompt(structuredPrompt);
286-
const options = this.buildOptions(state, runOpts);
310+
const useNativeStructuredOutput = shouldUseNativeStructuredOutput(runOpts?.outputSchema);
311+
const promptInput = useNativeStructuredOutput ? input : applyOutputSchemaPrompt(input, runOpts?.outputSchema);
312+
const prompt = toPrompt(promptInput);
313+
const options = this.buildOptions(state, runOpts, useNativeStructuredOutput);
287314
const generator = query({ prompt, options });
288315
const adapter = this;
289316

pnpm-lock.yaml

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)