/ src / tools / SyntheticOutputTool / SyntheticOutputTool.ts
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  }