Ask mode structured outputs#55
Conversation
There was a problem hiding this comment.
Orca Security Scan Summary
| Status | Check | Issues by priority | |
|---|---|---|---|
| Secrets | 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>
63fcf4f to
d390979
Compare
| 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); | ||
| } |
There was a problem hiding this comment.
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);There was a problem hiding this comment.
sweet, I had cursor do this change since I'm not 100% familiar with typescript. Would you mind checking the change is accurate?
| } 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); | ||
| } |
There was a problem hiding this comment.
Same as above here:
const finalState = JSON.parse(event.data);
return outputFormat === undefined
? mapAskModeResponse(finalState)
: mapAskModeResponse(finalState, outputFormat);| "files": [ | ||
| "dist" | ||
| ], | ||
| "dependencies": { |
There was a problem hiding this comment.
Since zod is part of the API of the client, we should list it as a peer dependency, rather than a normal dependency.
There was a problem hiding this comment.
done, thanks!
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>
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