@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/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
|
-
}
|
package/src/script-executor.ts
DELETED
|
@@ -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
|
-
}
|