Skip to content

Ask mode structured outputs#55

Merged
dannyjameswilliams merged 6 commits into
mainfrom
feat/structured_output
Jul 7, 2026
Merged

Ask mode structured outputs#55
dannyjameswilliams merged 6 commits into
mainfrom
feat/structured_output

Conversation

@dannyjameswilliams

Copy link
Copy Markdown
Contributor

Related to PR #1000 on the backend

Uses Zod (equivalent to Pydantic in TS) for models, but allows dicts also.

This PR was written by Claude

@orca-security-eu orca-security-eu Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orca Security Scan Summary

Status Check Issues by priority
Passed Passed Secrets high 0   medium 0   low 0   info 0 View in Orca

Adds an `outputFormat` option to `QueryAgent.ask` / `askStream`, mirroring the
Python client's `output_format`:

- a Zod schema (the Pydantic-model equivalent): the final answer is parsed and
  validated into `z.infer<typeof schema>`, returned as `ParsedAskModeResponse<T>`
- a raw Draft 2020-12 JSON Schema object: the final answer is parsed as JSON,
  returned as `ParsedAskModeResponse<Record<string, unknown>>`
- nothing (default): plain text on `finalAnswer`, returned as `AskModeResponse`

Overloads select the precise return type per input. Zod schemas are serialised
to JSON Schema with `z.toJSONSchema` and sent as `output_format` in the request
body. Adds zod as a dependency and tests for all three modes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/query/agent.ts Outdated
Comment on lines 211 to 219
if (outputFormat === undefined) {
return mapAskModeResponse(json);
}
// Both arms are the same call; the branch only narrows the type
// (Zod schema vs raw JSON Schema) to select the right overload.
return isZodSchema(outputFormat)
? mapAskModeResponse(json, outputFormat)
: mapAskModeResponse(json, outputFormat);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This two-arms-same-call is a little hacky IMO. The reason it's needed here is because outputFormat has type OutputFormat (i.e. z.ZodType | Record<string, unknown>) but mapAskModeResponse has no valid overload for that type (it has overloads for z.ZodType and Record<string, unknown> separately, but not the union.

If you add that additional overload, i.e.,

export function mapAskModeResponse(
  response: ApiAskModeResponse,
): AskModeResponse;
export function mapAskModeResponse<S extends z.ZodType>(
  response: ApiAskModeResponse,
  outputFormat: S,
): ParsedAskModeResponse<z.infer<S>>;
export function mapAskModeResponse(
  response: ApiAskModeResponse,
  outputFormat: Record<string, unknown>,
): ParsedAskModeResponse<Record<string, unknown>>;
export function mapAskModeResponse( // New
  response: ApiAskModeResponse,
  outputFormat: OutputFormat,
): ParsedAskModeResponse<unknown>;
export function mapAskModeResponse(
  response: ApiAskModeResponse,
  outputFormat?: OutputFormat,
): AskModeResponse | ParsedAskModeResponse<unknown> {
  ...
}

then this return can become

const json = await response.json();
return outputFormat === undefined
  ? mapAskModeResponse(json)
  : mapAskModeResponse(json, outputFormat);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweet, I had cursor do this change since I'm not 100% familiar with typescript. Would you mind checking the change is accurate?

Comment thread src/query/agent.ts Outdated
Comment on lines +586 to +592
} else {
// Both arms are the same call; the branch only narrows the type
// (Zod schema vs raw JSON Schema) to select the right overload.
output = isZodSchema(outputFormat)
? mapAskModeResponse(finalState, outputFormat)
: mapAskModeResponse(finalState, outputFormat);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above here:

const finalState = JSON.parse(event.data);
return outputFormat === undefined
  ? mapAskModeResponse(finalState)
  : mapAskModeResponse(finalState, outputFormat);

Comment thread package.json Outdated
"files": [
"dist"
],
"dependencies": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since zod is part of the API of the client, we should list it as a peer dependency, rather than a normal dependency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, thanks!

dannyjameswilliams and others added 4 commits June 24, 2026 16:04
Add an OutputFormat union overload so callers can pass a union-typed
outputFormat directly, removing the two-arms-same-call branches that
only existed to narrow the union for overload resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>
@dannyjameswilliams
dannyjameswilliams merged commit 69ddd76 into main Jul 7, 2026
5 checks passed
@dannyjameswilliams
dannyjameswilliams deleted the feat/structured_output branch July 7, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants