SyntheticOutputTool.ts
1 import { Ajv } from 'ajv' 2 import { z } from 'zod/v4' 3 import type { Tool, ToolInputJSONSchema } from '../../Tool.js' 4 import { buildTool, type ToolDef } from '../../Tool.js' 5 import { TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../utils/errors.js' 6 import { lazySchema } from '../../utils/lazySchema.js' 7 import type { PermissionResult } from '../../utils/permissions/PermissionResult.js' 8 import { jsonStringify } from '../../utils/slowOperations.js' 9 10 // Allow any input object since the schema is provided dynamically 11 const inputSchema = lazySchema(() => z.object({}).passthrough()) 12 type InputSchema = ReturnType<typeof inputSchema> 13 14 const outputSchema = lazySchema(() => 15 z.string().describe('Structured output tool result'), 16 ) 17 type OutputSchema = ReturnType<typeof outputSchema> 18 export type Output = z.infer<OutputSchema> 19 20 export const SYNTHETIC_OUTPUT_TOOL_NAME = 'StructuredOutput' 21 22 export function isSyntheticOutputToolEnabled(opts: { 23 isNonInteractiveSession: boolean 24 }): boolean { 25 return opts.isNonInteractiveSession 26 } 27 28 export const SyntheticOutputTool = buildTool({ 29 isMcp: false, 30 isEnabled() { 31 // This tool is only created when conditions are met (see main.tsx where 32 // isSyntheticOutputToolEnabled() gates tool creation). Once created, always enabled. 33 return true 34 }, 35 isConcurrencySafe() { 36 return true 37 }, 38 isReadOnly() { 39 return true 40 }, 41 isOpenWorld() { 42 return false 43 }, 44 name: SYNTHETIC_OUTPUT_TOOL_NAME, 45 searchHint: 'return the final response as structured JSON', 46 maxResultSizeChars: 100_000, 47 async description(): Promise<string> { 48 return 'Return structured output in the requested format' 49 }, 50 async prompt(): Promise<string> { 51 return `Use this tool to return your final response in the requested structured format. You MUST call this tool exactly once at the end of your response to provide the structured output.` 52 }, 53 get inputSchema(): InputSchema { 54 return inputSchema() 55 }, 56 get outputSchema(): OutputSchema { 57 return outputSchema() 58 }, 59 async call(input) { 60 // The tool just validates and returns the input as the structured output 61 return { 62 data: 'Structured output provided successfully', 63 structured_output: input, 64 } 65 }, 66 async checkPermissions(input): Promise<PermissionResult> { 67 // Always allow this tool - it's just returning data 68 return { 69 behavior: 'allow', 70 updatedInput: input, 71 } 72 }, 73 // Minimal UI implementations - this tool is for non-interactive SDK/CLI use 74 renderToolUseMessage(input: Record<string, unknown>) { 75 const keys = Object.keys(input) 76 if (keys.length === 0) return null 77 if (keys.length <= 3) { 78 return keys.map(k => `${k}: ${jsonStringify(input[k])}`).join(', ') 79 } 80 return `${keys.length} fields: ${keys.slice(0, 3).join(', ')}…` 81 }, 82 renderToolUseRejectedMessage() { 83 return 'Structured output rejected' 84 }, 85 renderToolUseErrorMessage() { 86 return 'Structured output error' 87 }, 88 renderToolUseProgressMessage() { 89 return null 90 }, 91 renderToolResultMessage(output: string) { 92 return output 93 }, 94 mapToolResultToToolResultBlockParam(content: string, toolUseID: string) { 95 return { 96 tool_use_id: toolUseID, 97 type: 'tool_result' as const, 98 content, 99 } 100 }, 101 } satisfies ToolDef<InputSchema, Output>) 102 103 type CreateResult = { tool: Tool<InputSchema> } | { error: string } 104 105 // Workflow scripts call agent({schema: BUGS_SCHEMA}) 30-80 times per run with 106 // the same schema object reference. Without caching, each call does 107 // new Ajv() + validateSchema() + compile() (~1.4ms of JIT codegen). Identity 108 // cache brings 80-call workflows from ~110ms to ~4ms Ajv overhead. 109 const toolCache = new WeakMap<object, CreateResult>() 110 111 /** 112 * Create a SyntheticOutputTool configured with the given JSON schema. 113 * Returns {tool} on success or {error} with Ajv's diagnostic message 114 * (e.g. "data/properties/bugs should be object") on invalid schema. 115 */ 116 export function createSyntheticOutputTool( 117 jsonSchema: Record<string, unknown>, 118 ): CreateResult { 119 const cached = toolCache.get(jsonSchema) 120 if (cached) return cached 121 122 const result = buildSyntheticOutputTool(jsonSchema) 123 toolCache.set(jsonSchema, result) 124 return result 125 } 126 127 function buildSyntheticOutputTool( 128 jsonSchema: Record<string, unknown>, 129 ): CreateResult { 130 try { 131 const ajv = new Ajv({ allErrors: true }) 132 const isValidSchema = ajv.validateSchema(jsonSchema) 133 if (!isValidSchema) { 134 return { error: ajv.errorsText(ajv.errors) } 135 } 136 const validateSchema = ajv.compile(jsonSchema) 137 138 return { 139 tool: { 140 ...SyntheticOutputTool, 141 inputJSONSchema: jsonSchema as ToolInputJSONSchema, 142 async call(input) { 143 const isValid = validateSchema(input) 144 if (!isValid) { 145 const errors = validateSchema.errors 146 ?.map(e => `${e.instancePath || 'root'}: ${e.message}`) 147 .join(', ') 148 throw new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS( 149 `Output does not match required schema: ${errors}`, 150 `StructuredOutput schema mismatch: ${(errors ?? '').slice(0, 150)}`, 151 ) 152 } 153 return { 154 data: 'Structured output provided successfully', 155 structured_output: input, 156 } 157 }, 158 }, 159 } 160 } catch (e) { 161 return { error: e instanceof Error ? e.message : String(e) } 162 } 163 }