@providerprotocol/agents 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.claude/settings.local.json +27 -0
  2. package/AGENTS.md +681 -0
  3. package/CLAUDE.md +681 -0
  4. package/README.md +15 -0
  5. package/bun.lock +472 -0
  6. package/eslint.config.js +75 -0
  7. package/index.ts +1 -0
  8. package/llms.md +796 -0
  9. package/package.json +37 -0
  10. package/specs/UAP-1.0.md +2355 -0
  11. package/src/agent/index.ts +384 -0
  12. package/src/agent/types.ts +91 -0
  13. package/src/checkpoint/file.ts +126 -0
  14. package/src/checkpoint/index.ts +40 -0
  15. package/src/checkpoint/types.ts +95 -0
  16. package/src/execution/index.ts +37 -0
  17. package/src/execution/loop.ts +310 -0
  18. package/src/execution/plan.ts +497 -0
  19. package/src/execution/react.ts +340 -0
  20. package/src/execution/tool-ordering.ts +186 -0
  21. package/src/execution/types.ts +315 -0
  22. package/src/index.ts +80 -0
  23. package/src/middleware/index.ts +7 -0
  24. package/src/middleware/logging.ts +123 -0
  25. package/src/middleware/types.ts +69 -0
  26. package/src/state/index.ts +301 -0
  27. package/src/state/types.ts +173 -0
  28. package/src/thread-tree/index.ts +249 -0
  29. package/src/thread-tree/types.ts +29 -0
  30. package/src/utils/uuid.ts +7 -0
  31. package/tests/live/agent-anthropic.test.ts +288 -0
  32. package/tests/live/agent-strategy-hooks.test.ts +268 -0
  33. package/tests/live/checkpoint.test.ts +243 -0
  34. package/tests/live/execution-strategies.test.ts +255 -0
  35. package/tests/live/plan-strategy.test.ts +160 -0
  36. package/tests/live/subagent-events.live.test.ts +249 -0
  37. package/tests/live/thread-tree.test.ts +186 -0
  38. package/tests/unit/agent.test.ts +703 -0
  39. package/tests/unit/checkpoint.test.ts +232 -0
  40. package/tests/unit/execution/equivalence.test.ts +402 -0
  41. package/tests/unit/execution/loop.test.ts +437 -0
  42. package/tests/unit/execution/plan.test.ts +590 -0
  43. package/tests/unit/execution/react.test.ts +604 -0
  44. package/tests/unit/execution/subagent-events.test.ts +235 -0
  45. package/tests/unit/execution/tool-ordering.test.ts +310 -0
  46. package/tests/unit/middleware/logging.test.ts +276 -0
  47. package/tests/unit/state.test.ts +573 -0
  48. package/tests/unit/thread-tree.test.ts +249 -0
  49. package/tsconfig.json +29 -0
@@ -0,0 +1,703 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { UserMessage, AssistantMessage } from '@providerprotocol/ai';
3
+ import type { Turn, Tool, ModelReference, LLMInstance } from '@providerprotocol/ai';
4
+ import { agent } from '../../src/agent/index.ts';
5
+ import { AgentState } from '../../src/state/index.ts';
6
+ import type { ExecutionStrategy } from '../../src/execution/types.ts';
7
+ import type { Middleware } from '../../src/middleware/types.ts';
8
+
9
+ // Mock model reference - use unknown cast for unit tests
10
+ const mockModel = {
11
+ provider: 'mock',
12
+ modelId: 'mock-model',
13
+ } as unknown as ModelReference;
14
+
15
+ // Mock Turn factory
16
+ function createMockTurn(text: string = 'Response'): Turn {
17
+ const response = new AssistantMessage(text);
18
+ return {
19
+ response,
20
+ messages: [response],
21
+ toolExecutions: [],
22
+ usage: {
23
+ inputTokens: 10,
24
+ outputTokens: 20,
25
+ totalTokens: 30,
26
+ },
27
+ cycles: 1,
28
+ } as unknown as Turn;
29
+ }
30
+
31
+ // Mock LLM instance for testing
32
+ const mockLLM = {
33
+ generate: async () => createMockTurn(),
34
+ stream: () => ({
35
+ async *[Symbol.asyncIterator] () {},
36
+ turn: Promise.resolve(createMockTurn()),
37
+ }),
38
+ } as unknown as LLMInstance;
39
+
40
+ describe('agent()', () => {
41
+ describe('creation', () => {
42
+ test('creates agent with unique ID', () => {
43
+ // Create a custom execution strategy for testing
44
+ const mockStrategy: ExecutionStrategy = {
45
+ name: 'mock',
46
+ async execute() {
47
+ return {
48
+ turn: createMockTurn(),
49
+ state: AgentState.initial(),
50
+ };
51
+ },
52
+ stream() {
53
+ return {
54
+ async *[Symbol.asyncIterator] () {},
55
+ result: Promise.resolve({
56
+ turn: createMockTurn(),
57
+ state: AgentState.initial(),
58
+ }),
59
+ abort: () => {},
60
+ };
61
+ },
62
+ };
63
+
64
+ const agent1 = agent({
65
+ model: mockModel,
66
+ execution: mockStrategy,
67
+ _llmInstance: mockLLM,
68
+ });
69
+
70
+ const agent2 = agent({
71
+ model: mockModel,
72
+ execution: mockStrategy,
73
+ _llmInstance: mockLLM,
74
+ });
75
+
76
+ expect(agent1.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
77
+ expect(agent1.id).not.toBe(agent2.id);
78
+ });
79
+
80
+ test('stores model reference', () => {
81
+ const mockStrategy: ExecutionStrategy = {
82
+ name: 'mock',
83
+ async execute() {
84
+ return {
85
+ turn: createMockTurn(),
86
+ state: AgentState.initial(),
87
+ };
88
+ },
89
+ stream() {
90
+ return {
91
+ async *[Symbol.asyncIterator] () {},
92
+ result: Promise.resolve({
93
+ turn: createMockTurn(),
94
+ state: AgentState.initial(),
95
+ }),
96
+ abort: () => {},
97
+ };
98
+ },
99
+ };
100
+
101
+ const a = agent({
102
+ model: mockModel,
103
+ execution: mockStrategy,
104
+ _llmInstance: mockLLM,
105
+ });
106
+
107
+ expect(a.model).toBe(mockModel);
108
+ });
109
+
110
+ test('stores tools', () => {
111
+ const mockTool: Tool = {
112
+ name: 'test_tool',
113
+ description: 'A test tool',
114
+ parameters: { type: 'object', properties: {} },
115
+ run: async () => 'result',
116
+ };
117
+
118
+ const mockStrategy: ExecutionStrategy = {
119
+ name: 'mock',
120
+ async execute() {
121
+ return {
122
+ turn: createMockTurn(),
123
+ state: AgentState.initial(),
124
+ };
125
+ },
126
+ stream() {
127
+ return {
128
+ async *[Symbol.asyncIterator] () {},
129
+ result: Promise.resolve({
130
+ turn: createMockTurn(),
131
+ state: AgentState.initial(),
132
+ }),
133
+ abort: () => {},
134
+ };
135
+ },
136
+ };
137
+
138
+ const a = agent({
139
+ model: mockModel,
140
+ tools: [mockTool],
141
+ execution: mockStrategy,
142
+ _llmInstance: mockLLM,
143
+ });
144
+
145
+ expect(a.tools).toHaveLength(1);
146
+ expect(a.tools[0]).toBe(mockTool);
147
+ });
148
+
149
+ test('stores system prompt', () => {
150
+ const mockStrategy: ExecutionStrategy = {
151
+ name: 'mock',
152
+ async execute() {
153
+ return {
154
+ turn: createMockTurn(),
155
+ state: AgentState.initial(),
156
+ };
157
+ },
158
+ stream() {
159
+ return {
160
+ async *[Symbol.asyncIterator] () {},
161
+ result: Promise.resolve({
162
+ turn: createMockTurn(),
163
+ state: AgentState.initial(),
164
+ }),
165
+ abort: () => {},
166
+ };
167
+ },
168
+ };
169
+
170
+ const a = agent({
171
+ model: mockModel,
172
+ system: 'You are a helpful assistant.',
173
+ execution: mockStrategy,
174
+ _llmInstance: mockLLM,
175
+ });
176
+
177
+ expect(a.system).toBe('You are a helpful assistant.');
178
+ });
179
+
180
+ test('defaults to empty tools array', () => {
181
+ const mockStrategy: ExecutionStrategy = {
182
+ name: 'mock',
183
+ async execute() {
184
+ return {
185
+ turn: createMockTurn(),
186
+ state: AgentState.initial(),
187
+ };
188
+ },
189
+ stream() {
190
+ return {
191
+ async *[Symbol.asyncIterator] () {},
192
+ result: Promise.resolve({
193
+ turn: createMockTurn(),
194
+ state: AgentState.initial(),
195
+ }),
196
+ abort: () => {},
197
+ };
198
+ },
199
+ };
200
+
201
+ const a = agent({
202
+ model: mockModel,
203
+ execution: mockStrategy,
204
+ _llmInstance: mockLLM,
205
+ });
206
+
207
+ expect(a.tools).toEqual([]);
208
+ });
209
+ });
210
+
211
+ describe('generate()', () => {
212
+ test('accepts string input', async () => {
213
+ let capturedInput: unknown;
214
+
215
+ const mockStrategy: ExecutionStrategy = {
216
+ name: 'mock',
217
+ async execute(ctx) {
218
+ capturedInput = ctx.input;
219
+ return {
220
+ turn: createMockTurn(),
221
+ state: ctx.state,
222
+ };
223
+ },
224
+ stream() {
225
+ return {
226
+ async *[Symbol.asyncIterator] () {},
227
+ result: Promise.resolve({
228
+ turn: createMockTurn(),
229
+ state: AgentState.initial(),
230
+ }),
231
+ abort: () => {},
232
+ };
233
+ },
234
+ };
235
+
236
+ const a = agent({
237
+ model: mockModel,
238
+ execution: mockStrategy,
239
+ _llmInstance: mockLLM,
240
+ });
241
+
242
+ await a.generate('Hello', AgentState.initial());
243
+
244
+ expect(capturedInput).toBeInstanceOf(UserMessage);
245
+ });
246
+
247
+ test('accepts Message input', async () => {
248
+ let capturedInput: unknown;
249
+ const inputMessage = new UserMessage('Hello');
250
+
251
+ const mockStrategy: ExecutionStrategy = {
252
+ name: 'mock',
253
+ async execute(ctx) {
254
+ capturedInput = ctx.input;
255
+ return {
256
+ turn: createMockTurn(),
257
+ state: ctx.state,
258
+ };
259
+ },
260
+ stream() {
261
+ return {
262
+ async *[Symbol.asyncIterator] () {},
263
+ result: Promise.resolve({
264
+ turn: createMockTurn(),
265
+ state: AgentState.initial(),
266
+ }),
267
+ abort: () => {},
268
+ };
269
+ },
270
+ };
271
+
272
+ const a = agent({
273
+ model: mockModel,
274
+ execution: mockStrategy,
275
+ _llmInstance: mockLLM,
276
+ });
277
+
278
+ await a.generate(inputMessage, AgentState.initial());
279
+
280
+ expect(capturedInput).toBe(inputMessage);
281
+ });
282
+
283
+ test('returns turn and state', async () => {
284
+ const mockTurn = createMockTurn('Test response');
285
+ const mockState = AgentState.initial().withStep(1);
286
+
287
+ const mockStrategy: ExecutionStrategy = {
288
+ name: 'mock',
289
+ async execute() {
290
+ return {
291
+ turn: mockTurn,
292
+ state: mockState,
293
+ };
294
+ },
295
+ stream() {
296
+ return {
297
+ async *[Symbol.asyncIterator] () {},
298
+ result: Promise.resolve({
299
+ turn: mockTurn,
300
+ state: mockState,
301
+ }),
302
+ abort: () => {},
303
+ };
304
+ },
305
+ };
306
+
307
+ const a = agent({
308
+ model: mockModel,
309
+ execution: mockStrategy,
310
+ _llmInstance: mockLLM,
311
+ });
312
+
313
+ const result = await a.generate('Hello', AgentState.initial());
314
+
315
+ expect(result.turn).toBe(mockTurn);
316
+ expect(result.state).toBe(mockState);
317
+ });
318
+
319
+ test('passes state to execution strategy', async () => {
320
+ let capturedState: AgentState | undefined;
321
+ const inputState = AgentState.initial()
322
+ .withStep(5)
323
+ .withMetadata('test', 'value');
324
+
325
+ const mockStrategy: ExecutionStrategy = {
326
+ name: 'mock',
327
+ async execute(ctx) {
328
+ capturedState = ctx.state;
329
+ return {
330
+ turn: createMockTurn(),
331
+ state: ctx.state,
332
+ };
333
+ },
334
+ stream() {
335
+ return {
336
+ async *[Symbol.asyncIterator] () {},
337
+ result: Promise.resolve({
338
+ turn: createMockTurn(),
339
+ state: AgentState.initial(),
340
+ }),
341
+ abort: () => {},
342
+ };
343
+ },
344
+ };
345
+
346
+ const a = agent({
347
+ model: mockModel,
348
+ execution: mockStrategy,
349
+ _llmInstance: mockLLM,
350
+ });
351
+
352
+ await a.generate('Hello', inputState);
353
+
354
+ expect(capturedState?.step).toBe(5);
355
+ expect(capturedState?.metadata).toEqual({ test: 'value' });
356
+ });
357
+ });
358
+
359
+ describe('ask()', () => {
360
+ test('preserves conversation history', async () => {
361
+ let callCount = 0;
362
+
363
+ const mockStrategy: ExecutionStrategy = {
364
+ name: 'mock',
365
+ async execute(ctx) {
366
+ callCount++;
367
+ return {
368
+ turn: createMockTurn(`Response ${callCount}`),
369
+ state: ctx.state.withStep(callCount),
370
+ };
371
+ },
372
+ stream() {
373
+ return {
374
+ async *[Symbol.asyncIterator] () {},
375
+ result: Promise.resolve({
376
+ turn: createMockTurn(),
377
+ state: AgentState.initial(),
378
+ }),
379
+ abort: () => {},
380
+ };
381
+ },
382
+ };
383
+
384
+ const a = agent({
385
+ model: mockModel,
386
+ execution: mockStrategy,
387
+ _llmInstance: mockLLM,
388
+ });
389
+
390
+ const state0 = AgentState.initial();
391
+ const result1 = await a.ask('First message', state0);
392
+
393
+ // State should have the input message added
394
+ expect(result1.state.messages.length).toBeGreaterThan(0);
395
+
396
+ const result2 = await a.ask('Second message', result1.state);
397
+
398
+ // State should have accumulated messages
399
+ expect(result2.state.messages.length).toBeGreaterThan(result1.state.messages.length);
400
+ });
401
+ });
402
+
403
+ describe('query()', () => {
404
+ test('is stateless', async () => {
405
+ const mockStrategy: ExecutionStrategy = {
406
+ name: 'mock',
407
+ async execute(ctx) {
408
+ // State should be fresh
409
+ expect(ctx.state.messages).toHaveLength(0);
410
+ expect(ctx.state.step).toBe(0);
411
+
412
+ return {
413
+ turn: createMockTurn(),
414
+ state: ctx.state,
415
+ };
416
+ },
417
+ stream() {
418
+ return {
419
+ async *[Symbol.asyncIterator] () {},
420
+ result: Promise.resolve({
421
+ turn: createMockTurn(),
422
+ state: AgentState.initial(),
423
+ }),
424
+ abort: () => {},
425
+ };
426
+ },
427
+ };
428
+
429
+ const a = agent({
430
+ model: mockModel,
431
+ execution: mockStrategy,
432
+ _llmInstance: mockLLM,
433
+ });
434
+
435
+ const turn = await a.query('Question');
436
+
437
+ expect(turn).toBeDefined();
438
+ expect(turn.response.text).toBe('Response');
439
+ });
440
+
441
+ test('returns only Turn, not state', async () => {
442
+ const mockStrategy: ExecutionStrategy = {
443
+ name: 'mock',
444
+ async execute(ctx) {
445
+ return {
446
+ turn: createMockTurn('Answer'),
447
+ state: ctx.state,
448
+ };
449
+ },
450
+ stream() {
451
+ return {
452
+ async *[Symbol.asyncIterator] () {},
453
+ result: Promise.resolve({
454
+ turn: createMockTurn(),
455
+ state: AgentState.initial(),
456
+ }),
457
+ abort: () => {},
458
+ };
459
+ },
460
+ };
461
+
462
+ const a = agent({
463
+ model: mockModel,
464
+ execution: mockStrategy,
465
+ _llmInstance: mockLLM,
466
+ });
467
+
468
+ const turn = await a.query('Question');
469
+
470
+ // Should return Turn, not GenerateResult
471
+ expect(turn.response).toBeDefined();
472
+ expect(turn.response.text).toBe('Answer');
473
+ expect((turn as unknown as { state?: unknown }).state).toBeUndefined();
474
+ });
475
+ });
476
+
477
+ describe('middleware', () => {
478
+ test('runs before middleware in order', async () => {
479
+ const order: string[] = [];
480
+
481
+ const mw1: Middleware = {
482
+ name: 'first',
483
+ async before(ctx) {
484
+ order.push('first-before');
485
+ return ctx;
486
+ },
487
+ };
488
+
489
+ const mw2: Middleware = {
490
+ name: 'second',
491
+ async before(ctx) {
492
+ order.push('second-before');
493
+ return ctx;
494
+ },
495
+ };
496
+
497
+ const mockStrategy: ExecutionStrategy = {
498
+ name: 'mock',
499
+ async execute(ctx) {
500
+ order.push('execute');
501
+ return {
502
+ turn: createMockTurn(),
503
+ state: ctx.state,
504
+ };
505
+ },
506
+ stream() {
507
+ return {
508
+ async *[Symbol.asyncIterator] () {},
509
+ result: Promise.resolve({
510
+ turn: createMockTurn(),
511
+ state: AgentState.initial(),
512
+ }),
513
+ abort: () => {},
514
+ };
515
+ },
516
+ };
517
+
518
+ const a = agent({
519
+ model: mockModel,
520
+ middleware: [mw1, mw2],
521
+ execution: mockStrategy,
522
+ _llmInstance: mockLLM,
523
+ });
524
+
525
+ await a.generate('Hello', AgentState.initial());
526
+
527
+ expect(order).toEqual(['first-before', 'second-before', 'execute']);
528
+ });
529
+
530
+ test('runs after middleware in reverse order', async () => {
531
+ const order: string[] = [];
532
+
533
+ const mw1: Middleware = {
534
+ name: 'first',
535
+ async after(ctx, result) {
536
+ order.push('first-after');
537
+ return result;
538
+ },
539
+ };
540
+
541
+ const mw2: Middleware = {
542
+ name: 'second',
543
+ async after(ctx, result) {
544
+ order.push('second-after');
545
+ return result;
546
+ },
547
+ };
548
+
549
+ const mockStrategy: ExecutionStrategy = {
550
+ name: 'mock',
551
+ async execute(ctx) {
552
+ order.push('execute');
553
+ return {
554
+ turn: createMockTurn(),
555
+ state: ctx.state,
556
+ };
557
+ },
558
+ stream() {
559
+ return {
560
+ async *[Symbol.asyncIterator] () {},
561
+ result: Promise.resolve({
562
+ turn: createMockTurn(),
563
+ state: AgentState.initial(),
564
+ }),
565
+ abort: () => {},
566
+ };
567
+ },
568
+ };
569
+
570
+ const a = agent({
571
+ model: mockModel,
572
+ middleware: [mw1, mw2],
573
+ execution: mockStrategy,
574
+ _llmInstance: mockLLM,
575
+ });
576
+
577
+ await a.generate('Hello', AgentState.initial());
578
+
579
+ expect(order).toEqual(['execute', 'second-after', 'first-after']);
580
+ });
581
+
582
+ test('middleware can modify result', async () => {
583
+ const mw: Middleware = {
584
+ name: 'modifier',
585
+ async after(ctx, result) {
586
+ return {
587
+ ...result,
588
+ state: result.state.withMetadata('modified', true),
589
+ };
590
+ },
591
+ };
592
+
593
+ const mockStrategy: ExecutionStrategy = {
594
+ name: 'mock',
595
+ async execute(ctx) {
596
+ return {
597
+ turn: createMockTurn(),
598
+ state: ctx.state,
599
+ };
600
+ },
601
+ stream() {
602
+ return {
603
+ async *[Symbol.asyncIterator] () {},
604
+ result: Promise.resolve({
605
+ turn: createMockTurn(),
606
+ state: AgentState.initial(),
607
+ }),
608
+ abort: () => {},
609
+ };
610
+ },
611
+ };
612
+
613
+ const a = agent({
614
+ model: mockModel,
615
+ middleware: [mw],
616
+ execution: mockStrategy,
617
+ _llmInstance: mockLLM,
618
+ });
619
+
620
+ const result = await a.generate('Hello', AgentState.initial());
621
+
622
+ expect(result.state.metadata).toEqual({ modified: true });
623
+ });
624
+ });
625
+
626
+ describe('stream()', () => {
627
+ test('returns AgentStreamResult', async () => {
628
+ const mockStrategy: ExecutionStrategy = {
629
+ name: 'mock',
630
+ async execute(ctx) {
631
+ return {
632
+ turn: createMockTurn(),
633
+ state: ctx.state,
634
+ };
635
+ },
636
+ stream() {
637
+ return {
638
+ async *[Symbol.asyncIterator] () {
639
+ yield { source: 'uap' as const, uap: { type: 'step_start' as const, step: 1, agentId: 'test', data: {} } };
640
+ },
641
+ result: Promise.resolve({
642
+ turn: createMockTurn(),
643
+ state: AgentState.initial(),
644
+ }),
645
+ abort: () => {},
646
+ };
647
+ },
648
+ };
649
+
650
+ const a = agent({
651
+ model: mockModel,
652
+ execution: mockStrategy,
653
+ _llmInstance: mockLLM,
654
+ });
655
+
656
+ const stream = a.stream('Hello', AgentState.initial());
657
+
658
+ expect(stream[Symbol.asyncIterator]).toBeDefined();
659
+ expect(stream.result).toBeInstanceOf(Promise);
660
+ expect(typeof stream.abort).toBe('function');
661
+ });
662
+
663
+ test('yields events from strategy', async () => {
664
+ const mockStrategy: ExecutionStrategy = {
665
+ name: 'mock',
666
+ async execute(ctx) {
667
+ return {
668
+ turn: createMockTurn(),
669
+ state: ctx.state,
670
+ };
671
+ },
672
+ stream() {
673
+ return {
674
+ async *[Symbol.asyncIterator] () {
675
+ yield { source: 'uap' as const, uap: { type: 'step_start' as const, step: 1, agentId: 'test', data: {} } };
676
+ yield { source: 'uap' as const, uap: { type: 'step_end' as const, step: 1, agentId: 'test', data: {} } };
677
+ },
678
+ result: Promise.resolve({
679
+ turn: createMockTurn(),
680
+ state: AgentState.initial(),
681
+ }),
682
+ abort: () => {},
683
+ };
684
+ },
685
+ };
686
+
687
+ const a = agent({
688
+ model: mockModel,
689
+ execution: mockStrategy,
690
+ _llmInstance: mockLLM,
691
+ });
692
+
693
+ const stream = a.stream('Hello', AgentState.initial());
694
+ const events: unknown[] = [];
695
+
696
+ for await (const event of stream) {
697
+ events.push(event);
698
+ }
699
+
700
+ expect(events).toHaveLength(2);
701
+ });
702
+ });
703
+ });