@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.
- package/LICENSE +21 -0
- package/dist/checkpoint/index.d.ts +43 -0
- package/dist/checkpoint/index.js +73 -0
- package/dist/checkpoint/index.js.map +1 -0
- package/{src/execution/loop.ts → dist/chunk-4ESYN66B.js} +54 -162
- package/dist/chunk-4ESYN66B.js.map +1 -0
- package/dist/chunk-EKRXMSDX.js +8 -0
- package/dist/chunk-EKRXMSDX.js.map +1 -0
- package/dist/chunk-T47B3VAF.js +427 -0
- package/dist/chunk-T47B3VAF.js.map +1 -0
- package/dist/execution/index.d.ts +105 -0
- package/dist/execution/index.js +679 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/index-qsPwbY86.d.ts +65 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +218 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.d.ts +23 -0
- package/dist/middleware/index.js +82 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/thread-tree/index.d.ts +115 -0
- package/dist/thread-tree/index.js +4 -0
- package/dist/thread-tree/index.js.map +1 -0
- package/dist/types-2Vsthzyu.d.ts +163 -0
- package/dist/types-BiyEVOnf.d.ts +65 -0
- package/dist/types-D1egxttz.d.ts +270 -0
- package/dist/types-DChRdQoX.d.ts +98 -0
- package/package.json +41 -9
- package/.claude/settings.local.json +0 -29
- package/AGENTS.md +0 -681
- package/CLAUDE.md +0 -681
- package/bun.lock +0 -472
- package/eslint.config.js +0 -75
- package/index.ts +0 -1
- package/llms.md +0 -796
- package/specs/UAP-1.0.md +0 -2355
- package/src/agent/index.ts +0 -384
- package/src/agent/types.ts +0 -91
- package/src/checkpoint/file.ts +0 -126
- package/src/checkpoint/index.ts +0 -40
- package/src/checkpoint/types.ts +0 -95
- package/src/execution/index.ts +0 -37
- package/src/execution/plan.ts +0 -497
- package/src/execution/react.ts +0 -340
- package/src/execution/tool-ordering.ts +0 -186
- package/src/execution/types.ts +0 -315
- package/src/index.ts +0 -80
- package/src/middleware/index.ts +0 -7
- package/src/middleware/logging.ts +0 -123
- package/src/middleware/types.ts +0 -69
- package/src/state/index.ts +0 -301
- package/src/state/types.ts +0 -173
- package/src/thread-tree/index.ts +0 -249
- package/src/thread-tree/types.ts +0 -29
- package/src/utils/uuid.ts +0 -7
- package/tests/live/agent-anthropic.test.ts +0 -288
- package/tests/live/agent-strategy-hooks.test.ts +0 -268
- package/tests/live/checkpoint.test.ts +0 -243
- package/tests/live/execution-strategies.test.ts +0 -255
- package/tests/live/plan-strategy.test.ts +0 -160
- package/tests/live/subagent-events.live.test.ts +0 -249
- package/tests/live/thread-tree.test.ts +0 -186
- package/tests/unit/agent.test.ts +0 -703
- package/tests/unit/checkpoint.test.ts +0 -232
- package/tests/unit/execution/equivalence.test.ts +0 -402
- package/tests/unit/execution/loop.test.ts +0 -437
- package/tests/unit/execution/plan.test.ts +0 -590
- package/tests/unit/execution/react.test.ts +0 -604
- package/tests/unit/execution/subagent-events.test.ts +0 -235
- package/tests/unit/execution/tool-ordering.test.ts +0 -310
- package/tests/unit/middleware/logging.test.ts +0 -276
- package/tests/unit/state.test.ts +0 -573
- package/tests/unit/thread-tree.test.ts +0 -249
- 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
|
-
});
|