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