agent-hitch 0.2.15 → 0.2.16
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.
|
@@ -21,8 +21,11 @@ const maxSteps = training ? binding.max_episode_steps : Number(process.env.HITCH
|
|
|
21
21
|
if (![maxTokens, maxSteps].every(n => Number.isSafeInteger(n) && n > 0)) throw new Error('invalid harness budget');
|
|
22
22
|
let prompt = '';
|
|
23
23
|
for await (const chunk of process.stdin) { prompt += chunk; if (Buffer.byteLength(prompt) > 8 * 1024 * 1024) throw new Error('prompt exceeds 8 MiB'); }
|
|
24
|
-
const messages = [
|
|
25
|
-
|
|
24
|
+
const messages = [
|
|
25
|
+
{ role: 'system', content: `You are operating a live terminal in the task container. The current working directory is ${process.cwd()}. Use the bash tool to complete the user's task autonomously. Inspect the actual files and command output before choosing paths or actions. Keep commands and scripts short and focused. Run the commands needed to make the requested changes, check their results, and correct errors. Do not substitute instructions or placeholder commands for performing the work. Give your final response only after checking that the requested result exists.` },
|
|
26
|
+
{ role: 'user', content: prompt },
|
|
27
|
+
];
|
|
28
|
+
const tools = [{ type: 'function', function: { name: 'bash', description: 'Run a command in the task container, with a 120-second timeout and up to 4096 characters of output.', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'], additionalProperties: false } } }];
|
|
26
29
|
const emit = event => console.log(JSON.stringify(event));
|
|
27
30
|
let child;
|
|
28
31
|
const abort = new AbortController();
|
|
@@ -51,18 +54,54 @@ for (let step = 0; step < maxSteps; step++) {
|
|
|
51
54
|
if (choice.finish_reason !== 'tool_calls') throw new Error('missing tool terminal state');
|
|
52
55
|
for (const call of calls) {
|
|
53
56
|
abort.signal.throwIfAborted();
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
+
if (typeof call.id !== 'string') throw new Error('missing tool call identity');
|
|
58
|
+
let input; let toolError;
|
|
59
|
+
if (call.function?.name !== 'bash') toolError = 'Unknown tool. The available tool is bash, with arguments {"command":"shell command"}.';
|
|
60
|
+
else {
|
|
61
|
+
try { input = JSON.parse(call.function.arguments); }
|
|
62
|
+
catch { toolError = 'Invalid JSON arguments. bash expects {"command":"shell command"}.'; }
|
|
63
|
+
if (!toolError && (!input || typeof input !== 'object' || Array.isArray(input)
|
|
64
|
+
|| typeof input.command !== 'string' || Object.keys(input).join(',') !== 'command'))
|
|
65
|
+
toolError = 'Invalid arguments. bash expects only a string command field.';
|
|
66
|
+
}
|
|
67
|
+
if (toolError) {
|
|
68
|
+
const result = `Tool error: ${toolError} No command was executed.`;
|
|
69
|
+
messages.push({ role: 'tool', tool_call_id: call.id, content: result });
|
|
70
|
+
emit({ type: 'tool.rejected', call_id: call.id, name: call.function?.name ?? null, error: toolError });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
57
73
|
emit({ type: 'tool.started', call_id: call.id, name: 'bash', arguments: input });
|
|
58
74
|
const result = await new Promise((resolve, reject) => {
|
|
59
|
-
let output = ''; let truncated = false;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
let output = ''; let truncated = false; let timedOut = false;
|
|
76
|
+
const tool = spawn('/bin/bash', ['-lc', input.command], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
77
|
+
child = tool;
|
|
78
|
+
let settled = false; let postExitTimer;
|
|
79
|
+
const append = data => { if (settled) return; const remaining = 4096 - output.length; if (remaining > 0) output += data.toString().slice(0, remaining); if (data.length > remaining) truncated = true; };
|
|
80
|
+
tool.stdout.on('data', append); tool.stderr.on('data', append);
|
|
81
|
+
const timer = setTimeout(() => { timedOut = true; killTool(); }, 120000);
|
|
82
|
+
const finish = (code, background = false) => {
|
|
83
|
+
if (settled) return;
|
|
84
|
+
settled = true; clearTimeout(timer); clearTimeout(postExitTimer);
|
|
85
|
+
if (child === tool) child = undefined;
|
|
86
|
+
resolve(`exit=${code}\n${output}${truncated ? '\n[tool output truncated]' : ''}${timedOut ? '\n[command timed out after 120 seconds]' : ''}${background ? '\n[command exited; background process kept output streams open]' : ''}`);
|
|
87
|
+
};
|
|
88
|
+
tool.once('error', error => {
|
|
89
|
+
if (settled) return;
|
|
90
|
+
settled = true; clearTimeout(timer); clearTimeout(postExitTimer);
|
|
91
|
+
if (child === tool) child = undefined;
|
|
92
|
+
reject(error);
|
|
93
|
+
});
|
|
94
|
+
tool.once('exit', code => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
// Let ordinary output drain; daemons must not hold the command open.
|
|
97
|
+
postExitTimer = setTimeout(() => {
|
|
98
|
+
finish(code, true);
|
|
99
|
+
// Keep draining and discard later logs while the harness is alive.
|
|
100
|
+
// Unref lets it exit; Harbor owns cleanup of the task's processes.
|
|
101
|
+
tool.stdout.unref(); tool.stderr.unref(); tool.unref();
|
|
102
|
+
}, 1000);
|
|
103
|
+
});
|
|
104
|
+
tool.once('close', code => finish(code));
|
|
66
105
|
});
|
|
67
106
|
messages.push({ role: 'tool', tool_call_id: call.id, content: result });
|
|
68
107
|
emit({ type: 'tool.completed', call_id: call.id, output: result });
|