strom-research 1.1.1 → 1.2.0
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/README.md +3 -1
- package/assets/lang/cs.json +15 -0
- package/assets/lang/de.json +15 -0
- package/assets/method/core.md +6 -2
- package/assets/method/reading.md +2 -1
- package/assets/plugins/README.md +9 -2
- package/assets/plugins/gates/README.md +95 -0
- package/assets/plugins/gates/claude-usage/gate.json +10 -0
- package/assets/plugins/gates/claude-usage/gate.ts +125 -0
- package/dist/brief/brief.js +2 -0
- package/dist/cli/main.js +26 -1
- package/dist/cli/menu.js +22 -2
- package/dist/cli/ui.js +15 -0
- package/dist/commands/browse.js +2 -1
- package/dist/commands/gates.js +58 -0
- package/dist/commands/index.js +1 -0
- package/dist/commands/research.js +5 -1
- package/dist/commands/session.js +152 -15
- package/dist/commands/setup.js +11 -0
- package/dist/commands/sources.js +24 -0
- package/dist/core/clock.js +55 -0
- package/dist/core/config.js +12 -0
- package/dist/core/gate.js +175 -0
- package/dist/core/overview.js +2 -0
- package/dist/core/tree.js +1 -1
- package/dist/runners/antigravity.js +2 -1
- package/dist/runners/claude.js +157 -114
- package/dist/runners/codex.js +14 -1
- package/dist/runners/jsonl.js +63 -37
- package/dist/runners/opencode.js +7 -2
- package/dist/runners/script.js +56 -47
- package/package.json +1 -1
package/dist/core/tree.js
CHANGED
|
@@ -23,7 +23,7 @@ import * as git from "./git.js";
|
|
|
23
23
|
import { commitSeal, createKey, fingerprint, loadKey, sha256, signOp } from "./seal.js";
|
|
24
24
|
import { stringifyCanonical } from "./json.js";
|
|
25
25
|
export const TREE_FILE = "strom.json";
|
|
26
|
-
export const VERSION = "1.
|
|
26
|
+
export const VERSION = "1.2.0";
|
|
27
27
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
28
28
|
.strom/
|
|
29
29
|
/media/
|
|
@@ -26,7 +26,8 @@ export const antigravityRunner = {
|
|
|
26
26
|
id: "antigravity",
|
|
27
27
|
command: "agy",
|
|
28
28
|
run(opts) {
|
|
29
|
-
|
|
29
|
+
// Its events name no conversation strom could resume: stopped at the time limit, it is just stopped.
|
|
30
|
+
return runJsonLines("agy", antigravityArgs(opts), opts.env, opts, undefined, (msg, heard) => {
|
|
30
31
|
const kind = String(msg.event ?? msg.type ?? "");
|
|
31
32
|
if (kind === "result") {
|
|
32
33
|
const r = (msg.result ?? {});
|
package/dist/runners/claude.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// count), headless or not. Browser tools
|
|
13
13
|
// are on only when a connector fetches through the browser (--chrome), and off
|
|
14
14
|
// otherwise (--no-chrome), whatever Claude Code's own default is.
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
15
16
|
import { appendLog, looksLikeLimit, spawnAgent, stopTree } from "./runner.js";
|
|
16
17
|
function describeTool(block) {
|
|
17
18
|
const input = block.input ?? {};
|
|
@@ -60,122 +61,164 @@ export function headlessEnv(env, timeoutMs) {
|
|
|
60
61
|
BASH_MAX_TIMEOUT_MS: long,
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
let
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const content = (msg.message?.content ?? []);
|
|
105
|
-
for (const b of content) {
|
|
106
|
-
if (b.type === "tool_use")
|
|
107
|
-
opts.onProgress?.(describeTool(b));
|
|
108
|
-
else if (b.type === "text" && b.text?.trim())
|
|
109
|
-
opts.onProgress?.(b.text.trim().split("\n")[0].slice(0, 160));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (msg.type === "result") {
|
|
113
|
-
text = String(msg.result ?? "");
|
|
114
|
-
isError = Boolean(msg.is_error);
|
|
115
|
-
const u = (msg.usage ?? {});
|
|
116
|
-
if (u.input_tokens !== undefined)
|
|
117
|
-
metrics.inputTokens = u.input_tokens;
|
|
118
|
-
if (u.output_tokens !== undefined)
|
|
119
|
-
metrics.outputTokens = u.output_tokens;
|
|
120
|
-
if (u.cache_read_input_tokens !== undefined)
|
|
121
|
-
metrics.cacheReadTokens = u.cache_read_input_tokens;
|
|
122
|
-
if (u.cache_creation_input_tokens !== undefined)
|
|
123
|
-
metrics.cacheWriteTokens = u.cache_creation_input_tokens;
|
|
124
|
-
if (typeof msg.total_cost_usd === "number")
|
|
125
|
-
metrics.costUsd = msg.total_cost_usd;
|
|
126
|
-
if (typeof msg.num_turns === "number")
|
|
127
|
-
metrics.turns = msg.num_turns;
|
|
128
|
-
if (typeof msg.duration_ms === "number")
|
|
129
|
-
metrics.durationMs = msg.duration_ms;
|
|
130
|
-
for (const d of (msg.permission_denials ?? [])) {
|
|
131
|
-
const input = d.tool_input ?? {};
|
|
132
|
-
const what = typeof input.command === "string" ? input.command : typeof input.file_path === "string" ? input.file_path : typeof input.url === "string" ? input.url : "";
|
|
133
|
-
denied.push(`${d.tool_name ?? "tool"}${what ? `: ${String(what).split("\n")[0].slice(0, 120)}` : ""}`);
|
|
134
|
-
}
|
|
135
|
-
if (denied.length)
|
|
136
|
-
metrics.denied = denied.length;
|
|
64
|
+
function attempt(opts, args, input, timeoutMs) {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const child = spawnAgent("claude", args, {
|
|
67
|
+
cwd: opts.cwd,
|
|
68
|
+
env: opts.interactive ? opts.env : headlessEnv(opts.env, opts.timeoutMs),
|
|
69
|
+
stdio: opts.interactive ? "inherit" : ["pipe", "pipe", "pipe"],
|
|
70
|
+
windowsHide: !opts.interactive,
|
|
71
|
+
});
|
|
72
|
+
// The brief goes in on stdin: no limit on its length on any platform.
|
|
73
|
+
if (!opts.interactive)
|
|
74
|
+
child.stdin?.end(input);
|
|
75
|
+
let timedOut = false;
|
|
76
|
+
const timer = timeoutMs
|
|
77
|
+
? setTimeout(() => {
|
|
78
|
+
timedOut = true;
|
|
79
|
+
stopTree(child);
|
|
80
|
+
}, timeoutMs)
|
|
81
|
+
: undefined;
|
|
82
|
+
const abort = () => stopTree(child);
|
|
83
|
+
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
84
|
+
const a = { code: null, timedOut: false, metrics: {}, reported: false, text: "", stderr: "", isError: false, denied: [] };
|
|
85
|
+
let buffer = "";
|
|
86
|
+
const handle = (line) => {
|
|
87
|
+
if (!line.trim())
|
|
88
|
+
return;
|
|
89
|
+
let msg;
|
|
90
|
+
try {
|
|
91
|
+
msg = JSON.parse(line);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (msg.type === "system" && msg.subtype === "init" && typeof msg.model === "string")
|
|
97
|
+
a.metrics.model = msg.model;
|
|
98
|
+
if (msg.type === "assistant") {
|
|
99
|
+
const content = (msg.message?.content ?? []);
|
|
100
|
+
for (const b of content) {
|
|
101
|
+
if (b.type === "tool_use")
|
|
102
|
+
opts.onProgress?.(describeTool(b));
|
|
103
|
+
else if (b.type === "text" && b.text?.trim())
|
|
104
|
+
opts.onProgress?.(b.text.trim().split("\n")[0].slice(0, 160));
|
|
137
105
|
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
106
|
+
}
|
|
107
|
+
if (msg.type === "result") {
|
|
108
|
+
a.reported = true;
|
|
109
|
+
a.text = String(msg.result ?? "");
|
|
110
|
+
a.isError = Boolean(msg.is_error);
|
|
111
|
+
const m = a.metrics;
|
|
112
|
+
const u = (msg.usage ?? {});
|
|
113
|
+
if (u.input_tokens !== undefined)
|
|
114
|
+
m.inputTokens = u.input_tokens;
|
|
115
|
+
if (u.output_tokens !== undefined)
|
|
116
|
+
m.outputTokens = u.output_tokens;
|
|
117
|
+
if (u.cache_read_input_tokens !== undefined)
|
|
118
|
+
m.cacheReadTokens = u.cache_read_input_tokens;
|
|
119
|
+
if (u.cache_creation_input_tokens !== undefined)
|
|
120
|
+
m.cacheWriteTokens = u.cache_creation_input_tokens;
|
|
121
|
+
if (typeof msg.total_cost_usd === "number")
|
|
122
|
+
m.costUsd = msg.total_cost_usd;
|
|
123
|
+
if (typeof msg.num_turns === "number")
|
|
124
|
+
m.turns = msg.num_turns;
|
|
125
|
+
if (typeof msg.duration_ms === "number")
|
|
126
|
+
m.durationMs = msg.duration_ms;
|
|
127
|
+
for (const d of (msg.permission_denials ?? [])) {
|
|
128
|
+
const input = d.tool_input ?? {};
|
|
129
|
+
const what = typeof input.command === "string" ? input.command : typeof input.file_path === "string" ? input.file_path : typeof input.url === "string" ? input.url : "";
|
|
130
|
+
a.denied.push(`${d.tool_name ?? "tool"}${what ? `: ${String(what).split("\n")[0].slice(0, 120)}` : ""}`);
|
|
147
131
|
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
: /log ?in|authenticat|api key|unauthori[sz]ed/i.test(all) && (code ?? 0) !== 0
|
|
173
|
-
? "auth"
|
|
174
|
-
: (code ?? 0) !== 0 || isError
|
|
175
|
-
? "error"
|
|
176
|
-
: "ok";
|
|
177
|
-
resolve({ exitCode: code ?? 1, outcome, text: text || stderr.trim(), metrics, ...(limit.resumeAt ? { resumeAt: limit.resumeAt } : {}), ...(denied.length ? { denied } : {}) });
|
|
178
|
-
});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
child.stdout?.on("data", (d) => {
|
|
135
|
+
const s = d.toString("utf8");
|
|
136
|
+
appendLog(opts.logFile, s);
|
|
137
|
+
buffer += s;
|
|
138
|
+
let nl;
|
|
139
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
140
|
+
handle(buffer.slice(0, nl));
|
|
141
|
+
buffer = buffer.slice(nl + 1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
child.stderr?.on("data", (d) => {
|
|
145
|
+
a.stderr += d.toString("utf8");
|
|
146
|
+
appendLog(opts.logFile, d.toString("utf8"));
|
|
147
|
+
});
|
|
148
|
+
const end = () => {
|
|
149
|
+
if (timer)
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
opts.signal?.removeEventListener("abort", abort);
|
|
152
|
+
};
|
|
153
|
+
child.on("error", (err) => {
|
|
154
|
+
end();
|
|
155
|
+
resolve({ ...a, failed: String(err.message) });
|
|
179
156
|
});
|
|
157
|
+
child.on("close", (code) => {
|
|
158
|
+
end();
|
|
159
|
+
if (buffer)
|
|
160
|
+
handle(buffer);
|
|
161
|
+
resolve({ ...a, code, timedOut });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/** Two starts as one session: the time's metrics added up. */
|
|
166
|
+
function together(first, then) {
|
|
167
|
+
const sum = (k) => first[k] === undefined && then[k] === undefined ? {} : { [k]: (first[k] ?? 0) + (then[k] ?? 0) };
|
|
168
|
+
return {
|
|
169
|
+
...((first.model ?? then.model) ? { model: (first.model ?? then.model) } : {}),
|
|
170
|
+
...sum("inputTokens"),
|
|
171
|
+
...sum("outputTokens"),
|
|
172
|
+
...sum("cacheReadTokens"),
|
|
173
|
+
...sum("cacheWriteTokens"),
|
|
174
|
+
...sum("costUsd"),
|
|
175
|
+
...sum("turns"),
|
|
176
|
+
...sum("durationMs"),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export const claudeRunner = {
|
|
180
|
+
id: "claude",
|
|
181
|
+
command: "claude",
|
|
182
|
+
async run(opts) {
|
|
183
|
+
// A headless session has an id of strom's choosing: stopped at its limit, it is resumed to write down what it found.
|
|
184
|
+
const id = opts.interactive ? undefined : randomUUID();
|
|
185
|
+
const args = claudeArgs(opts);
|
|
186
|
+
let a = await attempt(opts, id ? [...args, "--session-id", id] : args, opts.prompt, opts.timeoutMs);
|
|
187
|
+
if (a.failed)
|
|
188
|
+
return { exitCode: 127, outcome: "error", text: a.failed, metrics: a.metrics };
|
|
189
|
+
const timedOut = a.timedOut;
|
|
190
|
+
let metrics = { ...a.metrics, ...(a.reported ? {} : { costPartial: true }) };
|
|
191
|
+
const denied = [...a.denied];
|
|
192
|
+
if (timedOut) {
|
|
193
|
+
const min = Math.round(opts.timeoutMs / 60000);
|
|
194
|
+
if (opts.wrapUp && id && !opts.signal?.aborted) {
|
|
195
|
+
opts.onProgress?.(`time limit reached (${min} min) — the agent gets ${Math.round(opts.wrapUp.ms / 60000)} min to write down what it found`);
|
|
196
|
+
const more = await attempt(opts, [...args, "--resume", id], opts.wrapUp.prompt, opts.wrapUp.ms);
|
|
197
|
+
if (more.timedOut)
|
|
198
|
+
opts.onProgress?.("the agent did not finish in time — stopped");
|
|
199
|
+
metrics = { ...together(metrics, more.metrics), costPartial: true };
|
|
200
|
+
denied.push(...more.denied);
|
|
201
|
+
a = { ...more, code: more.code, stderr: a.stderr + more.stderr, text: more.text || a.text };
|
|
202
|
+
}
|
|
203
|
+
else
|
|
204
|
+
opts.onProgress?.(`time limit reached (${min} min) — the agent was stopped`);
|
|
205
|
+
}
|
|
206
|
+
if (denied.length)
|
|
207
|
+
metrics.denied = denied.length;
|
|
208
|
+
const all = `${a.text}\n${a.stderr}`;
|
|
209
|
+
const limit = looksLikeLimit(all);
|
|
210
|
+
const code = a.code;
|
|
211
|
+
const outcome = opts.signal?.aborted
|
|
212
|
+
? "stopped"
|
|
213
|
+
: timedOut
|
|
214
|
+
? "timeout"
|
|
215
|
+
: limit.limit
|
|
216
|
+
? "limit"
|
|
217
|
+
: /log ?in|authenticat|api key|unauthori[sz]ed/i.test(all) && (code ?? 0) !== 0
|
|
218
|
+
? "auth"
|
|
219
|
+
: (code ?? 0) !== 0 || a.isError
|
|
220
|
+
? "error"
|
|
221
|
+
: "ok";
|
|
222
|
+
return { exitCode: code ?? 1, outcome, text: a.text || a.stderr.trim(), metrics, ...(limit.resumeAt ? { resumeAt: limit.resumeAt } : {}), ...(denied.length ? { denied } : {}) };
|
|
180
223
|
},
|
|
181
224
|
};
|
package/dist/runners/codex.js
CHANGED
|
@@ -12,11 +12,24 @@ export function codexArgs(opts) {
|
|
|
12
12
|
: ["--sandbox", "workspace-write", "-c", "sandbox_workspace_write.network_access=true", ...(opts.shared ? ["--add-dir", opts.shared] : [])];
|
|
13
13
|
return ["exec", "--json", "--skip-git-repo-check", ...sandbox, ...(opts.model ? ["--model", opts.model] : []), ...(opts.extraArgs ?? []), "-"];
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Arguments that resume a headless session with a message on stdin (exported for tests). `exec resume` has no
|
|
17
|
+
* --sandbox or --add-dir: the same sandbox is set through its configuration.
|
|
18
|
+
*/
|
|
19
|
+
export function codexResumeArgs(id, opts) {
|
|
20
|
+
const sandbox = opts.permissions === "full"
|
|
21
|
+
? ["--dangerously-bypass-approvals-and-sandbox"]
|
|
22
|
+
: ["-c", 'sandbox_mode="workspace-write"', "-c", "sandbox_workspace_write.network_access=true", ...(opts.shared ? ["-c", `sandbox_workspace_write.writable_roots=[${JSON.stringify(opts.shared)}]`] : [])];
|
|
23
|
+
return ["exec", "resume", "--json", "--skip-git-repo-check", ...sandbox, ...(opts.model ? ["--model", opts.model] : []), id, "-"];
|
|
24
|
+
}
|
|
15
25
|
export const codexRunner = {
|
|
16
26
|
id: "codex",
|
|
17
27
|
command: "codex",
|
|
18
28
|
run(opts) {
|
|
19
|
-
|
|
29
|
+
const resume = (id, message) => ({ args: codexResumeArgs(id, opts), input: message });
|
|
30
|
+
return runJsonLines("codex", codexArgs(opts), opts.env, opts, resume, (msg, heard) => {
|
|
31
|
+
if (msg.type === "thread.started" && typeof msg.thread_id === "string")
|
|
32
|
+
heard.sessionId = msg.thread_id;
|
|
20
33
|
const item = msg.item;
|
|
21
34
|
if (msg.type === "item.started" && item?.type === "command_execution" && item.command)
|
|
22
35
|
opts.onProgress?.(`$ ${item.command.replace(/^\S*sh -lc '(.*)'$/s, "$1").split("\n")[0].slice(0, 140)}`);
|
package/dist/runners/jsonl.js
CHANGED
|
@@ -2,23 +2,69 @@
|
|
|
2
2
|
// `--output-format stream-json`): the brief on stdin, one event per line on
|
|
3
3
|
// stdout, a time limit, Ctrl-C, the subscription limit and a missing login
|
|
4
4
|
// recognised — the same for every such agent; each runner only reads its own
|
|
5
|
-
// events.
|
|
5
|
+
// events. Stopped at its time limit, an agent that can resume its session gets a
|
|
6
|
+
// few minutes to write down what it found (RunOptions.wrapUp).
|
|
6
7
|
import { appendLog, looksLikeLimit, spawnAgent, stopTree } from "./runner.js";
|
|
7
|
-
export function runJsonLines(command, args, env, opts, onEvent) {
|
|
8
|
+
export async function runJsonLines(command, args, env, opts, resume, onEvent) {
|
|
9
|
+
const started = Date.now();
|
|
10
|
+
const heard = { metrics: {}, text: "", isError: false, denied: [] };
|
|
11
|
+
let a = await attempt(command, args, opts.prompt, env, opts, opts.timeoutMs, heard, onEvent);
|
|
12
|
+
if (a.failed)
|
|
13
|
+
return { exitCode: 127, outcome: "error", text: a.failed, metrics: heard.metrics };
|
|
14
|
+
const timedOut = a.timedOut;
|
|
15
|
+
if (timedOut) {
|
|
16
|
+
const min = Math.round(opts.timeoutMs / 60000);
|
|
17
|
+
if (opts.wrapUp && resume && heard.sessionId && !opts.signal?.aborted) {
|
|
18
|
+
opts.onProgress?.(`time limit reached (${min} min) — the agent gets ${Math.round(opts.wrapUp.ms / 60000)} min to write down what it found`);
|
|
19
|
+
const r = resume(heard.sessionId, opts.wrapUp.prompt);
|
|
20
|
+
const more = await attempt(command, r.args, r.input, env, opts, opts.wrapUp.ms, heard, onEvent);
|
|
21
|
+
if (more.timedOut)
|
|
22
|
+
opts.onProgress?.("the agent did not finish in time — stopped");
|
|
23
|
+
a = { ...more, stderr: a.stderr + more.stderr };
|
|
24
|
+
}
|
|
25
|
+
else
|
|
26
|
+
opts.onProgress?.(`time limit reached (${min} min) — the agent was stopped`);
|
|
27
|
+
}
|
|
28
|
+
const code = a.code;
|
|
29
|
+
heard.metrics.durationMs ??= Date.now() - started;
|
|
30
|
+
if (heard.denied.length)
|
|
31
|
+
heard.metrics.denied = heard.denied.length;
|
|
32
|
+
const all = `${heard.text}\n${a.stderr}`;
|
|
33
|
+
const limit = looksLikeLimit(all);
|
|
34
|
+
const outcome = opts.signal?.aborted
|
|
35
|
+
? "stopped"
|
|
36
|
+
: timedOut
|
|
37
|
+
? "timeout"
|
|
38
|
+
: limit.limit
|
|
39
|
+
? "limit"
|
|
40
|
+
: /log ?in|authenticat|api[ _]?key|unauthori[sz]ed|GEMINI_API_KEY/i.test(all) && ((code ?? 0) !== 0 || heard.isError)
|
|
41
|
+
? "auth"
|
|
42
|
+
: (code ?? 0) !== 0 || heard.isError
|
|
43
|
+
? "error"
|
|
44
|
+
: "ok";
|
|
45
|
+
return {
|
|
46
|
+
exitCode: code ?? 1,
|
|
47
|
+
outcome,
|
|
48
|
+
text: heard.text || a.stderr.trim(),
|
|
49
|
+
metrics: heard.metrics,
|
|
50
|
+
...(limit.resumeAt ? { resumeAt: limit.resumeAt } : {}),
|
|
51
|
+
...(heard.denied.length ? { denied: heard.denied } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** One start of the agent; its events go to `heard`. */
|
|
55
|
+
function attempt(command, args, input, env, opts, timeoutMs, heard, onEvent) {
|
|
8
56
|
return new Promise((resolve) => {
|
|
9
|
-
const started = Date.now();
|
|
10
57
|
const child = spawnAgent(command, args, { cwd: opts.cwd, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
11
|
-
child.stdin?.end(
|
|
58
|
+
child.stdin?.end(input);
|
|
12
59
|
let timedOut = false;
|
|
13
|
-
const timer =
|
|
60
|
+
const timer = timeoutMs
|
|
14
61
|
? setTimeout(() => {
|
|
15
62
|
timedOut = true;
|
|
16
|
-
opts.onProgress?.(`time limit reached (${Math.round(opts.timeoutMs / 60000)} min) — stopping the agent`);
|
|
17
63
|
stopTree(child);
|
|
18
|
-
},
|
|
64
|
+
}, timeoutMs)
|
|
19
65
|
: undefined;
|
|
20
|
-
|
|
21
|
-
|
|
66
|
+
const abort = () => stopTree(child);
|
|
67
|
+
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
22
68
|
let buffer = "";
|
|
23
69
|
const handle = (line) => {
|
|
24
70
|
if (!line.trim())
|
|
@@ -47,40 +93,20 @@ export function runJsonLines(command, args, env, opts, onEvent) {
|
|
|
47
93
|
stderr += d.toString("utf8");
|
|
48
94
|
appendLog(opts.logFile, d.toString("utf8"));
|
|
49
95
|
});
|
|
50
|
-
|
|
96
|
+
const end = () => {
|
|
51
97
|
if (timer)
|
|
52
98
|
clearTimeout(timer);
|
|
53
|
-
|
|
99
|
+
opts.signal?.removeEventListener("abort", abort);
|
|
100
|
+
};
|
|
101
|
+
child.on("error", (err) => {
|
|
102
|
+
end();
|
|
103
|
+
resolve({ code: 127, timedOut, stderr, failed: String(err.message) });
|
|
54
104
|
});
|
|
55
105
|
child.on("close", (code) => {
|
|
56
|
-
|
|
57
|
-
clearTimeout(timer);
|
|
106
|
+
end();
|
|
58
107
|
if (buffer)
|
|
59
108
|
handle(buffer);
|
|
60
|
-
|
|
61
|
-
if (heard.denied.length)
|
|
62
|
-
heard.metrics.denied = heard.denied.length;
|
|
63
|
-
const all = `${heard.text}\n${stderr}`;
|
|
64
|
-
const limit = looksLikeLimit(all);
|
|
65
|
-
const outcome = opts.signal?.aborted
|
|
66
|
-
? "stopped"
|
|
67
|
-
: timedOut
|
|
68
|
-
? "timeout"
|
|
69
|
-
: limit.limit
|
|
70
|
-
? "limit"
|
|
71
|
-
: /log ?in|authenticat|api[ _]?key|unauthori[sz]ed|GEMINI_API_KEY/i.test(all) && ((code ?? 0) !== 0 || heard.isError)
|
|
72
|
-
? "auth"
|
|
73
|
-
: (code ?? 0) !== 0 || heard.isError
|
|
74
|
-
? "error"
|
|
75
|
-
: "ok";
|
|
76
|
-
resolve({
|
|
77
|
-
exitCode: code ?? 1,
|
|
78
|
-
outcome,
|
|
79
|
-
text: heard.text || stderr.trim(),
|
|
80
|
-
metrics: heard.metrics,
|
|
81
|
-
...(limit.resumeAt ? { resumeAt: limit.resumeAt } : {}),
|
|
82
|
-
...(heard.denied.length ? { denied: heard.denied } : {}),
|
|
83
|
-
});
|
|
109
|
+
resolve({ code, timedOut, stderr });
|
|
84
110
|
});
|
|
85
111
|
});
|
|
86
112
|
}
|
package/dist/runners/opencode.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// evidence and git denied); what they would ask about is refused in a run
|
|
5
5
|
// nobody watches — under ask and auto that is all; full adds --auto
|
|
6
6
|
// (everything the rules do not deny). Events are JSON lines: text, tool_use,
|
|
7
|
-
// step_start, step_finish (tokens and cost of a step), error
|
|
7
|
+
// step_start, step_finish (tokens and cost of a step), error; each names its
|
|
8
|
+
// session (sessionID), which `--session` resumes.
|
|
8
9
|
import { runJsonLines } from "./jsonl.js";
|
|
9
10
|
/** Command-line arguments of a headless run (exported for tests). */
|
|
10
11
|
export function opencodeArgs(opts) {
|
|
@@ -16,8 +17,12 @@ export const opencodeRunner = {
|
|
|
16
17
|
run(opts) {
|
|
17
18
|
const args = opencodeArgs({ ...opts, kickoff: "Your brief is above. Work only through `strom` in this folder, as it says." });
|
|
18
19
|
const tokens = { input: 0, output: 0, cacheRead: 0 };
|
|
19
|
-
|
|
20
|
+
// Resumed: its session by id, the message as the prompt.
|
|
21
|
+
const resume = (id, message) => ({ args: [...opencodeArgs({ ...opts, kickoff: message }).slice(0, -1), "--session", id, message], input: "" });
|
|
22
|
+
return runJsonLines("opencode", args, opts.env, opts, resume, (msg, heard) => {
|
|
20
23
|
const part = (msg.part ?? {});
|
|
24
|
+
if (typeof msg.sessionID === "string")
|
|
25
|
+
heard.sessionId = msg.sessionID;
|
|
21
26
|
if (msg.type === "tool_use") {
|
|
22
27
|
const state = (part.state ?? {});
|
|
23
28
|
const input = state.input ?? {};
|
package/dist/runners/script.js
CHANGED
|
@@ -7,52 +7,61 @@ import { appendLog, looksLikeLimit, stopTree } from "./runner.js";
|
|
|
7
7
|
export const scriptRunner = {
|
|
8
8
|
id: "script",
|
|
9
9
|
command: process.execPath,
|
|
10
|
-
run(opts) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
19
|
-
});
|
|
20
|
-
child.stdin.end(opts.prompt);
|
|
21
|
-
let timedOut = false;
|
|
22
|
-
const timer = opts.timeoutMs
|
|
23
|
-
? setTimeout(() => {
|
|
24
|
-
timedOut = true;
|
|
25
|
-
stopTree(child);
|
|
26
|
-
}, opts.timeoutMs)
|
|
27
|
-
: undefined;
|
|
28
|
-
opts.signal?.addEventListener("abort", () => stopTree(child), { once: true });
|
|
29
|
-
let out = "";
|
|
30
|
-
const denied = [];
|
|
31
|
-
const on = (d) => {
|
|
32
|
-
for (const l of d.toString("utf8").split("\n"))
|
|
33
|
-
if (l.startsWith("denied: "))
|
|
34
|
-
denied.push(l.slice(8).trim());
|
|
35
|
-
out += d.toString("utf8");
|
|
36
|
-
appendLog(opts.logFile, d.toString("utf8"));
|
|
37
|
-
for (const l of d.toString("utf8").split("\n"))
|
|
38
|
-
if (l.trim())
|
|
39
|
-
opts.onProgress?.(l.trim().slice(0, 160));
|
|
40
|
-
};
|
|
41
|
-
child.stdout.on("data", on);
|
|
42
|
-
child.stderr.on("data", on);
|
|
43
|
-
child.on("close", (code) => {
|
|
44
|
-
if (timer)
|
|
45
|
-
clearTimeout(timer);
|
|
46
|
-
const limit = looksLikeLimit(out);
|
|
47
|
-
resolve({
|
|
48
|
-
exitCode: code ?? 1,
|
|
49
|
-
outcome: opts.signal?.aborted ? "stopped" : timedOut ? "timeout" : limit.limit ? "limit" : code === 0 ? "ok" : "error",
|
|
50
|
-
text: out.trim().split("\n").slice(-5).join("\n"),
|
|
51
|
-
metrics: { turns: 1, ...(denied.length ? { denied: denied.length } : {}) },
|
|
52
|
-
...(limit.resumeAt ? { resumeAt: limit.resumeAt } : {}),
|
|
53
|
-
...(denied.length ? { denied } : {}),
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
});
|
|
10
|
+
async run(opts) {
|
|
11
|
+
const first = await once(opts, opts.prompt, opts.timeoutMs, false);
|
|
12
|
+
if (first.outcome !== "timeout" || !opts.wrapUp || opts.signal?.aborted)
|
|
13
|
+
return first;
|
|
14
|
+
// Stopped at its limit: resumed with the message to write down what it found (STROM_WRAP_UP=1).
|
|
15
|
+
opts.onProgress?.("time limit reached — the agent gets time to write down what it found");
|
|
16
|
+
const more = await once(opts, opts.wrapUp.prompt, opts.wrapUp.ms, true);
|
|
17
|
+
return { ...more, outcome: "timeout" };
|
|
57
18
|
},
|
|
58
19
|
};
|
|
20
|
+
function once(opts, prompt, timeoutMs, wrapUp) {
|
|
21
|
+
const script = opts.env.STROM_RUNNER_SCRIPT;
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
if (!script)
|
|
24
|
+
return resolve({ exitCode: 2, outcome: "error", text: "STROM_RUNNER_SCRIPT is not set", metrics: {} });
|
|
25
|
+
const child = spawn(process.execPath, [script, ...(opts.extraArgs ?? [])], {
|
|
26
|
+
cwd: opts.cwd,
|
|
27
|
+
env: { ...opts.env, STROM_PROMPT: prompt, ...(wrapUp ? { STROM_WRAP_UP: "1" } : {}) },
|
|
28
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
29
|
+
});
|
|
30
|
+
child.stdin.end(prompt);
|
|
31
|
+
let timedOut = false;
|
|
32
|
+
const timer = timeoutMs
|
|
33
|
+
? setTimeout(() => {
|
|
34
|
+
timedOut = true;
|
|
35
|
+
stopTree(child);
|
|
36
|
+
}, timeoutMs)
|
|
37
|
+
: undefined;
|
|
38
|
+
opts.signal?.addEventListener("abort", () => stopTree(child), { once: true });
|
|
39
|
+
let out = "";
|
|
40
|
+
const denied = [];
|
|
41
|
+
const on = (d) => {
|
|
42
|
+
for (const l of d.toString("utf8").split("\n"))
|
|
43
|
+
if (l.startsWith("denied: "))
|
|
44
|
+
denied.push(l.slice(8).trim());
|
|
45
|
+
out += d.toString("utf8");
|
|
46
|
+
appendLog(opts.logFile, d.toString("utf8"));
|
|
47
|
+
for (const l of d.toString("utf8").split("\n"))
|
|
48
|
+
if (l.trim())
|
|
49
|
+
opts.onProgress?.(l.trim().slice(0, 160));
|
|
50
|
+
};
|
|
51
|
+
child.stdout.on("data", on);
|
|
52
|
+
child.stderr.on("data", on);
|
|
53
|
+
child.on("close", (code) => {
|
|
54
|
+
if (timer)
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
const limit = looksLikeLimit(out);
|
|
57
|
+
resolve({
|
|
58
|
+
exitCode: code ?? 1,
|
|
59
|
+
outcome: opts.signal?.aborted ? "stopped" : timedOut ? "timeout" : limit.limit ? "limit" : code === 0 ? "ok" : "error",
|
|
60
|
+
text: out.trim().split("\n").slice(-5).join("\n"),
|
|
61
|
+
metrics: { turns: 1, ...(denied.length ? { denied: denied.length } : {}) },
|
|
62
|
+
...(limit.resumeAt ? { resumeAt: limit.resumeAt } : {}),
|
|
63
|
+
...(denied.length ? { denied } : {}),
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|