@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
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// runner/tests/plugin-loader.test.ts
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
|
-
import { loadProjectTools } from '../src/tools/plugin-loader.js';
|
|
5
|
-
|
|
6
|
-
const FIXTURES_DIR = resolve(import.meta.dirname, 'fixtures/tools');
|
|
7
|
-
|
|
8
|
-
describe('loadProjectTools', () => {
|
|
9
|
-
it('returns empty array when tools dir does not exist', async () => {
|
|
10
|
-
const result = await loadProjectTools('/nonexistent/path', '/tmp');
|
|
11
|
-
expect(result).toEqual([]);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('returns empty array when tools dir has no .tool.yaml files', async () => {
|
|
15
|
-
const result = await loadProjectTools('/tmp', '/tmp');
|
|
16
|
-
expect(result).toEqual([]);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('loads a shell-type tool and returns a working Tool', async () => {
|
|
20
|
-
const plugins = await loadProjectTools(FIXTURES_DIR, '/tmp');
|
|
21
|
-
const shellPlugin = plugins.find(p => p.name === 'test_shell');
|
|
22
|
-
expect(shellPlugin).toBeDefined();
|
|
23
|
-
expect(shellPlugin!.tools).toHaveLength(1);
|
|
24
|
-
|
|
25
|
-
const tool = shellPlugin!.tools[0]!;
|
|
26
|
-
expect(tool.name).toBe('test_shell-echo');
|
|
27
|
-
const result = await tool.execute({ message: 'hi' });
|
|
28
|
-
expect(result.success).toBe(true);
|
|
29
|
-
expect(result.output).toBe('hi');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('returns prompt_snippet from shell plugin', async () => {
|
|
33
|
-
const plugins = await loadProjectTools(FIXTURES_DIR, '/tmp');
|
|
34
|
-
const shellPlugin = plugins.find(p => p.name === 'test_shell');
|
|
35
|
-
expect(shellPlugin!.promptSnippet).toMatch(/test shell tool/);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('loads a builtin-type tool by delegating to existing TypeScript impl', async () => {
|
|
39
|
-
const plugins = await loadProjectTools(FIXTURES_DIR, '/tmp');
|
|
40
|
-
const builtinPlugin = plugins.find(p => p.name === 'test_builtin');
|
|
41
|
-
expect(builtinPlugin).toBeDefined();
|
|
42
|
-
const tool = builtinPlugin!.tools[0]!;
|
|
43
|
-
expect(tool.name).toBe('repo_manager-list_files');
|
|
44
|
-
// Can call it without error (uses the real TS impl); '.' lists /tmp itself
|
|
45
|
-
const result = await tool.execute({ path: '.' });
|
|
46
|
-
expect(result.success).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('skips builtin commands with unknown names (no crash)', async () => {
|
|
50
|
-
// test-builtin.tool.yaml only has repo_manager-list_files, which exists
|
|
51
|
-
const plugins = await loadProjectTools(FIXTURES_DIR, '/tmp');
|
|
52
|
-
expect(plugins.length).toBeGreaterThan(0);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prompt builder tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { buildPrompt } from '../src/prompt-builder.js';
|
|
7
|
-
import type { AgentConfig } from '@studio-foundation/contracts';
|
|
8
|
-
|
|
9
|
-
describe('PromptBuilder', () => {
|
|
10
|
-
it('should build basic prompt', () => {
|
|
11
|
-
const agent: AgentConfig = {
|
|
12
|
-
name: 'test-agent',
|
|
13
|
-
provider: 'openai',
|
|
14
|
-
model: 'gpt-4',
|
|
15
|
-
system_prompt: 'You are a helpful assistant.'
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const messages = buildPrompt({
|
|
19
|
-
agent,
|
|
20
|
-
task: {
|
|
21
|
-
description: 'Write a hello world function'
|
|
22
|
-
},
|
|
23
|
-
context: {}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
expect(messages).toHaveLength(2);
|
|
27
|
-
expect(messages[0]).toEqual({
|
|
28
|
-
role: 'system',
|
|
29
|
-
content: 'You are a helpful assistant.'
|
|
30
|
-
});
|
|
31
|
-
expect(messages[1].role).toBe('user');
|
|
32
|
-
expect(messages[1].content).toContain('Write a hello world function');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should include previous outputs in context', () => {
|
|
36
|
-
const agent: AgentConfig = {
|
|
37
|
-
name: 'test-agent',
|
|
38
|
-
provider: 'openai',
|
|
39
|
-
model: 'gpt-4'
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const messages = buildPrompt({
|
|
43
|
-
agent,
|
|
44
|
-
task: {
|
|
45
|
-
description: 'Generate code'
|
|
46
|
-
},
|
|
47
|
-
context: {
|
|
48
|
-
previous_outputs: {
|
|
49
|
-
analysis: { findings: 'Good code structure' }
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
expect(messages[1].content).toContain('Previous Stage Outputs');
|
|
55
|
-
expect(messages[1].content).toContain('analysis');
|
|
56
|
-
expect(messages[1].content).toContain('Good code structure');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should add retry escalation for attempt 2', () => {
|
|
60
|
-
const agent: AgentConfig = {
|
|
61
|
-
name: 'test-agent',
|
|
62
|
-
provider: 'openai',
|
|
63
|
-
model: 'gpt-4'
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const messages = buildPrompt({
|
|
67
|
-
agent,
|
|
68
|
-
task: {
|
|
69
|
-
description: 'Fix the bug'
|
|
70
|
-
},
|
|
71
|
-
context: {},
|
|
72
|
-
executionContext: {
|
|
73
|
-
attempt: 2,
|
|
74
|
-
previous_failures: [
|
|
75
|
-
{ error: 'No tool calls made', tool_calls_count: 0 }
|
|
76
|
-
]
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
expect(messages[1].content).toContain('RETRY ATTEMPT 2');
|
|
81
|
-
expect(messages[1].content).toContain('No tool calls made');
|
|
82
|
-
expect(messages[1].content).toContain('Problem: No tool calls were made');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should add stronger retry escalation for attempt 3+', () => {
|
|
86
|
-
const agent: AgentConfig = {
|
|
87
|
-
name: 'test-agent',
|
|
88
|
-
provider: 'openai',
|
|
89
|
-
model: 'gpt-4'
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const messages = buildPrompt({
|
|
93
|
-
agent,
|
|
94
|
-
task: {
|
|
95
|
-
description: 'Fix the bug'
|
|
96
|
-
},
|
|
97
|
-
context: {},
|
|
98
|
-
executionContext: {
|
|
99
|
-
attempt: 3,
|
|
100
|
-
previous_failures: [
|
|
101
|
-
{ error: 'No tool calls', tool_calls_count: 0 },
|
|
102
|
-
{ error: 'Still no tool calls', tool_calls_count: 0 }
|
|
103
|
-
]
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
expect(messages[1].content).toContain('CRITICAL: RETRY ATTEMPT 3');
|
|
108
|
-
expect(messages[1].content).toContain('YOU MUST:');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('does not inject domain-specific workflow instructions regardless of task description', () => {
|
|
112
|
-
const messages = buildPrompt({
|
|
113
|
-
agent: { name: 'coder', provider: 'mock', model: 'mock' },
|
|
114
|
-
task: { description: 'Generate code' },
|
|
115
|
-
context: {}
|
|
116
|
-
});
|
|
117
|
-
expect(messages[0].content).not.toContain('CRITICAL: Code Generation Workflow');
|
|
118
|
-
expect(messages[0].content).not.toContain('repo_manager-read_file');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('uses "end with" phrasing when contract requires tool calls', () => {
|
|
122
|
-
const messages = buildPrompt({
|
|
123
|
-
agent: { name: 'coder', provider: 'mock', model: 'mock' },
|
|
124
|
-
task: { description: 'Generate code' },
|
|
125
|
-
context: {},
|
|
126
|
-
outputContract: {
|
|
127
|
-
name: 'code-generation',
|
|
128
|
-
version: 1,
|
|
129
|
-
schema: { required_fields: ['summary', 'files_changed'] },
|
|
130
|
-
tool_calls: { minimum: 1 },
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
const system = messages[0].content as string;
|
|
134
|
-
expect(system).toContain('end with');
|
|
135
|
-
expect(system).toContain('Your final message (after all tool calls)');
|
|
136
|
-
expect(system).not.toContain('respond with');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('uses "respond with" phrasing when contract has no tool call requirement', () => {
|
|
140
|
-
const messages = buildPrompt({
|
|
141
|
-
agent: { name: 'analyst', provider: 'mock', model: 'mock' },
|
|
142
|
-
task: { description: 'Analyse the brief' },
|
|
143
|
-
context: {},
|
|
144
|
-
outputContract: {
|
|
145
|
-
name: 'brief-analysis',
|
|
146
|
-
version: 1,
|
|
147
|
-
schema: { required_fields: ['summary', 'requirements'] },
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
const system = messages[0].content as string;
|
|
151
|
-
expect(system).toContain('respond with');
|
|
152
|
-
expect(system).toContain('Your entire response');
|
|
153
|
-
expect(system).not.toContain('end with');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('retry messages do not reference specific tool names', () => {
|
|
157
|
-
const messages2 = buildPrompt({
|
|
158
|
-
agent: { name: 'coder', provider: 'mock', model: 'mock' },
|
|
159
|
-
task: { description: 'Generate code' },
|
|
160
|
-
context: {},
|
|
161
|
-
executionContext: {
|
|
162
|
-
attempt: 2,
|
|
163
|
-
previous_failures: [{ error: 'Required tool not called', tool_calls_count: 0 }]
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
expect(messages2[1].content).not.toContain('repo_manager-write_file');
|
|
167
|
-
|
|
168
|
-
const messages3 = buildPrompt({
|
|
169
|
-
agent: { name: 'coder', provider: 'mock', model: 'mock' },
|
|
170
|
-
task: { description: 'Generate code' },
|
|
171
|
-
context: {},
|
|
172
|
-
executionContext: {
|
|
173
|
-
attempt: 3,
|
|
174
|
-
previous_failures: [
|
|
175
|
-
{ error: 'No tool calls', tool_calls_count: 0 },
|
|
176
|
-
{ error: 'No tool calls', tool_calls_count: 0 }
|
|
177
|
-
]
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
expect(messages3[1].content).not.toContain('repo_manager-write_file');
|
|
181
|
-
|
|
182
|
-
const messages4 = buildPrompt({
|
|
183
|
-
agent: { name: 'coder', provider: 'mock', model: 'mock' },
|
|
184
|
-
task: { description: 'Generate code' },
|
|
185
|
-
context: {},
|
|
186
|
-
executionContext: {
|
|
187
|
-
attempt: 4,
|
|
188
|
-
previous_failures: [
|
|
189
|
-
{ error: 'No tool calls', tool_calls_count: 0 },
|
|
190
|
-
{ error: 'No tool calls', tool_calls_count: 0 },
|
|
191
|
-
{ error: 'No tool calls', tool_calls_count: 0 }
|
|
192
|
-
]
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
expect(messages4[1].content).not.toContain('repo_manager-write_file');
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
describe('buildPrompt - context_packs', () => {
|
|
200
|
-
it('renders each pack as its own ## section with description', () => {
|
|
201
|
-
const messages = buildPrompt({
|
|
202
|
-
agent: { name: 'test', system_prompt: 'You are helpful.', provider: 'mock', model: 'mock' } as any,
|
|
203
|
-
task: { description: 'Do the task.' },
|
|
204
|
-
context: {
|
|
205
|
-
context_packs: [
|
|
206
|
-
{
|
|
207
|
-
name: 'React Conventions',
|
|
208
|
-
description: 'React coding standards',
|
|
209
|
-
sections: [
|
|
210
|
-
{ title: 'Naming conventions', content: '- Components: PascalCase' },
|
|
211
|
-
],
|
|
212
|
-
},
|
|
213
|
-
],
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
218
|
-
expect(userContent).toContain('## React Conventions — React coding standards');
|
|
219
|
-
expect(userContent).toContain('### Naming conventions');
|
|
220
|
-
expect(userContent).toContain('- Components: PascalCase');
|
|
221
|
-
// Pack appears before ## Task
|
|
222
|
-
expect(userContent.indexOf('## React Conventions')).toBeLessThan(userContent.indexOf('## Task'));
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('renders pack without description (no dash suffix)', () => {
|
|
226
|
-
const messages = buildPrompt({
|
|
227
|
-
agent: { name: 'test', system_prompt: 'You are helpful.', provider: 'mock', model: 'mock' } as any,
|
|
228
|
-
task: { description: 'Do the task.' },
|
|
229
|
-
context: {
|
|
230
|
-
context_packs: [
|
|
231
|
-
{
|
|
232
|
-
name: 'Testing Standards',
|
|
233
|
-
sections: [{ title: 'Coverage', content: 'Aim for 80%.' }],
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
240
|
-
expect(userContent).toContain('## Testing Standards\n\n');
|
|
241
|
-
expect(userContent).not.toContain('## Testing Standards —');
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('renders multiple packs in order', () => {
|
|
245
|
-
const messages = buildPrompt({
|
|
246
|
-
agent: { name: 'test', system_prompt: 'You are helpful.', provider: 'mock', model: 'mock' } as any,
|
|
247
|
-
task: { description: 'Do the task.' },
|
|
248
|
-
context: {
|
|
249
|
-
context_packs: [
|
|
250
|
-
{ name: 'Pack A', sections: [{ title: 'A', content: 'a' }] },
|
|
251
|
-
{ name: 'Pack B', sections: [{ title: 'B', content: 'b' }] },
|
|
252
|
-
],
|
|
253
|
-
},
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
257
|
-
expect(userContent.indexOf('## Pack A')).toBeLessThan(userContent.indexOf('## Pack B'));
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('skips pack rendering when context_packs is empty or absent', () => {
|
|
261
|
-
const messages = buildPrompt({
|
|
262
|
-
agent: { name: 'test', system_prompt: 'You are helpful.', provider: 'mock', model: 'mock' } as any,
|
|
263
|
-
task: { description: 'Do the task.' },
|
|
264
|
-
context: { context_packs: [] },
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
268
|
-
// Only ## Task should be present (no pack ## headers)
|
|
269
|
-
const headers = [...userContent.matchAll(/^## /gm)];
|
|
270
|
-
expect(headers).toHaveLength(1);
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
describe('buildPrompt with promptSnippets', () => {
|
|
275
|
-
it('injects prompt snippets into system message', () => {
|
|
276
|
-
const messages = buildPrompt({
|
|
277
|
-
agent: { name: 'a', provider: 'anthropic', model: 'claude-haiku-4-5', tools: [] },
|
|
278
|
-
task: { description: 'Do something' },
|
|
279
|
-
context: {},
|
|
280
|
-
promptSnippets: ['Use tool X carefully.', 'Always verify results.'],
|
|
281
|
-
});
|
|
282
|
-
const system = messages.find(m => m.role === 'system')!;
|
|
283
|
-
expect(system.content).toContain('Use tool X carefully.');
|
|
284
|
-
expect(system.content).toContain('Always verify results.');
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('does not crash when promptSnippets is empty', () => {
|
|
288
|
-
expect(() =>
|
|
289
|
-
buildPrompt({
|
|
290
|
-
agent: { name: 'a', provider: 'anthropic', model: 'claude-haiku-4-5', tools: [] },
|
|
291
|
-
task: { description: 'Do something' },
|
|
292
|
-
context: {},
|
|
293
|
-
promptSnippets: [],
|
|
294
|
-
})
|
|
295
|
-
).not.toThrow();
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it('does not crash when promptSnippets is undefined', () => {
|
|
299
|
-
expect(() =>
|
|
300
|
-
buildPrompt({
|
|
301
|
-
agent: { name: 'a', provider: 'anthropic', model: 'claude-haiku-4-5', tools: [] },
|
|
302
|
-
task: { description: 'Do something' },
|
|
303
|
-
context: {},
|
|
304
|
-
})
|
|
305
|
-
).not.toThrow();
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
describe('buildPrompt — previous_tool_results', () => {
|
|
310
|
-
const baseAgent: AgentConfig = {
|
|
311
|
-
name: 'test',
|
|
312
|
-
provider: 'mock',
|
|
313
|
-
model: 'mock',
|
|
314
|
-
system_prompt: 'You are helpful.',
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
it('renders a "Previous Stage Discoveries" section per stage', () => {
|
|
318
|
-
const messages = buildPrompt({
|
|
319
|
-
agent: baseAgent,
|
|
320
|
-
task: { description: 'Do the task.' },
|
|
321
|
-
context: {
|
|
322
|
-
previous_tool_results: {
|
|
323
|
-
'brief-analysis': [
|
|
324
|
-
{
|
|
325
|
-
id: '1',
|
|
326
|
-
name: 'search-search_codebase',
|
|
327
|
-
arguments: { pattern: 'about' },
|
|
328
|
-
result: { matches: [{ file: 'src/pages/about.tsx', content: 'export default function About' }] },
|
|
329
|
-
},
|
|
330
|
-
],
|
|
331
|
-
},
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
336
|
-
expect(userContent).toContain('## Previous Stage Discoveries (brief-analysis)');
|
|
337
|
-
expect(userContent).toContain('search-search_codebase');
|
|
338
|
-
expect(userContent).toContain('about');
|
|
339
|
-
expect(userContent).toContain('about.tsx');
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('discoveries section appears before the Task section', () => {
|
|
343
|
-
const messages = buildPrompt({
|
|
344
|
-
agent: baseAgent,
|
|
345
|
-
task: { description: 'Do the task.' },
|
|
346
|
-
context: {
|
|
347
|
-
previous_tool_results: {
|
|
348
|
-
'stage-1': [{ id: '1', name: 'tool-x', arguments: { q: 'foo' }, result: 'bar' }],
|
|
349
|
-
},
|
|
350
|
-
},
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
354
|
-
const discoveriesIdx = userContent.indexOf('## Previous Stage Discoveries');
|
|
355
|
-
const taskIdx = userContent.indexOf('## Task');
|
|
356
|
-
expect(discoveriesIdx).toBeGreaterThan(-1);
|
|
357
|
-
expect(discoveriesIdx).toBeLessThan(taskIdx);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('truncates results longer than 2000 chars', () => {
|
|
361
|
-
const longResult = 'x'.repeat(3000);
|
|
362
|
-
const messages = buildPrompt({
|
|
363
|
-
agent: baseAgent,
|
|
364
|
-
task: { description: 'Do the task.' },
|
|
365
|
-
context: {
|
|
366
|
-
previous_tool_results: {
|
|
367
|
-
'stage-1': [{ id: '1', name: 'tool-x', arguments: {}, result: longResult }],
|
|
368
|
-
},
|
|
369
|
-
},
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
373
|
-
expect(userContent).toContain('[truncated]');
|
|
374
|
-
// Should not contain the full 3000-char result
|
|
375
|
-
expect(userContent).not.toContain(longResult);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it('renders tool error when present', () => {
|
|
379
|
-
const messages = buildPrompt({
|
|
380
|
-
agent: baseAgent,
|
|
381
|
-
task: { description: 'Do the task.' },
|
|
382
|
-
context: {
|
|
383
|
-
previous_tool_results: {
|
|
384
|
-
'stage-1': [{ id: '1', name: 'tool-x', arguments: {}, error: 'File not found' }],
|
|
385
|
-
},
|
|
386
|
-
},
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
390
|
-
expect(userContent).toContain('Error: File not found');
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it('skips rendering when previous_tool_results is empty or absent', () => {
|
|
394
|
-
const messages = buildPrompt({
|
|
395
|
-
agent: baseAgent,
|
|
396
|
-
task: { description: 'Do the task.' },
|
|
397
|
-
context: {},
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
401
|
-
expect(userContent).not.toContain('Previous Stage Discoveries');
|
|
402
|
-
});
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
describe('buildPrompt — group_feedback', () => {
|
|
406
|
-
const baseAgent: AgentConfig = {
|
|
407
|
-
name: 'test',
|
|
408
|
-
provider: 'mock',
|
|
409
|
-
model: 'mock',
|
|
410
|
-
system_prompt: 'You are helpful.',
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
it('renders group feedback as the first section in user message', () => {
|
|
414
|
-
const messages = buildPrompt({
|
|
415
|
-
agent: baseAgent,
|
|
416
|
-
task: { description: 'Generate code.' },
|
|
417
|
-
context: {
|
|
418
|
-
additional_context: 'Build a dark mode toggle',
|
|
419
|
-
group_feedback: {
|
|
420
|
-
iteration: 1,
|
|
421
|
-
max_iterations: 3,
|
|
422
|
-
rejection_reason: 'Missing localStorage persistence',
|
|
423
|
-
rejection_details: ['No localStorage.setItem call', 'Theme not restored on load'],
|
|
424
|
-
},
|
|
425
|
-
},
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
429
|
-
expect(userContent).toContain('REVISION REQUIRED');
|
|
430
|
-
expect(userContent).toContain('Iteration 2/3');
|
|
431
|
-
expect(userContent).toContain('Missing localStorage persistence');
|
|
432
|
-
expect(userContent).toContain('No localStorage.setItem call');
|
|
433
|
-
expect(userContent).toContain('Theme not restored on load');
|
|
434
|
-
// Feedback appears BEFORE additional context and task
|
|
435
|
-
expect(userContent.indexOf('REVISION REQUIRED')).toBeLessThan(userContent.indexOf('Additional Context'));
|
|
436
|
-
expect(userContent.indexOf('REVISION REQUIRED')).toBeLessThan(userContent.indexOf('## Task'));
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it('renders feedback without details when rejection_details is empty', () => {
|
|
440
|
-
const messages = buildPrompt({
|
|
441
|
-
agent: baseAgent,
|
|
442
|
-
task: { description: 'Generate code.' },
|
|
443
|
-
context: {
|
|
444
|
-
group_feedback: {
|
|
445
|
-
iteration: 0,
|
|
446
|
-
max_iterations: 3,
|
|
447
|
-
rejection_reason: 'Code quality too low',
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
453
|
-
expect(userContent).toContain('REVISION REQUIRED');
|
|
454
|
-
expect(userContent).toContain('Code quality too low');
|
|
455
|
-
expect(userContent).not.toContain('Issues to fix');
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it('skips feedback section when group_feedback is absent', () => {
|
|
459
|
-
const messages = buildPrompt({
|
|
460
|
-
agent: baseAgent,
|
|
461
|
-
task: { description: 'Generate code.' },
|
|
462
|
-
context: {},
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
const userContent = messages.find(m => m.role === 'user')!.content as string;
|
|
466
|
-
expect(userContent).not.toContain('REVISION REQUIRED');
|
|
467
|
-
});
|
|
468
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { runAgent } from '../src/runner.js';
|
|
3
|
-
import type { Provider } from '../src/providers/provider.js';
|
|
4
|
-
import type { LLMRequest, LLMResponse } from '@studio-foundation/contracts';
|
|
5
|
-
import { ProviderRegistry } from '../src/providers/registry.js';
|
|
6
|
-
import { ToolRegistry } from '../src/tools/tool-registry.js';
|
|
7
|
-
import { AnonymizationMiddleware } from '../src/middleware/anonymization.js';
|
|
8
|
-
|
|
9
|
-
class MockProvider implements Provider {
|
|
10
|
-
readonly name = 'mock';
|
|
11
|
-
capturedRequests: LLMRequest[] = [];
|
|
12
|
-
private response: string;
|
|
13
|
-
|
|
14
|
-
constructor(response: string) { this.response = response; }
|
|
15
|
-
|
|
16
|
-
async call(req: LLMRequest): Promise<LLMResponse> {
|
|
17
|
-
this.capturedRequests.push(req);
|
|
18
|
-
return {
|
|
19
|
-
content: this.response,
|
|
20
|
-
tool_calls: [],
|
|
21
|
-
finish_reason: 'stop',
|
|
22
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('runAgent with anonymization', () => {
|
|
28
|
-
it('anonymizes task description before LLM sees it', async () => {
|
|
29
|
-
const provider = new MockProvider('{"result": "done"}');
|
|
30
|
-
const providerRegistry = new ProviderRegistry();
|
|
31
|
-
providerRegistry.register(provider);
|
|
32
|
-
const middleware = new AnonymizationMiddleware();
|
|
33
|
-
|
|
34
|
-
await runAgent({
|
|
35
|
-
agent: { name: 'test', provider: 'mock', model: 'x' },
|
|
36
|
-
task: { description: 'Process mc@acme.com data' },
|
|
37
|
-
context: {},
|
|
38
|
-
toolRegistry: new ToolRegistry(),
|
|
39
|
-
providerRegistry,
|
|
40
|
-
anonymizationMiddleware: middleware,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const messages = provider.capturedRequests[0].messages;
|
|
44
|
-
const fullText = messages.map(m => m.content).join(' ');
|
|
45
|
-
expect(fullText).not.toContain('mc@acme.com');
|
|
46
|
-
expect(fullText).toContain('EMAIL_1');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('deanonymizes LLM output so caller gets real values', async () => {
|
|
50
|
-
// Middleware already has EMAIL_1 = mc@acme.com in its keymap
|
|
51
|
-
const middleware = new AnonymizationMiddleware();
|
|
52
|
-
middleware.anonymize('mc@acme.com'); // seeds keymap: EMAIL_1 → mc@acme.com
|
|
53
|
-
|
|
54
|
-
const provider = new MockProvider('{"email": "EMAIL_1"}');
|
|
55
|
-
const providerRegistry = new ProviderRegistry();
|
|
56
|
-
providerRegistry.register(provider);
|
|
57
|
-
|
|
58
|
-
const result = await runAgent({
|
|
59
|
-
agent: { name: 'test', provider: 'mock', model: 'x' },
|
|
60
|
-
task: { description: 'Test' },
|
|
61
|
-
context: {},
|
|
62
|
-
toolRegistry: new ToolRegistry(),
|
|
63
|
-
providerRegistry,
|
|
64
|
-
anonymizationMiddleware: middleware,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const output = result.output as { email: string };
|
|
68
|
-
expect(output.email).toBe('mc@acme.com');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('works identically when no middleware provided (regression)', async () => {
|
|
72
|
-
const provider = new MockProvider('{"result": "ok"}');
|
|
73
|
-
const providerRegistry = new ProviderRegistry();
|
|
74
|
-
providerRegistry.register(provider);
|
|
75
|
-
|
|
76
|
-
const result = await runAgent({
|
|
77
|
-
agent: { name: 'test', provider: 'mock', model: 'x' },
|
|
78
|
-
task: { description: 'Normal task mc@acme.com' },
|
|
79
|
-
context: {},
|
|
80
|
-
toolRegistry: new ToolRegistry(),
|
|
81
|
-
providerRegistry,
|
|
82
|
-
// No anonymizationMiddleware
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
expect(result.output).toEqual({ result: 'ok' });
|
|
86
|
-
const fullText = provider.capturedRequests[0].messages.map(m => m.content).join(' ');
|
|
87
|
-
expect(fullText).toContain('mc@acme.com');
|
|
88
|
-
});
|
|
89
|
-
});
|