@aeye/core 0.5.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +790 -724
- package/aeye-core-prompts.md +179 -178
- package/aeye-core-schema.md +43 -16
- package/aeye-core-tools.md +179 -177
- package/aeye-core-types.md +1 -0
- package/dist/common.d.ts +30 -5
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +134 -129
- package/dist/common.js.map +1 -1
- package/dist/delivery.d.ts +39 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +127 -0
- package/dist/delivery.js.map +1 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +84 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/prompt.d.ts +134 -15
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +220 -164
- package/dist/prompt.js.map +1 -1
- package/dist/schema/analyze.d.ts +94 -0
- package/dist/schema/analyze.d.ts.map +1 -0
- package/dist/schema/analyze.js +221 -0
- package/dist/schema/analyze.js.map +1 -0
- package/dist/schema/any-value.d.ts +32 -0
- package/dist/schema/any-value.d.ts.map +1 -0
- package/dist/schema/any-value.js +77 -0
- package/dist/schema/any-value.js.map +1 -0
- package/dist/schema/budget.d.ts +114 -0
- package/dist/schema/budget.d.ts.map +1 -0
- package/dist/schema/budget.js +223 -0
- package/dist/schema/budget.js.map +1 -0
- package/dist/schema/check-bag.d.ts +28 -0
- package/dist/schema/check-bag.d.ts.map +1 -0
- package/dist/schema/check-bag.js +129 -0
- package/dist/schema/check-bag.js.map +1 -0
- package/dist/schema/consistency.d.ts +27 -0
- package/dist/schema/consistency.d.ts.map +1 -0
- package/dist/schema/consistency.js +110 -0
- package/dist/schema/consistency.js.map +1 -0
- package/dist/schema/cycle-breaker.d.ts +14 -0
- package/dist/schema/cycle-breaker.d.ts.map +1 -0
- package/dist/schema/cycle-breaker.js +157 -0
- package/dist/schema/cycle-breaker.js.map +1 -0
- package/dist/schema/decode.d.ts +31 -0
- package/dist/schema/decode.d.ts.map +1 -0
- package/dist/schema/decode.js +207 -0
- package/dist/schema/decode.js.map +1 -0
- package/dist/schema/descriptor.d.ts +300 -0
- package/dist/schema/descriptor.d.ts.map +1 -0
- package/dist/schema/descriptor.js +15 -0
- package/dist/schema/descriptor.js.map +1 -0
- package/dist/schema/dialects/anthropic.d.ts +34 -0
- package/dist/schema/dialects/anthropic.d.ts.map +1 -0
- package/dist/schema/dialects/anthropic.js +77 -0
- package/dist/schema/dialects/anthropic.js.map +1 -0
- package/dist/schema/dialects/google.d.ts +47 -0
- package/dist/schema/dialects/google.d.ts.map +1 -0
- package/dist/schema/dialects/google.js +91 -0
- package/dist/schema/dialects/google.js.map +1 -0
- package/dist/schema/dialects/lenient.d.ts +4 -0
- package/dist/schema/dialects/lenient.d.ts.map +1 -0
- package/dist/schema/dialects/lenient.js +26 -0
- package/dist/schema/dialects/lenient.js.map +1 -0
- package/dist/schema/dialects/openai.d.ts +40 -0
- package/dist/schema/dialects/openai.d.ts.map +1 -0
- package/dist/schema/dialects/openai.js +81 -0
- package/dist/schema/dialects/openai.js.map +1 -0
- package/dist/schema/dialects/xai.d.ts +23 -0
- package/dist/schema/dialects/xai.d.ts.map +1 -0
- package/dist/schema/dialects/xai.js +53 -0
- package/dist/schema/dialects/xai.js.map +1 -0
- package/dist/schema/enum-values.d.ts +44 -0
- package/dist/schema/enum-values.d.ts.map +1 -0
- package/dist/schema/enum-values.js +56 -0
- package/dist/schema/enum-values.js.map +1 -0
- package/dist/schema/expressible.d.ts +59 -0
- package/dist/schema/expressible.d.ts.map +1 -0
- package/dist/schema/expressible.js +161 -0
- package/dist/schema/expressible.js.map +1 -0
- package/dist/schema/index.d.ts +20 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +20 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/json-schema.d.ts +64 -0
- package/dist/schema/json-schema.d.ts.map +1 -0
- package/dist/schema/json-schema.js +2 -0
- package/dist/schema/json-schema.js.map +1 -0
- package/dist/schema/registry.d.ts +73 -0
- package/dist/schema/registry.d.ts.map +1 -0
- package/dist/schema/registry.js +135 -0
- package/dist/schema/registry.js.map +1 -0
- package/dist/schema/strictify.d.ts +19 -0
- package/dist/schema/strictify.d.ts.map +1 -0
- package/dist/schema/strictify.js +237 -0
- package/dist/schema/strictify.js.map +1 -0
- package/dist/schema/to-json-schema.d.ts +29 -0
- package/dist/schema/to-json-schema.d.ts.map +1 -0
- package/dist/schema/to-json-schema.js +692 -0
- package/dist/schema/to-json-schema.js.map +1 -0
- package/dist/schema/walk-key.d.ts +24 -0
- package/dist/schema/walk-key.d.ts.map +1 -0
- package/dist/schema/walk-key.js +33 -0
- package/dist/schema/walk-key.js.map +1 -0
- package/dist/to.js +1 -1
- package/dist/to.js.map +1 -1
- package/dist/tool.d.ts +38 -4
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +52 -8
- package/dist/tool.js.map +1 -1
- package/dist/types.d.ts +265 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/dist/schema.d.ts +0 -840
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js +0 -2478
- package/dist/schema.js.map +0 -1
package/aeye-core-prompts.md
CHANGED
|
@@ -1,178 +1,179 @@
|
|
|
1
|
-
# @aeye/core — Prompt
|
|
2
|
-
|
|
3
|
-
`Prompt` (`src/prompt.ts`) generates an AI response from a Handlebars-templated message, optionally calls tools in a loop, and validates structured output against a Zod schema with automatic re-prompting.
|
|
4
|
-
|
|
5
|
-
```typescript
|
|
6
|
-
class Prompt<TContext = {}, TMetadata = {}, TName extends string = string,
|
|
7
|
-
TInput extends object = {}, TOutput extends object | string = string,
|
|
8
|
-
TTools extends Tuple<ToolCompatible<TContext, TMetadata>> = [],
|
|
9
|
-
TDecoded extends unknown = TOutput>
|
|
10
|
-
implements Component<...>
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
`TDecoded` is the decoded type a custom `parse` produces from the model's structured output — **any** value (a built AST/class instance, a number, an array, …), not just an object or string; it types `validate` and the decoded output. It defaults to the wire `TOutput` when no custom `parse` is supplied — `schema` always stays the wire type.
|
|
14
|
-
|
|
15
|
-
A `Prompt` only does real work when the context supplies `execute` and/or `stream` (see [types doc](./aeye-core-types.md)). Without them, get/run yields nothing.
|
|
16
|
-
|
|
17
|
-
## `PromptInput` (constructor config)
|
|
18
|
-
|
|
19
|
-
| Field | Type | Notes |
|
|
20
|
-
|------|------|-------|
|
|
21
|
-
| `name` | `string` | Unique identifier. |
|
|
22
|
-
| `description` | `string` | Informational. |
|
|
23
|
-
| `content` | `string` | Handlebars template. `{{tools}}` is auto-appended (wrapped in `<tools>`) when tools exist and you didn't include it. |
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- **`
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
| `'
|
|
72
|
-
| `'
|
|
73
|
-
| `'
|
|
74
|
-
| `'
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
else if (e.type === '
|
|
92
|
-
else if (e.type === '
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
- `
|
|
102
|
-
- `
|
|
103
|
-
-
|
|
104
|
-
- `
|
|
105
|
-
- `
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
- **
|
|
150
|
-
- **
|
|
151
|
-
- **
|
|
152
|
-
- `
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
-
|
|
176
|
-
- `
|
|
177
|
-
- `
|
|
178
|
-
-
|
|
1
|
+
# @aeye/core — Prompt
|
|
2
|
+
|
|
3
|
+
`Prompt` (`src/prompt.ts`) generates an AI response from a Handlebars-templated message, optionally calls tools in a loop, and validates structured output against a Zod schema with automatic re-prompting.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
class Prompt<TContext = {}, TMetadata = {}, TName extends string = string,
|
|
7
|
+
TInput extends object = {}, TOutput extends object | string = string,
|
|
8
|
+
TTools extends Tuple<ToolCompatible<TContext, TMetadata>> = [],
|
|
9
|
+
TDecoded extends unknown = TOutput>
|
|
10
|
+
implements Component<...>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`TDecoded` is the decoded type a custom `parse` produces from the model's structured output — **any** value (a built AST/class instance, a number, an array, …), not just an object or string; it types `validate` and the decoded output. It defaults to the wire `TOutput` when no custom `parse` is supplied — `schema` always stays the wire type.
|
|
14
|
+
|
|
15
|
+
A `Prompt` only does real work when the context supplies `execute` and/or `stream` (see [types doc](./aeye-core-types.md)). Without them, get/run yields nothing.
|
|
16
|
+
|
|
17
|
+
## `PromptInput` (constructor config)
|
|
18
|
+
|
|
19
|
+
| Field | Type | Notes |
|
|
20
|
+
|------|------|-------|
|
|
21
|
+
| `name` | `string` | Unique identifier. |
|
|
22
|
+
| `description` | `string` | Informational. |
|
|
23
|
+
| `content` | `string` | Handlebars template, sent as the system message. `{{tools}}` is auto-appended (wrapped in `<tools>`) when tools exist and you didn't include it. |
|
|
24
|
+
| `userTurn?` | `string` | User message added after the system message when there's no conversation (Anthropic, Bedrock and some OpenRouter hosts require one). Defaults to `ctx.userTurn`, then `DEFAULT_USER_TURN` (`'Begin.'`). |
|
|
25
|
+
| `input?` | `Fn<Record<string,any>, [TInput?, ctx]>` | Maps raw input → template variables. |
|
|
26
|
+
| `schema?` | `Fn<ZodType<TOutput> \| false, [TInput?, ctx]>` | Output schema. `false` → plain-text output. Omitted → text. |
|
|
27
|
+
| `parse?` | `(raw, ctx) => TDecoded \| Error \| Promise<…>` | **Custom output parser that REPLACES Zod.** Receives the structured value **already wire-decoded to the conceptual shape** — core first applies the request's `FormatDescriptor` decode (`decodeWire`: array-of-pairs→record, null→undefined for optionals, numeric-key→tuple, …) so your parser is provider-agnostic and never sees a model family's wire quirks — and fully owns turning it into the decoded `TDecoded`; return (or throw) an `Error` to reject it — routed through the same `outputRetries` channel a Zod failure would, surfacing the error's own `.message` (no Zod vocabulary, since Zod never runs). Lets a caller (e.g. `@aeye/query`'s parser) return a built AST plus compiler-style diagnostics. Only runs where Zod validation runs today (a structured, non-`ZodString` `schema` is present); `schema` is still required and still drives the model wire format. Absent ⇒ unchanged Zod path. |
|
|
28
|
+
| `strict?` | `boolean \| number` | Strict-mode policy, default `1`. See [schema doc](./aeye-core-schema.md). |
|
|
29
|
+
| `config?` | `Fn<Partial<Request> \| false, [TInput?, ctx]>` | Per-request overrides (temperature, model, toolChoice, …). `false` ⇒ prompt not compatible. |
|
|
30
|
+
| `reconfig?` | `(stats: PromptReconfigInput, ctx) => PromptReconfig` | Adapt config after each iteration based on runtime stats. |
|
|
31
|
+
| `tools?` | `TTools` | Tools the model may call. |
|
|
32
|
+
| `toolExecution?` | `'sequential' \| 'parallel' \| 'immediate'` | Default `immediate`. |
|
|
33
|
+
| `toolRetries?` | `number` | Retry failed tool calls. Default 2. |
|
|
34
|
+
| `toolIterations?` | `number` | Max tool-calling loop iterations. Default 3. |
|
|
35
|
+
| `toolsMax?` | `number` | Stop sending tools after this many successes. |
|
|
36
|
+
| `toolsOnly?` | `boolean` | Only call tools, don't generate text. |
|
|
37
|
+
| `toolsComplete?` | `boolean` | Auto-synthesize tool results for unpaired `tool_calls` (abort/interrupt). Default `true`. |
|
|
38
|
+
| `outputRetries?` | `number` | Retries to get valid structured output. Defaults to ctx (2). |
|
|
39
|
+
| `forgetRetries?` | `number` | Context-trimming retries. Defaults to ctx (1). |
|
|
40
|
+
| `validationErrorMaxLength?` | `number` | Truncate validation errors fed back to the model. Default 4096. |
|
|
41
|
+
| `retool?` | `Fn<RetoolResult, [TInput?, ctx]>` | Dynamically select tools (by name or object), or `false` if incompatible. |
|
|
42
|
+
| `dynamic?` | `boolean` | Re-resolve input/content/config/schema/tools each iteration; `undefined` ends the loop. |
|
|
43
|
+
| `excludeMessages?` | `boolean` | Don't include `ctx.messages` when rendering. |
|
|
44
|
+
| `onToolResult?` | `(event: ToolResultEvent<TContext, TMetadata, TTools>) => unknown \| Promise<unknown>` | Intercept each tool's **success** result before the model sees it; the return value is what's presented (serialized like any tool result). Model-facing only. See [Tool result transformer](#tool-result-transformer). |
|
|
45
|
+
| `metadata?` / `metadataFn?` | `TMetadata` / fn | Execution metadata (model, requirements). |
|
|
46
|
+
| `validate?` | `(output, ctx) => void \| Promise<void>` | Post-parse hook; throw to re-prompt. |
|
|
47
|
+
| `applicable?` | `(ctx) => boolean \| Promise<boolean>` | Availability check. |
|
|
48
|
+
|
|
49
|
+
`PromptReconfigInput` stats include: `iteration`, `maxIterations`, `toolParseErrors`, `toolCallErrors`, `tools` (names), `toolSuccesses`, and remaining `toolRetries` / `outputRetries` / `forgetRetries`. `PromptReconfig` lets you return `{ config?, maxIterations?, toolRetries?, outputRetries?, forgetRetries? }`.
|
|
50
|
+
|
|
51
|
+
Four semantics of that hook are load-bearing and are not visible in the types:
|
|
52
|
+
|
|
53
|
+
- **`PromptReconfig.maxIterations` is RELATIVE, not absolute.** The loop assigns `maxIterations = iterations + value`, so returning `N` means "this round plus `N - 1` more". Return `budget - stats.iteration` to set an absolute ceiling. Returning `0` **breaks out immediately** — and with no result parsed yet the run then throws its own generic `failed without a specified error`, so `0` is not a graceful stop.
|
|
54
|
+
- **`PromptReconfigInput.maxIterations` is the ceiling as it stood BEFORE this call** — whatever the previous round set. A decision that must track a ceiling you are raising in the same call has to use your own value, not this field.
|
|
55
|
+
- **The error/success counters are cumulative for the run**, not per round: `toolSuccesses` / `toolParseErrors` / `toolCallErrors` only ever grow, so "did THIS round make progress?" is a delta you keep yourself. `toolRetries` / `outputRetries` / `forgetRetries` are the opposite — they are the *remaining* allowances and count down.
|
|
56
|
+
- **`reconfig` runs at the END of an iteration**, after tool dispatch and output parsing and before the `dynamic` re-resolve — so anything it changes takes effect on the NEXT model call.
|
|
57
|
+
|
|
58
|
+
## Methods
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
get(mode?: PromptGetType, input?: TInput, ctx?: Context): PromptGet<mode, TOutput, TTools>
|
|
62
|
+
run(input?: TInput, ctx?: Context): AsyncGenerator<PromptEvent<TOutput, TTools>, TOutput | undefined>
|
|
63
|
+
applicable(ctx?: Context): Promise<boolean>
|
|
64
|
+
metadata(input?, ctx?): TMetadata | Promise<TMetadata>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `get()` modes
|
|
68
|
+
|
|
69
|
+
| Mode (default `'result'`) | Returns |
|
|
70
|
+
|------|---------|
|
|
71
|
+
| `'result'` | `Promise<TOutput \| undefined>` — final structured output |
|
|
72
|
+
| `'tools'` | `Promise<PromptToolOutput<TTools>[] \| undefined>` — `{ tool, result }[]` |
|
|
73
|
+
| `'stream'` | `AsyncGenerator<PromptEvent, TOutput \| undefined>` — all events |
|
|
74
|
+
| `'streamTools'` | `AsyncGenerator<PromptToolOutput, TOutput \| undefined>` |
|
|
75
|
+
| `'streamContent'` | `AsyncGenerator<string, TOutput \| undefined>` — text deltas only |
|
|
76
|
+
|
|
77
|
+
`run()` is equivalent to `get('stream', …)`.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Structured result
|
|
81
|
+
const r = await prompt.get('result', { text: 'hi' }, { execute, messages: [] });
|
|
82
|
+
|
|
83
|
+
// Stream text deltas
|
|
84
|
+
for await (const chunk of prompt.get('streamContent', { text: 'hi' }, { stream, messages: [] })) {
|
|
85
|
+
process.stdout.write(chunk);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Stream all events
|
|
89
|
+
for await (const e of prompt.get('stream', { text: 'hi' }, { stream, messages: [] })) {
|
|
90
|
+
if (e.type === 'textPartial') process.stdout.write(e.content);
|
|
91
|
+
else if (e.type === 'toolStart') console.log('tool:', e.tool.name);
|
|
92
|
+
else if (e.type === 'toolOutput') console.log('result:', e.result);
|
|
93
|
+
else if (e.type === 'complete') console.log('output:', e.output);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## `PromptEvent` union
|
|
98
|
+
|
|
99
|
+
Every event carries `request: Request`. Types:
|
|
100
|
+
|
|
101
|
+
- `request` (`+ iterations`), `textPartial`, `text`, `textComplete`, `textReset` (`+ reason?`)
|
|
102
|
+
- `refusal`, `reason` / `reasonPartial` (reasoning trace)
|
|
103
|
+
- `toolParseName`, `toolParseArguments` (`+ args`), `toolArgRepairAttempt` (`+ fields, success`)
|
|
104
|
+
- tool lifecycle: `toolStart`, `toolOutput` (`+ result` raw, `+ toModel` presented value), `toolInterrupt`, `toolSuspend`, `toolError` (`+ error, rawArgs?`) — each carries `tool`, `toolCallId` (the model's call id, also on its `role: 'tool'` reply) and `args`. `onToolIntercept(tool, args, ctx, call)` gets the `ToolCall` too. A valid call left unrun because a sibling in the batch was invalid (an unknown tool) is answered `[not run: …; call it again]`
|
|
105
|
+
- `message` (`+ message`), `suspend`, `complete` (`+ output`)
|
|
106
|
+
- `requestUsage`, `responseTokens`, `usage` (`+ usage`)
|
|
107
|
+
|
|
108
|
+
## Prompt with tools
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const advisor = new Prompt({
|
|
112
|
+
name: 'travelAdvisor',
|
|
113
|
+
description: 'Plans a trip',
|
|
114
|
+
content: 'Plan a trip to {{destination}}. Use getWeather to check conditions.',
|
|
115
|
+
input: (i: { destination: string }) => ({ destination: i.destination }),
|
|
116
|
+
tools: [getWeather], // see aeye-core-tools.md
|
|
117
|
+
toolExecution: 'parallel',
|
|
118
|
+
toolIterations: 3,
|
|
119
|
+
schema: z.object({
|
|
120
|
+
recommendations: z.array(z.string()),
|
|
121
|
+
packingList: z.array(z.string()),
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const advice = await advisor.get('result', { destination: 'Paris' }, { execute, messages: [] });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Tool result transformer
|
|
129
|
+
|
|
130
|
+
`onToolResult` intercepts each tool's **successful** result *before* it's handed to the model and returns what the model sees instead. It's fully type-safe: `event` is a discriminated union over the prompt's tools, so **narrowing on `event.tool` types both `event.result` and `event.args`** for that tool. The default (unmatched) branch is the catch-all.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const prompt = new Prompt({
|
|
134
|
+
name: 'searcher',
|
|
135
|
+
description: 'Searches and answers',
|
|
136
|
+
content: 'Answer using search.',
|
|
137
|
+
tools: [searchTool, mathTool], // searchTool → { hits, ids }, mathTool → number
|
|
138
|
+
onToolResult: (event) => {
|
|
139
|
+
if (event.tool === 'search') {
|
|
140
|
+
// event.result is the search result; event.args is { query }
|
|
141
|
+
return `Found ${event.result.hits} hits for "${event.args.query}"`;
|
|
142
|
+
}
|
|
143
|
+
// catch-all: here event is the `math` member (event.result: number)
|
|
144
|
+
return event.result; // pass-through
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- **The return value is what's presented.** It's serialized exactly like an untransformed result — a `string` is used verbatim, anything else is `JSON.stringify`-d — into the `role: 'tool'` message. To pass through unchanged, `return event.result`.
|
|
150
|
+
- **Model-facing only.** `get('tools')`, `streamTools`, and the `toolOutput` event's `result` field always report the RAW result. The presented value is additionally exposed on the `toolOutput` event as `toModel` (equal to `result` when no handler is set).
|
|
151
|
+
- **v1 = success results only.** Errored / suspended / interrupted / synthetic (`toolsComplete`) tool slots BYPASS the handler and keep their existing content. (A future v2 could add a `status: 'success' | 'error'` discriminant so handlers can transform errors too.)
|
|
152
|
+
- **A handler that throws** is treated as a tool error for that slot — the model sees the error content — so the `tool_call` ↔ `role: 'tool'` pairing guarantee still holds. The raw result stays available on `get('tools')`/`streamTools`.
|
|
153
|
+
- `async` handlers are awaited.
|
|
154
|
+
|
|
155
|
+
Compile-time safety: referencing a non-existent tool name, or accessing a field not on the narrowed tool's `result`/`args`, fails to compile.
|
|
156
|
+
|
|
157
|
+
## Token / context-window management
|
|
158
|
+
|
|
159
|
+
When the rendered messages plus reserved output tokens exceed the window, the prompt trims older messages and retries (`forgetRetries`). It removes whole groups, oldest first: a message with the `role: 'tool'` replies after it, so tool calls never lose their replies. System messages, the last user message and the most recent group stay; an agent's tool loop after the last user message is trimmed too. `forget` is `protected`, so a subclass can summarize instead of drop. Supply `estimateUsage` for accuracy:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
await prompt.get('result', { text }, {
|
|
163
|
+
execute, messages: history,
|
|
164
|
+
maxOutputTokens: 2048,
|
|
165
|
+
estimateUsage: (m) => ({ text: { input: Math.ceil(String(m.content).length / 4) } }),
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Suspend / resume (human-in-the-loop)
|
|
170
|
+
|
|
171
|
+
A tool can `throw new PromptSuspend(...)` to stop the loop *without* writing a tool result. The caller receives a `suspend` event whose `request.messages` includes the assistant message with the pending tool call. Persist those messages, do the external work (approval, etc.), append the `role: 'tool'` result message, then re-run the prompt with the combined messages as `ctx.messages`. `PromptSuspend` is never auto-paired even with `toolsComplete: true`.
|
|
172
|
+
|
|
173
|
+
## Gotchas
|
|
174
|
+
|
|
175
|
+
- Default `get` mode is `'result'`; pass the mode explicitly when streaming.
|
|
176
|
+
- `schema` returning `false` disables structured output — output is the raw text string.
|
|
177
|
+
- `config`/`retool` returning `false` marks the prompt incompatible with the context.
|
|
178
|
+
- `toolsMax` is a soft cap unless `toolsOneAtATime` (request flag) is set — it stops *offering* tools after the threshold.
|
|
179
|
+
- Validation errors (Zod / JSON / `validate`) are truncated to `validationErrorMaxLength` before being shown to the model.
|
package/aeye-core-schema.md
CHANGED
|
@@ -31,11 +31,12 @@ A descriptor (see `src/schema.ts` line ~26 for the fully documented interface) d
|
|
|
31
31
|
| `allowAllOf` / `allowAnyOf` / `allowOneOf` | Combinator support. |
|
|
32
32
|
| `allowRootRef` / `allowDefsRef` | Whether `$ref: '#'` / `$ref: '#/$defs/X'` are permitted. Under `allowDefsRef: false` the emitter never produces a `$defs` section at all: a shared node is **inlined** at each use site, and a non-root cycle takes the bounded placeholder (`supportsRecursion` alone does not license a `$ref` the descriptor cannot spell). |
|
|
33
33
|
| `anyEncoding` | How `z.any()` / `z.unknown()` is emitted — see below. |
|
|
34
|
-
| `supportedStringFormats` | `'all'` or a `Set<string>` whitelist. |
|
|
34
|
+
| `supportedStringFormats` | `'all'` (every format JSON Schema 2020-12 defines) or a `Set<string>` whitelist. |
|
|
35
35
|
| `supportsRecursion` | Whether `$ref` self-reference works. |
|
|
36
|
+
| `structuredOutputAlwaysStrict` | The family has no non-strict structured output (Anthropic) — see [below](#structuredoutputalwaysstrict). |
|
|
36
37
|
| `maxStrictTools` / `maxStrictOptionalParams` / `maxStrictUnionTypes` | Per-request slot budgets (`undefined` = no documented limit). |
|
|
37
38
|
| `schemaSizeLimits` | Per-SCHEMA size ceilings — properties, nesting, total string characters, enum values (`undefined` = no documented limit) — see below. |
|
|
38
|
-
| `maxEnumValues` | Opt-in ceiling on how many values ONE emitted `enum` may carry (`undefined` = no cap,
|
|
39
|
+
| `maxEnumValues` | Opt-in ceiling on how many values ONE emitted `enum` may carry (`undefined` = no cap). `GOOGLE_STRICT`/`GOOGLE_NON_STRICT` declare 40, a MEASURED response to a real provider rejection; every other built-in declares none — see below. |
|
|
39
40
|
|
|
40
41
|
### `schemaSizeLimits` — how BIG one strict schema may be
|
|
41
42
|
|
|
@@ -60,7 +61,7 @@ Scope is Structured Outputs — a tool carrying `strict: true`, or a `json_schem
|
|
|
60
61
|
|
|
61
62
|
The measurement is taken from the **Zod** schema rather than from the emitted JSON, so where an encoding is descriptor-dependent it counts the widest one (a record as its `{key, value}` pair form, a tuple as its numeric-key form). Over-counting costs strictness on a schema that would have fitted; under-counting costs an HTTP 400, so the rounding goes up. `$defs` entry names are not counted: they are generated at emit time, few, and short against a 120,000-character budget.
|
|
62
63
|
|
|
63
|
-
### `maxEnumValues` — an opt-in dialect ceiling that WIDENS, and why
|
|
64
|
+
### `maxEnumValues` — an opt-in dialect ceiling that WIDENS, and why only Google sets one
|
|
64
65
|
|
|
65
66
|
Some providers may reject a schema whose `enum` is simply too long. That is **not** one of the per-request budgets above: those are strict-mode allowances the `SchemaBudget` spends across a whole request, degrading an item that no longer fits to the family's non-strict descriptor, whereas this one would fire whatever the strictness, so it is enforced structurally at the point an `enum` is emitted.
|
|
66
67
|
|
|
@@ -75,15 +76,19 @@ Truncating would make the values past the cap **unreachable** — the wire schem
|
|
|
75
76
|
|
|
76
77
|
The trade is enforcement: a widened node no longer rejects a value structurally, so whatever validates the model's output afterwards must. `Tool.parse` does — it runs against the ORIGINAL zod schema, whose `enum` is untouched — so a hallucinated member comes back as a normal, re-promptable validation error.
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
**`GOOGLE_STRICT`/`GOOGLE_NON_STRICT` declare `maxEnumValues: 40`. Every other built-in declares no cap.** The field exists because a large `enum` was believed to make Gemini answer `400 INVALID_ARGUMENT`. An initial round of measurement against `google/gemini-3-flash-preview` through OpenRouter (2026-08-23) looked like it disproved that: single-parameter enums at 50/64/72/80/96/128/256/512/1024/**2048** SYNTHETIC values (`name_0`, `name_1`, …) all returned HTTP 200, as did an eleven-tool roster modelled on a real kind-authoring agent with a 96-value enum on one tool.
|
|
79
80
|
|
|
80
|
-
A
|
|
81
|
+
**That conclusion was wrong, because none of it used the CONTENT of a real session.** A third round replayed the exact wire bytes of an actual product request — `fn_load`'s `names` array, enum-constrained to a real session's 98 known component/fn names (`default_renderer`, `QueryWidget`, `ConfirmPrompt`, `list_widget_types`, …) — and it 400'd, reproducibly, twice, byte-for-byte identical to what was actually sent. Bisecting that real value list: 90 values passes, 93 fails. Bisecting what differs from the earlier synthetic probes: 98 SHORT synthetic values (284 total characters) pass; 206 short values matching the real list's total character count (714 characters) also pass; only the real, longer, identifier-shaped values fail, and only past ~92 of them. So the limit tracks neither raw member count nor total character/byte budget — most likely actual tokenized grammar-state count, which short repetitive synthetic strings compress far below what real identifier text does at the same count or character budget. This is why the first two rounds' synthetic and lightly-realistic probes never tripped it.
|
|
81
82
|
|
|
82
|
-
The
|
|
83
|
+
The widen-to-string fallback this field triggers was verified against the exact failing real schema: dropping `enum` and putting the same 98 values into the `description` of a plain string field returned HTTP 200 with the expected tool call — confirming the fallback mechanism actually fixes the failure it exists for, not just a synthetic stand-in.
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
`40` is not a measured edge — it is a conservative floor with roughly 2x margin under the real one (90 passes / 93 fails) measured on this one real value set. Given the failure tracks content, not just count, a different session's actual name list could plausibly fail at a different count; 40 was also the cap the product that surfaced this ran under in production for months, which is corroborating evidence it holds in practice, not just in this one measurement.
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
The reliability comparison from the earlier rounds still stands and is why a cap is preferable to leaving every enum uncapped "to be safe" at a much lower number: `enum` vs plain-string-with-full-list, 10 trials each over confusable near-duplicate names at 96 and 512 values, scored 10/10 on-target with zero off-list answers for BOTH encodings. So there is no reliability cost to raising this cap later if a higher safe value is measured — only downside risk to leaving it too high.
|
|
88
|
+
|
|
89
|
+
One trap for whoever measures this next: with `max_tokens` at 64, a forced-tool probe can come back `finish_reason: "length"` with no tool call and empty content, which reads exactly like a grammar that produced nothing. It usually is not — this model spends reasoning tokens first. Give a forced-tool probe room before calling an empty completion a failure.
|
|
90
|
+
|
|
91
|
+
So: **set this only where a provider is MEASURED to reject a large enum using the SESSION'S OWN real content, not synthetic stand-ins, and set it conservatively below the measured edge until more value sets are tested.** An `enum` that fits is the more correct encoding, because it is the one the model cannot answer outside of.
|
|
87
92
|
|
|
88
93
|
### `anyEncoding` — and what each mode requires
|
|
89
94
|
|
|
@@ -93,22 +98,42 @@ An open "any JSON value" slot has no single portable spelling, so each dialect p
|
|
|
93
98
|
|------|------|------|
|
|
94
99
|
| `'recursive-strict'` | `$defs/Any` with array-of-pairs records (OpenAI strict has no open objects) | `allowAnyOf`, `allowDefsRef`, `supportsRecursion` |
|
|
95
100
|
| `'recursive-open'` | `$defs/Any` with `additionalProperties: <self>` | `allowAnyOf`, `allowDefsRef`, `supportsRecursion` |
|
|
96
|
-
| `'flat'` | inline non-recursive `anyOf` over every JSON type | `allowAnyOf` |
|
|
101
|
+
| `'flat'` | inline non-recursive `anyOf` over every JSON type (xAI strict) | `allowAnyOf` |
|
|
97
102
|
| `'unconstrained'` | the empty schema `{}` (plus any `description`) | nothing |
|
|
103
|
+
| `'none'` | no spelling exists (Anthropic strict) — the schema is not sent strict | nothing |
|
|
104
|
+
|
|
105
|
+
`'none'` is for a dialect that rejects every spelling of "any": Anthropic strict answers 400 to `{}` ("Empty schema … is not supported"), a typeless schema, `additionalProperties: true` and a recursive `$defs/Any` alike. A schema containing `z.any()` / `z.unknown()`, or a recursion cycle (whose placeholder bottoms out at "any"), is therefore not sendable under it: `SchemaBudget` degrades such a tool to the family's non-strict descriptor (even under `strict: true`), and `canExpress` answers `false` so such an output goes as prompt text.
|
|
98
106
|
|
|
99
107
|
`'unconstrained'` is the only mode available to a dialect that forbids combinators **and** named `$defs` (Google). Per JSON Schema a schema with no assertion keywords validates every instance, which is exactly what "any value" means — so it is lossless as a constraint, and the source schema's `description` still rides along as the human/model-facing signal.
|
|
100
108
|
|
|
109
|
+
### `structuredOutputAlwaysStrict`
|
|
110
|
+
|
|
111
|
+
Anthropic compiles every structured output to a grammar (`output_config.format` is strict by definition, and OpenRouter flags every `json_schema` bound for Claude), so an output "degraded" to the non-strict dialect still earns `too many optional parameters (30) … (limit: 24)`. A descriptor declaring `structuredOutputAlwaysStrict: true` (`ANTHROPIC_STRICT`) changes two things:
|
|
112
|
+
|
|
113
|
+
- `canExpress` also requires the output schema to fit the strict limits **on its own** — the per-schema size limits and the per-request optional-parameter and union budgets. An output that doesn't fit is delivered as prompt text under `schemaDelivery: 'auto'`.
|
|
114
|
+
- `SchemaBudget` never degrades an output to the non-strict variant; it keeps the strict dialect.
|
|
115
|
+
|
|
116
|
+
## What every dialect emits
|
|
117
|
+
|
|
118
|
+
A few emitter rules hold under every descriptor, `LENIENT` included:
|
|
119
|
+
|
|
120
|
+
- **`z.nullable()` always admits `null`**, spelled as an `anyOf` branch (`anyOf: [T, { type: 'null' }]`) wherever the dialect has `anyOf`; a `["T", "null"]` type array is the spelling of last resort (o3's strict function validator rejects it). Earlier versions dropped the null outside OpenAI strict, so a grammar-constrained model could never answer it.
|
|
121
|
+
- **Only real JSON Schema formats are emitted.** `'all'` means the formats JSON Schema 2020-12 defines (`date-time`, `email`, `uri`, `uuid`, …); Zod's own check names that share the field — `regex` from `.regex()`, `cuid`, `starts_with` — are never emitted (DeepInfra answers `Unknown format: regex`). A whitelisted descriptor emits only its list.
|
|
122
|
+
- **Open records drop `propertyNames` for a plain string key**: it constrains nothing, and DeepInfra and Cloudflare reject the keyword (`Unimplemented keys: ["propertyNames"]`). A key schema with real constraints (an enum, a pattern) still emits it.
|
|
123
|
+
- **`strictify` handles `.meta({ id })`.** The strictified clone carries the id as `aid` (zod's global registry refuses a second schema with the same `id`), and `aid` names the same `$defs` entry.
|
|
124
|
+
|
|
101
125
|
## Built-in descriptors
|
|
102
126
|
|
|
103
|
-
|
|
127
|
+
Nine ship frozen and pre-registered:
|
|
104
128
|
|
|
105
129
|
| Export | Family / Strict | Notes |
|
|
106
130
|
|------|------|-------|
|
|
107
131
|
| `OPENAI_STRICT` | openai / strict | records→array-of-pairs, tuples→numeric-key objects, `optional → T \| null`, closed objects, restricted format whitelist. |
|
|
108
|
-
| `ANTHROPIC_STRICT` | anthropic / strict | closed objects, no recursion, no length/range constraints; budgets 20 tools / 24 optional params / 16 unions
|
|
132
|
+
| `ANTHROPIC_STRICT` | anthropic / strict | closed objects, no recursion, no length/range constraints; budgets 20 tools / 24 optional params / 16 unions; format whitelist `date-time`, `time`, `date`, `duration`, `email`, `hostname`, `uri`, `ipv4`, `ipv6`, `uuid`; `anyEncoding: 'none'`; `structuredOutputAlwaysStrict`. |
|
|
109
133
|
| `GOOGLE_STRICT` | google / strict | `prefixItems`, `$ref: '#'` recursion, `propertyOrdering`, restricted format whitelist, `z.any()` → `unconstrained` (Gemini forbids `anyOf` and named `$defs`). |
|
|
134
|
+
| `XAI_STRICT` | xai / strict | `anyOf` and `oneOf`, non-circular `$defs`, `prefixItems` tuples, open records, format whitelist incl. `uri`, `pattern`, length/item/numeric bounds; no `allOf`; `z.any()` → `flat`. |
|
|
110
135
|
| `LENIENT` | lenient / non-strict | No rewrites; everything passes through. Default for unannotated models. |
|
|
111
|
-
| `OPENAI_NON_STRICT`, `ANTHROPIC_NON_STRICT` | family / non-strict | Aliased to `LENIENT` but tagged with the family for diagnostics. |
|
|
136
|
+
| `OPENAI_NON_STRICT`, `ANTHROPIC_NON_STRICT`, `XAI_NON_STRICT` | family / non-strict | Aliased to `LENIENT` but tagged with the family for diagnostics. |
|
|
112
137
|
| `GOOGLE_NON_STRICT` | google / non-strict | `LENIENT` **except** `anyEncoding: 'unconstrained'`. Gemini compiles a decoding grammar whenever a tool call is forced, and rejects a self-referencing `$defs/Any` when it does — which has nothing to do with a per-tool strict flag (Google's function-calling API has none), so the encoding belongs to the dialect. |
|
|
113
138
|
|
|
114
139
|
## Functions
|
|
@@ -126,6 +151,7 @@ hasDescriptorFamily(family: string | undefined): boolean
|
|
|
126
151
|
resolveDescriptor(options: ToJSONSchemaOptions | boolean | FormatDescriptor): FormatDescriptor
|
|
127
152
|
strictestOf(a, b): FormatDescriptor
|
|
128
153
|
strictPriority(requested: boolean | number | undefined): number
|
|
154
|
+
canExpress(schema, descriptor): boolean // can it go out as structured output?
|
|
129
155
|
```
|
|
130
156
|
|
|
131
157
|
- `toJSONSchema` accepts a descriptor, a `ToJSONSchemaOptions` object, or a boolean (back-compat: `true` ⇒ `OPENAI_STRICT`, `false` ⇒ `LENIENT`).
|
|
@@ -151,18 +177,18 @@ toJSONSchema(schema, false); // === LENIENT
|
|
|
151
177
|
|
|
152
178
|
## `SchemaBudget`
|
|
153
179
|
|
|
154
|
-
`new SchemaBudget(
|
|
180
|
+
`new SchemaBudget(tool, output = tool)` allocates strict "slots" across a single request when a descriptor has per-request limits (e.g. Anthropic). Tools are emitted through `tool` and the structured output through `output`; they differ when a model has strict structured output but no strict tools (gpt-audio, for one) or the reverse, so neither kind is sent `strict: true` the model can't take. Only the slot counters are shared, sized from the stricter of the two. `@aeye/ai`'s `buildSchemaBudget` builds it from the model's `'toolsStrict'` and `'structured'` capabilities. Providers call:
|
|
155
181
|
|
|
156
182
|
```typescript
|
|
157
183
|
budget.allocateTool(schema, requested): FormatDescriptor // returns strict or the family's non-strict, decrements
|
|
158
184
|
budget.allocateOutput(schema, requested): FormatDescriptor // output schema; consumes param/union budget, not a tool slot
|
|
159
185
|
```
|
|
160
186
|
|
|
161
|
-
It falls back when `requested === false`, when the schema uses an unrepresentable feature, when the schema exceeds the dialect's `schemaSizeLimits`, or when a positive-number request can't fit the remaining budget (priority order; `true` always wins subject to *budget*, never subject to feasibility or size).
|
|
187
|
+
It falls back when `requested === false`, when the schema uses an unrepresentable feature, when the schema exceeds the dialect's `schemaSizeLimits`, when the schema's optional parameters or unions **alone** exceed the per-request budget (no other item can make it fit), or when a positive-number request can't fit the remaining budget (priority order; `true` always wins subject to *budget*, never subject to feasibility or size). An output under a `structuredOutputAlwaysStrict` descriptor is never degraded — `canExpress` already kept an unfitting one off the wire.
|
|
162
188
|
|
|
163
189
|
**The fallback is the descriptor's own family, non-strict — never the family-blind `LENIENT`.** A dialect rule can outlive strict mode: `GOOGLE_NON_STRICT` exists for exactly one such rule, the `unconstrained` encoding of `z.any()`, because Gemini compiles a decoding grammar whenever a tool call is forced and no per-tool `strict` flag is involved. Degrading to `LENIENT` put the self-referencing `$defs/Any` that encoding was created to avoid straight back on the Google wire — and did so for *every* schema that degrades, which under `GOOGLE_STRICT` (`allowAnyOf: false`) is every recursive gin/query program schema there is. For the other families the change is behaviour-neutral by construction: `OPENAI_NON_STRICT` / `ANTHROPIC_NON_STRICT` are `LENIENT` with a family tag, and a family that registers no non-strict variant resolves to `LENIENT`. Only the `descriptor` id pinned on the tool differs, and it round-trips through `getDescriptorById`.
|
|
164
190
|
|
|
165
|
-
"Unrepresentable" here means a **combinator the descriptor forbids** (`z.union` → `anyOf`, `z.intersection` → `allOf`), because nothing rewrites those away — a schema carrying one under `GOOGLE_STRICT` is a provider HTTP 400. **Recursion is not unrepresentable**: the emitter replaces any back-edge the descriptor cannot spell with a bounded placeholder, so a recursive schema stays strict (looser at the back-edge, never rejected). That is the difference between this check and `canExpress`, which additionally treats a widened back-edge as a reason to fall back — the right answer for a structured *output* (it can be delivered as prompt text instead) and the wrong one for a *tool* (it cannot).
|
|
191
|
+
"Unrepresentable" here means a **combinator the descriptor forbids** (`z.union` → `anyOf`, `z.intersection` → `allOf`), because nothing rewrites those away — a schema carrying one under `GOOGLE_STRICT` is a provider HTTP 400. **Recursion is not unrepresentable**: the emitter replaces any back-edge the descriptor cannot spell with a bounded placeholder, so a recursive schema stays strict (looser at the back-edge, never rejected). The exception is an `anyEncoding: 'none'` dialect (Anthropic strict): the placeholder bottoms out at "any", which it cannot spell, so a cycle — like `z.any()` / `z.unknown()` — degrades the tool there. That is the difference between this check and `canExpress`, which additionally treats a widened back-edge as a reason to fall back — the right answer for a structured *output* (it can be delivered as prompt text instead) and the wrong one for a *tool* (it cannot).
|
|
166
192
|
|
|
167
193
|
## Registering a custom descriptor
|
|
168
194
|
|
|
@@ -186,5 +212,6 @@ Once registered, `toJSONSchema(schema, MY_PROVIDER_STRICT)` / `strictify(...)` w
|
|
|
186
212
|
## Gotchas
|
|
187
213
|
|
|
188
214
|
- `ToolDefinition.parameters` and `ResponseFormat.type` stay **raw Zod**. The matching `strictify` is applied *lazily* by the provider once it knows the model's descriptor, then pinned via `descriptor` id so the validator (`Tool.parse` / Prompt output validation) matches the exact wire shape the model saw.
|
|
189
|
-
- Strict dialects drop features they can't represent (Anthropic: no
|
|
215
|
+
- Strict dialects drop features they can't represent (Anthropic: no numeric ranges, formats outside its whitelist). If a feature matters, check `analyzeSchema` against the descriptor or force `strict: false`.
|
|
216
|
+
- Under Anthropic strict, `z.any()`, `z.unknown()` and recursive schemas never go out strict: such a tool degrades to non-strict and such an output is sent as prompt text.
|
|
190
217
|
- The `JSONSchema` / `JSONSchemaType` / `ToJSONSchemaOptions` / `DescriptorFamily` types are exported for tooling.
|