@providerprotocol/agents 0.0.2 → 0.0.4

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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/dist/checkpoint/index.d.ts +43 -0
  3. package/dist/checkpoint/index.js +73 -0
  4. package/dist/checkpoint/index.js.map +1 -0
  5. package/{src/execution/loop.ts → dist/chunk-4ESYN66B.js} +54 -162
  6. package/dist/chunk-4ESYN66B.js.map +1 -0
  7. package/dist/chunk-EKRXMSDX.js +8 -0
  8. package/dist/chunk-EKRXMSDX.js.map +1 -0
  9. package/dist/chunk-T47B3VAF.js +427 -0
  10. package/dist/chunk-T47B3VAF.js.map +1 -0
  11. package/dist/execution/index.d.ts +105 -0
  12. package/dist/execution/index.js +679 -0
  13. package/dist/execution/index.js.map +1 -0
  14. package/dist/index-qsPwbY86.d.ts +65 -0
  15. package/dist/index.d.ts +101 -0
  16. package/dist/index.js +218 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/middleware/index.d.ts +23 -0
  19. package/dist/middleware/index.js +82 -0
  20. package/dist/middleware/index.js.map +1 -0
  21. package/dist/thread-tree/index.d.ts +115 -0
  22. package/dist/thread-tree/index.js +4 -0
  23. package/dist/thread-tree/index.js.map +1 -0
  24. package/dist/types-2Vsthzyu.d.ts +163 -0
  25. package/dist/types-BiyEVOnf.d.ts +65 -0
  26. package/dist/types-D1egxttz.d.ts +270 -0
  27. package/dist/types-DChRdQoX.d.ts +98 -0
  28. package/package.json +41 -9
  29. package/.claude/settings.local.json +0 -29
  30. package/AGENTS.md +0 -681
  31. package/CLAUDE.md +0 -681
  32. package/bun.lock +0 -472
  33. package/eslint.config.js +0 -75
  34. package/index.ts +0 -1
  35. package/llms.md +0 -796
  36. package/specs/UAP-1.0.md +0 -2355
  37. package/src/agent/index.ts +0 -384
  38. package/src/agent/types.ts +0 -91
  39. package/src/checkpoint/file.ts +0 -126
  40. package/src/checkpoint/index.ts +0 -40
  41. package/src/checkpoint/types.ts +0 -95
  42. package/src/execution/index.ts +0 -37
  43. package/src/execution/plan.ts +0 -497
  44. package/src/execution/react.ts +0 -340
  45. package/src/execution/tool-ordering.ts +0 -186
  46. package/src/execution/types.ts +0 -315
  47. package/src/index.ts +0 -80
  48. package/src/middleware/index.ts +0 -7
  49. package/src/middleware/logging.ts +0 -123
  50. package/src/middleware/types.ts +0 -69
  51. package/src/state/index.ts +0 -301
  52. package/src/state/types.ts +0 -173
  53. package/src/thread-tree/index.ts +0 -249
  54. package/src/thread-tree/types.ts +0 -29
  55. package/src/utils/uuid.ts +0 -7
  56. package/tests/live/agent-anthropic.test.ts +0 -288
  57. package/tests/live/agent-strategy-hooks.test.ts +0 -268
  58. package/tests/live/checkpoint.test.ts +0 -243
  59. package/tests/live/execution-strategies.test.ts +0 -255
  60. package/tests/live/plan-strategy.test.ts +0 -160
  61. package/tests/live/subagent-events.live.test.ts +0 -249
  62. package/tests/live/thread-tree.test.ts +0 -186
  63. package/tests/unit/agent.test.ts +0 -703
  64. package/tests/unit/checkpoint.test.ts +0 -232
  65. package/tests/unit/execution/equivalence.test.ts +0 -402
  66. package/tests/unit/execution/loop.test.ts +0 -437
  67. package/tests/unit/execution/plan.test.ts +0 -590
  68. package/tests/unit/execution/react.test.ts +0 -604
  69. package/tests/unit/execution/subagent-events.test.ts +0 -235
  70. package/tests/unit/execution/tool-ordering.test.ts +0 -310
  71. package/tests/unit/middleware/logging.test.ts +0 -276
  72. package/tests/unit/state.test.ts +0 -573
  73. package/tests/unit/thread-tree.test.ts +0 -249
  74. package/tsconfig.json +0 -29
@@ -1,243 +0,0 @@
1
- import { describe, test, expect, setDefaultTimeout, beforeEach, afterEach } from 'bun:test';
2
- import { rm } from 'node:fs/promises';
3
- import { anthropic } from '@providerprotocol/ai/anthropic';
4
- import type { Tool } from '@providerprotocol/ai';
5
- import { agent, AgentState } from '../../src/index.ts';
6
- import { fileCheckpoints } from '../../src/checkpoint/index.ts';
7
- import { loop } from '../../src/execution/index.ts';
8
-
9
- // Skip tests if no API key
10
- const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
11
-
12
- // Increase timeout for live API tests (60 seconds for multi-step tests)
13
- setDefaultTimeout(60_000);
14
-
15
- const TEST_CHECKPOINT_DIR = '.test-live-checkpoints';
16
-
17
- describe.skipIf(!ANTHROPIC_API_KEY)('Checkpointing with Live API', () => {
18
- beforeEach(async () => {
19
- // Clean up test directory before each test
20
- try {
21
- await rm(TEST_CHECKPOINT_DIR, { recursive: true, force: true });
22
- } catch {
23
- // Ignore if doesn't exist
24
- }
25
- });
26
-
27
- afterEach(async () => {
28
- // Clean up test directory after each test
29
- try {
30
- await rm(TEST_CHECKPOINT_DIR, { recursive: true, force: true });
31
- } catch {
32
- // Ignore
33
- }
34
- });
35
-
36
- describe('agent with checkpoints', () => {
37
- test('saves checkpoint after step_end', async () => {
38
- const store = fileCheckpoints({ dir: TEST_CHECKPOINT_DIR });
39
-
40
- const a = agent({
41
- model: anthropic('claude-3-5-haiku-latest'),
42
- params: { max_tokens: 100 },
43
- checkpoints: store,
44
- sessionId: 'test-session-1',
45
- });
46
-
47
- const state = AgentState.initial();
48
- await a.generate('Say hello.', state);
49
-
50
- // Wait a bit for async checkpoint to complete
51
- await new Promise((resolve) => setTimeout(resolve, 100));
52
-
53
- // Verify checkpoint was saved
54
- const sessions = await store.list();
55
- expect(sessions).toContain('test-session-1');
56
-
57
- const saved = await store.load('test-session-1');
58
- expect(saved).not.toBeNull();
59
- expect(saved?.step).toBeGreaterThan(0);
60
- });
61
-
62
- test('auto-generates sessionId when not provided', async () => {
63
- const store = fileCheckpoints({ dir: TEST_CHECKPOINT_DIR });
64
-
65
- const a = agent({
66
- model: anthropic('claude-3-5-haiku-latest'),
67
- params: { max_tokens: 50 },
68
- checkpoints: store,
69
- // sessionId not provided - should auto-generate
70
- });
71
-
72
- await a.generate('Hi', AgentState.initial());
73
-
74
- // Wait for checkpoint
75
- await new Promise((resolve) => setTimeout(resolve, 100));
76
-
77
- const sessions = await store.list();
78
- expect(sessions.length).toBe(1);
79
- // Should be a UUID
80
- expect(sessions[0]).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
81
- });
82
-
83
- test('streaming also saves checkpoints', async () => {
84
- const store = fileCheckpoints({ dir: TEST_CHECKPOINT_DIR });
85
-
86
- const a = agent({
87
- model: anthropic('claude-3-5-haiku-latest'),
88
- params: { max_tokens: 100 },
89
- checkpoints: store,
90
- sessionId: 'streaming-session',
91
- });
92
-
93
- const stream = a.stream('Say hello.', AgentState.initial());
94
-
95
- // Consume the stream
96
- for await (const event of stream) {
97
- // Process events (need to consume them)
98
- void event;
99
- }
100
-
101
- await stream.result;
102
-
103
- // Wait for checkpoint
104
- await new Promise((resolve) => setTimeout(resolve, 100));
105
-
106
- const saved = await store.load('streaming-session');
107
- expect(saved).not.toBeNull();
108
- });
109
- });
110
-
111
- describe('session resume', () => {
112
- test('can resume conversation from checkpoint', async () => {
113
- const store = fileCheckpoints({ dir: TEST_CHECKPOINT_DIR });
114
-
115
- // First agent - establish context
116
- const a1 = agent({
117
- model: anthropic('claude-3-5-haiku-latest'),
118
- params: { max_tokens: 100 },
119
- checkpoints: store,
120
- sessionId: 'resume-test',
121
- });
122
-
123
- // Use ask() to build conversation history properly
124
- const result1 = await a1.ask('My favorite number is 42.', AgentState.initial());
125
-
126
- // The checkpoint from generate() doesn't include ask()'s state enrichment,
127
- // so we manually save the enriched state for realistic resume behavior
128
- await store.save('resume-test', result1.state.toJSON());
129
-
130
- // Verify checkpoint was saved with conversation
131
- const saved = await store.load('resume-test');
132
- expect(saved).not.toBeNull();
133
- expect(saved?.messages.length).toBeGreaterThan(0);
134
-
135
- // Second agent - resume from checkpoint
136
- const a2 = agent({
137
- model: anthropic('claude-3-5-haiku-latest'),
138
- params: { max_tokens: 100 },
139
- checkpoints: store,
140
- sessionId: 'resume-test',
141
- });
142
-
143
- // Restore state from checkpoint (saved is verified non-null above)
144
- const restoredState = AgentState.fromJSON(saved as NonNullable<typeof saved>);
145
-
146
- // Continue conversation
147
- const result2 = await a2.ask('What is my favorite number?', restoredState);
148
-
149
- expect(result2.turn.response.text).toContain('42');
150
- });
151
-
152
- test('checkpoint preserves tool execution results', async () => {
153
- const store = fileCheckpoints({ dir: TEST_CHECKPOINT_DIR });
154
-
155
- const calculator: Tool = {
156
- name: 'calculate',
157
- description: 'Perform a calculation',
158
- parameters: {
159
- type: 'object',
160
- properties: {
161
- expression: { type: 'string', description: 'Math expression' },
162
- },
163
- required: ['expression'],
164
- },
165
- run: async (params: { expression: string }) => {
166
- const result = Function(`"use strict"; return (${params.expression})`)();
167
- return String(result);
168
- },
169
- };
170
-
171
- const a = agent({
172
- model: anthropic('claude-3-5-haiku-latest'),
173
- params: { max_tokens: 200 },
174
- tools: [calculator],
175
- execution: loop(),
176
- checkpoints: store,
177
- sessionId: 'tool-test',
178
- });
179
-
180
- await a.generate(
181
- 'Calculate 7 * 8 using the calculate tool.',
182
- AgentState.initial(),
183
- );
184
-
185
- // Wait for checkpoint
186
- await new Promise((resolve) => setTimeout(resolve, 100));
187
-
188
- // Load and verify checkpoint has the conversation with tool results
189
- const saved = await store.load('tool-test');
190
- expect(saved).not.toBeNull();
191
- expect(saved?.messages.length).toBeGreaterThan(0);
192
-
193
- // Should have tool result messages
194
- const hasToolResult = saved?.messages.some((m) => m.role === 'tool_result');
195
- expect(hasToolResult).toBe(true);
196
- });
197
- });
198
-
199
- describe('checkpoint updates', () => {
200
- test('checkpoints update with each step', async () => {
201
- const store = fileCheckpoints({ dir: TEST_CHECKPOINT_DIR });
202
-
203
- // Tool that requires multiple steps
204
- let callCount = 0;
205
- const countingTool: Tool = {
206
- name: 'count',
207
- description: 'Count calls',
208
- parameters: {
209
- type: 'object',
210
- properties: {},
211
- },
212
- run: async () => {
213
- callCount++;
214
- return `Call count: ${callCount}`;
215
- },
216
- };
217
-
218
- const a = agent({
219
- model: anthropic('claude-3-5-haiku-latest'),
220
- params: { max_tokens: 300 },
221
- tools: [countingTool],
222
- execution: loop(),
223
- checkpoints: store,
224
- sessionId: 'multi-step-test',
225
- });
226
-
227
- await a.generate(
228
- 'Call the count tool twice in separate requests. First call it once, then call it again.',
229
- AgentState.initial(),
230
- );
231
-
232
- // Wait for checkpoints
233
- await new Promise((resolve) => setTimeout(resolve, 200));
234
-
235
- // Load final checkpoint
236
- const saved = await store.load('multi-step-test');
237
- expect(saved).not.toBeNull();
238
-
239
- // Step count should reflect multiple steps
240
- expect(saved?.step).toBeGreaterThanOrEqual(1);
241
- });
242
- });
243
- });
@@ -1,255 +0,0 @@
1
- import { describe, test, expect, setDefaultTimeout } from 'bun:test';
2
- import { anthropic } from '@providerprotocol/ai/anthropic';
3
- import type { Tool } from '@providerprotocol/ai';
4
- import { agent, AgentState } from '../../src/index.ts';
5
- import { react, loop, orderToolCalls } from '../../src/execution/index.ts';
6
- import type { ToolWithDependencies } from '../../src/execution/index.ts';
7
-
8
- // Skip tests if no API key
9
- const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
10
-
11
- // Increase timeout for live API tests (60 seconds)
12
- setDefaultTimeout(60_000);
13
-
14
- describe.skipIf(!ANTHROPIC_API_KEY)('Execution Strategies (Live)', () => {
15
- describe('react() strategy', () => {
16
- test('captures reasoning during execution', async () => {
17
- const a = agent({
18
- model: anthropic('claude-3-5-haiku-latest'),
19
- params: { max_tokens: 400 },
20
- execution: react({ maxSteps: 1 }),
21
- });
22
-
23
- const result = await a.generate(
24
- 'What is 7 multiplied by 8? Think step by step.',
25
- AgentState.initial(),
26
- );
27
-
28
- // ReAct should capture reasoning
29
- expect(result.state.reasoning.length).toBeGreaterThan(0);
30
- // Answer should contain 56
31
- expect(result.turn.response.text).toContain('56');
32
- });
33
-
34
- test('completes with response', async () => {
35
- const a = agent({
36
- model: anthropic('claude-3-5-haiku-latest'),
37
- params: { max_tokens: 400 },
38
- execution: react({ maxSteps: 1 }),
39
- });
40
-
41
- const result = await a.generate(
42
- 'What is the capital of Japan?',
43
- AgentState.initial(),
44
- );
45
-
46
- // Should have reasoning
47
- expect(result.state.reasoning.length).toBeGreaterThan(0);
48
- // Should have a response
49
- expect(result.turn.response.text.toLowerCase()).toContain('tokyo');
50
- });
51
-
52
- test('streams with UAP reasoning events', async () => {
53
- const a = agent({
54
- model: anthropic('claude-3-5-haiku-latest'),
55
- params: { max_tokens: 300 },
56
- execution: react({ maxSteps: 1 }),
57
- });
58
-
59
- const stream = a.stream('What is the capital of France?', AgentState.initial());
60
-
61
- const uapEvents: Array<{ type: string }> = [];
62
-
63
- for await (const event of stream) {
64
- if (event.source === 'uap' && event.uap) {
65
- uapEvents.push({ type: event.uap.type });
66
- }
67
- }
68
-
69
- const result = await stream.result;
70
-
71
- // Should have step_start and reasoning UAP events
72
- expect(uapEvents.some((e) => e.type === 'step_start')).toBe(true);
73
- expect(uapEvents.some((e) => e.type === 'reasoning')).toBe(true);
74
- expect(result.turn.response.text.toLowerCase()).toContain('paris');
75
- });
76
- });
77
-
78
- describe('tool dependency ordering (unit tests)', () => {
79
- // These are pure unit tests for the orderToolCalls utility - no API calls needed
80
- test('orderToolCalls groups independent tools together', () => {
81
- const toolA: Tool = {
82
- name: 'tool_a',
83
- description: 'Tool A',
84
- parameters: { type: 'object', properties: {} },
85
- run: async () => 'a',
86
- };
87
-
88
- const toolB: Tool = {
89
- name: 'tool_b',
90
- description: 'Tool B',
91
- parameters: { type: 'object', properties: {} },
92
- run: async () => 'b',
93
- };
94
-
95
- const calls = [
96
- { toolCallId: 'call-1', toolName: 'tool_a', arguments: {} },
97
- { toolCallId: 'call-2', toolName: 'tool_b', arguments: {} },
98
- ];
99
-
100
- const groups = orderToolCalls(calls, [toolA, toolB]);
101
-
102
- // Should group both in one group (parallel execution)
103
- expect(groups.length).toBe(1);
104
- expect(groups[0]?.calls.length).toBe(2);
105
- expect(groups[0]?.isBarrier).toBe(false);
106
- });
107
-
108
- test('orderToolCalls respects dependsOn ordering', () => {
109
- const toolRead: ToolWithDependencies = {
110
- name: 'read',
111
- description: 'Read',
112
- parameters: { type: 'object', properties: {} },
113
- run: async () => 'read result',
114
- };
115
-
116
- const toolWrite: ToolWithDependencies = {
117
- name: 'write',
118
- description: 'Write',
119
- parameters: { type: 'object', properties: {} },
120
- dependsOn: ['read'],
121
- run: async () => 'write result',
122
- };
123
-
124
- const calls = [
125
- { toolCallId: 'call-read', toolName: 'read', arguments: {} },
126
- { toolCallId: 'call-write', toolName: 'write', arguments: {} },
127
- ];
128
-
129
- const groups = orderToolCalls(calls, [toolRead, toolWrite]);
130
-
131
- // Should have 2 groups - read first, then write
132
- expect(groups.length).toBe(2);
133
-
134
- const readGroupIndex = groups.findIndex(
135
- (g) => g.calls.some((c) => c.toolName === 'read'),
136
- );
137
- const writeGroupIndex = groups.findIndex(
138
- (g) => g.calls.some((c) => c.toolName === 'write'),
139
- );
140
-
141
- expect(readGroupIndex).toBeLessThan(writeGroupIndex);
142
- });
143
-
144
- test('orderToolCalls creates barrier for sequential tools', () => {
145
- const sequentialTool: ToolWithDependencies = {
146
- name: 'sequential_tool',
147
- description: 'Must run alone',
148
- parameters: { type: 'object', properties: {} },
149
- sequential: true,
150
- run: async () => 'done',
151
- };
152
-
153
- const calls = [
154
- { toolCallId: 'call-1', toolName: 'sequential_tool', arguments: {} },
155
- ];
156
-
157
- const groups = orderToolCalls(calls, [sequentialTool]);
158
-
159
- expect(groups.length).toBe(1);
160
- expect(groups[0]?.isBarrier).toBe(true);
161
- });
162
- });
163
-
164
- describe('stream/generate equivalence', () => {
165
- test('stream and generate produce equivalent final states', async () => {
166
- const a = agent({
167
- model: anthropic('claude-3-5-haiku-latest'),
168
- params: { max_tokens: 100 },
169
- execution: loop(),
170
- });
171
-
172
- const input = 'Say exactly: "Test response"';
173
- const state = AgentState.initial();
174
-
175
- // Run with generate
176
- const generateResult = await a.generate(input, state);
177
-
178
- // Run with stream
179
- const stream = a.stream(input, state);
180
- for await (const event of stream) {
181
- void event; // consume stream
182
- }
183
- const streamResult = await stream.result;
184
-
185
- // Compare structural equivalence
186
- expect(generateResult.state.step).toBe(streamResult.state.step);
187
- expect(generateResult.state.messages.length).toBe(streamResult.state.messages.length);
188
- // Both should have non-empty responses
189
- expect(generateResult.turn.response.text.length).toBeGreaterThan(0);
190
- expect(streamResult.turn.response.text.length).toBeGreaterThan(0);
191
- });
192
-
193
- test('react() stream and generate capture same reasoning count', async () => {
194
- const a = agent({
195
- model: anthropic('claude-3-5-haiku-latest'),
196
- params: { max_tokens: 300 },
197
- execution: react({ maxSteps: 1 }),
198
- });
199
-
200
- const input = 'What is 5 + 5?';
201
- const state = AgentState.initial();
202
-
203
- // Run with generate
204
- const generateResult = await a.generate(input, state);
205
-
206
- // Run with stream
207
- const stream = a.stream(input, state);
208
- for await (const event of stream) {
209
- void event; // consume stream
210
- }
211
- const streamResult = await stream.result;
212
-
213
- // Both should have captured reasoning
214
- expect(generateResult.state.reasoning.length).toBe(streamResult.state.reasoning.length);
215
- expect(generateResult.state.reasoning.length).toBeGreaterThan(0);
216
- });
217
- });
218
-
219
- describe('react() with tools', () => {
220
- test('uses tools in loop and answers correctly', async () => {
221
- const calculator: Tool = {
222
- name: 'calculate',
223
- description: 'Perform a mathematical calculation. Use this for any math operations.',
224
- parameters: {
225
- type: 'object',
226
- properties: {
227
- expression: { type: 'string', description: 'Math expression to evaluate, e.g., "2 + 3"' },
228
- },
229
- required: ['expression'],
230
- },
231
- run: async (params: { expression: string }) => {
232
- const result = Function(`"use strict"; return (${params.expression})`)();
233
- return String(result);
234
- },
235
- };
236
-
237
- const a = agent({
238
- model: anthropic('claude-3-5-haiku-latest'),
239
- params: { max_tokens: 500 },
240
- tools: [calculator],
241
- execution: loop(), // Use loop() for simpler tool execution
242
- });
243
-
244
- const result = await a.generate(
245
- 'Use the calculate tool to compute 123 + 456. What is the result?',
246
- AgentState.initial(),
247
- );
248
-
249
- // Should have tool executions
250
- expect(result.turn.toolExecutions.length).toBeGreaterThan(0);
251
- // Answer should contain 579
252
- expect(result.turn.response.text).toContain('579');
253
- });
254
- });
255
- });
@@ -1,160 +0,0 @@
1
- import { describe, test, expect, setDefaultTimeout } from 'bun:test';
2
- import { anthropic } from '@providerprotocol/ai/anthropic';
3
- import type { Tool } from '@providerprotocol/ai';
4
- import { agent, AgentState } from '../../src/index.ts';
5
- import { plan } from '../../src/execution/index.ts';
6
-
7
- // Skip tests if no API key
8
- const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
9
-
10
- // Increase timeout for live API tests (90 seconds for plan strategy)
11
- setDefaultTimeout(90_000);
12
-
13
- describe.skipIf(!ANTHROPIC_API_KEY)('Plan Strategy (Live)', () => {
14
- describe('basic planning', () => {
15
- test('generates and executes a plan', async () => {
16
- const a = agent({
17
- model: anthropic('claude-3-5-haiku-latest'),
18
- params: { max_tokens: 800 },
19
- execution: plan({ maxPlanSteps: 3 }),
20
- });
21
-
22
- const result = await a.generate(
23
- 'Think about how you would describe the color blue to someone. Create a simple 2-step plan.',
24
- AgentState.initial(),
25
- );
26
-
27
- // Should have generated a plan
28
- expect(result.state.plan).toBeDefined();
29
- expect(result.state.plan?.length).toBeGreaterThan(0);
30
- // Should have a text response
31
- expect(result.turn.response.text.length).toBeGreaterThan(0);
32
- });
33
-
34
- test('tracks plan step status', async () => {
35
- const a = agent({
36
- model: anthropic('claude-3-5-haiku-latest'),
37
- params: { max_tokens: 600 },
38
- execution: plan({ maxPlanSteps: 2 }),
39
- });
40
-
41
- const result = await a.generate(
42
- 'Create a 1-step plan to say hello.',
43
- AgentState.initial(),
44
- );
45
-
46
- // Plan should have executed
47
- expect(result.state.plan).toBeDefined();
48
- if (result.state.plan && result.state.plan.length > 0) {
49
- // At least one step should have been completed
50
- const completedSteps = result.state.plan.filter((s) => s.status === 'completed');
51
- expect(completedSteps.length).toBeGreaterThanOrEqual(0);
52
- }
53
- });
54
- });
55
-
56
- describe('plan with tools', () => {
57
- test('creates plan that uses tools', async () => {
58
- const notepadContents: string[] = [];
59
-
60
- const notepad: Tool = {
61
- name: 'notepad',
62
- description: 'Write a note to a notepad for later reference. You MUST use this tool to write notes.',
63
- parameters: {
64
- type: 'object',
65
- properties: {
66
- note: { type: 'string', description: 'The note to write' },
67
- },
68
- required: ['note'],
69
- },
70
- run: async (params: { note: string }) => {
71
- notepadContents.push(params.note);
72
- return `Note saved: ${params.note}`;
73
- },
74
- };
75
-
76
- const a = agent({
77
- model: anthropic('claude-3-5-haiku-latest'),
78
- params: { max_tokens: 1200 },
79
- tools: [notepad],
80
- execution: plan({ maxPlanSteps: 2 }),
81
- system: `You are a planning assistant. When asked to create a plan, you MUST respond with a valid JSON object.
82
- Your response MUST be a JSON object with a "steps" array. Each step must have:
83
- - "id": a unique string identifier
84
- - "description": what the step does
85
- - "dependsOn": array of step ids this depends on (empty array if no dependencies)
86
- - "tool": (optional) the tool to use
87
-
88
- Example response format:
89
- {"steps": [{"id": "step1", "description": "Write hello", "dependsOn": [], "tool": "notepad"}]}
90
-
91
- Do NOT include any text before or after the JSON. ONLY output valid JSON.`,
92
- });
93
-
94
- const result = await a.generate(
95
- 'Create a plan with 1 step: write "test" to the notepad. Respond ONLY with JSON.',
96
- AgentState.initial(),
97
- );
98
-
99
- // Plan should be created (even if execution varies)
100
- expect(result.state.plan).toBeDefined();
101
- expect(result.state.plan?.length).toBeGreaterThanOrEqual(1);
102
- });
103
- });
104
-
105
- describe('plan streaming', () => {
106
- test('streams plan creation and execution events', async () => {
107
- const a = agent({
108
- model: anthropic('claude-3-5-haiku-latest'),
109
- params: { max_tokens: 600 },
110
- execution: plan({ maxPlanSteps: 2 }),
111
- });
112
-
113
- const stream = a.stream(
114
- 'Create a simple 1-step plan to greet the user.',
115
- AgentState.initial(),
116
- );
117
-
118
- const uapEvents: Array<{ type: string; data?: Record<string, unknown> }> = [];
119
-
120
- for await (const event of stream) {
121
- if (event.source === 'uap' && event.uap) {
122
- uapEvents.push({ type: event.uap.type, data: event.uap.data });
123
- }
124
- }
125
-
126
- const result = await stream.result;
127
-
128
- // Should have step events
129
- expect(uapEvents.some((e) => e.type === 'step_start')).toBe(true);
130
-
131
- // Should have plan_created event
132
- const planCreatedEvent = uapEvents.find((e) => e.type === 'plan_created');
133
- expect(planCreatedEvent).toBeDefined();
134
-
135
- // Final result should have plan
136
- expect(result.state.plan).toBeDefined();
137
- });
138
- });
139
-
140
- describe('plan respects maxPlanSteps', () => {
141
- test('limits plan to maxPlanSteps', async () => {
142
- const a = agent({
143
- model: anthropic('claude-3-5-haiku-latest'),
144
- params: { max_tokens: 800 },
145
- execution: plan({ maxPlanSteps: 2 }),
146
- });
147
-
148
- const result = await a.generate(
149
- 'Create a 10-step plan to count from 1 to 10.',
150
- AgentState.initial(),
151
- );
152
-
153
- // Plan should be limited to maxPlanSteps
154
- expect(result.state.plan).toBeDefined();
155
- if (result.state.plan) {
156
- expect(result.state.plan.length).toBeLessThanOrEqual(2);
157
- }
158
- });
159
- });
160
- });