@studio-foundation/runner 0.3.0-beta.1 → 0.3.0-beta.6
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/package.json +6 -3
- package/ARCHITECTURE.md +0 -53
- package/configs/agents/analyst.agent.yaml +0 -31
- package/configs/agents/code-generator.agent.yaml +0 -31
- package/configs/agents/generic.agent.yaml +0 -23
- package/src/__tests__/script-executor.test.ts +0 -180
- package/src/index.ts +0 -68
- package/src/integrations/integration-loader.test.ts +0 -88
- package/src/integrations/integration-loader.ts +0 -68
- package/src/middleware/anonymization.ts +0 -38
- package/src/plugins/index.ts +0 -4
- package/src/plugins/mcp-client.test.ts +0 -148
- package/src/plugins/mcp-client.ts +0 -128
- package/src/plugins/oauth-provider.test.ts +0 -167
- package/src/plugins/oauth-provider.ts +0 -175
- package/src/plugins/plugin-loader.test.ts +0 -114
- package/src/plugins/plugin-loader.ts +0 -90
- package/src/prompt-builder.test.ts +0 -167
- package/src/prompt-builder.ts +0 -332
- package/src/providers/anthropic.test.ts +0 -101
- package/src/providers/anthropic.ts +0 -135
- package/src/providers/mock.ts +0 -57
- package/src/providers/ollama.test.ts +0 -166
- package/src/providers/ollama.ts +0 -152
- package/src/providers/openai-responses.ts +0 -212
- package/src/providers/openai.test.ts +0 -67
- package/src/providers/openai.ts +0 -139
- package/src/providers/provider.ts +0 -54
- package/src/providers/registry.ts +0 -77
- package/src/runner.test.ts +0 -343
- package/src/runner.ts +0 -396
- package/src/script-executor.ts +0 -107
- package/src/tools/builtin/git.ts +0 -311
- package/src/tools/builtin/patch.ts +0 -257
- package/src/tools/builtin/repo-manager.ts +0 -142
- package/src/tools/builtin/search.ts +0 -108
- package/src/tools/builtin/shell.ts +0 -82
- package/src/tools/builtin/studio-run.ts +0 -73
- package/src/tools/builtin/web-search.test.ts +0 -122
- package/src/tools/builtin/web-search.ts +0 -101
- package/src/tools/errors.test.ts +0 -12
- package/src/tools/errors.ts +0 -6
- package/src/tools/plugin-loader.test.ts +0 -130
- package/src/tools/plugin-loader.ts +0 -203
- package/src/tools/skills/README.md +0 -49
- package/src/tools/skills/skill-loader.test.ts +0 -106
- package/src/tools/skills/skill-loader.ts +0 -62
- package/src/tools/tool-executor.test.ts +0 -88
- package/src/tools/tool-executor.ts +0 -84
- package/src/tools/tool-registry.ts +0 -130
- package/src/tools/yaml-executor.ts +0 -120
- package/src/utils/race-signal.test.ts +0 -50
- package/src/utils/race-signal.ts +0 -17
- package/templates/integrations/linear.integration.yaml +0 -35
- package/templates/integrations/slack.integration.yaml +0 -22
- package/templates/integrations/webhook.integration.yaml +0 -17
- package/templates/tools/git.tool.yaml +0 -80
- package/templates/tools/repo-manager.tool.yaml +0 -64
- package/templates/tools/search.tool.yaml +0 -22
- package/templates/tools/shell.tool.yaml +0 -19
- package/templates/tools/web-search.tool.yaml +0 -24
- package/tests/anonymization-middleware.test.ts +0 -61
- package/tests/anthropic.test.ts +0 -87
- package/tests/apply-patch.test.ts +0 -355
- package/tests/fixtures/tools/test-builtin.tool.yaml +0 -14
- package/tests/fixtures/tools/test-shell.tool.yaml +0 -19
- package/tests/mock-provider.test.ts +0 -104
- package/tests/openai.test.ts +0 -72
- package/tests/plugin-loader.test.ts +0 -54
- package/tests/prompt-builder.test.ts +0 -468
- package/tests/runner-anonymization.test.ts +0 -89
- package/tests/runner.test.ts +0 -885
- package/tests/studio-run.test.ts +0 -94
- package/tests/tool-executor.test.ts +0 -115
- package/tests/tool-registry.test.ts +0 -84
- package/tests/yaml-executor.test.ts +0 -76
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -7
package/tests/runner.test.ts
DELETED
|
@@ -1,885 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runner tests with mock provider
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { runAgent } from '../src/runner.js';
|
|
7
|
-
import type { Provider } from '../src/providers/provider.js';
|
|
8
|
-
import type { LLMRequest, LLMResponse } from '@studio-foundation/contracts';
|
|
9
|
-
import { ProviderRegistry } from '../src/providers/registry.js';
|
|
10
|
-
import { ToolRegistry } from '../src/tools/tool-registry.js';
|
|
11
|
-
|
|
12
|
-
// Mock provider for testing
|
|
13
|
-
class MockProvider implements Provider {
|
|
14
|
-
readonly name = 'mock';
|
|
15
|
-
private responses: LLMResponse[];
|
|
16
|
-
private currentIndex = 0;
|
|
17
|
-
|
|
18
|
-
constructor(responses: LLMResponse[]) {
|
|
19
|
-
this.responses = responses;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async call(request: LLMRequest, _onToken?: (token: string) => void): Promise<LLMResponse> {
|
|
23
|
-
if (this.currentIndex >= this.responses.length) {
|
|
24
|
-
throw new Error('Mock provider ran out of responses');
|
|
25
|
-
}
|
|
26
|
-
return this.responses[this.currentIndex++];
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Provider that always returns a tool call (infinite loop simulation)
|
|
31
|
-
class InfiniteToolCallProvider implements Provider {
|
|
32
|
-
readonly name = 'mock';
|
|
33
|
-
|
|
34
|
-
async call(_request: LLMRequest, _onToken?: (token: string) => void): Promise<LLMResponse> {
|
|
35
|
-
return {
|
|
36
|
-
content: '',
|
|
37
|
-
tool_calls: [{ id: 'call-1', name: 'infinite_tool', arguments: {} }],
|
|
38
|
-
finish_reason: 'tool_calls',
|
|
39
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
describe('runAgent', () => {
|
|
45
|
-
it('should run a simple agent task without tools', async () => {
|
|
46
|
-
// Setup mock provider with a simple response
|
|
47
|
-
const mockProvider = new MockProvider([
|
|
48
|
-
{
|
|
49
|
-
content: '{"result": "Hello World"}',
|
|
50
|
-
tool_calls: [],
|
|
51
|
-
finish_reason: 'stop',
|
|
52
|
-
usage: {
|
|
53
|
-
prompt_tokens: 10,
|
|
54
|
-
completion_tokens: 5,
|
|
55
|
-
total_tokens: 15
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
const providerRegistry = new ProviderRegistry();
|
|
61
|
-
providerRegistry.register(mockProvider);
|
|
62
|
-
|
|
63
|
-
const toolRegistry = new ToolRegistry();
|
|
64
|
-
|
|
65
|
-
const result = await runAgent({
|
|
66
|
-
agent: {
|
|
67
|
-
name: 'test-agent',
|
|
68
|
-
provider: 'mock',
|
|
69
|
-
model: 'test-model'
|
|
70
|
-
},
|
|
71
|
-
task: {
|
|
72
|
-
description: 'Say hello'
|
|
73
|
-
},
|
|
74
|
-
context: {},
|
|
75
|
-
toolRegistry,
|
|
76
|
-
providerRegistry
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
expect(result.output).toEqual({ result: 'Hello World' });
|
|
80
|
-
expect(result.tool_calls_count).toBe(0);
|
|
81
|
-
expect(result.tool_calls).toEqual([]);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should execute tools in multi-turn conversation', async () => {
|
|
85
|
-
// Setup a tool
|
|
86
|
-
const toolRegistry = new ToolRegistry();
|
|
87
|
-
toolRegistry.register({
|
|
88
|
-
name: 'get_weather',
|
|
89
|
-
description: 'Get weather for a city',
|
|
90
|
-
parameters: {
|
|
91
|
-
type: 'object',
|
|
92
|
-
properties: { city: { type: 'string' } },
|
|
93
|
-
required: ['city'],
|
|
94
|
-
},
|
|
95
|
-
execute: async ({ city }) => ({
|
|
96
|
-
success: true,
|
|
97
|
-
output: { city, temperature: 20, condition: 'sunny' }
|
|
98
|
-
})
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Setup mock provider with tool call then final response
|
|
102
|
-
const mockProvider = new MockProvider([
|
|
103
|
-
// First response: make a tool call
|
|
104
|
-
{
|
|
105
|
-
content: '',
|
|
106
|
-
tool_calls: [
|
|
107
|
-
{
|
|
108
|
-
id: 'call-1',
|
|
109
|
-
name: 'get_weather',
|
|
110
|
-
arguments: { city: 'Paris' }
|
|
111
|
-
}
|
|
112
|
-
],
|
|
113
|
-
finish_reason: 'tool_calls',
|
|
114
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
115
|
-
},
|
|
116
|
-
// Second response: final answer
|
|
117
|
-
{
|
|
118
|
-
content: '{"result": "The weather in Paris is sunny, 20 degrees"}',
|
|
119
|
-
tool_calls: [],
|
|
120
|
-
finish_reason: 'stop',
|
|
121
|
-
usage: { prompt_tokens: 20, completion_tokens: 10, total_tokens: 30 }
|
|
122
|
-
}
|
|
123
|
-
]);
|
|
124
|
-
|
|
125
|
-
const providerRegistry = new ProviderRegistry();
|
|
126
|
-
providerRegistry.register(mockProvider);
|
|
127
|
-
|
|
128
|
-
const result = await runAgent({
|
|
129
|
-
agent: {
|
|
130
|
-
name: 'test-agent',
|
|
131
|
-
provider: 'mock',
|
|
132
|
-
model: 'test-model'
|
|
133
|
-
},
|
|
134
|
-
task: {
|
|
135
|
-
description: 'Get weather for Paris'
|
|
136
|
-
},
|
|
137
|
-
context: {},
|
|
138
|
-
toolRegistry,
|
|
139
|
-
providerRegistry
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
expect(result.tool_calls_count).toBe(1);
|
|
143
|
-
expect(result.tool_calls[0].name).toBe('get_weather');
|
|
144
|
-
expect(result.tool_calls[0].result).toEqual({
|
|
145
|
-
city: 'Paris',
|
|
146
|
-
temperature: 20,
|
|
147
|
-
condition: 'sunny'
|
|
148
|
-
});
|
|
149
|
-
expect(result.output).toEqual({
|
|
150
|
-
result: 'The weather in Paris is sunny, 20 degrees'
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('should track multiple tool calls', async () => {
|
|
155
|
-
const toolRegistry = new ToolRegistry();
|
|
156
|
-
toolRegistry.register({
|
|
157
|
-
name: 'tool_a',
|
|
158
|
-
description: 'Tool A',
|
|
159
|
-
parameters: {},
|
|
160
|
-
execute: async () => ({ success: true, output: 'A result' })
|
|
161
|
-
});
|
|
162
|
-
toolRegistry.register({
|
|
163
|
-
name: 'tool_b',
|
|
164
|
-
description: 'Tool B',
|
|
165
|
-
parameters: {},
|
|
166
|
-
execute: async () => ({ success: true, output: 'B result' })
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const mockProvider = new MockProvider([
|
|
170
|
-
{
|
|
171
|
-
content: '',
|
|
172
|
-
tool_calls: [
|
|
173
|
-
{ id: 'call-1', name: 'tool_a', arguments: {} }
|
|
174
|
-
],
|
|
175
|
-
finish_reason: 'tool_calls',
|
|
176
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
content: '',
|
|
180
|
-
tool_calls: [
|
|
181
|
-
{ id: 'call-2', name: 'tool_b', arguments: {} }
|
|
182
|
-
],
|
|
183
|
-
finish_reason: 'tool_calls',
|
|
184
|
-
usage: { prompt_tokens: 15, completion_tokens: 5, total_tokens: 20 }
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
content: '"Done"',
|
|
188
|
-
tool_calls: [],
|
|
189
|
-
finish_reason: 'stop',
|
|
190
|
-
usage: { prompt_tokens: 20, completion_tokens: 3, total_tokens: 23 }
|
|
191
|
-
}
|
|
192
|
-
]);
|
|
193
|
-
|
|
194
|
-
const providerRegistry = new ProviderRegistry();
|
|
195
|
-
providerRegistry.register(mockProvider);
|
|
196
|
-
|
|
197
|
-
const result = await runAgent({
|
|
198
|
-
agent: {
|
|
199
|
-
name: 'test-agent',
|
|
200
|
-
provider: 'mock',
|
|
201
|
-
model: 'test-model'
|
|
202
|
-
},
|
|
203
|
-
task: {
|
|
204
|
-
description: 'Run tools'
|
|
205
|
-
},
|
|
206
|
-
context: {},
|
|
207
|
-
toolRegistry,
|
|
208
|
-
providerRegistry
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
expect(result.tool_calls_count).toBe(2);
|
|
212
|
-
expect(result.tool_calls[0].name).toBe('tool_a');
|
|
213
|
-
expect(result.tool_calls[1].name).toBe('tool_b');
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('should track token usage from single response', async () => {
|
|
217
|
-
const mockProvider = new MockProvider([
|
|
218
|
-
{
|
|
219
|
-
content: '{"result": "ok"}',
|
|
220
|
-
tool_calls: [],
|
|
221
|
-
finish_reason: 'stop',
|
|
222
|
-
usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 },
|
|
223
|
-
},
|
|
224
|
-
]);
|
|
225
|
-
|
|
226
|
-
const providerRegistry = new ProviderRegistry();
|
|
227
|
-
providerRegistry.register(mockProvider);
|
|
228
|
-
const toolRegistry = new ToolRegistry();
|
|
229
|
-
|
|
230
|
-
const result = await runAgent({
|
|
231
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
232
|
-
task: { description: 'Test tokens' },
|
|
233
|
-
context: {},
|
|
234
|
-
toolRegistry,
|
|
235
|
-
providerRegistry,
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
expect(result.token_usage).toEqual({
|
|
239
|
-
prompt_tokens: 100,
|
|
240
|
-
completion_tokens: 50,
|
|
241
|
-
total_tokens: 150,
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('should return an error result (not throw) at the custom maxToolCalls limit', async () => {
|
|
246
|
-
const toolRegistry = new ToolRegistry();
|
|
247
|
-
toolRegistry.register({
|
|
248
|
-
name: 'infinite_tool',
|
|
249
|
-
description: 'A tool that always runs',
|
|
250
|
-
parameters: {},
|
|
251
|
-
execute: async () => ({ success: true, output: 'running' }),
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const providerRegistry = new ProviderRegistry();
|
|
255
|
-
providerRegistry.register(new InfiniteToolCallProvider());
|
|
256
|
-
|
|
257
|
-
const result = await runAgent({
|
|
258
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
259
|
-
task: { description: 'Run forever' },
|
|
260
|
-
context: {},
|
|
261
|
-
toolRegistry,
|
|
262
|
-
providerRegistry,
|
|
263
|
-
maxToolCalls: 3,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
expect(result.error).toBeDefined();
|
|
267
|
-
expect(result.error).toContain('Maximum tool calling iterations (3) reached');
|
|
268
|
-
expect(result.tool_calls).toHaveLength(3);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('should accumulate token usage across multi-turn tool calls', async () => {
|
|
272
|
-
const toolRegistry = new ToolRegistry();
|
|
273
|
-
toolRegistry.register({
|
|
274
|
-
name: 'tool_a',
|
|
275
|
-
description: 'Tool A',
|
|
276
|
-
parameters: {},
|
|
277
|
-
execute: async () => ({ success: true, output: 'result' }),
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
const mockProvider = new MockProvider([
|
|
281
|
-
{
|
|
282
|
-
content: '',
|
|
283
|
-
tool_calls: [{ id: 'call-1', name: 'tool_a', arguments: {} }],
|
|
284
|
-
finish_reason: 'tool_calls',
|
|
285
|
-
usage: { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120 },
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
content: '"done"',
|
|
289
|
-
tool_calls: [],
|
|
290
|
-
finish_reason: 'stop',
|
|
291
|
-
usage: { prompt_tokens: 150, completion_tokens: 30, total_tokens: 180 },
|
|
292
|
-
},
|
|
293
|
-
]);
|
|
294
|
-
|
|
295
|
-
const providerRegistry = new ProviderRegistry();
|
|
296
|
-
providerRegistry.register(mockProvider);
|
|
297
|
-
|
|
298
|
-
const result = await runAgent({
|
|
299
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
300
|
-
task: { description: 'Test accumulation' },
|
|
301
|
-
context: {},
|
|
302
|
-
toolRegistry,
|
|
303
|
-
providerRegistry,
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
expect(result.token_usage).toEqual({
|
|
307
|
-
prompt_tokens: 250,
|
|
308
|
-
completion_tokens: 50,
|
|
309
|
-
total_tokens: 300,
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
describe('runAgent — callbacks', () => {
|
|
315
|
-
it('calls onToolCallStart and onToolCallComplete for each tool call', async () => {
|
|
316
|
-
const toolRegistry = new ToolRegistry();
|
|
317
|
-
toolRegistry.register({
|
|
318
|
-
name: 'echo_tool',
|
|
319
|
-
description: 'Echoes input',
|
|
320
|
-
parameters: {
|
|
321
|
-
type: 'object',
|
|
322
|
-
properties: { msg: { type: 'string' } },
|
|
323
|
-
},
|
|
324
|
-
execute: async (args) => ({ success: true, output: args }),
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
const mockProvider = new MockProvider([
|
|
328
|
-
{
|
|
329
|
-
content: '',
|
|
330
|
-
tool_calls: [{ id: 'tc-1', name: 'echo_tool', arguments: { msg: 'hello' } }],
|
|
331
|
-
finish_reason: 'tool_calls',
|
|
332
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
content: '"done"',
|
|
336
|
-
tool_calls: [],
|
|
337
|
-
finish_reason: 'stop',
|
|
338
|
-
usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 },
|
|
339
|
-
},
|
|
340
|
-
]);
|
|
341
|
-
|
|
342
|
-
const providerRegistry = new ProviderRegistry();
|
|
343
|
-
providerRegistry.register(mockProvider);
|
|
344
|
-
|
|
345
|
-
const startEvents: import('@studio-foundation/contracts').ToolCallStartEvent[] = [];
|
|
346
|
-
const completeEvents: import('@studio-foundation/contracts').ToolCallCompleteEvent[] = [];
|
|
347
|
-
|
|
348
|
-
await runAgent({
|
|
349
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
350
|
-
task: { description: 'Test callbacks' },
|
|
351
|
-
context: {},
|
|
352
|
-
toolRegistry,
|
|
353
|
-
providerRegistry,
|
|
354
|
-
callbacks: {
|
|
355
|
-
onToolCallStart: (e) => startEvents.push(e),
|
|
356
|
-
onToolCallComplete: (e) => completeEvents.push(e),
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
expect(startEvents).toHaveLength(1);
|
|
361
|
-
expect(startEvents[0].tool).toBe('echo_tool');
|
|
362
|
-
expect(startEvents[0].params).toEqual({ msg: 'hello' });
|
|
363
|
-
expect(startEvents[0].timestamp).toBeTypeOf('number');
|
|
364
|
-
|
|
365
|
-
expect(completeEvents).toHaveLength(1);
|
|
366
|
-
expect(completeEvents[0].tool).toBe('echo_tool');
|
|
367
|
-
expect(completeEvents[0].result).toEqual({ msg: 'hello' });
|
|
368
|
-
expect(completeEvents[0].error).toBeUndefined();
|
|
369
|
-
expect(completeEvents[0].duration_ms).toBeGreaterThanOrEqual(0);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it('includes error in onToolCallComplete when tool fails', async () => {
|
|
373
|
-
const toolRegistry = new ToolRegistry();
|
|
374
|
-
toolRegistry.register({
|
|
375
|
-
name: 'broken_tool',
|
|
376
|
-
description: 'Always fails',
|
|
377
|
-
parameters: {},
|
|
378
|
-
execute: async () => ({ success: false, error: 'something went wrong' }),
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
const mockProvider = new MockProvider([
|
|
382
|
-
{
|
|
383
|
-
content: '',
|
|
384
|
-
tool_calls: [{ id: 'tc-1', name: 'broken_tool', arguments: {} }],
|
|
385
|
-
finish_reason: 'tool_calls',
|
|
386
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
content: '"recovered"',
|
|
390
|
-
tool_calls: [],
|
|
391
|
-
finish_reason: 'stop',
|
|
392
|
-
usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 },
|
|
393
|
-
},
|
|
394
|
-
]);
|
|
395
|
-
|
|
396
|
-
const providerRegistry = new ProviderRegistry();
|
|
397
|
-
providerRegistry.register(mockProvider);
|
|
398
|
-
|
|
399
|
-
const completeEvents: import('@studio-foundation/contracts').ToolCallCompleteEvent[] = [];
|
|
400
|
-
|
|
401
|
-
await runAgent({
|
|
402
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
403
|
-
task: { description: 'Test error callback' },
|
|
404
|
-
context: {},
|
|
405
|
-
toolRegistry,
|
|
406
|
-
providerRegistry,
|
|
407
|
-
callbacks: {
|
|
408
|
-
onToolCallComplete: (e) => completeEvents.push(e),
|
|
409
|
-
},
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
expect(completeEvents[0].error).toBe('something went wrong');
|
|
413
|
-
expect(completeEvents[0].result).toBeUndefined();
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it('calls callbacks in agent-loop provider path', async () => {
|
|
417
|
-
// AgentLoopProvider mock — owns the full loop and calls executeTool
|
|
418
|
-
const agentLoopProvider: import('../src/providers/provider.js').AgentLoopProvider = {
|
|
419
|
-
name: 'mock-loop',
|
|
420
|
-
call: async () => { throw new Error('not used'); },
|
|
421
|
-
runAgentLoop: async (_req, executeTool) => {
|
|
422
|
-
const outcome = await executeTool('loop_tool', { x: 1 }, 'call-loop-1');
|
|
423
|
-
return {
|
|
424
|
-
content: '"loop done"',
|
|
425
|
-
tool_calls: [{ id: 'call-loop-1', name: 'loop_tool', arguments: { x: 1 }, ...outcome }],
|
|
426
|
-
finish_reason: 'stop',
|
|
427
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
428
|
-
};
|
|
429
|
-
},
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
const toolRegistry = new ToolRegistry();
|
|
433
|
-
toolRegistry.register({
|
|
434
|
-
name: 'loop_tool',
|
|
435
|
-
description: 'Loop path tool',
|
|
436
|
-
parameters: {
|
|
437
|
-
type: 'object',
|
|
438
|
-
properties: { x: { type: 'number' } },
|
|
439
|
-
},
|
|
440
|
-
execute: async (args) => ({ success: true, output: args }),
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
const providerRegistry = new ProviderRegistry();
|
|
444
|
-
providerRegistry.register(agentLoopProvider);
|
|
445
|
-
|
|
446
|
-
const startEvents: import('@studio-foundation/contracts').ToolCallStartEvent[] = [];
|
|
447
|
-
const completeEvents: import('@studio-foundation/contracts').ToolCallCompleteEvent[] = [];
|
|
448
|
-
|
|
449
|
-
await runAgent({
|
|
450
|
-
agent: { name: 'test-agent', provider: 'mock-loop', model: 'test-model' },
|
|
451
|
-
task: { description: 'Test loop callbacks' },
|
|
452
|
-
context: {},
|
|
453
|
-
toolRegistry,
|
|
454
|
-
providerRegistry,
|
|
455
|
-
callbacks: {
|
|
456
|
-
onToolCallStart: (e) => startEvents.push(e),
|
|
457
|
-
onToolCallComplete: (e) => completeEvents.push(e),
|
|
458
|
-
},
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
expect(startEvents).toHaveLength(1);
|
|
462
|
-
expect(startEvents[0].tool).toBe('loop_tool');
|
|
463
|
-
expect(startEvents[0].params).toEqual({ x: 1 });
|
|
464
|
-
expect(startEvents[0].timestamp).toBeTypeOf('number');
|
|
465
|
-
expect(completeEvents).toHaveLength(1);
|
|
466
|
-
expect(completeEvents[0].tool).toBe('loop_tool');
|
|
467
|
-
expect(completeEvents[0].result).toEqual({ x: 1 });
|
|
468
|
-
expect(completeEvents[0].error).toBeUndefined();
|
|
469
|
-
expect(completeEvents[0].duration_ms).toBeGreaterThanOrEqual(0);
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
it('emits onAgentThinking when first-turn LLM text accompanies tool calls', async () => {
|
|
473
|
-
const toolRegistry = new ToolRegistry();
|
|
474
|
-
toolRegistry.register({
|
|
475
|
-
name: 'fetch_data',
|
|
476
|
-
description: 'Fetches data',
|
|
477
|
-
parameters: {},
|
|
478
|
-
execute: async () => ({ success: true, output: 'data' }),
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
const mockProvider = new MockProvider([
|
|
482
|
-
{
|
|
483
|
-
content: 'Let me fetch the data for you.',
|
|
484
|
-
tool_calls: [{ id: 'tc-1', name: 'fetch_data', arguments: {} }],
|
|
485
|
-
finish_reason: 'tool_calls',
|
|
486
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
487
|
-
},
|
|
488
|
-
{
|
|
489
|
-
content: '"done"',
|
|
490
|
-
tool_calls: [],
|
|
491
|
-
finish_reason: 'stop',
|
|
492
|
-
usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 },
|
|
493
|
-
},
|
|
494
|
-
]);
|
|
495
|
-
|
|
496
|
-
const providerRegistry = new ProviderRegistry();
|
|
497
|
-
providerRegistry.register(mockProvider);
|
|
498
|
-
|
|
499
|
-
const thinkingEvents: import('@studio-foundation/contracts').AgentThinkingEvent[] = [];
|
|
500
|
-
const progressEvents: import('@studio-foundation/contracts').AgentProgressEvent[] = [];
|
|
501
|
-
|
|
502
|
-
await runAgent({
|
|
503
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
504
|
-
task: { description: 'Fetch data' },
|
|
505
|
-
context: {},
|
|
506
|
-
toolRegistry,
|
|
507
|
-
providerRegistry,
|
|
508
|
-
callbacks: {
|
|
509
|
-
onAgentThinking: (e) => thinkingEvents.push(e),
|
|
510
|
-
onAgentProgress: (e) => progressEvents.push(e),
|
|
511
|
-
},
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
expect(thinkingEvents).toHaveLength(1);
|
|
515
|
-
expect(thinkingEvents[0].thought).toBe('Let me fetch the data for you.');
|
|
516
|
-
expect(thinkingEvents[0].timestamp).toBeTypeOf('number');
|
|
517
|
-
expect(progressEvents).toHaveLength(0);
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
it('emits onAgentProgress (not onAgentThinking) for text in subsequent turns', async () => {
|
|
521
|
-
const toolRegistry = new ToolRegistry();
|
|
522
|
-
toolRegistry.register({
|
|
523
|
-
name: 'step_a',
|
|
524
|
-
description: 'Step A',
|
|
525
|
-
parameters: {},
|
|
526
|
-
execute: async () => ({ success: true, output: 'a' }),
|
|
527
|
-
});
|
|
528
|
-
toolRegistry.register({
|
|
529
|
-
name: 'step_b',
|
|
530
|
-
description: 'Step B',
|
|
531
|
-
parameters: {},
|
|
532
|
-
execute: async () => ({ success: true, output: 'b' }),
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
const mockProvider = new MockProvider([
|
|
536
|
-
// Turn 0: thinking text + first tool call
|
|
537
|
-
{
|
|
538
|
-
content: 'Starting with step A.',
|
|
539
|
-
tool_calls: [{ id: 'tc-1', name: 'step_a', arguments: {} }],
|
|
540
|
-
finish_reason: 'tool_calls',
|
|
541
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
542
|
-
},
|
|
543
|
-
// Turn 1: progress text + second tool call
|
|
544
|
-
{
|
|
545
|
-
content: 'Now moving to step B.',
|
|
546
|
-
tool_calls: [{ id: 'tc-2', name: 'step_b', arguments: {} }],
|
|
547
|
-
finish_reason: 'tool_calls',
|
|
548
|
-
usage: { prompt_tokens: 20, completion_tokens: 8, total_tokens: 28 },
|
|
549
|
-
},
|
|
550
|
-
// Final
|
|
551
|
-
{
|
|
552
|
-
content: '"complete"',
|
|
553
|
-
tool_calls: [],
|
|
554
|
-
finish_reason: 'stop',
|
|
555
|
-
usage: { prompt_tokens: 30, completion_tokens: 5, total_tokens: 35 },
|
|
556
|
-
},
|
|
557
|
-
]);
|
|
558
|
-
|
|
559
|
-
const providerRegistry = new ProviderRegistry();
|
|
560
|
-
providerRegistry.register(mockProvider);
|
|
561
|
-
|
|
562
|
-
const thinkingEvents: import('@studio-foundation/contracts').AgentThinkingEvent[] = [];
|
|
563
|
-
const progressEvents: import('@studio-foundation/contracts').AgentProgressEvent[] = [];
|
|
564
|
-
|
|
565
|
-
await runAgent({
|
|
566
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
567
|
-
task: { description: 'Two-step task' },
|
|
568
|
-
context: {},
|
|
569
|
-
toolRegistry,
|
|
570
|
-
providerRegistry,
|
|
571
|
-
callbacks: {
|
|
572
|
-
onAgentThinking: (e) => thinkingEvents.push(e),
|
|
573
|
-
onAgentProgress: (e) => progressEvents.push(e),
|
|
574
|
-
},
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
expect(thinkingEvents).toHaveLength(1);
|
|
578
|
-
expect(thinkingEvents[0].thought).toBe('Starting with step A.');
|
|
579
|
-
|
|
580
|
-
expect(progressEvents).toHaveLength(1);
|
|
581
|
-
expect(progressEvents[0].message).toBe('Now moving to step B.');
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
it('does not emit thinking/progress when LLM text is empty alongside tool calls', async () => {
|
|
585
|
-
const toolRegistry = new ToolRegistry();
|
|
586
|
-
toolRegistry.register({
|
|
587
|
-
name: 'silent_tool',
|
|
588
|
-
description: 'Silent tool',
|
|
589
|
-
parameters: {},
|
|
590
|
-
execute: async () => ({ success: true, output: 'done' }),
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
const mockProvider = new MockProvider([
|
|
594
|
-
{
|
|
595
|
-
content: '',
|
|
596
|
-
tool_calls: [{ id: 'tc-1', name: 'silent_tool', arguments: {} }],
|
|
597
|
-
finish_reason: 'tool_calls',
|
|
598
|
-
usage: { prompt_tokens: 10, completion_tokens: 2, total_tokens: 12 },
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
content: '"ok"',
|
|
602
|
-
tool_calls: [],
|
|
603
|
-
finish_reason: 'stop',
|
|
604
|
-
usage: { prompt_tokens: 15, completion_tokens: 3, total_tokens: 18 },
|
|
605
|
-
},
|
|
606
|
-
]);
|
|
607
|
-
|
|
608
|
-
const providerRegistry = new ProviderRegistry();
|
|
609
|
-
providerRegistry.register(mockProvider);
|
|
610
|
-
|
|
611
|
-
const thinkingEvents: import('@studio-foundation/contracts').AgentThinkingEvent[] = [];
|
|
612
|
-
|
|
613
|
-
await runAgent({
|
|
614
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
615
|
-
task: { description: 'Silent' },
|
|
616
|
-
context: {},
|
|
617
|
-
toolRegistry,
|
|
618
|
-
providerRegistry,
|
|
619
|
-
callbacks: { onAgentThinking: (e) => thinkingEvents.push(e) },
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
expect(thinkingEvents).toHaveLength(0);
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
it('should propagate onAgentToken when provider emits tokens', async () => {
|
|
626
|
-
const receivedTokens: string[] = [];
|
|
627
|
-
|
|
628
|
-
class TokenStreamingProvider implements Provider {
|
|
629
|
-
readonly name = 'mock';
|
|
630
|
-
async call(_request: LLMRequest, onToken?: (token: string) => void): Promise<LLMResponse> {
|
|
631
|
-
onToken?.('Hello');
|
|
632
|
-
onToken?.(' world');
|
|
633
|
-
return {
|
|
634
|
-
content: '{"result": "streamed"}',
|
|
635
|
-
tool_calls: [],
|
|
636
|
-
finish_reason: 'stop',
|
|
637
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const providerRegistry = new ProviderRegistry();
|
|
643
|
-
providerRegistry.register(new TokenStreamingProvider());
|
|
644
|
-
const toolRegistry = new ToolRegistry();
|
|
645
|
-
|
|
646
|
-
await runAgent({
|
|
647
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
648
|
-
task: { description: 'stream test' },
|
|
649
|
-
context: {},
|
|
650
|
-
toolRegistry,
|
|
651
|
-
providerRegistry,
|
|
652
|
-
callbacks: {
|
|
653
|
-
onAgentToken: (event) => receivedTokens.push(event.token),
|
|
654
|
-
},
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
expect(receivedTokens).toEqual(['Hello', ' world']);
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
it('works fine when no callbacks are provided', async () => {
|
|
661
|
-
const mockProvider = new MockProvider([
|
|
662
|
-
{
|
|
663
|
-
content: '"ok"',
|
|
664
|
-
tool_calls: [],
|
|
665
|
-
finish_reason: 'stop',
|
|
666
|
-
usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
|
667
|
-
},
|
|
668
|
-
]);
|
|
669
|
-
const providerRegistry = new ProviderRegistry();
|
|
670
|
-
providerRegistry.register(mockProvider);
|
|
671
|
-
|
|
672
|
-
await expect(
|
|
673
|
-
runAgent({
|
|
674
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
675
|
-
task: { description: 'No callbacks' },
|
|
676
|
-
context: {},
|
|
677
|
-
toolRegistry: new ToolRegistry(),
|
|
678
|
-
providerRegistry,
|
|
679
|
-
})
|
|
680
|
-
).resolves.toBeDefined();
|
|
681
|
-
});
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
describe('runAgent — abort signal', () => {
|
|
685
|
-
it('throws AbortError when signal is aborted before LLM call', async () => {
|
|
686
|
-
const controller = new AbortController();
|
|
687
|
-
controller.abort();
|
|
688
|
-
|
|
689
|
-
const mockProvider = new MockProvider([
|
|
690
|
-
{
|
|
691
|
-
content: '"ok"',
|
|
692
|
-
tool_calls: [],
|
|
693
|
-
finish_reason: 'stop',
|
|
694
|
-
usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
|
695
|
-
},
|
|
696
|
-
]);
|
|
697
|
-
const providerRegistry = new ProviderRegistry();
|
|
698
|
-
providerRegistry.register(mockProvider);
|
|
699
|
-
|
|
700
|
-
await expect(
|
|
701
|
-
runAgent({
|
|
702
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
703
|
-
task: { description: 'test' },
|
|
704
|
-
context: {},
|
|
705
|
-
toolRegistry: new ToolRegistry(),
|
|
706
|
-
providerRegistry,
|
|
707
|
-
signal: controller.signal,
|
|
708
|
-
})
|
|
709
|
-
).rejects.toThrow();
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
it('throws when signal aborts between tool call turns', async () => {
|
|
713
|
-
const controller = new AbortController();
|
|
714
|
-
|
|
715
|
-
const toolRegistry = new ToolRegistry();
|
|
716
|
-
toolRegistry.register({
|
|
717
|
-
name: 'test_tool',
|
|
718
|
-
description: 'Test',
|
|
719
|
-
parameters: {},
|
|
720
|
-
execute: async () => {
|
|
721
|
-
controller.abort();
|
|
722
|
-
return { success: true, output: 'done' };
|
|
723
|
-
},
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
const mockProvider = new MockProvider([
|
|
727
|
-
{
|
|
728
|
-
content: '',
|
|
729
|
-
tool_calls: [{ id: 'tc1', name: 'test_tool', arguments: {} }],
|
|
730
|
-
finish_reason: 'tool_use',
|
|
731
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
732
|
-
},
|
|
733
|
-
{
|
|
734
|
-
content: '"ok"',
|
|
735
|
-
tool_calls: [],
|
|
736
|
-
finish_reason: 'stop',
|
|
737
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
738
|
-
},
|
|
739
|
-
]);
|
|
740
|
-
const providerRegistry = new ProviderRegistry();
|
|
741
|
-
providerRegistry.register(mockProvider);
|
|
742
|
-
|
|
743
|
-
await expect(
|
|
744
|
-
runAgent({
|
|
745
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
746
|
-
task: { description: 'test' },
|
|
747
|
-
context: {},
|
|
748
|
-
toolRegistry,
|
|
749
|
-
providerRegistry,
|
|
750
|
-
signal: controller.signal,
|
|
751
|
-
})
|
|
752
|
-
).rejects.toThrow();
|
|
753
|
-
});
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
describe('runAgent — tool_calls_count semantics', () => {
|
|
757
|
-
it('excludes failed tool calls from tool_calls_count (standard path)', async () => {
|
|
758
|
-
const toolRegistry = new ToolRegistry();
|
|
759
|
-
toolRegistry.register({
|
|
760
|
-
name: 'broken_tool',
|
|
761
|
-
description: 'Always fails',
|
|
762
|
-
parameters: {},
|
|
763
|
-
execute: async () => ({ success: false, error: 'tool error' }),
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
const mockProvider = new MockProvider([
|
|
767
|
-
{
|
|
768
|
-
content: '',
|
|
769
|
-
tool_calls: [{ id: 'tc-1', name: 'broken_tool', arguments: {} }],
|
|
770
|
-
finish_reason: 'tool_calls',
|
|
771
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
772
|
-
},
|
|
773
|
-
{
|
|
774
|
-
content: '"recovered"',
|
|
775
|
-
tool_calls: [],
|
|
776
|
-
finish_reason: 'stop',
|
|
777
|
-
usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 },
|
|
778
|
-
},
|
|
779
|
-
]);
|
|
780
|
-
|
|
781
|
-
const providerRegistry = new ProviderRegistry();
|
|
782
|
-
providerRegistry.register(mockProvider);
|
|
783
|
-
|
|
784
|
-
const result = await runAgent({
|
|
785
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
786
|
-
task: { description: 'test failed tool' },
|
|
787
|
-
context: {},
|
|
788
|
-
toolRegistry,
|
|
789
|
-
providerRegistry,
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
// tool_calls still records the attempt
|
|
793
|
-
expect(result.tool_calls).toHaveLength(1);
|
|
794
|
-
expect(result.tool_calls[0].error).toBe('tool error');
|
|
795
|
-
// tool_calls_count only counts successful calls
|
|
796
|
-
expect(result.tool_calls_count).toBe(0);
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
it('counts only successful calls in a mixed-result batch (standard path)', async () => {
|
|
800
|
-
const toolRegistry = new ToolRegistry();
|
|
801
|
-
toolRegistry.register({
|
|
802
|
-
name: 'good_tool',
|
|
803
|
-
description: 'Succeeds',
|
|
804
|
-
parameters: {},
|
|
805
|
-
execute: async () => ({ success: true, output: 'ok' }),
|
|
806
|
-
});
|
|
807
|
-
toolRegistry.register({
|
|
808
|
-
name: 'bad_tool',
|
|
809
|
-
description: 'Fails',
|
|
810
|
-
parameters: {},
|
|
811
|
-
execute: async () => ({ success: false, error: 'bad' }),
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
const mockProvider = new MockProvider([
|
|
815
|
-
{
|
|
816
|
-
content: '',
|
|
817
|
-
tool_calls: [
|
|
818
|
-
{ id: 'tc-1', name: 'good_tool', arguments: {} },
|
|
819
|
-
{ id: 'tc-2', name: 'bad_tool', arguments: {} },
|
|
820
|
-
],
|
|
821
|
-
finish_reason: 'tool_calls',
|
|
822
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
823
|
-
},
|
|
824
|
-
{
|
|
825
|
-
content: '"done"',
|
|
826
|
-
tool_calls: [],
|
|
827
|
-
finish_reason: 'stop',
|
|
828
|
-
usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 },
|
|
829
|
-
},
|
|
830
|
-
]);
|
|
831
|
-
|
|
832
|
-
const providerRegistry = new ProviderRegistry();
|
|
833
|
-
providerRegistry.register(mockProvider);
|
|
834
|
-
|
|
835
|
-
const result = await runAgent({
|
|
836
|
-
agent: { name: 'test-agent', provider: 'mock', model: 'test-model' },
|
|
837
|
-
task: { description: 'mixed tools' },
|
|
838
|
-
context: {},
|
|
839
|
-
toolRegistry,
|
|
840
|
-
providerRegistry,
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
expect(result.tool_calls).toHaveLength(2);
|
|
844
|
-
expect(result.tool_calls_count).toBe(1); // only good_tool succeeded
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
it('excludes failed tool calls from tool_calls_count (agent-loop path)', async () => {
|
|
848
|
-
const agentLoopProvider: import('../src/providers/provider.js').AgentLoopProvider = {
|
|
849
|
-
name: 'mock-loop',
|
|
850
|
-
call: async () => { throw new Error('not used'); },
|
|
851
|
-
runAgentLoop: async (_req, executeTool) => {
|
|
852
|
-
const outcome = await executeTool('failing_loop_tool', {}, 'call-fail-1');
|
|
853
|
-
return {
|
|
854
|
-
content: '"recovered"',
|
|
855
|
-
tool_calls: [{ id: 'call-fail-1', name: 'failing_loop_tool', arguments: {}, ...outcome }],
|
|
856
|
-
finish_reason: 'stop',
|
|
857
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
858
|
-
};
|
|
859
|
-
},
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
const toolRegistry = new ToolRegistry();
|
|
863
|
-
toolRegistry.register({
|
|
864
|
-
name: 'failing_loop_tool',
|
|
865
|
-
description: 'Always fails in loop',
|
|
866
|
-
parameters: {},
|
|
867
|
-
execute: async () => ({ success: false, error: 'loop tool failed' }),
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
const providerRegistry = new ProviderRegistry();
|
|
871
|
-
providerRegistry.register(agentLoopProvider);
|
|
872
|
-
|
|
873
|
-
const result = await runAgent({
|
|
874
|
-
agent: { name: 'test-agent', provider: 'mock-loop', model: 'test-model' },
|
|
875
|
-
task: { description: 'test loop fail' },
|
|
876
|
-
context: {},
|
|
877
|
-
toolRegistry,
|
|
878
|
-
providerRegistry,
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
expect(result.tool_calls).toHaveLength(1);
|
|
882
|
-
expect(result.tool_calls[0].error).toBe('loop tool failed');
|
|
883
|
-
expect(result.tool_calls_count).toBe(0);
|
|
884
|
-
});
|
|
885
|
-
});
|