Skip to content

@shutter-network/concorde/pi ​

The pi Agent Implementation drives the pi coding agent as the Signal Worker's Runtime. An Agent Implementation is the interchangeable agent program a Run happens in, and pi is the one this package adapts.

createPiRuntime is the whole of it for an Operator: hand it an Agent Container, and pass what comes back as the Signal Worker's runtime. piRun and interpretPiOutput are pure functions, exported to be called from a test and to be read. piRun holds everything specific to pi and nothing else does, so it is the entire size of the job for an author writing a second Agent Implementation.

Nothing about a container is here. The Agent Container, the Mount Table, the argument assembly, the confinement flags, the process handling and the diagnosis appended to a failure are all on @shutter-network/concorde/agent-container, generic over which agent runs, so a second Agent Implementation takes them unchanged. Read that subpath for what an Agent Container declares: createPiRuntime takes one written exactly as it is written there.

Nothing pi-shaped is here either, and there is no configuration type at all. The model and the provider are defaultModel and defaultProvider in a settings.json the Operator mounts. The working directory and the agent's own directory are WORKDIR and PI_CODING_AGENT_DIR in an image the Operator builds, no pi image being published. The Session directory is pi's own to resolve. Nothing here writes a file or names a path, and so nothing here can refuse a deployment that is missing one: that deployment is a Gateway which starts, serves, and then fails its first Run permanently.

Example ​

A Gateway whose Runtime is pi, in a container the Operator declared.

ts
import { readFileSync } from "node:fs";
import { createGateway } from "@shutter-network/concorde/gateway";
import { createPiRuntime } from "@shutter-network/concorde/pi";
import { templateHandler } from "@shutter-network/concorde/signals";

const runtime = createPiRuntime({
  image: "my-agent:1",
  networks: ["concorde_default"],
  // Only what is named here reaches the agent. None of the Gateway's own environment does.
  env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? "" },
  mounts: {
    runtimeDir: "/srv/concorde",
    entries: [
      { agentPath: "/workspace", path: "workspace" },
      { agentPath: "/workspace/AGENTS.md", path: "AGENTS.md", readOnly: true },
    ],
  },
});

// The command line, without starting a container: the one way to see the defaults applied.
console.log(runtime.commandFor({ session: "notes", text: "say hello" }).redactedArgs);

const gateway = createGateway({
  databaseUrl: process.env.DATABASE_URL ?? "",
  runtime,
  // Not loopback: the agent reaches this server from a container of its own.
  agentListen: { host: "0.0.0.0", port: 8081 },
  publicListen: { host: "0.0.0.0", port: 8080 },
  handlers: () => ({
    "note.written": templateHandler({
      template: readFileSync(new URL("./prompts/note-written.hbs", import.meta.url), "utf8"),
      session: () => "notes",
      data: (signal) => signal.payload,
    }),
  }),
});

await gateway.start();

Functions ​

createPiRuntime() ​

ts
function createPiRuntime(container: AgentContainer): AgentContainerRuntime;

Builds a Runtime that runs pi as one fresh container per Run, of the image the container names.

Two defaults sit beneath the Operator's own, and a container stating either one gets what it asked for. entrypoint is ["pi"], so an image that starts something else, or a pi installed somewhere unusual, is a field rather than a workaround. PI_OFFLINE is set, because a Gateway has no use for pi's version check and its update telemetry, and a Run must not depend on reaching pi.dev.

Parameters ​

container ​

AgentContainer

Returns ​

AgentContainerRuntime

Throws ​

If the container names no image, or if its Mount Table cannot mean what it says.


interpretPiOutput() ​

ts
function interpretPiOutput(source: AsyncIterable<Uint8Array<ArrayBufferLike>>, session: string): Promise<RunOutcome>;

Reads one Run's pi --mode json output and reports how the Run ended.

No exit code is read and none is taken, because --mode json exits 0 on a model error and on an API error. What decides the outcome is the stop reason on the last assistant message before the agent settled. An agent_end record is not that settle: it fires per low-level agent run, and a retry or a compaction can follow it and continue the same Run, so a stream ending after one is a Run that did not finish.

The source is the container's stdout as raw chunks rather than as decoded text, a chunk boundary falling wherever the operating system puts it, including inside a multi-byte character.

Bad output never throws. A stream that stopped early, ended mid-record, or carried a line that is not a record is a failed Run with a reason, and never a success inferred from the records that did parse. Every reason names the session, because a Run's error column is the only thing an Operator has to go on, and Session user_42 produced no output at all says where to look.

The whole source is consumed even once the outcome is known, a subprocess whose stdout stops being read blocking as soon as the pipe fills, which would turn a finished Run into a hang. There is no timeout here or anywhere else, so a stream that never ends never returns.

Parameters ​

source ​

AsyncIterable<Uint8Array<ArrayBufferLike>>

session ​

string

Returns ​

Promise<RunOutcome>


piRun() ​

ts
function piRun(prompt: RunPrompt): RunPlan;

Plans one Run as pi needs it performed: three flags, the Prompt on stdin, and a reader for the JSONL that comes back. The flags are --mode json, --session-id <session> and --no-approve, and nothing else is passed.

The Prompt goes on stdin, never argv, and that is not a style choice. pi reads a leading @word on argv as a file to include, and refuses an argument starting with - as an unknown option. Both are ordinary Handlebars output. Piped stdin becomes the initial message with neither treatment applied.

Pure, and a total function of its Prompt. Nothing is started, nothing is written, and no Session name is invented: the Session is already a name by the time it arrives here, the Signal Worker having answered a Handler's request for a fresh one against the Run row it had just written. The reader is interpretPiOutput, closed over that Session, so a failure says which Session it was.

Parameters ​

prompt ​

RunPrompt

Returns ​

RunPlan

Throws ​

If the Prompt has no text. The agent drops an empty message rather than answering it, so the Run would settle having said nothing.