Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

145 changes: 70 additions & 75 deletions packages/runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This package runs such agents on your own machine — one, or several at once.
Pi, and Oh My Pi (`omp`). Each preset sets the unattended flags and the session resume.
- Runs your own `command` instead, if you prefer. The command takes the task on stdin.
- Answers the agent's chat. Sends the text and the tool calls while the CLI prints them.
- Sends the messages and the tool calls of a queued run to the issue while the run works.
- Receives the tasks on a port instead of polling, for an agent whose Connection is
Endpoint. See [Receiving tasks on a port](#receiving-tasks-on-a-port).

Expand Down Expand Up @@ -107,9 +108,8 @@ on stdin. The runner keeps no session for it, because it does not know how to re

## Several agents on one runner

One key is one agent. A runner set up the way you want it can serve several of them at
once: it polls for each key on its own, and every agent uses the coding agent, the working
directory and the arguments the file already gives.
One key is one agent. One runner can serve several agents. It polls for each key
separately, and every agent uses the `agent`, `cwd` and `args` from the file.

```json
{
Expand All @@ -120,13 +120,13 @@ directory and the arguments the file already gives.
}
```

Nothing else changes: the runner gives each command its own `ITSAPLAN_API_KEY`, so the one
MCP config in that folder works for all of them.
The runner gives each command its own `ITSAPLAN_API_KEY`, so one MCP config in that
folder works for all agents.

Use `agents` instead of `apiKeys` where an agent needs something of its own. A field an
entry sets replaces the shared one, and it inherits every field it leaves out, except its
`apiKey`, which every entry sets itself. `name` is what the runner's log lines call that
agent; without it they number the agents `#1`, `#2` in the order of the file.
Use `agents` instead of `apiKeys` when an agent needs its own settings. Each entry sets
its own `apiKey`. The other fields that an entry sets replace the shared ones, and the
entry inherits the rest. `name` is the agent's label in the log. Without it, the log
numbers the agents `#1`, `#2` in the order of the file.

```json
{
Expand All @@ -144,21 +144,20 @@ agent; without it they number the agents `#1`, `#2` in the order of the file.
}
```

`concurrency` counts per agent and per feed, so a runner with three agents at
`concurrency: 2` can have six queued runs in flight and six chat answers alongside them,
each one a coding agent CLI of its own. The environment variables and the command-line
options describe every agent at once; an entry has priority over them.
`concurrency` counts per agent and per feed. A runner with three agents at
`concurrency: 2` can run six queued runs and six chat answers at once, each in its own
CLI process.

An agent whose key the instance refuses stops on its own, and the others keep working.
If the instance refuses the key of an agent, that agent stops. The other agents continue.

## Receiving tasks on a port

By default the runner polls the instance for tasks. With `listen`, the instance sends each
task to the runner when the task is created. The runner then does not poll.

Use `listen` for a runner on a server that the instance can reach. On a laptop behind a
router, use polling. Polling needs no setup, and the chat is equally fast with it: a chat
claim waits on the server until a message comes.
router, use polling. Polling needs no setup. The chat is as fast with polling, because a
chat claim waits on the server until a message comes.

**1.** In the agent's settings, open Connection and select Endpoint. Enter the public
HTTPS address of the runner's port (see [Reaching the port](#reaching-the-port)), and
Expand Down Expand Up @@ -243,23 +242,23 @@ npx -y @itsaplan/runner /path/to/config.json
The environment variables have priority over the file. The command-line options have
priority over both. An `agents` entry has priority over all three, for the fields it sets.

| Field | Environment variable | Default | What it does |
| ---------------- | --------------------------- | ------------------ | ------------------------------------------------------------------ |
| `url` | `ITSAPLAN_URL` | required | Your Itsaplan instance |
| `apiKey` | `ITSAPLAN_API_KEY` | required | The external agent's key. Replaced by `apiKeys` or `agents` |
| `apiKeys` | | — | Several keys, all with these settings. Not with `agents` |
| Field | Environment variable | Default | What it does |
| ---------------- | --------------------------- | ------------------ | ---------------------------------------------------------------------- |
| `url` | `ITSAPLAN_URL` | required | Your Itsaplan instance |
| `apiKey` | `ITSAPLAN_API_KEY` | required | The external agent's key. Replaced by `apiKeys` or `agents` |
| `apiKeys` | | — | Several keys, all with these settings. Not with `agents` |
| `agents` | | — | Several agents, each with its own key and settings. Not with `apiKeys` |
| `agent` | `ITSAPLAN_AGENT` | — | The coding agent to run |
| `args` | | `[]` | Arguments added after the preset's |
| `command` | `ITSAPLAN_COMMAND` | — | Your own command, instead of `agent` |
| `cwd` | `ITSAPLAN_CWD` | where you start it | Working directory for the command |
| `env` | | `{}` | Extra variables for the command |
| `concurrency` | `ITSAPLAN_CONCURRENCY` | `1` | Tasks at once, per agent. Queued runs and chat answers count apart |
| `pollIntervalMs` | `ITSAPLAN_POLL_INTERVAL_MS` | `3000` | Wait after an empty queue. Minimum 1000 |
| `timeoutMs` | `ITSAPLAN_TIMEOUT_MS` | `1800000` | Time before the runner stops a task |
| `outputFormat` | `ITSAPLAN_OUTPUT_FORMAT` | the preset's | How the runner reads run and chat output |
| `listen` | `ITSAPLAN_LISTEN` | — | Port that receives the tasks. The runner does not poll for the agent |
| `signingSecret` | `ITSAPLAN_SIGNING_SECRET` | — | The agent's signing secret. Required with `listen` |
| `agent` | `ITSAPLAN_AGENT` | — | The coding agent to run |
| `args` | | `[]` | Arguments added after the preset's |
| `command` | `ITSAPLAN_COMMAND` | — | Your own command, instead of `agent` |
| `cwd` | `ITSAPLAN_CWD` | where you start it | Working directory for the command |
| `env` | | `{}` | Extra variables for the command |
| `concurrency` | `ITSAPLAN_CONCURRENCY` | `1` | Tasks at once, per agent. Queued runs and chat answers count apart |
| `pollIntervalMs` | `ITSAPLAN_POLL_INTERVAL_MS` | `3000` | Wait after an empty queue. Minimum 1000 |
| `timeoutMs` | `ITSAPLAN_TIMEOUT_MS` | `1800000` | Time before the runner stops a task |
| `outputFormat` | `ITSAPLAN_OUTPUT_FORMAT` | the preset's | How the runner reads the output. See [The output](#the-output) |
| `listen` | `ITSAPLAN_LISTEN` | — | Port that receives the tasks. The runner does not poll for the agent |
| `signingSecret` | `ITSAPLAN_SIGNING_SECRET` | — | The agent's signing secret. Required with `listen` |

Set `agent` or `command`. With the environment you need no file at all:

Expand All @@ -274,8 +273,8 @@ npx -y @itsaplan/runner --agent claude

The server writes the task text. The text states what occurred, what to do, and to post
the result as a comment on the issue. Your own `command` receives the text on **stdin**. A
preset gives it to its CLI in the way that CLI accepts it: on stdin for `claude` and
`codex`, as an argument for `antigravity`, `copilot`, `opencode`, `pi` and `omp`.
preset gives it to its CLI in the way that CLI accepts it: on stdin for `claude`, `codex`
and `pi`, as an argument for `antigravity`, `copilot`, `opencode` and `omp`.

The agent reads all other data through the MCP server at `$ITSAPLAN_URL/mcp`. The agent
sends `$ITSAPLAN_API_KEY` as a bearer token, and acts as its own user with its role.
Expand All @@ -290,12 +289,11 @@ Every run holds these variables:
| `ITSAPLAN_API_KEY` | the agent's key, for the API and the MCP server |
| `ITSAPLAN_TRIGGER` | `mention`, `delegation`, `field`, `schedule`, `manual`, `status`, or `chat` |
| `ITSAPLAN_SYSTEM_PROMPT` | the context of the run, for a command that takes a system prompt |
| `ITSAPLAN_TIMEOUT_MS` | the effective task timeout in milliseconds, including per-agent overrides |
| `ITSAPLAN_TIMEOUT_MS` | the task timeout in milliseconds |

Custom commands can read `ITSAPLAN_TIMEOUT_MS` to configure their own execution timeout.
The runner supplies the resolved `timeoutMs`, overriding any inherited value or value in
`env`. Its timer starts when the command is spawned, so startup and waiting inside the
command count toward the same limit.
A command can read `ITSAPLAN_TIMEOUT_MS` to set its own timeout. The value is the
`timeoutMs` of that agent, and it replaces a value from the environment or from `env`.
The time starts when the command starts.

A queued run adds three more:

Expand All @@ -314,27 +312,46 @@ A chat message adds three others in their place, and `ITSAPLAN_TRIGGER` is `chat
| `ITSAPLAN_SESSION_ID` | the coding agent's session to resume. Empty for the first message |

The task text of a chat message includes the conversation, unless a session holds it. See
Sessions below.
[Sessions](#sessions).

### The result
## The output

Exit code `0` is a success. Any other code is a failure. The end of stderr then becomes
the error. In both cases the run's output is the answer: all of stdout with `outputFormat`
`text`, and with an event stream format the text the agent wrote after its last tool call
(see Chat below for the formats).
Exit code `0` is a success. Any other code is a failure, and the end of stderr becomes the
error.

## Chat

The runner streams the answer to the chat while the command runs. It reports the tool
calls with the answer. `outputFormat` tells the runner how to read the output:
`outputFormat` tells the runner how to read stdout:

- `text` — all the output is the answer.
- `text` — all of stdout is the answer.
- `claude-stream-json`, `codex-jsonl`, `opencode-json`, `antigravity-stream-json`,
`copilot-json`, `pi-json` — the event stream of that CLI. `pi` and `omp` both use
`pi-json`.
`pi-json`. The runner reads the text and the tool calls from it.

A preset sets `outputFormat` for you. Set it yourself only with your own `command`. If the
output does not agree with the format, the runner sends the output as plain text.
output does not agree with the format, the runner uses the output as plain text.

A queued run reports one answer as its output. With an event stream format, the answer is
the text that the agent wrote after its last tool call.

### Run transcripts

While a queued run works, the runner sends the agent's messages and tool calls to the
instance. The issue shows them as the run's transcript.

- The tool calls need an event stream format. With `text`, the transcript holds only the
output.
- With `codex-jsonl`, the run shows an **Open in Codex** link. The link opens the session
only where that session is: in the Codex app of the viewer, or on a connected host. The
link does not copy the session.
- The instance keeps up to 2,000 events or 1 MB for each attempt, and marks a transcript
that it cut. The cut does not change the result of the run.
- If the last upload fails with a network or server error, the runner tries it one more
time. Then it reports the result of the command, even with an incomplete transcript.
Any other refusal fails the run.

## Chat

The runner streams the answer to the chat while the command runs. It sends the tool calls
with the answer.

### Sessions

Expand All @@ -350,8 +367,7 @@ Without a session the server sends the last 20 messages of the conversation as p
## Requirements

- **Node 20 or later.**
- **An It's a Plan instance 0.10.0 or later.** The chat needs 0.11.0. An older instance
gives the runner only the queued runs. `listen` needs 1.6.0.
- **An It's a Plan instance 1.7.0 or later.**
- **An external agent in your project.** The runner authenticates with its API key.
- **The coding agent, installed and signed in on the same machine.** The runner starts the
CLI as you, but it cannot sign in for you.
Expand All @@ -376,24 +392,3 @@ Do everything else in WSL: Node, the coding agent, the config file, and the runn
Copyright © 2026 Andrii Poluosmak.

[Apache-2.0](LICENSE).

### Issue run transcripts

A server that advertises `traceEvents` on a run claim accepts its streamed messages
and tool calls at `/agent-runs/:runId/events`. The runner reports numbered batches
for the claimed attempt before reporting its result. Retries of a batch use the
same offsets. A server without this capability still receives the final result.

A transient failure of the final event batch is retried once. If it still fails,
the runner logs a trace-upload warning and reports the command's actual result.
Authorization, stale-attempt and other permanent rejections are not retried or ignored.

The `codex-jsonl` format records the session UUID from `thread.started`, shell
commands, MCP calls and file changes. Plan shows an **Open in Codex** link for that
attempt. The session must be available in the viewer's local Codex app or a
connected host; the link does not publish or copy the Codex session. The events
recorded in Plan are readable there independently of the local session.

Each attempt records up to 2,000 events or 1 MB. Plan marks a truncated transcript;
the final run result is reported separately. Plain-text commands provide messages
and output, while tool details require a supported structured output format.
2 changes: 1 addition & 1 deletion packages/runner/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@itsaplan/runner",
"version": "0.6.0",
"version": "0.7.0",
"description": "Runs an Itsaplan external agent's queued tasks and answers its chat on your own machine",
"keywords": [
"itsaplan",
Expand Down
Loading