@vibe-agent-toolkit/vat-development-agents 0.1.13 → 0.1.14
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/dist/generated/resources/skills/SKILL.d.ts +1 -0
- package/dist/generated/resources/skills/SKILL.js +14 -9
- package/dist/skills/vibe-agent-toolkit/SKILL.md +62 -18
- package/dist/skills/vibe-agent-toolkit/resources/CLAUDE.md +577 -0
- package/dist/skills/vibe-agent-toolkit/resources/adding-runtime-adapters.md +628 -0
- package/dist/skills/vibe-agent-toolkit/resources/agent-authoring.md +905 -0
- package/dist/skills/vibe-agent-toolkit/resources/agent-skills-best-practices.md +594 -0
- package/dist/skills/vibe-agent-toolkit/resources/audit.md +325 -0
- package/dist/skills/vibe-agent-toolkit/resources/getting-started.md +360 -0
- package/dist/skills/vibe-agent-toolkit/resources/import.md +389 -0
- package/dist/skills/vibe-agent-toolkit/resources/orchestration.md +859 -0
- package/dist/skills/vibe-agent-toolkit/resources/rag-usage-guide.md +770 -0
- package/dist/skills/vibe-agent-toolkit/resources/rag.md +542 -0
- package/package.json +3 -4
- package/dist/skills/vibe-agent-toolkit/README.md +0 -279
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
# Agent Authoring Guide
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
This guide shows how to create VAT agents that return standardized result envelopes. All agents follow consistent patterns for error handling, type safety, and orchestration.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### Minimal Pure Function Agent
|
|
10
|
+
|
|
11
|
+
The simplest agent is a pure function that validates or transforms data:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { defineAgent, type OneShotAgentOutput } from '@vibe-agent-toolkit/agent-schema';
|
|
16
|
+
|
|
17
|
+
// 1. Define input schema
|
|
18
|
+
const InputSchema = z.object({
|
|
19
|
+
text: z.string(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 2. Define output data schema
|
|
23
|
+
const OutputDataSchema = z.object({
|
|
24
|
+
valid: z.boolean(),
|
|
25
|
+
reason: z.string().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 3. Define error type
|
|
29
|
+
type ValidationError = 'too-short' | 'too-long' | 'invalid-format';
|
|
30
|
+
|
|
31
|
+
// 4. Define agent
|
|
32
|
+
export const myValidator = defineAgent<
|
|
33
|
+
z.infer<typeof InputSchema>,
|
|
34
|
+
OneShotAgentOutput<z.infer<typeof OutputDataSchema>, ValidationError>
|
|
35
|
+
>({
|
|
36
|
+
name: 'my-validator',
|
|
37
|
+
description: 'Validates text input',
|
|
38
|
+
version: '1.0.0',
|
|
39
|
+
inputSchema: InputSchema,
|
|
40
|
+
outputSchema: z.object({
|
|
41
|
+
result: z.discriminatedUnion('status', [
|
|
42
|
+
z.object({
|
|
43
|
+
status: z.literal('success'),
|
|
44
|
+
data: OutputDataSchema,
|
|
45
|
+
}),
|
|
46
|
+
z.object({
|
|
47
|
+
status: z.literal('error'),
|
|
48
|
+
error: z.enum(['too-short', 'too-long', 'invalid-format']),
|
|
49
|
+
}),
|
|
50
|
+
]),
|
|
51
|
+
}),
|
|
52
|
+
|
|
53
|
+
// 5. Implement execution logic
|
|
54
|
+
async execute(input) {
|
|
55
|
+
if (input.text.length < 5) {
|
|
56
|
+
return {
|
|
57
|
+
result: {
|
|
58
|
+
status: 'error' as const,
|
|
59
|
+
error: 'too-short' as const,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (input.text.length > 100) {
|
|
65
|
+
return {
|
|
66
|
+
result: {
|
|
67
|
+
status: 'error' as const,
|
|
68
|
+
error: 'too-long' as const,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
result: {
|
|
75
|
+
status: 'success' as const,
|
|
76
|
+
data: {
|
|
77
|
+
valid: true,
|
|
78
|
+
reason: 'Passes all validation rules',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Minimal LLM-Based Agent
|
|
87
|
+
|
|
88
|
+
Add LLM calls with error handling:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { z } from 'zod';
|
|
92
|
+
import { defineAgent, type OneShotAgentOutput, type LLMError } from '@vibe-agent-toolkit/agent-schema';
|
|
93
|
+
|
|
94
|
+
const InputSchema = z.object({
|
|
95
|
+
text: z.string(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const OutputDataSchema = z.object({
|
|
99
|
+
sentiment: z.enum(['positive', 'negative', 'neutral']),
|
|
100
|
+
confidence: z.number().min(0).max(1),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const sentimentAnalyzer = defineAgent<
|
|
104
|
+
z.infer<typeof InputSchema>,
|
|
105
|
+
OneShotAgentOutput<z.infer<typeof OutputDataSchema>, LLMError>
|
|
106
|
+
>({
|
|
107
|
+
name: 'sentiment-analyzer',
|
|
108
|
+
description: 'Analyzes sentiment of text',
|
|
109
|
+
version: '1.0.0',
|
|
110
|
+
inputSchema: InputSchema,
|
|
111
|
+
outputSchema: z.object({
|
|
112
|
+
result: z.discriminatedUnion('status', [
|
|
113
|
+
z.object({
|
|
114
|
+
status: z.literal('success'),
|
|
115
|
+
data: OutputDataSchema,
|
|
116
|
+
}),
|
|
117
|
+
z.object({
|
|
118
|
+
status: z.literal('error'),
|
|
119
|
+
error: z.enum([
|
|
120
|
+
'llm-refusal',
|
|
121
|
+
'llm-invalid-output',
|
|
122
|
+
'llm-timeout',
|
|
123
|
+
'llm-rate-limit',
|
|
124
|
+
'llm-token-limit',
|
|
125
|
+
'llm-unavailable',
|
|
126
|
+
]),
|
|
127
|
+
}),
|
|
128
|
+
]),
|
|
129
|
+
}),
|
|
130
|
+
|
|
131
|
+
async execute(input, context) {
|
|
132
|
+
try {
|
|
133
|
+
const prompt = `Analyze the sentiment of this text: "${input.text}"\n\nRespond with JSON: {"sentiment": "positive" | "negative" | "neutral", "confidence": 0.0-1.0}`;
|
|
134
|
+
|
|
135
|
+
const response = await context.callLLM([
|
|
136
|
+
{ role: 'user', content: prompt },
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
// Parse LLM response
|
|
140
|
+
const parsed = JSON.parse(response);
|
|
141
|
+
const validated = OutputDataSchema.parse(parsed);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
result: {
|
|
145
|
+
status: 'success' as const,
|
|
146
|
+
data: validated,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// Map exceptions to LLMError types
|
|
151
|
+
if (error instanceof Error && error.message.includes('timeout')) {
|
|
152
|
+
return {
|
|
153
|
+
result: { status: 'error' as const, error: 'llm-timeout' as const },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
result: { status: 'error' as const, error: 'llm-invalid-output' as const },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Minimal Conversational Agent
|
|
166
|
+
|
|
167
|
+
Multi-turn conversation with session state:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { z } from 'zod';
|
|
171
|
+
import { defineAgent, type ConversationalAgentOutput } from '@vibe-agent-toolkit/agent-schema';
|
|
172
|
+
|
|
173
|
+
const ProfileSchema = z.object({
|
|
174
|
+
name: z.string().optional(),
|
|
175
|
+
age: z.number().optional(),
|
|
176
|
+
conversationPhase: z.enum(['gathering', 'ready']).default('gathering'),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const InputSchema = z.object({
|
|
180
|
+
message: z.string(),
|
|
181
|
+
sessionState: z.object({
|
|
182
|
+
profile: ProfileSchema,
|
|
183
|
+
}).optional(),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const FinalDataSchema = z.object({
|
|
187
|
+
profile: ProfileSchema,
|
|
188
|
+
greeting: z.string(),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export const greeterAgent = defineAgent<
|
|
192
|
+
z.infer<typeof InputSchema>,
|
|
193
|
+
ConversationalAgentOutput<z.infer<typeof FinalDataSchema>, 'abandoned', z.infer<typeof ProfileSchema>>
|
|
194
|
+
>({
|
|
195
|
+
name: 'greeter',
|
|
196
|
+
description: 'Gathers user info and provides personalized greeting',
|
|
197
|
+
version: '1.0.0',
|
|
198
|
+
inputSchema: InputSchema,
|
|
199
|
+
outputSchema: z.object({
|
|
200
|
+
reply: z.string(),
|
|
201
|
+
sessionState: ProfileSchema,
|
|
202
|
+
result: z.discriminatedUnion('status', [
|
|
203
|
+
z.object({
|
|
204
|
+
status: z.literal('in-progress'),
|
|
205
|
+
metadata: ProfileSchema.optional(),
|
|
206
|
+
}),
|
|
207
|
+
z.object({
|
|
208
|
+
status: z.literal('success'),
|
|
209
|
+
data: FinalDataSchema,
|
|
210
|
+
}),
|
|
211
|
+
z.object({
|
|
212
|
+
status: z.literal('error'),
|
|
213
|
+
error: z.literal('abandoned'),
|
|
214
|
+
}),
|
|
215
|
+
]),
|
|
216
|
+
}),
|
|
217
|
+
|
|
218
|
+
async execute(input, context) {
|
|
219
|
+
const profile = input.sessionState?.profile ?? { conversationPhase: 'gathering' as const };
|
|
220
|
+
|
|
221
|
+
// Gathering phase
|
|
222
|
+
if (profile.conversationPhase === 'gathering') {
|
|
223
|
+
if (!profile.name) {
|
|
224
|
+
return {
|
|
225
|
+
reply: "Hi! What's your name?",
|
|
226
|
+
sessionState: profile,
|
|
227
|
+
result: {
|
|
228
|
+
status: 'in-progress' as const,
|
|
229
|
+
metadata: profile,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!profile.age) {
|
|
235
|
+
return {
|
|
236
|
+
reply: `Nice to meet you, ${profile.name}! How old are you?`,
|
|
237
|
+
sessionState: { ...profile, conversationPhase: 'gathering' as const },
|
|
238
|
+
result: {
|
|
239
|
+
status: 'in-progress' as const,
|
|
240
|
+
metadata: profile,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Have all info - transition to ready
|
|
246
|
+
const greeting = `Great, ${profile.name}! At ${profile.age} years old, you're in your prime!`;
|
|
247
|
+
return {
|
|
248
|
+
reply: greeting,
|
|
249
|
+
sessionState: { ...profile, conversationPhase: 'ready' as const },
|
|
250
|
+
result: {
|
|
251
|
+
status: 'success' as const,
|
|
252
|
+
data: {
|
|
253
|
+
profile: { ...profile, conversationPhase: 'ready' as const },
|
|
254
|
+
greeting,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Already complete
|
|
261
|
+
return {
|
|
262
|
+
reply: 'We already completed our conversation!',
|
|
263
|
+
sessionState: profile,
|
|
264
|
+
result: {
|
|
265
|
+
status: 'success' as const,
|
|
266
|
+
data: {
|
|
267
|
+
profile,
|
|
268
|
+
greeting: `Hello again, ${profile.name}!`,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Core Patterns
|
|
277
|
+
|
|
278
|
+
### Pattern 1: Pure Function Agent
|
|
279
|
+
|
|
280
|
+
**When to use**: Stateless validation, transformation, or computation
|
|
281
|
+
|
|
282
|
+
**Characteristics**:
|
|
283
|
+
- No LLM calls
|
|
284
|
+
- Deterministic output
|
|
285
|
+
- Fast execution
|
|
286
|
+
- Easy to test
|
|
287
|
+
|
|
288
|
+
**Template**:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
export const myAgent = defineAgent({
|
|
292
|
+
name: 'my-agent',
|
|
293
|
+
// ... metadata
|
|
294
|
+
|
|
295
|
+
async execute(input) {
|
|
296
|
+
// Validate input
|
|
297
|
+
if (/* invalid condition */) {
|
|
298
|
+
return {
|
|
299
|
+
result: { status: 'error', error: 'specific-error-code' },
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Compute result
|
|
304
|
+
const data = computeData(input);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
result: { status: 'success', data },
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Examples**: `haiku-validator`, `name-validator`
|
|
314
|
+
|
|
315
|
+
### Pattern 2: One-Shot LLM Analyzer
|
|
316
|
+
|
|
317
|
+
**When to use**: Single LLM call for analysis, classification, or generation
|
|
318
|
+
|
|
319
|
+
**Characteristics**:
|
|
320
|
+
- One LLM call per execution
|
|
321
|
+
- Stateless
|
|
322
|
+
- Handles LLM errors
|
|
323
|
+
- Parses and validates LLM output
|
|
324
|
+
|
|
325
|
+
**Template**:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
export const myAnalyzer = defineAgent({
|
|
329
|
+
name: 'my-analyzer',
|
|
330
|
+
// ... metadata
|
|
331
|
+
|
|
332
|
+
async execute(input, context) {
|
|
333
|
+
try {
|
|
334
|
+
// Build prompt
|
|
335
|
+
const prompt = buildPrompt(input);
|
|
336
|
+
|
|
337
|
+
// Call LLM
|
|
338
|
+
const response = await context.callLLM([
|
|
339
|
+
{ role: 'user', content: prompt },
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
// Parse and validate
|
|
343
|
+
const parsed = parseResponse(response);
|
|
344
|
+
const validated = OutputSchema.parse(parsed);
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
result: { status: 'success', data: validated },
|
|
348
|
+
};
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return {
|
|
351
|
+
result: { status: 'error', error: mapError(error) },
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Examples**: `photo-analyzer`, `description-parser`, `name-generator`, `haiku-generator`
|
|
359
|
+
|
|
360
|
+
### Pattern 3: Conversational Assistant
|
|
361
|
+
|
|
362
|
+
**When to use**: Multi-turn dialogue, progressive data collection
|
|
363
|
+
|
|
364
|
+
**Characteristics**:
|
|
365
|
+
- Multiple LLM calls across turns
|
|
366
|
+
- Maintains session state
|
|
367
|
+
- Phases (gathering → ready → complete)
|
|
368
|
+
- Natural language replies + machine-readable results
|
|
369
|
+
|
|
370
|
+
**Template**:
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
export const myAssistant = defineAgent({
|
|
374
|
+
name: 'my-assistant',
|
|
375
|
+
// ... metadata
|
|
376
|
+
|
|
377
|
+
async execute(input, context) {
|
|
378
|
+
// Get current state
|
|
379
|
+
const state = input.sessionState ?? getInitialState();
|
|
380
|
+
|
|
381
|
+
// Phase 1: Gathering
|
|
382
|
+
if (state.phase === 'gathering') {
|
|
383
|
+
if (/* need more info */) {
|
|
384
|
+
return {
|
|
385
|
+
reply: 'Question to gather info?',
|
|
386
|
+
sessionState: state,
|
|
387
|
+
result: {
|
|
388
|
+
status: 'in-progress',
|
|
389
|
+
metadata: { progress: state },
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Enough info - transition to ready
|
|
395
|
+
state.phase = 'ready';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Phase 2: Ready/Complete
|
|
399
|
+
if (state.phase === 'ready') {
|
|
400
|
+
const finalData = computeFinalData(state);
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
reply: 'Here is your result!',
|
|
404
|
+
sessionState: { ...state, phase: 'complete' },
|
|
405
|
+
result: {
|
|
406
|
+
status: 'success',
|
|
407
|
+
data: finalData,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Already complete
|
|
413
|
+
return {
|
|
414
|
+
reply: 'We finished!',
|
|
415
|
+
sessionState: state,
|
|
416
|
+
result: {
|
|
417
|
+
status: 'success',
|
|
418
|
+
data: state.finalData,
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Examples**: `breed-advisor`
|
|
426
|
+
|
|
427
|
+
### Pattern 4: External Event Integrator
|
|
428
|
+
|
|
429
|
+
**When to use**: Waiting for external events (human approval, webhooks, etc.)
|
|
430
|
+
|
|
431
|
+
**Characteristics**:
|
|
432
|
+
- Emits event, blocks waiting for response
|
|
433
|
+
- Timeout handling
|
|
434
|
+
- External system unavailability
|
|
435
|
+
- Can be mocked for testing
|
|
436
|
+
|
|
437
|
+
**Template**:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
export const myEventAgent = defineAgent({
|
|
441
|
+
name: 'my-event-agent',
|
|
442
|
+
// ... metadata
|
|
443
|
+
|
|
444
|
+
async execute(input, options = {}) {
|
|
445
|
+
const { mockable = true, timeout = 30000 } = options;
|
|
446
|
+
|
|
447
|
+
if (mockable) {
|
|
448
|
+
// Mock mode - instant response for testing
|
|
449
|
+
return {
|
|
450
|
+
result: {
|
|
451
|
+
status: 'success',
|
|
452
|
+
data: getMockResponse(input),
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
// Emit event to external system
|
|
459
|
+
const response = await emitEvent(input, timeout);
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
result: {
|
|
463
|
+
status: 'success',
|
|
464
|
+
data: response,
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (error.code === 'TIMEOUT') {
|
|
469
|
+
return {
|
|
470
|
+
result: { status: 'error', error: 'event-timeout' },
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
result: { status: 'error', error: 'event-unavailable' },
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Examples**: `human-approval`
|
|
483
|
+
|
|
484
|
+
## Error Handling Strategy
|
|
485
|
+
|
|
486
|
+
### Principle: Errors are Data
|
|
487
|
+
|
|
488
|
+
Errors are part of the result envelope, not exceptions. **Always use exported constants**, not string literals:
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
import {
|
|
492
|
+
RESULT_ERROR,
|
|
493
|
+
LLM_TIMEOUT,
|
|
494
|
+
LLM_RATE_LIMIT,
|
|
495
|
+
LLM_INVALID_OUTPUT,
|
|
496
|
+
} from '@vibe-agent-toolkit/agent-schema';
|
|
497
|
+
|
|
498
|
+
// ✅ GOOD - Error as data with constants
|
|
499
|
+
return {
|
|
500
|
+
result: { status: RESULT_ERROR, error: LLM_TIMEOUT },
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// ❌ BAD - String literals (no autocomplete, typo-prone)
|
|
504
|
+
return {
|
|
505
|
+
result: { status: 'error', error: 'llm-timeout' },
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// ❌ WORSE - Throwing exceptions
|
|
509
|
+
throw new Error('LLM timeout');
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Standard Error Constants
|
|
513
|
+
|
|
514
|
+
**Always import and use constants** for error types and status values:
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
import {
|
|
518
|
+
// Status constants
|
|
519
|
+
RESULT_SUCCESS,
|
|
520
|
+
RESULT_ERROR,
|
|
521
|
+
RESULT_IN_PROGRESS,
|
|
522
|
+
// LLM error constants
|
|
523
|
+
LLM_REFUSAL,
|
|
524
|
+
LLM_INVALID_OUTPUT,
|
|
525
|
+
LLM_TIMEOUT,
|
|
526
|
+
LLM_RATE_LIMIT,
|
|
527
|
+
LLM_TOKEN_LIMIT,
|
|
528
|
+
LLM_UNAVAILABLE,
|
|
529
|
+
// Event error constants
|
|
530
|
+
EVENT_TIMEOUT,
|
|
531
|
+
EVENT_UNAVAILABLE,
|
|
532
|
+
EVENT_REJECTED,
|
|
533
|
+
EVENT_INVALID_RESPONSE,
|
|
534
|
+
// Types
|
|
535
|
+
type LLMError,
|
|
536
|
+
type ExternalEventError,
|
|
537
|
+
} from '@vibe-agent-toolkit/agent-schema';
|
|
538
|
+
|
|
539
|
+
// LLM-based agents use LLM error constants
|
|
540
|
+
type MyError = LLMError;
|
|
541
|
+
|
|
542
|
+
// Event-based agents use Event error constants
|
|
543
|
+
type MyError = ExternalEventError;
|
|
544
|
+
|
|
545
|
+
// Custom domain errors (for pure function agents)
|
|
546
|
+
// ✅ GOOD - Define constants first
|
|
547
|
+
const VALIDATION_ERROR = 'invalid-format' as const;
|
|
548
|
+
const PROCESSING_ERROR = 'processing-failed' as const;
|
|
549
|
+
|
|
550
|
+
type MyError =
|
|
551
|
+
| typeof VALIDATION_ERROR
|
|
552
|
+
| typeof PROCESSING_ERROR;
|
|
553
|
+
|
|
554
|
+
// ❌ BAD - String literals in type (no single source of truth)
|
|
555
|
+
type MyError =
|
|
556
|
+
| 'invalid-format'
|
|
557
|
+
| 'processing-failed';
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**Benefits of constants:**
|
|
561
|
+
- Autocomplete in IDEs
|
|
562
|
+
- Typo prevention at compile time
|
|
563
|
+
- Easy refactoring (change once, update everywhere)
|
|
564
|
+
- Consistent error strings across codebase
|
|
565
|
+
|
|
566
|
+
### Pure Function Agent Pattern
|
|
567
|
+
|
|
568
|
+
Use `definePureFunction()` to create agents with automatic input/output validation:
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
import { definePureFunction } from '@vibe-agent-toolkit/agent-runtime';
|
|
572
|
+
import { z } from 'zod';
|
|
573
|
+
|
|
574
|
+
// Define schemas
|
|
575
|
+
const MyInputSchema = z.object({ value: z.string() });
|
|
576
|
+
const MyOutputSchema = z.object({ result: z.boolean() });
|
|
577
|
+
|
|
578
|
+
// Define agent with declarative config
|
|
579
|
+
export const myAgent = definePureFunction(
|
|
580
|
+
{
|
|
581
|
+
name: 'my-validator',
|
|
582
|
+
version: '1.0.0',
|
|
583
|
+
description: 'Validates input data',
|
|
584
|
+
inputSchema: MyInputSchema,
|
|
585
|
+
outputSchema: MyOutputSchema,
|
|
586
|
+
},
|
|
587
|
+
(input) => {
|
|
588
|
+
// Input is already validated by wrapper
|
|
589
|
+
// Just return the output - wrapper handles validation
|
|
590
|
+
return processData(input.value);
|
|
591
|
+
}
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
// Usage
|
|
595
|
+
const result = myAgent.execute({ value: 'test' }); // Returns MyOutput directly
|
|
596
|
+
// Throws exception if input is invalid
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**Complete example from vat-example-cat-agents:**
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
import { definePureFunction } from '@vibe-agent-toolkit/agent-runtime';
|
|
603
|
+
import { NameValidationInputSchema, NameValidationResultSchema } from './schemas.js';
|
|
604
|
+
|
|
605
|
+
export const nameValidatorAgent = definePureFunction(
|
|
606
|
+
{
|
|
607
|
+
name: 'name-validator',
|
|
608
|
+
version: '1.0.0',
|
|
609
|
+
description: 'Validates cat names for proper nobility conventions',
|
|
610
|
+
inputSchema: NameValidationInputSchema, // ✅ Declarative validation
|
|
611
|
+
outputSchema: NameValidationResultSchema, // ✅ Output validation too
|
|
612
|
+
},
|
|
613
|
+
(input) => {
|
|
614
|
+
// Input is already validated - just implement logic
|
|
615
|
+
return validateCatName(input.name, input.characteristics); // ✅ Clean handler
|
|
616
|
+
}
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
// Usage
|
|
620
|
+
const result = nameValidatorAgent.execute({ name: 'Duke Sterling III' });
|
|
621
|
+
// result = { status: 'valid', reason: '...' }
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**Key benefits of `definePureFunction`:**
|
|
625
|
+
- ✅ Automatic input validation (throws on invalid input)
|
|
626
|
+
- ✅ Automatic output validation (throws on invalid output)
|
|
627
|
+
- ✅ No manual result envelope wrapping needed
|
|
628
|
+
- ✅ Synchronous execution (returns directly, not wrapped in Promise)
|
|
629
|
+
- ✅ Manifest auto-generated from schemas
|
|
630
|
+
|
|
631
|
+
### Mapping Exceptions
|
|
632
|
+
|
|
633
|
+
Catch exceptions and map to error constants:
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
import { RESULT_ERROR, LLM_TIMEOUT, LLM_RATE_LIMIT, LLM_UNAVAILABLE } from '@vibe-agent-toolkit/agent-schema';
|
|
637
|
+
|
|
638
|
+
async execute(input, context) {
|
|
639
|
+
try {
|
|
640
|
+
const response = await context.callLLM(/* ... */);
|
|
641
|
+
// ... process response
|
|
642
|
+
} catch (error) {
|
|
643
|
+
// Map to standard error constants
|
|
644
|
+
if (error instanceof Error) {
|
|
645
|
+
if (error.message.includes('timeout')) {
|
|
646
|
+
return { result: { status: RESULT_ERROR, error: LLM_TIMEOUT } };
|
|
647
|
+
}
|
|
648
|
+
if (error.message.includes('rate limit')) {
|
|
649
|
+
return { result: { status: RESULT_ERROR, error: LLM_RATE_LIMIT } };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Default to generic error
|
|
654
|
+
return { result: { status: RESULT_ERROR, error: LLM_UNAVAILABLE } };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Validation Errors
|
|
660
|
+
|
|
661
|
+
Use Zod for input/output validation with error constants:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
import { RESULT_SUCCESS, RESULT_ERROR, LLM_INVALID_OUTPUT } from '@vibe-agent-toolkit/agent-schema';
|
|
665
|
+
|
|
666
|
+
async execute(input) {
|
|
667
|
+
// Validate input (automatic with schemas)
|
|
668
|
+
const validated = InputSchema.parse(input);
|
|
669
|
+
|
|
670
|
+
// ... process
|
|
671
|
+
|
|
672
|
+
// Validate output before returning
|
|
673
|
+
try {
|
|
674
|
+
const data = OutputDataSchema.parse(computedData);
|
|
675
|
+
return { result: { status: RESULT_SUCCESS, data } };
|
|
676
|
+
} catch (error) {
|
|
677
|
+
return { result: { status: RESULT_ERROR, error: LLM_INVALID_OUTPUT } };
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
## Testing Patterns
|
|
683
|
+
|
|
684
|
+
### Unit Tests for Pure Functions
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
import { describe, expect, it } from 'vitest';
|
|
688
|
+
import { resultMatchers } from '@vibe-agent-toolkit/agent-runtime';
|
|
689
|
+
|
|
690
|
+
describe('myValidator', () => {
|
|
691
|
+
it('should validate correct input', async () => {
|
|
692
|
+
const output = await myValidator.execute({
|
|
693
|
+
text: 'valid input',
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
resultMatchers.expectSuccess(output.result);
|
|
697
|
+
expect(output.result.data.valid).toBe(true);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('should reject invalid input', async () => {
|
|
701
|
+
const output = await myValidator.execute({
|
|
702
|
+
text: 'x', // too short
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
resultMatchers.expectError(output.result);
|
|
706
|
+
expect(output.result.error).toBe('too-short');
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### Integration Tests with Mock LLM
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
import { createMockContext } from '@vibe-agent-toolkit/agent-runtime';
|
|
715
|
+
|
|
716
|
+
describe('myAnalyzer with mock LLM', () => {
|
|
717
|
+
it('should parse LLM response', async () => {
|
|
718
|
+
const mockContext = createMockContext(
|
|
719
|
+
JSON.stringify({ sentiment: 'positive', confidence: 0.9 })
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
const output = await myAnalyzer.execute(
|
|
723
|
+
{ text: 'Great product!' },
|
|
724
|
+
mockContext
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
resultMatchers.expectSuccess(output.result);
|
|
728
|
+
expect(output.result.data.sentiment).toBe('positive');
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('should handle invalid LLM output', async () => {
|
|
732
|
+
const mockContext = createMockContext('invalid json');
|
|
733
|
+
|
|
734
|
+
const output = await myAnalyzer.execute(
|
|
735
|
+
{ text: 'Test' },
|
|
736
|
+
mockContext
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
resultMatchers.expectError(output.result);
|
|
740
|
+
expect(output.result.error).toBe('llm-invalid-output');
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### Conversational Agent Tests
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
describe('greeterAgent conversation flow', () => {
|
|
749
|
+
it('should gather name first', async () => {
|
|
750
|
+
const output = await greeterAgent.execute({
|
|
751
|
+
message: 'Hello',
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
expect(output.reply).toContain("What's your name?");
|
|
755
|
+
resultMatchers.expectInProgress(output.result);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('should gather age second', async () => {
|
|
759
|
+
const output = await greeterAgent.execute({
|
|
760
|
+
message: 'My name is Alice',
|
|
761
|
+
sessionState: {
|
|
762
|
+
profile: { name: 'Alice', conversationPhase: 'gathering' },
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
expect(output.reply).toContain('How old are you?');
|
|
767
|
+
expect(output.sessionState.name).toBe('Alice');
|
|
768
|
+
resultMatchers.expectInProgress(output.result);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('should complete with greeting', async () => {
|
|
772
|
+
const output = await greeterAgent.execute({
|
|
773
|
+
message: 'I am 30',
|
|
774
|
+
sessionState: {
|
|
775
|
+
profile: {
|
|
776
|
+
name: 'Alice',
|
|
777
|
+
age: 30,
|
|
778
|
+
conversationPhase: 'gathering',
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
expect(output.reply).toContain('in your prime');
|
|
784
|
+
resultMatchers.expectSuccess(output.result);
|
|
785
|
+
expect(output.result.data.greeting).toBeDefined();
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
## Best Practices
|
|
791
|
+
|
|
792
|
+
### 1. Define Clear Schemas
|
|
793
|
+
|
|
794
|
+
Use Zod for type-safe schemas:
|
|
795
|
+
|
|
796
|
+
```typescript
|
|
797
|
+
// ✅ GOOD - Explicit schemas
|
|
798
|
+
const InputSchema = z.object({
|
|
799
|
+
text: z.string().min(1).max(1000),
|
|
800
|
+
options: z.object({
|
|
801
|
+
strict: z.boolean().default(false),
|
|
802
|
+
}).optional(),
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// ❌ BAD - Loose types
|
|
806
|
+
const InputSchema = z.object({
|
|
807
|
+
text: z.string(),
|
|
808
|
+
options: z.any(),
|
|
809
|
+
});
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
### 2. Use Discriminated Unions
|
|
813
|
+
|
|
814
|
+
Leverage TypeScript's type narrowing:
|
|
815
|
+
|
|
816
|
+
```typescript
|
|
817
|
+
// ✅ GOOD - Discriminated union
|
|
818
|
+
type Result =
|
|
819
|
+
| { status: 'success'; data: Data }
|
|
820
|
+
| { status: 'error'; error: Error };
|
|
821
|
+
|
|
822
|
+
// ❌ BAD - Flat structure
|
|
823
|
+
type Result = {
|
|
824
|
+
status: 'success' | 'error';
|
|
825
|
+
data?: Data;
|
|
826
|
+
error?: Error;
|
|
827
|
+
};
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### 3. Consistent Error Types
|
|
831
|
+
|
|
832
|
+
Use enums or literal unions:
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
// ✅ GOOD - Typed errors
|
|
836
|
+
type Error = 'too-short' | 'too-long' | 'invalid';
|
|
837
|
+
|
|
838
|
+
// ❌ BAD - String errors
|
|
839
|
+
type Error = string;
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### 4. Document Metadata
|
|
843
|
+
|
|
844
|
+
For conversational agents, document metadata structure:
|
|
845
|
+
|
|
846
|
+
```typescript
|
|
847
|
+
/**
|
|
848
|
+
* Metadata for in-progress state
|
|
849
|
+
*/
|
|
850
|
+
interface Metadata {
|
|
851
|
+
/** Current conversation phase */
|
|
852
|
+
phase: 'gathering' | 'ready';
|
|
853
|
+
/** Number of questions answered */
|
|
854
|
+
answered: number;
|
|
855
|
+
/** Total questions required */
|
|
856
|
+
total: number;
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### 5. Test All Paths
|
|
861
|
+
|
|
862
|
+
Cover success, errors, and edge cases:
|
|
863
|
+
|
|
864
|
+
```typescript
|
|
865
|
+
describe('myAgent', () => {
|
|
866
|
+
it('should handle success case', /* ... */);
|
|
867
|
+
it('should handle validation error', /* ... */);
|
|
868
|
+
it('should handle LLM timeout', /* ... */);
|
|
869
|
+
it('should handle malformed input', /* ... */);
|
|
870
|
+
});
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
### 6. Use Mock Mode for External Events
|
|
874
|
+
|
|
875
|
+
Enable testing without external dependencies:
|
|
876
|
+
|
|
877
|
+
```typescript
|
|
878
|
+
async execute(input, options = {}) {
|
|
879
|
+
const { mockable = true } = options;
|
|
880
|
+
|
|
881
|
+
if (mockable) {
|
|
882
|
+
// Instant response for tests
|
|
883
|
+
return getMockResponse(input);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Real external call
|
|
887
|
+
return await callExternalSystem(input);
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
## Related Documentation
|
|
892
|
+
|
|
893
|
+
- [Orchestration Guide](orchestration.md) - Composing agents into workflows
|
|
894
|
+
- Architecture Overview - Package structure
|
|
895
|
+
|
|
896
|
+
## Examples
|
|
897
|
+
|
|
898
|
+
See `@vibe-agent-toolkit/vat-example-cat-agents` for complete working examples:
|
|
899
|
+
|
|
900
|
+
```bash
|
|
901
|
+
cd packages/vat-example-cat-agents
|
|
902
|
+
bun run test # Run all tests
|
|
903
|
+
bun run demo:photos # Photo analysis demo
|
|
904
|
+
bun run demo:conversation # Conversational demo
|
|
905
|
+
```
|