@aeye/core 0.3.7 → 0.3.9
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 +380 -225
- package/aeye-core-prompts.md +171 -0
- package/aeye-core-schema.md +117 -0
- package/aeye-core-tools.md +177 -0
- package/aeye-core-types.md +178 -0
- package/aeye-core.md +88 -0
- package/dist/common.d.ts +36 -0
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +118 -4
- package/dist/common.js.map +1 -1
- package/dist/prompt.d.ts +269 -16
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +673 -279
- package/dist/prompt.js.map +1 -1
- package/dist/schema.d.ts +432 -18
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1403 -209
- package/dist/schema.js.map +1 -1
- package/dist/to.js +1 -0
- package/dist/to.js.map +1 -1
- package/dist/tool.d.ts +134 -17
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +158 -9
- package/dist/tool.js.map +1 -1
- package/dist/types.d.ts +58 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
| `input?` | `Fn<Record<string,any>, [TInput?, ctx]>` | Maps raw input → template variables. |
|
|
25
|
+
| `schema?` | `Fn<ZodType<TOutput> \| false, [TInput?, ctx]>` | Output schema. `false` → plain-text output. Omitted → text. |
|
|
26
|
+
| `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. |
|
|
27
|
+
| `strict?` | `boolean \| number` | Strict-mode policy, default `1`. See [schema doc](./aeye-core-schema.md). |
|
|
28
|
+
| `config?` | `Fn<Partial<Request> \| false, [TInput?, ctx]>` | Per-request overrides (temperature, model, toolChoice, …). `false` ⇒ prompt not compatible. |
|
|
29
|
+
| `reconfig?` | `(stats: PromptReconfigInput, ctx) => PromptReconfig` | Adapt config after each iteration based on runtime stats. |
|
|
30
|
+
| `tools?` | `TTools` | Tools the model may call. |
|
|
31
|
+
| `toolExecution?` | `'sequential' \| 'parallel' \| 'immediate'` | Default `immediate`. |
|
|
32
|
+
| `toolRetries?` | `number` | Retry failed tool calls. Default 2. |
|
|
33
|
+
| `toolIterations?` | `number` | Max tool-calling loop iterations. Default 3. |
|
|
34
|
+
| `toolsMax?` | `number` | Stop sending tools after this many successes. |
|
|
35
|
+
| `toolsOnly?` | `boolean` | Only call tools, don't generate text. |
|
|
36
|
+
| `toolsComplete?` | `boolean` | Auto-synthesize tool results for unpaired `tool_calls` (abort/interrupt). Default `true`. |
|
|
37
|
+
| `outputRetries?` | `number` | Retries to get valid structured output. Defaults to ctx (2). |
|
|
38
|
+
| `forgetRetries?` | `number` | Context-trimming retries. Defaults to ctx (1). |
|
|
39
|
+
| `validationErrorMaxLength?` | `number` | Truncate validation errors fed back to the model. Default 4096. |
|
|
40
|
+
| `retool?` | `Fn<RetoolResult, [TInput?, ctx]>` | Dynamically select tools (by name or object), or `false` if incompatible. |
|
|
41
|
+
| `dynamic?` | `boolean` | Re-resolve input/content/config/schema/tools each iteration; `undefined` ends the loop. |
|
|
42
|
+
| `excludeMessages?` | `boolean` | Don't include `ctx.messages` when rendering. |
|
|
43
|
+
| `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). |
|
|
44
|
+
| `metadata?` / `metadataFn?` | `TMetadata` / fn | Execution metadata (model, requirements). |
|
|
45
|
+
| `validate?` | `(output, ctx) => void \| Promise<void>` | Post-parse hook; throw to re-prompt. |
|
|
46
|
+
| `applicable?` | `(ctx) => boolean \| Promise<boolean>` | Availability check. |
|
|
47
|
+
|
|
48
|
+
`PromptReconfigInput` stats include: `iteration`, `maxIterations`, `toolParseErrors`, `toolCallErrors`, `tools` (names), `toolSuccesses`, and remaining `toolRetries` / `outputRetries` / `forgetRetries`. `PromptReconfig` lets you return `{ config?, maxIterations?, toolRetries?, outputRetries?, forgetRetries? }`.
|
|
49
|
+
|
|
50
|
+
## Methods
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
get(mode?: PromptGetType, input?: TInput, ctx?: Context): PromptGet<mode, TOutput, TTools>
|
|
54
|
+
run(input?: TInput, ctx?: Context): AsyncGenerator<PromptEvent<TOutput, TTools>, TOutput | undefined>
|
|
55
|
+
applicable(ctx?: Context): Promise<boolean>
|
|
56
|
+
metadata(input?, ctx?): TMetadata | Promise<TMetadata>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `get()` modes
|
|
60
|
+
|
|
61
|
+
| Mode (default `'result'`) | Returns |
|
|
62
|
+
|------|---------|
|
|
63
|
+
| `'result'` | `Promise<TOutput \| undefined>` — final structured output |
|
|
64
|
+
| `'tools'` | `Promise<PromptToolOutput<TTools>[] \| undefined>` — `{ tool, result }[]` |
|
|
65
|
+
| `'stream'` | `AsyncGenerator<PromptEvent, TOutput \| undefined>` — all events |
|
|
66
|
+
| `'streamTools'` | `AsyncGenerator<PromptToolOutput, TOutput \| undefined>` |
|
|
67
|
+
| `'streamContent'` | `AsyncGenerator<string, TOutput \| undefined>` — text deltas only |
|
|
68
|
+
|
|
69
|
+
`run()` is equivalent to `get('stream', …)`.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Structured result
|
|
73
|
+
const r = await prompt.get('result', { text: 'hi' }, { execute, messages: [] });
|
|
74
|
+
|
|
75
|
+
// Stream text deltas
|
|
76
|
+
for await (const chunk of prompt.get('streamContent', { text: 'hi' }, { stream, messages: [] })) {
|
|
77
|
+
process.stdout.write(chunk);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Stream all events
|
|
81
|
+
for await (const e of prompt.get('stream', { text: 'hi' }, { stream, messages: [] })) {
|
|
82
|
+
if (e.type === 'textPartial') process.stdout.write(e.content);
|
|
83
|
+
else if (e.type === 'toolStart') console.log('tool:', e.tool.name);
|
|
84
|
+
else if (e.type === 'toolOutput') console.log('result:', e.result);
|
|
85
|
+
else if (e.type === 'complete') console.log('output:', e.output);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## `PromptEvent` union
|
|
90
|
+
|
|
91
|
+
Every event carries `request: Request`. Types:
|
|
92
|
+
|
|
93
|
+
- `request` (`+ iterations`), `textPartial`, `text`, `textComplete`, `textReset` (`+ reason?`)
|
|
94
|
+
- `refusal`, `reason` / `reasonPartial` (reasoning trace)
|
|
95
|
+
- `toolParseName`, `toolParseArguments` (`+ args`), `toolArgRepairAttempt` (`+ fields, success`)
|
|
96
|
+
- tool lifecycle: `toolStart`, `toolOutput` (`+ result` raw, `+ toModel` presented value), `toolInterrupt`, `toolSuspend`, `toolError` (`+ error, rawArgs?`) — each carries `tool` + `args`
|
|
97
|
+
- `message` (`+ message`), `suspend`, `complete` (`+ output`)
|
|
98
|
+
- `requestUsage`, `responseTokens`, `usage` (`+ usage`)
|
|
99
|
+
|
|
100
|
+
## Prompt with tools
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const advisor = new Prompt({
|
|
104
|
+
name: 'travelAdvisor',
|
|
105
|
+
description: 'Plans a trip',
|
|
106
|
+
content: 'Plan a trip to {{destination}}. Use getWeather to check conditions.',
|
|
107
|
+
input: (i: { destination: string }) => ({ destination: i.destination }),
|
|
108
|
+
tools: [getWeather], // see aeye-core-tools.md
|
|
109
|
+
toolExecution: 'parallel',
|
|
110
|
+
toolIterations: 3,
|
|
111
|
+
schema: z.object({
|
|
112
|
+
recommendations: z.array(z.string()),
|
|
113
|
+
packingList: z.array(z.string()),
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const advice = await advisor.get('result', { destination: 'Paris' }, { execute, messages: [] });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Tool result transformer
|
|
121
|
+
|
|
122
|
+
`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.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const prompt = new Prompt({
|
|
126
|
+
name: 'searcher',
|
|
127
|
+
description: 'Searches and answers',
|
|
128
|
+
content: 'Answer using search.',
|
|
129
|
+
tools: [searchTool, mathTool], // searchTool → { hits, ids }, mathTool → number
|
|
130
|
+
onToolResult: (event) => {
|
|
131
|
+
if (event.tool === 'search') {
|
|
132
|
+
// event.result is the search result; event.args is { query }
|
|
133
|
+
return `Found ${event.result.hits} hits for "${event.args.query}"`;
|
|
134
|
+
}
|
|
135
|
+
// catch-all: here event is the `math` member (event.result: number)
|
|
136
|
+
return event.result; // pass-through
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- **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`.
|
|
142
|
+
- **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).
|
|
143
|
+
- **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.)
|
|
144
|
+
- **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`.
|
|
145
|
+
- `async` handlers are awaited.
|
|
146
|
+
|
|
147
|
+
Compile-time safety: referencing a non-existent tool name, or accessing a field not on the narrowed tool's `result`/`args`, fails to compile.
|
|
148
|
+
|
|
149
|
+
## Token / context-window management
|
|
150
|
+
|
|
151
|
+
When the rendered messages plus reserved output tokens exceed the window, the prompt trims older messages and retries (`forgetRetries`). Supply `estimateUsage` for accuracy:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
await prompt.get('result', { text }, {
|
|
155
|
+
execute, messages: history,
|
|
156
|
+
maxOutputTokens: 2048,
|
|
157
|
+
estimateUsage: (m) => ({ text: { input: Math.ceil(String(m.content).length / 4) } }),
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Suspend / resume (human-in-the-loop)
|
|
162
|
+
|
|
163
|
+
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`.
|
|
164
|
+
|
|
165
|
+
## Gotchas
|
|
166
|
+
|
|
167
|
+
- Default `get` mode is `'result'`; pass the mode explicitly when streaming.
|
|
168
|
+
- `schema` returning `false` disables structured output — output is the raw text string.
|
|
169
|
+
- `config`/`retool` returning `false` marks the prompt incompatible with the context.
|
|
170
|
+
- `toolsMax` is a soft cap unless `toolsOneAtATime` (request flag) is set — it stops *offering* tools after the threshold.
|
|
171
|
+
- Validation errors (Zod / JSON / `validate`) are truncated to `validationErrorMaxLength` before being shown to the model.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @aeye/core — Schema & strict mode
|
|
2
|
+
|
|
3
|
+
`src/schema.ts` converts Zod schemas to provider-correct JSON Schema. Each LLM provider implements strict structured output / strict tool args with its *own* JSON-Schema dialect; a `FormatDescriptor` captures one dialect, so a single Zod schema can target all of them. Most apps never call these directly — `@aeye/ai` selects and applies a descriptor per request — but provider authors and advanced users do.
|
|
4
|
+
|
|
5
|
+
## The `strict` policy (tri-state)
|
|
6
|
+
|
|
7
|
+
`Tool.strict` and `Prompt.strict` (and `ToolDefinition.strict` / `ResponseFormat.strict`) are `boolean | number`:
|
|
8
|
+
|
|
9
|
+
| Value | Meaning |
|
|
10
|
+
|------|---------|
|
|
11
|
+
| `true` | **Require** strict. `@aeye/ai` filters model selection to strict-capable models; fails if none qualify. |
|
|
12
|
+
| `false` | **Force** lenient. Standard JSON Schema, no `strict: true` on the wire. |
|
|
13
|
+
| `number > 0` (default `1`) | **Prefer** strict, accept fallback. Higher number = higher priority when more strict items are requested than the model's per-request budget allows. |
|
|
14
|
+
| omitted | Treated as `1`. |
|
|
15
|
+
|
|
16
|
+
Default `1` keeps things working against unannotated models. Use `true` only when strict is non-negotiable.
|
|
17
|
+
|
|
18
|
+
## `FormatDescriptor`
|
|
19
|
+
|
|
20
|
+
A descriptor (see `src/schema.ts` line ~26 for the fully documented interface) describes how to emit JSON Schema for one dialect. Key fields:
|
|
21
|
+
|
|
22
|
+
| Field | Controls |
|
|
23
|
+
|------|----------|
|
|
24
|
+
| `id` | Unique id; pinned onto `request.responseFormat.descriptor` / `ToolDefinition.descriptor` so the validator can re-resolve it. |
|
|
25
|
+
| `family` | `'openai' \| 'anthropic' \| 'google' \| (string & {})`. |
|
|
26
|
+
| `objectAllFieldsRequired` | Whether every property must appear in `required[]`. |
|
|
27
|
+
| `objectClosedByDefault` | Whether objects emit `additionalProperties: false`. |
|
|
28
|
+
| `recordEncoding` | `'array-of-pairs'` vs `'open-record'`. |
|
|
29
|
+
| `tupleEncoding` | `'object-numeric-keys'` vs `'prefix-items'` vs `'items-union'`. |
|
|
30
|
+
| `optionalAsNullable` | Emit optionals as `T \| null` (OpenAI strict) vs drop from `required[]`. |
|
|
31
|
+
| `allowAllOf` / `allowAnyOf` / `allowOneOf` | Combinator support. |
|
|
32
|
+
| `supportedStringFormats` | `'all'` or a `Set<string>` whitelist. |
|
|
33
|
+
| `supportsRecursion` | Whether `$ref` self-reference works. |
|
|
34
|
+
| `maxStrictTools` / `maxStrictOptionalParams` / `maxStrictUnionTypes` | Per-request slot budgets (`undefined` = no documented limit). |
|
|
35
|
+
|
|
36
|
+
## Built-in descriptors
|
|
37
|
+
|
|
38
|
+
Seven ship frozen and pre-registered:
|
|
39
|
+
|
|
40
|
+
| Export | Family / Strict | Notes |
|
|
41
|
+
|------|------|-------|
|
|
42
|
+
| `OPENAI_STRICT` | openai / strict | records→array-of-pairs, tuples→numeric-key objects, `optional → T \| null`, closed objects, restricted format whitelist. |
|
|
43
|
+
| `ANTHROPIC_STRICT` | anthropic / strict | closed objects, no recursion, no length/range constraints; budgets 20 tools / 24 optional params / 16 unions. |
|
|
44
|
+
| `GOOGLE_STRICT` | google / strict | `prefixItems`, `$ref: '#'` recursion, `propertyOrdering`, restricted format whitelist. |
|
|
45
|
+
| `LENIENT` | lenient / non-strict | No rewrites; everything passes through. Default for unannotated models. |
|
|
46
|
+
| `OPENAI_NON_STRICT`, `ANTHROPIC_NON_STRICT`, `GOOGLE_NON_STRICT` | family / non-strict | Aliased to `LENIENT` but tagged with the family for diagnostics. |
|
|
47
|
+
|
|
48
|
+
## Functions
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
toJSONSchema(schema: z.ZodType,
|
|
52
|
+
options: ToJSONSchemaOptions | boolean | FormatDescriptor): JSONSchema
|
|
53
|
+
strictify<S extends z.ZodType>(schema: S, descriptor?: FormatDescriptor): S // default OPENAI_STRICT
|
|
54
|
+
analyzeSchema(schema): SchemaFeatures
|
|
55
|
+
registerDescriptor(descriptor: FormatDescriptor): void
|
|
56
|
+
getDescriptor(family: DescriptorFamily, strict: boolean): FormatDescriptor
|
|
57
|
+
getDescriptorById(id: string | undefined): FormatDescriptor
|
|
58
|
+
hasDescriptorFamily(family: string | undefined): boolean
|
|
59
|
+
resolveDescriptor(options: ToJSONSchemaOptions | boolean | FormatDescriptor): FormatDescriptor
|
|
60
|
+
strictestOf(a, b): FormatDescriptor
|
|
61
|
+
strictPriority(requested: boolean | number | undefined): number
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- `toJSONSchema` accepts a descriptor, a `ToJSONSchemaOptions` object, or a boolean (back-compat: `true` ⇒ `OPENAI_STRICT`, `false` ⇒ `LENIENT`).
|
|
65
|
+
- `strictify` rewrites a Zod schema in place per the descriptor (results cached in a WeakMap; same OOM-safe pattern as `analyzeSchema`).
|
|
66
|
+
- `analyzeSchema` returns `SchemaFeatures`: `{ hasRecursion, optionalParameterCount, unionTypeCount, recordCount, tupleCount }` — cached per schema.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { OPENAI_STRICT, ANTHROPIC_STRICT, getDescriptor, toJSONSchema } from '@aeye/core';
|
|
70
|
+
|
|
71
|
+
const schema = z.object({
|
|
72
|
+
name: z.string(),
|
|
73
|
+
tags: z.record(z.string(), z.string()),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
toJSONSchema(schema, OPENAI_STRICT); // tags → array of {key, value}
|
|
77
|
+
toJSONSchema(schema, ANTHROPIC_STRICT); // tags → array of {key, value}
|
|
78
|
+
toJSONSchema(schema, getDescriptor('google', true)); // tags → standard additionalProperties
|
|
79
|
+
toJSONSchema(schema, true); // === OPENAI_STRICT
|
|
80
|
+
toJSONSchema(schema, false); // === LENIENT
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## `SchemaBudget`
|
|
84
|
+
|
|
85
|
+
`new SchemaBudget(descriptor)` allocates strict "slots" across a single request when a descriptor has per-request limits (e.g. Anthropic). Providers call:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
budget.allocateTool(schema, requested): FormatDescriptor // returns strict or LENIENT, decrements
|
|
89
|
+
budget.allocateOutput(schema, requested): FormatDescriptor // output schema; consumes param/union budget, not a tool slot
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
It returns `LENIENT` when `requested === false`, when the schema uses an unrepresentable feature, or when a positive-number request can't fit the remaining budget (priority order; `true` always wins subject to feasibility).
|
|
93
|
+
|
|
94
|
+
## Registering a custom descriptor
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { registerDescriptor, type FormatDescriptor, OPENAI_STRICT } from '@aeye/core';
|
|
98
|
+
|
|
99
|
+
const MY_PROVIDER_STRICT: FormatDescriptor = {
|
|
100
|
+
...OPENAI_STRICT, // start from a known-good baseline
|
|
101
|
+
id: 'my-provider-strict',
|
|
102
|
+
family: 'openai',
|
|
103
|
+
recordEncoding: 'open-record', // override only the deltas
|
|
104
|
+
tupleEncoding: 'prefix-items',
|
|
105
|
+
supportsRecursion: true,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
registerDescriptor(MY_PROVIDER_STRICT);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Once registered, `toJSONSchema(schema, MY_PROVIDER_STRICT)` / `strictify(...)` work, and the Prompt/Tool validation roundtrip resolves `'my-provider-strict'` by id from the request.
|
|
112
|
+
|
|
113
|
+
## Gotchas
|
|
114
|
+
|
|
115
|
+
- `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.
|
|
116
|
+
- Strict dialects drop features they can't represent (Anthropic: no recursion, no numeric ranges). If a feature matters, check `analyzeSchema` against the descriptor or force `strict: false`.
|
|
117
|
+
- The `JSONSchema` / `JSONSchemaType` / `ToJSONSchemaOptions` / `DescriptorFamily` types are exported for tooling.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# @aeye/core — Tool & Agent
|
|
2
|
+
|
|
3
|
+
Both are `Component`s. A **Tool** is a single callable function exposed to the model (or invoked directly). An **Agent** is plain orchestration code that composes other components — no LLM loop of its own.
|
|
4
|
+
|
|
5
|
+
## Tool
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
class Tool<TContext = {}, TMetadata = {}, TName extends string = string,
|
|
9
|
+
TParams extends object = {}, TOutput = string,
|
|
10
|
+
TRefs extends Tuple<ComponentCompatible<TContext, TMetadata>> = [],
|
|
11
|
+
TDecoded extends unknown = TParams>
|
|
12
|
+
implements Component<...>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`TDecoded` is the decoded type a custom `parse` produces from the raw wire args — **any** value (a built class instance, a number, a `Date`, …), not just an object; it types `call`, `validate`, and `metadataFn`. It defaults to the wire `TParams` when no custom `parse` is supplied — `schema` always stays the wire type.
|
|
16
|
+
|
|
17
|
+
### `ToolInput` (constructor config)
|
|
18
|
+
|
|
19
|
+
| Field | Type | Notes |
|
|
20
|
+
|------|------|-------|
|
|
21
|
+
| `name` | `TName` | Shown to the model; unique per request. |
|
|
22
|
+
| `description` | `string` | Shown to the model. |
|
|
23
|
+
| `descriptionFn?` | `Fn<string, [ctx]>` | Refine description from context. |
|
|
24
|
+
| `instructions?` | `string` | Handlebars usage instructions. |
|
|
25
|
+
| `instructionsFn?` | `Fn<string, [ctx]>` | Context-derived instructions. |
|
|
26
|
+
| `input?` | `Fn<Record<string,any>, [ctx]>` | Variables for the instructions template. |
|
|
27
|
+
| `schema` | `Fn<ZodType<TParams> \| undefined, [ctx]>` | **Required.** Param schema; may depend on ctx. `undefined` ⇒ unavailable. |
|
|
28
|
+
| `strict?` | `boolean \| number` | Strict-mode policy, default `1`. See [schema doc](./aeye-core-schema.md). |
|
|
29
|
+
| `refs?` | `TRefs` | Components passed positionally to `call`. |
|
|
30
|
+
| `call` | `(input: TParams, refs: TRefs, ctx) => TOutput` | **Required.** Implementation (sync or async). |
|
|
31
|
+
| `validate?` | `(input, ctx) => void \| Promise<void>` | Runs after Zod parse; throw to re-prompt. |
|
|
32
|
+
| `maxArgsLength?` | `number` | Reject raw args string over N chars before `JSON.parse` (guards against providers that corrupt large tool args). |
|
|
33
|
+
| `applicable?` | `(ctx) => boolean \| Promise<boolean>` | Availability check. |
|
|
34
|
+
| `metadata?` / `metadataFn?` | `TMetadata` / fn | Execution metadata. |
|
|
35
|
+
|
|
36
|
+
### Methods
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
run(input?: TParams, ctx?: Context): TOutput // call directly
|
|
40
|
+
compile(ctx): Promise<readonly [string, ToolDefinition] | undefined> // build AI tool def
|
|
41
|
+
parse(ctx, args, schema?, descriptor?, onRepairAttempt?): Promise<...> // validate raw JSON args
|
|
42
|
+
applicable(ctx?): Promise<boolean>
|
|
43
|
+
metadata(input?, ctx?): TMetadata | Promise<TMetadata>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`compile` returns `undefined` when the tool isn't applicable / has no schema for the context. `parse` validates the model's JSON `arguments` string against the (descriptor-strictified) schema and runs `validate`; it tolerates one provider misbehavior — top-level JSON-stringified fields are re-parsed and `onRepairAttempt` fires for telemetry. When a custom `parse` is supplied (it REPLACES Zod validation), core first wire-decodes the args to the conceptual shape with the request's `FormatDescriptor` (`decodeWire` — array-of-pairs→record, null→undefined, numeric-key→tuple, …), so a custom parser is provider-agnostic and never sees a model family's wire quirks.
|
|
47
|
+
|
|
48
|
+
### Basic tool
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { Tool } from '@aeye/core';
|
|
52
|
+
import z from 'zod';
|
|
53
|
+
|
|
54
|
+
const getWeather = new Tool({
|
|
55
|
+
name: 'getWeather',
|
|
56
|
+
description: 'Get current weather for a location',
|
|
57
|
+
instructions: 'Use this to look up weather for {{location}}.',
|
|
58
|
+
schema: z.object({
|
|
59
|
+
location: z.string().describe('City name'),
|
|
60
|
+
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
|
|
61
|
+
}),
|
|
62
|
+
call: async (input, _refs, _ctx) => {
|
|
63
|
+
const data = await fetchWeather(input.location, input.units);
|
|
64
|
+
return { temperature: data.temp, condition: data.condition };
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Direct invocation (no model needed):
|
|
69
|
+
const w = await getWeather.run({ location: 'Paris' }, {});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Context-aware schema & applicability
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
const manageData = new Tool({
|
|
76
|
+
name: 'manageData',
|
|
77
|
+
description: 'Manage data records',
|
|
78
|
+
schema: (ctx: Context<MyCtx, {}>) =>
|
|
79
|
+
ctx.userRole === 'admin'
|
|
80
|
+
? z.object({ action: z.enum(['read', 'write', 'delete']) })
|
|
81
|
+
: z.object({ action: z.enum(['read']) }),
|
|
82
|
+
applicable: (ctx) => ctx.isAuthenticated,
|
|
83
|
+
call: async (input, _refs, ctx) => ({ action: input.action, by: ctx.userRole }),
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Business-logic validation
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const placeOrder = new Tool({
|
|
91
|
+
name: 'placeOrder',
|
|
92
|
+
description: 'Place a product order',
|
|
93
|
+
schema: z.object({ itemId: z.string(), quantity: z.number().int().min(1) }),
|
|
94
|
+
validate: async (input) => {
|
|
95
|
+
const inStock = await checkInventory(input.itemId);
|
|
96
|
+
if (inStock < input.quantity) throw new Error(`Only ${inStock} available.`);
|
|
97
|
+
},
|
|
98
|
+
call: async (input) => ({ orderId: crypto.randomUUID(), ...input }),
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Control-flow errors
|
|
103
|
+
|
|
104
|
+
- `ToolInterrupt(message?)` — stop the prompt loop and return control to the caller. With `toolsComplete` a synthetic `[interrupted]` result is paired so the next round-trip is valid.
|
|
105
|
+
- `PromptSuspend(message?)` — suspend without writing a tool result, for human-in-the-loop. See [prompts doc](./aeye-core-prompts.md#suspend--resume-human-in-the-loop). Never auto-paired.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { PromptSuspend } from '@aeye/core';
|
|
109
|
+
|
|
110
|
+
call: async (input, _refs, ctx) => {
|
|
111
|
+
if (await needsApproval(input)) throw new PromptSuspend('awaiting approval');
|
|
112
|
+
return performAction(input);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Also exported: `AnyTool`, `ToolCompatible<TContext, TMetadata>`.
|
|
117
|
+
|
|
118
|
+
## Agent
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
class Agent<TContext = {}, TMetadata = {}, TName extends string = string,
|
|
122
|
+
TInput extends object = {}, TOutput = string,
|
|
123
|
+
TRefs extends Tuple<ComponentCompatible<TContext, TMetadata>> = []>
|
|
124
|
+
implements Component<...>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
An Agent is just typed orchestration — its `call` receives the resolved `refs` tuple positionally and wires components together however you like.
|
|
128
|
+
|
|
129
|
+
### `AgentInput`
|
|
130
|
+
|
|
131
|
+
| Field | Type | Notes |
|
|
132
|
+
|------|------|-------|
|
|
133
|
+
| `name` | `TName` | Identifier. |
|
|
134
|
+
| `description` | `string` | Informational. |
|
|
135
|
+
| `refs` | `TRefs` | **Required.** Components this agent uses. |
|
|
136
|
+
| `call` | `(input: TInput, refs: TRefs, ctx) => TOutput` | **Required.** Orchestration logic. |
|
|
137
|
+
| `applicable?` | `(ctx) => boolean \| Promise<boolean>` | Defaults to "any ref is applicable" (or `true` when no refs). |
|
|
138
|
+
| `metadata?` / `metadataFn?` | `TMetadata` / fn | Execution metadata. |
|
|
139
|
+
|
|
140
|
+
### Methods
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
run(input?: TInput, ctx?: Context): TOutput
|
|
144
|
+
applicable(ctx?): Promise<boolean>
|
|
145
|
+
metadata(input?, ctx?): TMetadata | Promise<TMetadata>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Example
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { Agent } from '@aeye/core';
|
|
152
|
+
|
|
153
|
+
const researchAgent = new Agent({
|
|
154
|
+
name: 'researcher',
|
|
155
|
+
description: 'Researches a topic',
|
|
156
|
+
refs: [searchTool, summarizePrompt, analyzePrompt] as const,
|
|
157
|
+
call: async (input: { topic: string }, [search, summarize, analyze], ctx) => {
|
|
158
|
+
const results = await search.run({ query: input.topic, limit: 5 }, ctx);
|
|
159
|
+
const summaries: string[] = [];
|
|
160
|
+
for (const r of results.items) {
|
|
161
|
+
const s = await summarize.get('result', { text: r.content }, ctx);
|
|
162
|
+
summaries.push(s?.summary ?? '');
|
|
163
|
+
}
|
|
164
|
+
return await analyze.get('result', { topic: input.topic, sources: summaries }, ctx);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const out = await researchAgent.run({ topic: 'Quantum Computing' }, { execute, messages: [] });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Gotchas
|
|
172
|
+
|
|
173
|
+
- Pass `refs` `as const` to keep the tuple's element types (so destructuring is correctly typed).
|
|
174
|
+
- An Agent does **no** AI on its own — any model calls come from the prompts/tools it invokes, which need `execute`/`stream` on the forwarded `ctx`.
|
|
175
|
+
- Tool `schema` returning `undefined` (or `applicable` false) excludes the tool from `compile()` output, so the model never sees it for that context.
|
|
176
|
+
- `call` can be sync or async; `TOutput` flows through `Resolved<>` where the framework awaits it (e.g. tool results in prompt events).
|
|
177
|
+
- A tool's return value is what the model sees by default (strings verbatim, otherwise `JSON.stringify`-d). To transform per-prompt what a tool result looks like to the model without changing the tool, use the prompt's [`onToolResult`](./aeye-core-prompts.md#tool-result-transformer) transformer (model-facing only; the raw result stays on `get('tools')`/`streamTools`).
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# @aeye/core — Type model & utilities
|
|
2
|
+
|
|
3
|
+
The shared, provider-agnostic type model (`src/types.ts`) plus helper functions (`src/common.ts`) and Node resource helpers (`src/to.ts`). All re-exported from `@aeye/core`.
|
|
4
|
+
|
|
5
|
+
## Component interface
|
|
6
|
+
|
|
7
|
+
Every primitive implements `Component`:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
interface Component<TContext = {}, TMetadata = {}, TName extends string = string,
|
|
11
|
+
TInput extends object = {}, TOutput = string,
|
|
12
|
+
TRefs extends Tuple<ComponentCompatible<TContext, TMetadata>> = []> {
|
|
13
|
+
kind: string; // 'prompt' | 'tool' | 'agent' (or custom)
|
|
14
|
+
name: TName;
|
|
15
|
+
description: string;
|
|
16
|
+
refs: TRefs;
|
|
17
|
+
run(...[input, ctx]: OptionalParams<[TInput, Context<...>]>): TOutput;
|
|
18
|
+
applicable(...[ctx]): Promise<boolean>;
|
|
19
|
+
metadata(): TMetadata;
|
|
20
|
+
metadata(input?, ctx?): TMetadata | Promise<TMetadata>;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Useful generic helpers exported alongside it: `AnyComponent`, `ComponentCompatible<C,M>`, `ComponentTuple`, and extractors `ComponentContext<C>`, `ComponentMetadata<C>`, `ComponentInput<C>`, `ComponentOutput<C>`, `ComponentRefs<C>`, `ComponentsAll<C>` (transitive refs), `Names<T>`. Type plumbing: `Tuple<T>` (`[] | [T, ...T[]]`), `Simplify`, `Extend`, `Plus`, `RequiredKeys`, `OptionalParams` (makes trailing params optional when they have no required keys — why `run()`/`get()` can be called with no args).
|
|
25
|
+
|
|
26
|
+
## Context
|
|
27
|
+
|
|
28
|
+
`Context<TContext, TMetadata>` = your `TContext` intersected with core execution fields:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
type Context<TContext, TMetadata> = TContext & {
|
|
32
|
+
messages?: Message[];
|
|
33
|
+
execute?: Executor<TContext, TMetadata>;
|
|
34
|
+
stream?: Streamer<TContext, TMetadata>;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
estimateUsage?: (message: Message) => Usage | undefined;
|
|
37
|
+
maxOutputTokens?: number; // reserve tokens for the model's output
|
|
38
|
+
contextWindow?: number; // override window size for trimming
|
|
39
|
+
outputRetries?: number; // default 2
|
|
40
|
+
forgetRetries?: number; // context-trim retries, default 1
|
|
41
|
+
toolRetries?: number; // default 2
|
|
42
|
+
instance?: Instance<any>; // current execution instance
|
|
43
|
+
runner?: Runner; // override how components execute (see withEvents)
|
|
44
|
+
toolCallId?: string; // set by the prompt loop before a tool's call()
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`Executor` / `Streamer` are the functions you must supply to actually run a prompt:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
type Executor<TContext, TMetadata> =
|
|
52
|
+
(request: Request, context, metadata?, signal?: AbortSignal) => Promise<Response>;
|
|
53
|
+
|
|
54
|
+
type Streamer<TContext, TMetadata> =
|
|
55
|
+
(request: Request, context, metadata?, signal?: AbortSignal) => AsyncGenerator<Chunk, Response>;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Messages & content
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
|
62
|
+
|
|
63
|
+
interface Message {
|
|
64
|
+
role: MessageRole;
|
|
65
|
+
content: string | MessageContent[];
|
|
66
|
+
tokens?: number; // used for automatic context trimming
|
|
67
|
+
name?: string;
|
|
68
|
+
toolCallId?: string; // role: 'tool' — links result to a ToolCall
|
|
69
|
+
toolCalls?: ToolCall[]; // role: 'assistant' — model wants to call tools
|
|
70
|
+
refusal?: string;
|
|
71
|
+
reasoning?: Reasoning;
|
|
72
|
+
cache?: Record<string, any>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface MessageContent { // type: 'text' | 'image' | 'file' | 'audio'
|
|
76
|
+
type: MessageContentType;
|
|
77
|
+
content: Resource; // string | URL | Blob | Uint8Array | stream | ...
|
|
78
|
+
format?: string; // 'png', 'mp3', 'pdf', ...
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface ToolCall { id: string; name: string; arguments: string; } // arguments = JSON string
|
|
82
|
+
interface ToolDefinition {
|
|
83
|
+
name: string; description?: string;
|
|
84
|
+
parameters: z.ZodType<object>; // raw Zod; strictify applied lazily by provider
|
|
85
|
+
strict?: boolean | number;
|
|
86
|
+
descriptor?: string; // FormatDescriptor id chosen at request build time
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Request & Response
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
interface Request extends BaseRequest { // BaseRequest: { model?: ModelInput; extra?: ... }
|
|
94
|
+
name?: string;
|
|
95
|
+
messages: Message[];
|
|
96
|
+
temperature?: number; // 0.0–2.0
|
|
97
|
+
maxTokens?: number;
|
|
98
|
+
topP?: number; // 0.0–1.0
|
|
99
|
+
frequencyPenalty?: number; // -2.0–2.0
|
|
100
|
+
presencePenalty?: number; // -2.0–2.0
|
|
101
|
+
stop?: string | string[];
|
|
102
|
+
logProbabilities?: boolean;
|
|
103
|
+
logitBias?: Record<string, number>;
|
|
104
|
+
tools?: ToolDefinition[];
|
|
105
|
+
toolsOneAtATime?: boolean;
|
|
106
|
+
toolChoice?: ToolChoice; // 'auto' | 'none' | 'required' | { tool: string }
|
|
107
|
+
responseFormat?: ResponseFormat;// 'text' | 'json' | { type: ZodType, strict, descriptor? }
|
|
108
|
+
reason?: { effort?: 'low'|'medium'|'high'; maxTokens?: number };
|
|
109
|
+
cacheKey?: string;
|
|
110
|
+
userKey?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface Response extends BaseResponse { // BaseResponse: { usage?, model, extra? }
|
|
114
|
+
content: string;
|
|
115
|
+
toolCalls?: ToolCall[];
|
|
116
|
+
finishReason: FinishReason; // 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'refusal'
|
|
117
|
+
refusal?: string;
|
|
118
|
+
reasoning?: Reasoning;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface Chunk extends BaseChunk { // streaming delta
|
|
122
|
+
content?: string;
|
|
123
|
+
toolCallNamed?: ToolCall; // name fully received
|
|
124
|
+
toolCallArguments?: ToolCall; // args streaming
|
|
125
|
+
toolCall?: ToolCall; // fully received
|
|
126
|
+
finishReason?: FinishReason;
|
|
127
|
+
refusal?: string;
|
|
128
|
+
reasoning?: Reasoning;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Usage
|
|
133
|
+
|
|
134
|
+
`Usage` mirrors model pricing for cost tracking — per-modality token counts (`text`, `audio`, `image`, `reasoning`, `embeddings`) plus an optional `cost` in dollars. See `src/types.ts` for the full shape.
|
|
135
|
+
|
|
136
|
+
## Instances & Events (lifecycle tracking)
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
type InstanceStatus = 'pending' | 'running' | 'completed' | 'failed' | 'interrupted';
|
|
140
|
+
|
|
141
|
+
interface Instance<C extends AnyComponent> {
|
|
142
|
+
parent?: Instance<AnyComponent>;
|
|
143
|
+
id: string; component: C; context; input; status: InstanceStatus;
|
|
144
|
+
started?; running?; completed?; // timestamps (ms)
|
|
145
|
+
error?; output?; children?: Instance<...>[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface Events<TRoot extends AnyComponent> {
|
|
149
|
+
onStatus?: (node: Instance<...>) => void;
|
|
150
|
+
onChild?: (node, child) => void;
|
|
151
|
+
onPromptEvent?: (instance, event: PromptEvent<...>) => void;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A `Runner` wraps every component execution. Use `withEvents(events)` to build one:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { withEvents } from '@aeye/core';
|
|
159
|
+
|
|
160
|
+
const runner = withEvents({
|
|
161
|
+
onStatus: (i) => console.log(`${i.component.name}: ${i.status}`),
|
|
162
|
+
onPromptEvent: (_i, e) => { if (e.type === 'usage') console.log('tokens', e.usage); },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await prompt.get('result', { text: 'hi' }, { execute, messages: [], runner });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## `common.ts` utilities
|
|
169
|
+
|
|
170
|
+
- `resolveFn(fn, reprocess?)` — normalizes a `Fn<R, A>` (value | promise | function) into `(...args) => Promise<R>`. The pattern behind every `schema`/`config`/`input`/`*Fn` field.
|
|
171
|
+
- Usage helpers: `accumulateUsage(target, add)`, `getInputTokens`, `getOutputTokens`, `getTotalTokens`, `accumulateReasoning`, `getReasoningText`.
|
|
172
|
+
- Conversion helpers: `getModel(input)`, `getResponseFromChunks(chunks, model?)`, `getChunksFromResponse(response)`.
|
|
173
|
+
- Async helpers: `isPromise`, `isAsyncGenerator`, `isSettled`, `resolve`, `yieldAll`, `consumeAll`.
|
|
174
|
+
- `Fn<R, A>`, `FnResult`, `FnArgs`, `FnResolved`, `Resolved<T>` type helpers.
|
|
175
|
+
|
|
176
|
+
## `to.ts` resource helpers (Node only — excluded from the browser build)
|
|
177
|
+
|
|
178
|
+
`getResourceFormat`, `toURL`, `toBase64`, `toText`, `toStream`, `toReadableStream`, `toFile` — convert a `Resource` (string, URL, Blob, Uint8Array, stream, file path, …) to the form a provider needs. Import these only in Node contexts; the browser entry omits them.
|