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
77 changes: 36 additions & 41 deletions docs/design/agent-workflows/documentation/adapters/agenta.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,21 @@ text. The contract between the two halves is the skill **name**: `AGENTA_FORCED_
names, and each must match a committed directory under the runner's skills root.

Because the Agenta harness IS Pi, its tools are delivered the Pi-native way (through the
extension on the ACP path, through `buildCustomTools` in process), never over MCP. The forced
`read` and `bash` tools are Pi built-ins, so they ride the wire as built-in names, not resolved
specs.
extension on the ACP path), never over MCP. The forced `read` and `bash` tools are Pi
built-ins, so they ride the wire as built-in names, not resolved specs.

## How a skill reaches the model

The flow below is for the in-process engine. The deployed path (sandbox-agent over ACP) reaches the
same end state by a different mechanism, described in
[On the sandbox-agent (ACP) path](#on-the-sandbox-agent-acp-path) below.
The Agenta harness runs over the sandbox-agent ACP path, the one engine the runner has. The
forced *skills* cannot ride the `/run` wire as text (a skill is a directory that may
reference relative scripts and assets), so the wire carries only the skill **names** and the
runner lays the bundled directories into the Pi agent dir.

1. `AgentaHarness._to_harness_config` puts the forced skill names on the `skills` field of
the `/run` request (`AgentaAgentConfig.wire_tools`).
2. The in-process Pi engine (`engines/pi.ts`) resolves each name against its bundled
`skills/` root (override with `AGENTA_AGENT_SKILLS_DIR`) and passes the directories to Pi's
`DefaultResourceLoader` as `additionalSkillPaths`, with `noSkills: true` so only the
bundled skills load (the run stays hermetic, like `noContextFiles`).
2. `runSandboxAgent` resolves each name against its bundled `skills/` root
(`engines/skills.ts`, override with `AGENTA_AGENT_SKILLS_DIR`) and writes the directories
into the Pi agent dir's `skills/` (user scope).
3. Pi loads them, and because the forced `read` tool is enabled, surfaces them in the system
prompt. The model reads a skill's `SKILL.md` on demand (progressive disclosure).

Expand All @@ -56,42 +55,38 @@ This follows Pi's own split (see `PiAgentConfig`): the **persona** ("who the age
belongs in `append_system`, and **project conventions** belong in `AGENTS.md`. So the Agenta
persona is a forced `append_system`, while the Agenta base preamble plus the author's
instructions are the `AGENTS.md`. An author's own `system` / `append_system` (via
`AgentConfig.harness_options["pi"]`) still apply, layered after the forced persona.
`AgentConfig.harness_options["pi_core"]`) still apply, layered after the forced persona.

## Selecting it

`agenta` is a harness option alongside `pi` and `claude` (the playground dropdown, the
`pi_agenta` is a harness option alongside `pi_core` and `claude` (the playground dropdown, the
`harness` field). The deployed service path routes it through `SandboxAgentBackend`, which
drives Pi over ACP and layers the Agenta persona and tools on top.

## On the sandbox-agent (ACP) path

`SandboxAgentBackend` also lists `HarnessType.AGENTA` as supported, so `agenta` runs over ACP through
the sandbox-agent daemon as well. This is what lets it use the Daytona sandbox. The Agenta harness is
Pi with an opinion, and the sandbox-agent daemon only knows real agents (`pi`, `claude`, …), so the
runner maps `agenta` onto the `pi` ACP agent (`acpAgent` in `engines/sandbox_agent.ts`) and treats it
as Pi for capabilities, model resolution, and tracing.

The forced *skills* cannot ride the `/run` wire as text (a skill is a directory that may
reference relative scripts and assets), so the wire carries only the skill **names** and the
runner lays the bundled directories into the Pi **agent dir**'s `skills/` (user scope).
`runSandboxAgent` resolves the names against the bundled `skills/` root (`engines/skills.ts`, shared
with the in-process engine). The agent dir is deliberate. Pi auto-discovers and enables
user-scope skills (`<agentDir>/skills/`) on every run, whereas project skills
(`<cwd>/.pi/skills/`) are trust-gated and would not load in this headless run.

Because the forced skills are user-scope, writing them into the *shared* agent dir would leak
them into later plain `pi` runs on the same sidecar (and could pollute a developer's real
`~/.pi/agent`). So each path gives the run its own agent dir: on **Daytona** the sandbox is
already fresh per run (`uploadSkillsToSandbox`); on **local** an Agenta run gets a throwaway
per-run agent dir seeded from the login (`auth.json` / `settings.json`), with the extension and
skills installed into it and the daemon pointed at it via `PI_CODING_AGENT_DIR`
(`prepareLocalAgentDir`), removed after the run. A plain `pi` run is unchanged (it installs only
the extension into the shared agent dir).

The base AGENTS.md preamble still rides the wire as `agentsMd` (written into the session `cwd`),
and the forced `read` / `bash` tools are Pi defaults under pi-acp. The one gap versus the
in-process path is the persona `appendSystemPrompt`, which pi-acp gives no per-run hook to set;
it is logged and skipped on the sandbox-agent Pi path (the same pre-existing limitation as plain Pi over
ACP), so on sandbox-agent the Agenta persona is not yet applied. Daytona skill uploads are UTF-8 text
only (`writeFsFile` takes a string body); binary skill assets are a follow-up.
`SandboxAgentBackend` lists `HarnessType.AGENTA` (`pi_agenta`) as supported, so it runs over
ACP through the sandbox-agent daemon. This is what lets it use the Daytona sandbox. The Agenta
harness is Pi with an opinion, and the sandbox-agent daemon only knows real agents (`pi`,
`claude`, …), so the runner maps `pi_agenta` onto the `pi` ACP agent (`acpAgent` in
`engines/sandbox_agent/run-plan.ts`, where `pi_core` and `pi_agenta` both resolve to `pi`) and
treats it as Pi for capabilities, model resolution, and tracing.

The agent dir matters. Pi auto-discovers and enables user-scope skills
(`<agentDir>/skills/`) on every run, whereas project skills (`<cwd>/.pi/skills/`) are
trust-gated and would not load in this headless run. Because the forced skills are user-scope,
writing them into the *shared* agent dir would leak them into later plain `pi_core` runs on
the same sidecar (and could pollute a developer's real `~/.pi/agent`). So each path gives the
run its own agent dir: on **Daytona** the sandbox is already fresh per run
(`uploadSkillsToSandbox`); on **local** an Agenta run gets a throwaway per-run agent dir
seeded from the login (`auth.json` / `settings.json`), with the extension and skills installed
into it and the daemon pointed at it via `PI_CODING_AGENT_DIR` (`prepareLocalAgentDir`),
removed after the run. A plain `pi_core` run is unchanged (it installs only the extension into
the shared agent dir).

The base AGENTS.md preamble rides the wire as `agentsMd` (written into the session `cwd`), and
the forced `read` / `bash` tools are Pi defaults under pi-acp. The persona rides the wire as
`appendSystemPrompt` and the engine writes it into the per-run Pi agent dir as
`APPEND_SYSTEM.md` (`engines/sandbox_agent/pi-assets.ts`), so Pi loads it on the run. Daytona
skill uploads are UTF-8 text only (`writeFsFile` takes a string body); binary skill assets are
a follow-up.
52 changes: 13 additions & 39 deletions docs/design/agent-workflows/documentation/adapters/pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@ can use, so much of the work happens inside Pi rather than around it.
Read the [architecture](../architecture.md) and [ports and adapters](../ports-and-adapters.md)
pages first. This page assumes the relay and the wire contract.

## Two ways Pi runs
## How Pi runs

Pi runs through one of two engines, both behind the same port:

- **Over ACP, through sandbox-agent** (`engines/sandbox_agent.ts` with `harness: pi`). This is the main
path and the one the rest of this page describes. The sandbox-agent daemon starts the `pi-acp`
adapter, which starts the `pi` CLI.
- **In-process** (`engines/pi.ts`). This drives the Pi SDK directly inside the sidecar, with
no daemon, no adapter, and no ACP. It is the simplest local path and a fallback. The last
section covers it.
Pi runs over ACP, through sandbox-agent (`engines/sandbox_agent.ts`). The harness value
`pi_core` (plain Pi) and `pi_agenta` (Pi with Agenta's forced opinion) both map to the `pi`
ACP agent. This is the one engine the runner has. The sandbox-agent daemon starts the
`pi-acp` adapter, which starts the `pi` CLI.

## The ACP path: pi-acp plus a bundled extension

Expand Down Expand Up @@ -82,30 +78,26 @@ adapter reads only its own slice:
AgentConfig(
instructions="Project: a SQL analytics tool. Run `make lint` before finishing.", # AGENTS.md
harness_options={
"pi": {
"pi_core": {
"system": "You are a SQL expert. Only answer with queries.", # replaces base prompt
"append_system": "Always explain each query in one line.", # adds to base prompt
}
},
)
```

`PiHarness` lifts the `pi` slice onto `PiAgentConfig.system` / `append_system`, which emit
`PiHarness` lifts the `pi_core` slice onto `PiAgentConfig.system` / `append_system`, which emit
`systemPrompt` / `appendSystemPrompt` on the `/run` wire. An empty or whitespace value is
dropped, so it never reaches the runner as a real override.

### Delivery status

The **in-process Pi engine** honors both. It feeds them through the resource loader's
`systemPromptOverride` / `appendSystemPromptOverride`, so the run stays hermetic: only what
the request carries applies, never a `SYSTEM.md` or `APPEND_SYSTEM.md` left on disk.

The **ACP (sandbox-agent) path does not deliver them yet**. It drives Pi through `pi-acp`, which gives
us no per-run hook to set the prompt: a project `.pi/SYSTEM.md` is trust-gated, and the CLI
`--system-prompt` flag cannot be set per session through the adapter. The engine logs a
warning when these fields are set on that path so the gap is visible, not silent. `AGENTS.md`
still applies there, because Pi loads context files regardless of trust. Wiring the ACP path
(via project trust plus `.pi/SYSTEM.md`, or per-session CLI flags) is the remaining work.
The **ACP (sandbox-agent) path honors both**. The engine writes `SYSTEM.md` /
`APPEND_SYSTEM.md` into the per-run Pi agent dir, local and Daytona
(`services/agent/src/engines/sandbox_agent/pi-assets.ts`), and Pi loads them on the run.
Because each run gets its own agent dir, the override stays scoped to that run and never
leaks to a later run on the same sidecar. `AGENTS.md` still applies alongside, because Pi
loads context files regardless.

## Tracing: Pi instruments itself

Expand Down Expand Up @@ -158,21 +150,3 @@ Two things differ on Daytona. The sandbox-agent `-full` image ships the `pi-acp`
pre-baked snapshot that already has it (the snapshot path avoids a slow per-run install).
And auth comes from the provider key in the sandbox env when present, or from an uploaded
`auth.json` (the developer's OAuth login) when no key is set.

## The in-process engine

The in-process Pi engine (`engines/pi.ts`, reached with `backend: "pi"`) skips sandbox-agent
entirely. It drives Pi's `createAgentSession` directly, with everything in memory: AGENTS.md
injected through the resource loader, the session and settings managers in memory, and a
throwaway working directory. It registers the same tools as Pi `customTools` through
`buildCustomTools`, and traces with the same extension logic, just wired in process rather than
loaded from disk. One difference from the ACP path: there is no file relay. Because the engine
runs in the same process as the runner, each tool body executes directly through
`runResolvedTool` (a gateway tool POSTs to `/tools/call`, a code tool spawns a local
subprocess). The relay only exists on the ACP path, where a separate Pi process or a Daytona
sandbox cannot reach Agenta or hold the private spec. The in-process engine also ignores
`mcp_servers` entirely (`PI_CAPABILITIES.mcpTools` is false).

It returns the same `/run` result as the sandbox-agent path, which is the whole point of the ports:
the workflow author cannot tell which engine ran. It exists for the simplest local case and
as a path that does not depend on the sandbox-agent daemon being present.
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Its fields and defaults:
| `model` | `str` | `"gpt-5.5"` | `x-parameter: grouped_choice`, plain string |
| `tools` | `List[ToolConfig]` | empty list | typed discriminated union |
| `mcp_servers` | `List[MCPServerConfig]` | empty list | typed |
| `harness` | `Literal["pi","claude","agenta"]` | `"pi"` | enum |
| `harness` | `Literal["pi_core","claude","pi_agenta"]` | `"pi_core"` | enum |
| `sandbox` | `Literal["local","daytona"]` | `"local"` | enum |
| `permission_policy` | `Literal["auto","deny"]` | `"auto"` | enum |

Expand Down Expand Up @@ -192,7 +192,7 @@ Legend: (a) catalog/schema, (b) SDK neutral config, (c) runtime.
| agents_md | yes, `agents_md: str` | yes, as `instructions` | wired to `agentsMd` | The schema names it `agents_md`. The neutral config names it `instructions`. |
| harness | yes, enum | no, on `RunSelection` | wired, picks the harness class | Enum-enforced. The runtime validates via `make_harness`. |
| sandbox | yes, enum | no, on `RunSelection` | wired to the backend, absent from `SessionConfig` | Backend concern, not agent identity. |
| permission_policy | yes, enum | no, on `RunSelection` | wired to `SessionConfig` | Only the Claude harness reads it. Pi ignores it, so it is decorative for pi and agenta. |
| permission_policy | yes, enum | no, on `RunSelection` | wired to `SessionConfig` | Only the Claude harness reads it. Pi ignores it, so it is decorative for `pi_core` and `pi_agenta`. |

## Notable gaps and quirks

Expand Down Expand Up @@ -230,7 +230,7 @@ This is what the playground saves and the runtime reads:
"args": ["-y", "@modelcontextprotocol/server-github"]
}
],
"harness": "pi",
"harness": "pi_core",
"sandbox": "local",
"permission_policy": "auto"
}
Expand Down
Loading
Loading