@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.
Files changed (124) hide show
  1. package/README.md +790 -724
  2. package/aeye-core-prompts.md +179 -178
  3. package/aeye-core-schema.md +43 -16
  4. package/aeye-core-tools.md +179 -177
  5. package/aeye-core-types.md +1 -0
  6. package/dist/common.d.ts +30 -5
  7. package/dist/common.d.ts.map +1 -1
  8. package/dist/common.js +134 -129
  9. package/dist/common.js.map +1 -1
  10. package/dist/delivery.d.ts +39 -0
  11. package/dist/delivery.d.ts.map +1 -0
  12. package/dist/delivery.js +127 -0
  13. package/dist/delivery.js.map +1 -0
  14. package/dist/errors.d.ts +60 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/errors.js +84 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/prompt.d.ts +134 -15
  23. package/dist/prompt.d.ts.map +1 -1
  24. package/dist/prompt.js +220 -164
  25. package/dist/prompt.js.map +1 -1
  26. package/dist/schema/analyze.d.ts +94 -0
  27. package/dist/schema/analyze.d.ts.map +1 -0
  28. package/dist/schema/analyze.js +221 -0
  29. package/dist/schema/analyze.js.map +1 -0
  30. package/dist/schema/any-value.d.ts +32 -0
  31. package/dist/schema/any-value.d.ts.map +1 -0
  32. package/dist/schema/any-value.js +77 -0
  33. package/dist/schema/any-value.js.map +1 -0
  34. package/dist/schema/budget.d.ts +114 -0
  35. package/dist/schema/budget.d.ts.map +1 -0
  36. package/dist/schema/budget.js +223 -0
  37. package/dist/schema/budget.js.map +1 -0
  38. package/dist/schema/check-bag.d.ts +28 -0
  39. package/dist/schema/check-bag.d.ts.map +1 -0
  40. package/dist/schema/check-bag.js +129 -0
  41. package/dist/schema/check-bag.js.map +1 -0
  42. package/dist/schema/consistency.d.ts +27 -0
  43. package/dist/schema/consistency.d.ts.map +1 -0
  44. package/dist/schema/consistency.js +110 -0
  45. package/dist/schema/consistency.js.map +1 -0
  46. package/dist/schema/cycle-breaker.d.ts +14 -0
  47. package/dist/schema/cycle-breaker.d.ts.map +1 -0
  48. package/dist/schema/cycle-breaker.js +157 -0
  49. package/dist/schema/cycle-breaker.js.map +1 -0
  50. package/dist/schema/decode.d.ts +31 -0
  51. package/dist/schema/decode.d.ts.map +1 -0
  52. package/dist/schema/decode.js +207 -0
  53. package/dist/schema/decode.js.map +1 -0
  54. package/dist/schema/descriptor.d.ts +300 -0
  55. package/dist/schema/descriptor.d.ts.map +1 -0
  56. package/dist/schema/descriptor.js +15 -0
  57. package/dist/schema/descriptor.js.map +1 -0
  58. package/dist/schema/dialects/anthropic.d.ts +34 -0
  59. package/dist/schema/dialects/anthropic.d.ts.map +1 -0
  60. package/dist/schema/dialects/anthropic.js +77 -0
  61. package/dist/schema/dialects/anthropic.js.map +1 -0
  62. package/dist/schema/dialects/google.d.ts +47 -0
  63. package/dist/schema/dialects/google.d.ts.map +1 -0
  64. package/dist/schema/dialects/google.js +91 -0
  65. package/dist/schema/dialects/google.js.map +1 -0
  66. package/dist/schema/dialects/lenient.d.ts +4 -0
  67. package/dist/schema/dialects/lenient.d.ts.map +1 -0
  68. package/dist/schema/dialects/lenient.js +26 -0
  69. package/dist/schema/dialects/lenient.js.map +1 -0
  70. package/dist/schema/dialects/openai.d.ts +40 -0
  71. package/dist/schema/dialects/openai.d.ts.map +1 -0
  72. package/dist/schema/dialects/openai.js +81 -0
  73. package/dist/schema/dialects/openai.js.map +1 -0
  74. package/dist/schema/dialects/xai.d.ts +23 -0
  75. package/dist/schema/dialects/xai.d.ts.map +1 -0
  76. package/dist/schema/dialects/xai.js +53 -0
  77. package/dist/schema/dialects/xai.js.map +1 -0
  78. package/dist/schema/enum-values.d.ts +44 -0
  79. package/dist/schema/enum-values.d.ts.map +1 -0
  80. package/dist/schema/enum-values.js +56 -0
  81. package/dist/schema/enum-values.js.map +1 -0
  82. package/dist/schema/expressible.d.ts +59 -0
  83. package/dist/schema/expressible.d.ts.map +1 -0
  84. package/dist/schema/expressible.js +161 -0
  85. package/dist/schema/expressible.js.map +1 -0
  86. package/dist/schema/index.d.ts +20 -0
  87. package/dist/schema/index.d.ts.map +1 -0
  88. package/dist/schema/index.js +20 -0
  89. package/dist/schema/index.js.map +1 -0
  90. package/dist/schema/json-schema.d.ts +64 -0
  91. package/dist/schema/json-schema.d.ts.map +1 -0
  92. package/dist/schema/json-schema.js +2 -0
  93. package/dist/schema/json-schema.js.map +1 -0
  94. package/dist/schema/registry.d.ts +73 -0
  95. package/dist/schema/registry.d.ts.map +1 -0
  96. package/dist/schema/registry.js +135 -0
  97. package/dist/schema/registry.js.map +1 -0
  98. package/dist/schema/strictify.d.ts +19 -0
  99. package/dist/schema/strictify.d.ts.map +1 -0
  100. package/dist/schema/strictify.js +237 -0
  101. package/dist/schema/strictify.js.map +1 -0
  102. package/dist/schema/to-json-schema.d.ts +29 -0
  103. package/dist/schema/to-json-schema.d.ts.map +1 -0
  104. package/dist/schema/to-json-schema.js +692 -0
  105. package/dist/schema/to-json-schema.js.map +1 -0
  106. package/dist/schema/walk-key.d.ts +24 -0
  107. package/dist/schema/walk-key.d.ts.map +1 -0
  108. package/dist/schema/walk-key.js +33 -0
  109. package/dist/schema/walk-key.js.map +1 -0
  110. package/dist/to.js +1 -1
  111. package/dist/to.js.map +1 -1
  112. package/dist/tool.d.ts +38 -4
  113. package/dist/tool.d.ts.map +1 -1
  114. package/dist/tool.js +52 -8
  115. package/dist/tool.js.map +1 -1
  116. package/dist/types.d.ts +265 -8
  117. package/dist/types.d.ts.map +1 -1
  118. package/dist/types.js +4 -1
  119. package/dist/types.js.map +1 -1
  120. package/package.json +2 -2
  121. package/dist/schema.d.ts +0 -840
  122. package/dist/schema.d.ts.map +0 -1
  123. package/dist/schema.js +0 -2478
  124. package/dist/schema.js.map +0 -1
package/README.md CHANGED
@@ -1,724 +1,790 @@
1
- # @aeye/core
2
-
3
- Core primitives for building AI agents, tools, and prompts with TypeScript. Provides a type-safe, composable framework for creating sophisticated AI applications with structured inputs/outputs, tool calling, and context management.
4
-
5
- ## Features
6
-
7
- - **🎯 Type-Safe Components** - Prompts, Tools, and Agents with full TypeScript support
8
- - **🔧 Tool Calling** - Native support for function/tool calling with schema validation
9
- - **📝 Template-Based Prompts** - Handlebars templates for dynamic prompt generation
10
- - **✅ Schema Validation** - Zod integration for structured inputs and outputs
11
- - **🌊 Streaming Support** - First-class streaming for real-time AI responses
12
- - **🔄 Composable Architecture** - Tools can use other tools, prompts can use tools, agents orchestrate everything
13
- - **📊 Context Management** - Type-safe context threading and automatic token window management
14
- - **🎛️ Flexible Execution** - Sequential, parallel, or immediate tool execution modes
15
-
16
- ## Installation
17
-
18
- ```bash
19
- npm install @aeye/core zod handlebars
20
- ```
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
-
24
- ## Core Concepts
25
-
26
- ### Components
27
-
28
- All AI primitives implement the `Component` interface:
29
-
30
- - **Prompt** - Generates AI responses with optional tool usage and structured outputs
31
- - **Tool** - Extends AI capabilities with custom functions and external integrations
32
- - **Agent** - Orchestrates complex workflows combining prompts and tools
33
-
34
- ### Context & Metadata
35
-
36
- - **Context (`TContext`)** - Application-specific data threaded through operations (user, db, etc.)
37
- - **Metadata (`TMetadata`)** - Execution settings for AI requests (model, temperature, etc.)
38
-
39
- ## Quick Start
40
-
41
- ### Basic Prompt
42
-
43
- ```typescript
44
- import { Prompt } from '@aeye/core';
45
- import z from 'zod';
46
-
47
- const summarizer = new Prompt({
48
- name: 'summarize',
49
- description: 'Summarizes text concisely',
50
- content: 'Summarize the following text:\n\n{{text}}',
51
-
52
- // Transform raw input into template variables
53
- input: (input: { text: string }) => ({ text: input.text }),
54
-
55
- // Define output schema (structured JSON)
56
- schema: z.object({
57
- summary: z.string().describe('A concise summary'),
58
- keyPoints: z.array(z.string()).describe('Main points'),
59
- }),
60
- });
61
-
62
- // Execute — ctx must provide execute or stream (from @aeye/ai or custom)
63
- const result = await summarizer.get(
64
- 'result',
65
- { text: 'Long article text...' },
66
- {
67
- execute: yourAIExecutor, // (request, ctx, metadata?, signal?) => Promise<Response>
68
- messages: [],
69
- }
70
- );
71
-
72
- console.log(result?.summary);
73
- console.log(result?.keyPoints);
74
- ```
75
-
76
- ### Creating Tools
77
-
78
- ```typescript
79
- import { Tool } from '@aeye/core';
80
- import z from 'zod';
81
-
82
- const getWeather = new Tool({
83
- name: 'getWeather',
84
- description: 'Get current weather for a location',
85
- instructions: 'Use this tool to get weather data for {{location}}.',
86
-
87
- schema: z.object({
88
- location: z.string().describe('City name or coordinates'),
89
- units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
90
- }),
91
-
92
- call: async (input, _refs, _ctx) => {
93
- const response = await fetch(
94
- `https://api.weather.com/v1/${encodeURIComponent(input.location)}`
95
- );
96
- const data = await response.json();
97
- return {
98
- temperature: data.temp,
99
- condition: data.condition,
100
- humidity: data.humidity,
101
- };
102
- },
103
- });
104
- ```
105
-
106
- ### Prompts with Tools
107
-
108
- ```typescript
109
- import { Prompt } from '@aeye/core';
110
- import z from 'zod';
111
-
112
- const travelAdvisor = new Prompt({
113
- name: 'travelAdvisor',
114
- description: 'Provides travel advice based on weather',
115
- content: `You are a travel advisor. Help plan a trip to {{destination}}.
116
-
117
- Use the getWeather tool to check current conditions, then provide:
118
- - What to pack
119
- - Recommended activities
120
- - Best times to visit`,
121
-
122
- input: (input: { destination: string }) => ({
123
- destination: input.destination,
124
- }),
125
-
126
- tools: [getWeather],
127
-
128
- schema: z.object({
129
- recommendations: z.array(z.string()),
130
- packingList: z.array(z.string()),
131
- weatherNotes: z.string(),
132
- }),
133
- });
134
-
135
- // The AI will automatically call getWeather if needed
136
- const advice = await travelAdvisor.get(
137
- 'result',
138
- { destination: 'Paris' },
139
- { execute: yourAIExecutor, messages: [] }
140
- );
141
-
142
- console.log(advice?.recommendations);
143
- ```
144
-
145
- ### Streaming Responses
146
-
147
- ```typescript
148
- // Stream text content only
149
- for await (const chunk of summarizer.get(
150
- 'streamContent',
151
- { text: 'Long text...' },
152
- { stream: yourAIStreamer, messages: [] }
153
- )) {
154
- process.stdout.write(chunk);
155
- }
156
-
157
- // Stream all events (including tool calls)
158
- for await (const event of summarizer.get(
159
- 'stream',
160
- { text: 'Long text...' },
161
- { stream: yourAIStreamer, messages: [] }
162
- )) {
163
- if (event.type === 'textPartial') {
164
- process.stdout.write(event.content);
165
- } else if (event.type === 'toolStart') {
166
- console.log('Tool started:', event.tool.name);
167
- } else if (event.type === 'toolOutput') {
168
- console.log('Tool result:', event.result);
169
- } else if (event.type === 'complete') {
170
- console.log('Final output:', event.output);
171
- }
172
- }
173
- ```
174
-
175
- ### Building Agents
176
-
177
- ```typescript
178
- import { Agent } from '@aeye/core';
179
- import z from 'zod';
180
-
181
- // Assume searchTool, summarizePrompt, and analyzePrompt are defined elsewhere
182
- const researchAgent = new Agent({
183
- name: 'researcher',
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
- ) => {
193
- // Step 1: Search for information
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 ?? '');
201
- }
202
-
203
- // Step 3: Synthesize into a final analysis
204
- const analysis = await analyze.get(
205
- 'result',
206
- { topic: input.topic, sources: summaries },
207
- ctx
208
- );
209
-
210
- return analysis;
211
- },
212
- });
213
-
214
- const research = await researchAgent.run(
215
- { topic: 'Quantum Computing' },
216
- { execute: yourAIExecutor, messages: [] }
217
- );
218
- ```
219
-
220
- ## Prompt Modes
221
-
222
- The `get(mode, input?, ctx?)` method supports different execution modes:
223
-
224
- | Mode | Description | Returns |
225
- |------|-------------|---------|
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>` |
231
-
232
- ```typescript
233
- // Get structured result
234
- const result = await prompt.get('result', input, ctx);
235
-
236
- // Get tool outputs only
237
- const tools = await prompt.get('tools', input, ctx);
238
-
239
- // Stream everything
240
- for await (const event of prompt.get('stream', input, ctx)) {
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);
248
- }
249
- ```
250
-
251
- ## Tool Execution Modes
252
-
253
- Control how tools are executed by the prompt:
254
-
255
- ```typescript
256
- const prompt = new Prompt({
257
- name: 'multi-tool',
258
- description: 'Uses multiple tools',
259
- content: 'Analyze this data using the available tools.',
260
- tools: [tool1, tool2, tool3],
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,
271
- });
272
- ```
273
-
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)
277
-
278
- ## Advanced Features
279
-
280
- ### Context-Aware Tools
281
-
282
- Tool schemas and applicability can depend on the current context:
283
-
284
- ```typescript
285
- interface MyContext {
286
- userRole: 'admin' | 'viewer';
287
- isAuthenticated: boolean;
288
- }
289
-
290
- const contextTool = new Tool({
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, {}>) => {
297
- if (ctx.userRole === 'admin') {
298
- return z.object({ action: z.enum(['read', 'write', 'delete']) });
299
- }
300
- return z.object({ action: z.enum(['read']) });
301
- },
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 };
308
- },
309
- });
310
- ```
311
-
312
- ### Custom Validation
313
-
314
- Add business-logic validation beyond Zod schema checks:
315
-
316
- ```typescript
317
- const placeOrder = new Tool({
318
- name: 'placeOrder',
319
- description: 'Place a product order',
320
-
321
- schema: z.object({
322
- itemId: z.string(),
323
- quantity: z.number().int().min(1),
324
- }),
325
-
326
- // Runs after schema parsing succeeds; throw to trigger re-prompting
327
- validate: async (input, ctx) => {
328
- const inventory = await checkInventory(input.itemId);
329
- if (inventory < input.quantity) {
330
- throw new Error(`Only ${inventory} units of ${input.itemId} are available.`);
331
- }
332
- },
333
-
334
- call: async (input, _refs, _ctx) => {
335
- return { orderId: crypto.randomUUID(), ...input };
336
- },
337
- });
338
- ```
339
-
340
- ### Event Tracking with `withEvents`
341
-
342
- ```typescript
343
- import { withEvents } from '@aeye/core';
344
-
345
- const runner = withEvents({
346
- onStatus: (instance) => {
347
- console.log(`${instance.component.name}: ${instance.status}`);
348
- if (instance.status === 'completed' && instance.completed && instance.running) {
349
- console.log(` Took ${instance.completed - instance.running}ms`);
350
- }
351
- },
352
-
353
- onPromptEvent: (_instance, event) => {
354
- if (event.type === 'usage') {
355
- console.log('Tokens used:', event.usage);
356
- }
357
- },
358
- });
359
-
360
- const result = await prompt.get(
361
- 'result',
362
- { text: 'Hello' },
363
- { execute: yourAIExecutor, messages: [], runner }
364
- );
365
- ```
366
-
367
- ### Token Management
368
-
369
- The prompt automatically trims old messages when the context window is full:
370
-
371
- ```typescript
372
- const result = await prompt.get(
373
- 'result',
374
- { text: 'Query' },
375
- {
376
- execute: yourAIExecutor,
377
- messages: conversationHistory, // Will be trimmed if needed
378
-
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
- );
388
- ```
389
-
390
- ### Dynamic Reconfiguration
391
-
392
- Use `reconfig` to adapt the prompt based on runtime statistics:
393
-
394
- ```typescript
395
- const adaptivePrompt = new Prompt({
396
- name: 'adaptive',
397
- description: 'Adapts based on runtime stats',
398
- content: 'Solve: {{problem}}',
399
- schema: z.object({ solution: z.string() }),
400
-
401
- reconfig: (stats, _ctx) => {
402
- // If many tool errors occurred, switch to one-at-a-time mode
403
- if (stats.toolCallErrors > 3) {
404
- return { config: { toolsOneAtATime: true } };
405
- }
406
- return {};
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()),
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);
503
- ```
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
- | `schemaSizeLimits` | Per-SCHEMA size ceilings (properties, nesting, total string characters, enum values); `undefined` means no documented limit. An over-size schema degrades on its own, even when it asked for `strict: true`. |
525
-
526
- 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.
527
-
528
- ## API Reference
529
-
530
- ### Prompt
531
-
532
- ```typescript
533
- class Prompt<TContext, TMetadata, TName extends string, TInput extends object, TOutput extends object | string, TTools extends Tuple<ToolCompatible<TContext, TMetadata>>, TDecoded extends unknown = TOutput>
534
- ```
535
-
536
- `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.
537
-
538
- **Constructor Options (`PromptInput`):**
539
- - `name: string` - Unique identifier
540
- - `description: string` - Purpose description
541
- - `content: string` - Handlebars template for the system/user message
542
- - `input?: (input: TInput, ctx: Context<TContext, TMetadata>) => Record<string, any>` - Maps raw input to template variables
543
- - `schema?: ZodType<TOutput> | ((input: TInput | undefined, ctx: Context<TContext, TMetadata>) => ZodType<TOutput> | false)` - Output schema; `false` disables structured output
544
- - `strict?: boolean` - Enforce strict Zod schema (default: `true`)
545
- - `config?: Partial<Request> | ((input: TInput | undefined, ctx: Context<TContext, TMetadata>) => Partial<Request> | false)` - AI request overrides
546
- - `tools?: TTools` - Tools the prompt may call
547
- - `toolExecution?: 'sequential' | 'parallel' | 'immediate'` - Tool execution mode
548
- - `toolRetries?: number` - Retry count for failed tools (default: 2)
549
- - `toolIterations?: number` - Max tool-calling iterations (default: 3)
550
- - `toolsMax?: number` - Max successful tool calls
551
- - `outputRetries?: number` - Retry count for invalid structured outputs
552
- - `metadata?: TMetadata` - Static metadata for this prompt
553
- - `validate?: (output: TOutput, ctx: Context<TContext, TMetadata>) => void | Promise<void>` - Post-parse validation
554
- - `applicable?: (ctx: Context<TContext, TMetadata>) => boolean | Promise<boolean>` - Availability check
555
-
556
- **Methods:**
557
- - `get(mode: PromptGetType, input?: TInput, ctx?: Context<TContext, TMetadata>): PromptGet<...>` - Execute and return output in the specified mode
558
- - `run(input?: TInput, ctx?: Context<TContext, TMetadata>): AsyncGenerator<PromptEvent<TOutput, TTools>, TOutput | undefined>` - Full streaming generator
559
- - `applicable(ctx?: Context<TContext, TMetadata>): Promise<boolean>` - Check if prompt is usable
560
-
561
- ### Tool
562
-
563
- ```typescript
564
- class Tool<TContext, TMetadata, TName extends string, TParams extends object, TOutput, TRefs extends Tuple<ComponentCompatible<TContext, TMetadata>>, TDecoded extends unknown = TParams>
565
- ```
566
-
567
- `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.
568
-
569
- **Constructor Options (`ToolInput`):**
570
- - `name: string` - Unique identifier (shown to the AI model)
571
- - `description: string` - What the tool does (shown to the AI model)
572
- - `instructions?: string` - Handlebars template for usage instructions
573
- - `schema: ZodType<TParams> | ((ctx: Context<TContext, TMetadata>) => ZodType<TParams> | undefined)` - Input parameter schema
574
- - `strict?: boolean` - Enforce strict Zod schema (default: `true`)
575
- - `refs?: TRefs` - Referenced components
576
- - `call: (input: TParams, refs: TRefs, ctx: Context<TContext, TMetadata>) => TOutput` - Implementation
577
- - `validate?: (input: TParams, ctx: Context<TContext, TMetadata>) => void | Promise<void>` - Post-parse validation
578
- - `applicable?: (ctx: Context<TContext, TMetadata>) => boolean | Promise<boolean>` - Availability check
579
-
580
- **Methods:**
581
- - `run(input?: TParams, ctx?: Context<TContext, TMetadata>): TOutput` - Execute the tool directly
582
- - `compile(ctx: Context<TContext, TMetadata>): Promise<readonly [string, ToolDefinition] | undefined>` - Generate AI tool definition
583
- - `applicable(ctx?: Context<TContext, TMetadata>): Promise<boolean>` - Check if tool is usable
584
-
585
- ### Agent
586
-
587
- ```typescript
588
- class Agent<TContext, TMetadata, TName extends string, TInput extends object, TOutput, TRefs extends Tuple<ComponentCompatible<TContext, TMetadata>>>
589
- ```
590
-
591
- **Constructor Options (`AgentInput`):**
592
- - `name: string` - Unique identifier
593
- - `description: string` - Purpose description
594
- - `refs: TRefs` - Components this agent uses
595
- - `call: (input: TInput, refs: TRefs, ctx: Context<TContext, TMetadata>) => TOutput` - Implementation
596
- - `applicable?: (ctx: Context<TContext, TMetadata>) => boolean | Promise<boolean>` - Availability check
597
-
598
- **Methods:**
599
- - `run(input?: TInput, ctx?: Context<TContext, TMetadata>): TOutput` - Execute the agent
600
- - `applicable(ctx?: Context<TContext, TMetadata>): Promise<boolean>` - Check if agent is usable
601
-
602
- ## Context Structure
603
-
604
- The `Context<TContext, TMetadata>` type combines your custom context with the core execution context:
605
-
606
- ```typescript
607
- // Core fields available in every context
608
- interface CoreContextFields {
609
- // AI executor for non-streaming requests
610
- execute?: (request: Request, ctx: TContext, metadata?: TMetadata, signal?: AbortSignal) => Promise<Response>;
611
- // AI streamer for streaming requests
612
- stream?: (request: Request, ctx: TContext, metadata?: TMetadata, signal?: AbortSignal) => AsyncGenerator<Chunk, Response>;
613
-
614
- // Conversation history passed to the model
615
- messages?: Message[];
616
-
617
- // Reserve N tokens for the model's output (used for context trimming)
618
- maxOutputTokens?: number;
619
- // Override context window size (used for context trimming)
620
- contextWindow?: number;
621
-
622
- // Custom per-message usage estimator
623
- estimateUsage?: (message: Message) => Usage | undefined;
624
-
625
- // Number of output-format retries (default: 2)
626
- outputRetries?: number;
627
- // Number of context-trimming retries (default: 1)
628
- forgetRetries?: number;
629
-
630
- // Abort signal for cancellation
631
- signal?: AbortSignal;
632
- // Event runner for tracking component lifecycle
633
- runner?: Events;
634
- }
635
- ```
636
-
637
- ## Request Configuration
638
-
639
- ```typescript
640
- interface Request {
641
- messages: Message[];
642
- temperature?: number; // 0.0–2.0
643
- topP?: number; // 0.0–1.0
644
- maxTokens?: number;
645
- stop?: string | string[];
646
- tools?: ToolDefinition[];
647
- toolChoice?: 'auto' | 'required' | 'none' | { type: 'tool'; name: string };
648
- responseFormat?: 'text' | 'json' | { type: 'json_schema'; schema: object; name: string; strict?: boolean };
649
- reason?: { effort?: 'low' | 'medium' | 'high'; maxTokens?: number };
650
- frequencyPenalty?: number;
651
- presencePenalty?: number;
652
- seed?: number;
653
- // ... more options
654
- }
655
- ```
656
-
657
- ## Best Practices
658
-
659
- 1. **Type Safety** - Use TypeScript generics for context and metadata
660
- 2. **Schema Validation** - Use Zod for robust input/output validation
661
- 3. **Token Management** - Supply `estimateUsage` for accurate context trimming
662
- 4. **Tool Organization** - Group related tools and use agents to orchestrate them
663
- 5. **Testing** - Unit test tools and prompts independently
664
- 6. **Context Minimization** - Pass only necessary data in context
665
- 7. **Streaming** - Use streaming for better UX with long responses
666
- 8. **Validation** - Use `validate` hooks for business-logic checks after schema parsing
667
-
668
- ## Examples
669
-
670
- See the `src/__tests__` directory for comprehensive examples:
671
-
672
- - **prompt-core-features.test.ts** - Basic prompt usage
673
- - **prompt-streaming-tool-events.test.ts** - Streaming and events
674
- - **tool.test.ts** - Tool creation and usage
675
- - **agent.test.ts** - Agent orchestration
676
- - **context-propagation.test.ts** - Context handling
677
-
678
- ## TypeScript Support
679
-
680
- Full type inference across component hierarchies:
681
-
682
- ```typescript
683
- import { Prompt, Tool } from '@aeye/core';
684
- import z from 'zod';
685
-
686
- // Output type is inferred from the schema
687
- const prompt = new Prompt({
688
- name: 'example',
689
- description: 'Example prompt',
690
- content: 'Say hello to {{name}}.',
691
- input: (input: { name: string }) => ({ name: input.name }),
692
- schema: z.object({ greeting: z.string() }),
693
- });
694
-
695
- // result is typed as { greeting: string } | undefined
696
- const result = await prompt.get('result', { name: 'World' }, ctx);
697
- console.log(result?.greeting);
698
-
699
- // Tool call parameters are type-safe
700
- const mathTool = new Tool({
701
- name: 'add',
702
- description: 'Adds two numbers',
703
- schema: z.object({ a: z.number(), b: z.number() }),
704
- call: (input, _refs, _ctx) => {
705
- // input is typed as { a: number; b: number }
706
- return input.a + input.b;
707
- },
708
- });
709
- ```
710
-
711
- ## Contributing
712
-
713
- Contributions are welcome! See the main [@aeye repository](https://github.com/ClickerMonkey/aeye) for contribution guidelines.
714
-
715
- ## License
716
-
717
- GPL-3.0 © [ClickerMonkey](https://github.com/ClickerMonkey)
718
-
719
- ## Links
720
-
721
- - [Root README](../../README.md)
722
- - [@aeye/ai README](../ai/README.md)
723
- - [GitHub Repository](https://github.com/ClickerMonkey/aeye)
724
- - [Issue Tracker](https://github.com/ClickerMonkey/aeye/issues)
1
+ # @aeye/core
2
+
3
+ Core primitives for building AI agents, tools, and prompts with TypeScript. Provides a type-safe, composable framework for creating sophisticated AI applications with structured inputs/outputs, tool calling, and context management.
4
+
5
+ ## Features
6
+
7
+ - **🎯 Type-Safe Components** - Prompts, Tools, and Agents with full TypeScript support
8
+ - **🔧 Tool Calling** - Native support for function/tool calling with schema validation
9
+ - **📝 Template-Based Prompts** - Handlebars templates for dynamic prompt generation
10
+ - **✅ Schema Validation** - Zod integration for structured inputs and outputs
11
+ - **🌊 Streaming Support** - First-class streaming for real-time AI responses
12
+ - **🔄 Composable Architecture** - Tools can use other tools, prompts can use tools, agents orchestrate everything
13
+ - **📊 Context Management** - Type-safe context threading and automatic token window management
14
+ - **🎛️ Flexible Execution** - Sequential, parallel, or immediate tool execution modes
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @aeye/core zod handlebars
20
+ ```
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
+
24
+ ## Core Concepts
25
+
26
+ ### Components
27
+
28
+ All AI primitives implement the `Component` interface:
29
+
30
+ - **Prompt** - Generates AI responses with optional tool usage and structured outputs
31
+ - **Tool** - Extends AI capabilities with custom functions and external integrations
32
+ - **Agent** - Orchestrates complex workflows combining prompts and tools
33
+
34
+ ### Context & Metadata
35
+
36
+ - **Context (`TContext`)** - Application-specific data threaded through operations (user, db, etc.)
37
+ - **Metadata (`TMetadata`)** - Execution settings for AI requests (model, temperature, etc.)
38
+
39
+ ## Quick Start
40
+
41
+ ### Basic Prompt
42
+
43
+ ```typescript
44
+ import { Prompt } from '@aeye/core';
45
+ import z from 'zod';
46
+
47
+ const summarizer = new Prompt({
48
+ name: 'summarize',
49
+ description: 'Summarizes text concisely',
50
+ content: 'Summarize the following text:\n\n{{text}}',
51
+
52
+ // Transform raw input into template variables
53
+ input: (input: { text: string }) => ({ text: input.text }),
54
+
55
+ // Define output schema (structured JSON)
56
+ schema: z.object({
57
+ summary: z.string().describe('A concise summary'),
58
+ keyPoints: z.array(z.string()).describe('Main points'),
59
+ }),
60
+ });
61
+
62
+ // Execute — ctx must provide execute or stream (from @aeye/ai or custom)
63
+ const result = await summarizer.get(
64
+ 'result',
65
+ { text: 'Long article text...' },
66
+ {
67
+ execute: yourAIExecutor, // (request, ctx, metadata?, signal?) => Promise<Response>
68
+ messages: [],
69
+ }
70
+ );
71
+
72
+ console.log(result?.summary);
73
+ console.log(result?.keyPoints);
74
+ ```
75
+
76
+ ### Creating Tools
77
+
78
+ ```typescript
79
+ import { Tool } from '@aeye/core';
80
+ import z from 'zod';
81
+
82
+ const getWeather = new Tool({
83
+ name: 'getWeather',
84
+ description: 'Get current weather for a location',
85
+ instructions: 'Use this tool to get weather data for {{location}}.',
86
+
87
+ schema: z.object({
88
+ location: z.string().describe('City name or coordinates'),
89
+ units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
90
+ }),
91
+
92
+ call: async (input, _refs, _ctx) => {
93
+ const response = await fetch(
94
+ `https://api.weather.com/v1/${encodeURIComponent(input.location)}`
95
+ );
96
+ const data = await response.json();
97
+ return {
98
+ temperature: data.temp,
99
+ condition: data.condition,
100
+ humidity: data.humidity,
101
+ };
102
+ },
103
+ });
104
+ ```
105
+
106
+ ### Prompts with Tools
107
+
108
+ ```typescript
109
+ import { Prompt } from '@aeye/core';
110
+ import z from 'zod';
111
+
112
+ const travelAdvisor = new Prompt({
113
+ name: 'travelAdvisor',
114
+ description: 'Provides travel advice based on weather',
115
+ content: `You are a travel advisor. Help plan a trip to {{destination}}.
116
+
117
+ Use the getWeather tool to check current conditions, then provide:
118
+ - What to pack
119
+ - Recommended activities
120
+ - Best times to visit`,
121
+
122
+ input: (input: { destination: string }) => ({
123
+ destination: input.destination,
124
+ }),
125
+
126
+ tools: [getWeather],
127
+
128
+ schema: z.object({
129
+ recommendations: z.array(z.string()),
130
+ packingList: z.array(z.string()),
131
+ weatherNotes: z.string(),
132
+ }),
133
+ });
134
+
135
+ // The AI will automatically call getWeather if needed
136
+ const advice = await travelAdvisor.get(
137
+ 'result',
138
+ { destination: 'Paris' },
139
+ { execute: yourAIExecutor, messages: [] }
140
+ );
141
+
142
+ console.log(advice?.recommendations);
143
+ ```
144
+
145
+ ### Streaming Responses
146
+
147
+ ```typescript
148
+ // Stream text content only
149
+ for await (const chunk of summarizer.get(
150
+ 'streamContent',
151
+ { text: 'Long text...' },
152
+ { stream: yourAIStreamer, messages: [] }
153
+ )) {
154
+ process.stdout.write(chunk);
155
+ }
156
+
157
+ // Stream all events (including tool calls)
158
+ for await (const event of summarizer.get(
159
+ 'stream',
160
+ { text: 'Long text...' },
161
+ { stream: yourAIStreamer, messages: [] }
162
+ )) {
163
+ if (event.type === 'textPartial') {
164
+ process.stdout.write(event.content);
165
+ } else if (event.type === 'toolStart') {
166
+ console.log('Tool started:', event.tool.name);
167
+ } else if (event.type === 'toolOutput') {
168
+ console.log('Tool result:', event.result);
169
+ } else if (event.type === 'complete') {
170
+ console.log('Final output:', event.output);
171
+ }
172
+ }
173
+ ```
174
+
175
+ ### Building Agents
176
+
177
+ ```typescript
178
+ import { Agent } from '@aeye/core';
179
+ import z from 'zod';
180
+
181
+ // Assume searchTool, summarizePrompt, and analyzePrompt are defined elsewhere
182
+ const researchAgent = new Agent({
183
+ name: 'researcher',
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
+ ) => {
193
+ // Step 1: Search for information
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 ?? '');
201
+ }
202
+
203
+ // Step 3: Synthesize into a final analysis
204
+ const analysis = await analyze.get(
205
+ 'result',
206
+ { topic: input.topic, sources: summaries },
207
+ ctx
208
+ );
209
+
210
+ return analysis;
211
+ },
212
+ });
213
+
214
+ const research = await researchAgent.run(
215
+ { topic: 'Quantum Computing' },
216
+ { execute: yourAIExecutor, messages: [] }
217
+ );
218
+ ```
219
+
220
+ ## Prompt Modes
221
+
222
+ The `get(mode, input?, ctx?)` method supports different execution modes:
223
+
224
+ | Mode | Description | Returns |
225
+ |------|-------------|---------|
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>` |
231
+
232
+ ```typescript
233
+ // Get structured result
234
+ const result = await prompt.get('result', input, ctx);
235
+
236
+ // Get tool outputs only
237
+ const tools = await prompt.get('tools', input, ctx);
238
+
239
+ // Stream everything
240
+ for await (const event of prompt.get('stream', input, ctx)) {
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);
248
+ }
249
+ ```
250
+
251
+ ## Tool Execution Modes
252
+
253
+ Control how tools are executed by the prompt:
254
+
255
+ ```typescript
256
+ const prompt = new Prompt({
257
+ name: 'multi-tool',
258
+ description: 'Uses multiple tools',
259
+ content: 'Analyze this data using the available tools.',
260
+ tools: [tool1, tool2, tool3],
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,
271
+ });
272
+ ```
273
+
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)
277
+
278
+ ## Advanced Features
279
+
280
+ ### Context-Aware Tools
281
+
282
+ Tool schemas and applicability can depend on the current context:
283
+
284
+ ```typescript
285
+ interface MyContext {
286
+ userRole: 'admin' | 'viewer';
287
+ isAuthenticated: boolean;
288
+ }
289
+
290
+ const contextTool = new Tool({
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, {}>) => {
297
+ if (ctx.userRole === 'admin') {
298
+ return z.object({ action: z.enum(['read', 'write', 'delete']) });
299
+ }
300
+ return z.object({ action: z.enum(['read']) });
301
+ },
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 };
308
+ },
309
+ });
310
+ ```
311
+
312
+ ### Custom Validation
313
+
314
+ Add business-logic validation beyond Zod schema checks:
315
+
316
+ ```typescript
317
+ const placeOrder = new Tool({
318
+ name: 'placeOrder',
319
+ description: 'Place a product order',
320
+
321
+ schema: z.object({
322
+ itemId: z.string(),
323
+ quantity: z.number().int().min(1),
324
+ }),
325
+
326
+ // Runs after schema parsing succeeds; throw to trigger re-prompting
327
+ validate: async (input, ctx) => {
328
+ const inventory = await checkInventory(input.itemId);
329
+ if (inventory < input.quantity) {
330
+ throw new Error(`Only ${inventory} units of ${input.itemId} are available.`);
331
+ }
332
+ },
333
+
334
+ call: async (input, _refs, _ctx) => {
335
+ return { orderId: crypto.randomUUID(), ...input };
336
+ },
337
+ });
338
+ ```
339
+
340
+ ### Event Tracking with `withEvents`
341
+
342
+ ```typescript
343
+ import { withEvents } from '@aeye/core';
344
+
345
+ const runner = withEvents({
346
+ onStatus: (instance) => {
347
+ console.log(`${instance.component.name}: ${instance.status}`);
348
+ if (instance.status === 'completed' && instance.completed && instance.running) {
349
+ console.log(` Took ${instance.completed - instance.running}ms`);
350
+ }
351
+ },
352
+
353
+ onPromptEvent: (_instance, event) => {
354
+ if (event.type === 'usage') {
355
+ console.log('Tokens used:', event.usage);
356
+ }
357
+ },
358
+ });
359
+
360
+ const result = await prompt.get(
361
+ 'result',
362
+ { text: 'Hello' },
363
+ { execute: yourAIExecutor, messages: [], runner }
364
+ );
365
+ ```
366
+
367
+ ### Token Management
368
+
369
+ The prompt automatically trims old messages when the context window is full:
370
+
371
+ ```typescript
372
+ const result = await prompt.get(
373
+ 'result',
374
+ { text: 'Query' },
375
+ {
376
+ execute: yourAIExecutor,
377
+ messages: conversationHistory, // Will be trimmed if needed
378
+
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
+ );
388
+ ```
389
+
390
+ ### Dynamic Reconfiguration
391
+
392
+ Use `reconfig` to adapt the prompt based on runtime statistics:
393
+
394
+ ```typescript
395
+ const adaptivePrompt = new Prompt({
396
+ name: 'adaptive',
397
+ description: 'Adapts based on runtime stats',
398
+ content: 'Solve: {{problem}}',
399
+ schema: z.object({ solution: z.string() }),
400
+
401
+ reconfig: (stats, _ctx) => {
402
+ // If many tool errors occurred, switch to one-at-a-time mode
403
+ if (stats.toolCallErrors > 3) {
404
+ return { config: { toolsOneAtATime: true } };
405
+ }
406
+ return {};
407
+ },
408
+ });
409
+ ```
410
+
411
+ ## Schema Fallbacks
412
+
413
+ A provider that rejects a schema throws `SchemaRejectedError` with a `target` naming the output or the tool. The prompt then steps down its `schemaFallbacks`, by default `SCHEMA_FALLBACKS`: the rejected schema goes non-strict, then the output moves to JSON mode with its schema in the prompt (`schemaDelivery: 'json'`), then to plain text with its schema in the prompt (`'prompt'`). Steps stay on the request for the rest of the run, and each emits a `schemaFallback` event.
414
+
415
+ ```typescript
416
+ import { Prompt, SCHEMA_FALLBACKS } from '@aeye/core';
417
+
418
+ const prompt = new Prompt({
419
+ name: 'extract',
420
+ description: 'Extracts fields',
421
+ content: 'Extract the fields from: {{text}}',
422
+ schema,
423
+ schemaFallbacks: SCHEMA_FALLBACKS.filter(step => step.name !== 'json'),
424
+ });
425
+ ```
426
+
427
+ Providers use `schemaRejection(request, error)` to classify a bad request and `deliverSchema(request)` to send a `json` or `prompt` schema without changing the prompt's request.
428
+
429
+ ## Error Kinds
430
+
431
+ Every prompt failure has `kind: 'model' | 'tool'`. `ToolInputError` (an unknown tool or bad arguments) and `OutputValidationError` (output that isn't JSON or fails the schema or `validate`) are model mistakes, fed back to the model for another try. `ToolExecutionError` is a tool's own throw. They appear on the `toolError` event's `failure`, on tool result messages' `failure`, and on the `textReset` event's `error`. A prompt that gives up throws a `PromptError` whose `cause` is the last failure.
432
+
433
+ ## Review
434
+
435
+ `review` judges the parsed, validated output before it's returned. Feedback becomes a user message and the model answers again, up to `reviewRetries` times; after that the prompt throws a `PromptError` caused by a `ReviewRejectedError` with the last output.
436
+
437
+ ```typescript
438
+ const answer = new Prompt({
439
+ name: 'answer',
440
+ description: 'Answers briefly',
441
+ content: 'Answer in under 20 words: {{question}}',
442
+ input: (input: { question: string }) => input,
443
+ review: (output) => output.split(' ').length < 20 ? { pass: true } : { feedback: 'Use fewer than 20 words.' },
444
+ });
445
+ ```
446
+
447
+ ## Model Capabilities
448
+
449
+ @aeye uses a capability system for automatic model selection. Models advertise their capabilities and the library selects the best match for each request:
450
+
451
+ | Capability | Description |
452
+ |------------|-------------|
453
+ | `chat` | Basic text completion |
454
+ | `streaming` | Real-time response streaming |
455
+ | `image` | Image generation |
456
+ | `vision` | Image understanding |
457
+ | `audio` | Text-to-speech |
458
+ | `hearing` | Speech-to-text |
459
+ | `embedding` | Text embeddings |
460
+ | `rerank` | Ordering documents by relevance |
461
+ | `decision` | Typed answers (probabilities, choices, scores) from decision models |
462
+ | `tools` | Function/tool calling |
463
+ | `json` | JSON output mode |
464
+ | `structured` | Structured outputs with schemas |
465
+ | `toolsStrict` | Strict-mode tool input schemas (auto-derived; see [Strict Mode](#strict-mode)) |
466
+ | `reasoning` | Extended chain-of-thought reasoning |
467
+ | `zdr` | Zero data retention |
468
+
469
+ ## Strict Mode
470
+
471
+ 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.
472
+
473
+ ### `strict` on Tool / Prompt
474
+
475
+ `Tool.strict` and `Prompt.strict` are `boolean | number`:
476
+
477
+ | Value | Meaning |
478
+ |---|---|
479
+ | `true` | **Require** strict. `@aeye/ai` filters selection down to models that declare strict support; the request fails if none qualify. |
480
+ | `false` | **Force** lenient. Standard JSON Schema; no `strict: true` on the wire. |
481
+ | `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. |
482
+ | omitted | Treated as `1`. |
483
+
484
+ ```typescript
485
+ new Tool({ name: 'must', /* default 1 */ schema: z.object({...}) }); // best-effort
486
+ new Tool({ name: 'critical', strict: true, schema: z.object({...}) }); // hard require
487
+ new Tool({ name: 'meh', strict: false, schema: z.object({...}) }); // never strict
488
+ new Tool({ name: 'top', strict: 100, schema: z.object({...}) }); // high priority
489
+ ```
490
+
491
+ 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.
492
+
493
+ ### Native format descriptors
494
+
495
+ Nine descriptors ship out of the box:
496
+
497
+ | Descriptor | Family / Strict | Notes |
498
+ |---|---|---|
499
+ | `OPENAI_STRICT` | openai / strict | Records → array-of-pairs, tuples → numeric-key objects, `optional → T \| null`, closed objects, restricted format whitelist. |
500
+ | `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), format whitelist (OpenAI's nine plus `uri`). No spelling for "any" (see below), and structured output is always strict. |
501
+ | `GOOGLE_STRICT` | google / strict | `prefixItems`, `$ref: '#'` recursion, `propertyOrdering` emitted, restricted format whitelist. |
502
+ | `XAI_STRICT` | xai / strict | `anyOf` / `oneOf`, non-circular `$defs`, `prefixItems` tuples, open records, format whitelist with `uri`; no `allOf`. |
503
+ | `LENIENT` | lenient / non-strict | No rewrites, all features pass through. Default for unannotated models. |
504
+ | `OPENAI_NON_STRICT`, `ANTHROPIC_NON_STRICT`, `XAI_NON_STRICT` | family / non-strict | Aliased to `LENIENT` but tagged with the family for diagnostics. |
505
+ | `GOOGLE_NON_STRICT` | google / non-strict | `LENIENT` except `z.any()` is the empty schema `{}`: Gemini rejects a recursive `$defs/Any` whenever a tool call is forced. |
506
+
507
+ ```typescript
508
+ import { OPENAI_STRICT, ANTHROPIC_STRICT, getDescriptor, toJSONSchema } from '@aeye/core';
509
+
510
+ const schema = z.object({
511
+ name: z.string(),
512
+ tags: z.record(z.string(), z.string()),
513
+ });
514
+
515
+ // Same Zod, different wire shapes:
516
+ toJSONSchema(schema, OPENAI_STRICT); // tags → array of {key, value}
517
+ toJSONSchema(schema, ANTHROPIC_STRICT); // tags → array of {key, value} (open records unsupported)
518
+ toJSONSchema(schema, getDescriptor('google', true)); // tags → standard additionalProperties
519
+
520
+ // Backward-compat overloads still work:
521
+ toJSONSchema(schema, true); // === OPENAI_STRICT
522
+ toJSONSchema(schema, false); // === LENIENT
523
+ ```
524
+
525
+ Under every descriptor, `z.nullable()` admits `null` (as an `anyOf` branch where the dialect has `anyOf`), only formats JSON Schema defines are emitted (`.regex()`'s `regex` is dropped), and an open record with a plain string key leaves out `propertyNames`. `strictify` accepts schemas named with `.meta({ id })`.
526
+
527
+ ### When a schema can't go out strict
528
+
529
+ Some schemas fall back even under `strict: true`, because no allocation could make the API accept them:
530
+
531
+ - **Size.** A schema over the dialect's per-schema `schemaSizeLimits`, or whose optional parameters or unions alone exceed the per-request budget, degrades to the family's non-strict descriptor.
532
+ - **"Any" on Anthropic.** Anthropic strict rejects every spelling of "any JSON value" (`{}`, a typeless schema, `additionalProperties: true`, a recursive `$defs/Any`), so `ANTHROPIC_STRICT` uses `anyEncoding: 'none'`. A tool with `z.any()`, `z.unknown()` or a recursion cycle goes out non-strict; a structured output with one goes as prompt text (`canExpress` answers `false`).
533
+ - **Anthropic structured output.** Anthropic compiles every structured output, strict flag or not, so `ANTHROPIC_STRICT` sets `structuredOutputAlwaysStrict`. An output that doesn't fit the strict limits on its own is sent as prompt text rather than as a "non-strict" schema the API would still reject.
534
+
535
+ `SchemaBudget` takes separate tool and output descriptors (`new SchemaBudget(tool, output)`), so a model with strict structured output but no strict tools never gets `strict: true` tools, and the reverse.
536
+
537
+ ### Registering a custom descriptor
538
+
539
+ 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)`:
540
+
541
+ ```typescript
542
+ import { registerDescriptor, type FormatDescriptor, OPENAI_STRICT } from '@aeye/core';
543
+
544
+ const MY_PROVIDER_STRICT: FormatDescriptor = {
545
+ ...OPENAI_STRICT, // start from a known good baseline
546
+ id: 'my-provider-strict',
547
+ family: 'openai', // or 'anthropic' | 'google' | 'lenient'
548
+ // Override only the fields that differ from the baseline:
549
+ recordEncoding: 'open-record', // your provider supports additionalProperties: <schema>
550
+ tupleEncoding: 'prefix-items',
551
+ supportsRecursion: true,
552
+ };
553
+
554
+ registerDescriptor(MY_PROVIDER_STRICT);
555
+ ```
556
+
557
+ Once registered:
558
+
559
+ - Provider code can pass the descriptor to `toJSONSchema(schema, MY_PROVIDER_STRICT)` and `strictify(schema, MY_PROVIDER_STRICT)`.
560
+ - The Prompt's validation roundtrip (which looks up descriptors by id from the request) will resolve `'my-provider-strict'` correctly.
561
+ - A custom `Provider` implementation can return this descriptor from its `getStrictFormat`-equivalent helper.
562
+
563
+ The descriptor's full field list is exported as the `FormatDescriptor` interface — every flag is documented inline. Common adjustments:
564
+
565
+ | Field | Controls |
566
+ |---|---|
567
+ | `objectAllFieldsRequired` | Whether every property must appear in `required[]`. |
568
+ | `objectClosedByDefault` | Whether every object emits `additionalProperties: false`. |
569
+ | `recordEncoding` | `'array-of-pairs'` vs `'open-record'`. |
570
+ | `tupleEncoding` | `'object-numeric-keys'` vs `'prefix-items'` vs `'items-union'`. |
571
+ | `allowAllOf` / `allowAnyOf` / `allowOneOf` | JSON-Schema combinator support. |
572
+ | `supportedStringFormats` | `'all'` (every format JSON Schema defines) or a `Set<string>` whitelist (`'date-time'`, `'email'`, etc.). |
573
+ | `optionalAsNullable` | Whether `optional` fields are emitted as `T \| null` (OpenAI strict) vs omitted from `required[]`. |
574
+ | `supportsRecursion` | Whether `$ref` self-reference works under this dialect. |
575
+ | `anyEncoding` | How `z.any()` / `z.unknown()` is spelled: `'recursive-strict'`, `'recursive-open'`, `'flat'`, `'unconstrained'` (`{}`), or `'none'` (not sendable strict). |
576
+ | `structuredOutputAlwaysStrict` | The API compiles every structured output, so an output over the strict limits goes as prompt text instead of degrading. |
577
+ | `maxStrictTools` / `maxStrictOptionalParams` / `maxStrictUnionTypes` | Per-request budget caps; `undefined` means no documented limit. |
578
+ | `schemaSizeLimits` | Per-SCHEMA size ceilings (properties, nesting, total string characters, enum values); `undefined` means no documented limit. An over-size schema degrades on its own, even when it asked for `strict: true`. |
579
+
580
+ 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.
581
+
582
+ ## API Reference
583
+
584
+ ### Prompt
585
+
586
+ ```typescript
587
+ class Prompt<TContext, TMetadata, TName extends string, TInput extends object, TOutput extends object | string, TTools extends Tuple<ToolCompatible<TContext, TMetadata>>, TDecoded extends unknown = TOutput>
588
+ ```
589
+
590
+ `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.
591
+
592
+ **Constructor Options (`PromptInput`):**
593
+ - `name: string` - Unique identifier
594
+ - `description: string` - Purpose description
595
+ - `content: string` - Handlebars template for the system message
596
+ - `userTurn?: string` - User message added when there's no conversation (default: the context's, then `DEFAULT_USER_TURN`, `'Begin.'`)
597
+ - `input?: (input: TInput, ctx: Context<TContext, TMetadata>) => Record<string, any>` - Maps raw input to template variables
598
+ - `schema?: ZodType<TOutput> | ((input: TInput | undefined, ctx: Context<TContext, TMetadata>) => ZodType<TOutput> | false)` - Output schema; `false` disables structured output
599
+ - `strict?: boolean | number` - Strict output schema: `true` requires, `false` disables, a number is a preference priority (default: `1`)
600
+ - `config?: Partial<Request> | ((input: TInput | undefined, ctx: Context<TContext, TMetadata>) => Partial<Request> | false)` - AI request overrides
601
+ - `tools?: TTools` - Tools the prompt may call
602
+ - `toolExecution?: 'sequential' | 'parallel' | 'immediate'` - Tool execution mode
603
+ - `toolRetries?: number` - Retry count for failed tools (default: 2)
604
+ - `toolIterations?: number` - Max tool-calling iterations (default: 3)
605
+ - `toolsMax?: number` - Max successful tool calls
606
+ - `outputRetries?: number` - Retry count for invalid structured outputs
607
+ - `schemaDelivery?: 'auto' | 'structured' | 'json' | 'prompt'` - How the output schema reaches the model (default: `'auto'`)
608
+ - `schemaFallbacks?: readonly SchemaFallback[]` - Steps taken when a provider rejects a schema (default: the context's, then `SCHEMA_FALLBACKS`)
609
+ - `metadata?: TMetadata` - Static metadata for this prompt
610
+ - `validate?: (output: TOutput, ctx: Context<TContext, TMetadata>) => void | Promise<void>` - Post-parse validation
611
+ - `review?: (output: TDecoded, context: PromptReviewContext) => PromptReview | Promise<PromptReview>` - Review the valid output: `{ pass: true }` or `{ feedback }` for another round
612
+ - `reviewRetries?: number` - Rounds the model gets to answer review feedback (default: 1)
613
+ - `applicable?: (ctx: Context<TContext, TMetadata>) => boolean | Promise<boolean>` - Availability check
614
+
615
+ **Methods:**
616
+ - `get(mode: PromptGetType, input?: TInput, ctx?: Context<TContext, TMetadata>): PromptGet<...>` - Execute and return output in the specified mode
617
+ - `run(input?: TInput, ctx?: Context<TContext, TMetadata>): AsyncGenerator<PromptEvent<TOutput, TTools>, TOutput | undefined>` - Full streaming generator
618
+ - `applicable(ctx?: Context<TContext, TMetadata>): Promise<boolean>` - Check if prompt is usable
619
+
620
+ ### Tool
621
+
622
+ ```typescript
623
+ class Tool<TContext, TMetadata, TName extends string, TParams extends object, TOutput, TRefs extends Tuple<ComponentCompatible<TContext, TMetadata>>, TDecoded extends unknown = TParams>
624
+ ```
625
+
626
+ `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.
627
+
628
+ **Constructor Options (`ToolInput`):**
629
+ - `name: string` - Unique identifier (shown to the AI model)
630
+ - `description: string` - What the tool does (shown to the AI model)
631
+ - `instructions?: string` - Handlebars template for usage instructions
632
+ - `schema: ZodType<TParams> | ((ctx: Context<TContext, TMetadata>) => ZodType<TParams> | undefined)` - Input parameter schema
633
+ - `strict?: boolean | number` - Strict input schema: `true` requires, `false` disables, a number is a preference priority (default: `1`)
634
+ - `format?: ToolTextFormat` - Take free text (`{ type: 'text' }`) or grammar-constrained text (`{ type: 'grammar', syntax: 'lark' | 'regex', definition }`) as `input` instead of JSON; use `TEXT_TOOL_SCHEMA` as the schema. Providers without free-text tools send a one-field function whose description `describeTool(definition)` builds, with the grammar appended
635
+ - `defer?: boolean` - Load only when the model searches for it (OpenAI tool search)
636
+ - `async?: boolean` - The model keeps working while the tool runs (OpenAI Responses)
637
+ - `refs?: TRefs` - Referenced components
638
+ - `call: (input: TParams, refs: TRefs, ctx: Context<TContext, TMetadata>) => TOutput` - Implementation
639
+ - `validate?: (input: TParams, ctx: Context<TContext, TMetadata>) => void | Promise<void>` - Post-parse validation
640
+ - `applicable?: (ctx: Context<TContext, TMetadata>) => boolean | Promise<boolean>` - Availability check
641
+
642
+ **Methods:**
643
+ - `run(input?: TParams, ctx?: Context<TContext, TMetadata>): TOutput` - Execute the tool directly
644
+ - `compile(ctx: Context<TContext, TMetadata>): Promise<readonly [string, ToolDefinition] | undefined>` - Generate AI tool definition
645
+ - `applicable(ctx?: Context<TContext, TMetadata>): Promise<boolean>` - Check if tool is usable
646
+
647
+ ### Agent
648
+
649
+ ```typescript
650
+ class Agent<TContext, TMetadata, TName extends string, TInput extends object, TOutput, TRefs extends Tuple<ComponentCompatible<TContext, TMetadata>>>
651
+ ```
652
+
653
+ **Constructor Options (`AgentInput`):**
654
+ - `name: string` - Unique identifier
655
+ - `description: string` - Purpose description
656
+ - `refs: TRefs` - Components this agent uses
657
+ - `call: (input: TInput, refs: TRefs, ctx: Context<TContext, TMetadata>) => TOutput` - Implementation
658
+ - `applicable?: (ctx: Context<TContext, TMetadata>) => boolean | Promise<boolean>` - Availability check
659
+
660
+ **Methods:**
661
+ - `run(input?: TInput, ctx?: Context<TContext, TMetadata>): TOutput` - Execute the agent
662
+ - `applicable(ctx?: Context<TContext, TMetadata>): Promise<boolean>` - Check if agent is usable
663
+
664
+ ## Context Structure
665
+
666
+ The `Context<TContext, TMetadata>` type combines your custom context with the core execution context:
667
+
668
+ ```typescript
669
+ // Core fields available in every context
670
+ interface CoreContextFields {
671
+ // AI executor for non-streaming requests
672
+ execute?: (request: Request, ctx: TContext, metadata?: TMetadata, signal?: AbortSignal) => Promise<Response>;
673
+ // AI streamer for streaming requests
674
+ stream?: (request: Request, ctx: TContext, metadata?: TMetadata, signal?: AbortSignal) => AsyncGenerator<Chunk, Response>;
675
+
676
+ // Conversation history passed to the model
677
+ messages?: Message[];
678
+
679
+ // Reserve N tokens for the model's output (used for context trimming)
680
+ maxOutputTokens?: number;
681
+ // Override context window size (used for context trimming)
682
+ contextWindow?: number;
683
+
684
+ // Custom per-message usage estimator
685
+ estimateUsage?: (message: Message) => Usage | undefined;
686
+
687
+ // Number of output-format retries (default: 2)
688
+ outputRetries?: number;
689
+ // Number of context-trimming retries (default: 1)
690
+ forgetRetries?: number;
691
+ // Steps a prompt takes when a provider rejects a schema (default: SCHEMA_FALLBACKS)
692
+ schemaFallbacks?: readonly SchemaFallback[];
693
+ // User message a prompt adds when there's no conversation (default: DEFAULT_USER_TURN, 'Begin.')
694
+ userTurn?: string;
695
+
696
+ // Abort signal for cancellation
697
+ signal?: AbortSignal;
698
+ // Event runner for tracking component lifecycle
699
+ runner?: Events;
700
+ }
701
+ ```
702
+
703
+ ## Request Configuration
704
+
705
+ ```typescript
706
+ interface Request {
707
+ messages: Message[];
708
+ temperature?: number; // 0.0–2.0
709
+ topP?: number; // 0.0–1.0
710
+ maxTokens?: number;
711
+ stop?: string | string[];
712
+ tools?: ToolDefinition[];
713
+ toolChoice?: 'auto' | 'required' | 'none' | { tool: string } | { allowed: string[]; required?: boolean };
714
+ responseFormat?: 'text' | 'json' | { type: ZodType; strict: boolean | number };
715
+ reason?: { effort?: ReasoningEffort; maxTokens?: number; summary?: 'auto' | 'concise' | 'detailed'; exclude?: boolean };
716
+ frequencyPenalty?: number;
717
+ presencePenalty?: number;
718
+ seed?: number;
719
+ // ... more options
720
+ }
721
+ ```
722
+
723
+ ## Best Practices
724
+
725
+ 1. **Type Safety** - Use TypeScript generics for context and metadata
726
+ 2. **Schema Validation** - Use Zod for robust input/output validation
727
+ 3. **Token Management** - Supply `estimateUsage` for accurate context trimming
728
+ 4. **Tool Organization** - Group related tools and use agents to orchestrate them
729
+ 5. **Testing** - Unit test tools and prompts independently
730
+ 6. **Context Minimization** - Pass only necessary data in context
731
+ 7. **Streaming** - Use streaming for better UX with long responses
732
+ 8. **Validation** - Use `validate` hooks for business-logic checks after schema parsing
733
+
734
+ ## Examples
735
+
736
+ See the `src/__tests__` directory for comprehensive examples:
737
+
738
+ - **prompt-core-features.test.ts** - Basic prompt usage
739
+ - **prompt-streaming-tool-events.test.ts** - Streaming and events
740
+ - **tool.test.ts** - Tool creation and usage
741
+ - **agent.test.ts** - Agent orchestration
742
+ - **context-propagation.test.ts** - Context handling
743
+
744
+ ## TypeScript Support
745
+
746
+ Full type inference across component hierarchies:
747
+
748
+ ```typescript
749
+ import { Prompt, Tool } from '@aeye/core';
750
+ import z from 'zod';
751
+
752
+ // Output type is inferred from the schema
753
+ const prompt = new Prompt({
754
+ name: 'example',
755
+ description: 'Example prompt',
756
+ content: 'Say hello to {{name}}.',
757
+ input: (input: { name: string }) => ({ name: input.name }),
758
+ schema: z.object({ greeting: z.string() }),
759
+ });
760
+
761
+ // result is typed as { greeting: string } | undefined
762
+ const result = await prompt.get('result', { name: 'World' }, ctx);
763
+ console.log(result?.greeting);
764
+
765
+ // Tool call parameters are type-safe
766
+ const mathTool = new Tool({
767
+ name: 'add',
768
+ description: 'Adds two numbers',
769
+ schema: z.object({ a: z.number(), b: z.number() }),
770
+ call: (input, _refs, _ctx) => {
771
+ // input is typed as { a: number; b: number }
772
+ return input.a + input.b;
773
+ },
774
+ });
775
+ ```
776
+
777
+ ## Contributing
778
+
779
+ Contributions are welcome! See the main [@aeye repository](https://github.com/ClickerMonkey/aeye) for contribution guidelines.
780
+
781
+ ## License
782
+
783
+ GPL-3.0 © [ClickerMonkey](https://github.com/ClickerMonkey)
784
+
785
+ ## Links
786
+
787
+ - [Root README](../../README.md)
788
+ - [@aeye/ai README](../ai/README.md)
789
+ - [GitHub Repository](https://github.com/ClickerMonkey/aeye)
790
+ - [Issue Tracker](https://github.com/ClickerMonkey/aeye/issues)