@itsaplan/runner 0.6.0 → 0.8.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 +86 -66
- package/dist/cli.js +196 -32
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -8,9 +8,11 @@ This package runs such agents on your own machine — one, or several at once.
|
|
|
8
8
|
- Polls your instance for the agent's queued runs. Runs each one with a coding agent CLI.
|
|
9
9
|
Reports the result.
|
|
10
10
|
- Has presets for Claude Code, Codex, opencode, Antigravity CLI, GitHub Copilot CLI,
|
|
11
|
-
Pi,
|
|
11
|
+
Pi, Oh My Pi (`omp`), and Mastra Code. Each preset sets the unattended flags and the
|
|
12
|
+
session resume.
|
|
12
13
|
- Runs your own `command` instead, if you prefer. The command takes the task on stdin.
|
|
13
14
|
- Answers the agent's chat. Sends the text and the tool calls while the CLI prints them.
|
|
15
|
+
- Sends the messages and the tool calls of a queued run to the issue while the run works.
|
|
14
16
|
- Receives the tasks on a port instead of polling, for an agent whose Connection is
|
|
15
17
|
Endpoint. See [Receiving tasks on a port](#receiving-tasks-on-a-port).
|
|
16
18
|
|
|
@@ -22,9 +24,9 @@ of kind External. The key appears one time only, at creation.
|
|
|
22
24
|
**2. Enable MCP** for the project, in Settings, MCP Server. MCP is off by default. The
|
|
23
25
|
agent reads the issue and writes its result through it.
|
|
24
26
|
|
|
25
|
-
**3. Give the coding agent the address of that server.** Claude Code, Oh My Pi,
|
|
26
|
-
(through `pi-mcp-adapter`) read `.mcp.json` from the folder they run in. For
|
|
27
|
-
CLIs, see
|
|
27
|
+
**3. Give the coding agent the address of that server.** Claude Code, Oh My Pi, Mastra
|
|
28
|
+
Code, and Pi (through `pi-mcp-adapter`) read `.mcp.json` from the folder they run in. For
|
|
29
|
+
the other CLIs, see
|
|
28
30
|
[Coding agent setup](https://github.com/croffasia/itsaplan/blob/main/docs/runner.md).
|
|
29
31
|
|
|
30
32
|
```json
|
|
@@ -66,15 +68,16 @@ or mention it in a comment. The task then starts in your terminal.
|
|
|
66
68
|
`agent` names a CLI that the runner knows. The runner then builds the command itself: the
|
|
67
69
|
flags for an unattended run, and the session resume. Every preset keeps a chat session.
|
|
68
70
|
|
|
69
|
-
| Agent | Runs
|
|
70
|
-
| ------------- |
|
|
71
|
-
| `claude` | Claude Code
|
|
72
|
-
| `codex` | Codex CLI
|
|
73
|
-
| `opencode` | opencode
|
|
74
|
-
| `antigravity` | Antigravity CLI
|
|
75
|
-
| `copilot` | GitHub Copilot CLI
|
|
76
|
-
| `pi` | Pi
|
|
77
|
-
| `omp` | Oh My Pi
|
|
71
|
+
| Agent | Runs |
|
|
72
|
+
| ------------- | ---------------------------------------------------------------------------- |
|
|
73
|
+
| `claude` | [Claude Code](https://code.claude.com/docs/en/overview) |
|
|
74
|
+
| `codex` | [Codex CLI](https://github.com/openai/codex) |
|
|
75
|
+
| `opencode` | [opencode](https://opencode.ai) |
|
|
76
|
+
| `antigravity` | [Antigravity CLI](https://antigravity.google/docs/getting-started?tab=cli) |
|
|
77
|
+
| `copilot` | [GitHub Copilot CLI](https://github.com/github/copilot-cli) |
|
|
78
|
+
| `pi` | [Pi](https://pi.dev) |
|
|
79
|
+
| `omp` | [Oh My Pi](https://omp.sh) |
|
|
80
|
+
| `mastracode` | [Mastra Code](https://code.mastra.ai) |
|
|
78
81
|
|
|
79
82
|
Each preset needs the CLI's own MCP config.
|
|
80
83
|
[Coding agent setup](https://github.com/croffasia/itsaplan/blob/main/docs/runner.md) holds
|
|
@@ -107,9 +110,8 @@ on stdin. The runner keeps no session for it, because it does not know how to re
|
|
|
107
110
|
|
|
108
111
|
## Several agents on one runner
|
|
109
112
|
|
|
110
|
-
One key is one agent.
|
|
111
|
-
|
|
112
|
-
directory and the arguments the file already gives.
|
|
113
|
+
One key is one agent. One runner can serve several agents. It polls for each key
|
|
114
|
+
separately, and every agent uses the `agent`, `cwd` and `args` from the file.
|
|
113
115
|
|
|
114
116
|
```json
|
|
115
117
|
{
|
|
@@ -120,13 +122,13 @@ directory and the arguments the file already gives.
|
|
|
120
122
|
}
|
|
121
123
|
```
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
The runner gives each command its own `ITSAPLAN_API_KEY`, so one MCP config in that
|
|
126
|
+
folder works for all agents.
|
|
125
127
|
|
|
126
|
-
Use `agents` instead of `apiKeys`
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
Use `agents` instead of `apiKeys` when an agent needs its own settings. Each entry sets
|
|
129
|
+
its own `apiKey`. The other fields that an entry sets replace the shared ones, and the
|
|
130
|
+
entry inherits the rest. `name` is the agent's label in the log. Without it, the log
|
|
131
|
+
numbers the agents `#1`, `#2` in the order of the file.
|
|
130
132
|
|
|
131
133
|
```json
|
|
132
134
|
{
|
|
@@ -144,12 +146,11 @@ agent; without it they number the agents `#1`, `#2` in the order of the file.
|
|
|
144
146
|
}
|
|
145
147
|
```
|
|
146
148
|
|
|
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.
|
|
149
|
+
`concurrency` counts per agent and per feed. A runner with three agents at
|
|
150
|
+
`concurrency: 2` can run six queued runs and six chat answers at once, each in its own
|
|
151
|
+
CLI process.
|
|
151
152
|
|
|
152
|
-
|
|
153
|
+
If the instance refuses the key of an agent, that agent stops. The other agents continue.
|
|
153
154
|
|
|
154
155
|
## Receiving tasks on a port
|
|
155
156
|
|
|
@@ -157,8 +158,8 @@ By default the runner polls the instance for tasks. With `listen`, the instance
|
|
|
157
158
|
task to the runner when the task is created. The runner then does not poll.
|
|
158
159
|
|
|
159
160
|
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.
|
|
161
|
+
router, use polling. Polling needs no setup. The chat is as fast with polling, because a
|
|
162
|
+
chat claim waits on the server until a message comes.
|
|
162
163
|
|
|
163
164
|
**1.** In the agent's settings, open Connection and select Endpoint. Enter the public
|
|
164
165
|
HTTPS address of the runner's port (see [Reaching the port](#reaching-the-port)), and
|
|
@@ -243,23 +244,23 @@ npx -y @itsaplan/runner /path/to/config.json
|
|
|
243
244
|
The environment variables have priority over the file. The command-line options have
|
|
244
245
|
priority over both. An `agents` entry has priority over all three, for the fields it sets.
|
|
245
246
|
|
|
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`
|
|
247
|
+
| Field | Environment variable | Default | What it does |
|
|
248
|
+
| ---------------- | --------------------------- | ------------------ | ---------------------------------------------------------------------- |
|
|
249
|
+
| `url` | `ITSAPLAN_URL` | required | Your Itsaplan instance |
|
|
250
|
+
| `apiKey` | `ITSAPLAN_API_KEY` | required | The external agent's key. Replaced by `apiKeys` or `agents` |
|
|
251
|
+
| `apiKeys` | | — | Several keys, all with these settings. Not with `agents` |
|
|
251
252
|
| `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`
|
|
253
|
+
| `agent` | `ITSAPLAN_AGENT` | — | The coding agent to run |
|
|
254
|
+
| `args` | | `[]` | Arguments added after the preset's |
|
|
255
|
+
| `command` | `ITSAPLAN_COMMAND` | — | Your own command, instead of `agent` |
|
|
256
|
+
| `cwd` | `ITSAPLAN_CWD` | where you start it | Working directory for the command |
|
|
257
|
+
| `env` | | `{}` | Extra variables for the command |
|
|
258
|
+
| `concurrency` | `ITSAPLAN_CONCURRENCY` | `1` | Tasks at once, per agent. Queued runs and chat answers count apart |
|
|
259
|
+
| `pollIntervalMs` | `ITSAPLAN_POLL_INTERVAL_MS` | `3000` | Wait after an empty queue. Minimum 1000 |
|
|
260
|
+
| `timeoutMs` | `ITSAPLAN_TIMEOUT_MS` | `1800000` | Time before the runner stops a task |
|
|
261
|
+
| `outputFormat` | `ITSAPLAN_OUTPUT_FORMAT` | the preset's | How the runner reads the output. See [The output](#the-output) |
|
|
262
|
+
| `listen` | `ITSAPLAN_LISTEN` | — | Port that receives the tasks. The runner does not poll for the agent |
|
|
263
|
+
| `signingSecret` | `ITSAPLAN_SIGNING_SECRET` | — | The agent's signing secret. Required with `listen` |
|
|
263
264
|
|
|
264
265
|
Set `agent` or `command`. With the environment you need no file at all:
|
|
265
266
|
|
|
@@ -274,8 +275,8 @@ npx -y @itsaplan/runner --agent claude
|
|
|
274
275
|
|
|
275
276
|
The server writes the task text. The text states what occurred, what to do, and to post
|
|
276
277
|
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
|
-
`
|
|
278
|
+
preset gives it to its CLI in the way that CLI accepts it: on stdin for `claude`, `codex`,
|
|
279
|
+
`pi` and `mastracode`, as an argument for `antigravity`, `copilot`, `opencode` and `omp`.
|
|
279
280
|
|
|
280
281
|
The agent reads all other data through the MCP server at `$ITSAPLAN_URL/mcp`. The agent
|
|
281
282
|
sends `$ITSAPLAN_API_KEY` as a bearer token, and acts as its own user with its role.
|
|
@@ -290,12 +291,11 @@ Every run holds these variables:
|
|
|
290
291
|
| `ITSAPLAN_API_KEY` | the agent's key, for the API and the MCP server |
|
|
291
292
|
| `ITSAPLAN_TRIGGER` | `mention`, `delegation`, `field`, `schedule`, `manual`, `status`, or `chat` |
|
|
292
293
|
| `ITSAPLAN_SYSTEM_PROMPT` | the context of the run, for a command that takes a system prompt |
|
|
293
|
-
| `ITSAPLAN_TIMEOUT_MS`
|
|
294
|
+
| `ITSAPLAN_TIMEOUT_MS` | the task timeout in milliseconds |
|
|
294
295
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
command count toward the same limit.
|
|
296
|
+
A command can read `ITSAPLAN_TIMEOUT_MS` to set its own timeout. The value is the
|
|
297
|
+
`timeoutMs` of that agent, and it replaces a value from the environment or from `env`.
|
|
298
|
+
The time starts when the command starts.
|
|
299
299
|
|
|
300
300
|
A queued run adds three more:
|
|
301
301
|
|
|
@@ -314,25 +314,46 @@ A chat message adds three others in their place, and `ITSAPLAN_TRIGGER` is `chat
|
|
|
314
314
|
| `ITSAPLAN_SESSION_ID` | the coding agent's session to resume. Empty for the first message |
|
|
315
315
|
|
|
316
316
|
The task text of a chat message includes the conversation, unless a session holds it. See
|
|
317
|
-
Sessions
|
|
317
|
+
[Sessions](#sessions).
|
|
318
318
|
|
|
319
|
-
|
|
319
|
+
## The output
|
|
320
320
|
|
|
321
|
-
Exit code `0` is a success. Any other code is a failure
|
|
322
|
-
|
|
321
|
+
Exit code `0` is a success. Any other code is a failure, and the end of stderr becomes the
|
|
322
|
+
error.
|
|
323
323
|
|
|
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:
|
|
324
|
+
`outputFormat` tells the runner how to read stdout:
|
|
328
325
|
|
|
329
|
-
- `text` — all
|
|
326
|
+
- `text` — all of stdout is the answer.
|
|
330
327
|
- `claude-stream-json`, `codex-jsonl`, `opencode-json`, `antigravity-stream-json`,
|
|
331
|
-
`copilot-json`, `pi-json` — the event stream of that CLI. `pi` and
|
|
332
|
-
`pi-json`.
|
|
328
|
+
`copilot-json`, `pi-json`, `mastracode-jsonl` — the event stream of that CLI. `pi` and
|
|
329
|
+
`omp` both use `pi-json`. The runner reads the text and the tool calls from it.
|
|
333
330
|
|
|
334
331
|
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
|
|
332
|
+
output does not agree with the format, the runner uses the output as plain text.
|
|
333
|
+
|
|
334
|
+
A queued run reports one answer as its output. With an event stream format, the answer is
|
|
335
|
+
the text that the agent wrote after its last tool call.
|
|
336
|
+
|
|
337
|
+
### Run transcripts
|
|
338
|
+
|
|
339
|
+
While a queued run works, the runner sends the agent's messages and tool calls to the
|
|
340
|
+
instance. The issue shows them as the run's transcript.
|
|
341
|
+
|
|
342
|
+
- The tool calls need an event stream format. With `text`, the transcript holds only the
|
|
343
|
+
output.
|
|
344
|
+
- With `codex-jsonl`, the run shows an **Open in Codex** link. The link opens the session
|
|
345
|
+
only where that session is: in the Codex app of the viewer, or on a connected host. The
|
|
346
|
+
link does not copy the session.
|
|
347
|
+
- The instance keeps up to 2,000 events or 1 MB for each attempt, and marks a transcript
|
|
348
|
+
that it cut. The cut does not change the result of the run.
|
|
349
|
+
- If the last upload fails with a network or server error, the runner tries it one more
|
|
350
|
+
time. Then it reports the result of the command, even with an incomplete transcript.
|
|
351
|
+
Any other refusal fails the run.
|
|
352
|
+
|
|
353
|
+
## Chat
|
|
354
|
+
|
|
355
|
+
The runner streams the answer to the chat while the command runs. It sends the tool calls
|
|
356
|
+
with the answer.
|
|
336
357
|
|
|
337
358
|
### Sessions
|
|
338
359
|
|
|
@@ -348,8 +369,7 @@ Without a session the server sends the last 20 messages of the conversation as p
|
|
|
348
369
|
## Requirements
|
|
349
370
|
|
|
350
371
|
- **Node 20 or later.**
|
|
351
|
-
- **An It's a Plan instance
|
|
352
|
-
gives the runner only the queued runs. `listen` needs 1.6.0.
|
|
372
|
+
- **An It's a Plan instance 1.7.0 or later.**
|
|
353
373
|
- **An external agent in your project.** The runner authenticates with its API key.
|
|
354
374
|
- **The coding agent, installed and signed in on the same machine.** The runner starts the
|
|
355
375
|
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,20 @@ var PRESETS = {
|
|
|
168
176
|
"--auto-approve",
|
|
169
177
|
...sessionId ? ["--resume", sessionId] : []
|
|
170
178
|
],
|
|
171
|
-
tail: []
|
|
179
|
+
tail: ["--"]
|
|
180
|
+
},
|
|
181
|
+
mastracode: {
|
|
182
|
+
bin: "mastracode",
|
|
183
|
+
outputFormat: "mastracode-jsonl",
|
|
184
|
+
promptVia: "stdin",
|
|
185
|
+
head: (sessionId) => [
|
|
186
|
+
"--output",
|
|
187
|
+
"jsonl",
|
|
188
|
+
"--permission-mode",
|
|
189
|
+
"auto",
|
|
190
|
+
...sessionId ? ["--thread", sessionId] : []
|
|
191
|
+
],
|
|
192
|
+
tail: ["--prompt", "-"]
|
|
172
193
|
}
|
|
173
194
|
};
|
|
174
195
|
var PRESET_NAMES = Object.keys(PRESETS);
|
|
@@ -200,7 +221,8 @@ var OUTPUT_FORMATS = [
|
|
|
200
221
|
"opencode-json",
|
|
201
222
|
"antigravity-stream-json",
|
|
202
223
|
"copilot-json",
|
|
203
|
-
"pi-json"
|
|
224
|
+
"pi-json",
|
|
225
|
+
"mastracode-jsonl"
|
|
204
226
|
];
|
|
205
227
|
function presetOf(config) {
|
|
206
228
|
if (config.command || !config.agent)
|
|
@@ -521,6 +543,7 @@ function startListener(port, agents, log) {
|
|
|
521
543
|
var FLUSH_CHARS = 1500;
|
|
522
544
|
var DELTA_LIMIT = 12000;
|
|
523
545
|
var TOOL_TEXT_LIMIT = 32000;
|
|
546
|
+
var TERMINAL_CODES = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
524
547
|
|
|
525
548
|
class AnswerStream {
|
|
526
549
|
format;
|
|
@@ -536,8 +559,11 @@ class AnswerStream {
|
|
|
536
559
|
openTextPart = "";
|
|
537
560
|
openToolCalls = new Set;
|
|
538
561
|
closedToolCalls = new Set;
|
|
562
|
+
mastraMessageId;
|
|
539
563
|
sawPartialText = false;
|
|
540
564
|
sawAnyText = false;
|
|
565
|
+
segment = "";
|
|
566
|
+
lastSegment = "";
|
|
541
567
|
sending = Promise.resolve();
|
|
542
568
|
usage;
|
|
543
569
|
constructor(format, threadId, runId, send) {
|
|
@@ -564,6 +590,9 @@ class AnswerStream {
|
|
|
564
590
|
startedSession() {
|
|
565
591
|
return this.sessionId;
|
|
566
592
|
}
|
|
593
|
+
answer() {
|
|
594
|
+
return (this.segment.trim() ? this.segment : this.lastSegment).trim();
|
|
595
|
+
}
|
|
567
596
|
contextUsage() {
|
|
568
597
|
return this.usage.value();
|
|
569
598
|
}
|
|
@@ -612,6 +641,7 @@ class AnswerStream {
|
|
|
612
641
|
return;
|
|
613
642
|
this.sawAnyText = true;
|
|
614
643
|
this.text += text;
|
|
644
|
+
this.segment += text;
|
|
615
645
|
if (this.text.length >= FLUSH_CHARS)
|
|
616
646
|
this.drainText();
|
|
617
647
|
}
|
|
@@ -640,7 +670,9 @@ class AnswerStream {
|
|
|
640
670
|
try {
|
|
641
671
|
parsed = JSON.parse(trimmed);
|
|
642
672
|
} catch {
|
|
643
|
-
|
|
673
|
+
const plain = trimmed.replace(TERMINAL_CODES, "").trim();
|
|
674
|
+
if (plain)
|
|
675
|
+
this.appendText(`${plain}
|
|
644
676
|
`);
|
|
645
677
|
return;
|
|
646
678
|
}
|
|
@@ -666,6 +698,39 @@ class AnswerStream {
|
|
|
666
698
|
case "pi-json":
|
|
667
699
|
this.readPiLine(parsed);
|
|
668
700
|
break;
|
|
701
|
+
case "mastracode-jsonl":
|
|
702
|
+
this.readMastraLine(parsed);
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
readMastraLine(message) {
|
|
707
|
+
switch (message.type) {
|
|
708
|
+
case "thread_created":
|
|
709
|
+
if (message.thread?.id)
|
|
710
|
+
this.sessionId ??= message.thread.id;
|
|
711
|
+
return;
|
|
712
|
+
case "message_start":
|
|
713
|
+
if (message.message?.role === "assistant")
|
|
714
|
+
this.mastraMessageId = message.message.id;
|
|
715
|
+
return;
|
|
716
|
+
case "message_update":
|
|
717
|
+
if (message.id === this.mastraMessageId && message.event?.type === "text-delta") {
|
|
718
|
+
this.appendText(message.event.delta ?? "");
|
|
719
|
+
}
|
|
720
|
+
return;
|
|
721
|
+
case "tool_start":
|
|
722
|
+
if (message.toolCallId) {
|
|
723
|
+
this.pushToolCall(message.toolCallId, message.toolName ?? "tool", JSON.stringify(message.args ?? {}));
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
726
|
+
case "tool_end":
|
|
727
|
+
if (message.toolCallId)
|
|
728
|
+
this.pushToolResult(message.toolCallId, toolResultText(message.result));
|
|
729
|
+
return;
|
|
730
|
+
case "result":
|
|
731
|
+
if (!this.sawAnyText)
|
|
732
|
+
this.appendText(message.text || message.error?.message || "");
|
|
733
|
+
return;
|
|
669
734
|
}
|
|
670
735
|
}
|
|
671
736
|
readPiLine(message) {
|
|
@@ -690,7 +755,7 @@ class AnswerStream {
|
|
|
690
755
|
if (!message.toolCallId || this.closedToolCalls.has(message.toolCallId))
|
|
691
756
|
return;
|
|
692
757
|
this.closedToolCalls.add(message.toolCallId);
|
|
693
|
-
this.pushToolResult(message.toolCallId,
|
|
758
|
+
this.pushToolResult(message.toolCallId, toolResultText(message.result));
|
|
694
759
|
return;
|
|
695
760
|
}
|
|
696
761
|
case "message_end":
|
|
@@ -774,15 +839,36 @@ class AnswerStream {
|
|
|
774
839
|
return;
|
|
775
840
|
}
|
|
776
841
|
const item = message.item;
|
|
777
|
-
if (
|
|
842
|
+
if (!item)
|
|
778
843
|
return;
|
|
779
|
-
if (item.type === "agent_message" && item.text) {
|
|
844
|
+
if (message.type === "item.completed" && item.type === "agent_message" && item.text) {
|
|
780
845
|
this.appendText(item.text);
|
|
781
846
|
return;
|
|
782
847
|
}
|
|
783
|
-
if (item.
|
|
784
|
-
|
|
785
|
-
|
|
848
|
+
if (!item.id || !["command_execution", "mcp_tool_call", "file_change", "web_search"].includes(item.type ?? ""))
|
|
849
|
+
return;
|
|
850
|
+
if (!["item.started", "item.completed"].includes(message.type ?? ""))
|
|
851
|
+
return;
|
|
852
|
+
if (!this.openToolCalls.has(item.id)) {
|
|
853
|
+
this.openToolCalls.add(item.id);
|
|
854
|
+
const name = item.type === "command_execution" ? "shell" : item.tool ?? item.type ?? "tool";
|
|
855
|
+
const args = item.command ?? JSON.stringify(item.arguments ?? item.changes ?? { query: item.query });
|
|
856
|
+
this.pushToolCall(item.id, name, args);
|
|
857
|
+
}
|
|
858
|
+
if (message.type === "item.completed" && !this.closedToolCalls.has(item.id)) {
|
|
859
|
+
this.closedToolCalls.add(item.id);
|
|
860
|
+
let result = item.aggregated_output;
|
|
861
|
+
if (result == null || item.status === "failed" || item.exit_code != null && item.exit_code !== 0) {
|
|
862
|
+
result = JSON.stringify({
|
|
863
|
+
status: item.status,
|
|
864
|
+
exitCode: item.exit_code,
|
|
865
|
+
output: item.aggregated_output,
|
|
866
|
+
result: item.result,
|
|
867
|
+
error: item.error,
|
|
868
|
+
changes: item.changes
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
this.pushToolResult(item.id, result);
|
|
786
872
|
}
|
|
787
873
|
}
|
|
788
874
|
readOpencodeLine(message) {
|
|
@@ -860,10 +946,17 @@ class AnswerStream {
|
|
|
860
946
|
}
|
|
861
947
|
pushToolCall(toolCallId, toolCallName, args) {
|
|
862
948
|
this.drainText();
|
|
949
|
+
this.closeSegment();
|
|
863
950
|
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
951
|
}
|
|
952
|
+
closeSegment() {
|
|
953
|
+
if (this.segment.trim())
|
|
954
|
+
this.lastSegment = this.segment;
|
|
955
|
+
this.segment = "";
|
|
956
|
+
}
|
|
865
957
|
pushToolResult(toolCallId, content) {
|
|
866
958
|
this.drainText();
|
|
959
|
+
this.closeSegment();
|
|
867
960
|
this.queued.push({
|
|
868
961
|
type: "TOOL_CALL_RESULT",
|
|
869
962
|
messageId: this.messageId,
|
|
@@ -928,8 +1021,19 @@ class UsageReader {
|
|
|
928
1021
|
case "pi-json":
|
|
929
1022
|
this.readPi(parsed);
|
|
930
1023
|
return;
|
|
1024
|
+
case "mastracode-jsonl":
|
|
1025
|
+
this.readMastra(parsed);
|
|
1026
|
+
return;
|
|
931
1027
|
}
|
|
932
1028
|
}
|
|
1029
|
+
readMastra(message) {
|
|
1030
|
+
if (message.type !== "usage_update" || !message.usage)
|
|
1031
|
+
return;
|
|
1032
|
+
this.last = {
|
|
1033
|
+
inputTokens: message.usage.promptTokens ?? 0,
|
|
1034
|
+
outputTokens: message.usage.completionTokens ?? 0
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
933
1037
|
readClaude(message) {
|
|
934
1038
|
const event = message.type === "stream_event" ? message.event : undefined;
|
|
935
1039
|
if (!event)
|
|
@@ -1000,7 +1104,7 @@ function textOfResult(content) {
|
|
|
1000
1104
|
}
|
|
1001
1105
|
return "";
|
|
1002
1106
|
}
|
|
1003
|
-
function
|
|
1107
|
+
function toolResultText(result) {
|
|
1004
1108
|
if (result && typeof result === "object" && !Array.isArray(result) && "content" in result) {
|
|
1005
1109
|
return textOfResult(result.content);
|
|
1006
1110
|
}
|
|
@@ -1138,6 +1242,78 @@ async function answer(config, client, message, stop) {
|
|
|
1138
1242
|
await client.chatResult(message.id, { status: "failed", error, usage: stream.contextUsage() });
|
|
1139
1243
|
}
|
|
1140
1244
|
|
|
1245
|
+
// src/run.ts
|
|
1246
|
+
async function runTask(config, client, run) {
|
|
1247
|
+
let offset = 0;
|
|
1248
|
+
const stream = new AnswerStream(config.outputFormat, `run-${run.id}`, String(run.id), async (events) => {
|
|
1249
|
+
if (!run.traceEvents)
|
|
1250
|
+
return;
|
|
1251
|
+
const session = config.outputFormat === "codex-jsonl" ? stream.startedSession() : null;
|
|
1252
|
+
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;
|
|
1253
|
+
for (let start = 0;start < events.length; start += 200) {
|
|
1254
|
+
const batch = events.slice(start, start + 200);
|
|
1255
|
+
await client.runEvents(run, offset + start, batch, sessionId);
|
|
1256
|
+
}
|
|
1257
|
+
offset += events.length;
|
|
1258
|
+
});
|
|
1259
|
+
const usage = new UsageReader(config.outputFormat);
|
|
1260
|
+
const timer = setInterval(() => {
|
|
1261
|
+
stream.flush().catch(() => {});
|
|
1262
|
+
}, 500);
|
|
1263
|
+
try {
|
|
1264
|
+
const outcome = await execute(config, {
|
|
1265
|
+
prompt: run.prompt,
|
|
1266
|
+
systemPrompt: run.systemPrompt,
|
|
1267
|
+
env: {
|
|
1268
|
+
ITSAPLAN_RUN_ID: String(run.id),
|
|
1269
|
+
ITSAPLAN_TRIGGER: run.trigger,
|
|
1270
|
+
ITSAPLAN_SYSTEM_PROMPT: run.systemPrompt,
|
|
1271
|
+
ITSAPLAN_ISSUE: run.issueIdentifier ?? "",
|
|
1272
|
+
ITSAPLAN_ISSUE_ID: run.issueId == null ? "" : String(run.issueId)
|
|
1273
|
+
}
|
|
1274
|
+
}, {
|
|
1275
|
+
onData: (chunk) => {
|
|
1276
|
+
usage.write(chunk);
|
|
1277
|
+
stream.write(chunk);
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
clearInterval(timer);
|
|
1281
|
+
usage.end();
|
|
1282
|
+
try {
|
|
1283
|
+
if (outcome.status === "success")
|
|
1284
|
+
await stream.finish(outcome.output);
|
|
1285
|
+
else
|
|
1286
|
+
await stream.fail(outcome.error ?? "The command failed", outcome.output);
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
if (!isTransientEventError(error))
|
|
1289
|
+
throw error;
|
|
1290
|
+
try {
|
|
1291
|
+
await stream.flush();
|
|
1292
|
+
} catch (retryError) {
|
|
1293
|
+
if (!isTransientEventError(retryError))
|
|
1294
|
+
throw retryError;
|
|
1295
|
+
console.error(`Run ${run.id}: trace upload failed; reporting the command result: ${String(retryError)}`);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const output = config.outputFormat === "text" ? outcome.output : stream.answer();
|
|
1299
|
+
await client.report(run.id, {
|
|
1300
|
+
...outcome,
|
|
1301
|
+
output,
|
|
1302
|
+
...run.traceEvents && { attempt: run.attempts },
|
|
1303
|
+
usage: usage.value()
|
|
1304
|
+
});
|
|
1305
|
+
return outcome;
|
|
1306
|
+
} finally {
|
|
1307
|
+
clearInterval(timer);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
function isTransientEventError(error) {
|
|
1311
|
+
if (error instanceof RequestError) {
|
|
1312
|
+
return error.status === 408 || error.status === 429 || error.status >= 500;
|
|
1313
|
+
}
|
|
1314
|
+
return error instanceof TypeError || error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1141
1317
|
// src/tasks.ts
|
|
1142
1318
|
var HEARTBEAT_MS = 60000;
|
|
1143
1319
|
async function withHeartbeat(log, beat, work) {
|
|
@@ -1150,32 +1326,20 @@ async function withHeartbeat(log, beat, work) {
|
|
|
1150
1326
|
clearInterval(timer);
|
|
1151
1327
|
}
|
|
1152
1328
|
}
|
|
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
1329
|
async function handleRun(config, client, log, run) {
|
|
1167
1330
|
const label = run.issueIdentifier ?? `run ${run.id}`;
|
|
1168
1331
|
log(`${label}: started (${run.trigger})`);
|
|
1169
1332
|
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() });
|
|
1333
|
+
const outcome = await withHeartbeat(log, () => client.heartbeat(run.id, run.traceEvents ? run.attempts : undefined), runTask(config, client, run));
|
|
1174
1334
|
log(`${label}: ${outcome.status}${outcome.error ? ` — ${outcome.error}` : ""}`);
|
|
1175
1335
|
} catch (err) {
|
|
1176
1336
|
const message = err instanceof Error ? err.message : String(err);
|
|
1177
1337
|
log(`${label}: runner error — ${message}`);
|
|
1178
|
-
await client.report(run.id, {
|
|
1338
|
+
await client.report(run.id, {
|
|
1339
|
+
status: "failed",
|
|
1340
|
+
error: message,
|
|
1341
|
+
...run.traceEvents && { attempt: run.attempts }
|
|
1342
|
+
}).catch(() => {});
|
|
1179
1343
|
}
|
|
1180
1344
|
}
|
|
1181
1345
|
async function handleChat(config, client, log, message) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itsaplan/runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Runs an Itsaplan external agent's queued tasks and answers its chat on your own machine",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"itsaplan",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"antigravity",
|
|
21
21
|
"opencode",
|
|
22
22
|
"pi",
|
|
23
|
-
"omp"
|
|
23
|
+
"omp",
|
|
24
|
+
"mastracode"
|
|
24
25
|
],
|
|
25
26
|
"homepage": "https://github.com/croffasia/itsaplan/tree/main/packages/runner#readme",
|
|
26
27
|
"repository": {
|