@aeye/core 0.5.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +790 -724
- package/aeye-core-prompts.md +179 -178
- package/aeye-core-schema.md +43 -16
- package/aeye-core-tools.md +179 -177
- package/aeye-core-types.md +1 -0
- package/dist/common.d.ts +30 -5
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +134 -129
- package/dist/common.js.map +1 -1
- package/dist/delivery.d.ts +39 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +127 -0
- package/dist/delivery.js.map +1 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +84 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/prompt.d.ts +134 -15
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +220 -164
- package/dist/prompt.js.map +1 -1
- package/dist/schema/analyze.d.ts +94 -0
- package/dist/schema/analyze.d.ts.map +1 -0
- package/dist/schema/analyze.js +221 -0
- package/dist/schema/analyze.js.map +1 -0
- package/dist/schema/any-value.d.ts +32 -0
- package/dist/schema/any-value.d.ts.map +1 -0
- package/dist/schema/any-value.js +77 -0
- package/dist/schema/any-value.js.map +1 -0
- package/dist/schema/budget.d.ts +114 -0
- package/dist/schema/budget.d.ts.map +1 -0
- package/dist/schema/budget.js +223 -0
- package/dist/schema/budget.js.map +1 -0
- package/dist/schema/check-bag.d.ts +28 -0
- package/dist/schema/check-bag.d.ts.map +1 -0
- package/dist/schema/check-bag.js +129 -0
- package/dist/schema/check-bag.js.map +1 -0
- package/dist/schema/consistency.d.ts +27 -0
- package/dist/schema/consistency.d.ts.map +1 -0
- package/dist/schema/consistency.js +110 -0
- package/dist/schema/consistency.js.map +1 -0
- package/dist/schema/cycle-breaker.d.ts +14 -0
- package/dist/schema/cycle-breaker.d.ts.map +1 -0
- package/dist/schema/cycle-breaker.js +157 -0
- package/dist/schema/cycle-breaker.js.map +1 -0
- package/dist/schema/decode.d.ts +31 -0
- package/dist/schema/decode.d.ts.map +1 -0
- package/dist/schema/decode.js +207 -0
- package/dist/schema/decode.js.map +1 -0
- package/dist/schema/descriptor.d.ts +300 -0
- package/dist/schema/descriptor.d.ts.map +1 -0
- package/dist/schema/descriptor.js +15 -0
- package/dist/schema/descriptor.js.map +1 -0
- package/dist/schema/dialects/anthropic.d.ts +34 -0
- package/dist/schema/dialects/anthropic.d.ts.map +1 -0
- package/dist/schema/dialects/anthropic.js +77 -0
- package/dist/schema/dialects/anthropic.js.map +1 -0
- package/dist/schema/dialects/google.d.ts +47 -0
- package/dist/schema/dialects/google.d.ts.map +1 -0
- package/dist/schema/dialects/google.js +91 -0
- package/dist/schema/dialects/google.js.map +1 -0
- package/dist/schema/dialects/lenient.d.ts +4 -0
- package/dist/schema/dialects/lenient.d.ts.map +1 -0
- package/dist/schema/dialects/lenient.js +26 -0
- package/dist/schema/dialects/lenient.js.map +1 -0
- package/dist/schema/dialects/openai.d.ts +40 -0
- package/dist/schema/dialects/openai.d.ts.map +1 -0
- package/dist/schema/dialects/openai.js +81 -0
- package/dist/schema/dialects/openai.js.map +1 -0
- package/dist/schema/dialects/xai.d.ts +23 -0
- package/dist/schema/dialects/xai.d.ts.map +1 -0
- package/dist/schema/dialects/xai.js +53 -0
- package/dist/schema/dialects/xai.js.map +1 -0
- package/dist/schema/enum-values.d.ts +44 -0
- package/dist/schema/enum-values.d.ts.map +1 -0
- package/dist/schema/enum-values.js +56 -0
- package/dist/schema/enum-values.js.map +1 -0
- package/dist/schema/expressible.d.ts +59 -0
- package/dist/schema/expressible.d.ts.map +1 -0
- package/dist/schema/expressible.js +161 -0
- package/dist/schema/expressible.js.map +1 -0
- package/dist/schema/index.d.ts +20 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +20 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/json-schema.d.ts +64 -0
- package/dist/schema/json-schema.d.ts.map +1 -0
- package/dist/schema/json-schema.js +2 -0
- package/dist/schema/json-schema.js.map +1 -0
- package/dist/schema/registry.d.ts +73 -0
- package/dist/schema/registry.d.ts.map +1 -0
- package/dist/schema/registry.js +135 -0
- package/dist/schema/registry.js.map +1 -0
- package/dist/schema/strictify.d.ts +19 -0
- package/dist/schema/strictify.d.ts.map +1 -0
- package/dist/schema/strictify.js +237 -0
- package/dist/schema/strictify.js.map +1 -0
- package/dist/schema/to-json-schema.d.ts +29 -0
- package/dist/schema/to-json-schema.d.ts.map +1 -0
- package/dist/schema/to-json-schema.js +692 -0
- package/dist/schema/to-json-schema.js.map +1 -0
- package/dist/schema/walk-key.d.ts +24 -0
- package/dist/schema/walk-key.d.ts.map +1 -0
- package/dist/schema/walk-key.js +33 -0
- package/dist/schema/walk-key.js.map +1 -0
- package/dist/to.js +1 -1
- package/dist/to.js.map +1 -1
- package/dist/tool.d.ts +38 -4
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +52 -8
- package/dist/tool.js.map +1 -1
- package/dist/types.d.ts +265 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/dist/schema.d.ts +0 -840
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js +0 -2478
- package/dist/schema.js.map +0 -1
package/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
|
-
##
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
|
460
|
-
|
|
461
|
-
| `
|
|
462
|
-
| `
|
|
463
|
-
| `
|
|
464
|
-
| `
|
|
465
|
-
| `
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
- `
|
|
594
|
-
- `
|
|
595
|
-
- `
|
|
596
|
-
- `
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
- `
|
|
600
|
-
- `
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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)
|