@providerprotocol/agents 0.0.2 → 0.0.3

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 (73) hide show
  1. package/dist/checkpoint/index.d.ts +43 -0
  2. package/dist/checkpoint/index.js +64 -0
  3. package/dist/checkpoint/index.js.map +1 -0
  4. package/{src/execution/loop.ts → dist/chunk-4ESYN66B.js} +54 -162
  5. package/dist/chunk-4ESYN66B.js.map +1 -0
  6. package/dist/chunk-EKRXMSDX.js +8 -0
  7. package/dist/chunk-EKRXMSDX.js.map +1 -0
  8. package/dist/chunk-PHI5ULBV.js +427 -0
  9. package/dist/chunk-PHI5ULBV.js.map +1 -0
  10. package/dist/execution/index.d.ts +105 -0
  11. package/dist/execution/index.js +679 -0
  12. package/dist/execution/index.js.map +1 -0
  13. package/dist/index-qsPwbY86.d.ts +65 -0
  14. package/dist/index.d.ts +101 -0
  15. package/dist/index.js +218 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/middleware/index.d.ts +23 -0
  18. package/dist/middleware/index.js +82 -0
  19. package/dist/middleware/index.js.map +1 -0
  20. package/dist/thread-tree/index.d.ts +115 -0
  21. package/dist/thread-tree/index.js +4 -0
  22. package/dist/thread-tree/index.js.map +1 -0
  23. package/dist/types-2Vsthzyu.d.ts +163 -0
  24. package/dist/types-BhX9uD_d.d.ts +91 -0
  25. package/dist/types-DR02gtFv.d.ts +270 -0
  26. package/dist/types-NGQMdnaD.d.ts +65 -0
  27. package/package.json +40 -8
  28. package/.claude/settings.local.json +0 -29
  29. package/AGENTS.md +0 -681
  30. package/CLAUDE.md +0 -681
  31. package/bun.lock +0 -472
  32. package/eslint.config.js +0 -75
  33. package/index.ts +0 -1
  34. package/llms.md +0 -796
  35. package/specs/UAP-1.0.md +0 -2355
  36. package/src/agent/index.ts +0 -384
  37. package/src/agent/types.ts +0 -91
  38. package/src/checkpoint/file.ts +0 -126
  39. package/src/checkpoint/index.ts +0 -40
  40. package/src/checkpoint/types.ts +0 -95
  41. package/src/execution/index.ts +0 -37
  42. package/src/execution/plan.ts +0 -497
  43. package/src/execution/react.ts +0 -340
  44. package/src/execution/tool-ordering.ts +0 -186
  45. package/src/execution/types.ts +0 -315
  46. package/src/index.ts +0 -80
  47. package/src/middleware/index.ts +0 -7
  48. package/src/middleware/logging.ts +0 -123
  49. package/src/middleware/types.ts +0 -69
  50. package/src/state/index.ts +0 -301
  51. package/src/state/types.ts +0 -173
  52. package/src/thread-tree/index.ts +0 -249
  53. package/src/thread-tree/types.ts +0 -29
  54. package/src/utils/uuid.ts +0 -7
  55. package/tests/live/agent-anthropic.test.ts +0 -288
  56. package/tests/live/agent-strategy-hooks.test.ts +0 -268
  57. package/tests/live/checkpoint.test.ts +0 -243
  58. package/tests/live/execution-strategies.test.ts +0 -255
  59. package/tests/live/plan-strategy.test.ts +0 -160
  60. package/tests/live/subagent-events.live.test.ts +0 -249
  61. package/tests/live/thread-tree.test.ts +0 -186
  62. package/tests/unit/agent.test.ts +0 -703
  63. package/tests/unit/checkpoint.test.ts +0 -232
  64. package/tests/unit/execution/equivalence.test.ts +0 -402
  65. package/tests/unit/execution/loop.test.ts +0 -437
  66. package/tests/unit/execution/plan.test.ts +0 -590
  67. package/tests/unit/execution/react.test.ts +0 -604
  68. package/tests/unit/execution/subagent-events.test.ts +0 -235
  69. package/tests/unit/execution/tool-ordering.test.ts +0 -310
  70. package/tests/unit/middleware/logging.test.ts +0 -276
  71. package/tests/unit/state.test.ts +0 -573
  72. package/tests/unit/thread-tree.test.ts +0 -249
  73. 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
- });