@itsaplan/runner 0.5.3 → 0.7.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 +150 -48
- package/dist/cli.js +1039 -741
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,225 +3,753 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
5
|
|
|
6
|
-
// src/
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
var TOOL_TEXT_LIMIT = 32000;
|
|
6
|
+
// src/client.ts
|
|
7
|
+
var REQUEST_TIMEOUT_MS = 30000;
|
|
8
|
+
var CHAT_CLAIM_TIMEOUT_MS = 35000;
|
|
10
9
|
|
|
11
|
-
class
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
messageId;
|
|
17
|
-
queued = [];
|
|
18
|
-
text = "";
|
|
19
|
-
started = false;
|
|
20
|
-
line = "";
|
|
21
|
-
sessionId = null;
|
|
22
|
-
openTextPart = "";
|
|
23
|
-
openToolCalls = new Set;
|
|
24
|
-
closedToolCalls = new Set;
|
|
25
|
-
sawPartialText = false;
|
|
26
|
-
sawAnyText = false;
|
|
27
|
-
sending = Promise.resolve();
|
|
28
|
-
usage;
|
|
29
|
-
constructor(format, threadId, runId, send) {
|
|
30
|
-
this.format = format;
|
|
31
|
-
this.threadId = threadId;
|
|
32
|
-
this.runId = runId;
|
|
33
|
-
this.send = send;
|
|
34
|
-
this.usage = new UsageReader(format);
|
|
35
|
-
this.messageId = `msg-${runId}`;
|
|
36
|
-
this.queued.push({ type: "RUN_STARTED", threadId, runId });
|
|
37
|
-
}
|
|
38
|
-
write(chunk) {
|
|
39
|
-
if (this.format === "text") {
|
|
40
|
-
this.appendText(chunk);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
this.line += chunk;
|
|
44
|
-
const lines = this.line.split(`
|
|
45
|
-
`);
|
|
46
|
-
this.line = lines.pop() ?? "";
|
|
47
|
-
for (const line of lines)
|
|
48
|
-
this.readLine(line);
|
|
49
|
-
}
|
|
50
|
-
startedSession() {
|
|
51
|
-
return this.sessionId;
|
|
52
|
-
}
|
|
53
|
-
contextUsage() {
|
|
54
|
-
return this.usage.value();
|
|
10
|
+
class RequestError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(status, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
55
15
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class Client {
|
|
19
|
+
config;
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.config = config;
|
|
60
22
|
}
|
|
61
|
-
async
|
|
62
|
-
this.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
23
|
+
async post(path, body, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
24
|
+
const res = await fetch(`${this.config.url}${path}`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"x-api-key": this.config.apiKey,
|
|
28
|
+
...body === undefined ? {} : { "content-type": "application/json" }
|
|
29
|
+
},
|
|
30
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
31
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
throw new RequestError(res.status, `POST ${path} failed with ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
72
35
|
}
|
|
36
|
+
return res;
|
|
73
37
|
}
|
|
74
|
-
async
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
38
|
+
async claim() {
|
|
39
|
+
const res = await this.post("/agent-runs/claim");
|
|
40
|
+
const body = await res.json();
|
|
41
|
+
return body.run;
|
|
78
42
|
}
|
|
79
|
-
async
|
|
80
|
-
this.
|
|
81
|
-
this.queued.push({ type: "RUN_ERROR", message });
|
|
82
|
-
await this.flush();
|
|
43
|
+
async heartbeat(runId, attempt) {
|
|
44
|
+
await this.post(`/agent-runs/${runId}/heartbeat`, attempt == null ? undefined : { attempt });
|
|
83
45
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.readLine(this.line);
|
|
87
|
-
this.line = "";
|
|
88
|
-
}
|
|
89
|
-
if (!this.sawAnyText && fallback)
|
|
90
|
-
this.appendText(fallback);
|
|
91
|
-
this.drainText();
|
|
92
|
-
if (this.started)
|
|
93
|
-
this.queued.push({ type: "TEXT_MESSAGE_END", messageId: this.messageId });
|
|
94
|
-
this.started = false;
|
|
46
|
+
async report(runId, result) {
|
|
47
|
+
await this.post(`/agent-runs/${runId}/result`, result);
|
|
95
48
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
49
|
+
async runEvents(run, offset, events, sessionId) {
|
|
50
|
+
await this.post(`/agent-runs/${run.id}/events`, {
|
|
51
|
+
attempt: run.attempts,
|
|
52
|
+
offset,
|
|
53
|
+
events,
|
|
54
|
+
...sessionId && { sessionId }
|
|
55
|
+
});
|
|
103
56
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.started = true;
|
|
109
|
-
this.queued.push({
|
|
110
|
-
type: "TEXT_MESSAGE_START",
|
|
111
|
-
messageId: this.messageId,
|
|
112
|
-
role: "assistant"
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
while (this.text.length > 0) {
|
|
116
|
-
const delta = this.text.slice(0, DELTA_LIMIT);
|
|
117
|
-
this.text = this.text.slice(DELTA_LIMIT);
|
|
118
|
-
this.queued.push({ type: "TEXT_MESSAGE_CONTENT", messageId: this.messageId, delta });
|
|
119
|
-
}
|
|
57
|
+
async claimChat() {
|
|
58
|
+
const res = await this.post("/agent-chats/claim", undefined, CHAT_CLAIM_TIMEOUT_MS);
|
|
59
|
+
const body = await res.json();
|
|
60
|
+
return body.message;
|
|
120
61
|
}
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
parsed = JSON.parse(trimmed);
|
|
128
|
-
} catch {
|
|
129
|
-
this.appendText(`${trimmed}
|
|
130
|
-
`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (!parsed || typeof parsed !== "object")
|
|
134
|
-
return;
|
|
135
|
-
this.usage.readLine(parsed);
|
|
136
|
-
switch (this.format) {
|
|
137
|
-
case "claude-stream-json":
|
|
138
|
-
this.readClaudeLine(parsed);
|
|
139
|
-
break;
|
|
140
|
-
case "codex-jsonl":
|
|
141
|
-
this.readCodexLine(parsed);
|
|
142
|
-
break;
|
|
143
|
-
case "opencode-json":
|
|
144
|
-
this.readOpencodeLine(parsed);
|
|
145
|
-
break;
|
|
146
|
-
case "antigravity-stream-json":
|
|
147
|
-
this.readAntigravityLine(parsed);
|
|
148
|
-
break;
|
|
149
|
-
case "copilot-json":
|
|
150
|
-
this.readCopilotLine(parsed);
|
|
151
|
-
break;
|
|
152
|
-
case "pi-json":
|
|
153
|
-
this.readPiLine(parsed);
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
62
|
+
async chatEvents(messageId, events, sessionId) {
|
|
63
|
+
const res = await this.post(`/agent-chats/${messageId}/events`, {
|
|
64
|
+
events,
|
|
65
|
+
...sessionId && { sessionId }
|
|
66
|
+
});
|
|
67
|
+
return canceled(res);
|
|
156
68
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.sessionId ??= message.id;
|
|
160
|
-
switch (message.type) {
|
|
161
|
-
case "message_update": {
|
|
162
|
-
const event = message.assistantMessageEvent;
|
|
163
|
-
if (event?.type === "text_delta" && event.delta) {
|
|
164
|
-
this.sawPartialText = true;
|
|
165
|
-
this.appendText(event.delta);
|
|
166
|
-
}
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
case "tool_execution_start":
|
|
170
|
-
if (message.toolCallId && !this.openToolCalls.has(message.toolCallId)) {
|
|
171
|
-
this.openToolCalls.add(message.toolCallId);
|
|
172
|
-
this.pushToolCall(message.toolCallId, message.toolName ?? "tool", JSON.stringify(message.args ?? {}));
|
|
173
|
-
}
|
|
174
|
-
return;
|
|
175
|
-
case "tool_execution_end": {
|
|
176
|
-
if (!message.toolCallId || this.closedToolCalls.has(message.toolCallId))
|
|
177
|
-
return;
|
|
178
|
-
this.closedToolCalls.add(message.toolCallId);
|
|
179
|
-
this.pushToolResult(message.toolCallId, piResultText(message.result));
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
case "message_end":
|
|
183
|
-
if (message.message?.role === "assistant" && !this.sawAnyText && message.message.content) {
|
|
184
|
-
const body = textOfResult(message.message.content);
|
|
185
|
-
if (body)
|
|
186
|
-
this.appendText(body);
|
|
187
|
-
}
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
69
|
+
async chatHeartbeat(messageId) {
|
|
70
|
+
return canceled(await this.post(`/agent-chats/${messageId}/heartbeat`));
|
|
190
71
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
switch (message.type) {
|
|
194
|
-
case "result":
|
|
195
|
-
if (message.sessionId)
|
|
196
|
-
this.sessionId ??= message.sessionId;
|
|
197
|
-
return;
|
|
198
|
-
case "assistant.message_delta":
|
|
199
|
-
if (data?.deltaContent) {
|
|
200
|
-
this.sawPartialText = true;
|
|
201
|
-
this.appendText(data.deltaContent);
|
|
202
|
-
}
|
|
203
|
-
return;
|
|
204
|
-
case "assistant.message":
|
|
205
|
-
if (data?.content && !this.sawPartialText)
|
|
206
|
-
this.appendText(data.content);
|
|
207
|
-
return;
|
|
208
|
-
case "tool.execution_start":
|
|
209
|
-
if (data?.toolCallId) {
|
|
210
|
-
this.pushToolCall(data.toolCallId, data.toolName ?? "tool", JSON.stringify(data.arguments ?? {}));
|
|
211
|
-
}
|
|
212
|
-
return;
|
|
213
|
-
case "tool.execution_complete": {
|
|
214
|
-
if (!data?.toolCallId)
|
|
215
|
-
return;
|
|
216
|
-
const result = data.success ? data.result?.content : data.error?.message;
|
|
217
|
-
if (typeof result === "string")
|
|
218
|
-
this.pushToolResult(data.toolCallId, result);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
72
|
+
async chatResult(messageId, result) {
|
|
73
|
+
await this.post(`/agent-chats/${messageId}/result`, result);
|
|
222
74
|
}
|
|
223
|
-
|
|
224
|
-
|
|
75
|
+
}
|
|
76
|
+
async function canceled(res) {
|
|
77
|
+
const body = await res.json().catch(() => ({}));
|
|
78
|
+
return body.canceled === true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/config.ts
|
|
82
|
+
import { readFile } from "node:fs/promises";
|
|
83
|
+
|
|
84
|
+
// src/presets.ts
|
|
85
|
+
var PRESETS = {
|
|
86
|
+
claude: {
|
|
87
|
+
bin: "claude",
|
|
88
|
+
outputFormat: "claude-stream-json",
|
|
89
|
+
promptVia: "stdin",
|
|
90
|
+
systemPromptFlag: "--append-system-prompt",
|
|
91
|
+
head: (sessionId) => [
|
|
92
|
+
"-p",
|
|
93
|
+
...sessionId ? ["--resume", sessionId] : [],
|
|
94
|
+
"--output-format",
|
|
95
|
+
"stream-json",
|
|
96
|
+
"--include-partial-messages",
|
|
97
|
+
"--verbose",
|
|
98
|
+
"--permission-mode",
|
|
99
|
+
"auto"
|
|
100
|
+
],
|
|
101
|
+
tail: []
|
|
102
|
+
},
|
|
103
|
+
codex: {
|
|
104
|
+
bin: "codex",
|
|
105
|
+
outputFormat: "codex-jsonl",
|
|
106
|
+
promptVia: "stdin",
|
|
107
|
+
head: (sessionId) => [
|
|
108
|
+
...sessionId ? ["exec", "resume", sessionId] : ["exec"],
|
|
109
|
+
"--json",
|
|
110
|
+
"-c",
|
|
111
|
+
'sandbox_mode="workspace-write"'
|
|
112
|
+
],
|
|
113
|
+
tail: ["-"]
|
|
114
|
+
},
|
|
115
|
+
opencode: {
|
|
116
|
+
bin: "opencode",
|
|
117
|
+
outputFormat: "opencode-json",
|
|
118
|
+
promptVia: "arg",
|
|
119
|
+
head: (sessionId) => [
|
|
120
|
+
"run",
|
|
121
|
+
"--format",
|
|
122
|
+
"json",
|
|
123
|
+
...sessionId ? ["--session", sessionId] : []
|
|
124
|
+
],
|
|
125
|
+
tail: ["--"]
|
|
126
|
+
},
|
|
127
|
+
antigravity: {
|
|
128
|
+
bin: "agy",
|
|
129
|
+
outputFormat: "antigravity-stream-json",
|
|
130
|
+
promptVia: "arg",
|
|
131
|
+
head: (sessionId) => [
|
|
132
|
+
...sessionId ? ["--conversation", sessionId] : [],
|
|
133
|
+
"--output-format",
|
|
134
|
+
"stream-json",
|
|
135
|
+
"--dangerously-skip-permissions",
|
|
136
|
+
"--print-timeout",
|
|
137
|
+
"24h"
|
|
138
|
+
],
|
|
139
|
+
tail: ["-p"]
|
|
140
|
+
},
|
|
141
|
+
copilot: {
|
|
142
|
+
bin: "copilot",
|
|
143
|
+
outputFormat: "copilot-json",
|
|
144
|
+
promptVia: "arg",
|
|
145
|
+
head: (sessionId) => [
|
|
146
|
+
"--allow-all-tools",
|
|
147
|
+
"--no-ask-user",
|
|
148
|
+
"--output-format",
|
|
149
|
+
"json",
|
|
150
|
+
...sessionId ? ["--session-id", sessionId] : []
|
|
151
|
+
],
|
|
152
|
+
tail: ["-p"]
|
|
153
|
+
},
|
|
154
|
+
pi: {
|
|
155
|
+
bin: "pi",
|
|
156
|
+
outputFormat: "pi-json",
|
|
157
|
+
promptVia: "stdin",
|
|
158
|
+
systemPromptFlag: "--append-system-prompt",
|
|
159
|
+
head: (sessionId) => [
|
|
160
|
+
"-p",
|
|
161
|
+
"--mode",
|
|
162
|
+
"json",
|
|
163
|
+
...sessionId ? ["--session-id", sessionId] : []
|
|
164
|
+
],
|
|
165
|
+
tail: []
|
|
166
|
+
},
|
|
167
|
+
omp: {
|
|
168
|
+
bin: "omp",
|
|
169
|
+
outputFormat: "pi-json",
|
|
170
|
+
promptVia: "arg",
|
|
171
|
+
systemPromptFlag: "--append-system-prompt",
|
|
172
|
+
head: (sessionId) => [
|
|
173
|
+
"-p",
|
|
174
|
+
"--mode",
|
|
175
|
+
"json",
|
|
176
|
+
"--auto-approve",
|
|
177
|
+
...sessionId ? ["--resume", sessionId] : []
|
|
178
|
+
],
|
|
179
|
+
tail: ["--"]
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var PRESET_NAMES = Object.keys(PRESETS);
|
|
183
|
+
function isPresetName(value) {
|
|
184
|
+
return value in PRESETS;
|
|
185
|
+
}
|
|
186
|
+
function presetPrompt(preset, systemPrompt, prompt) {
|
|
187
|
+
if (preset.systemPromptFlag || !systemPrompt)
|
|
188
|
+
return prompt;
|
|
189
|
+
return `${systemPrompt}
|
|
190
|
+
|
|
191
|
+
${prompt}`;
|
|
192
|
+
}
|
|
193
|
+
function presetArgv(preset, sessionId, systemPrompt, extraArgs, prompt) {
|
|
194
|
+
return [
|
|
195
|
+
...preset.head(sessionId),
|
|
196
|
+
...preset.systemPromptFlag && systemPrompt ? [preset.systemPromptFlag, systemPrompt] : [],
|
|
197
|
+
...extraArgs,
|
|
198
|
+
...preset.tail,
|
|
199
|
+
...preset.promptVia === "arg" ? [presetPrompt(preset, systemPrompt, prompt)] : []
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/config.ts
|
|
204
|
+
var OUTPUT_FORMATS = [
|
|
205
|
+
"text",
|
|
206
|
+
"claude-stream-json",
|
|
207
|
+
"codex-jsonl",
|
|
208
|
+
"opencode-json",
|
|
209
|
+
"antigravity-stream-json",
|
|
210
|
+
"copilot-json",
|
|
211
|
+
"pi-json"
|
|
212
|
+
];
|
|
213
|
+
function presetOf(config) {
|
|
214
|
+
if (config.command || !config.agent)
|
|
215
|
+
return;
|
|
216
|
+
return PRESETS[config.agent];
|
|
217
|
+
}
|
|
218
|
+
var DEFAULTS = {
|
|
219
|
+
concurrency: 1,
|
|
220
|
+
pollIntervalMs: 3000,
|
|
221
|
+
timeoutMs: 30 * 60 * 1000
|
|
222
|
+
};
|
|
223
|
+
var MIN_POLL_INTERVAL_MS = 1000;
|
|
224
|
+
function textOf(value) {
|
|
225
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
226
|
+
return text || undefined;
|
|
227
|
+
}
|
|
228
|
+
function intFrom(value, fallback) {
|
|
229
|
+
const parsed = typeof value === "string" ? Number.parseInt(value, 10) : value;
|
|
230
|
+
return typeof parsed === "number" && Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
231
|
+
}
|
|
232
|
+
function outputFormatFrom(value, preset) {
|
|
233
|
+
const name = textOf(value);
|
|
234
|
+
if (!name)
|
|
235
|
+
return preset?.outputFormat ?? "text";
|
|
236
|
+
const format = OUTPUT_FORMATS.find((candidate) => candidate === name);
|
|
237
|
+
if (!format) {
|
|
238
|
+
throw new Error(`outputFormat must be one of ${OUTPUT_FORMATS.join(", ")}`);
|
|
239
|
+
}
|
|
240
|
+
return format;
|
|
241
|
+
}
|
|
242
|
+
function agentFrom(value) {
|
|
243
|
+
const name = textOf(value);
|
|
244
|
+
if (!name)
|
|
245
|
+
return;
|
|
246
|
+
if (!isPresetName(name)) {
|
|
247
|
+
throw new Error(`agent must be one of ${PRESET_NAMES.join(", ")}`);
|
|
248
|
+
}
|
|
249
|
+
return name;
|
|
250
|
+
}
|
|
251
|
+
function portFrom(value) {
|
|
252
|
+
if (value === undefined || value === "")
|
|
253
|
+
return;
|
|
254
|
+
const port = intFrom(value, 0);
|
|
255
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
256
|
+
throw new Error("listen must be a port number from 1 to 65535");
|
|
257
|
+
}
|
|
258
|
+
return port;
|
|
259
|
+
}
|
|
260
|
+
function argsFrom(value) {
|
|
261
|
+
if (value === undefined)
|
|
262
|
+
return [];
|
|
263
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
264
|
+
throw new Error("args must be an array of strings");
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
function required(value, field) {
|
|
269
|
+
const text = textOf(value);
|
|
270
|
+
if (!text)
|
|
271
|
+
throw new Error(`${field} is required — set it in the config file or the environment`);
|
|
272
|
+
return text;
|
|
273
|
+
}
|
|
274
|
+
async function readConfigFile(path) {
|
|
275
|
+
let text;
|
|
276
|
+
try {
|
|
277
|
+
text = await readFile(path, "utf8");
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (err.code === "ENOENT")
|
|
280
|
+
return {};
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
return JSON.parse(text);
|
|
285
|
+
} catch {
|
|
286
|
+
throw new Error(`${path} is not valid JSON`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function agentFieldsFrom(file) {
|
|
290
|
+
if (file.apiKeys !== undefined && file.agents !== undefined) {
|
|
291
|
+
throw new Error("set either apiKeys or agents, not both");
|
|
292
|
+
}
|
|
293
|
+
if (file.apiKeys !== undefined) {
|
|
294
|
+
const keys = file.apiKeys;
|
|
295
|
+
if (!Array.isArray(keys) || keys.some((key) => typeof key !== "string")) {
|
|
296
|
+
throw new Error("apiKeys must be an array of strings");
|
|
297
|
+
}
|
|
298
|
+
return keys.map((apiKey) => ({ apiKey }));
|
|
299
|
+
}
|
|
300
|
+
if (file.agents === undefined)
|
|
301
|
+
return [{}];
|
|
302
|
+
const agents = file.agents;
|
|
303
|
+
if (!Array.isArray(agents) || agents.some((entry) => typeof entry !== "object" || entry === null || Array.isArray(entry) || !textOf(entry.apiKey))) {
|
|
304
|
+
throw new Error("agents must be an array of objects, each with its own apiKey");
|
|
305
|
+
}
|
|
306
|
+
return agents;
|
|
307
|
+
}
|
|
308
|
+
function merge(shared, own) {
|
|
309
|
+
const merged = { ...shared, ...own };
|
|
310
|
+
const mode = textOf(own.agent) ?? textOf(own.command);
|
|
311
|
+
const changesMode = mode !== undefined && mode !== (textOf(shared.agent) ?? textOf(shared.command));
|
|
312
|
+
if (textOf(own.agent))
|
|
313
|
+
delete merged.command;
|
|
314
|
+
else if (textOf(own.command))
|
|
315
|
+
delete merged.agent;
|
|
316
|
+
if (changesMode && !textOf(own.outputFormat))
|
|
317
|
+
delete merged.outputFormat;
|
|
318
|
+
return merged;
|
|
319
|
+
}
|
|
320
|
+
function nameOf(entry, index, total) {
|
|
321
|
+
if (total === 1)
|
|
322
|
+
return "";
|
|
323
|
+
return textOf(entry.name) ?? `#${index + 1}`;
|
|
324
|
+
}
|
|
325
|
+
function configFrom(fields, name, extraArgs) {
|
|
326
|
+
const agent = agentFrom(fields.agent);
|
|
327
|
+
const command = textOf(fields.command);
|
|
328
|
+
const listen = portFrom(fields.listen);
|
|
329
|
+
const signingSecret = textOf(fields.signingSecret);
|
|
330
|
+
if (listen !== undefined && !signingSecret) {
|
|
331
|
+
throw new Error("signingSecret is required with listen — copy it from the agent settings");
|
|
332
|
+
}
|
|
333
|
+
if (!agent && !command) {
|
|
334
|
+
throw new Error(`set either agent (one of ${PRESET_NAMES.join(", ")}) or command in the config file or the environment`);
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
name,
|
|
338
|
+
url: required(fields.url, "url").replace(/\/+$/, ""),
|
|
339
|
+
apiKey: required(fields.apiKey, "apiKey"),
|
|
340
|
+
agent,
|
|
341
|
+
command,
|
|
342
|
+
args: [...argsFrom(fields.args), ...extraArgs],
|
|
343
|
+
cwd: textOf(fields.cwd)?.replace(/^~/, process.env.HOME ?? "~"),
|
|
344
|
+
env: fields.env ?? {},
|
|
345
|
+
concurrency: intFrom(fields.concurrency, DEFAULTS.concurrency),
|
|
346
|
+
pollIntervalMs: Math.max(intFrom(fields.pollIntervalMs, DEFAULTS.pollIntervalMs), MIN_POLL_INTERVAL_MS),
|
|
347
|
+
timeoutMs: intFrom(fields.timeoutMs, DEFAULTS.timeoutMs),
|
|
348
|
+
outputFormat: outputFormatFrom(fields.outputFormat, presetOf({ agent, command })),
|
|
349
|
+
listen,
|
|
350
|
+
signingSecret
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function assertSecretsApart(configs) {
|
|
354
|
+
const seen = new Set;
|
|
355
|
+
for (const config of configs) {
|
|
356
|
+
if (config.listen === undefined)
|
|
357
|
+
continue;
|
|
358
|
+
const key = `${config.listen} ${config.signingSecret}`;
|
|
359
|
+
if (seen.has(key)) {
|
|
360
|
+
throw new Error("each agent on a listen port needs its own signingSecret — list them in agents");
|
|
361
|
+
}
|
|
362
|
+
seen.add(key);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function sharedFields(file, overrides) {
|
|
366
|
+
const env = process.env;
|
|
367
|
+
const outside = {
|
|
368
|
+
url: env.ITSAPLAN_URL,
|
|
369
|
+
apiKey: env.ITSAPLAN_API_KEY,
|
|
370
|
+
agent: overrides.agent ?? env.ITSAPLAN_AGENT,
|
|
371
|
+
command: env.ITSAPLAN_COMMAND,
|
|
372
|
+
cwd: env.ITSAPLAN_CWD,
|
|
373
|
+
concurrency: env.ITSAPLAN_CONCURRENCY,
|
|
374
|
+
pollIntervalMs: env.ITSAPLAN_POLL_INTERVAL_MS,
|
|
375
|
+
timeoutMs: env.ITSAPLAN_TIMEOUT_MS,
|
|
376
|
+
outputFormat: env.ITSAPLAN_OUTPUT_FORMAT,
|
|
377
|
+
listen: env.ITSAPLAN_LISTEN,
|
|
378
|
+
signingSecret: env.ITSAPLAN_SIGNING_SECRET
|
|
379
|
+
};
|
|
380
|
+
for (const [field, value] of Object.entries(outside)) {
|
|
381
|
+
if (!textOf(value))
|
|
382
|
+
delete outside[field];
|
|
383
|
+
}
|
|
384
|
+
return merge(file, outside);
|
|
385
|
+
}
|
|
386
|
+
async function loadConfig(path, overrides = {}) {
|
|
387
|
+
const file = await readConfigFile(path);
|
|
388
|
+
const entries = agentFieldsFrom(file);
|
|
389
|
+
if (entries.length === 0) {
|
|
390
|
+
throw new Error("no agents configured — apiKeys and agents cannot be empty");
|
|
391
|
+
}
|
|
392
|
+
const shared = sharedFields(file, overrides);
|
|
393
|
+
const configs = entries.map((entry, index) => configFrom(merge(shared, entry), nameOf(entry, index, entries.length), overrides.args ?? []));
|
|
394
|
+
assertSecretsApart(configs);
|
|
395
|
+
return configs;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/listen.ts
|
|
399
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
400
|
+
import { createServer } from "node:http";
|
|
401
|
+
var SIGNATURE_TOLERANCE_S = 300;
|
|
402
|
+
var MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
403
|
+
function verifySignature(secret, header, body, nowSeconds = Date.now() / 1000) {
|
|
404
|
+
const fields = new Map(header.split(",").map((part) => {
|
|
405
|
+
const at = part.indexOf("=");
|
|
406
|
+
return [part.slice(0, at).trim(), part.slice(at + 1).trim()];
|
|
407
|
+
}));
|
|
408
|
+
const t = fields.get("t");
|
|
409
|
+
const v1 = fields.get("v1");
|
|
410
|
+
if (!t || !v1 || !Number.isFinite(Number(t)))
|
|
411
|
+
return false;
|
|
412
|
+
if (Math.abs(nowSeconds - Number(t)) > SIGNATURE_TOLERANCE_S)
|
|
413
|
+
return false;
|
|
414
|
+
const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
|
|
415
|
+
const given = Buffer.from(v1);
|
|
416
|
+
const wanted = Buffer.from(expected);
|
|
417
|
+
return given.length === wanted.length && timingSafeEqual(given, wanted);
|
|
418
|
+
}
|
|
419
|
+
function feed(limit, work, inFlight) {
|
|
420
|
+
let active = 0;
|
|
421
|
+
const waiting = [];
|
|
422
|
+
const held = new Set;
|
|
423
|
+
const acquire = () => {
|
|
424
|
+
if (active < limit) {
|
|
425
|
+
active++;
|
|
426
|
+
return Promise.resolve();
|
|
427
|
+
}
|
|
428
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
429
|
+
};
|
|
430
|
+
const release = () => {
|
|
431
|
+
const next = waiting.shift();
|
|
432
|
+
if (next)
|
|
433
|
+
next();
|
|
434
|
+
else
|
|
435
|
+
active--;
|
|
436
|
+
};
|
|
437
|
+
return (item) => {
|
|
438
|
+
if (held.has(item.id))
|
|
439
|
+
return;
|
|
440
|
+
held.add(item.id);
|
|
441
|
+
const task = acquire().then(() => work(item)).finally(() => {
|
|
442
|
+
held.delete(item.id);
|
|
443
|
+
release();
|
|
444
|
+
});
|
|
445
|
+
inFlight.add(task);
|
|
446
|
+
task.finally(() => inFlight.delete(task)).catch(() => {});
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
class TooLargeError extends Error {
|
|
451
|
+
}
|
|
452
|
+
function readBody(req) {
|
|
453
|
+
return new Promise((resolve, reject) => {
|
|
454
|
+
const chunks = [];
|
|
455
|
+
let size = 0;
|
|
456
|
+
req.on("data", (chunk) => {
|
|
457
|
+
size += chunk.length;
|
|
458
|
+
if (size > MAX_BODY_BYTES) {
|
|
459
|
+
reject(new TooLargeError);
|
|
460
|
+
req.destroy();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
chunks.push(chunk);
|
|
464
|
+
});
|
|
465
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
466
|
+
req.on("error", reject);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function startListener(port, agents, log) {
|
|
470
|
+
const inFlight = new Set;
|
|
471
|
+
const served = agents.map((agent) => ({
|
|
472
|
+
secret: agent.secret,
|
|
473
|
+
run: feed(agent.concurrency, agent.run, inFlight),
|
|
474
|
+
chat: feed(agent.concurrency, agent.chat, inFlight)
|
|
475
|
+
}));
|
|
476
|
+
const server = createServer((req, res) => {
|
|
477
|
+
if (req.method !== "POST") {
|
|
478
|
+
res.writeHead(405, { allow: "POST" }).end();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
readBody(req).then((body) => {
|
|
482
|
+
const header = req.headers["x-itsaplan-signature"];
|
|
483
|
+
const agent = typeof header === "string" ? served.find((candidate) => verifySignature(candidate.secret, header, body)) : undefined;
|
|
484
|
+
if (!agent) {
|
|
485
|
+
log("refused a request that no configured agent signed");
|
|
486
|
+
res.writeHead(401).end("bad signature");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
let payload;
|
|
490
|
+
try {
|
|
491
|
+
payload = JSON.parse(body);
|
|
492
|
+
} catch {
|
|
493
|
+
res.writeHead(400).end("body is not JSON");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (payload.event === "agent.run" && payload.run)
|
|
497
|
+
agent.run(payload.run);
|
|
498
|
+
else if (payload.event === "agent.chat" && payload.message)
|
|
499
|
+
agent.chat(payload.message);
|
|
500
|
+
else if (payload.event !== "ping") {
|
|
501
|
+
res.writeHead(400).end(`unknown event ${String(payload.event)}`);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
res.writeHead(200).end("ok");
|
|
505
|
+
}, (err) => {
|
|
506
|
+
res.writeHead(err instanceof TooLargeError ? 413 : 400).end();
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
return new Promise((resolve, reject) => {
|
|
510
|
+
server.once("error", reject);
|
|
511
|
+
server.listen(port, () => {
|
|
512
|
+
server.off("error", reject);
|
|
513
|
+
const address = server.address();
|
|
514
|
+
resolve({
|
|
515
|
+
port: typeof address === "object" && address ? address.port : port,
|
|
516
|
+
close: async () => {
|
|
517
|
+
await new Promise((done) => {
|
|
518
|
+
server.close(() => done());
|
|
519
|
+
server.closeIdleConnections();
|
|
520
|
+
});
|
|
521
|
+
await Promise.allSettled([...inFlight]);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/agui.ts
|
|
529
|
+
var FLUSH_CHARS = 1500;
|
|
530
|
+
var DELTA_LIMIT = 12000;
|
|
531
|
+
var TOOL_TEXT_LIMIT = 32000;
|
|
532
|
+
|
|
533
|
+
class AnswerStream {
|
|
534
|
+
format;
|
|
535
|
+
threadId;
|
|
536
|
+
runId;
|
|
537
|
+
send;
|
|
538
|
+
messageId;
|
|
539
|
+
queued = [];
|
|
540
|
+
text = "";
|
|
541
|
+
started = false;
|
|
542
|
+
line = "";
|
|
543
|
+
sessionId = null;
|
|
544
|
+
openTextPart = "";
|
|
545
|
+
openToolCalls = new Set;
|
|
546
|
+
closedToolCalls = new Set;
|
|
547
|
+
sawPartialText = false;
|
|
548
|
+
sawAnyText = false;
|
|
549
|
+
segment = "";
|
|
550
|
+
lastSegment = "";
|
|
551
|
+
sending = Promise.resolve();
|
|
552
|
+
usage;
|
|
553
|
+
constructor(format, threadId, runId, send) {
|
|
554
|
+
this.format = format;
|
|
555
|
+
this.threadId = threadId;
|
|
556
|
+
this.runId = runId;
|
|
557
|
+
this.send = send;
|
|
558
|
+
this.usage = new UsageReader(format);
|
|
559
|
+
this.messageId = `msg-${runId}`;
|
|
560
|
+
this.queued.push({ type: "RUN_STARTED", threadId, runId });
|
|
561
|
+
}
|
|
562
|
+
write(chunk) {
|
|
563
|
+
if (this.format === "text") {
|
|
564
|
+
this.appendText(chunk);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
this.line += chunk;
|
|
568
|
+
const lines = this.line.split(`
|
|
569
|
+
`);
|
|
570
|
+
this.line = lines.pop() ?? "";
|
|
571
|
+
for (const line of lines)
|
|
572
|
+
this.readLine(line);
|
|
573
|
+
}
|
|
574
|
+
startedSession() {
|
|
575
|
+
return this.sessionId;
|
|
576
|
+
}
|
|
577
|
+
answer() {
|
|
578
|
+
return (this.segment.trim() ? this.segment : this.lastSegment).trim();
|
|
579
|
+
}
|
|
580
|
+
contextUsage() {
|
|
581
|
+
return this.usage.value();
|
|
582
|
+
}
|
|
583
|
+
async flush() {
|
|
584
|
+
const sending = this.sending.catch(() => {}).then(() => this.sendQueued());
|
|
585
|
+
this.sending = sending.catch(() => {});
|
|
586
|
+
return sending;
|
|
587
|
+
}
|
|
588
|
+
async sendQueued() {
|
|
589
|
+
this.drainText();
|
|
590
|
+
if (this.queued.length === 0)
|
|
591
|
+
return;
|
|
592
|
+
const batch = this.queued;
|
|
593
|
+
this.queued = [];
|
|
594
|
+
try {
|
|
595
|
+
await this.send(batch);
|
|
596
|
+
} catch (err) {
|
|
597
|
+
this.queued.unshift(...batch);
|
|
598
|
+
throw err;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async finish(fallback) {
|
|
602
|
+
this.closeText(fallback);
|
|
603
|
+
this.queued.push({ type: "RUN_FINISHED", threadId: this.threadId, runId: this.runId });
|
|
604
|
+
await this.flush();
|
|
605
|
+
}
|
|
606
|
+
async fail(message, fallback = "") {
|
|
607
|
+
this.closeText(fallback);
|
|
608
|
+
this.queued.push({ type: "RUN_ERROR", message });
|
|
609
|
+
await this.flush();
|
|
610
|
+
}
|
|
611
|
+
closeText(fallback) {
|
|
612
|
+
if (this.line) {
|
|
613
|
+
this.readLine(this.line);
|
|
614
|
+
this.line = "";
|
|
615
|
+
}
|
|
616
|
+
if (!this.sawAnyText && fallback)
|
|
617
|
+
this.appendText(fallback);
|
|
618
|
+
this.drainText();
|
|
619
|
+
if (this.started)
|
|
620
|
+
this.queued.push({ type: "TEXT_MESSAGE_END", messageId: this.messageId });
|
|
621
|
+
this.started = false;
|
|
622
|
+
}
|
|
623
|
+
appendText(text) {
|
|
624
|
+
if (!text)
|
|
625
|
+
return;
|
|
626
|
+
this.sawAnyText = true;
|
|
627
|
+
this.text += text;
|
|
628
|
+
this.segment += text;
|
|
629
|
+
if (this.text.length >= FLUSH_CHARS)
|
|
630
|
+
this.drainText();
|
|
631
|
+
}
|
|
632
|
+
drainText() {
|
|
633
|
+
if (this.text.length === 0)
|
|
634
|
+
return;
|
|
635
|
+
if (!this.started) {
|
|
636
|
+
this.started = true;
|
|
637
|
+
this.queued.push({
|
|
638
|
+
type: "TEXT_MESSAGE_START",
|
|
639
|
+
messageId: this.messageId,
|
|
640
|
+
role: "assistant"
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
while (this.text.length > 0) {
|
|
644
|
+
const delta = this.text.slice(0, DELTA_LIMIT);
|
|
645
|
+
this.text = this.text.slice(DELTA_LIMIT);
|
|
646
|
+
this.queued.push({ type: "TEXT_MESSAGE_CONTENT", messageId: this.messageId, delta });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
readLine(line) {
|
|
650
|
+
const trimmed = line.trim();
|
|
651
|
+
if (!trimmed)
|
|
652
|
+
return;
|
|
653
|
+
let parsed;
|
|
654
|
+
try {
|
|
655
|
+
parsed = JSON.parse(trimmed);
|
|
656
|
+
} catch {
|
|
657
|
+
this.appendText(`${trimmed}
|
|
658
|
+
`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (!parsed || typeof parsed !== "object")
|
|
662
|
+
return;
|
|
663
|
+
this.usage.readLine(parsed);
|
|
664
|
+
switch (this.format) {
|
|
665
|
+
case "claude-stream-json":
|
|
666
|
+
this.readClaudeLine(parsed);
|
|
667
|
+
break;
|
|
668
|
+
case "codex-jsonl":
|
|
669
|
+
this.readCodexLine(parsed);
|
|
670
|
+
break;
|
|
671
|
+
case "opencode-json":
|
|
672
|
+
this.readOpencodeLine(parsed);
|
|
673
|
+
break;
|
|
674
|
+
case "antigravity-stream-json":
|
|
675
|
+
this.readAntigravityLine(parsed);
|
|
676
|
+
break;
|
|
677
|
+
case "copilot-json":
|
|
678
|
+
this.readCopilotLine(parsed);
|
|
679
|
+
break;
|
|
680
|
+
case "pi-json":
|
|
681
|
+
this.readPiLine(parsed);
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
readPiLine(message) {
|
|
686
|
+
if (message.type === "session" && message.id)
|
|
687
|
+
this.sessionId ??= message.id;
|
|
688
|
+
switch (message.type) {
|
|
689
|
+
case "message_update": {
|
|
690
|
+
const event = message.assistantMessageEvent;
|
|
691
|
+
if (event?.type === "text_delta" && event.delta) {
|
|
692
|
+
this.sawPartialText = true;
|
|
693
|
+
this.appendText(event.delta);
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
case "tool_execution_start":
|
|
698
|
+
if (message.toolCallId && !this.openToolCalls.has(message.toolCallId)) {
|
|
699
|
+
this.openToolCalls.add(message.toolCallId);
|
|
700
|
+
this.pushToolCall(message.toolCallId, message.toolName ?? "tool", JSON.stringify(message.args ?? {}));
|
|
701
|
+
}
|
|
702
|
+
return;
|
|
703
|
+
case "tool_execution_end": {
|
|
704
|
+
if (!message.toolCallId || this.closedToolCalls.has(message.toolCallId))
|
|
705
|
+
return;
|
|
706
|
+
this.closedToolCalls.add(message.toolCallId);
|
|
707
|
+
this.pushToolResult(message.toolCallId, piResultText(message.result));
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
case "message_end":
|
|
711
|
+
if (message.message?.role === "assistant" && !this.sawAnyText && message.message.content) {
|
|
712
|
+
const body = textOfResult(message.message.content);
|
|
713
|
+
if (body)
|
|
714
|
+
this.appendText(body);
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
readCopilotLine(message) {
|
|
720
|
+
const data = message.data;
|
|
721
|
+
switch (message.type) {
|
|
722
|
+
case "result":
|
|
723
|
+
if (message.sessionId)
|
|
724
|
+
this.sessionId ??= message.sessionId;
|
|
725
|
+
return;
|
|
726
|
+
case "assistant.message_delta":
|
|
727
|
+
if (data?.deltaContent) {
|
|
728
|
+
this.sawPartialText = true;
|
|
729
|
+
this.appendText(data.deltaContent);
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
case "assistant.message":
|
|
733
|
+
if (data?.content && !this.sawPartialText)
|
|
734
|
+
this.appendText(data.content);
|
|
735
|
+
return;
|
|
736
|
+
case "tool.execution_start":
|
|
737
|
+
if (data?.toolCallId) {
|
|
738
|
+
this.pushToolCall(data.toolCallId, data.toolName ?? "tool", JSON.stringify(data.arguments ?? {}));
|
|
739
|
+
}
|
|
740
|
+
return;
|
|
741
|
+
case "tool.execution_complete": {
|
|
742
|
+
if (!data?.toolCallId)
|
|
743
|
+
return;
|
|
744
|
+
const result = data.success ? data.result?.content : data.error?.message;
|
|
745
|
+
if (typeof result === "string")
|
|
746
|
+
this.pushToolResult(data.toolCallId, result);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
readAntigravityLine(message) {
|
|
752
|
+
const step = message.step_update;
|
|
225
753
|
const conversationId = message.conversation_id ?? step?.conversation_id ?? message.result?.conversation_id;
|
|
226
754
|
if (conversationId)
|
|
227
755
|
this.sessionId ??= conversationId;
|
|
@@ -260,15 +788,36 @@ class AnswerStream {
|
|
|
260
788
|
return;
|
|
261
789
|
}
|
|
262
790
|
const item = message.item;
|
|
263
|
-
if (
|
|
791
|
+
if (!item)
|
|
264
792
|
return;
|
|
265
|
-
if (item.type === "agent_message" && item.text) {
|
|
793
|
+
if (message.type === "item.completed" && item.type === "agent_message" && item.text) {
|
|
266
794
|
this.appendText(item.text);
|
|
267
795
|
return;
|
|
268
796
|
}
|
|
269
|
-
if (item.
|
|
270
|
-
|
|
271
|
-
|
|
797
|
+
if (!item.id || !["command_execution", "mcp_tool_call", "file_change", "web_search"].includes(item.type ?? ""))
|
|
798
|
+
return;
|
|
799
|
+
if (!["item.started", "item.completed"].includes(message.type ?? ""))
|
|
800
|
+
return;
|
|
801
|
+
if (!this.openToolCalls.has(item.id)) {
|
|
802
|
+
this.openToolCalls.add(item.id);
|
|
803
|
+
const name = item.type === "command_execution" ? "shell" : item.tool ?? item.type ?? "tool";
|
|
804
|
+
const args = item.command ?? JSON.stringify(item.arguments ?? item.changes ?? { query: item.query });
|
|
805
|
+
this.pushToolCall(item.id, name, args);
|
|
806
|
+
}
|
|
807
|
+
if (message.type === "item.completed" && !this.closedToolCalls.has(item.id)) {
|
|
808
|
+
this.closedToolCalls.add(item.id);
|
|
809
|
+
let result = item.aggregated_output;
|
|
810
|
+
if (result == null || item.status === "failed" || item.exit_code != null && item.exit_code !== 0) {
|
|
811
|
+
result = JSON.stringify({
|
|
812
|
+
status: item.status,
|
|
813
|
+
exitCode: item.exit_code,
|
|
814
|
+
output: item.aggregated_output,
|
|
815
|
+
result: item.result,
|
|
816
|
+
error: item.error,
|
|
817
|
+
changes: item.changes
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
this.pushToolResult(item.id, result);
|
|
272
821
|
}
|
|
273
822
|
}
|
|
274
823
|
readOpencodeLine(message) {
|
|
@@ -345,444 +894,164 @@ class AnswerStream {
|
|
|
345
894
|
}
|
|
346
895
|
}
|
|
347
896
|
pushToolCall(toolCallId, toolCallName, args) {
|
|
348
|
-
this.drainText();
|
|
349
|
-
this.
|
|
350
|
-
|
|
351
|
-
pushToolResult(toolCallId, content) {
|
|
352
|
-
this.drainText();
|
|
353
|
-
this.queued.push({
|
|
354
|
-
type: "TOOL_CALL_RESULT",
|
|
355
|
-
messageId: this.messageId,
|
|
356
|
-
toolCallId,
|
|
357
|
-
content: tail(content, TOOL_TEXT_LIMIT)
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
class UsageReader {
|
|
363
|
-
format;
|
|
364
|
-
last = null;
|
|
365
|
-
buffered = "";
|
|
366
|
-
constructor(format) {
|
|
367
|
-
this.format = format;
|
|
368
|
-
}
|
|
369
|
-
write(chunk) {
|
|
370
|
-
if (this.format === "text")
|
|
371
|
-
return;
|
|
372
|
-
this.buffered += chunk;
|
|
373
|
-
const lines = this.buffered.split(`
|
|
374
|
-
`);
|
|
375
|
-
this.buffered = lines.pop() ?? "";
|
|
376
|
-
for (const line of lines)
|
|
377
|
-
this.readText(line);
|
|
378
|
-
}
|
|
379
|
-
end() {
|
|
380
|
-
if (!this.buffered)
|
|
381
|
-
return;
|
|
382
|
-
this.readText(this.buffered);
|
|
383
|
-
this.buffered = "";
|
|
384
|
-
}
|
|
385
|
-
value() {
|
|
386
|
-
if (this.format === "copilot-json")
|
|
387
|
-
return null;
|
|
388
|
-
return this.last ?? undefined;
|
|
389
|
-
}
|
|
390
|
-
readText(line) {
|
|
391
|
-
const trimmed = line.trim();
|
|
392
|
-
if (!trimmed)
|
|
393
|
-
return;
|
|
394
|
-
try {
|
|
395
|
-
const parsed = JSON.parse(trimmed);
|
|
396
|
-
if (parsed && typeof parsed === "object")
|
|
397
|
-
this.readLine(parsed);
|
|
398
|
-
} catch {}
|
|
399
|
-
}
|
|
400
|
-
readLine(parsed) {
|
|
401
|
-
switch (this.format) {
|
|
402
|
-
case "claude-stream-json":
|
|
403
|
-
this.readClaude(parsed);
|
|
404
|
-
return;
|
|
405
|
-
case "codex-jsonl":
|
|
406
|
-
this.readCodex(parsed);
|
|
407
|
-
return;
|
|
408
|
-
case "opencode-json":
|
|
409
|
-
this.readOpencode(parsed);
|
|
410
|
-
return;
|
|
411
|
-
case "antigravity-stream-json":
|
|
412
|
-
this.readAntigravity(parsed);
|
|
413
|
-
return;
|
|
414
|
-
case "pi-json":
|
|
415
|
-
this.readPi(parsed);
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
readClaude(message) {
|
|
420
|
-
const event = message.type === "stream_event" ? message.event : undefined;
|
|
421
|
-
if (!event)
|
|
422
|
-
return;
|
|
423
|
-
const usage = event.type === "message_start" ? event.message?.usage : event.usage;
|
|
424
|
-
if (!usage)
|
|
425
|
-
return;
|
|
426
|
-
const read = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
427
|
-
const written = usage.output_tokens ?? 0;
|
|
428
|
-
if (event.type === "message_start") {
|
|
429
|
-
this.last = { inputTokens: read, outputTokens: written };
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (event.type !== "message_delta")
|
|
433
|
-
return;
|
|
434
|
-
this.last = { inputTokens: read || (this.last?.inputTokens ?? 0), outputTokens: written };
|
|
435
|
-
}
|
|
436
|
-
readCodex(message) {
|
|
437
|
-
if (message.type !== "turn.completed" || !message.usage)
|
|
438
|
-
return;
|
|
439
|
-
this.last = {
|
|
440
|
-
inputTokens: message.usage.input_tokens ?? 0,
|
|
441
|
-
outputTokens: message.usage.output_tokens ?? 0
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
readOpencode(message) {
|
|
445
|
-
const tokens = message.part?.type === "step-finish" ? message.part.tokens : undefined;
|
|
446
|
-
if (!tokens)
|
|
447
|
-
return;
|
|
448
|
-
this.last = {
|
|
449
|
-
inputTokens: (tokens.input ?? 0) + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0),
|
|
450
|
-
outputTokens: (tokens.output ?? 0) + (tokens.reasoning ?? 0)
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
readAntigravity(message) {
|
|
454
|
-
const step = message.step_update;
|
|
455
|
-
if (!step)
|
|
456
|
-
return;
|
|
457
|
-
const usage = step.usage ?? message.usage;
|
|
458
|
-
if (!usage)
|
|
459
|
-
return;
|
|
460
|
-
this.last = {
|
|
461
|
-
inputTokens: (usage.input_tokens ?? 0) + (usage.cache_read_tokens ?? 0),
|
|
462
|
-
outputTokens: (usage.output_tokens ?? 0) + (usage.thinking_tokens ?? 0)
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
readPi(message) {
|
|
466
|
-
const usage = message.usage;
|
|
467
|
-
if (!usage)
|
|
468
|
-
return;
|
|
469
|
-
this.last = {
|
|
470
|
-
inputTokens: (usage.input ?? usage.input_tokens ?? 0) + (usage.cacheRead ?? 0),
|
|
471
|
-
outputTokens: usage.output ?? usage.output_tokens ?? 0
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
function tail(text, limit) {
|
|
476
|
-
if (text.length <= limit)
|
|
477
|
-
return text;
|
|
478
|
-
const mark = "…";
|
|
479
|
-
return mark + text.slice(-(limit - mark.length));
|
|
480
|
-
}
|
|
481
|
-
function textOfResult(content) {
|
|
482
|
-
if (typeof content === "string")
|
|
483
|
-
return content;
|
|
484
|
-
if (Array.isArray(content)) {
|
|
485
|
-
return content.map((part) => typeof part === "object" && part && ("text" in part) ? String(part.text) : "").join("");
|
|
897
|
+
this.drainText();
|
|
898
|
+
this.closeSegment();
|
|
899
|
+
this.queued.push({ type: "TOOL_CALL_START", toolCallId, toolCallName, parentMessageId: this.messageId }, { type: "TOOL_CALL_ARGS", toolCallId, delta: tail(args, TOOL_TEXT_LIMIT) }, { type: "TOOL_CALL_END", toolCallId });
|
|
486
900
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return textOfResult(result.content);
|
|
901
|
+
closeSegment() {
|
|
902
|
+
if (this.segment.trim())
|
|
903
|
+
this.lastSegment = this.segment;
|
|
904
|
+
this.segment = "";
|
|
492
905
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
// src/presets.ts
|
|
504
|
-
var PRESETS = {
|
|
505
|
-
claude: {
|
|
506
|
-
bin: "claude",
|
|
507
|
-
outputFormat: "claude-stream-json",
|
|
508
|
-
promptVia: "stdin",
|
|
509
|
-
systemPromptFlag: "--append-system-prompt",
|
|
510
|
-
head: (sessionId) => [
|
|
511
|
-
"-p",
|
|
512
|
-
...sessionId ? ["--resume", sessionId] : [],
|
|
513
|
-
"--output-format",
|
|
514
|
-
"stream-json",
|
|
515
|
-
"--include-partial-messages",
|
|
516
|
-
"--verbose",
|
|
517
|
-
"--permission-mode",
|
|
518
|
-
"auto"
|
|
519
|
-
],
|
|
520
|
-
tail: []
|
|
521
|
-
},
|
|
522
|
-
codex: {
|
|
523
|
-
bin: "codex",
|
|
524
|
-
outputFormat: "codex-jsonl",
|
|
525
|
-
promptVia: "stdin",
|
|
526
|
-
head: (sessionId) => [
|
|
527
|
-
...sessionId ? ["exec", "resume", sessionId] : ["exec"],
|
|
528
|
-
"--json",
|
|
529
|
-
"-c",
|
|
530
|
-
'sandbox_mode="workspace-write"'
|
|
531
|
-
],
|
|
532
|
-
tail: ["-"]
|
|
533
|
-
},
|
|
534
|
-
opencode: {
|
|
535
|
-
bin: "opencode",
|
|
536
|
-
outputFormat: "opencode-json",
|
|
537
|
-
promptVia: "arg",
|
|
538
|
-
head: (sessionId) => [
|
|
539
|
-
"run",
|
|
540
|
-
"--format",
|
|
541
|
-
"json",
|
|
542
|
-
...sessionId ? ["--session", sessionId] : []
|
|
543
|
-
],
|
|
544
|
-
tail: []
|
|
545
|
-
},
|
|
546
|
-
antigravity: {
|
|
547
|
-
bin: "agy",
|
|
548
|
-
outputFormat: "antigravity-stream-json",
|
|
549
|
-
promptVia: "arg",
|
|
550
|
-
head: (sessionId) => [
|
|
551
|
-
...sessionId ? ["--conversation", sessionId] : [],
|
|
552
|
-
"--output-format",
|
|
553
|
-
"stream-json",
|
|
554
|
-
"--dangerously-skip-permissions",
|
|
555
|
-
"--print-timeout",
|
|
556
|
-
"24h"
|
|
557
|
-
],
|
|
558
|
-
tail: ["-p"]
|
|
559
|
-
},
|
|
560
|
-
copilot: {
|
|
561
|
-
bin: "copilot",
|
|
562
|
-
outputFormat: "copilot-json",
|
|
563
|
-
promptVia: "arg",
|
|
564
|
-
head: (sessionId) => [
|
|
565
|
-
"--allow-all-tools",
|
|
566
|
-
"--no-ask-user",
|
|
567
|
-
"--output-format",
|
|
568
|
-
"json",
|
|
569
|
-
...sessionId ? ["--session-id", sessionId] : []
|
|
570
|
-
],
|
|
571
|
-
tail: ["-p"]
|
|
572
|
-
},
|
|
573
|
-
pi: {
|
|
574
|
-
bin: "pi",
|
|
575
|
-
outputFormat: "pi-json",
|
|
576
|
-
promptVia: "arg",
|
|
577
|
-
systemPromptFlag: "--append-system-prompt",
|
|
578
|
-
head: (sessionId) => [
|
|
579
|
-
"-p",
|
|
580
|
-
"--mode",
|
|
581
|
-
"json",
|
|
582
|
-
...sessionId ? ["--session-id", sessionId] : []
|
|
583
|
-
],
|
|
584
|
-
tail: []
|
|
585
|
-
},
|
|
586
|
-
omp: {
|
|
587
|
-
bin: "omp",
|
|
588
|
-
outputFormat: "pi-json",
|
|
589
|
-
promptVia: "arg",
|
|
590
|
-
systemPromptFlag: "--append-system-prompt",
|
|
591
|
-
head: (sessionId) => [
|
|
592
|
-
"-p",
|
|
593
|
-
"--mode",
|
|
594
|
-
"json",
|
|
595
|
-
"--auto-approve",
|
|
596
|
-
...sessionId ? ["--resume", sessionId] : []
|
|
597
|
-
],
|
|
598
|
-
tail: []
|
|
906
|
+
pushToolResult(toolCallId, content) {
|
|
907
|
+
this.drainText();
|
|
908
|
+
this.closeSegment();
|
|
909
|
+
this.queued.push({
|
|
910
|
+
type: "TOOL_CALL_RESULT",
|
|
911
|
+
messageId: this.messageId,
|
|
912
|
+
toolCallId,
|
|
913
|
+
content: tail(content, TOOL_TEXT_LIMIT)
|
|
914
|
+
});
|
|
599
915
|
}
|
|
600
|
-
};
|
|
601
|
-
var PRESET_NAMES = Object.keys(PRESETS);
|
|
602
|
-
function isPresetName(value) {
|
|
603
|
-
return value in PRESETS;
|
|
604
|
-
}
|
|
605
|
-
function presetPrompt(preset, systemPrompt, prompt) {
|
|
606
|
-
if (preset.systemPromptFlag || !systemPrompt)
|
|
607
|
-
return prompt;
|
|
608
|
-
return `${systemPrompt}
|
|
609
|
-
|
|
610
|
-
${prompt}`;
|
|
611
|
-
}
|
|
612
|
-
function presetArgv(preset, sessionId, systemPrompt, extraArgs, prompt) {
|
|
613
|
-
return [
|
|
614
|
-
...preset.head(sessionId),
|
|
615
|
-
...preset.systemPromptFlag && systemPrompt ? [preset.systemPromptFlag, systemPrompt] : [],
|
|
616
|
-
...extraArgs,
|
|
617
|
-
...preset.tail,
|
|
618
|
-
...preset.promptVia === "arg" ? [presetPrompt(preset, systemPrompt, prompt)] : []
|
|
619
|
-
];
|
|
620
916
|
}
|
|
621
917
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
"
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
"antigravity-stream-json",
|
|
629
|
-
"copilot-json",
|
|
630
|
-
"pi-json"
|
|
631
|
-
];
|
|
632
|
-
function presetOf(config) {
|
|
633
|
-
if (config.command || !config.agent)
|
|
634
|
-
return;
|
|
635
|
-
return PRESETS[config.agent];
|
|
636
|
-
}
|
|
637
|
-
var DEFAULTS = {
|
|
638
|
-
concurrency: 1,
|
|
639
|
-
pollIntervalMs: 3000,
|
|
640
|
-
timeoutMs: 30 * 60 * 1000
|
|
641
|
-
};
|
|
642
|
-
var MIN_POLL_INTERVAL_MS = 1000;
|
|
643
|
-
function textOf(value) {
|
|
644
|
-
const text = typeof value === "string" ? value.trim() : "";
|
|
645
|
-
return text || undefined;
|
|
646
|
-
}
|
|
647
|
-
function intFrom(value, fallback) {
|
|
648
|
-
const parsed = typeof value === "string" ? Number.parseInt(value, 10) : value;
|
|
649
|
-
return typeof parsed === "number" && Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
650
|
-
}
|
|
651
|
-
function outputFormatFrom(value, preset) {
|
|
652
|
-
const name = textOf(value);
|
|
653
|
-
if (!name)
|
|
654
|
-
return preset?.outputFormat ?? "text";
|
|
655
|
-
const format = OUTPUT_FORMATS.find((candidate) => candidate === name);
|
|
656
|
-
if (!format) {
|
|
657
|
-
throw new Error(`outputFormat must be one of ${OUTPUT_FORMATS.join(", ")}`);
|
|
918
|
+
class UsageReader {
|
|
919
|
+
format;
|
|
920
|
+
last = null;
|
|
921
|
+
buffered = "";
|
|
922
|
+
constructor(format) {
|
|
923
|
+
this.format = format;
|
|
658
924
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
925
|
+
write(chunk) {
|
|
926
|
+
if (this.format === "text")
|
|
927
|
+
return;
|
|
928
|
+
this.buffered += chunk;
|
|
929
|
+
const lines = this.buffered.split(`
|
|
930
|
+
`);
|
|
931
|
+
this.buffered = lines.pop() ?? "";
|
|
932
|
+
for (const line of lines)
|
|
933
|
+
this.readText(line);
|
|
667
934
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
674
|
-
throw new Error("args must be an array of strings");
|
|
935
|
+
end() {
|
|
936
|
+
if (!this.buffered)
|
|
937
|
+
return;
|
|
938
|
+
this.readText(this.buffered);
|
|
939
|
+
this.buffered = "";
|
|
675
940
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
941
|
+
value() {
|
|
942
|
+
if (this.format === "copilot-json")
|
|
943
|
+
return null;
|
|
944
|
+
return this.last ?? undefined;
|
|
945
|
+
}
|
|
946
|
+
readText(line) {
|
|
947
|
+
const trimmed = line.trim();
|
|
948
|
+
if (!trimmed)
|
|
949
|
+
return;
|
|
950
|
+
try {
|
|
951
|
+
const parsed = JSON.parse(trimmed);
|
|
952
|
+
if (parsed && typeof parsed === "object")
|
|
953
|
+
this.readLine(parsed);
|
|
954
|
+
} catch {}
|
|
955
|
+
}
|
|
956
|
+
readLine(parsed) {
|
|
957
|
+
switch (this.format) {
|
|
958
|
+
case "claude-stream-json":
|
|
959
|
+
this.readClaude(parsed);
|
|
960
|
+
return;
|
|
961
|
+
case "codex-jsonl":
|
|
962
|
+
this.readCodex(parsed);
|
|
963
|
+
return;
|
|
964
|
+
case "opencode-json":
|
|
965
|
+
this.readOpencode(parsed);
|
|
966
|
+
return;
|
|
967
|
+
case "antigravity-stream-json":
|
|
968
|
+
this.readAntigravity(parsed);
|
|
969
|
+
return;
|
|
970
|
+
case "pi-json":
|
|
971
|
+
this.readPi(parsed);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
692
974
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
975
|
+
readClaude(message) {
|
|
976
|
+
const event = message.type === "stream_event" ? message.event : undefined;
|
|
977
|
+
if (!event)
|
|
978
|
+
return;
|
|
979
|
+
const usage = event.type === "message_start" ? event.message?.usage : event.usage;
|
|
980
|
+
if (!usage)
|
|
981
|
+
return;
|
|
982
|
+
const read = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
983
|
+
const written = usage.output_tokens ?? 0;
|
|
984
|
+
if (event.type === "message_start") {
|
|
985
|
+
this.last = { inputTokens: read, outputTokens: written };
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (event.type !== "message_delta")
|
|
989
|
+
return;
|
|
990
|
+
this.last = { inputTokens: read || (this.last?.inputTokens ?? 0), outputTokens: written };
|
|
697
991
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
992
|
+
readCodex(message) {
|
|
993
|
+
if (message.type !== "turn.completed" || !message.usage)
|
|
994
|
+
return;
|
|
995
|
+
this.last = {
|
|
996
|
+
inputTokens: message.usage.input_tokens ?? 0,
|
|
997
|
+
outputTokens: message.usage.output_tokens ?? 0
|
|
998
|
+
};
|
|
702
999
|
}
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
if (!
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1000
|
+
readOpencode(message) {
|
|
1001
|
+
const tokens = message.part?.type === "step-finish" ? message.part.tokens : undefined;
|
|
1002
|
+
if (!tokens)
|
|
1003
|
+
return;
|
|
1004
|
+
this.last = {
|
|
1005
|
+
inputTokens: (tokens.input ?? 0) + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0),
|
|
1006
|
+
outputTokens: (tokens.output ?? 0) + (tokens.reasoning ?? 0)
|
|
1007
|
+
};
|
|
709
1008
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1009
|
+
readAntigravity(message) {
|
|
1010
|
+
const step = message.step_update;
|
|
1011
|
+
if (!step)
|
|
1012
|
+
return;
|
|
1013
|
+
const usage = step.usage ?? message.usage;
|
|
1014
|
+
if (!usage)
|
|
1015
|
+
return;
|
|
1016
|
+
this.last = {
|
|
1017
|
+
inputTokens: (usage.input_tokens ?? 0) + (usage.cache_read_tokens ?? 0),
|
|
1018
|
+
outputTokens: (usage.output_tokens ?? 0) + (usage.thinking_tokens ?? 0)
|
|
1019
|
+
};
|
|
715
1020
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
else if (textOf(own.command))
|
|
725
|
-
delete merged.agent;
|
|
726
|
-
if (changesMode && !textOf(own.outputFormat))
|
|
727
|
-
delete merged.outputFormat;
|
|
728
|
-
return merged;
|
|
729
|
-
}
|
|
730
|
-
function nameOf(entry, index, total) {
|
|
731
|
-
if (total === 1)
|
|
732
|
-
return "";
|
|
733
|
-
return textOf(entry.name) ?? `#${index + 1}`;
|
|
734
|
-
}
|
|
735
|
-
function configFrom(fields, name, extraArgs) {
|
|
736
|
-
const agent = agentFrom(fields.agent);
|
|
737
|
-
const command = textOf(fields.command);
|
|
738
|
-
if (!agent && !command) {
|
|
739
|
-
throw new Error(`set either agent (one of ${PRESET_NAMES.join(", ")}) or command in the config file or the environment`);
|
|
1021
|
+
readPi(message) {
|
|
1022
|
+
const usage = message.usage;
|
|
1023
|
+
if (!usage)
|
|
1024
|
+
return;
|
|
1025
|
+
this.last = {
|
|
1026
|
+
inputTokens: (usage.input ?? usage.input_tokens ?? 0) + (usage.cacheRead ?? 0),
|
|
1027
|
+
outputTokens: usage.output ?? usage.output_tokens ?? 0
|
|
1028
|
+
};
|
|
740
1029
|
}
|
|
741
|
-
return {
|
|
742
|
-
name,
|
|
743
|
-
url: required(fields.url, "url").replace(/\/+$/, ""),
|
|
744
|
-
apiKey: required(fields.apiKey, "apiKey"),
|
|
745
|
-
agent,
|
|
746
|
-
command,
|
|
747
|
-
args: [...argsFrom(fields.args), ...extraArgs],
|
|
748
|
-
cwd: textOf(fields.cwd)?.replace(/^~/, process.env.HOME ?? "~"),
|
|
749
|
-
env: fields.env ?? {},
|
|
750
|
-
concurrency: intFrom(fields.concurrency, DEFAULTS.concurrency),
|
|
751
|
-
pollIntervalMs: Math.max(intFrom(fields.pollIntervalMs, DEFAULTS.pollIntervalMs), MIN_POLL_INTERVAL_MS),
|
|
752
|
-
timeoutMs: intFrom(fields.timeoutMs, DEFAULTS.timeoutMs),
|
|
753
|
-
outputFormat: outputFormatFrom(fields.outputFormat, presetOf({ agent, command }))
|
|
754
|
-
};
|
|
755
1030
|
}
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
outputFormat: env.ITSAPLAN_OUTPUT_FORMAT
|
|
768
|
-
};
|
|
769
|
-
for (const [field, value] of Object.entries(outside)) {
|
|
770
|
-
if (!textOf(value))
|
|
771
|
-
delete outside[field];
|
|
1031
|
+
function tail(text, limit) {
|
|
1032
|
+
if (text.length <= limit)
|
|
1033
|
+
return text;
|
|
1034
|
+
const mark = "…";
|
|
1035
|
+
return mark + text.slice(-(limit - mark.length));
|
|
1036
|
+
}
|
|
1037
|
+
function textOfResult(content) {
|
|
1038
|
+
if (typeof content === "string")
|
|
1039
|
+
return content;
|
|
1040
|
+
if (Array.isArray(content)) {
|
|
1041
|
+
return content.map((part) => typeof part === "object" && part && ("text" in part) ? String(part.text) : "").join("");
|
|
772
1042
|
}
|
|
773
|
-
return
|
|
1043
|
+
return "";
|
|
774
1044
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
if (entries.length === 0) {
|
|
779
|
-
throw new Error("no agents configured — apiKeys and agents cannot be empty");
|
|
1045
|
+
function piResultText(result) {
|
|
1046
|
+
if (result && typeof result === "object" && !Array.isArray(result) && "content" in result) {
|
|
1047
|
+
return textOfResult(result.content);
|
|
780
1048
|
}
|
|
781
|
-
const
|
|
782
|
-
return
|
|
1049
|
+
const text = textOfResult(result);
|
|
1050
|
+
return text || (result == null ? "" : JSON.stringify(result));
|
|
783
1051
|
}
|
|
784
1052
|
|
|
785
1053
|
// src/execute.ts
|
|
1054
|
+
import { spawn } from "node:child_process";
|
|
786
1055
|
var OUTPUT_LIMIT = 8000;
|
|
787
1056
|
var ERROR_LIMIT = 400;
|
|
788
1057
|
function tail2(text, limit) {
|
|
@@ -911,85 +1180,83 @@ async function answer(config, client, message, stop) {
|
|
|
911
1180
|
await client.chatResult(message.id, { status: "failed", error, usage: stream.contextUsage() });
|
|
912
1181
|
}
|
|
913
1182
|
|
|
914
|
-
// src/
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1183
|
+
// src/run.ts
|
|
1184
|
+
async function runTask(config, client, run) {
|
|
1185
|
+
let offset = 0;
|
|
1186
|
+
const stream = new AnswerStream(config.outputFormat, `run-${run.id}`, String(run.id), async (events) => {
|
|
1187
|
+
if (!run.traceEvents)
|
|
1188
|
+
return;
|
|
1189
|
+
const session = config.outputFormat === "codex-jsonl" ? stream.startedSession() : null;
|
|
1190
|
+
const sessionId = session && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(session) ? session : undefined;
|
|
1191
|
+
for (let start = 0;start < events.length; start += 200) {
|
|
1192
|
+
const batch = events.slice(start, start + 200);
|
|
1193
|
+
await client.runEvents(run, offset + start, batch, sessionId);
|
|
1194
|
+
}
|
|
1195
|
+
offset += events.length;
|
|
1196
|
+
});
|
|
1197
|
+
const usage = new UsageReader(config.outputFormat);
|
|
1198
|
+
const timer = setInterval(() => {
|
|
1199
|
+
stream.flush().catch(() => {});
|
|
1200
|
+
}, 500);
|
|
1201
|
+
try {
|
|
1202
|
+
const outcome = await execute(config, {
|
|
1203
|
+
prompt: run.prompt,
|
|
1204
|
+
systemPrompt: run.systemPrompt,
|
|
1205
|
+
env: {
|
|
1206
|
+
ITSAPLAN_RUN_ID: String(run.id),
|
|
1207
|
+
ITSAPLAN_TRIGGER: run.trigger,
|
|
1208
|
+
ITSAPLAN_SYSTEM_PROMPT: run.systemPrompt,
|
|
1209
|
+
ITSAPLAN_ISSUE: run.issueIdentifier ?? "",
|
|
1210
|
+
ITSAPLAN_ISSUE_ID: run.issueId == null ? "" : String(run.issueId)
|
|
1211
|
+
}
|
|
1212
|
+
}, {
|
|
1213
|
+
onData: (chunk) => {
|
|
1214
|
+
usage.write(chunk);
|
|
1215
|
+
stream.write(chunk);
|
|
1216
|
+
}
|
|
940
1217
|
});
|
|
941
|
-
|
|
942
|
-
|
|
1218
|
+
clearInterval(timer);
|
|
1219
|
+
usage.end();
|
|
1220
|
+
try {
|
|
1221
|
+
if (outcome.status === "success")
|
|
1222
|
+
await stream.finish(outcome.output);
|
|
1223
|
+
else
|
|
1224
|
+
await stream.fail(outcome.error ?? "The command failed", outcome.output);
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
if (!isTransientEventError(error))
|
|
1227
|
+
throw error;
|
|
1228
|
+
try {
|
|
1229
|
+
await stream.flush();
|
|
1230
|
+
} catch (retryError) {
|
|
1231
|
+
if (!isTransientEventError(retryError))
|
|
1232
|
+
throw retryError;
|
|
1233
|
+
console.error(`Run ${run.id}: trace upload failed; reporting the command result: ${String(retryError)}`);
|
|
1234
|
+
}
|
|
943
1235
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
async heartbeat(runId) {
|
|
952
|
-
await this.post(`/agent-runs/${runId}/heartbeat`);
|
|
953
|
-
}
|
|
954
|
-
async report(runId, result) {
|
|
955
|
-
await this.post(`/agent-runs/${runId}/result`, result);
|
|
956
|
-
}
|
|
957
|
-
async claimChat() {
|
|
958
|
-
const res = await this.post("/agent-chats/claim", undefined, CHAT_CLAIM_TIMEOUT_MS);
|
|
959
|
-
const body = await res.json();
|
|
960
|
-
return body.message;
|
|
961
|
-
}
|
|
962
|
-
async chatEvents(messageId, events, sessionId) {
|
|
963
|
-
const res = await this.post(`/agent-chats/${messageId}/events`, {
|
|
964
|
-
events,
|
|
965
|
-
...sessionId && { sessionId }
|
|
1236
|
+
const output = config.outputFormat === "text" ? outcome.output : stream.answer();
|
|
1237
|
+
await client.report(run.id, {
|
|
1238
|
+
...outcome,
|
|
1239
|
+
output,
|
|
1240
|
+
...run.traceEvents && { attempt: run.attempts },
|
|
1241
|
+
usage: usage.value()
|
|
966
1242
|
});
|
|
967
|
-
return
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
return canceled(await this.post(`/agent-chats/${messageId}/heartbeat`));
|
|
971
|
-
}
|
|
972
|
-
async chatResult(messageId, result) {
|
|
973
|
-
await this.post(`/agent-chats/${messageId}/result`, result);
|
|
1243
|
+
return outcome;
|
|
1244
|
+
} finally {
|
|
1245
|
+
clearInterval(timer);
|
|
974
1246
|
}
|
|
975
1247
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1248
|
+
function isTransientEventError(error) {
|
|
1249
|
+
if (error instanceof RequestError) {
|
|
1250
|
+
return error.status === 408 || error.status === 429 || error.status >= 500;
|
|
1251
|
+
}
|
|
1252
|
+
return error instanceof TypeError || error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
|
|
979
1253
|
}
|
|
980
1254
|
|
|
981
|
-
// src/
|
|
1255
|
+
// src/tasks.ts
|
|
982
1256
|
var HEARTBEAT_MS = 60000;
|
|
983
|
-
|
|
984
|
-
function prefixOf(name) {
|
|
985
|
-
return name ? `[itsaplan-runner ${name}]` : "[itsaplan-runner]";
|
|
986
|
-
}
|
|
987
|
-
function log(message) {
|
|
988
|
-
console.log(`${prefixOf("")} ${message}`);
|
|
989
|
-
}
|
|
990
|
-
async function withHeartbeat(log2, beat, work) {
|
|
1257
|
+
async function withHeartbeat(log, beat, work) {
|
|
991
1258
|
const timer = setInterval(() => {
|
|
992
|
-
beat().catch((err) =>
|
|
1259
|
+
beat().catch((err) => log(`heartbeat failed: ${String(err)}`));
|
|
993
1260
|
}, HEARTBEAT_MS);
|
|
994
1261
|
try {
|
|
995
1262
|
return await work;
|
|
@@ -997,53 +1264,52 @@ async function withHeartbeat(log2, beat, work) {
|
|
|
997
1264
|
clearInterval(timer);
|
|
998
1265
|
}
|
|
999
1266
|
}
|
|
1000
|
-
function
|
|
1001
|
-
return {
|
|
1002
|
-
prompt: run.prompt,
|
|
1003
|
-
systemPrompt: run.systemPrompt,
|
|
1004
|
-
env: {
|
|
1005
|
-
ITSAPLAN_RUN_ID: String(run.id),
|
|
1006
|
-
ITSAPLAN_TRIGGER: run.trigger,
|
|
1007
|
-
ITSAPLAN_SYSTEM_PROMPT: run.systemPrompt,
|
|
1008
|
-
ITSAPLAN_ISSUE: run.issueIdentifier ?? "",
|
|
1009
|
-
ITSAPLAN_ISSUE_ID: run.issueId == null ? "" : String(run.issueId)
|
|
1010
|
-
}
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
async function handle(config, client, log2, run) {
|
|
1267
|
+
async function handleRun(config, client, log, run) {
|
|
1014
1268
|
const label = run.issueIdentifier ?? `run ${run.id}`;
|
|
1015
|
-
|
|
1269
|
+
log(`${label}: started (${run.trigger})`);
|
|
1016
1270
|
try {
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
usage.end();
|
|
1020
|
-
await client.report(run.id, { ...outcome, usage: usage.value() });
|
|
1021
|
-
log2(`${label}: ${outcome.status}${outcome.error ? ` — ${outcome.error}` : ""}`);
|
|
1271
|
+
const outcome = await withHeartbeat(log, () => client.heartbeat(run.id, run.traceEvents ? run.attempts : undefined), runTask(config, client, run));
|
|
1272
|
+
log(`${label}: ${outcome.status}${outcome.error ? ` — ${outcome.error}` : ""}`);
|
|
1022
1273
|
} catch (err) {
|
|
1023
1274
|
const message = err instanceof Error ? err.message : String(err);
|
|
1024
|
-
|
|
1025
|
-
await client.report(run.id, {
|
|
1275
|
+
log(`${label}: runner error — ${message}`);
|
|
1276
|
+
await client.report(run.id, {
|
|
1277
|
+
status: "failed",
|
|
1278
|
+
error: message,
|
|
1279
|
+
...run.traceEvents && { attempt: run.attempts }
|
|
1280
|
+
}).catch(() => {});
|
|
1026
1281
|
}
|
|
1027
1282
|
}
|
|
1028
|
-
async function handleChat(config, client,
|
|
1029
|
-
|
|
1283
|
+
async function handleChat(config, client, log, message) {
|
|
1284
|
+
log(`chat ${message.id}: answering`);
|
|
1030
1285
|
const stop = new AbortController;
|
|
1031
1286
|
try {
|
|
1032
|
-
await withHeartbeat(
|
|
1287
|
+
await withHeartbeat(log, async () => {
|
|
1033
1288
|
if (await client.chatHeartbeat(message.id))
|
|
1034
1289
|
stop.abort();
|
|
1035
1290
|
}, answer(config, client, message, stop));
|
|
1036
1291
|
} catch (err) {
|
|
1037
1292
|
if (!stop.signal.aborted) {
|
|
1038
1293
|
const text = err instanceof Error ? err.message : String(err);
|
|
1039
|
-
|
|
1294
|
+
log(`chat ${message.id}: runner error — ${text}`);
|
|
1040
1295
|
await client.chatResult(message.id, { status: "failed", error: text }).catch(() => {});
|
|
1041
1296
|
return;
|
|
1042
1297
|
}
|
|
1043
1298
|
}
|
|
1044
|
-
|
|
1299
|
+
log(`chat ${message.id}: ${stop.signal.aborted ? "stopped" : "answered"}`);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/cli.ts
|
|
1303
|
+
var ERROR_BACKOFF_MS = 5000;
|
|
1304
|
+
function prefixOf(name) {
|
|
1305
|
+
return name ? `[itsaplan-runner ${name}]` : "[itsaplan-runner]";
|
|
1306
|
+
}
|
|
1307
|
+
function logFor(name) {
|
|
1308
|
+
const prefix = prefixOf(name);
|
|
1309
|
+
return (message) => console.log(`${prefix} ${message}`);
|
|
1045
1310
|
}
|
|
1046
|
-
|
|
1311
|
+
var log = logFor("");
|
|
1312
|
+
async function drain(state, log, concurrency, take, run, onEmpty) {
|
|
1047
1313
|
const active = new Set;
|
|
1048
1314
|
let done = false;
|
|
1049
1315
|
while (!state.stopping && !done) {
|
|
@@ -1057,7 +1323,7 @@ async function drain(state, log2, concurrency, take, run, onEmpty) {
|
|
|
1057
1323
|
} catch (err) {
|
|
1058
1324
|
if (err instanceof RequestError && (err.status === 401 || err.status === 403))
|
|
1059
1325
|
throw err;
|
|
1060
|
-
|
|
1326
|
+
log(`claim failed: ${String(err)}`);
|
|
1061
1327
|
await sleep(ERROR_BACKOFF_MS);
|
|
1062
1328
|
continue;
|
|
1063
1329
|
}
|
|
@@ -1095,36 +1361,68 @@ function parseArgv(argv) {
|
|
|
1095
1361
|
}
|
|
1096
1362
|
return parsed;
|
|
1097
1363
|
}
|
|
1098
|
-
async function
|
|
1364
|
+
async function poll(state, config) {
|
|
1099
1365
|
const client = new Client(config);
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
log2(`running ${config.agent ?? "the configured command"}, polling ${config.url} every ` + `${config.pollIntervalMs}ms, up to ${config.concurrency} at once`);
|
|
1366
|
+
const log = logFor(config.name);
|
|
1367
|
+
log(`running ${config.agent ?? "the configured command"}, polling ${config.url} every ` + `${config.pollIntervalMs}ms, up to ${config.concurrency} at once`);
|
|
1103
1368
|
let chatSupported = true;
|
|
1104
1369
|
await Promise.all([
|
|
1105
|
-
drain(state,
|
|
1370
|
+
drain(state, log, config.concurrency, () => client.claim(), (run) => handleRun(config, client, log, run), async () => {
|
|
1106
1371
|
await sleep(config.pollIntervalMs);
|
|
1107
1372
|
return true;
|
|
1108
1373
|
}),
|
|
1109
|
-
drain(state,
|
|
1374
|
+
drain(state, log, config.concurrency, async () => {
|
|
1110
1375
|
try {
|
|
1111
1376
|
return await client.claimChat();
|
|
1112
1377
|
} catch (err) {
|
|
1113
1378
|
if (err instanceof RequestError && err.status === 404) {
|
|
1114
|
-
|
|
1379
|
+
log("this instance has no chat feed — only queued runs will be answered");
|
|
1115
1380
|
chatSupported = false;
|
|
1116
1381
|
return null;
|
|
1117
1382
|
}
|
|
1118
1383
|
throw err;
|
|
1119
1384
|
}
|
|
1120
|
-
}, (message) => handleChat(config, client,
|
|
1385
|
+
}, (message) => handleChat(config, client, log, message), () => Promise.resolve(chatSupported))
|
|
1121
1386
|
]);
|
|
1122
1387
|
}
|
|
1388
|
+
async function listenOn(state, port, configs) {
|
|
1389
|
+
const listener = await startListener(port, configs.map((config) => {
|
|
1390
|
+
const client = new Client(config);
|
|
1391
|
+
const log = logFor(config.name);
|
|
1392
|
+
log(`running ${config.agent ?? "the configured command"} for the tasks ${config.url} ` + `sends to port ${port}, up to ${config.concurrency} at once`);
|
|
1393
|
+
return {
|
|
1394
|
+
secret: config.signingSecret,
|
|
1395
|
+
concurrency: config.concurrency,
|
|
1396
|
+
run: (run) => handleRun(config, client, log, run),
|
|
1397
|
+
chat: (message) => handleChat(config, client, log, message)
|
|
1398
|
+
};
|
|
1399
|
+
}), log);
|
|
1400
|
+
log(`listening on port ${listener.port}`);
|
|
1401
|
+
await state.stopped;
|
|
1402
|
+
await listener.close();
|
|
1403
|
+
}
|
|
1404
|
+
function byPort(configs) {
|
|
1405
|
+
const ports = new Map;
|
|
1406
|
+
for (const config of configs) {
|
|
1407
|
+
if (config.listen === undefined)
|
|
1408
|
+
continue;
|
|
1409
|
+
ports.set(config.listen, [...ports.get(config.listen) ?? [], config]);
|
|
1410
|
+
}
|
|
1411
|
+
return ports;
|
|
1412
|
+
}
|
|
1413
|
+
function settle(name, work) {
|
|
1414
|
+
return work.then(() => true, (err) => {
|
|
1415
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1416
|
+
console.error(`${prefixOf(name)} stopped — ${message}`);
|
|
1417
|
+
return false;
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1123
1420
|
async function main() {
|
|
1124
1421
|
const cli = parseArgv(process.argv.slice(2));
|
|
1125
1422
|
const configPath = cli.configPath ?? process.env.ITSAPLAN_RUNNER_CONFIG ?? "./itsaplan-runner.json";
|
|
1126
1423
|
const configs = await loadConfig(configPath, { agent: cli.agent, args: cli.args });
|
|
1127
|
-
|
|
1424
|
+
let stop;
|
|
1425
|
+
const state = { stopping: false, stopped: new Promise((resolve) => stop = resolve) };
|
|
1128
1426
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
1129
1427
|
process.on(signal, () => {
|
|
1130
1428
|
if (state.stopping) {
|
|
@@ -1132,14 +1430,14 @@ async function main() {
|
|
|
1132
1430
|
process.exit(1);
|
|
1133
1431
|
}
|
|
1134
1432
|
state.stopping = true;
|
|
1433
|
+
stop();
|
|
1135
1434
|
log("stopping — finishing the tasks in flight, press again to quit now");
|
|
1136
1435
|
});
|
|
1137
1436
|
}
|
|
1138
|
-
const served = await Promise.all(
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
})));
|
|
1437
|
+
const served = await Promise.all([
|
|
1438
|
+
...configs.filter((config) => config.listen === undefined).map((config) => settle(config.name, poll(state, config))),
|
|
1439
|
+
...[...byPort(configs)].map(([port, listening]) => settle(listening.length === 1 ? listening[0].name : "", listenOn(state, port, listening)))
|
|
1440
|
+
]);
|
|
1143
1441
|
if (served.includes(false))
|
|
1144
1442
|
process.exit(1);
|
|
1145
1443
|
}
|