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
1 change: 1 addition & 0 deletions .github/workflows/create-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ on:
- image-resize
- llm-router
- mcp
- pi
- provider-anthropic
- provider-openai
- session-manager
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
- 'image-resize/v*'
- 'llm-router/v*'
- 'mcp/v*'
- 'pi/v*'
- 'provider-anthropic/v*'
- 'provider-openai/v*'
- 'session-manager/v*'
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ npx skills add iii-hq/iii --all
| [`harness`](harness/) | Node | TS port of the iii harness stack — bundles `harness` (provider registry + credentials/settings/permissions via the `configuration` worker), `turn-orchestrator`, `hook-fanout`, `models-catalog`, the `provider-*` workers, `llm-budget`, and `context-compaction` as one pnpm monorepo. Approval is delegated to the standalone `approval-gate` worker via the `pre_trigger` hook. Conversations persist in `session-manager`. See [`harness/README.md`](harness/README.md). |
| [`codex`](codex/) | Rust | OpenAI Codex as an iii worker — `codex::*` spawn the codex CLI for headless turns, mirror raw thread events onto `codex::events`, and stream AgentEvent frames onto `agent::events`. |
| [`claude-code`](claude-code/) | Node | Claude Code as an iii worker — `claude::*` runs headless Claude Code turns, mirrors raw messages onto `claude::events`, and streams AgentEvent frames onto `agent::events`. |
| [`pi`](pi/) | Node | Pi coding agent as an iii worker — `pi::*` run headless Pi turns, mirror raw events onto `pi::events`, and stream AgentEvent frames onto `agent::events`. |
| [`session-manager`](session-manager/) | Rust | Durable, reactive, branching conversation store — fourteen `session::*` functions plus six trigger types; the transcript backend for `harness` and `console`. See [`session-manager/architecture/`](session-manager/architecture/). |
| [`telegram-bot`](telegram-bot/) | Rust | Telegram webhook bridge to the harness stack — live message edits, inline approval keyboards, and configurable verbosity. |
| [`context-manager`](context-manager/) | Rust | Model-ready context assembly — four `context::*` functions for token counting, function-result pruning, and history compaction over caller-supplied messages. Storage-agnostic; summarisation via `llm-router` when installed. |
Expand Down
8 changes: 7 additions & 1 deletion claude-code/src/iii-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,17 @@ The live engine is the single source of truth. Ask it — never assume:
- \`iii trigger <function> --help\` — that function's description and request schema, served by
the engine. THIS IS THE API REFERENCE for every call you make. Fetch it BEFORE the first call
to any function; a one-line description from \`list\` is a hint, not the contract.
- \`iii trigger engine::workers::list\` — every connected worker;
- \`iii trigger engine::workers::list\` — every WS-connected (currently running) worker;
\`iii trigger engine::workers::info name=<name>\` — one worker's full surface.
- \`iii trigger worker::list\` — installed + running workers, including daemon-managed
builtins. \`engine::workers::list\` sees only WS-connected workers, so to confirm a worker is
RUNNING, merge the two by name.
- \`iii trigger engine::triggers::list\` — every trigger TYPE;
\`iii trigger engine::registered-triggers::list\` — every trigger INSTANCE already bound.

Trust a successful runtime call over an empty list: an empty \`*::list\` can mean lag, not
absence — never conclude a function or worker is missing from one empty read.

Need a backend capability? Check what is already registered FIRST — it is usually one call
away. When nothing fits, search the public registry before building anything:
\`iii trigger directory::registry::workers::list --json '{"search":"<capability>"}'\` pages the
Expand Down
8 changes: 7 additions & 1 deletion codex/src/iii_prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,17 @@ The live engine is the single source of truth. Ask it — never assume:
- `iii trigger <function> --help` — that function's description and request schema, served by
the engine. THIS IS THE API REFERENCE for every call you make. Fetch it BEFORE the first call
to any function; a one-line description from `list` is a hint, not the contract.
- `iii trigger engine::workers::list` — every connected worker;
- `iii trigger engine::workers::list` — every WS-connected (currently running) worker;
`iii trigger engine::workers::info name=<name>` — one worker's full surface.
- `iii trigger worker::list` — installed + running workers, including daemon-managed
builtins. `engine::workers::list` sees only WS-connected workers, so to confirm a worker is
RUNNING, merge the two by name.
- `iii trigger engine::triggers::list` — every trigger TYPE;
`iii trigger engine::registered-triggers::list` — every trigger INSTANCE already bound.

Trust a successful runtime call over an empty list: an empty `*::list` can mean lag, not
absence — never conclude a function or worker is missing from one empty read.

Need a backend capability? Check what is already registered FIRST — it is usually one call
away. When nothing fits, search the public registry before building anything:
`iii trigger directory::registry::workers::list --json '{"search":"<capability>"}'` pages the
Expand Down
1 change: 1 addition & 0 deletions pi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.tsbuildinfo
183 changes: 183 additions & 0 deletions pi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# pi

Pi coding agent as an iii worker: the Pi API exposed as functions and streams on the iii bus, nothing else. The worker runs the same in-process agent loop Pi runs in the terminal, with the same tools (read, bash, edit, write) against any host directory. `pi::run` executes one headless turn and returns the result; the raw Pi events mirror verbatim onto the `pi::events` stream, and a translated AgentEvent view lands on `agent::events`, so the iii console, the acp worker, and any sibling worker observe a Pi run exactly like a native harness turn. The worker also registers `run::start_and_wait`, the same entrypoint the console and the acp worker drive, so both run Pi with no changes.

## Install

```bash
iii worker add pi
```

Pi runs the loop in-process (no CLI subprocess), so the worker environment needs model credentials — `ANTHROPIC_API_KEY` (or the provider key Pi is configured for) or an existing Pi login.

## Skills

Install the `pi` agent skill for Claude Code, Cursor, and 30+ other agents:

```bash
npx skills add iii-hq/workers --skill pi
```

## Quickstart

From zero to a Pi turn over the bus:

```bash
curl -fsSL https://install.iii.dev/iii/main/install.sh | sh
iii worker add pi
iii # starts the engine + worker
```

Then talk to it like any other function: from the console chat, from `iii trigger pi::run`, or from any SDK:

```ts
import { registerWorker } from 'iii-sdk';

const iii = registerWorker('ws://127.0.0.1:49134', { workerName: 'demo' });

const res = await iii.trigger({
function_id: 'pi::run',
payload: {
prompt: 'Add a /health endpoint to server.ts and run the tests',
cwd: '/path/to/repo',
},
timeout_ms: 600_000,
});
// { session_id, pi_session_id, result, stop_reason, usage, total_cost_usd }
```

Or straight from the terminal with the `iii trigger` CLI:

```bash
# one full turn (raise the timeout; the default 30s is too short for agent turns)
iii trigger pi::run --timeout-ms 600000 \
--json '{"prompt":"add a /health endpoint and run the tests","cwd":"/path/to/repo"}'

# quick reads use key=value syntax
iii trigger pi::sessions::list
iii trigger pi::status session_id=<session_id>

# background turn + control
iii trigger pi::start --json '{"prompt":"...","cwd":"/path/to/repo"}'
iii trigger pi::stop session_id=<session_id>

# ask the running engine for a function's description
iii trigger pi::run --help
```

A turn from the CLI returns the result with token usage and cost:

![iii trigger pi::run returning pong with usage and cost](https://raw.githubusercontent.com/iii-hq/workers/main/pi/assets/cli-run.png)

`iii trigger pi::run --help` prints the published request schema as a parameter table:

![iii trigger pi::run --help printing the request schema as a parameter table](https://raw.githubusercontent.com/iii-hq/workers/main/pi/assets/cli-help.png)

Call `pi::run` again with the returned `session_id` to continue the same conversation: the worker maps iii session ids to Pi session files in engine state and resumes automatically.

![iii trigger pi::sessions::list showing the stored session records](https://raw.githubusercontent.com/iii-hq/workers/main/pi/assets/cli-sessions.png)

Two ids come back from every run. `session_id` is the iii session id: the key for `pi::status`, `pi::stop`, `pi::steer`, resume, and the stream group. `pi_session_id` is Pi's internal session id — returned for reference, not a lookup key.

Long turns: use `pi::start` to return immediately, then watch `agent::events` (group_id = your session_id) for `message_complete`, `function_execution_start/end`, and `turn_end` frames. `pi::stop` interrupts a live run, `pi::status` reads a point-in-time view, `pi::sessions::list` enumerates past sessions.

## Functions

| Function | Purpose |
| --- | --- |
| `pi::run` | Run one turn, wait, return the final result |
| `pi::start` | Fire-and-forget turn; progress arrives on `agent::events` |
| `pi::steer` | Inject a steering instruction into a live run |
| `pi::follow_up` | Queue a follow-up message for a live run |
| `pi::stop` | Interrupt a live run |
| `pi::status` | Session state, live flag, usage, cost |
| `pi::sessions::list` | All sessions this worker has run |
| `run::start_and_wait` | Alias for `pi::run` under the entrypoint the console and acp worker drive |

`pi::run` accepts either a bare `prompt` string or a `messages` array (`[{ role: 'user', content: [{ type: 'text', text }] }]`), plus `model`, `cwd`, `thinking_level`, `tools`, and `iii_context` overrides.

### Raw events

Every event Pi emits (`agent_start/end`, `turn_start/end`, `message_start/update/end`, `tool_execution_start/update/end`, and the session events `queue_update`, `compaction_start/end`) is mirrored verbatim onto the `pi::events` stream, group_id = session_id. Consumers that want the exact Pi event format read `pi::events`; consumers that want harness-shaped frames read `agent::events`. Same turn, two views.

## Steering a live run

A turn started with `pi::start` keeps its session reachable while it streams. Two functions push instructions into it:

```bash
# start a long run
iii trigger pi::start --json '{"prompt":"refactor the auth module","cwd":"/path/to/repo","session_id":"s1"}'

# redirect it mid-flight — applied after the current tool calls finish
iii trigger pi::steer --json '{"session_id":"s1","prompt":"stop, keep the public API unchanged"}'

# queue work for after it would otherwise stop
iii trigger pi::follow_up --json '{"session_id":"s1","prompt":"then add tests for the new paths"}'
```

`pi::steer` maps onto Pi's steering queue (interrupt-style), `pi::follow_up` onto its follow-up queue (wait-style). Both no-op with `{steered:false}` / `{queued:false}` when no run is live for the session.

## The agent on the bus

By default every turn's prompt carries the iii runtime context: the same engine-grounded rules as the harness identity prompts, retargeted to the `iii` CLI the agent reaches through its shell. The agent discovers capabilities from the live engine instead of memory — `iii trigger engine::functions::list` to find function ids, `iii trigger <fn> --help` as the contract before every first call, the registry flow (`directory::registry::workers::list/info`, `worker::add`) when nothing registered fits — plus the calling rules and error-handling discipline that go with them. Local file edits stay on Pi's native tools; backend actions go through registered functions.

```bash
# the agent answers this by querying the live engine itself
iii trigger pi::run --timeout-ms 300000 \
--json '{"prompt":"List every worker connected to this engine and what each one does.","cwd":"/tmp"}'
```

Pi answers by querying the live engine itself, grouping every connected worker by role:

![iii trigger pi::run enumerating every worker connected to the engine](https://raw.githubusercontent.com/iii-hq/workers/main/pi/assets/cli-discovery.png)

The context is prepended on a fresh session and skipped on resume (it is already in the conversation history). Turn it off entirely with `"iii_context": false` per call or globally in `config.yaml`.

## Thinking and tools

`thinking_level` maps straight onto Pi's reasoning levels, per turn:

| Level | Behavior |
| --- | --- |
| `off` | No extended reasoning |
| `minimal` / `low` | Short reasoning budget |
| `medium` | The worker default |
| `high` / `xhigh` | Deep reasoning (xhigh on supported model families) |

`tools` is an allowlist: leave it empty for Pi's defaults (`read`, `bash`, `edit`, `write`), or pass a subset to narrow what the turn can do — e.g. `{"tools":["read","bash"]}` for a read-and-run turn that cannot edit or write files.

## Configuration

```yaml
engine_url: ws://127.0.0.1:49134

defaults:
model: "" # empty = Pi settings default; else "provider/modelId"
thinking_level: medium # off | minimal | low | medium | high | xhigh
cwd: "" # default working directory for runs
tools: [] # empty = Pi defaults (read, bash, edit, write)
agent_dir: "" # Pi global config dir; empty = ~/.pi/agent

events_stream: agent::events # translated AgentEvent frames
raw_events_stream: pi::events # verbatim Pi events
iii_context: true # prepend the iii runtime context on fresh sessions
```

`config.yaml` is the seed: on first boot the worker registers it with the built-in `configuration` worker as the initial value, then reads the live value back and hot-reloads on every `configuration:updated`. `engine_url` is excluded from the managed schema — it is bootstrap, so it stays on the local seed / `--url`.

## Observability

Every `pi::run` is an ordinary traced invocation on the engine: the trace carries the full input payload (prompt, cwd, caller worker id) and the output (result, stop reason, token usage, cost) as span events, with per-function p50/p95/p99 in the console's trace explorer — no extra instrumentation in the worker.

## How it maps

| Pi | iii |
| --- | --- |
| `AgentSession.prompt()` turn | `pi::run` invocation |
| every AgentSession event, verbatim | `pi::events` stream frame |
| assistant `message_end` | `message_complete` frame on `agent::events` |
| `tool_execution_start` / `tool_execution_end` | `function_execution_start` / `function_execution_end` frames |
| final result | `turn_end` + `agent_end` frames, function return value |
| `steer()` / `followUp()` | `pi::steer` / `pi::follow_up` |
| session resume | engine state scope `pi_sessions`, keyed by iii session_id |
| extra capability | another iii worker on the bus (`shell`, `database`, `storage`, ...) |
Binary file added pi/assets/cli-discovery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pi/assets/cli-help.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pi/assets/cli-run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pi/assets/cli-sessions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions pi/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
"root": false,
"vcs": { "enabled": false, "clientKind": "git" },
"files": {
"ignoreUnknown": false,
"includes": ["**", "!!**/dist"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "off"
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "warn"
},
"style": {
"useImportType": "off",
"useNodejsImportProtocol": "error"
},
"complexity": {
"noForEach": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "all",
"semicolons": "always"
}
}
}
23 changes: 23 additions & 0 deletions pi/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
engine_url: ws://127.0.0.1:49134

defaults:
model: "" # empty = Pi settings default; else "provider/modelId", e.g. "anthropic/claude-sonnet-4-20250514"
thinking_level: medium # off | minimal | low | medium | high | xhigh
cwd: "" # default working directory for runs
tools: [] # empty = Pi defaults (read, bash, edit, write); else an allowlist of tool names
agent_dir: "" # Pi global config dir; empty = ~/.pi/agent

# Stream AgentEvent frames here, grouped by session_id. The console and
# acp worker both read this stream.
events_stream: agent::events

# Raw Pi events (exact AgentSession event shapes: agent_start/end, turn_*,
# message_*, tool_execution_*) mirrored verbatim here, grouped by session_id.
raw_events_stream: pi::events

# Append the iii runtime context to the system prompt: teaches the agent
# live discovery against the engine catalog through the iii CLI
# (engine::functions::list, `iii trigger <fn> --help`). Per-turn override
# via the iii_context payload field; a caller-supplied system_prompt
# always wins and gets nothing appended.
iii_context: true
16 changes: 16 additions & 0 deletions pi/iii-permissions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Agent permissions for the pi worker.
# Spec: docs/sops/new-worker.md § 7. First-match-wins.
#
# pi::run / pi::start (and the run::start_and_wait alias) spawn a full Pi
# coding agent with the host's filesystem and shell — an agent invoking those
# without human approval is a privilege escalation, so they are NOT
# allow-listed and stay at the needs_approval default. pi::steer / pi::follow_up
# inject instructions into a live run, so they stay gated too. Read-only
# introspection is safe to allow.
version: 1

rules:
# internal config-reload callback — bus-internal, never agent-callable
- '!pi::on-config-change'
- pi::status
- pi::sessions::list
16 changes: 16 additions & 0 deletions pi/iii.worker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
iii: v1
name: pi
language: javascript
deploy: bundle
manifest: package.json
description: Pi coding agent as an iii worker — pi::* functions run headless Pi turns, mirror raw events onto pi::events, and stream AgentEvent frames onto agent::events.

runtime:
kind: javascript

scripts:
start: node ./index.mjs
Comment thread
rohitg00 marked this conversation as resolved.

dependencies:
iii-state: "^0.17.0"
iii-stream: "^0.17.0"
44 changes: 44 additions & 0 deletions pi/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "pi",
"version": "0.1.0",
"private": true,
"description": "Pi coding agent as an iii worker: headless turns over the iii bus, raw events on pi::events, AgentEvent frames on agent::events.",
"license": "Apache-2.0",
"type": "module",
"engines": {
"node": ">=22"
},
"packageManager": "pnpm@10.18.2",
"scripts": {
"build": "tsc -b",
"build:bundle": "node scripts/build-bundle.mjs",
"typecheck": "tsc -b --noEmit",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"test": "vitest run",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"bin": {
"pi": "./dist/index.js"
},
"dependencies": {
"@earendil-works/pi-coding-agent": "^0.79.0",
"iii-sdk": "^0.19.2",
"yaml": "^2.6.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@biomejs/biome": "2.4.10",
"@types/node": "^22.10.0",
"esbuild": "^0.25.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
}
}
Loading
Loading