@aeondave/pi-persona 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +249 -0
- package/CHANGELOG.md +260 -0
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/agents/balthasar.md +32 -0
- package/agents/casper.md +33 -0
- package/agents/evidence-verifier.md +23 -0
- package/agents/melchior.md +32 -0
- package/agents/operator.md +63 -0
- package/agents/research.md +58 -0
- package/agents/reviewer.md +49 -0
- package/agents/scout.md +23 -0
- package/agents/verifier.md +21 -0
- package/assets/banner.png +0 -0
- package/assets/demo1.gif +0 -0
- package/assets/demo1.mp4 +0 -0
- package/assets/workflow.png +0 -0
- package/contracts/finding.contract.json +9 -0
- package/docs/ARCHITECTURE.md +727 -0
- package/docs/EXPERIENCE.md +96 -0
- package/docs/MONITORS.md +84 -0
- package/docs/README.md +53 -0
- package/docs/REFERENCE.md +408 -0
- package/docs/SPINE.md +152 -0
- package/docs/STRATEGIES.md +215 -0
- package/docs/TELEMETRY.md +9 -0
- package/flows/deep-review.flow.json +8 -0
- package/flows/gated-build.flow.json +9 -0
- package/package.json +106 -0
- package/personas/audit.md +29 -0
- package/personas/dev.md +52 -0
- package/personas/elite.md +256 -0
- package/personas/judge.md +32 -0
- package/personas/magi.md +41 -0
- package/personas/planner.md +37 -0
- package/personas/researcher.md +37 -0
- package/personas/swarm.md +38 -0
- package/personas/verify.md +31 -0
- package/presets/magi-rounds.preset.json +5 -0
- package/prompts/spine.md +11 -0
- package/prompts/spine.worker.md +9 -0
- package/src/agents/agent.ts +78 -0
- package/src/bridge.ts +209 -0
- package/src/bus/broker/client.ts +379 -0
- package/src/bus/broker/framing.ts +43 -0
- package/src/bus/broker/host.ts +407 -0
- package/src/bus/broker/messages.ts +87 -0
- package/src/bus/broker/paths.ts +31 -0
- package/src/bus/contact.ts +145 -0
- package/src/bus/inproc.ts +279 -0
- package/src/bus/peers.ts +114 -0
- package/src/core/brief.ts +356 -0
- package/src/core/capabilities.ts +114 -0
- package/src/core/config.ts +228 -0
- package/src/core/contract.ts +391 -0
- package/src/core/data-root.ts +260 -0
- package/src/core/discovery.ts +33 -0
- package/src/core/display-label.ts +47 -0
- package/src/core/fence.ts +151 -0
- package/src/core/format.ts +13 -0
- package/src/core/frontmatter.ts +202 -0
- package/src/core/models.ts +192 -0
- package/src/core/monitor.ts +415 -0
- package/src/core/naming.ts +42 -0
- package/src/core/nudge.ts +238 -0
- package/src/core/ownership.ts +102 -0
- package/src/core/permissions.ts +56 -0
- package/src/core/pi-compat.ts +50 -0
- package/src/core/seed.ts +770 -0
- package/src/core/session-identity.ts +89 -0
- package/src/core/time.ts +202 -0
- package/src/core/timer.ts +221 -0
- package/src/core/types.ts +28 -0
- package/src/engine/adapter.ts +257 -0
- package/src/engine/async.ts +898 -0
- package/src/engine/change-report.ts +143 -0
- package/src/engine/child.ts +532 -0
- package/src/engine/errors.ts +30 -0
- package/src/engine/fallback.ts +57 -0
- package/src/engine/handles.ts +21 -0
- package/src/engine/inproc.ts +857 -0
- package/src/engine/signals.ts +11 -0
- package/src/engine/spec-preflight.ts +33 -0
- package/src/engine/stream.ts +369 -0
- package/src/engine/worktree.ts +144 -0
- package/src/exocom/activation.ts +31 -0
- package/src/exocom/codes.ts +261 -0
- package/src/exocom/envelope.ts +180 -0
- package/src/exocom/gate.ts +6 -0
- package/src/exocom/guards.ts +45 -0
- package/src/exocom/inbound.ts +141 -0
- package/src/exocom/install.ts +970 -0
- package/src/exocom/ledger.ts +603 -0
- package/src/exocom/limits.ts +20 -0
- package/src/exocom/paths.ts +35 -0
- package/src/exocom/plane.ts +1116 -0
- package/src/exocom/registry.ts +307 -0
- package/src/exocom/scope.ts +42 -0
- package/src/exocom/status.ts +86 -0
- package/src/exocom/untrusted.ts +40 -0
- package/src/exocom/wait.ts +23 -0
- package/src/exocom/write-guard.ts +89 -0
- package/src/extension/broker-host.ts +181 -0
- package/src/extension/clock.ts +14 -0
- package/src/extension/engine.ts +321 -0
- package/src/extension/hooks.ts +796 -0
- package/src/extension/identity.ts +244 -0
- package/src/extension/shared.ts +175 -0
- package/src/extension.ts +2221 -0
- package/src/loader.ts +218 -0
- package/src/monitor/process.ts +116 -0
- package/src/monitor/session.ts +61 -0
- package/src/orchestration/flow-journal.ts +86 -0
- package/src/orchestration/flow-run.ts +301 -0
- package/src/orchestration/flow.ts +125 -0
- package/src/orchestration/judge.ts +78 -0
- package/src/orchestration/model-retry.ts +85 -0
- package/src/orchestration/parallel.ts +107 -0
- package/src/orchestration/params.ts +23 -0
- package/src/orchestration/reducers.ts +123 -0
- package/src/orchestration/render.ts +127 -0
- package/src/orchestration/roster.ts +133 -0
- package/src/orchestration/sdk.ts +326 -0
- package/src/orchestration/strategies/compete.ts +182 -0
- package/src/orchestration/strategies/council-rounds.ts +124 -0
- package/src/orchestration/strategies/critic-loop.ts +168 -0
- package/src/orchestration/strategies/debate.ts +126 -0
- package/src/orchestration/strategies/fanout.ts +20 -0
- package/src/orchestration/strategies/judge.ts +117 -0
- package/src/orchestration/strategies/magi.ts +239 -0
- package/src/orchestration/strategies/map.ts +327 -0
- package/src/orchestration/strategies/pair.ts +72 -0
- package/src/orchestration/strategies/pipeline.ts +91 -0
- package/src/orchestration/strategies/synthesize.ts +99 -0
- package/src/orchestration/strategy.ts +49 -0
- package/src/orchestration/types.ts +31 -0
- package/src/orchestration/voting.ts +126 -0
- package/src/persona/config-store.ts +215 -0
- package/src/persona/controller.ts +173 -0
- package/src/persona/gating.ts +82 -0
- package/src/persona/orchestrate.ts +83 -0
- package/src/persona/persona.ts +294 -0
- package/src/persona/spine.ts +221 -0
- package/src/persona/state.ts +28 -0
- package/src/telemetry/contract.ts +244 -0
- package/src/telemetry/paths.ts +14 -0
- package/src/telemetry/producer.ts +390 -0
- package/src/tools/council.ts +200 -0
- package/src/tools/delegate-tool.ts +637 -0
- package/src/tools/delegate.ts +661 -0
- package/src/tools/exocom-work.ts +328 -0
- package/src/tools/exocom.ts +476 -0
- package/src/tools/flow.ts +117 -0
- package/src/tools/intercom-tool.ts +292 -0
- package/src/tools/intercom.ts +181 -0
- package/src/tools/models.ts +39 -0
- package/src/tools/monitor.ts +66 -0
- package/src/tools/timer.ts +105 -0
- package/src/ui/agent-overlay.ts +526 -0
- package/src/ui/agent-tree.ts +307 -0
- package/src/ui/live-clock.ts +42 -0
- package/src/ui/model-picker.ts +160 -0
- package/src/ui/presentation.ts +170 -0
- package/src/ui/usage.ts +133 -0
- package/teams.yaml +27 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
See [`docs/README.md`](./README.md) for the canonical doc set index.
|
|
4
|
+
|
|
5
|
+
The design contract for pi-persona — the model, the binding invariants, the module map, and the
|
|
6
|
+
runtime seams. This is the **canonical design reference**: on any conflict, this document and the
|
|
7
|
+
invariants below win. For the user guide see the [README](../README.md); for the orchestration layer
|
|
8
|
+
in depth see [STRATEGIES.md](STRATEGIES.md); for the contributor quick-reference (commands,
|
|
9
|
+
conventions, gotchas) see [AGENTS.md](../AGENTS.md).
|
|
10
|
+
|
|
11
|
+
pi-persona is a single Pi coding-agent extension (`@earendil-works/pi-*`, loaded via tsx/jiti — no
|
|
12
|
+
build step) for **supervised multi-agent orchestration**: async workers, live steering, cross-session
|
|
13
|
+
collaboration, and switchable personas. It fuses three concerns — delegation/engine, coordination/bus,
|
|
14
|
+
persona/identity — into one cohesive, cross-OS, data-driven codebase.
|
|
15
|
+
|
|
16
|
+
## The model
|
|
17
|
+
|
|
18
|
+
| Thing | Is | Lives in |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| **Persona** | the control surface: a supervisor identity (system-prompt body) + how it orchestrates | `personas/*.md` (`persona: true`) |
|
|
21
|
+
| **Agent** | a unit of work that gets spawned (prompt + model + enforced tool allow/deny); does not orchestrate | `agents/*.md` |
|
|
22
|
+
| **Strategy** | an execution shape (vote, loop, fan-out) authored as code on the SDK | `src/orchestration/strategies/*.ts` |
|
|
23
|
+
| **Flow** | a declarative DAG that composes strategies into phases with `needs` edges + gates | `flows/*.flow.json` |
|
|
24
|
+
| **Team** | a named roster of agents a strategy runs over | `teams.yaml` |
|
|
25
|
+
| **Contract** | the structured shape a sub-agent returns, so votes/judges tally | `contracts/*.contract.json` |
|
|
26
|
+
|
|
27
|
+
A persona binds to an orchestration mode → which selects a built-in shape or a strategy/flow → which
|
|
28
|
+
runs over a roster of agents → lowering to engine primitives + deterministic JS reducers. A persona
|
|
29
|
+
file *is* an agent file with `persona: true` plus the orchestration grammar; one frontmatter engine
|
|
30
|
+
parses both.
|
|
31
|
+
|
|
32
|
+
Persona names are data, never dispatch keys. Optional `delegation:` fields apply generic gates to any
|
|
33
|
+
builtin, user, or project persona: `requireBrief`, `outputContract`, `requireDisjointWrites`,
|
|
34
|
+
`requireFreshVerification`, and `verificationAgents`. Council behavior remains independently
|
|
35
|
+
declarative through `council: { strategy, roster, params }`; a custom persona can compose any installed
|
|
36
|
+
strategy/team without a core change. The gates use effective agent/tool capabilities, conservatively
|
|
37
|
+
treat shell-capable, MCP-enabled, or unknown tools as potential writers, and enforce the requested fan-out
|
|
38
|
+
concurrency in both synchronous and background execution. A serialized verifier is accepted only
|
|
39
|
+
after every material writer in that batch; otherwise the call fails before a child starts. The rule
|
|
40
|
+
also holds ACROSS calls: a verifier launched while a material-writer background run is still live is
|
|
41
|
+
rejected with the run id to wait on, so a later call cannot slip past the in-batch ordering.
|
|
42
|
+
|
|
43
|
+
### The effort ladder (make the simple case simple; complexity is opt-in)
|
|
44
|
+
|
|
45
|
+
- **L0 — prompt-driven.** Just the persona body + the `delegate` tool, no orchestration block. The
|
|
46
|
+
supervisor LLM fans out by its own judgement ("research X, Y, Z" → one sub-agent per item). Absent
|
|
47
|
+
an orchestration block ⇒ pure L0.
|
|
48
|
+
- **L1 — declarative shape.** A persona `orchestration: { mode: parallel|pipeline, roster }` (or the
|
|
49
|
+
`council:` tool form) runs a built-in strategy deterministically.
|
|
50
|
+
A caller may borrow another installed persona's declared council with `council({ persona: "name" })`;
|
|
51
|
+
this resolves configuration only and never activates or inherits authority from the target persona.
|
|
52
|
+
- **L2 — strategy file.** `src/orchestration/strategies/<name>.ts` on the Strategy SDK drives a
|
|
53
|
+
richer shape (magi vote, critic loop, debate).
|
|
54
|
+
- **L3 — flow file.** `flows/<name>.flow.json` composes strategies into a resumable DAG.
|
|
55
|
+
|
|
56
|
+
You climb only when you want determinism, structure, or reuse. Nothing forces the higher tiers.
|
|
57
|
+
|
|
58
|
+
## Binding invariants (I1–I7)
|
|
59
|
+
|
|
60
|
+
These are the guardrails a contributor must not violate. They are enforced in code, not by convention.
|
|
61
|
+
|
|
62
|
+
- **I1 — Child-process is the correctness baseline.** `ChildProcessEngine` (`engine/child.ts`, spawns
|
|
63
|
+
`pi --mode json -p`) is the reference backend; `InProcessEngine` (`engine/inproc.ts`,
|
|
64
|
+
`createAgentSession`) is the runtime **default** but a swappable optimization behind the
|
|
65
|
+
`StrategyEngine` seam. Behavior must hold on the child backend first; worktree isolation always uses
|
|
66
|
+
the child engine. Opt to the baseline with `PI_PERSONA_ENGINE=child`.
|
|
67
|
+
- **I2 — Strategies are trusted project code, NOT a security sandbox.** Gated by Pi project-trust; the
|
|
68
|
+
SDK is a constrained API *by convention*. Safety comes from **runtime limits, not isolation**:
|
|
69
|
+
`RUN_LIMITS` (`maxChildren`, `maxConcurrency`, `budgetTokens`, `timeoutMs` idle window) are enforced
|
|
70
|
+
by the SDK on every `agent()` call and by the engine per child. Concurrency is shared across all
|
|
71
|
+
calls in one SDK instance; token admission uses completed usage and is rechecked after queueing, so
|
|
72
|
+
active legs can overshoot it. Nesting depth is not one of these numeric knobs — it is structural,
|
|
73
|
+
capped at 1: a child's whole pi-persona extension activation short-circuits under
|
|
74
|
+
`PI_PERSONA_DISABLE=1` (the **fork-bomb guard**), so it registers NO tools at all —
|
|
75
|
+
`delegate`/`council`/`orchestrate`/`flow` included. On the child engine this is a one-shot env var
|
|
76
|
+
set on each spawned process; the in-process engine instead shares one Node process across
|
|
77
|
+
concurrently-building sub-sessions, so it ref-counts the guard (`inproc.ts`) to keep it set until the
|
|
78
|
+
LAST one finishes. The in-process engine also excludes `ORCHESTRATION_TOOLS` from the child session
|
|
79
|
+
directly (`engine/inproc.ts`) as a
|
|
80
|
+
second line of defense, in case anything else ever left the extension active. `PI_PERSONA_LEG=1`
|
|
81
|
+
rides alongside `PI_PERSONA_DISABLE` — a **dedicated** worker-leg marker, distinct from the
|
|
82
|
+
user-settable `PI_PERSONA_DISABLE` kill switch, that a companion extension (e.g. pi-persona-mind)
|
|
83
|
+
reads to tell a real delegated leg from a disabled supervisor. Never claim isolation from
|
|
84
|
+
`fs`/`net`/`process`. The param schema and other convenience checks stay **lenient** (warn, never
|
|
85
|
+
hard-fail) for the same reason.
|
|
86
|
+
- **I3 — Per-run pinning.** A run pins `contract@hash` at start; hot-reload affects **new runs only**;
|
|
87
|
+
an active run never changes schema/logic mid-flight. `makeEngine`/`makeInProcessEngine` pin the
|
|
88
|
+
contract on first use and reuse the frozen snapshot for the whole run.
|
|
89
|
+
- **I4 — Capabilities enforced at call time, never prompt-only.** Every tool call, delegate, and bus
|
|
90
|
+
action passes one `EffectiveCapabilities` (`core/capabilities.ts`: `tools`, `delegateTargets`,
|
|
91
|
+
`canUseBus`), resolved once on persona activation (deny-wins; `allow` present ⇒ allowlist; absent ⇒
|
|
92
|
+
default-allow). Prompt text is advisory only. Declarative delegation gates are checked before spawn:
|
|
93
|
+
required briefs, contract defaults, parallel write ownership/overlap, and stale concurrent verifier
|
|
94
|
+
topology. Restricting `tools` keeps `delegate` unless explicitly denied; `canUseBus` is OFF only when
|
|
95
|
+
the persona explicitly denies `intercom`.
|
|
96
|
+
- **I5 — Flows are the top tier.** A declarative DAG (statically verified: acyclicity, references
|
|
97
|
+
resolve, join reducers valid) above strategies. The flow engine stays thin — parse, verify,
|
|
98
|
+
schedule, persist, resume — and a flow node may call any strategy. Journaled: finished nodes replay
|
|
99
|
+
from disk with no re-spend.
|
|
100
|
+
- **I6 — Spawning is opportunistic by default; mandatory only when the persona declares it.** Two
|
|
101
|
+
first-class cases: *opportunistic* (`mode: solo` / no block — the supervisor MAY delegate by its own
|
|
102
|
+
judgement; never forced) and *mandatory* (`mode: strategy|flow|parallel|pipeline` with a roster —
|
|
103
|
+
the engine runs the shape; the LLM can't opt out, only do its part). A persona picks exactly one.
|
|
104
|
+
Mandatory hand-off preserves runtime status: only `ok:true` becomes a ruling; failure/cancellation
|
|
105
|
+
remains visibly unresolved and its fenced output is evidence, never an implicit success.
|
|
106
|
+
- **I7 — Adoption is a success criterion.** The opportunistic path must be *engineered to actually
|
|
107
|
+
fire* — under-delegation is the explicit failure to avoid. The `delegate` tool description is
|
|
108
|
+
compelling and model-agnostic; opportunistic persona bodies coach delegation with concrete
|
|
109
|
+
heuristics; success is measured by delegations actually happening, not by the capability existing.
|
|
110
|
+
|
|
111
|
+
## Design decisions (D1–D7)
|
|
112
|
+
|
|
113
|
+
- **D1 — One engine interface, two backends.** The `StrategyEngine` seam makes strategies
|
|
114
|
+
backend-agnostic; the concrete backend is an optimization/isolation choice (see I1).
|
|
115
|
+
- **D2 — One shared `core`.** A single frontmatter/YAML parser, one permission engine, one discovery,
|
|
116
|
+
one config, one contract module, one set of env constants. `core/*` is **pure** (no Pi imports) and
|
|
117
|
+
unit-tested — removing a whole class of two-parser / naming-mirror drift bugs.
|
|
118
|
+
- **D3 — Three authoring surfaces, data-driven.** `agents/*.md`, `strategies/*.ts` (files on a core
|
|
119
|
+
SDK), `flows/*.flow.json` — all lower to engine primitives + JS reducers. Built-in strategies ship
|
|
120
|
+
**as files** and double as reference examples.
|
|
121
|
+
- **D4 — Personas are a thin layer over orchestration.** persona = identity + optional default
|
|
122
|
+
strategy/flow/council + generic delegation policy; the persona file is the control surface. Runtime
|
|
123
|
+
code must not branch on a persona name.
|
|
124
|
+
- **D5 — Bias mitigations baked into the reducers.** Anonymise authors, shuffle order, keep-best
|
|
125
|
+
fallback, preserved dissent, invalid-output quarantine — deterministic JS, safe by default (see
|
|
126
|
+
[STRATEGIES.md](STRATEGIES.md#bias-guard-invariants-do-not-fix-these)).
|
|
127
|
+
- **D6 — Cross-OS by construction.** In-process avoids IPC entirely; only the opt-in cross-process
|
|
128
|
+
bus needs OS-specific transport (Unix socket vs Windows named pipe), confined to the broker — so no
|
|
129
|
+
fragile per-OS code lives in strategies.
|
|
130
|
+
- **D7 — Contracts are schema-defined and hot-editable.** The return contract lives in an editable
|
|
131
|
+
`contracts/*.contract.json`; reducers read fields generically. Add/remove fields without
|
|
132
|
+
recompiling. Hardened by I3 (hot-reload for new runs only). The contract both **instructs and
|
|
133
|
+
validates**: when a spec carries `outputContract`, each engine appends `contractInstructions(def)`
|
|
134
|
+
(the format, derived mechanically from the same pinned def it validates against) to the member's
|
|
135
|
+
task — so ANY agent can be convened by a voting strategy, not just those whose `.md` hand-writes
|
|
136
|
+
the JSON format.
|
|
137
|
+
|
|
138
|
+
## Module layout
|
|
139
|
+
|
|
140
|
+
Domain dependencies remain downward-only and acyclic: `core ← all`; `engine`/`bus → core`;
|
|
141
|
+
`orchestration → engine + bus + core`; `persona → orchestration + core`; the Exocom transport and
|
|
142
|
+
ledger → `core + bus`; `tools`/`ui → lower layers`. `src/exocom/install.ts` is deliberately the
|
|
143
|
+
composition adapter for that feature, colocated with Exocom but allowed to wire extension, persona,
|
|
144
|
+
engine, telemetry, UI, and tool surfaces; it is not part of the transport/domain layer.
|
|
145
|
+
`src/extension/*` contains extracted factory internals (which must not import `extension.ts`), while
|
|
146
|
+
`extension.ts` remains the package entry and composition root (`package.json` `pi.extensions` →
|
|
147
|
+
`./src/extension.ts`).
|
|
148
|
+
|
|
149
|
+
- **`src/agents/`** — the agent definition (`agent.ts`: `AgentConfig`/`parseAgent`, the
|
|
150
|
+
`persona: false` sibling of the persona parser; both share one frontmatter engine).
|
|
151
|
+
- **`src/core/`** — pure kernel (no Pi imports, unit-tested): `frontmatter`, `permissions` +
|
|
152
|
+
`capabilities`, `contract` (+`parseContract`), `config`, `discovery`, `seed`, `fence`
|
|
153
|
+
(`fenceUntrusted` / `attributeInbound`), `ownership` (`findWriteSetOverlaps` /
|
|
154
|
+
`validateParallelWriteSets` / `writeSetPathError` — parallel write-set overlap, shared by
|
|
155
|
+
`delegate` and `map`'s `ownership` param), `pi-compat` (`MIN_PI_VERSION`, `installedPiVersion`,
|
|
156
|
+
`satisfiesFloor` — the one place the supported host floor is declared; `package.json`
|
|
157
|
+
`peerDependencies`, `/doctor`'s `pi:` line, and the README's stated floor trace back to this
|
|
158
|
+
constant), `models`, `brief` (`buildDelegationBrief` — the per-turn delegation brief: live roster +
|
|
159
|
+
standing hand-off default; and `buildExocomBrief` — the per-turn peer brief: live peers, the
|
|
160
|
+
peer-vs-sub-agent split, and the relevance bound on an exchange), `nudge` (the two
|
|
161
|
+
runtime-reinforcement state machines, `DelegationNudge` + `PersistenceNudge`), `display-label`
|
|
162
|
+
(`sanitizeDisplayLabel` — an untrusted name reduced to bounded metadata before interpolation
|
|
163
|
+
outside a fence), `timer` (`TimerScheduler` — the pure alarm engine behind the `timer` tool), `time`
|
|
164
|
+
(`formatDuration` / `peerSentLabel` / `sessionElapsedLabel` / `buildSessionAnchor` — the elapsed-time
|
|
165
|
+
readings and the prompt-cache rule that sets each one's granularity), `types`.
|
|
166
|
+
- **`src/engine/`** — "run an agent → `AgentResult`", backend-agnostic: `child.ts`, `inproc.ts`
|
|
167
|
+
(default), `adapter.ts` (child-engine adapter), `fallback.ts` (provider fallback),
|
|
168
|
+
`spec-preflight.ts` (the one unknown-agent/unknown-contract preflight shared by both engines),
|
|
169
|
+
`change-report.ts` (`captureStatus`/`diffStatus`/`renderChangeReport` — the files-changed report
|
|
170
|
+
`src/extension/engine.ts` appends to a non-worktree leg's result; pure over the injected `GitExec`
|
|
171
|
+
from `worktree.ts`), `async.ts` (async tracker / peek), `worktree.ts` (git-worktree isolation),
|
|
172
|
+
`stream.ts` (event → state), `handles.ts` (one bus-handle sequence shared by BOTH engines),
|
|
173
|
+
`signals.ts` (`combineSignals`).
|
|
174
|
+
- **`src/orchestration/`** — the heart: `sdk.ts` (the Strategy SDK), `strategy.ts` (registry +
|
|
175
|
+
`knownParams`), `strategies/*.ts`, `voting.ts`, `judge.ts` (anonymise-for-judge), `reducers.ts`,
|
|
176
|
+
`roster.ts` (teams + `rosterSpec`), `flow*.ts` (DAG + JSONL journal + gates), `render.ts`.
|
|
177
|
+
- **`src/bus/`** — coordination: `inproc.ts` (handle-based mailbox: send/ask/reply/onMessage),
|
|
178
|
+
`contact.ts` (child `contact_supervisor`), `peers.ts` (child `contact_peer`), `broker/` (cross-process
|
|
179
|
+
relay, on by default: `paths`/`framing`/`messages` pure, `host`/`client` over `node:net`).
|
|
180
|
+
- **`src/exocom/`** — the external peer plane (the exocom section below): `plane.ts` (lifecycle —
|
|
181
|
+
bind/join/teardown + reconnect), `activation.ts` (the exact optional-value CLI shim), `codes.ts`
|
|
182
|
+
(persistent four-character alias allocation), `scope.ts` (home identity versus selected scope),
|
|
183
|
+
`registry.ts` (scope-selected presence + stale pruning), `paths.ts` (pure path layout),
|
|
184
|
+
`envelope.ts`/`inbound.ts` (wire format + the pure guardrailed delivery chain: hop cap, dedup,
|
|
185
|
+
budgets, truncation, fence/attribute), `limits.ts` (constants), `guards.ts`
|
|
186
|
+
(`SenderBudget`/`SeenMessages`), `ledger.ts` (scope JSONL work ledger), `untrusted.ts`
|
|
187
|
+
(`untrusted()`/`UNTRUSTED_MAX` — the ONE sanitizer every surface rendering a peer-authored ledger
|
|
188
|
+
field runs first, so a hostile peer's escapes or instruction-shaped text never reach output
|
|
189
|
+
unfenced), `status.ts` (`formatLedgerStatus` — the ownership view `exocom_status`/`/exocom`
|
|
190
|
+
render), `write-guard.ts` (`peerClaimFor`/`writeWarningReason`/`WriteWarnings` — the advisory,
|
|
191
|
+
warn-once-then-allow guard on a `write`/`edit` overlapping a peer's open claim, built on
|
|
192
|
+
`core/ownership.ts`'s path-overlap primitives), `wait.ts`/`gate.ts` (non-blocking join + inbound
|
|
193
|
+
constrained-turn allowlist), `install.ts` (the feature's composition adapter: session-scoped plane +
|
|
194
|
+
ledger + wait/tool/hook wiring).
|
|
195
|
+
- **`src/telemetry/`** — a generic, versioned observer/export contract for future plugins: projected
|
|
196
|
+
lifecycle metadata only, never an agent-message router or control surface.
|
|
197
|
+
- **`src/persona/`** — identity: `persona.ts` (parse + `expandCouncilPreset` + `composeSystemPrompt`),
|
|
198
|
+
`controller.ts`, `gating.ts`, `orchestrate.ts`, `config-store.ts`, `state.ts` (last-selected persona),
|
|
199
|
+
`spine.ts` (the shared behavioral layer's SOURCE resolution — docs/SPINE.md; composition sits in
|
|
200
|
+
`persona.ts` for supervisor turns and in the engines for delegated legs).
|
|
201
|
+
- **`src/tools/`** — `delegate.ts`, `intercom.ts`, `exocom.ts` (the `exocom_list`/`exocom_send`
|
|
202
|
+
tools), `exocom-work.ts` (claim/ask/answer/decline/wait/release/progress). **`src/ui/`** — agent tree/overlay, model picker, `presentation.ts` (the shared
|
|
203
|
+
card-compaction/sanitization helpers behind the projection rules below), `usage.ts`
|
|
204
|
+
(token/usage formatting, `ChildUsageLedger` / `toToolUsage` — sub-agent spend is
|
|
205
|
+
attached as Pi `toolResult.usage` so the footer and pi-theme-1337's frame count it;
|
|
206
|
+
leftover background-leg cost is published on the `persona-cost` status key).
|
|
207
|
+
- **`src/loader.ts`** — the discovery loader (`loadDefinitions`/`loadContracts`/`loadPresets`/
|
|
208
|
+
`loadTeams`), the concrete read-side of the discovery precedence table.
|
|
209
|
+
- **`src/bridge.ts`** — the child-mode-only wiring, loaded instead of the full extension when
|
|
210
|
+
`PI_PERSONA_BUS` is set (a broker child).
|
|
211
|
+
- **`src/extension.ts`** — the single `ExtensionFactory` (default export `piPersona`): still the
|
|
212
|
+
only file `package.json` `pi.extensions` names. Tools/commands stay registered here; Pi lifecycle,
|
|
213
|
+
prompt composition, the pending-ask tool gate, and engine construction were extracted to
|
|
214
|
+
`src/extension/hooks.ts`, `src/extension/engine.ts`, and leaf helpers in `src/extension/shared.ts`.
|
|
215
|
+
Those modules must not import the factory. Broker host state is declared before `installHooks` so
|
|
216
|
+
shutdown cannot TDZ on `brokerHostPromise`.
|
|
217
|
+
|
|
218
|
+
## The two engines
|
|
219
|
+
|
|
220
|
+
Both backends sit behind the `StrategyEngine` seam (`run(spec, onProgress?, signal?, onSteerable?) →
|
|
221
|
+
AgentResult`) and enforce three independent deadlines, all classified as `failureKind: "timeout"`
|
|
222
|
+
(never a provider reroute): `RUN_LIMITS.timeoutMs`, an **idle window** (no events for that long ⇒
|
|
223
|
+
abort); `PI_PERSONA_AGENT_MAX_MS`, an **opt-in hard wall-clock cap** — a lifetime ceiling, armed once
|
|
224
|
+
and never reset, that settles a busy-but-non-converging child (a loop that keeps emitting) the idle
|
|
225
|
+
window never catches (OFF by default = unlimited, so a healthy child has no lifetime ceiling); and
|
|
226
|
+
`PI_PERSONA_AGENT_STARTUP_MS`, a **startup deadline** (default 300000, `0` disables) that kills a
|
|
227
|
+
child making ZERO progress — no completed turn, no tokens, no streamed output. It fast-fails the
|
|
228
|
+
"never started" case the generous idle window is too slow for — notably a headless `mcp: true` leg
|
|
229
|
+
whose `pi-mcp-adapter` hangs on interactive OAuth; the first real progress cancels it.
|
|
230
|
+
|
|
231
|
+
The idle window and startup deadline stay ARMED for every child, coaching or not — a blind exemption
|
|
232
|
+
that once disabled both while a child could legitimately block on a supervisor reply let a genuinely
|
|
233
|
+
stalled coaching leg run forever. Instead, when either fires, the source of truth is asked directly:
|
|
234
|
+
does this child have a live `decision`/`interview` ask outstanding right now? In-process consults the
|
|
235
|
+
bus (`hasPendingAskFrom`); the child-process engine consults the broker through the same shape
|
|
236
|
+
(`isBlocked`). A live pending ask re-arms the window; no pending ask means the silence is real and the
|
|
237
|
+
leg is killed. The hard wall-clock cap is the one ceiling that still applies unconditionally —
|
|
238
|
+
including to a coaching child blocked on a reply — so a supervisor that never answers still has a
|
|
239
|
+
backstop.
|
|
240
|
+
|
|
241
|
+
In-process deadlines and caller cancellation are armed **before session construction**; the bus
|
|
242
|
+
consult that re-arms idle/startup only matters after construction, since no ask can be pending before
|
|
243
|
+
a session exists. Cancellation settles the run and releases its bus handle; a session returned late is
|
|
244
|
+
disposed and a late factory rejection is consumed. Pi's resource loader does not expose forcible
|
|
245
|
+
cancellation — the recursive-extension guard stays held until that factory settles, so a loader that
|
|
246
|
+
never returns needs a Pi restart (and `PI_PERSONA_ENGINE=child` for process-level termination). Run
|
|
247
|
+
settlement does not promise that arbitrary in-process code has physically stopped.
|
|
248
|
+
|
|
249
|
+
- **InProcessEngine** (default) — a `createAgentSession` per sub-agent: cheaper, shares the host's
|
|
250
|
+
auth/model registry, and **steerable** (inject a live user message into a running sub-agent).
|
|
251
|
+
- **ChildProcessEngine** (`PI_PERSONA_ENGINE=child`, the correctness baseline) — spawns `pi --mode
|
|
252
|
+
json -p`, delivering the task over **stdin** (never argv — a flow-phase task would blow Windows'
|
|
253
|
+
~32 KiB command-line cap). Worktree isolation always uses this engine.
|
|
254
|
+
|
|
255
|
+
Tool grants have three distinct states on both engines: absent inherits Pi's session defaults, a
|
|
256
|
+
non-empty array is an allowlist, and an explicit empty array means no tools (`--no-tools` on the
|
|
257
|
+
child backend); denylists apply independently via `excludeTools`/`--exclude-tools`.
|
|
258
|
+
|
|
259
|
+
A spec naming an agent or output contract that isn't installed fails before anything spawns, on both
|
|
260
|
+
backends alike (`engine/spec-preflight.ts`): the message names what IS installed, capped at 12 names.
|
|
261
|
+
The `delegate` tool runs its own earlier check with a different cap — `unknownAgentError`/
|
|
262
|
+
`unknownContractError` (`src/tools/delegate.ts`) reject a typo before either engine is reached, listing
|
|
263
|
+
up to 16 names. Same idea, two layers, two caps — see docs/REFERENCE.md's example.
|
|
264
|
+
|
|
265
|
+
`isolation: worktree` is fail-closed. It requires a clean Git checkout so the detached `HEAD` view
|
|
266
|
+
cannot silently omit staged/unstaged/untracked supervisor work; a non-repository, dirty checkout,
|
|
267
|
+
worktree creation failure, missing diff artifact, or an artifact over the return limit fails the leg —
|
|
268
|
+
the base engine is never invoked against the real tree as a fallback. The diff returns to the
|
|
269
|
+
supervisor before the temporary tree is removed, and every git call is asynchronous so the extension
|
|
270
|
+
host keeps rendering, ticking timers, and serving broker sockets while a large checkout runs.
|
|
271
|
+
|
|
272
|
+
Transient retries inside one agent session belong to the host **Pi runtime**'s `retry.*` settings, not
|
|
273
|
+
a persona contract — pi-persona neither parses retry notation nor schedules its own backoff. Both
|
|
274
|
+
backends read the same resolved global agent directory (`PI_CODING_AGENT_DIR` for spawned children).
|
|
275
|
+
|
|
276
|
+
`buildEngine` wraps the chosen backend with **provider fallback** (`engine/fallback.ts`): a run whose
|
|
277
|
+
model's provider fails at call time (auth/outage/5xx/model-not-supported) can retry the same model id
|
|
278
|
+
only when the selection was unpinned/default, through the data-driven provider policy for that model
|
|
279
|
+
family. A provider-qualified `spec.model` is an explicit pin and stays strict by default. Only
|
|
280
|
+
`failureKind === "provider"` reroutes; abort/timeout/contract/unknown/agent are terminal — this
|
|
281
|
+
prevents an explicitly selected OpenAI or native Claude leg from silently moving to an unrelated paid
|
|
282
|
+
provider. (`"verification"` is a further terminal value that never appears on an `AgentResult` — see
|
|
283
|
+
`map`'s `verify` param in [`docs/STRATEGIES.md`](./STRATEGIES.md).)
|
|
284
|
+
|
|
285
|
+
### MCP (and other `session_start`-scoped extensions) in sub-agents
|
|
286
|
+
|
|
287
|
+
**A sub-agent does NOT share the supervisor's MCP session, and an in-process sub-agent gets NO MCP at
|
|
288
|
+
all.** MCP servers are provided by a separate extension (`pi-mcp-adapter`), which opens its
|
|
289
|
+
connections inside a `session_start` hook:
|
|
290
|
+
|
|
291
|
+
- The **in-process engine** builds a fresh `createAgentSession` and only calls `session.prompt()` — it
|
|
292
|
+
never fires `session_start` (that requires `AgentSession.bindExtensions`). `pi-mcp-adapter` registers
|
|
293
|
+
its `mcp*`/direct tools at load (they *appear*) but never initializes: calls come back **"MCP not
|
|
294
|
+
initialized"** — the tools are present but dead.
|
|
295
|
+
- The **child engine** spawns a real `pi -p`, whose normal startup DOES fire `session_start`, so
|
|
296
|
+
`pi-mcp-adapter` initializes as that child's **own** connection (its own stdio/HTTP clients) — a
|
|
297
|
+
*separate* MCP session that does not see the supervisor's MCP workspace/interactive-shell state.
|
|
298
|
+
|
|
299
|
+
Firing `session_start` for every in-process sub-agent is deliberately NOT done: it would spin up the
|
|
300
|
+
full MCP fleet N times per fan-out, and the adapter's OAuth/UI/consent machinery assumes an interactive
|
|
301
|
+
session. There is no cheap way to *share* one live MCP connection across sessions here.
|
|
302
|
+
|
|
303
|
+
**The `mcp: true` opt-in — a delegable MCP leg.** Because the child engine fires `session_start`, a
|
|
304
|
+
sub-agent that needs live MCP tools is routed there: mark the agent `mcp: true` in its frontmatter, or
|
|
305
|
+
pass it on a `delegate` task (`AgentRunSpec.mcp`). The engine runs that leg through `childEngineAt(root)`
|
|
306
|
+
— the same mechanism `isolation: worktree` uses for MCP, minus the git worktree — connecting to the
|
|
307
|
+
SAME servers from `~/.pi/agent/mcp.json`. Cost is one `pi` spawn per leg; for an **HTTP** server it is
|
|
308
|
+
just a client reconnect, not an N× stdio fleet spawn.
|
|
309
|
+
|
|
310
|
+
**Shared state via a server-keyed backend.** The child gets its own MCP session, not the supervisor's
|
|
311
|
+
handle — but a server that keys state (workspaces, shells, artifacts) by a **session id passed as a
|
|
312
|
+
tool argument** lets a child that reconnects to the same HTTP endpoint AND is handed that session id
|
|
313
|
+
operate on the SAME server-side state. Put the session id in the task packet. (A stdio server whose
|
|
314
|
+
state lives in-process is genuinely separate — `mcp: true` there gives the leg its own clean session.)
|
|
315
|
+
|
|
316
|
+
**Guidance.** Default: treat MCP as a **supervisor capability** — do MCP-dependent work up top and
|
|
317
|
+
hand sub-agents the resulting **artifacts** to reason over. When a leg must DRIVE MCP itself, delegate
|
|
318
|
+
with `mcp: true` and pass the session id. Do not over-restrict such an agent's `tools` allowlist, or
|
|
319
|
+
the `mcp*` tools get filtered out.
|
|
320
|
+
|
|
321
|
+
## The spine — one shared behavioral layer
|
|
322
|
+
|
|
323
|
+
An optional layer of behavioral baseline (scope discipline, verification, how to treat another
|
|
324
|
+
agent's text) that a persona would otherwise restate in its own words. [SPINE.md](SPINE.md) covers
|
|
325
|
+
the content and the A/B that gates its default; the architectural shape is:
|
|
326
|
+
|
|
327
|
+
- **Where it sits.** Between Pi's base prompt and the persona body — the stable, cacheable *prefix*,
|
|
328
|
+
the opposite end of the prompt from the per-turn delegation brief. `composeSystemPrompt(base,
|
|
329
|
+
persona, spine)` (`persona/persona.ts`) is the one supervisor-side composition: a persona-less turn
|
|
330
|
+
still gets base + layer, and a `replace` persona (which drops Pi's base) keeps the layer as its only
|
|
331
|
+
scaffolding.
|
|
332
|
+
- **Two texts, two selectors.** The supervisor reads `spine.md`, a delegated leg reads
|
|
333
|
+
`spine.worker.md` — a leg runs headless, so "confirm irreversible actions with the user" would make
|
|
334
|
+
it stall or hand the question back as its deliverable. `PI_PERSONA_SPINE` / `PI_PERSONA_SPINE_LEGS`
|
|
335
|
+
(the legs' selector follows the supervisor's unless set) each take off / on / an explicit path; `on`
|
|
336
|
+
resolves the user dir before the bundled `prompts/`, a path never falls back to another file.
|
|
337
|
+
- **Source resolution is pure, composition is per-role.** `persona/spine.ts` answers only *which file,
|
|
338
|
+
if any* — over an injected reader, so the whole precedence table is unit-testable without a disk.
|
|
339
|
+
The legs' text is prepended by each engine (`engine/inproc.ts`, and `--append-system-prompt` on the
|
|
340
|
+
child adapter), so a leg keeps Pi's full base prompt either way.
|
|
341
|
+
- **Off by default, opt-out at both ends.** Unset ⇒ every composition above is byte-identical to a
|
|
342
|
+
pre-spine pi-persona. Frontmatter `spine: false` opts a persona or an agent out, and a persona's
|
|
343
|
+
opt-out carries to the legs it spawns. A file that is unreadable, empty, or over `MAX_SPINE_BYTES`
|
|
344
|
+
(64 KiB, refused on its `stat` so the bytes never enter the process) degrades to a warning and no
|
|
345
|
+
layer — a prompt file is never a hard failure.
|
|
346
|
+
|
|
347
|
+
## Runtime metadata is not a new user request
|
|
348
|
+
|
|
349
|
+
The supervisor system prompt carries one fixed runtime-context rule, after persona/spine composition
|
|
350
|
+
and before the time anchor and live rosters. It applies with no persona, append/replace personas and
|
|
351
|
+
`spine: false`: it is protocol guidance, not an activation of the optional behavioral spine. Identity,
|
|
352
|
+
skill-prefetch, clock and routine status metadata do not require repeated acknowledgements. Initial
|
|
353
|
+
setup and actionable questions still require attention; explicit user questions about metadata are
|
|
354
|
+
answered normally. Skill availability alone is not evidence that instructions were loaded or used.
|
|
355
|
+
|
|
356
|
+
The rule contains no interpolated handle or changing counters, creates no conversation message and
|
|
357
|
+
does not redact assistant history. The pre-name identity bootstrap remains actionable and disappears
|
|
358
|
+
once naming succeeds. An old session may contain repetitive prose; updating the extension does not
|
|
359
|
+
rewrite it. Reload extension code with Pi's `/reload` (when delegated work has settled), not merely
|
|
360
|
+
`/persona reload`, which only reloads persona data.
|
|
361
|
+
|
|
362
|
+
## Time awareness — one rule, three granularities
|
|
363
|
+
|
|
364
|
+
Pi's base prompt carries no date and no time, so from inside a turn five minutes of work and five
|
|
365
|
+
hours look identical. `core/time.ts` renders the three durations that fix that, and the rule that
|
|
366
|
+
decides how precise each may be is a **prompt-cache** rule, not a taste one:
|
|
367
|
+
|
|
368
|
+
- **Tail surfaces are free.** A tool result or a delivered message is written once and never re-sent
|
|
369
|
+
as the cached prefix, so precision costs nothing there. An inbound peer delivery dates itself
|
|
370
|
+
(`[rune] — reply · sent 20m ago`), and every settled leg reports its wall time on every
|
|
371
|
+
tracker-backed path — the done/failed/stopped report, the join, `intercom result`, and each
|
|
372
|
+
settled line of a peek digest.
|
|
373
|
+
- **The system prompt is not.** It is re-composed and re-sent every turn and IS the cached prefix,
|
|
374
|
+
so a minute-granular value there rewrites it every minute and throws away the provider's cache of
|
|
375
|
+
everything before it. The **session anchor** therefore reads on a deliberately coarse ladder —
|
|
376
|
+
one bucket for the first quarter hour, then quarter hours, then whole hours, then days — which
|
|
377
|
+
steps 27 times in a full day where a minute-granular label would step 1440. `pi-persona-mind`
|
|
378
|
+
makes the same trade one scale coarser for the same reason.
|
|
379
|
+
|
|
380
|
+
The anchor answers *"how long have I been on this problem"* rather than *"what turn is this"*
|
|
381
|
+
because it reads pi's `SessionHeader.timestamp` — the first entry of the append-only session file.
|
|
382
|
+
That is what makes it survive **compaction** (the system prompt is re-sent, never summarized) and
|
|
383
|
+
**restart** (`/resume` re-reads the original start, not the resume time). Branching mints a new
|
|
384
|
+
session file, so the reading restarts there — correctly: the line claims the life of *this* session.
|
|
385
|
+
|
|
386
|
+
Both readings state an elapsed time as fact, so both refuse the inputs that would make that a lie.
|
|
387
|
+
A timestamp is accepted only when it names exactly one moment — `Date.parse` reads a zone-less
|
|
388
|
+
date-time as the *receiver's* local time, so two peers would print two different ages for the same
|
|
389
|
+
frame — and only when it is plausible: a peer age outside a 30-day window, or a session start in
|
|
390
|
+
the future or older than a year, is corruption rather than a long wait, and degrades to an honest
|
|
391
|
+
label or to no anchor at all. A peer's timestamp stays a peer's claim; bounding the window narrows
|
|
392
|
+
what it can assert, it does not authenticate it.
|
|
393
|
+
|
|
394
|
+
## The three communication planes
|
|
395
|
+
|
|
396
|
+
Three planes with **disjoint vocabularies** — a concept name lives in exactly one, so "progress" is
|
|
397
|
+
never simultaneously a telemetry event, a routed message, and a UI source of truth.
|
|
398
|
+
|
|
399
|
+
- **EngineEvent** — runtime lifecycle/telemetry (`started · token · tool_call · completed · failed ·
|
|
400
|
+
usage`). Feeds derived UI; never routed as an agent message.
|
|
401
|
+
- **Bus Msg** — semantic agent↔agent communication (`progress · decision · interview`, and sibling
|
|
402
|
+
peer sends). `src/bus`. `progress_update` is a Bus message only, not an EngineEvent.
|
|
403
|
+
- **ProgressView** — a derived, read-only UI view computed *from* EngineEvents. Never authored, never
|
|
404
|
+
a source of truth. The `f9` agent tree and `peek` digest are ProgressViews.
|
|
405
|
+
|
|
406
|
+
Steering is always a Bus action; the peek digest is always a read-only ProgressView.
|
|
407
|
+
|
|
408
|
+
External telemetry is not a fourth communication plane: it is a generic observer/export contract that
|
|
409
|
+
future plugins may consume or produce, not an exclusive pi-persona protocol. It observes projected
|
|
410
|
+
events and cannot route, reply, steer, or otherwise control agents.
|
|
411
|
+
|
|
412
|
+
## The comm plane in practice
|
|
413
|
+
|
|
414
|
+
- **In-process bus** (`bus/inproc.ts`) — a handle-based mailbox: `send` (one-way), `ask` (blocks for a
|
|
415
|
+
reply), `reply`, `onMessage`. `contact_supervisor` (child→supervisor, gated by `coaching: on`) and
|
|
416
|
+
`contact_peer` (sibling→sibling) are the child-side tools bound onto it. Each unread inbox holds at
|
|
417
|
+
most 200 messages; overflow evicts an old one-way note first, and an inbox full of live asks rejects
|
|
418
|
+
new delivery rather than stranding a sender. Ask settlement (reply, timeout, abort, departure, or
|
|
419
|
+
failed delivery) releases its timer/listener and unread entry; a previously retained question stays
|
|
420
|
+
readable with `expectsReply: false`, so old questions never advertise an active reply id.
|
|
421
|
+
- **Sibling peer comm** — a strategy opts a run in via `AgentRunSpec.peers` (gated by `canUseBus`). The
|
|
422
|
+
child gets `contact_peer` (`list`/`send`, ONE-WAY so peers can never deadlock; per-engine-instance
|
|
423
|
+
scoping; a 20-send budget and an 8,000-character body limit, enforced before consuming a send). The
|
|
424
|
+
engine's **delivery bridge** steers incoming bus messages into the child session, fenced with the
|
|
425
|
+
sender attributed OUTSIDE the fence (`attributeInbound`, shared by both engines so the anti-spoofing
|
|
426
|
+
format can't drift) — the same bridge delivers the supervisor's `intercom send`. `debate`/`pair`
|
|
427
|
+
always use peers; `map`/`synthesize` opt in via `params.peers`; `magi`/`judge`/`fanout`/`compete`/
|
|
428
|
+
`council-rounds` stay peer-less by design (independence is a bias guard — see
|
|
429
|
+
[STRATEGIES.md](STRATEGIES.md#bias-guard-invariants-do-not-fix-these)).
|
|
430
|
+
- **Cross-process broker** (on by default; `PI_PERSONA_BROKER=off` restores pre-broker spawn;
|
|
431
|
+
`bus/broker/`) — gives child-process runs, every `isolation: worktree` leg, and every `mcp: true` leg
|
|
432
|
+
the same comm plane and **steer** the in-process ones have. It is a session-scoped (POSIX socket /
|
|
433
|
+
Windows named pipe under the session id), supervisor-hosted relay into the local `InProcessBus`: a
|
|
434
|
+
connected child is indistinguishable from an in-process one, so the supervisor side (intercom, idle
|
|
435
|
+
notifier, f9, peek) is unchanged by construction. Off ⇒ the host never starts and the child spawns
|
|
436
|
+
byte-identical to pre-broker pi-persona. Ask failures echo the originating `msgId` so the client
|
|
437
|
+
rejects the right request immediately, and a client cancellation propagates only to that connection's
|
|
438
|
+
own host ask. `extension/broker-host.ts`'s `SupervisorBroker` owns the lifecycle: it starts the host
|
|
439
|
+
lazily on the first child-engine build, warns once on a failed bind (children built while it's down
|
|
440
|
+
spawn without a bus endpoint, so they never burn connect backoff against a dead socket), and the next
|
|
441
|
+
build retries; `/doctor` surfaces its state on one line. A dropped client connection flips the
|
|
442
|
+
child-side bridge to `⇄ offline` and fails a fresh ask fast rather than hanging.
|
|
443
|
+
|
|
444
|
+
### Presentation is a projection, not another comm plane
|
|
445
|
+
|
|
446
|
+
The semantic payload and its human presentation have different lifetimes. A result remains complete
|
|
447
|
+
for the supervisor and for explicit retrieval; the default TUI projection is deliberately small:
|
|
448
|
+
|
|
449
|
+
- async completions are coalesced and bounded fairly across legs; `intercom { action: "result", to:
|
|
450
|
+
"<run-id>" }` retrieves one retained result in full and consumes a still-pending duplicate
|
|
451
|
+
notification;
|
|
452
|
+
- drained bus messages have a separate session-local retention window: at most 256 messages and
|
|
453
|
+
256,000 body characters across the bus, FIFO eviction. `intercom { action: "message", messageId:
|
|
454
|
+
"<message-id>" }` retrieves a retained message; it never interprets a message id as a run id.
|
|
455
|
+
Oversized or evicted messages return an unavailable diagnostic — retention is not durable evidence
|
|
456
|
+
storage;
|
|
457
|
+
- collapsed delegate/intercom/council/flow cards show state, identity, a short sanitized preview, and
|
|
458
|
+
Pi's expand-key hint, each surfacing failure at the top in its own shape: the delegate card **sorts**
|
|
459
|
+
failed legs first, a failed council/flow card leads with the cause in its title, and an
|
|
460
|
+
`intercom wait` card leads with the `N settled — X done, Y failed` tally (per-leg causes sit in the
|
|
461
|
+
body, below the preview cut, so `wait`'s own success doesn't get a `failed` prefix);
|
|
462
|
+
- follow-up cards (`pi-persona`, exocom) retain their complete semantic content but render a bounded
|
|
463
|
+
preview until expanded; terminal escape/control sequences are removed from the visible projection;
|
|
464
|
+
- the sticky agent and exocom widgets have fixed row budgets — F9/`/agents` and paginated
|
|
465
|
+
`exocom_list({ offset, limit })` are the explicit detail surfaces, so a wide fan-out cannot push the
|
|
466
|
+
editor off screen or dump an entire peer registry into model context. Agent-tree deltas share one
|
|
467
|
+
16 ms widget composition per terminal frame; identical lines/status are not republished, hierarchy
|
|
468
|
+
rendering uses one adjacency index, and the F9 overlay rebuilds lazily when Pi paints rather than
|
|
469
|
+
once per streamed token;
|
|
470
|
+
- `/flow` and `/orchestrate` append durable, TUI-only expandable result entries rather than dumping a
|
|
471
|
+
large notification or a second copy into the model context;
|
|
472
|
+
- the F9 overlay's *directed* keys (`x` stop, `s` steer) act only on the agent the ▸ marker still
|
|
473
|
+
shows; if that agent settles and is pruned mid-keystroke, the selection re-anchors visibly and the
|
|
474
|
+
keypress is spent re-aiming instead of aborting an agent the user never chose. ↑↓, ⏎, and the scroll
|
|
475
|
+
keys never refuse;
|
|
476
|
+
- every agent-tree node carries `startedAt`/`lastAdvanceAt`; `LiveClock` (`src/ui/live-clock.ts`)
|
|
477
|
+
repaints the sticky widget and the F9 overlay once a second while any node is running and stops
|
|
478
|
+
itself once nothing is. A running row shows elapsed time until it goes `STALL_FLAG_MS` (90s) quiet,
|
|
479
|
+
then the same `⚠ stalled <duration>` badge the async peek watchdog uses. `x` marks the target
|
|
480
|
+
`stopping…` at once and is offered only when a live stop handle exists; a refusal (no handle,
|
|
481
|
+
already settled) surfaces as a one-line notice instead of silently no-op'ing.
|
|
482
|
+
|
|
483
|
+
This is a UI invariant only: truncating a collapsed card must never be confused with truncating the
|
|
484
|
+
underlying result or changing a strategy's contract.
|
|
485
|
+
|
|
486
|
+
## exocom — the external plane
|
|
487
|
+
|
|
488
|
+
`src/exocom/` is a separate plane from everything above: every plane in "the comm plane in practice"
|
|
489
|
+
is **internal** to one supervisor's own run — hierarchical, keyed by that session id, talking to
|
|
490
|
+
children *it* spawned. **exocom is flat and external**: independent, top-level Pi instances sharing
|
|
491
|
+
one explicit scope — normally the same workspace, or a Pi that joined by code — discover each other
|
|
492
|
+
and message peer-to-peer, with no parent/child relationship. (intercom = internal comm; exocom =
|
|
493
|
+
external comm.)
|
|
494
|
+
|
|
495
|
+
Exocom supplies fenced one-way postcards and presence; chat `message` never mutates shared work
|
|
496
|
+
state. Separately, a workspace work ledger (`ledger.ts`, tools in `tools/exocom-work.ts`) records
|
|
497
|
+
claims/asks/answers/progress/releases. `exocom_ask` canonicalizes its target through
|
|
498
|
+
`plane.resolvePeer` (raw session ids on the ledger, never the display name); registry display name is
|
|
499
|
+
keyed by `from_session`, never the envelope's self-reported `from_name`. A pending ask constrains the
|
|
500
|
+
turn (`gate.ts`: answer/decline plus read-only tools) behind a bounded `pendingAskPrompt`.
|
|
501
|
+
`exocom_wait` is non-blocking. Clean shutdown releases this session's claims/asks; if that best-effort
|
|
502
|
+
write fails, the vanished owner is pruned on the next ledger transaction (live registry sessions are
|
|
503
|
+
the lease), and `expireWaiters` wakes any waiter left behind instead of leaving `exocom_wait` armed
|
|
504
|
+
forever. This gate is cooperative coordination for participating local Pi processes — not filesystem
|
|
505
|
+
authorization, not isolation from another same-user process, and not a delegate/council or task/run
|
|
506
|
+
replacement.
|
|
507
|
+
|
|
508
|
+
- **Opt-in, OFF by default.** `PI_PERSONA_EXOCOM=1` or bare `--exocom` joins the current workspace;
|
|
509
|
+
`--exocom=Ab0T` joins an existing scope by its exact, case-sensitive four-character Base62 alias
|
|
510
|
+
from another cwd (Pi's flag API can't express an optional-value boolean, so the extension recovers
|
|
511
|
+
the equals form from raw argv). Unknown/malformed/conflicting codes fail closed rather than falling
|
|
512
|
+
back to the caller's workspace; selection is frozen for the session. Participation also requires
|
|
513
|
+
the active persona's `canUseBus` plus at least one obligation closer
|
|
514
|
+
(`exocom_answer`/`exocom_decline`), re-evaluated on every persona switch — a persona that loses
|
|
515
|
+
either leaves the registry rather than advertising a peer that could be permanently wedged by an
|
|
516
|
+
inbound ask, and rejoins once an admissible persona is active. OFF ⇒ no bind, no registry entry, no
|
|
517
|
+
tools registered.
|
|
518
|
+
- **Discovery — a scope-selected file registry, not an elected hub.** A persistent alias map under
|
|
519
|
+
the agent directory resolves each four-character code to its full 24-hex workspace identity
|
|
520
|
+
(allocation is atomic, collision-aware, bounded); the code is a routing convenience, never the
|
|
521
|
+
storage identity. Each instance binds its own socket (POSIX) / named pipe (Windows) and
|
|
522
|
+
self-registers one JSON entry under `<agentDir>/persona/exocom/<workspace-hash>/agents/<session-key>.json`
|
|
523
|
+
(a hash of the session id, so a read drops any entry whose filename doesn't match its own
|
|
524
|
+
`session_id`), heartbeating it; discovery is just reading that directory. Dead-pid and
|
|
525
|
+
stale-heartbeat entries are pruned on read — no host election, no failover. Cleanup is
|
|
526
|
+
ownership-aware (`session_id` + endpoint + signing key) and atomically claims an entry before
|
|
527
|
+
deletion, so a failed/replaced session cannot erase a live replacement's slot.
|
|
528
|
+
- **Workspace identity is the Pi's actual cwd.** The chosen scope controls registry, endpoint, ledger,
|
|
529
|
+
and artifact paths but never overwrites the member's home workspace identity. Peer lists, the
|
|
530
|
+
standing brief, and `/exocom` label a peer as same-workspace or external without exposing its
|
|
531
|
+
absolute `cwd`. External peers are full Pi instances that can inspect files in their own workspace;
|
|
532
|
+
paths are never implicitly shared.
|
|
533
|
+
- **Postcards plus a durable, non-blocking join.** `exocom_send` returns a `msg_id` immediately; a
|
|
534
|
+
reply is another `exocom_send` with `in_reply_to` set. `target: "*"` broadcasts postcards
|
|
535
|
+
best-effort (one unreachable peer doesn't fail the rest). Work coordination is separate:
|
|
536
|
+
`exocom_ask` commits an obligation to the ledger, `answer`/`decline` settles it, and `exocom_wait`
|
|
537
|
+
arms a bounded idle wake rather than blocking. The signed frame is a wake signal; the ledger remains
|
|
538
|
+
the source of truth if delivery is deferred.
|
|
539
|
+
- **Cross-workspace writes are advisory-only in v1.** A repository-relative claim path has meaning
|
|
540
|
+
only in the workspace that owns the selected scope, so a member whose home workspace differs from
|
|
541
|
+
the scope cannot `exocom_claim` (the tool and the signed-frame receiver both reject a foreign
|
|
542
|
+
claim) — it can still send, inspect its own files, ask, answer/decline, wait, and release its own
|
|
543
|
+
asks. Multi-repository write coordination would need a versioned resource namespace; paths from
|
|
544
|
+
unrelated repositories are never compared as if they named the same files.
|
|
545
|
+
- **Reply routing is session-stable.** `exocom_list` shows human display names (`name`/`name#2`);
|
|
546
|
+
sends and inbound reply hints use `name@<96-bit session hash>`, so a retained qualified target
|
|
547
|
+
stays valid after a rename. Ambiguous suffix matches fail closed rather than guess. The
|
|
548
|
+
authenticated registry entry (endpoint + signing key) is cached with the bounded inbound context,
|
|
549
|
+
so a stale/pruned sender can't be retargeted to a same-name twin. The reply hint is CONDITIONAL —
|
|
550
|
+
reply only if it changes what someone does — so a settled point can keep running on agreement and
|
|
551
|
+
thanks instead of a bare `Reply:` making answering the default.
|
|
552
|
+
- **Identity is session-stable, persona is presence metadata.** `extension/identity.ts` owns one
|
|
553
|
+
handle independent of Exocom. `agent_name` names a standalone session; `exocom_name` does the same
|
|
554
|
+
while Exocom is active and refreshes presence immediately. A chosen name persists in a session-bound
|
|
555
|
+
entry, survives resume/reload/persona changes, and is not inherited by a fork with a new session id;
|
|
556
|
+
generic persona/agent names are rejected. Before selection, the `context` hook supplies the
|
|
557
|
+
actionable naming bootstrap to ordinary turns and custom Exocom wakes; after selection it injects
|
|
558
|
+
no identity pseudo-turn. `exocom_name` only replaces the display label — the
|
|
559
|
+
registry key stays the session, so a rename can't take over another peer's slot or its inbound
|
|
560
|
+
replies, and changing persona never changes the key or grants authority over another peer. Delegated
|
|
561
|
+
workers keep the leader's `delegate.name` from launch, including peer/broker display labels.
|
|
562
|
+
- **Fenced and attributed from the REGISTRY, never the envelope — the security core.** An inbound
|
|
563
|
+
message is head-truncated and delivered under a header the RECEIVER writes, above a body quoted by
|
|
564
|
+
`fencePeer` (the peer flavor of `core/fence.ts`'s `fenceUntrusted`/`attributeInbound`). Attribution
|
|
565
|
+
sits OUTSIDE the fence so a payload can't spoof its sender by closing the block, and the label comes
|
|
566
|
+
from the registry entry keyed by the connecting session, never the envelope's self-reported
|
|
567
|
+
`from_name`. A message over the inline budget spills to a scope-selected artifact file (a small
|
|
568
|
+
preview stays inline); the spill descriptor (`preview`, `path`, `size`) is verified at the
|
|
569
|
+
RECEIVER's transport boundary before anything reaches its model — the path must be the selected
|
|
570
|
+
scope's own `artifacts/<msg_id>.txt`, a regular unlinked file whose size matches the declared one
|
|
571
|
+
and sits between the inline cap and `ARTIFACT_MAX_BYTES`. The receiver then reads through one held,
|
|
572
|
+
identity-checked descriptor, rechecks the source didn't change, and writes an unpredictable
|
|
573
|
+
receiver-owned snapshot with exclusive creation — only that snapshot path reaches the model.
|
|
574
|
+
Anything else is NACKed rather than exposed. Received snapshots join sender spills under the same
|
|
575
|
+
TTL/file-cap cleanup, and the per-sender byte window charges only what crossed the wire (not a
|
|
576
|
+
spill's declared size), so a legitimate large spill is delivered rather than refused as "budget".
|
|
577
|
+
Transport guardrails enforced at the boundary: a hop cap (bounds only a THREADED reply chain,
|
|
578
|
+
derived from `in_reply_to`; untreaded alternating sends aren't depth-bounded by the transport), a
|
|
579
|
+
per-sender rate+byte budget, and a (sender, msg_id) dedup set so an at-least-once resend can't
|
|
580
|
+
double-trigger a turn. An exchange's LENGTH is instead bounded prompt-side: the per-turn peer brief
|
|
581
|
+
(`core/brief.ts`) carries a relevance bound — send only what changes what someone does — deliberately
|
|
582
|
+
not a round count, since a counter can't see whether a round still serves the work.
|
|
583
|
+
- **Tools are lazy and fail closed.** `exocom_list({ offset?, limit? })` exposes bounded, paginated
|
|
584
|
+
presence; `exocom_send({ target, message, in_reply_to? })` sends one-way (a session-qualified-looking
|
|
585
|
+
target resolves only as one, never falling back to a display name, since names are self-chosen);
|
|
586
|
+
`exocom_name({ name })` rebrands the display call-sign without moving any state. The ledger tools —
|
|
587
|
+
`exocom_claim`, `exocom_ask`, `exocom_answer`, `exocom_decline`, `exocom_wait`, `exocom_progress`,
|
|
588
|
+
`exocom_release` — are capability-gated with the postcard tools; the live accessor, capability gate,
|
|
589
|
+
and active-tool set all deny them once the plane stops, since Pi has no dynamic tool-unregister API.
|
|
590
|
+
Admission requires at least one obligation closer, because every published peer may receive a
|
|
591
|
+
durable ask and must retain a way to settle it.
|
|
592
|
+
- **Inbound delivery is bounded without loss.** Each message is injected under the same byte cap
|
|
593
|
+
whether plain text or an artifact descriptor. Bursts stay FIFO-queued; each rate-limited wake drains
|
|
594
|
+
as many whole messages as fit, leaving the rest queued. A presentation cap on the collapsed card
|
|
595
|
+
never discards the rest of the queue.
|
|
596
|
+
|
|
597
|
+
exocom never touches the delegate/council/broker path. One instance can be **both** a supervisor
|
|
598
|
+
(delegating its own children) **and** an exocom peer at once — the planes are independent and
|
|
599
|
+
independently gated. Whoever initiates a collaboration is merely the coordinator de facto: the plane
|
|
600
|
+
stays flat, and no peer gains stop/steer authority over another (that exists only on the hierarchical
|
|
601
|
+
intercom plane). The join code is not authentication: exocom is cooperative same-user, same-host
|
|
602
|
+
coordination between processes sharing one effective Pi agent directory, not a remote/network plane —
|
|
603
|
+
a local process able to modify that directory is already inside the trust boundary.
|
|
604
|
+
|
|
605
|
+
## Supervision & the waiting model
|
|
606
|
+
|
|
607
|
+
- **sync** — the supervisor actively blocks on the delegate/strategy call (results still stream); no
|
|
608
|
+
idle/peek/steer.
|
|
609
|
+
- **async** — the supervisor returns control and goes **idle, spending no tokens**, until woken by an
|
|
610
|
+
**event** (a child's `contact_supervisor`: a `decision`/`interview` blocks for a reply, `progress` is
|
|
611
|
+
one-way) or the **peek watchdog**, which fires while async children run but stays SILENT unless there
|
|
612
|
+
is something to act on — a healthy background run never interrupts. It surfaces on two independent
|
|
613
|
+
signals: a **fast** wakeup (`PI_PERSONA_PEEK_MS`, ~30s, `0` disables) when a child NEWLY crosses the
|
|
614
|
+
`STALL_FLAG_MS` (90s) stall window (a focused *possibly stuck* alert, framed patience-first — ask the
|
|
615
|
+
leg, don't probe its environment) or messages the supervisor; and a **slow routine check-in**
|
|
616
|
+
(`PI_PERSONA_CHECKIN_MS`, ~5 min, `0` disables) delivering the compact ProgressView digest — never
|
|
617
|
+
full transcripts — so the supervisor can catch a leg going off-track early. Both let an idle
|
|
618
|
+
supervisor steer/stop a wedged or drifting child even with NO completion fired; the enforcing
|
|
619
|
+
backstop is the engines' hard wall-clock cap (above). The full digest is also on demand via `/peek`.
|
|
620
|
+
Progress tokens are labelled **cumulative input/output**, not context occupancy; cache-read/write
|
|
621
|
+
accounting is separate. Do not infer a full context window from that counter. File age or line-count
|
|
622
|
+
changes alone likewise do not establish a stall, a passing test or completed work.
|
|
623
|
+
Async failures are ALWAYS reported; the runtime `DelegationLedger` vetoes a blind retry loop (an
|
|
624
|
+
identical agent+model+task delegation that failed twice is stopped before it spawns). Coaching is
|
|
625
|
+
gated by `coaching: on` AND `canUseBus`.
|
|
626
|
+
|
|
627
|
+
**Runtime reinforcement of the hand-off default** comes as a standing part and a reactive part, because
|
|
628
|
+
a persona directive lives at the TOP of the prompt and its pull decays as recent tool output balloons:
|
|
629
|
+
|
|
630
|
+
- The **delegation brief** (`core/brief.ts`) is the STANDING half: live roster (agents + teams + flows)
|
|
631
|
+
and the hand-off default, appended to the system-prompt TAIL every turn, where recency wins the
|
|
632
|
+
tug-of-war a top-of-prompt line loses. It regenerates from the live registry (so it can't desync) and
|
|
633
|
+
filters to the active persona's `delegate` allowlist. It never dictates how MANY sub-agents or which
|
|
634
|
+
shape — that is each persona's own method.
|
|
635
|
+
- The **nudges** (`core/nudge.ts`, on by default) are the REACTIVE half, landing in RECENT context on
|
|
636
|
+
the event that warrants them:
|
|
637
|
+
- **DelegationNudge** — a `tool_result` hook watches the supervisor's own tool stream and, when a
|
|
638
|
+
delegating persona grinds heavy work by hand (output burn since the last `delegate`/`council`
|
|
639
|
+
crosses a threshold), appends a compact labelled checkpoint (`Trigger` / `Scope` / `Action`) to
|
|
640
|
+
that command's result for the model and mirrors it into a durable, expandable
|
|
641
|
+
`pi-persona-nudge` custom entry for the operator. The entry is TUI-only (no duplicate model
|
|
642
|
+
context or turn) and guarantees visibility even when a built-in/custom tool renderer suppresses
|
|
643
|
+
result `content`. A successful hand-off resets the streak; a failed one keeps it and returns the
|
|
644
|
+
same visible re-dispatch checkpoint.
|
|
645
|
+
- **PersistenceNudge** — the counterweight to premature surrender: a delegated leg's report carrying
|
|
646
|
+
`[BLOCKED]`/`FLAG: UNKNOWN` gets a "don't bank it yet" reminder. All three delivery paths carry it
|
|
647
|
+
(sync `delegate`/`council`, background completion, `intercom wait`), but coverage isn't identical:
|
|
648
|
+
the sync path scans the WHOLE tool result including failed legs, while `renderCompletion` scans
|
|
649
|
+
only `status === "done"` runs by design — a failed background/`wait` leg already gets a failure
|
|
650
|
+
block, so a `[BLOCKED]` marker there doesn't also get the persistence note. Same marker, same leg,
|
|
651
|
+
different counterweight depending on how it was collected.
|
|
652
|
+
- **The off switch covers every path.** `PI_PERSONA_NUDGE=off` silences DelegationNudge entirely and
|
|
653
|
+
PersistenceNudge on every collection path (sync result, background notifier, `intercom wait`).
|
|
654
|
+
|
|
655
|
+
## Discovery & seeding
|
|
656
|
+
|
|
657
|
+
- **Precedence (all file kinds):** `builtin < user (~/.pi/agent/persona/…) < project (<root>/.pi/…)`; project
|
|
658
|
+
wins collisions. Personas and agents **share a folder**, split by the `persona: true` marker (a
|
|
659
|
+
persona and an agent must not share a name). The loader reports a same-name collision and omits
|
|
660
|
+
both ambiguous definitions from the effective registries, so a malformed install fails closed
|
|
661
|
+
instead of silently routing persona selection and delegation to different files.
|
|
662
|
+
- **Personas/agents load ONLY from the user and project dirs.** The bundled `personas/`+`agents/` are a
|
|
663
|
+
**seed source, not a live discovery layer** — a fresh install shows NO personas until installed.
|
|
664
|
+
`/persona seed` copies missing defaults, `/persona restore` force-restores originals (`core/seed.ts`);
|
|
665
|
+
first-run auto-install is opt-in (`PI_PERSONA_SEED=on`, guarded by a marker). When a release changes
|
|
666
|
+
a bundled default, activation may call the exact-size/hash migration helper: it upgrades only a known
|
|
667
|
+
pristine prior copy and preserves edited, linked, or unknown files. A migration can declare a bundled
|
|
668
|
+
agent dependency; that missing file is installed first with no-clobber atomic creation, or the parent
|
|
669
|
+
remains on its old bytes. Upgrades, dependency installs, and warnings are exposed in `/doctor`.
|
|
670
|
+
Contracts/presets/teams keep a builtin layer (they aren't personas).
|
|
671
|
+
|
|
672
|
+
## Cross-OS constructs
|
|
673
|
+
|
|
674
|
+
Never raw `child_process` — reuse Pi's helpers: `getPiInvocation()` (resolve the current `pi` binary),
|
|
675
|
+
`killProcessTree` (`taskkill /F /T` on Windows vs POSIX process-group kill), temp/path helpers. Always
|
|
676
|
+
attach an `error` listener to every spawned process and socket/server (an unhandled `error` event
|
|
677
|
+
crashes the host). Cross-OS is a regression gate: Windows AND POSIX child-spawn + process-tree-kill
|
|
678
|
+
must pass. The broker's transport is the only OS-specific code, confined to `bus/broker/`.
|
|
679
|
+
|
|
680
|
+
## Error-handling & lose-nothing invariants
|
|
681
|
+
|
|
682
|
+
- Per-run `AbortController`; recoverable failures return `AgentResult.error` + diagnostics, never
|
|
683
|
+
silently lost. A throwing ENGINE call becomes that leg's own `ok: false` result (`legFailure`,
|
|
684
|
+
flagged `infrastructure: true` so failure-punishing consumers can tell it from a model-side
|
|
685
|
+
failure), so one blown leg can't discard the fan-out's completed — already billed — siblings.
|
|
686
|
+
A rejection that reaches `sdk.parallel` is run-fatal by construction (a limit breach, a throwing
|
|
687
|
+
host callback): the batch stops pulling new items and rethrows the FIRST error immediately.
|
|
688
|
+
`runPersonaStrategy` returns `null` only for "nothing to run" (a persona naming no mode) — that
|
|
689
|
+
is not a failure.
|
|
690
|
+
- Model/thinking baseline is snapshot-once and restored on omit; tools are restored from the **full**
|
|
691
|
+
registry, never the active subset ("never strip Pi power").
|
|
692
|
+
- The completion/mutation guard keeps child-claimed success ≠ runtime-verified; the `maxChildren` cap
|
|
693
|
+
rejects an oversized fan-out **visibly** (an error before spawn), never by silently trimming it.
|
|
694
|
+
- Sub-agent output is UNTRUSTED — fenced (`fenceUntrusted`) before it reaches the supervisor as a
|
|
695
|
+
follow-up or tool result (prompt-injection defense).
|
|
696
|
+
- The broker host is `unref`'d (never keeps Pi alive) with a permanent error sink; never unlink a live
|
|
697
|
+
socket; teardown is idempotent.
|
|
698
|
+
|
|
699
|
+
## Reference: the core seams
|
|
700
|
+
|
|
701
|
+
Session event monitors follow the [time and event wake contract](MONITORS.md). A pure bounded
|
|
702
|
+
`MonitorManager` owns command-watch lifetimes; one process adapter reuses `killProcessTree`,
|
|
703
|
+
and a bounded session queue delivers fenced events through the existing idle follow-up path.
|
|
704
|
+
They are supervisor-owned resources, cancelled on shutdown or execution-permission revocation.
|
|
705
|
+
They do not replace engine events, the semantic bus, or Exocom, and are not an OS authorization layer.
|
|
706
|
+
|
|
707
|
+
The stable contracts other layers build on:
|
|
708
|
+
|
|
709
|
+
- **Engine:** `AgentRunSpec` → `AgentResult` (`ok`, `output`, `structured?`, `usage`, `error?`,
|
|
710
|
+
`failureKind?`, `modelUsed?`) via `StrategyEngine.run`.
|
|
711
|
+
- **Strategy SDK:** `agent` · `parallel` · `reduce.{aggregate,vote,judge}` · `roster.team` · `signal`
|
|
712
|
+
· `limits` · `log` (see [STRATEGIES.md](STRATEGIES.md#the-strategy-sdk)).
|
|
713
|
+
- **Strategy:** `{ name, params?, run(input, sdk) }`. **Reducer:** the `ReducerResult` status union
|
|
714
|
+
(`winner | tie | no_consensus | invalid_outputs`, dissent + invalid preserved, `usedFallback`).
|
|
715
|
+
- **Persona:** identity + `orchestration` grammar / `council` draft. **Bus:**
|
|
716
|
+
`send`/`ask`/`reply`/`onMessage`/`participants` over logical handles (resolved to a broker name only
|
|
717
|
+
when a message crosses a process).
|
|
718
|
+
|
|
719
|
+
## Deliberately deferred
|
|
720
|
+
|
|
721
|
+
- **`context: fork`** — `fresh` is the right child default; fork stays deferred (fail-fast, no silent
|
|
722
|
+
downgrade).
|
|
723
|
+
- Blocking peer asks and hard param validation are out of scope by design — see the reasons in
|
|
724
|
+
[STRATEGIES.md](STRATEGIES.md) and the comm-plane section above. Inter-session comm (the broker
|
|
725
|
+
endpoint stays per-session by design) is no longer a gap: **exocom** (above) is the flat, opt-in
|
|
726
|
+
plane for independently-launched instances sharing a selected scope, including explicitly joined
|
|
727
|
+
peers whose actual files live in another workspace.
|