@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 CHANGED
@@ -19,6 +19,8 @@ Core primitives for building AI agents, tools, and prompts with TypeScript. Prov
19
19
  npm install @aeye/core zod handlebars
20
20
  ```
21
21
 
22
+ > **Note:** `@aeye/core` defines the primitives — you need an executor/streamer to run prompts. These are provided by `@aeye/ai` via `ai.buildCoreContext()`, or you can supply your own `execute` / `stream` functions.
23
+
22
24
  ## Core Concepts
23
25
 
24
26
  ### Components
@@ -46,29 +48,29 @@ const summarizer = new Prompt({
46
48
  name: 'summarize',
47
49
  description: 'Summarizes text concisely',
48
50
  content: 'Summarize the following text:\n\n{{text}}',
49
-
50
- // Transform input
51
+
52
+ // Transform raw input into template variables
51
53
  input: (input: { text: string }) => ({ text: input.text }),
52
-
53
- // Define output schema
54
+
55
+ // Define output schema (structured JSON)
54
56
  schema: z.object({
55
57
  summary: z.string().describe('A concise summary'),
56
- keyPoints: z.array(z.string()).describe('Main points')
57
- })
58
+ keyPoints: z.array(z.string()).describe('Main points'),
59
+ }),
58
60
  });
59
61
 
60
- // Execute with a context that has an executor
62
+ // Execute — ctx must provide execute or stream (from @aeye/ai or custom)
61
63
  const result = await summarizer.get(
62
64
  'result',
63
65
  { text: 'Long article text...' },
64
66
  {
65
- execute: yourAIExecutor, // Executor function from a provider
66
- messages: []
67
+ execute: yourAIExecutor, // (request, ctx, metadata?, signal?) => Promise<Response>
68
+ messages: [],
67
69
  }
68
70
  );
69
71
 
70
- console.log(result.summary);
71
- console.log(result.keyPoints);
72
+ console.log(result?.summary);
73
+ console.log(result?.keyPoints);
72
74
  ```
73
75
 
74
76
  ### Creating Tools
@@ -77,69 +79,73 @@ console.log(result.keyPoints);
77
79
  import { Tool } from '@aeye/core';
78
80
  import z from 'zod';
79
81
 
80
- const weatherTool = new Tool({
82
+ const getWeather = new Tool({
81
83
  name: 'getWeather',
82
84
  description: 'Get current weather for a location',
83
- instructions: 'Use this tool to get weather data',
84
-
85
+ instructions: 'Use this tool to get weather data for {{location}}.',
86
+
85
87
  schema: z.object({
86
88
  location: z.string().describe('City name or coordinates'),
87
- units: z.enum(['celsius', 'fahrenheit']).default('celsius')
89
+ units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
88
90
  }),
89
-
90
- call: async (input, refs, ctx) => {
91
+
92
+ call: async (input, _refs, _ctx) => {
91
93
  const response = await fetch(
92
- `https://api.weather.com/v1/${input.location}`
94
+ `https://api.weather.com/v1/${encodeURIComponent(input.location)}`
93
95
  );
94
96
  const data = await response.json();
95
-
96
97
  return {
97
98
  temperature: data.temp,
98
99
  condition: data.condition,
99
- humidity: data.humidity
100
+ humidity: data.humidity,
100
101
  };
101
- }
102
+ },
102
103
  });
103
104
  ```
104
105
 
105
106
  ### Prompts with Tools
106
107
 
107
108
  ```typescript
109
+ import { Prompt } from '@aeye/core';
110
+ import z from 'zod';
111
+
108
112
  const travelAdvisor = new Prompt({
109
113
  name: 'travelAdvisor',
110
114
  description: 'Provides travel advice based on weather',
111
115
  content: `You are a travel advisor. Help plan a trip to {{destination}}.
112
-
113
- Use the weather tool to check current conditions, then provide:
116
+
117
+ Use the getWeather tool to check current conditions, then provide:
114
118
  - What to pack
115
- - Activities to do
119
+ - Recommended activities
116
120
  - Best times to visit`,
117
-
118
- input: (input: { destination: string }) => ({
119
- destination: input.destination
121
+
122
+ input: (input: { destination: string }) => ({
123
+ destination: input.destination,
120
124
  }),
121
-
122
- tools: [weatherTool],
123
-
125
+
126
+ tools: [getWeather],
127
+
124
128
  schema: z.object({
125
129
  recommendations: z.array(z.string()),
126
130
  packingList: z.array(z.string()),
127
- weatherNotes: z.string()
128
- })
131
+ weatherNotes: z.string(),
132
+ }),
129
133
  });
130
134
 
131
- // The AI will automatically call weatherTool if needed
135
+ // The AI will automatically call getWeather if needed
132
136
  const advice = await travelAdvisor.get(
133
137
  'result',
134
138
  { destination: 'Paris' },
135
139
  { execute: yourAIExecutor, messages: [] }
136
140
  );
141
+
142
+ console.log(advice?.recommendations);
137
143
  ```
138
144
 
139
145
  ### Streaming Responses
140
146
 
141
147
  ```typescript
142
- // Stream content only
148
+ // Stream text content only
143
149
  for await (const chunk of summarizer.get(
144
150
  'streamContent',
145
151
  { text: 'Long text...' },
@@ -155,11 +161,13 @@ for await (const event of summarizer.get(
155
161
  { stream: yourAIStreamer, messages: [] }
156
162
  )) {
157
163
  if (event.type === 'textPartial') {
158
- console.log('Text:', event.content);
164
+ process.stdout.write(event.content);
159
165
  } else if (event.type === 'toolStart') {
160
166
  console.log('Tool started:', event.tool.name);
161
167
  } else if (event.type === 'toolOutput') {
162
168
  console.log('Tool result:', event.result);
169
+ } else if (event.type === 'complete') {
170
+ console.log('Final output:', event.output);
163
171
  }
164
172
  }
165
173
  ```
@@ -168,40 +176,39 @@ for await (const event of summarizer.get(
168
176
 
169
177
  ```typescript
170
178
  import { Agent } from '@aeye/core';
179
+ import z from 'zod';
171
180
 
181
+ // Assume searchTool, summarizePrompt, and analyzePrompt are defined elsewhere
172
182
  const researchAgent = new Agent({
173
183
  name: 'researcher',
174
- description: 'Conducts research on topics',
175
-
176
- refs: [searchTool, summarizeTool, analyzeTool],
177
-
178
- call: async (input: { topic: string }, [search, summarize, analyze], ctx) => {
184
+ description: 'Conducts research on a topic',
185
+
186
+ refs: [searchTool, summarizePrompt, analyzePrompt] as const,
187
+
188
+ call: async (
189
+ input: { topic: string },
190
+ [search, summarize, analyze],
191
+ ctx
192
+ ) => {
179
193
  // Step 1: Search for information
180
- const searchResults = await search.run(
181
- { query: input.topic, limit: 5 },
182
- ctx
183
- );
184
-
185
- // Step 2: Summarize findings
186
- const summaries = [];
187
- for (const result of searchResults) {
188
- const summary = await summarize.get(
189
- 'result',
190
- { text: result.content },
191
- ctx
192
- );
193
- summaries.push(summary);
194
+ const searchResults = await search.run({ query: input.topic, limit: 5 }, ctx);
195
+
196
+ // Step 2: Summarize each result
197
+ const summaries: string[] = [];
198
+ for (const result of searchResults.items) {
199
+ const summary = await summarize.get('result', { text: result.content }, ctx);
200
+ summaries.push(summary?.summary ?? '');
194
201
  }
195
-
196
- // Step 3: Analyze and synthesize
202
+
203
+ // Step 3: Synthesize into a final analysis
197
204
  const analysis = await analyze.get(
198
205
  'result',
199
206
  { topic: input.topic, sources: summaries },
200
207
  ctx
201
208
  );
202
-
209
+
203
210
  return analysis;
204
- }
211
+ },
205
212
  });
206
213
 
207
214
  const research = await researchAgent.run(
@@ -212,15 +219,15 @@ const research = await researchAgent.run(
212
219
 
213
220
  ## Prompt Modes
214
221
 
215
- The `get` method supports different execution modes:
222
+ The `get(mode, input?, ctx?)` method supports different execution modes:
216
223
 
217
224
  | Mode | Description | Returns |
218
225
  |------|-------------|---------|
219
- | `'result'` | Get final structured output | `TOutput` |
220
- | `'tools'` | Get tool call results | `PromptToolOutput[]` |
221
- | `'stream'` | Stream all events | `AsyncGenerator<PromptEvent>` |
222
- | `'streamTools'` | Stream tool outputs | `AsyncGenerator<PromptToolOutput>` |
223
- | `'streamContent'` | Stream text content only | `AsyncGenerator<string>` |
226
+ | `'result'` | Await the final structured output | `Promise<TOutput \| undefined>` |
227
+ | `'tools'` | Await all tool call results | `Promise<PromptToolOutput[] \| undefined>` |
228
+ | `'stream'` | Stream all events | `AsyncGenerator<PromptEvent<TOutput, TTools>, TOutput \| undefined>` |
229
+ | `'streamTools'` | Stream tool output events | `AsyncGenerator<PromptToolOutput<TTools>, TOutput \| undefined>` |
230
+ | `'streamContent'` | Stream text content only | `AsyncGenerator<string, TOutput \| undefined>` |
224
231
 
225
232
  ```typescript
226
233
  // Get structured result
@@ -231,95 +238,106 @@ const tools = await prompt.get('tools', input, ctx);
231
238
 
232
239
  // Stream everything
233
240
  for await (const event of prompt.get('stream', input, ctx)) {
234
- // Handle different event types
241
+ // event.type is one of: 'request', 'textPartial', 'text', 'toolStart',
242
+ // 'toolOutput', 'toolError', 'message', 'complete', 'usage', ...
243
+ }
244
+
245
+ // Stream text only
246
+ for await (const text of prompt.get('streamContent', input, ctx)) {
247
+ process.stdout.write(text);
235
248
  }
236
249
  ```
237
250
 
238
251
  ## Tool Execution Modes
239
252
 
240
- Control how tools are executed:
253
+ Control how tools are executed by the prompt:
241
254
 
242
255
  ```typescript
243
256
  const prompt = new Prompt({
244
257
  name: 'multi-tool',
245
258
  description: 'Uses multiple tools',
246
- content: 'Analyze the data',
259
+ content: 'Analyze this data using the available tools.',
247
260
  tools: [tool1, tool2, tool3],
248
-
249
- // Tool execution mode
250
- toolExecution: 'parallel', // 'sequential' | 'parallel' | 'immediate'
251
-
252
- // Retry configuration
253
- toolRetries: 2, // Retry failed tools
254
- toolIterations: 3, // Max iterations for tool calls
255
- toolsMax: 5, // Max total tool calls
261
+
262
+ // 'sequential' | 'parallel' | 'immediate' (default: 'immediate')
263
+ toolExecution: 'parallel',
264
+
265
+ // Number of times to retry a failed tool call (default: 2)
266
+ toolRetries: 2,
267
+ // Max tool-calling iterations per request (default: 3)
268
+ toolIterations: 3,
269
+ // Max total successful tool calls before stopping (no strict limit by default)
270
+ toolsMax: 5,
256
271
  });
257
272
  ```
258
273
 
259
- - **`sequential`** - Wait for each tool to finish before continuing
260
- - **`parallel`** - Start all tools at once, wait for all to complete
261
- - **`immediate`** - Start tools immediately as they're available
274
+ - **`sequential`** - Wait for each tool to complete before starting the next
275
+ - **`parallel`** - Start all pending tools at once and wait for all to complete
276
+ - **`immediate`** - Start each tool as soon as its call is received (default)
262
277
 
263
278
  ## Advanced Features
264
279
 
265
280
  ### Context-Aware Tools
266
281
 
282
+ Tool schemas and applicability can depend on the current context:
283
+
267
284
  ```typescript
285
+ interface MyContext {
286
+ userRole: 'admin' | 'viewer';
287
+ isAuthenticated: boolean;
288
+ }
289
+
268
290
  const contextTool = new Tool({
269
- name: 'contextAware',
270
- description: 'A context-aware tool',
271
- instructions: 'Use this tool...',
272
-
273
- // Schema can depend on context
274
- schema: (ctx) => {
291
+ name: 'manageData',
292
+ description: 'Manage data records',
293
+ instructions: 'Use this tool to read or write data.',
294
+
295
+ // Schema varies based on the caller's role
296
+ schema: (ctx: Context<MyContext, {}>) => {
275
297
  if (ctx.userRole === 'admin') {
276
- return z.object({
277
- action: z.enum(['read', 'write', 'delete'])
278
- });
298
+ return z.object({ action: z.enum(['read', 'write', 'delete']) });
279
299
  }
280
- return z.object({
281
- action: z.enum(['read'])
282
- });
300
+ return z.object({ action: z.enum(['read']) });
283
301
  },
284
-
285
- // Check if tool is applicable
286
- applicable: (ctx) => {
287
- return ctx.isAuthenticated === true;
302
+
303
+ // Tool is only available to authenticated users
304
+ applicable: (ctx: Context<MyContext, {}>) => ctx.isAuthenticated,
305
+
306
+ call: async (input, _refs, ctx) => {
307
+ return { action: input.action, executedBy: ctx.userRole };
288
308
  },
289
-
290
- call: async (input, refs, ctx) => {
291
- // Implementation with context access
292
- }
293
309
  });
294
310
  ```
295
311
 
296
312
  ### Custom Validation
297
313
 
314
+ Add business-logic validation beyond Zod schema checks:
315
+
298
316
  ```typescript
299
- const validatedTool = new Tool({
317
+ const placeOrder = new Tool({
300
318
  name: 'placeOrder',
301
- description: 'Place an order',
302
-
319
+ description: 'Place a product order',
320
+
303
321
  schema: z.object({
304
322
  itemId: z.string(),
305
- quantity: z.number().min(1)
323
+ quantity: z.number().int().min(1),
306
324
  }),
307
-
308
- // Additional validation beyond schema
325
+
326
+ // Runs after schema parsing succeeds; throw to trigger re-prompting
309
327
  validate: async (input, ctx) => {
310
328
  const inventory = await checkInventory(input.itemId);
311
329
  if (inventory < input.quantity) {
312
- throw new Error(`Only ${inventory} items available`);
330
+ throw new Error(`Only ${inventory} units of ${input.itemId} are available.`);
313
331
  }
314
332
  },
315
-
316
- call: async (input, refs, ctx) => {
317
- // Place order
318
- }
333
+
334
+ call: async (input, _refs, _ctx) => {
335
+ return { orderId: crypto.randomUUID(), ...input };
336
+ },
319
337
  });
320
338
  ```
321
339
 
322
- ### Event Tracking
340
+ ### Event Tracking with `withEvents`
323
341
 
324
342
  ```typescript
325
343
  import { withEvents } from '@aeye/core';
@@ -327,164 +345,291 @@ import { withEvents } from '@aeye/core';
327
345
  const runner = withEvents({
328
346
  onStatus: (instance) => {
329
347
  console.log(`${instance.component.name}: ${instance.status}`);
330
- if (instance.status === 'completed') {
331
- const duration = instance.completed - instance.started;
332
- console.log(`Took ${duration}ms`);
348
+ if (instance.status === 'completed' && instance.completed && instance.running) {
349
+ console.log(` Took ${instance.completed - instance.running}ms`);
333
350
  }
334
351
  },
335
-
336
- onPromptEvent: (instance, event) => {
352
+
353
+ onPromptEvent: (_instance, event) => {
337
354
  if (event.type === 'usage') {
338
355
  console.log('Tokens used:', event.usage);
339
356
  }
340
- }
357
+ },
341
358
  });
342
359
 
343
360
  const result = await prompt.get(
344
361
  'result',
345
362
  { text: 'Hello' },
346
- {
347
- execute: yourAIExecutor,
348
- messages: [],
349
- runner
350
- }
363
+ { execute: yourAIExecutor, messages: [], runner }
351
364
  );
352
365
  ```
353
366
 
354
367
  ### Token Management
355
368
 
356
- Automatic conversation trimming when token limits are reached:
369
+ The prompt automatically trims old messages when the context window is full:
357
370
 
358
371
  ```typescript
359
- const context = {
360
- execute: yourAIExecutor,
361
- messages: conversationHistory,
362
-
363
- // Token configuration
364
- defaultCompletionTokens: 2048,
365
-
366
- // Custom token estimation
367
- estimateTokens: (message) => {
368
- return message.content.length / 4; // Rough estimate
369
- }
370
- };
372
+ const result = await prompt.get(
373
+ 'result',
374
+ { text: 'Query' },
375
+ {
376
+ execute: yourAIExecutor,
377
+ messages: conversationHistory, // Will be trimmed if needed
371
378
 
372
- // Prompt will automatically trim messages if needed
373
- const result = await prompt.get('result', { text: 'Query' }, context);
379
+ // Reserve this many tokens for the model's response
380
+ maxOutputTokens: 2048,
381
+
382
+ // Custom per-message token estimator
383
+ estimateUsage: (message) => ({
384
+ text: { input: Math.ceil((message.content as string).length / 4) }
385
+ }),
386
+ }
387
+ );
374
388
  ```
375
389
 
376
390
  ### Dynamic Reconfiguration
377
391
 
392
+ Use `reconfig` to adapt the prompt based on runtime statistics:
393
+
378
394
  ```typescript
379
395
  const adaptivePrompt = new Prompt({
380
396
  name: 'adaptive',
381
- description: 'Adapts based on execution stats',
397
+ description: 'Adapts based on runtime stats',
382
398
  content: 'Solve: {{problem}}',
383
399
  schema: z.object({ solution: z.string() }),
384
-
385
- // Adjust configuration during execution
386
- reconfig: (stats, ctx) => {
387
- // If tools are failing, change strategy
400
+
401
+ reconfig: (stats, _ctx) => {
402
+ // If many tool errors occurred, switch to one-at-a-time mode
388
403
  if (stats.toolCallErrors > 3) {
389
- return {
390
- config: { toolsOneAtATime: true },
391
- maxIterations: 2
392
- };
404
+ return { config: { toolsOneAtATime: true } };
393
405
  }
394
406
  return {};
395
- }
407
+ },
408
+ });
409
+ ```
410
+
411
+ ## Model Capabilities
412
+
413
+ @aeye uses a capability system for automatic model selection. Models advertise their capabilities and the library selects the best match for each request:
414
+
415
+ | Capability | Description |
416
+ |------------|-------------|
417
+ | `chat` | Basic text completion |
418
+ | `streaming` | Real-time response streaming |
419
+ | `image` | Image generation |
420
+ | `vision` | Image understanding |
421
+ | `audio` | Text-to-speech |
422
+ | `hearing` | Speech-to-text |
423
+ | `embedding` | Text embeddings |
424
+ | `tools` | Function/tool calling |
425
+ | `json` | JSON output mode |
426
+ | `structured` | Structured outputs with schemas |
427
+ | `toolsStrict` | Strict-mode tool input schemas (auto-derived; see [Strict Mode](#strict-mode)) |
428
+ | `reasoning` | Extended chain-of-thought reasoning |
429
+ | `zdr` | Zero data retention |
430
+
431
+ ## Strict Mode
432
+
433
+ Each LLM provider that supports strict mode does so with its own JSON Schema dialect. `@aeye/core` reconciles them under a single `FormatDescriptor` so a Zod schema authored once works against any of them.
434
+
435
+ ### `strict` on Tool / Prompt
436
+
437
+ `Tool.strict` and `Prompt.strict` are `boolean | number`:
438
+
439
+ | Value | Meaning |
440
+ |---|---|
441
+ | `true` | **Require** strict. `@aeye/ai` filters selection down to models that declare strict support; the request fails if none qualify. |
442
+ | `false` | **Force** lenient. Standard JSON Schema; no `strict: true` on the wire. |
443
+ | `number > 0` (default `1`) | **Prefer** strict, accept fallback. Higher numbers win when there are more strict-requesting items than the chosen model's per-request budget allows. |
444
+ | omitted | Treated as `1`. |
445
+
446
+ ```typescript
447
+ new Tool({ name: 'must', /* default 1 */ schema: z.object({...}) }); // best-effort
448
+ new Tool({ name: 'critical', strict: true, schema: z.object({...}) }); // hard require
449
+ new Tool({ name: 'meh', strict: false, schema: z.object({...}) }); // never strict
450
+ new Tool({ name: 'top', strict: 100, schema: z.object({...}) }); // high priority
451
+ ```
452
+
453
+ Default `1` keeps "it just works" against unannotated models — strict where the chosen model supports it, lenient elsewhere. Set `true` only when strict is non-negotiable.
454
+
455
+ ### Native format descriptors
456
+
457
+ Seven descriptors ship out of the box:
458
+
459
+ | Descriptor | Family / Strict | Notes |
460
+ |---|---|---|
461
+ | `OPENAI_STRICT` | openai / strict | Records → array-of-pairs, tuples → numeric-key objects, `optional → T \| null`, closed objects, restricted format whitelist. |
462
+ | `ANTHROPIC_STRICT` | anthropic / strict | Closed objects, no recursion, no length / numeric range constraints, per-request slot budgets (20 strict tools / 24 optional params / 16 union types). |
463
+ | `GOOGLE_STRICT` | google / strict | `prefixItems`, `$ref: '#'` recursion, `propertyOrdering` emitted, restricted format whitelist. |
464
+ | `LENIENT` | lenient / non-strict | No rewrites, all features pass through. Default for unannotated models. |
465
+ | `OPENAI_NON_STRICT`, `ANTHROPIC_NON_STRICT`, `GOOGLE_NON_STRICT` | family / non-strict | Aliased to `LENIENT` but tagged with the family for diagnostics. |
466
+
467
+ ```typescript
468
+ import { OPENAI_STRICT, ANTHROPIC_STRICT, getDescriptor, toJSONSchema } from '@aeye/core';
469
+
470
+ const schema = z.object({
471
+ name: z.string(),
472
+ tags: z.record(z.string(), z.string()),
396
473
  });
474
+
475
+ // Same Zod, different wire shapes:
476
+ toJSONSchema(schema, OPENAI_STRICT); // tags → array of {key, value}
477
+ toJSONSchema(schema, ANTHROPIC_STRICT); // tags → array of {key, value} (open records unsupported)
478
+ toJSONSchema(schema, getDescriptor('google', true)); // tags → standard additionalProperties
479
+
480
+ // Backward-compat overloads still work:
481
+ toJSONSchema(schema, true); // === OPENAI_STRICT
482
+ toJSONSchema(schema, false); // === LENIENT
483
+ ```
484
+
485
+ ### Registering a custom descriptor
486
+
487
+ You can ship your own descriptor for a provider or dialect that isn't supported natively. Define it as a `FormatDescriptor` value and register it with `registerDescriptor(descriptor)`:
488
+
489
+ ```typescript
490
+ import { registerDescriptor, type FormatDescriptor, OPENAI_STRICT } from '@aeye/core';
491
+
492
+ const MY_PROVIDER_STRICT: FormatDescriptor = {
493
+ ...OPENAI_STRICT, // start from a known good baseline
494
+ id: 'my-provider-strict',
495
+ family: 'openai', // or 'anthropic' | 'google' | 'lenient'
496
+ // Override only the fields that differ from the baseline:
497
+ recordEncoding: 'open-record', // your provider supports additionalProperties: <schema>
498
+ tupleEncoding: 'prefix-items',
499
+ supportsRecursion: true,
500
+ };
501
+
502
+ registerDescriptor(MY_PROVIDER_STRICT);
397
503
  ```
398
504
 
505
+ Once registered:
506
+
507
+ - Provider code can pass the descriptor to `toJSONSchema(schema, MY_PROVIDER_STRICT)` and `strictify(schema, MY_PROVIDER_STRICT)`.
508
+ - The Prompt's validation roundtrip (which looks up descriptors by id from the request) will resolve `'my-provider-strict'` correctly.
509
+ - A custom `Provider` implementation can return this descriptor from its `getStrictFormat`-equivalent helper.
510
+
511
+ The descriptor's full field list is exported as the `FormatDescriptor` interface — every flag is documented inline. Common adjustments:
512
+
513
+ | Field | Controls |
514
+ |---|---|
515
+ | `objectAllFieldsRequired` | Whether every property must appear in `required[]`. |
516
+ | `objectClosedByDefault` | Whether every object emits `additionalProperties: false`. |
517
+ | `recordEncoding` | `'array-of-pairs'` vs `'open-record'`. |
518
+ | `tupleEncoding` | `'object-numeric-keys'` vs `'prefix-items'` vs `'items-union'`. |
519
+ | `allowAllOf` / `allowAnyOf` / `allowOneOf` | JSON-Schema combinator support. |
520
+ | `supportedStringFormats` | `'all'` or a `Set<string>` whitelist (`'date-time'`, `'email'`, etc.). |
521
+ | `optionalAsNullable` | Whether `optional` fields are emitted as `T \| null` (OpenAI strict) vs omitted from `required[]`. |
522
+ | `supportsRecursion` | Whether `$ref` self-reference works under this dialect. |
523
+ | `maxStrictTools` / `maxStrictOptionalParams` / `maxStrictUnionTypes` | Per-request budget caps; `undefined` means no documented limit. |
524
+
525
+ You don't need to set every field — spread an existing descriptor and override deltas. Tested via `analyzeSchema` + the schema test suite; see the strict-mode guide in `@aeye/docs` for end-to-end examples.
526
+
399
527
  ## API Reference
400
528
 
401
529
  ### Prompt
402
530
 
403
531
  ```typescript
404
- class Prompt<TContext, TMetadata, TName, TInput, TOutput, TTools>
532
+ class Prompt<TContext, TMetadata, TName extends string, TInput extends object, TOutput extends object | string, TTools extends Tuple<ToolCompatible<TContext, TMetadata>>, TDecoded extends unknown = TOutput>
405
533
  ```
406
534
 
407
- **Constructor Options:**
535
+ `TDecoded` is the type a custom `parse` produces from the model's structured output (e.g. a built AST/class instance); it types `validate` and the decoded output. It defaults to `TOutput` (the wire type) when no custom `parse` is supplied — `schema` always stays the wire type.
536
+
537
+ **Constructor Options (`PromptInput`):**
408
538
  - `name: string` - Unique identifier
409
539
  - `description: string` - Purpose description
410
- - `content: string` - Handlebars template
411
- - `input?: Function` - Transform input for template
412
- - `schema?: ZodType | Function` - Output schema
413
- - `config?: Partial<Request> | Function` - AI request config
414
- - `tools?: Tool[]` - Available tools
415
- - `toolExecution?: 'sequential' | 'parallel' | 'immediate'`
416
- - `toolRetries?: number` - Retry failed tools
417
- - `toolIterations?: number` - Max tool call iterations
418
- - `outputRetries?: number` - Retry invalid outputs
419
- - `metadata?: TMetadata` - Execution metadata
420
- - `validate?: Function` - Post-validation hook
421
- - `applicable?: Function` - Applicability check
540
+ - `content: string` - Handlebars template for the system/user message
541
+ - `input?: (input: TInput, ctx: Context<TContext, TMetadata>) => Record<string, any>` - Maps raw input to template variables
542
+ - `schema?: ZodType<TOutput> | ((input: TInput | undefined, ctx: Context<TContext, TMetadata>) => ZodType<TOutput> | false)` - Output schema; `false` disables structured output
543
+ - `strict?: boolean` - Enforce strict Zod schema (default: `true`)
544
+ - `config?: Partial<Request> | ((input: TInput | undefined, ctx: Context<TContext, TMetadata>) => Partial<Request> | false)` - AI request overrides
545
+ - `tools?: TTools` - Tools the prompt may call
546
+ - `toolExecution?: 'sequential' | 'parallel' | 'immediate'` - Tool execution mode
547
+ - `toolRetries?: number` - Retry count for failed tools (default: 2)
548
+ - `toolIterations?: number` - Max tool-calling iterations (default: 3)
549
+ - `toolsMax?: number` - Max successful tool calls
550
+ - `outputRetries?: number` - Retry count for invalid structured outputs
551
+ - `metadata?: TMetadata` - Static metadata for this prompt
552
+ - `validate?: (output: TOutput, ctx: Context<TContext, TMetadata>) => void | Promise<void>` - Post-parse validation
553
+ - `applicable?: (ctx: Context<TContext, TMetadata>) => boolean | Promise<boolean>` - Availability check
422
554
 
423
555
  **Methods:**
424
- - `get(input, mode, ctx)` - Execute and retrieve output
425
- - `run(input, ctx)` - Execute with full streaming
426
- - `applicable(ctx)` - Check if prompt can run
556
+ - `get(mode: PromptGetType, input?: TInput, ctx?: Context<TContext, TMetadata>): PromptGet<...>` - Execute and return output in the specified mode
557
+ - `run(input?: TInput, ctx?: Context<TContext, TMetadata>): AsyncGenerator<PromptEvent<TOutput, TTools>, TOutput | undefined>` - Full streaming generator
558
+ - `applicable(ctx?: Context<TContext, TMetadata>): Promise<boolean>` - Check if prompt is usable
427
559
 
428
560
  ### Tool
429
561
 
430
562
  ```typescript
431
- class Tool<TContext, TMetadata, TName, TParams, TOutput, TRefs>
563
+ class Tool<TContext, TMetadata, TName extends string, TParams extends object, TOutput, TRefs extends Tuple<ComponentCompatible<TContext, TMetadata>>, TDecoded extends unknown = TParams>
432
564
  ```
433
565
 
434
- **Constructor Options:**
435
- - `name: string` - Unique identifier
436
- - `description: string` - Purpose description
437
- - `instructions?: string` - Handlebars usage instructions
438
- - `schema: ZodType | Function` - Input schema
439
- - `refs?: Component[]` - Referenced components
440
- - `call: Function` - Implementation
441
- - `validate?: Function` - Post-validation hook
442
- - `applicable?: Function` - Applicability check
566
+ `TDecoded` is the type a custom `parse` produces from the raw wire args (e.g. a built class instance); it types `call`, `validate`, and `metadataFn`. It defaults to `TParams` (the wire type) when no custom `parse` is supplied — `schema` always stays the wire type.
567
+
568
+ **Constructor Options (`ToolInput`):**
569
+ - `name: string` - Unique identifier (shown to the AI model)
570
+ - `description: string` - What the tool does (shown to the AI model)
571
+ - `instructions?: string` - Handlebars template for usage instructions
572
+ - `schema: ZodType<TParams> | ((ctx: Context<TContext, TMetadata>) => ZodType<TParams> | undefined)` - Input parameter schema
573
+ - `strict?: boolean` - Enforce strict Zod schema (default: `true`)
574
+ - `refs?: TRefs` - Referenced components
575
+ - `call: (input: TParams, refs: TRefs, ctx: Context<TContext, TMetadata>) => TOutput` - Implementation
576
+ - `validate?: (input: TParams, ctx: Context<TContext, TMetadata>) => void | Promise<void>` - Post-parse validation
577
+ - `applicable?: (ctx: Context<TContext, TMetadata>) => boolean | Promise<boolean>` - Availability check
443
578
 
444
579
  **Methods:**
445
- - `run(input, ctx)` - Execute the tool
446
- - `compile(ctx)` - Generate tool definition for AI
447
- - `parse(ctx, args)` - Parse and validate arguments
448
- - `applicable(ctx)` - Check if tool can run
580
+ - `run(input?: TParams, ctx?: Context<TContext, TMetadata>): TOutput` - Execute the tool directly
581
+ - `compile(ctx: Context<TContext, TMetadata>): Promise<readonly [string, ToolDefinition] | undefined>` - Generate AI tool definition
582
+ - `applicable(ctx?: Context<TContext, TMetadata>): Promise<boolean>` - Check if tool is usable
449
583
 
450
584
  ### Agent
451
585
 
452
586
  ```typescript
453
- class Agent<TContext, TMetadata, TName, TInput, TOutput, TRefs>
587
+ class Agent<TContext, TMetadata, TName extends string, TInput extends object, TOutput, TRefs extends Tuple<ComponentCompatible<TContext, TMetadata>>>
454
588
  ```
455
589
 
456
- **Constructor Options:**
590
+ **Constructor Options (`AgentInput`):**
457
591
  - `name: string` - Unique identifier
458
592
  - `description: string` - Purpose description
459
- - `refs?: Component[]` - Referenced components
460
- - `call: Function` - Implementation with refs
461
- - `applicable?: Function` - Applicability check
593
+ - `refs: TRefs` - Components this agent uses
594
+ - `call: (input: TInput, refs: TRefs, ctx: Context<TContext, TMetadata>) => TOutput` - Implementation
595
+ - `applicable?: (ctx: Context<TContext, TMetadata>) => boolean | Promise<boolean>` - Availability check
462
596
 
463
597
  **Methods:**
464
- - `run(input, ctx)` - Execute the agent
465
- - `applicable(ctx)` - Check if agent can run
598
+ - `run(input?: TInput, ctx?: Context<TContext, TMetadata>): TOutput` - Execute the agent
599
+ - `applicable(ctx?: Context<TContext, TMetadata>): Promise<boolean>` - Check if agent is usable
466
600
 
467
601
  ## Context Structure
468
602
 
603
+ The `Context<TContext, TMetadata>` type combines your custom context with the core execution context:
604
+
469
605
  ```typescript
470
- interface Context<TContext, TMetadata> {
471
- // Required: AI execution
472
- execute?: Executor<TContext, TMetadata>;
473
- stream?: Streamer<TContext, TMetadata>;
474
-
475
- // Messages
476
- messages: Message[];
477
-
478
- // Token management
479
- defaultCompletionTokens?: number;
480
- estimateTokens?: (message: Message) => number;
481
-
482
- // Execution control
606
+ // Core fields available in every context
607
+ interface CoreContextFields {
608
+ // AI executor for non-streaming requests
609
+ execute?: (request: Request, ctx: TContext, metadata?: TMetadata, signal?: AbortSignal) => Promise<Response>;
610
+ // AI streamer for streaming requests
611
+ stream?: (request: Request, ctx: TContext, metadata?: TMetadata, signal?: AbortSignal) => AsyncGenerator<Chunk, Response>;
612
+
613
+ // Conversation history passed to the model
614
+ messages?: Message[];
615
+
616
+ // Reserve N tokens for the model's output (used for context trimming)
617
+ maxOutputTokens?: number;
618
+ // Override context window size (used for context trimming)
619
+ contextWindow?: number;
620
+
621
+ // Custom per-message usage estimator
622
+ estimateUsage?: (message: Message) => Usage | undefined;
623
+
624
+ // Number of output-format retries (default: 2)
625
+ outputRetries?: number;
626
+ // Number of context-trimming retries (default: 1)
627
+ forgetRetries?: number;
628
+
629
+ // Abort signal for cancellation
483
630
  signal?: AbortSignal;
631
+ // Event runner for tracking component lifecycle
484
632
  runner?: Events;
485
-
486
- // User context (TContext) spreads here
487
- [key: string]: any;
488
633
  }
489
634
  ```
490
635
 
@@ -493,13 +638,17 @@ interface Context<TContext, TMetadata> {
493
638
  ```typescript
494
639
  interface Request {
495
640
  messages: Message[];
496
- temperature?: number; // 0-2
497
- topP?: number; // 0-1
641
+ temperature?: number; // 0.0–2.0
642
+ topP?: number; // 0.0–1.0
498
643
  maxTokens?: number;
499
644
  stop?: string | string[];
500
645
  tools?: ToolDefinition[];
501
- toolChoice?: 'auto' | 'required' | 'none' | { tool: string };
502
- responseFormat?: 'text' | 'json' | ResponseFormat;
646
+ toolChoice?: 'auto' | 'required' | 'none' | { type: 'tool'; name: string };
647
+ responseFormat?: 'text' | 'json' | { type: 'json_schema'; schema: object; name: string; strict?: boolean };
648
+ reason?: { effort?: 'low' | 'medium' | 'high'; maxTokens?: number };
649
+ frequencyPenalty?: number;
650
+ presencePenalty?: number;
651
+ seed?: number;
503
652
  // ... more options
504
653
  }
505
654
  ```
@@ -507,15 +656,13 @@ interface Request {
507
656
  ## Best Practices
508
657
 
509
658
  1. **Type Safety** - Use TypeScript generics for context and metadata
510
- 2. **Schema Validation** - Use Zod for robust input/output validation
511
- 3. **Error Handling** - Wrap component execution in try-catch blocks
512
- 4. **Token Management** - Provide `estimateTokens` for accurate trimming
513
- 5. **Tool Organization** - Group related tools in agents
514
- 6. **Testing** - Unit test tools and prompts independently
515
- 7. **Documentation** - Document template variables and schemas
516
- 8. **Context Minimization** - Pass only necessary data in context
517
- 9. **Streaming** - Use streaming for better UX with long responses
518
- 10. **Validation** - Use `validate` hooks for business logic validation
659
+ 2. **Schema Validation** - Use Zod for robust input/output validation
660
+ 3. **Token Management** - Supply `estimateUsage` for accurate context trimming
661
+ 4. **Tool Organization** - Group related tools and use agents to orchestrate them
662
+ 5. **Testing** - Unit test tools and prompts independently
663
+ 6. **Context Minimization** - Pass only necessary data in context
664
+ 7. **Streaming** - Use streaming for better UX with long responses
665
+ 8. **Validation** - Use `validate` hooks for business-logic checks after schema parsing
519
666
 
520
667
  ## Examples
521
668
 
@@ -532,24 +679,31 @@ See the `src/__tests__` directory for comprehensive examples:
532
679
  Full type inference across component hierarchies:
533
680
 
534
681
  ```typescript
535
- // Types are automatically inferred
682
+ import { Prompt, Tool } from '@aeye/core';
683
+ import z from 'zod';
684
+
685
+ // Output type is inferred from the schema
536
686
  const prompt = new Prompt({
537
687
  name: 'example',
538
- schema: z.object({ result: z.string() })
539
- // ...
688
+ description: 'Example prompt',
689
+ content: 'Say hello to {{name}}.',
690
+ input: (input: { name: string }) => ({ name: input.name }),
691
+ schema: z.object({ greeting: z.string() }),
540
692
  });
541
693
 
542
- // TypeScript knows the output type
543
- const output = await prompt.get({}); // { result: string }
694
+ // result is typed as { greeting: string } | undefined
695
+ const result = await prompt.get('result', { name: 'World' }, ctx);
696
+ console.log(result?.greeting);
544
697
 
545
- // Tool parameters are type-safe
546
- const tool = new Tool({
547
- name: 'math',
698
+ // Tool call parameters are type-safe
699
+ const mathTool = new Tool({
700
+ name: 'add',
701
+ description: 'Adds two numbers',
548
702
  schema: z.object({ a: z.number(), b: z.number() }),
549
- call: (input, refs, ctx) => {
550
- // input is typed as { a: number, b: number }
703
+ call: (input, _refs, _ctx) => {
704
+ // input is typed as { a: number; b: number }
551
705
  return input.a + input.b;
552
- }
706
+ },
553
707
  });
554
708
  ```
555
709
 
@@ -563,6 +717,7 @@ GPL-3.0 © [ClickerMonkey](https://github.com/ClickerMonkey)
563
717
 
564
718
  ## Links
565
719
 
720
+ - [Root README](../../README.md)
721
+ - [@aeye/ai README](../ai/README.md)
566
722
  - [GitHub Repository](https://github.com/ClickerMonkey/aeye)
567
- - [Documentation](https://github.com/ClickerMonkey/aeye/tree/main/packages/core)
568
723
  - [Issue Tracker](https://github.com/ClickerMonkey/aeye/issues)