Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
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.
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