13. Tool Output And Strict OpenAI Schema
This chapter explains how tool results serve the model, frontend debug, and runtime telemetry at the same time. The core rule is: the model sees text, while the runtime keeps structured metadata.
After reading this chapter, you should understand:
- why internal tool output and model-visible content are separate
- why recoverable errors enter history but fatal errors stop the run
- how timeout and abort enter the unified output path
- why OpenAI strict schema changes optional parameter representation
Background
Real tools raised two practical problems:
- What exactly should a tool return internally?
- What schema should OpenAI strict function calling receive?
Both problems affect production behavior.
Tool Output Contract
The project introduced:
lib/agent-tool-output.tsThe internal output shape is:
type AgentToolOutput =
| { type: 'success'; contentText: string; details?: unknown; notice?: string; truncated?: boolean }
| { type: 'respond_to_model'; error: AgentToolError; details?: unknown }
| { type: 'fatal'; error: AgentToolError; details?: unknown };Model-Visible Serialization
The model does not receive this envelope.
The model receives plain text:
success:
contentText
[optional notice]
respond_to_model:
Error [CODE]: message
fatal:
no function_call_output; stop the runThis keeps model input readable while preserving structured details for logs, debug UI, and future telemetry.
Timeout And Abort
The runtime converts timeout and abort into recoverable model-visible tool outputs:
Error [TIMEOUT]: ...
Error [ABORTED]: ...That lets the model understand that a tool did not complete.
OpenAI Strict Schema Problem
The first browser run with real tools hit an upstream error:
Invalid schema for function 'read': ... 'required' is required ...OpenAI strict tools require every property to appear in required. Optional fields must be represented by allowing null.
Correct Boundary For The Fix
The fix belongs in:
lib/openai-tool-schema.tsnot in the agent-owned tool contract.
The agent tool can say:
path required
offset optional
limit optionalThe OpenAI adapter compiles it to:
required: ['path', 'offset', 'limit']
offset.type = ['number', 'null']
limit.type = ['number', 'null']The runtime Zod parser accepts null for strict-mode optional fields and normalizes it to undefined.
Git Evidence
Relevant commit:
ec40dc3 Add structured tool output contractThe strict schema fix and tests grew out of the first real frontend run after that layer.
Tests
Relevant tests:
tests/openai-tool-schema.test.ts
tests/agent-builtins.test.ts
tests/agent-sampling-loop.test.tsCommon Misunderstandings
Misunderstanding 1: Tools Should Send JSON Envelopes To The Model
Models usually benefit from concise text. ok, metadata, duration, and truncation details are useful to runtime and debug surfaces, not always to model context.
Misunderstanding 2: All Errors Should Be Fatal
Recoverable errors should become tool outputs the model can see, such as path not found or validation failure. Fatal errors are for runtime states that cannot continue.
Misunderstanding 3: OpenAI Strict Schema Is A Minor Provider Detail
Strict schema affects how tool parameters are declared. The internal contract can express optional fields, while the OpenAI wire schema may need required + nullable fields.
Chapter Summary
This chapter splits tool output into model-visible text and runtime/debug metadata. It also fixes timeout, abort, error serialization, and OpenAI strict schema boundaries.
Chapter Checkpoint
The strict-schema compilation rule and the runtime's acceptance of null each have a key-free verification path:
npx tsx --test tests/openai-tool-schema.test.tsMeasured output:
✔ OpenAI strict tool schema marks every property as required (0.853791ms)
✔ OpenAI strict tool schema represents optional properties with null type (0.106375ms)
ℹ tests 2
ℹ pass 2
ℹ fail 0These two cases pin the fix boundary of this chapter: optional parameters are compiled into required + nullable, not omitted from required. Then verify that the runtime side really accepts strict-mode null:
npx tsx --test --test-name-pattern "strict" tests/agent-builtins.test.tsThe measured output is ✔ read accepts OpenAI strict-mode null optional arguments — both sides of the contract, wire schema and Zod parser, are nailed down by tests.