@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.
@@ -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
+ ```