@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.
Files changed (78) hide show
  1. package/package.json +6 -3
  2. package/ARCHITECTURE.md +0 -53
  3. package/configs/agents/analyst.agent.yaml +0 -31
  4. package/configs/agents/code-generator.agent.yaml +0 -31
  5. package/configs/agents/generic.agent.yaml +0 -23
  6. package/src/__tests__/script-executor.test.ts +0 -180
  7. package/src/index.ts +0 -68
  8. package/src/integrations/integration-loader.test.ts +0 -88
  9. package/src/integrations/integration-loader.ts +0 -68
  10. package/src/middleware/anonymization.ts +0 -38
  11. package/src/plugins/index.ts +0 -4
  12. package/src/plugins/mcp-client.test.ts +0 -148
  13. package/src/plugins/mcp-client.ts +0 -128
  14. package/src/plugins/oauth-provider.test.ts +0 -167
  15. package/src/plugins/oauth-provider.ts +0 -175
  16. package/src/plugins/plugin-loader.test.ts +0 -114
  17. package/src/plugins/plugin-loader.ts +0 -90
  18. package/src/prompt-builder.test.ts +0 -167
  19. package/src/prompt-builder.ts +0 -332
  20. package/src/providers/anthropic.test.ts +0 -101
  21. package/src/providers/anthropic.ts +0 -135
  22. package/src/providers/mock.ts +0 -57
  23. package/src/providers/ollama.test.ts +0 -166
  24. package/src/providers/ollama.ts +0 -152
  25. package/src/providers/openai-responses.ts +0 -212
  26. package/src/providers/openai.test.ts +0 -67
  27. package/src/providers/openai.ts +0 -139
  28. package/src/providers/provider.ts +0 -54
  29. package/src/providers/registry.ts +0 -77
  30. package/src/runner.test.ts +0 -343
  31. package/src/runner.ts +0 -396
  32. package/src/script-executor.ts +0 -107
  33. package/src/tools/builtin/git.ts +0 -311
  34. package/src/tools/builtin/patch.ts +0 -257
  35. package/src/tools/builtin/repo-manager.ts +0 -142
  36. package/src/tools/builtin/search.ts +0 -108
  37. package/src/tools/builtin/shell.ts +0 -82
  38. package/src/tools/builtin/studio-run.ts +0 -73
  39. package/src/tools/builtin/web-search.test.ts +0 -122
  40. package/src/tools/builtin/web-search.ts +0 -101
  41. package/src/tools/errors.test.ts +0 -12
  42. package/src/tools/errors.ts +0 -6
  43. package/src/tools/plugin-loader.test.ts +0 -130
  44. package/src/tools/plugin-loader.ts +0 -203
  45. package/src/tools/skills/README.md +0 -49
  46. package/src/tools/skills/skill-loader.test.ts +0 -106
  47. package/src/tools/skills/skill-loader.ts +0 -62
  48. package/src/tools/tool-executor.test.ts +0 -88
  49. package/src/tools/tool-executor.ts +0 -84
  50. package/src/tools/tool-registry.ts +0 -130
  51. package/src/tools/yaml-executor.ts +0 -120
  52. package/src/utils/race-signal.test.ts +0 -50
  53. package/src/utils/race-signal.ts +0 -17
  54. package/templates/integrations/linear.integration.yaml +0 -35
  55. package/templates/integrations/slack.integration.yaml +0 -22
  56. package/templates/integrations/webhook.integration.yaml +0 -17
  57. package/templates/tools/git.tool.yaml +0 -80
  58. package/templates/tools/repo-manager.tool.yaml +0 -64
  59. package/templates/tools/search.tool.yaml +0 -22
  60. package/templates/tools/shell.tool.yaml +0 -19
  61. package/templates/tools/web-search.tool.yaml +0 -24
  62. package/tests/anonymization-middleware.test.ts +0 -61
  63. package/tests/anthropic.test.ts +0 -87
  64. package/tests/apply-patch.test.ts +0 -355
  65. package/tests/fixtures/tools/test-builtin.tool.yaml +0 -14
  66. package/tests/fixtures/tools/test-shell.tool.yaml +0 -19
  67. package/tests/mock-provider.test.ts +0 -104
  68. package/tests/openai.test.ts +0 -72
  69. package/tests/plugin-loader.test.ts +0 -54
  70. package/tests/prompt-builder.test.ts +0 -468
  71. package/tests/runner-anonymization.test.ts +0 -89
  72. package/tests/runner.test.ts +0 -885
  73. package/tests/studio-run.test.ts +0 -94
  74. package/tests/tool-executor.test.ts +0 -115
  75. package/tests/tool-registry.test.ts +0 -84
  76. package/tests/yaml-executor.test.ts +0 -76
  77. package/tsconfig.json +0 -20
  78. package/vitest.config.ts +0 -7
package/src/runner.ts DELETED
@@ -1,396 +0,0 @@
1
- /**
2
- * Main agent runner function - executes agent with LLM + tools
3
- */
4
-
5
- import type { ResolvedAgentConfig, ToolCall, LLMResponse, Message, OutputContract, RunnerCallbacks } from '@studio-foundation/contracts';
6
- import { buildPrompt, type TaskInput, type AgentContext, type ExecutionContext } from './prompt-builder.js';
7
- import type { ToolRegistry } from './tools/tool-registry.js';
8
- import { ToolExecutor } from './tools/tool-executor.js';
9
- import type { ProviderRegistry } from './providers/registry.js';
10
- import { isAgentLoopProvider } from './providers/provider.js';
11
- import type { AnonymizationMiddleware } from './middleware/anonymization.js';
12
-
13
- export interface RunAgentConfig {
14
- agent: ResolvedAgentConfig;
15
- task: TaskInput;
16
- context: AgentContext;
17
- executionContext?: ExecutionContext;
18
- toolRegistry: ToolRegistry;
19
- providerRegistry: ProviderRegistry;
20
- outputContract?: OutputContract;
21
- maxToolCalls?: number;
22
- anonymizationMiddleware?: AnonymizationMiddleware;
23
- callbacks?: RunnerCallbacks;
24
- signal?: AbortSignal;
25
- }
26
-
27
- export interface AgentRunResult {
28
- output: unknown;
29
- tool_calls: ToolCall[];
30
- tool_calls_count: number;
31
- raw_response?: LLMResponse;
32
- duration_ms: number;
33
- token_usage?: {
34
- prompt_tokens: number;
35
- completion_tokens: number;
36
- total_tokens: number;
37
- };
38
- /** Set when the runner hit a terminal error (e.g. max tool iterations). RALPH treats this as a validation failure. */
39
- error?: string;
40
- }
41
-
42
- const DEFAULT_MAX_TOOL_CALLS = 20; // Safety limit for tool calling loop
43
-
44
- /**
45
- * Run an agent task with LLM + tool execution
46
- *
47
- * Flow:
48
- * 1. Build prompt with context + retry info
49
- * 2. Call LLM provider
50
- * 3. Execute tool calls (multi-turn loop)
51
- * 4. Return complete result with tracked tool calls
52
- */
53
- export async function runAgent(config: RunAgentConfig): Promise<AgentRunResult> {
54
- const startTime = Date.now();
55
- const { agent, task, context, executionContext, toolRegistry, providerRegistry, signal } = config;
56
- const maxToolCalls = config.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
57
- const mw = config.anonymizationMiddleware;
58
-
59
- // Get provider
60
- const provider = providerRegistry.get(agent.provider);
61
-
62
- // Get allowed tools for this agent (filter if specified)
63
- const allowedTools = agent.tools && agent.tools.length > 0
64
- ? toolRegistry.filter(agent.tools)
65
- : toolRegistry;
66
-
67
- const promptSnippets = allowedTools.getActiveSnippets();
68
-
69
- // Injection point 1: Anonymize task input before building prompt
70
- const taskForPrompt = mw
71
- ? { ...task, description: mw.anonymize(task.description) }
72
- : task;
73
-
74
- // Build initial prompt
75
- const messages = buildPrompt({
76
- agent,
77
- task: taskForPrompt,
78
- context,
79
- executionContext,
80
- outputContract: config.outputContract,
81
- promptSnippets,
82
- });
83
-
84
- const toolDefinitions = allowedTools.toToolDefinitions();
85
-
86
- // Tool executor
87
- const toolExecutor = new ToolExecutor(allowedTools);
88
-
89
- // Track all tool calls made during execution
90
- const allToolCalls: ToolCall[] = [];
91
-
92
- // Accumulate token usage across turns
93
- const tokenAccumulator = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
94
-
95
- // Build onToken wrapper that bridges provider token callbacks → RunnerCallbacks.onAgentToken
96
- const onToken = config.callbacks?.onAgentToken
97
- ? (token: string) => config.callbacks!.onAgentToken!({ token, timestamp: Date.now() })
98
- : undefined;
99
-
100
- // --- Delegate to provider if it owns the full agent loop (e.g. Responses API) ---
101
- if (isAgentLoopProvider(provider)) {
102
- const loopResult = await provider.runAgentLoop(
103
- {
104
- model: agent.model,
105
- messages,
106
- tools: toolDefinitions.length > 0 ? toolDefinitions : undefined,
107
- temperature: agent.temperature,
108
- max_tokens: agent.max_tokens,
109
- stage_name: task.contract_name,
110
- },
111
- async (name, args, callId) => {
112
- const tcStart = Date.now();
113
- config.callbacks?.onToolCallStart?.({
114
- tool: name,
115
- params: args,
116
- timestamp: tcStart,
117
- });
118
-
119
- // pre_tool_use: check if hook wants to block this tool call
120
- if (config.callbacks?.onPreToolUse) {
121
- const preResult = await config.callbacks.onPreToolUse({ tool: name, params: args, timestamp: tcStart });
122
- if (preResult.blocked) {
123
- const blockedCall: ToolCall = {
124
- id: callId,
125
- name,
126
- arguments: args,
127
- error: preResult.error ?? 'Pre-tool hook blocked execution',
128
- };
129
- allToolCalls.push(blockedCall);
130
- return { result: undefined, error: blockedCall.error };
131
- }
132
- }
133
-
134
- const executed = await toolExecutor.execute({ id: callId, name, arguments: args });
135
- allToolCalls.push(executed);
136
-
137
- config.callbacks?.onToolCallComplete?.({
138
- tool: name,
139
- result: executed.result,
140
- error: executed.error,
141
- duration_ms: Date.now() - tcStart,
142
- timestamp: Date.now(),
143
- });
144
-
145
- // post_tool_use: notify (append_message not injected in agent loop path — provider controls conversation)
146
- if (config.callbacks?.onPostToolUse) {
147
- await config.callbacks.onPostToolUse({
148
- tool: name,
149
- params: args,
150
- result: executed.result,
151
- error: executed.error,
152
- timestamp: Date.now(),
153
- });
154
- }
155
-
156
- // Injection point 2: Anonymize tool results before returning to LLM
157
- let result = executed.result;
158
- if (mw && result !== undefined) {
159
- const resultStr = mw.anonymize(JSON.stringify(result));
160
- try { result = JSON.parse(resultStr); } catch { result = resultStr; }
161
- }
162
- return { result, error: executed.error };
163
- },
164
- onToken,
165
- signal
166
- );
167
-
168
- if (loopResult.usage) {
169
- tokenAccumulator.prompt_tokens += loopResult.usage.prompt_tokens;
170
- tokenAccumulator.completion_tokens += loopResult.usage.completion_tokens;
171
- tokenAccumulator.total_tokens += loopResult.usage.total_tokens;
172
- }
173
-
174
- const finalContent = mw ? mw.deanonymize(loopResult.content) : loopResult.content;
175
- const output = parseAgentOutput(finalContent);
176
- const duration = Date.now() - startTime;
177
- return {
178
- output,
179
- tool_calls: allToolCalls,
180
- tool_calls_count: allToolCalls.filter(tc => !tc.error).length,
181
- raw_response: {
182
- content: loopResult.content,
183
- tool_calls: loopResult.tool_calls.map(tc => ({ id: tc.id, name: tc.name, arguments: tc.arguments })),
184
- finish_reason: loopResult.finish_reason,
185
- usage: loopResult.usage,
186
- },
187
- duration_ms: duration,
188
- token_usage: tokenAccumulator.total_tokens > 0 ? tokenAccumulator : undefined,
189
- };
190
- }
191
-
192
- // --- Standard multi-turn loop (Chat Completions style) ---
193
- let currentMessages: Message[] = messages;
194
- let iterations = 0;
195
- let lastResponse: LLMResponse | null = null;
196
-
197
- while (iterations < maxToolCalls) {
198
- // Check for cancellation before calling LLM
199
- if (signal?.aborted) {
200
- throw new DOMException('The operation was aborted', 'AbortError');
201
- }
202
-
203
- // Call LLM
204
- const response = await provider.call({
205
- model: agent.model,
206
- messages: currentMessages,
207
- tools: toolDefinitions.length > 0 ? toolDefinitions : undefined,
208
- temperature: agent.temperature,
209
- max_tokens: agent.max_tokens,
210
- stage_name: task.contract_name,
211
- json_mode: !!task.contract_name,
212
- }, onToken, signal);
213
-
214
- lastResponse = response;
215
-
216
- if (response.usage) {
217
- tokenAccumulator.prompt_tokens += response.usage.prompt_tokens;
218
- tokenAccumulator.completion_tokens += response.usage.completion_tokens;
219
- tokenAccumulator.total_tokens += response.usage.total_tokens;
220
- }
221
-
222
- // Check if there are tool calls to execute
223
- if (!response.tool_calls || response.tool_calls.length === 0) {
224
- // No tool calls - this is the final response
225
- break;
226
- }
227
-
228
- // Emit thinking/progress if the LLM produced text alongside tool calls
229
- const thinkingText = response.content?.trim();
230
- if (thinkingText) {
231
- const now = Date.now();
232
- if (iterations === 0) {
233
- config.callbacks?.onAgentThinking?.({ thought: thinkingText, timestamp: now });
234
- } else {
235
- config.callbacks?.onAgentProgress?.({ message: thinkingText, timestamp: now });
236
- }
237
- }
238
-
239
- // Execute each tool call
240
- const executedToolCalls: ToolCall[] = [];
241
- const appendMessages = new Map<string, string>(); // tc.id → post-hook message
242
-
243
- for (const tc of response.tool_calls) {
244
- const tcStart = Date.now();
245
- config.callbacks?.onToolCallStart?.({
246
- tool: tc.name,
247
- params: tc.arguments,
248
- timestamp: tcStart,
249
- });
250
-
251
- // pre_tool_use: check if hook wants to block this tool call
252
- let executed!: ToolCall;
253
- let wasBlocked = false;
254
- if (config.callbacks?.onPreToolUse) {
255
- const preResult = await config.callbacks.onPreToolUse({
256
- tool: tc.name,
257
- params: tc.arguments,
258
- timestamp: tcStart,
259
- });
260
- if (preResult.blocked) {
261
- wasBlocked = true;
262
- executed = {
263
- id: tc.id,
264
- name: tc.name,
265
- arguments: tc.arguments,
266
- error: preResult.error ?? 'Pre-tool hook blocked execution',
267
- };
268
- }
269
- }
270
-
271
- if (!wasBlocked) {
272
- executed = await toolExecutor.execute({
273
- id: tc.id,
274
- name: tc.name,
275
- arguments: tc.arguments,
276
- });
277
- }
278
-
279
- executedToolCalls.push(executed);
280
- allToolCalls.push(executed);
281
-
282
- if (!wasBlocked) {
283
- config.callbacks?.onToolCallComplete?.({
284
- tool: tc.name,
285
- result: executed.result,
286
- error: executed.error,
287
- duration_ms: Date.now() - tcStart,
288
- timestamp: Date.now(),
289
- });
290
- }
291
-
292
- // post_tool_use: only called if tool was not blocked
293
- if (!wasBlocked && config.callbacks?.onPostToolUse) {
294
- const postResult = await config.callbacks.onPostToolUse({
295
- tool: tc.name,
296
- params: tc.arguments,
297
- result: executed.result,
298
- error: executed.error,
299
- timestamp: Date.now(),
300
- });
301
- if (postResult.append_message) {
302
- appendMessages.set(tc.id, postResult.append_message);
303
- }
304
- }
305
- }
306
-
307
- // Add assistant message with tool calls to conversation
308
- currentMessages.push({
309
- role: 'assistant',
310
- content: response.content || ''
311
- });
312
-
313
- // Add tool results as user messages
314
- // Format them clearly so the LLM can understand the results
315
- const toolResultsMessage = executedToolCalls.map(tc => {
316
- let msg: string;
317
- if (tc.error) {
318
- msg = `Tool ${tc.name} (id: ${tc.id}) failed: ${tc.error}`;
319
- } else {
320
- msg = `Tool ${tc.name} (id: ${tc.id}) result: ${JSON.stringify(tc.result)}`;
321
- }
322
- const appendMsg = appendMessages.get(tc.id);
323
- if (appendMsg) {
324
- msg += `\n\nPost-hook note: ${appendMsg}`;
325
- }
326
- return msg;
327
- }).join('\n\n');
328
-
329
- const toolResultContent = `Tool execution results:\n\n${toolResultsMessage}`;
330
- currentMessages.push({
331
- role: 'user',
332
- // Injection point 4: Anonymize tool results before adding to conversation
333
- content: mw ? mw.anonymize(toolResultContent) : toolResultContent,
334
- });
335
-
336
- iterations++;
337
- }
338
-
339
- if (iterations >= maxToolCalls) {
340
- const duration = Date.now() - startTime;
341
- return {
342
- output: null,
343
- tool_calls: allToolCalls,
344
- tool_calls_count: allToolCalls.filter(tc => !tc.error).length,
345
- raw_response: lastResponse!,
346
- duration_ms: duration,
347
- token_usage: tokenAccumulator.total_tokens > 0 ? tokenAccumulator : undefined,
348
- error: `Maximum tool calling iterations (${maxToolCalls}) reached. Possible infinite loop.`,
349
- };
350
- }
351
-
352
- if (!lastResponse) {
353
- throw new Error('No response received from LLM');
354
- }
355
-
356
- // Parse final output from the last response content
357
- const finalContent = mw ? mw.deanonymize(lastResponse.content) : lastResponse.content;
358
- const output = parseAgentOutput(finalContent);
359
-
360
- const duration = Date.now() - startTime;
361
-
362
- return {
363
- output,
364
- tool_calls: allToolCalls,
365
- tool_calls_count: allToolCalls.filter(tc => !tc.error).length,
366
- raw_response: lastResponse,
367
- duration_ms: duration,
368
- token_usage: tokenAccumulator.total_tokens > 0 ? tokenAccumulator : undefined,
369
- };
370
- }
371
-
372
- function parseAgentOutput(rawContent: string): unknown {
373
- // Try 1: Direct JSON parse
374
- try {
375
- return JSON.parse(rawContent);
376
- } catch {}
377
-
378
- // Try 2: Extract from markdown code block
379
- const codeBlockMatch = rawContent.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
380
- if (codeBlockMatch) {
381
- try {
382
- return JSON.parse(codeBlockMatch[1].trim());
383
- } catch {}
384
- }
385
-
386
- // Try 3: Find first { ... } in the response
387
- const jsonMatch = rawContent.match(/\{[\s\S]*\}/);
388
- if (jsonMatch) {
389
- try {
390
- return JSON.parse(jsonMatch[0]);
391
- } catch {}
392
- }
393
-
394
- // Failed to parse — return raw string, ralph validation will reject it
395
- return rawContent;
396
- }
@@ -1,107 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import type { AgentRunResult } from './runner.js';
5
- import type { AgentContext } from './prompt-builder.js';
6
-
7
- export interface ScriptExecutorConfig {
8
- scriptPath: string;
9
- runtime: 'python' | 'node' | 'shell';
10
- context: AgentContext;
11
- cwd?: string;
12
- timeoutMs?: number;
13
- }
14
-
15
- const RUNTIME_COMMANDS: Record<string, string> = {
16
- python: 'python3',
17
- node: 'node',
18
- shell: 'sh',
19
- };
20
-
21
- function buildEnv(runtime: string, cwd: string): Record<string, string> {
22
- const env = { ...process.env } as Record<string, string>;
23
-
24
- if (runtime === 'python') {
25
- const venvPath = existsSync(join(cwd, 'venv'))
26
- ? join(cwd, 'venv')
27
- : existsSync(join(cwd, '.venv'))
28
- ? join(cwd, '.venv')
29
- : null;
30
-
31
- if (venvPath) {
32
- env.VIRTUAL_ENV = venvPath;
33
- env.PATH = `${join(venvPath, 'bin')}:${env.PATH ?? ''}`;
34
- }
35
- }
36
-
37
- return env;
38
- }
39
-
40
- export async function runScript(config: ScriptExecutorConfig): Promise<AgentRunResult> {
41
- const startTime = Date.now();
42
- const cwd = config.cwd ?? process.cwd();
43
- const timeoutMs = config.timeoutMs ?? 30_000;
44
- const cmd = RUNTIME_COMMANDS[config.runtime];
45
- const env = buildEnv(config.runtime, cwd);
46
- const stdin = JSON.stringify(config.context);
47
-
48
- return new Promise((resolve) => {
49
- const proc = spawn(cmd, [config.scriptPath], {
50
- cwd,
51
- env,
52
- stdio: ['pipe', 'pipe', 'pipe'],
53
- });
54
-
55
- let stdout = '';
56
- let stderr = '';
57
- let timedOut = false;
58
- let settled = false;
59
-
60
- const settle = (result: Parameters<typeof resolve>[0]) => {
61
- if (settled) return;
62
- settled = true;
63
- clearTimeout(timer);
64
- resolve(result);
65
- };
66
-
67
- const timer = setTimeout(() => {
68
- timedOut = true;
69
- proc.kill('SIGTERM');
70
- setTimeout(() => proc.kill('SIGKILL'), 1000);
71
- }, timeoutMs);
72
-
73
- proc.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); });
74
- proc.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); });
75
-
76
- proc.stdin.write(stdin);
77
- proc.stdin.end();
78
-
79
- proc.on('close', (exitCode) => {
80
- const duration_ms = Date.now() - startTime;
81
-
82
- if (timedOut) {
83
- settle({ output: null, tool_calls: [], tool_calls_count: 0, duration_ms, error: `Script timed out after ${timeoutMs}ms` });
84
- return;
85
- }
86
-
87
- if (exitCode !== 0) {
88
- settle({ output: null, tool_calls: [], tool_calls_count: 0, duration_ms, error: `Script exited with code ${exitCode}: ${stderr.trim()}` });
89
- return;
90
- }
91
-
92
- let output: unknown;
93
- try {
94
- output = JSON.parse(stdout.trim());
95
- } catch {
96
- settle({ output: null, tool_calls: [], tool_calls_count: 0, duration_ms, error: `Script output is not valid JSON: ${stdout.slice(0, 200)}` });
97
- return;
98
- }
99
-
100
- settle({ output, tool_calls: [], tool_calls_count: 0, duration_ms });
101
- });
102
-
103
- proc.on('error', (err) => {
104
- settle({ output: null, tool_calls: [], tool_calls_count: 0, duration_ms: Date.now() - startTime, error: `Script process error: ${err.message}` });
105
- });
106
- });
107
- }