@itsaplan/runner 0.6.0 → 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 +70 -52
- package/dist/cli.js +130 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ This package runs such agents on your own machine — one, or several at once.
|
|
|
11
11
|
Pi, and Oh My Pi (`omp`). Each preset sets the unattended flags and the session resume.
|
|
12
12
|
- Runs your own `command` instead, if you prefer. The command takes the task on stdin.
|
|
13
13
|
- Answers the agent's chat. Sends the text and the tool calls while the CLI prints them.
|
|
14
|
+
- Sends the messages and the tool calls of a queued run to the issue while the run works.
|
|
14
15
|
- Receives the tasks on a port instead of polling, for an agent whose Connection is
|
|
15
16
|
Endpoint. See [Receiving tasks on a port](#receiving-tasks-on-a-port).
|
|
16
17
|
|
|
@@ -107,9 +108,8 @@ on stdin. The runner keeps no session for it, because it does not know how to re
|
|
|
107
108
|
|
|
108
109
|
## Several agents on one runner
|
|
109
110
|
|
|
110
|
-
One key is one agent.
|
|
111
|
-
|
|
112
|
-
directory and the arguments the file already gives.
|
|
111
|
+
One key is one agent. One runner can serve several agents. It polls for each key
|
|
112
|
+
separately, and every agent uses the `agent`, `cwd` and `args` from the file.
|
|
113
113
|
|
|
114
114
|
```json
|
|
115
115
|
{
|
|
@@ -120,13 +120,13 @@ directory and the arguments the file already gives.
|
|
|
120
120
|
}
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
The runner gives each command its own `ITSAPLAN_API_KEY`, so one MCP config in that
|
|
124
|
+
folder works for all agents.
|
|
125
125
|
|
|
126
|
-
Use `agents` instead of `apiKeys`
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
Use `agents` instead of `apiKeys` when an agent needs its own settings. Each entry sets
|
|
127
|
+
its own `apiKey`. The other fields that an entry sets replace the shared ones, and the
|
|
128
|
+
entry inherits the rest. `name` is the agent's label in the log. Without it, the log
|
|
129
|
+
numbers the agents `#1`, `#2` in the order of the file.
|
|
130
130
|
|
|
131
131
|
```json
|
|
132
132
|
{
|
|
@@ -144,12 +144,11 @@ agent; without it they number the agents `#1`, `#2` in the order of the file.
|
|
|
144
144
|
}
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
`concurrency` counts per agent and per feed
|
|
148
|
-
`concurrency: 2` can
|
|
149
|
-
|
|
150
|
-
options describe every agent at once; an entry has priority over them.
|
|
147
|
+
`concurrency` counts per agent and per feed. A runner with three agents at
|
|
148
|
+
`concurrency: 2` can run six queued runs and six chat answers at once, each in its own
|
|
149
|
+
CLI process.
|
|
151
150
|
|
|
152
|
-
|
|
151
|
+
If the instance refuses the key of an agent, that agent stops. The other agents continue.
|
|
153
152
|
|
|
154
153
|
## Receiving tasks on a port
|
|
155
154
|
|
|
@@ -157,8 +156,8 @@ By default the runner polls the instance for tasks. With `listen`, the instance
|
|
|
157
156
|
task to the runner when the task is created. The runner then does not poll.
|
|
158
157
|
|
|
159
158
|
Use `listen` for a runner on a server that the instance can reach. On a laptop behind a
|
|
160
|
-
router, use polling. Polling needs no setup
|
|
161
|
-
claim waits on the server until a message comes.
|
|
159
|
+
router, use polling. Polling needs no setup. The chat is as fast with polling, because a
|
|
160
|
+
chat claim waits on the server until a message comes.
|
|
162
161
|
|
|
163
162
|
**1.** In the agent's settings, open Connection and select Endpoint. Enter the public
|
|
164
163
|
HTTPS address of the runner's port (see [Reaching the port](#reaching-the-port)), and
|
|
@@ -243,23 +242,23 @@ npx -y @itsaplan/runner /path/to/config.json
|
|
|
243
242
|
The environment variables have priority over the file. The command-line options have
|
|
244
243
|
priority over both. An `agents` entry has priority over all three, for the fields it sets.
|
|
245
244
|
|
|
246
|
-
| Field | Environment variable | Default | What it does
|
|
247
|
-
| ---------------- | --------------------------- | ------------------ |
|
|
248
|
-
| `url` | `ITSAPLAN_URL` | required | Your Itsaplan instance
|
|
249
|
-
| `apiKey` | `ITSAPLAN_API_KEY` | required | The external agent's key. Replaced by `apiKeys` or `agents`
|
|
250
|
-
| `apiKeys` | | — | Several keys, all with these settings. Not with `agents`
|
|
245
|
+
| Field | Environment variable | Default | What it does |
|
|
246
|
+
| ---------------- | --------------------------- | ------------------ | ---------------------------------------------------------------------- |
|
|
247
|
+
| `url` | `ITSAPLAN_URL` | required | Your Itsaplan instance |
|
|
248
|
+
| `apiKey` | `ITSAPLAN_API_KEY` | required | The external agent's key. Replaced by `apiKeys` or `agents` |
|
|
249
|
+
| `apiKeys` | | — | Several keys, all with these settings. Not with `agents` |
|
|
251
250
|
| `agents` | | — | Several agents, each with its own key and settings. Not with `apiKeys` |
|
|
252
|
-
| `agent` | `ITSAPLAN_AGENT` | — | The coding agent to run
|
|
253
|
-
| `args` | | `[]` | Arguments added after the preset's
|
|
254
|
-
| `command` | `ITSAPLAN_COMMAND` | — | Your own command, instead of `agent`
|
|
255
|
-
| `cwd` | `ITSAPLAN_CWD` | where you start it | Working directory for the command
|
|
256
|
-
| `env` | | `{}` | Extra variables for the command
|
|
257
|
-
| `concurrency` | `ITSAPLAN_CONCURRENCY` | `1` | Tasks at once, per agent. Queued runs and chat answers count apart
|
|
258
|
-
| `pollIntervalMs` | `ITSAPLAN_POLL_INTERVAL_MS` | `3000` | Wait after an empty queue. Minimum 1000
|
|
259
|
-
| `timeoutMs` | `ITSAPLAN_TIMEOUT_MS` | `1800000` | Time before the runner stops a task
|
|
260
|
-
| `outputFormat` | `ITSAPLAN_OUTPUT_FORMAT` | the preset's | How the runner reads
|
|
261
|
-
| `listen` | `ITSAPLAN_LISTEN` | — | Port that receives the tasks. The runner does not poll for the agent
|
|
262
|
-
| `signingSecret` | `ITSAPLAN_SIGNING_SECRET` | — | The agent's signing secret. Required with `listen`
|
|
251
|
+
| `agent` | `ITSAPLAN_AGENT` | — | The coding agent to run |
|
|
252
|
+
| `args` | | `[]` | Arguments added after the preset's |
|
|
253
|
+
| `command` | `ITSAPLAN_COMMAND` | — | Your own command, instead of `agent` |
|
|
254
|
+
| `cwd` | `ITSAPLAN_CWD` | where you start it | Working directory for the command |
|
|
255
|
+
| `env` | | `{}` | Extra variables for the command |
|
|
256
|
+
| `concurrency` | `ITSAPLAN_CONCURRENCY` | `1` | Tasks at once, per agent. Queued runs and chat answers count apart |
|
|
257
|
+
| `pollIntervalMs` | `ITSAPLAN_POLL_INTERVAL_MS` | `3000` | Wait after an empty queue. Minimum 1000 |
|
|
258
|
+
| `timeoutMs` | `ITSAPLAN_TIMEOUT_MS` | `1800000` | Time before the runner stops a task |
|
|
259
|
+
| `outputFormat` | `ITSAPLAN_OUTPUT_FORMAT` | the preset's | How the runner reads the output. See [The output](#the-output) |
|
|
260
|
+
| `listen` | `ITSAPLAN_LISTEN` | — | Port that receives the tasks. The runner does not poll for the agent |
|
|
261
|
+
| `signingSecret` | `ITSAPLAN_SIGNING_SECRET` | — | The agent's signing secret. Required with `listen` |
|
|
263
262
|
|
|
264
263
|
Set `agent` or `command`. With the environment you need no file at all:
|
|
265
264
|
|
|
@@ -274,8 +273,8 @@ npx -y @itsaplan/runner --agent claude
|
|
|
274
273
|
|
|
275
274
|
The server writes the task text. The text states what occurred, what to do, and to post
|
|
276
275
|
the result as a comment on the issue. Your own `command` receives the text on **stdin**. A
|
|
277
|
-
preset gives it to its CLI in the way that CLI accepts it: on stdin for `claude`
|
|
278
|
-
`
|
|
276
|
+
preset gives it to its CLI in the way that CLI accepts it: on stdin for `claude`, `codex`
|
|
277
|
+
and `pi`, as an argument for `antigravity`, `copilot`, `opencode` and `omp`.
|
|
279
278
|
|
|
280
279
|
The agent reads all other data through the MCP server at `$ITSAPLAN_URL/mcp`. The agent
|
|
281
280
|
sends `$ITSAPLAN_API_KEY` as a bearer token, and acts as its own user with its role.
|
|
@@ -290,12 +289,11 @@ Every run holds these variables:
|
|
|
290
289
|
| `ITSAPLAN_API_KEY` | the agent's key, for the API and the MCP server |
|
|
291
290
|
| `ITSAPLAN_TRIGGER` | `mention`, `delegation`, `field`, `schedule`, `manual`, `status`, or `chat` |
|
|
292
291
|
| `ITSAPLAN_SYSTEM_PROMPT` | the context of the run, for a command that takes a system prompt |
|
|
293
|
-
| `ITSAPLAN_TIMEOUT_MS`
|
|
292
|
+
| `ITSAPLAN_TIMEOUT_MS` | the task timeout in milliseconds |
|
|
294
293
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
command count toward the same limit.
|
|
294
|
+
A command can read `ITSAPLAN_TIMEOUT_MS` to set its own timeout. The value is the
|
|
295
|
+
`timeoutMs` of that agent, and it replaces a value from the environment or from `env`.
|
|
296
|
+
The time starts when the command starts.
|
|
299
297
|
|
|
300
298
|
A queued run adds three more:
|
|
301
299
|
|
|
@@ -314,25 +312,46 @@ A chat message adds three others in their place, and `ITSAPLAN_TRIGGER` is `chat
|
|
|
314
312
|
| `ITSAPLAN_SESSION_ID` | the coding agent's session to resume. Empty for the first message |
|
|
315
313
|
|
|
316
314
|
The task text of a chat message includes the conversation, unless a session holds it. See
|
|
317
|
-
Sessions
|
|
315
|
+
[Sessions](#sessions).
|
|
318
316
|
|
|
319
|
-
|
|
317
|
+
## The output
|
|
320
318
|
|
|
321
|
-
Exit code `0` is a success. Any other code is a failure
|
|
322
|
-
|
|
319
|
+
Exit code `0` is a success. Any other code is a failure, and the end of stderr becomes the
|
|
320
|
+
error.
|
|
323
321
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
The runner streams the answer to the chat while the command runs. It reports the tool
|
|
327
|
-
calls with the answer. `outputFormat` tells the runner how to read the output:
|
|
322
|
+
`outputFormat` tells the runner how to read stdout:
|
|
328
323
|
|
|
329
|
-
- `text` — all
|
|
324
|
+
- `text` — all of stdout is the answer.
|
|
330
325
|
- `claude-stream-json`, `codex-jsonl`, `opencode-json`, `antigravity-stream-json`,
|
|
331
326
|
`copilot-json`, `pi-json` — the event stream of that CLI. `pi` and `omp` both use
|
|
332
|
-
`pi-json`.
|
|
327
|
+
`pi-json`. The runner reads the text and the tool calls from it.
|
|
333
328
|
|
|
334
329
|
A preset sets `outputFormat` for you. Set it yourself only with your own `command`. If the
|
|
335
|
-
output does not agree with the format, the runner
|
|
330
|
+
output does not agree with the format, the runner uses the output as plain text.
|
|
331
|
+
|
|
332
|
+
A queued run reports one answer as its output. With an event stream format, the answer is
|
|
333
|
+
the text that the agent wrote after its last tool call.
|
|
334
|
+
|
|
335
|
+
### Run transcripts
|
|
336
|
+
|
|
337
|
+
While a queued run works, the runner sends the agent's messages and tool calls to the
|
|
338
|
+
instance. The issue shows them as the run's transcript.
|
|
339
|
+
|
|
340
|
+
- The tool calls need an event stream format. With `text`, the transcript holds only the
|
|
341
|
+
output.
|
|
342
|
+
- With `codex-jsonl`, the run shows an **Open in Codex** link. The link opens the session
|
|
343
|
+
only where that session is: in the Codex app of the viewer, or on a connected host. The
|
|
344
|
+
link does not copy the session.
|
|
345
|
+
- The instance keeps up to 2,000 events or 1 MB for each attempt, and marks a transcript
|
|
346
|
+
that it cut. The cut does not change the result of the run.
|
|
347
|
+
- If the last upload fails with a network or server error, the runner tries it one more
|
|
348
|
+
time. Then it reports the result of the command, even with an incomplete transcript.
|
|
349
|
+
Any other refusal fails the run.
|
|
350
|
+
|
|
351
|
+
## Chat
|
|
352
|
+
|
|
353
|
+
The runner streams the answer to the chat while the command runs. It sends the tool calls
|
|
354
|
+
with the answer.
|
|
336
355
|
|
|
337
356
|
### Sessions
|
|
338
357
|
|
|
@@ -348,8 +367,7 @@ Without a session the server sends the last 20 messages of the conversation as p
|
|
|
348
367
|
## Requirements
|
|
349
368
|
|
|
350
369
|
- **Node 20 or later.**
|
|
351
|
-
- **An It's a Plan instance
|
|
352
|
-
gives the runner only the queued runs. `listen` needs 1.6.0.
|
|
370
|
+
- **An It's a Plan instance 1.7.0 or later.**
|
|
353
371
|
- **An external agent in your project.** The runner authenticates with its API key.
|
|
354
372
|
- **The coding agent, installed and signed in on the same machine.** The runner starts the
|
|
355
373
|
CLI as you, but it cannot sign in for you.
|
package/dist/cli.js
CHANGED
|
@@ -40,12 +40,20 @@ class Client {
|
|
|
40
40
|
const body = await res.json();
|
|
41
41
|
return body.run;
|
|
42
42
|
}
|
|
43
|
-
async heartbeat(runId) {
|
|
44
|
-
await this.post(`/agent-runs/${runId}/heartbeat
|
|
43
|
+
async heartbeat(runId, attempt) {
|
|
44
|
+
await this.post(`/agent-runs/${runId}/heartbeat`, attempt == null ? undefined : { attempt });
|
|
45
45
|
}
|
|
46
46
|
async report(runId, result) {
|
|
47
47
|
await this.post(`/agent-runs/${runId}/result`, result);
|
|
48
48
|
}
|
|
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
|
+
});
|
|
56
|
+
}
|
|
49
57
|
async claimChat() {
|
|
50
58
|
const res = await this.post("/agent-chats/claim", undefined, CHAT_CLAIM_TIMEOUT_MS);
|
|
51
59
|
const body = await res.json();
|
|
@@ -114,7 +122,7 @@ var PRESETS = {
|
|
|
114
122
|
"json",
|
|
115
123
|
...sessionId ? ["--session", sessionId] : []
|
|
116
124
|
],
|
|
117
|
-
tail: []
|
|
125
|
+
tail: ["--"]
|
|
118
126
|
},
|
|
119
127
|
antigravity: {
|
|
120
128
|
bin: "agy",
|
|
@@ -146,7 +154,7 @@ var PRESETS = {
|
|
|
146
154
|
pi: {
|
|
147
155
|
bin: "pi",
|
|
148
156
|
outputFormat: "pi-json",
|
|
149
|
-
promptVia: "
|
|
157
|
+
promptVia: "stdin",
|
|
150
158
|
systemPromptFlag: "--append-system-prompt",
|
|
151
159
|
head: (sessionId) => [
|
|
152
160
|
"-p",
|
|
@@ -168,7 +176,7 @@ var PRESETS = {
|
|
|
168
176
|
"--auto-approve",
|
|
169
177
|
...sessionId ? ["--resume", sessionId] : []
|
|
170
178
|
],
|
|
171
|
-
tail: []
|
|
179
|
+
tail: ["--"]
|
|
172
180
|
}
|
|
173
181
|
};
|
|
174
182
|
var PRESET_NAMES = Object.keys(PRESETS);
|
|
@@ -538,6 +546,8 @@ class AnswerStream {
|
|
|
538
546
|
closedToolCalls = new Set;
|
|
539
547
|
sawPartialText = false;
|
|
540
548
|
sawAnyText = false;
|
|
549
|
+
segment = "";
|
|
550
|
+
lastSegment = "";
|
|
541
551
|
sending = Promise.resolve();
|
|
542
552
|
usage;
|
|
543
553
|
constructor(format, threadId, runId, send) {
|
|
@@ -564,6 +574,9 @@ class AnswerStream {
|
|
|
564
574
|
startedSession() {
|
|
565
575
|
return this.sessionId;
|
|
566
576
|
}
|
|
577
|
+
answer() {
|
|
578
|
+
return (this.segment.trim() ? this.segment : this.lastSegment).trim();
|
|
579
|
+
}
|
|
567
580
|
contextUsage() {
|
|
568
581
|
return this.usage.value();
|
|
569
582
|
}
|
|
@@ -612,6 +625,7 @@ class AnswerStream {
|
|
|
612
625
|
return;
|
|
613
626
|
this.sawAnyText = true;
|
|
614
627
|
this.text += text;
|
|
628
|
+
this.segment += text;
|
|
615
629
|
if (this.text.length >= FLUSH_CHARS)
|
|
616
630
|
this.drainText();
|
|
617
631
|
}
|
|
@@ -774,15 +788,36 @@ class AnswerStream {
|
|
|
774
788
|
return;
|
|
775
789
|
}
|
|
776
790
|
const item = message.item;
|
|
777
|
-
if (
|
|
791
|
+
if (!item)
|
|
778
792
|
return;
|
|
779
|
-
if (item.type === "agent_message" && item.text) {
|
|
793
|
+
if (message.type === "item.completed" && item.type === "agent_message" && item.text) {
|
|
780
794
|
this.appendText(item.text);
|
|
781
795
|
return;
|
|
782
796
|
}
|
|
783
|
-
if (item.
|
|
784
|
-
|
|
785
|
-
|
|
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);
|
|
786
821
|
}
|
|
787
822
|
}
|
|
788
823
|
readOpencodeLine(message) {
|
|
@@ -860,10 +895,17 @@ class AnswerStream {
|
|
|
860
895
|
}
|
|
861
896
|
pushToolCall(toolCallId, toolCallName, args) {
|
|
862
897
|
this.drainText();
|
|
898
|
+
this.closeSegment();
|
|
863
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 });
|
|
864
900
|
}
|
|
901
|
+
closeSegment() {
|
|
902
|
+
if (this.segment.trim())
|
|
903
|
+
this.lastSegment = this.segment;
|
|
904
|
+
this.segment = "";
|
|
905
|
+
}
|
|
865
906
|
pushToolResult(toolCallId, content) {
|
|
866
907
|
this.drainText();
|
|
908
|
+
this.closeSegment();
|
|
867
909
|
this.queued.push({
|
|
868
910
|
type: "TOOL_CALL_RESULT",
|
|
869
911
|
messageId: this.messageId,
|
|
@@ -1138,6 +1180,78 @@ async function answer(config, client, message, stop) {
|
|
|
1138
1180
|
await client.chatResult(message.id, { status: "failed", error, usage: stream.contextUsage() });
|
|
1139
1181
|
}
|
|
1140
1182
|
|
|
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
|
+
}
|
|
1217
|
+
});
|
|
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
|
+
}
|
|
1235
|
+
}
|
|
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()
|
|
1242
|
+
});
|
|
1243
|
+
return outcome;
|
|
1244
|
+
} finally {
|
|
1245
|
+
clearInterval(timer);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
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);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1141
1255
|
// src/tasks.ts
|
|
1142
1256
|
var HEARTBEAT_MS = 60000;
|
|
1143
1257
|
async function withHeartbeat(log, beat, work) {
|
|
@@ -1150,32 +1264,20 @@ async function withHeartbeat(log, beat, work) {
|
|
|
1150
1264
|
clearInterval(timer);
|
|
1151
1265
|
}
|
|
1152
1266
|
}
|
|
1153
|
-
function taskOf(run) {
|
|
1154
|
-
return {
|
|
1155
|
-
prompt: run.prompt,
|
|
1156
|
-
systemPrompt: run.systemPrompt,
|
|
1157
|
-
env: {
|
|
1158
|
-
ITSAPLAN_RUN_ID: String(run.id),
|
|
1159
|
-
ITSAPLAN_TRIGGER: run.trigger,
|
|
1160
|
-
ITSAPLAN_SYSTEM_PROMPT: run.systemPrompt,
|
|
1161
|
-
ITSAPLAN_ISSUE: run.issueIdentifier ?? "",
|
|
1162
|
-
ITSAPLAN_ISSUE_ID: run.issueId == null ? "" : String(run.issueId)
|
|
1163
|
-
}
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
1267
|
async function handleRun(config, client, log, run) {
|
|
1167
1268
|
const label = run.issueIdentifier ?? `run ${run.id}`;
|
|
1168
1269
|
log(`${label}: started (${run.trigger})`);
|
|
1169
1270
|
try {
|
|
1170
|
-
const
|
|
1171
|
-
const outcome = await withHeartbeat(log, () => client.heartbeat(run.id), execute(config, taskOf(run), { onData: (chunk) => usage.write(chunk) }));
|
|
1172
|
-
usage.end();
|
|
1173
|
-
await client.report(run.id, { ...outcome, usage: usage.value() });
|
|
1271
|
+
const outcome = await withHeartbeat(log, () => client.heartbeat(run.id, run.traceEvents ? run.attempts : undefined), runTask(config, client, run));
|
|
1174
1272
|
log(`${label}: ${outcome.status}${outcome.error ? ` — ${outcome.error}` : ""}`);
|
|
1175
1273
|
} catch (err) {
|
|
1176
1274
|
const message = err instanceof Error ? err.message : String(err);
|
|
1177
1275
|
log(`${label}: runner error — ${message}`);
|
|
1178
|
-
await client.report(run.id, {
|
|
1276
|
+
await client.report(run.id, {
|
|
1277
|
+
status: "failed",
|
|
1278
|
+
error: message,
|
|
1279
|
+
...run.traceEvents && { attempt: run.attempts }
|
|
1280
|
+
}).catch(() => {});
|
|
1179
1281
|
}
|
|
1180
1282
|
}
|
|
1181
1283
|
async function handleChat(config, client, log, message) {
|