@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.
- package/.claude/settings.local.json +27 -0
- package/AGENTS.md +681 -0
- package/CLAUDE.md +681 -0
- package/README.md +15 -0
- package/bun.lock +472 -0
- package/eslint.config.js +75 -0
- package/index.ts +1 -0
- package/llms.md +796 -0
- package/package.json +37 -0
- package/specs/UAP-1.0.md +2355 -0
- package/src/agent/index.ts +384 -0
- package/src/agent/types.ts +91 -0
- package/src/checkpoint/file.ts +126 -0
- package/src/checkpoint/index.ts +40 -0
- package/src/checkpoint/types.ts +95 -0
- package/src/execution/index.ts +37 -0
- package/src/execution/loop.ts +310 -0
- package/src/execution/plan.ts +497 -0
- package/src/execution/react.ts +340 -0
- package/src/execution/tool-ordering.ts +186 -0
- package/src/execution/types.ts +315 -0
- package/src/index.ts +80 -0
- package/src/middleware/index.ts +7 -0
- package/src/middleware/logging.ts +123 -0
- package/src/middleware/types.ts +69 -0
- package/src/state/index.ts +301 -0
- package/src/state/types.ts +173 -0
- package/src/thread-tree/index.ts +249 -0
- package/src/thread-tree/types.ts +29 -0
- package/src/utils/uuid.ts +7 -0
- package/tests/live/agent-anthropic.test.ts +288 -0
- package/tests/live/agent-strategy-hooks.test.ts +268 -0
- package/tests/live/checkpoint.test.ts +243 -0
- package/tests/live/execution-strategies.test.ts +255 -0
- package/tests/live/plan-strategy.test.ts +160 -0
- package/tests/live/subagent-events.live.test.ts +249 -0
- package/tests/live/thread-tree.test.ts +186 -0
- package/tests/unit/agent.test.ts +703 -0
- package/tests/unit/checkpoint.test.ts +232 -0
- package/tests/unit/execution/equivalence.test.ts +402 -0
- package/tests/unit/execution/loop.test.ts +437 -0
- package/tests/unit/execution/plan.test.ts +590 -0
- package/tests/unit/execution/react.test.ts +604 -0
- package/tests/unit/execution/subagent-events.test.ts +235 -0
- package/tests/unit/execution/tool-ordering.test.ts +310 -0
- package/tests/unit/middleware/logging.test.ts +276 -0
- package/tests/unit/state.test.ts +573 -0
- package/tests/unit/thread-tree.test.ts +249 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { rm, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { UserMessage, AssistantMessage } from '@providerprotocol/ai';
|
|
5
|
+
import { fileCheckpoints } from '../../src/checkpoint/file.ts';
|
|
6
|
+
import { AgentState } from '../../src/state/index.ts';
|
|
7
|
+
import type { CheckpointStore } from '../../src/checkpoint/types.ts';
|
|
8
|
+
|
|
9
|
+
const TEST_DIR = '.test-checkpoints';
|
|
10
|
+
|
|
11
|
+
describe('fileCheckpoints()', () => {
|
|
12
|
+
let store: CheckpointStore;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
// Clean up test directory before each test
|
|
16
|
+
try {
|
|
17
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
18
|
+
} catch {
|
|
19
|
+
// Ignore if doesn't exist
|
|
20
|
+
}
|
|
21
|
+
store = fileCheckpoints({ dir: TEST_DIR });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
// Clean up test directory after each test
|
|
26
|
+
try {
|
|
27
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
28
|
+
} catch {
|
|
29
|
+
// Ignore
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('save()', () => {
|
|
34
|
+
test('saves state to disk', async () => {
|
|
35
|
+
const state = AgentState.initial()
|
|
36
|
+
.withMessage(new UserMessage('Hello'))
|
|
37
|
+
.withStep(1);
|
|
38
|
+
|
|
39
|
+
await store.save('session-1', state.toJSON());
|
|
40
|
+
|
|
41
|
+
// Verify file exists
|
|
42
|
+
const file = Bun.file(join(TEST_DIR, 'session-1', 'checkpoint.json'));
|
|
43
|
+
expect(await file.exists()).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('saves metadata alongside state', async () => {
|
|
47
|
+
const state = AgentState.initial().withStep(5);
|
|
48
|
+
|
|
49
|
+
await store.save('session-1', state.toJSON());
|
|
50
|
+
|
|
51
|
+
// Verify metadata file exists
|
|
52
|
+
const file = Bun.file(join(TEST_DIR, 'session-1', 'metadata.json'));
|
|
53
|
+
expect(await file.exists()).toBe(true);
|
|
54
|
+
|
|
55
|
+
const metadata = await file.json();
|
|
56
|
+
expect(metadata.sessionId).toBe('session-1');
|
|
57
|
+
expect(metadata.step).toBe(5);
|
|
58
|
+
expect(metadata.timestamp).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('creates directory if it does not exist', async () => {
|
|
62
|
+
const state = AgentState.initial();
|
|
63
|
+
|
|
64
|
+
await store.save('new-session', state.toJSON());
|
|
65
|
+
|
|
66
|
+
const loaded = await store.load('new-session');
|
|
67
|
+
expect(loaded).not.toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('overwrites existing checkpoint', async () => {
|
|
71
|
+
const state1 = AgentState.initial().withStep(1);
|
|
72
|
+
const state2 = AgentState.initial().withStep(2);
|
|
73
|
+
|
|
74
|
+
await store.save('session-1', state1.toJSON());
|
|
75
|
+
await store.save('session-1', state2.toJSON());
|
|
76
|
+
|
|
77
|
+
const loaded = await store.load('session-1');
|
|
78
|
+
expect(loaded?.step).toBe(2);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('load()', () => {
|
|
83
|
+
test('loads saved state', async () => {
|
|
84
|
+
const original = AgentState.initial()
|
|
85
|
+
.withMessage(new UserMessage('Hello'))
|
|
86
|
+
.withMessage(new AssistantMessage('Hi there!'))
|
|
87
|
+
.withStep(3)
|
|
88
|
+
.withMetadata('key', 'value');
|
|
89
|
+
|
|
90
|
+
await store.save('session-1', original.toJSON());
|
|
91
|
+
|
|
92
|
+
const loaded = await store.load('session-1');
|
|
93
|
+
|
|
94
|
+
expect(loaded).not.toBeNull();
|
|
95
|
+
expect(loaded?.id).toBe(original.id);
|
|
96
|
+
expect(loaded?.step).toBe(3);
|
|
97
|
+
expect(loaded?.metadata).toEqual({ key: 'value' });
|
|
98
|
+
expect(loaded?.messages).toHaveLength(2);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('returns null for non-existent session', async () => {
|
|
102
|
+
const loaded = await store.load('non-existent');
|
|
103
|
+
expect(loaded).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('returns null for corrupt file', async () => {
|
|
107
|
+
// Create corrupt checkpoint file
|
|
108
|
+
const sessionDir = join(TEST_DIR, 'corrupt-session');
|
|
109
|
+
await mkdir(sessionDir, { recursive: true });
|
|
110
|
+
await Bun.write(join(sessionDir, 'checkpoint.json'), 'not valid json');
|
|
111
|
+
|
|
112
|
+
const loaded = await store.load('corrupt-session');
|
|
113
|
+
expect(loaded).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('delete()', () => {
|
|
118
|
+
test('removes session directory', async () => {
|
|
119
|
+
const state = AgentState.initial();
|
|
120
|
+
await store.save('session-to-delete', state.toJSON());
|
|
121
|
+
|
|
122
|
+
// Verify exists
|
|
123
|
+
expect(await store.load('session-to-delete')).not.toBeNull();
|
|
124
|
+
|
|
125
|
+
await store.delete('session-to-delete');
|
|
126
|
+
|
|
127
|
+
// Verify deleted
|
|
128
|
+
expect(await store.load('session-to-delete')).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('does not throw for non-existent session', async () => {
|
|
132
|
+
// Should not throw
|
|
133
|
+
await store.delete('non-existent-session');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('list()', () => {
|
|
138
|
+
test('returns empty array when no sessions', async () => {
|
|
139
|
+
const sessions = await store.list();
|
|
140
|
+
expect(sessions).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('returns all session IDs', async () => {
|
|
144
|
+
const state = AgentState.initial();
|
|
145
|
+
|
|
146
|
+
await store.save('session-a', state.toJSON());
|
|
147
|
+
await store.save('session-b', state.toJSON());
|
|
148
|
+
await store.save('session-c', state.toJSON());
|
|
149
|
+
|
|
150
|
+
const sessions = await store.list();
|
|
151
|
+
|
|
152
|
+
expect(sessions).toHaveLength(3);
|
|
153
|
+
expect(sessions).toContain('session-a');
|
|
154
|
+
expect(sessions).toContain('session-b');
|
|
155
|
+
expect(sessions).toContain('session-c');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('does not include deleted sessions', async () => {
|
|
159
|
+
const state = AgentState.initial();
|
|
160
|
+
|
|
161
|
+
await store.save('keep', state.toJSON());
|
|
162
|
+
await store.save('delete-me', state.toJSON());
|
|
163
|
+
await store.delete('delete-me');
|
|
164
|
+
|
|
165
|
+
const sessions = await store.list();
|
|
166
|
+
|
|
167
|
+
expect(sessions).toEqual(['keep']);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('round-trip', () => {
|
|
172
|
+
test('preserves full state through save/load cycle', async () => {
|
|
173
|
+
const original = AgentState.initial()
|
|
174
|
+
.withMessage(new UserMessage('User message'))
|
|
175
|
+
.withMessage(new AssistantMessage('Assistant response'))
|
|
176
|
+
.withStep(10)
|
|
177
|
+
.withMetadata('count', 42)
|
|
178
|
+
.withMetadata('enabled', true)
|
|
179
|
+
.withReasoning('First reasoning')
|
|
180
|
+
.withReasoning('Second reasoning')
|
|
181
|
+
.withPlan([
|
|
182
|
+
{ id: '1', description: 'Step 1', dependsOn: [], status: 'completed' },
|
|
183
|
+
{ id: '2', description: 'Step 2', dependsOn: ['1'], status: 'pending' },
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
await store.save('full-state', original.toJSON());
|
|
187
|
+
const loaded = await store.load('full-state');
|
|
188
|
+
|
|
189
|
+
expect(loaded).not.toBeNull();
|
|
190
|
+
|
|
191
|
+
// Verify can restore to AgentState (loaded is verified non-null above)
|
|
192
|
+
const restored = AgentState.fromJSON(loaded as NonNullable<typeof loaded>);
|
|
193
|
+
|
|
194
|
+
expect(restored.id).toBe(original.id);
|
|
195
|
+
expect(restored.step).toBe(original.step);
|
|
196
|
+
expect(restored.messages.length).toBe(original.messages.length);
|
|
197
|
+
expect(restored.metadata).toEqual(original.metadata);
|
|
198
|
+
expect([...restored.reasoning]).toEqual([...original.reasoning]);
|
|
199
|
+
expect(restored.plan?.length).toBe(original.plan?.length);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('CheckpointStore interface', () => {
|
|
205
|
+
test('default directory is .checkpoints', () => {
|
|
206
|
+
// This is a documentation/contract test
|
|
207
|
+
const store = fileCheckpoints();
|
|
208
|
+
// We can't easily verify the directory without saving,
|
|
209
|
+
// but we can verify the store is created
|
|
210
|
+
expect(store).toBeDefined();
|
|
211
|
+
expect(store.save).toBeDefined();
|
|
212
|
+
expect(store.load).toBeDefined();
|
|
213
|
+
expect(store.delete).toBeDefined();
|
|
214
|
+
expect(store.list).toBeDefined();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('custom directory is respected', async () => {
|
|
218
|
+
const customDir = '.custom-checkpoint-dir';
|
|
219
|
+
const store = fileCheckpoints({ dir: customDir });
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const state = AgentState.initial();
|
|
223
|
+
await store.save('test', state.toJSON());
|
|
224
|
+
|
|
225
|
+
// Verify file is in custom directory
|
|
226
|
+
const file = Bun.file(join(customDir, 'test', 'checkpoint.json'));
|
|
227
|
+
expect(await file.exists()).toBe(true);
|
|
228
|
+
} finally {
|
|
229
|
+
await rm(customDir, { recursive: true, force: true });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream/Generate Equivalence Tests
|
|
3
|
+
*
|
|
4
|
+
* UAP-1.0 Spec Section 11.4 states:
|
|
5
|
+
* > The `state` returned by `stream.result` MUST include the complete execution history...
|
|
6
|
+
* > The returned state MUST be identical to what `generate()` would return for the same execution.
|
|
7
|
+
*
|
|
8
|
+
* These tests verify that stream() and generate() produce structurally equivalent final states.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, test, expect } from 'bun:test';
|
|
11
|
+
import { UserMessage, AssistantMessage } from '@providerprotocol/ai';
|
|
12
|
+
import type { Turn, LLMInstance, ToolCall, ToolExecution } from '@providerprotocol/ai';
|
|
13
|
+
import { loop } from '../../../src/execution/loop.ts';
|
|
14
|
+
import { react } from '../../../src/execution/react.ts';
|
|
15
|
+
import { AgentState } from '../../../src/state/index.ts';
|
|
16
|
+
import type { ExecutionContext } from '../../../src/execution/types.ts';
|
|
17
|
+
|
|
18
|
+
// Deterministic mock Turn factory
|
|
19
|
+
function createMockTurn(options: {
|
|
20
|
+
text?: string;
|
|
21
|
+
toolCalls?: ToolCall[];
|
|
22
|
+
toolExecutions?: ToolExecution[];
|
|
23
|
+
}): Turn {
|
|
24
|
+
const response = new AssistantMessage(options.text ?? 'Response', options.toolCalls);
|
|
25
|
+
return {
|
|
26
|
+
response,
|
|
27
|
+
messages: [response],
|
|
28
|
+
toolExecutions: options.toolExecutions ?? [],
|
|
29
|
+
usage: {
|
|
30
|
+
inputTokens: 10,
|
|
31
|
+
outputTokens: 20,
|
|
32
|
+
totalTokens: 30,
|
|
33
|
+
},
|
|
34
|
+
cycles: 1,
|
|
35
|
+
} as unknown as Turn;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Mock LLM that returns consistent results for both generate and stream
|
|
39
|
+
function createDeterministicLLM(turns: Turn[]): () => LLMInstance {
|
|
40
|
+
return () => {
|
|
41
|
+
let callIndex = 0;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
generate: async () => {
|
|
45
|
+
const turn = turns[callIndex] ?? turns[turns.length - 1];
|
|
46
|
+
callIndex++;
|
|
47
|
+
return turn;
|
|
48
|
+
},
|
|
49
|
+
stream: () => {
|
|
50
|
+
const turn = turns[callIndex] ?? turns[turns.length - 1];
|
|
51
|
+
callIndex++;
|
|
52
|
+
|
|
53
|
+
const events: Array<{ type: string; delta?: { text?: string } }> = [];
|
|
54
|
+
if (turn) {
|
|
55
|
+
events.push({ type: 'text_delta', delta: { text: turn.response.text } });
|
|
56
|
+
events.push({ type: 'message_stop' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
async *[Symbol.asyncIterator]() {
|
|
61
|
+
for (const event of events) {
|
|
62
|
+
yield event;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
turn: Promise.resolve(turn),
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
} as unknown as LLMInstance;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compare two states for structural equivalence.
|
|
74
|
+
* We don't compare IDs since those are unique per state instance.
|
|
75
|
+
*/
|
|
76
|
+
function statesAreEquivalent(a: AgentState, b: AgentState): boolean {
|
|
77
|
+
// Compare step
|
|
78
|
+
if (a.step !== b.step) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Compare message count
|
|
83
|
+
if (a.messages.length !== b.messages.length) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Compare reasoning count
|
|
88
|
+
if (a.reasoning.length !== b.reasoning.length) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Compare reasoning content
|
|
93
|
+
for (let i = 0; i < a.reasoning.length; i++) {
|
|
94
|
+
if (a.reasoning[i] !== b.reasoning[i]) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Compare plan presence
|
|
100
|
+
if ((a.plan === undefined) !== (b.plan === undefined)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Compare plan length if present
|
|
105
|
+
if (a.plan && b.plan && a.plan.length !== b.plan.length) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe('Stream/Generate Equivalence (Section 11.4)', () => {
|
|
113
|
+
describe('loop() strategy', () => {
|
|
114
|
+
test('stream() and generate() produce equivalent states for simple case', async () => {
|
|
115
|
+
const mockTurn = createMockTurn({ text: 'Simple response' });
|
|
116
|
+
const createLLM = createDeterministicLLM([mockTurn]);
|
|
117
|
+
|
|
118
|
+
const strategy = loop();
|
|
119
|
+
const input = new UserMessage('Hello');
|
|
120
|
+
const initialState = AgentState.initial();
|
|
121
|
+
|
|
122
|
+
// Execute with generate()
|
|
123
|
+
const generateContext: ExecutionContext = {
|
|
124
|
+
agent: { id: 'test-agent' },
|
|
125
|
+
llm: createLLM(),
|
|
126
|
+
input,
|
|
127
|
+
state: initialState,
|
|
128
|
+
tools: [],
|
|
129
|
+
strategy: {},
|
|
130
|
+
};
|
|
131
|
+
const generateResult = await strategy.execute(generateContext);
|
|
132
|
+
|
|
133
|
+
// Execute with stream()
|
|
134
|
+
const streamContext: ExecutionContext = {
|
|
135
|
+
agent: { id: 'test-agent' },
|
|
136
|
+
llm: createLLM(),
|
|
137
|
+
input,
|
|
138
|
+
state: initialState,
|
|
139
|
+
tools: [],
|
|
140
|
+
strategy: {},
|
|
141
|
+
};
|
|
142
|
+
const streamResult = strategy.stream(streamContext);
|
|
143
|
+
|
|
144
|
+
// Consume the stream
|
|
145
|
+
for await (const event of streamResult) {
|
|
146
|
+
// Consume events (required to complete stream)
|
|
147
|
+
void event;
|
|
148
|
+
}
|
|
149
|
+
const streamFinalResult = await streamResult.result;
|
|
150
|
+
|
|
151
|
+
// Compare states
|
|
152
|
+
expect(statesAreEquivalent(generateResult.state, streamFinalResult.state)).toBe(true);
|
|
153
|
+
expect(generateResult.state.step).toBe(streamFinalResult.state.step);
|
|
154
|
+
expect(generateResult.state.messages.length).toBe(streamFinalResult.state.messages.length);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('stream() and generate() produce equivalent states with tool calls', async () => {
|
|
158
|
+
const toolCall: ToolCall = {
|
|
159
|
+
toolCallId: 'call-1',
|
|
160
|
+
toolName: 'test_tool',
|
|
161
|
+
arguments: {},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const turn1 = createMockTurn({
|
|
165
|
+
text: 'Using tool',
|
|
166
|
+
toolCalls: [toolCall],
|
|
167
|
+
toolExecutions: [{
|
|
168
|
+
toolCallId: 'call-1',
|
|
169
|
+
toolName: 'test_tool',
|
|
170
|
+
arguments: {},
|
|
171
|
+
result: 'ok',
|
|
172
|
+
duration: 10,
|
|
173
|
+
isError: false,
|
|
174
|
+
}],
|
|
175
|
+
});
|
|
176
|
+
const turn2 = createMockTurn({ text: 'Done' });
|
|
177
|
+
|
|
178
|
+
const createLLM = createDeterministicLLM([turn1, turn2]);
|
|
179
|
+
const strategy = loop();
|
|
180
|
+
const input = new UserMessage('Do something');
|
|
181
|
+
const initialState = AgentState.initial();
|
|
182
|
+
|
|
183
|
+
// Execute with generate()
|
|
184
|
+
const generateContext: ExecutionContext = {
|
|
185
|
+
agent: { id: 'test-agent' },
|
|
186
|
+
llm: createLLM(),
|
|
187
|
+
input,
|
|
188
|
+
state: initialState,
|
|
189
|
+
tools: [],
|
|
190
|
+
strategy: {},
|
|
191
|
+
};
|
|
192
|
+
const generateResult = await strategy.execute(generateContext);
|
|
193
|
+
|
|
194
|
+
// Execute with stream()
|
|
195
|
+
const streamContext: ExecutionContext = {
|
|
196
|
+
agent: { id: 'test-agent' },
|
|
197
|
+
llm: createLLM(),
|
|
198
|
+
input,
|
|
199
|
+
state: initialState,
|
|
200
|
+
tools: [],
|
|
201
|
+
strategy: {},
|
|
202
|
+
};
|
|
203
|
+
const streamResult = strategy.stream(streamContext);
|
|
204
|
+
|
|
205
|
+
for await (const event of streamResult) {
|
|
206
|
+
void event;
|
|
207
|
+
}
|
|
208
|
+
const streamFinalResult = await streamResult.result;
|
|
209
|
+
|
|
210
|
+
// Compare states
|
|
211
|
+
expect(statesAreEquivalent(generateResult.state, streamFinalResult.state)).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('react() strategy', () => {
|
|
216
|
+
test('stream() and generate() produce equivalent states', async () => {
|
|
217
|
+
const reasoningTurn = createMockTurn({ text: 'Thinking about this...' });
|
|
218
|
+
const actionTurn = createMockTurn({ text: 'Here is my answer.' });
|
|
219
|
+
|
|
220
|
+
const createLLM = createDeterministicLLM([reasoningTurn, actionTurn]);
|
|
221
|
+
const strategy = react();
|
|
222
|
+
const input = new UserMessage('Question');
|
|
223
|
+
const initialState = AgentState.initial();
|
|
224
|
+
|
|
225
|
+
// Execute with generate()
|
|
226
|
+
const generateContext: ExecutionContext = {
|
|
227
|
+
agent: { id: 'test-agent' },
|
|
228
|
+
llm: createLLM(),
|
|
229
|
+
input,
|
|
230
|
+
state: initialState,
|
|
231
|
+
tools: [],
|
|
232
|
+
strategy: {},
|
|
233
|
+
};
|
|
234
|
+
const generateResult = await strategy.execute(generateContext);
|
|
235
|
+
|
|
236
|
+
// Execute with stream()
|
|
237
|
+
const streamContext: ExecutionContext = {
|
|
238
|
+
agent: { id: 'test-agent' },
|
|
239
|
+
llm: createLLM(),
|
|
240
|
+
input,
|
|
241
|
+
state: initialState,
|
|
242
|
+
tools: [],
|
|
243
|
+
strategy: {},
|
|
244
|
+
};
|
|
245
|
+
const streamResult = strategy.stream(streamContext);
|
|
246
|
+
|
|
247
|
+
for await (const event of streamResult) {
|
|
248
|
+
void event;
|
|
249
|
+
}
|
|
250
|
+
const streamFinalResult = await streamResult.result;
|
|
251
|
+
|
|
252
|
+
// Compare states
|
|
253
|
+
expect(statesAreEquivalent(generateResult.state, streamFinalResult.state)).toBe(true);
|
|
254
|
+
|
|
255
|
+
// Reasoning should be captured in both
|
|
256
|
+
expect(generateResult.state.reasoning.length).toBe(streamFinalResult.state.reasoning.length);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('stream() and generate() capture same reasoning content', async () => {
|
|
260
|
+
const reasoningText = 'Step by step reasoning process';
|
|
261
|
+
const reasoningTurn = createMockTurn({ text: reasoningText });
|
|
262
|
+
const actionTurn = createMockTurn({ text: 'Final answer.' });
|
|
263
|
+
|
|
264
|
+
const createLLM = createDeterministicLLM([reasoningTurn, actionTurn]);
|
|
265
|
+
const strategy = react();
|
|
266
|
+
const input = new UserMessage('Think');
|
|
267
|
+
const initialState = AgentState.initial();
|
|
268
|
+
|
|
269
|
+
// Execute with generate()
|
|
270
|
+
const generateContext: ExecutionContext = {
|
|
271
|
+
agent: { id: 'test-agent' },
|
|
272
|
+
llm: createLLM(),
|
|
273
|
+
input,
|
|
274
|
+
state: initialState,
|
|
275
|
+
tools: [],
|
|
276
|
+
strategy: {},
|
|
277
|
+
};
|
|
278
|
+
const generateResult = await strategy.execute(generateContext);
|
|
279
|
+
|
|
280
|
+
// Execute with stream()
|
|
281
|
+
const streamContext: ExecutionContext = {
|
|
282
|
+
agent: { id: 'test-agent' },
|
|
283
|
+
llm: createLLM(),
|
|
284
|
+
input,
|
|
285
|
+
state: initialState,
|
|
286
|
+
tools: [],
|
|
287
|
+
strategy: {},
|
|
288
|
+
};
|
|
289
|
+
const streamResult = strategy.stream(streamContext);
|
|
290
|
+
|
|
291
|
+
for await (const event of streamResult) {
|
|
292
|
+
void event;
|
|
293
|
+
}
|
|
294
|
+
const streamFinalResult = await streamResult.result;
|
|
295
|
+
|
|
296
|
+
// Both should have captured the reasoning
|
|
297
|
+
expect(generateResult.state.reasoning[0]).toBe(reasoningText);
|
|
298
|
+
expect(streamFinalResult.state.reasoning[0]).toBe(reasoningText);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('state completeness', () => {
|
|
303
|
+
test('stream result includes all messages from execution', async () => {
|
|
304
|
+
const toolCall: ToolCall = {
|
|
305
|
+
toolCallId: 'call-1',
|
|
306
|
+
toolName: 'test_tool',
|
|
307
|
+
arguments: {},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const turns = [
|
|
311
|
+
createMockTurn({
|
|
312
|
+
text: 'First',
|
|
313
|
+
toolCalls: [toolCall],
|
|
314
|
+
toolExecutions: [{
|
|
315
|
+
toolCallId: 'call-1',
|
|
316
|
+
toolName: 'test_tool',
|
|
317
|
+
arguments: {},
|
|
318
|
+
result: 'ok',
|
|
319
|
+
duration: 10,
|
|
320
|
+
isError: false,
|
|
321
|
+
}],
|
|
322
|
+
}),
|
|
323
|
+
createMockTurn({ text: 'Second' }),
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
const createLLM = createDeterministicLLM(turns);
|
|
327
|
+
const strategy = loop();
|
|
328
|
+
const input = new UserMessage('Test');
|
|
329
|
+
const initialState = AgentState.initial();
|
|
330
|
+
|
|
331
|
+
const streamContext: ExecutionContext = {
|
|
332
|
+
agent: { id: 'test-agent' },
|
|
333
|
+
llm: createLLM(),
|
|
334
|
+
input,
|
|
335
|
+
state: initialState,
|
|
336
|
+
tools: [],
|
|
337
|
+
strategy: {},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const streamResult = strategy.stream(streamContext);
|
|
341
|
+
|
|
342
|
+
for await (const event of streamResult) {
|
|
343
|
+
void event;
|
|
344
|
+
}
|
|
345
|
+
const result = await streamResult.result;
|
|
346
|
+
|
|
347
|
+
// Should have messages from both iterations
|
|
348
|
+
expect(result.state.messages.length).toBeGreaterThan(1);
|
|
349
|
+
expect(result.state.step).toBe(2); // Two iterations
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('stream result includes correct step count', async () => {
|
|
353
|
+
const reasoningTurn = createMockTurn({ text: 'Reasoning' });
|
|
354
|
+
const actionWithTool = createMockTurn({
|
|
355
|
+
text: 'Action',
|
|
356
|
+
toolCalls: [{
|
|
357
|
+
toolCallId: 'call-1',
|
|
358
|
+
toolName: 'tool',
|
|
359
|
+
arguments: {},
|
|
360
|
+
}],
|
|
361
|
+
toolExecutions: [{
|
|
362
|
+
toolCallId: 'call-1',
|
|
363
|
+
toolName: 'tool',
|
|
364
|
+
arguments: {},
|
|
365
|
+
result: 'ok',
|
|
366
|
+
duration: 10,
|
|
367
|
+
isError: false,
|
|
368
|
+
}],
|
|
369
|
+
});
|
|
370
|
+
const reasoning2 = createMockTurn({ text: 'More reasoning' });
|
|
371
|
+
const finalAction = createMockTurn({ text: 'Done' });
|
|
372
|
+
|
|
373
|
+
const createLLM = createDeterministicLLM([
|
|
374
|
+
reasoningTurn, actionWithTool,
|
|
375
|
+
reasoning2, finalAction,
|
|
376
|
+
]);
|
|
377
|
+
const strategy = react();
|
|
378
|
+
const input = new UserMessage('Multi-step task');
|
|
379
|
+
const initialState = AgentState.initial();
|
|
380
|
+
|
|
381
|
+
const streamContext: ExecutionContext = {
|
|
382
|
+
agent: { id: 'test-agent' },
|
|
383
|
+
llm: createLLM(),
|
|
384
|
+
input,
|
|
385
|
+
state: initialState,
|
|
386
|
+
tools: [],
|
|
387
|
+
strategy: {},
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const streamResult = strategy.stream(streamContext);
|
|
391
|
+
|
|
392
|
+
for await (const event of streamResult) {
|
|
393
|
+
void event;
|
|
394
|
+
}
|
|
395
|
+
const result = await streamResult.result;
|
|
396
|
+
|
|
397
|
+
// Should have completed 2 ReAct steps
|
|
398
|
+
expect(result.state.step).toBe(2);
|
|
399
|
+
expect(result.state.reasoning).toHaveLength(2);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|