@aeondave/pi-persona 1.17.4 → 1.17.5
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 +13 -0
- package/CHANGELOG.md +30 -0
- package/README.md +11 -5
- package/docs/ARCHITECTURE.md +109 -18
- package/docs/STRATEGIES.md +6 -0
- package/package.json +2 -2
- package/src/bridge.ts +8 -15
- package/src/bus/broker/client.ts +32 -9
- package/src/bus/broker/host.ts +7 -3
- package/src/bus/broker/messages.ts +4 -1
- package/src/bus/contact-transport.ts +8 -0
- package/src/bus/contact.ts +10 -7
- package/src/bus/peers.ts +8 -6
- package/src/core/companion.ts +64 -0
- package/src/engine/adapter.ts +18 -2
- package/src/engine/async.ts +4 -1
- package/src/engine/child.ts +1 -0
- package/src/engine/inproc.ts +11 -1
- package/src/extension/companion.ts +38 -0
- package/src/extension/engine.ts +20 -8
- package/src/extension/hooks.ts +18 -9
- package/src/extension/memory-owner.ts +54 -0
- package/src/extension/mind-worker.ts +129 -0
- package/src/extension/results.ts +14 -12
- package/src/extension.ts +117 -134
- package/src/orchestration/agent-attempt.ts +15 -0
- package/src/orchestration/flow-run.ts +4 -1
- package/src/orchestration/sdk.ts +19 -15
- package/src/persona/orchestrate.ts +4 -7
- package/src/telemetry/contract.ts +1 -1
- package/src/ui/activity-rows.ts +34 -0
- package/src/ui/agent-overlay.ts +13 -11
- package/src/ui/agent-tree.ts +30 -11
- package/src/ui/strategy-leg.ts +40 -0
package/AGENTS.md
CHANGED
|
@@ -268,6 +268,19 @@ the shared behavioral prompt layer: [`docs/SPINE.md`](docs/SPINE.md).
|
|
|
268
268
|
option (a terminal event is observed before the permission gate). A countdown replaces the stall clock; `x`
|
|
269
269
|
cancels one via the stop registry; `X`, the `N agents` count and telemetry exclude them.
|
|
270
270
|
|
|
271
|
+
- Loaded companions: `core/companion.ts` defines the public local presence, live memory-owner and
|
|
272
|
+
activity contracts; `extension/{companion,memory-owner}.ts` bind them to the current native owner.
|
|
273
|
+
Mind requires a compatible loaded partner, never a settings-file inference. Actual selection is
|
|
274
|
+
authoritative with persistence off; both engines freeze it for delegated legs (private per-child
|
|
275
|
+
factory in-process, spawn-local environment for child processes). `ui/activity-rows.ts` projects
|
|
276
|
+
bounded local Mind work without agent counts, controls, wakes, model usage or agent telemetry.
|
|
277
|
+
Automatic semantic review instead uses `pi-persona:mind-worker:v1`: one real tracked Mind worker,
|
|
278
|
+
with the current session model/effort, no tools/fallback/bus, normal shared concurrency and F9
|
|
279
|
+
controls, a 180s deadline, and a terminal archive. Memory/delegate/target permissions still apply.
|
|
280
|
+
Its completion belongs to Mind; it never wakes the supervisor. See the companion architecture contract.
|
|
281
|
+
These cooperative contracts are not authentication; see the loaded-companion section
|
|
282
|
+
in `docs/ARCHITECTURE.md`.
|
|
283
|
+
|
|
271
284
|
- `src/core/` — pure kernel: frontmatter, permissions, contract (+`parseContract`), config, discovery, fence (`fenceUntrusted`), brief (`buildDelegationBrief` — the per-turn roster + standing hand-off default; `buildExocomBrief` — the per-turn exocom peer roster, the peer-vs-sub-agent split, and the relevance bound on a peer exchange), timer (`TimerScheduler` — the alarm engine behind the `timer` tool), session-runs (pure restore-notice helper: what a reload/resume lost + the run-id counter), types.
|
|
272
285
|
- `src/engine/` — `child.ts`, `inproc.ts` (default), `adapter.ts`, `async.ts` (async tracker/peek), `worktree.ts` (git-worktree isolation), `stream.ts` (event→state).
|
|
273
286
|
- `src/orchestration/` — `sdk.ts` (`agent`/`parallel`/`reduce`), `strategy.ts` (registry), `strategies/*.ts`, `voting.ts`, `flow*.ts` (DAG + JSONL journal + checkpoint gates), `roster.ts` (teams + `rosterSpec`: a roster member is a bare name OR an inline `{ agent, role, model, skills }` that specialises one agent — every strategy runs members through `rosterSpec`).
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows the version scheme
|
|
5
5
|
used in its [GitHub releases](https://github.com/AeonDave/pi-persona/releases).
|
|
6
6
|
|
|
7
|
+
## [1.17.5](https://github.com/AeonDave/pi-persona/releases/tag/v1.17.5) - 2026-10-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- A visible asynchronous Mind maintenance worker, pinned to the current session model and effort.
|
|
12
|
+
It shares normal concurrency, F9 controls and result archival, has no tools or model fallback,
|
|
13
|
+
and returns its completion to Mind without starting another supervisor turn.
|
|
14
|
+
- Loaded-companion presence, live memory ownership and bounded `Mind` activity rows. Mind can
|
|
15
|
+
require the actual loaded Persona partner and follow persona changes even with persistence off;
|
|
16
|
+
both engines freeze that owner for delegated legs. Activity stays separate from agents, controls,
|
|
17
|
+
wakes and model usage. These paired contracts require the updated Mind and Persona together.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Council, MAGI, auxiliary actors and flow legs keep their own model and effort in the live
|
|
22
|
+
tree and F9 when abbreviated roles collide or identical roles use different inline pins.
|
|
23
|
+
Runtime callbacks carry exact attempt identity; retries clear stale observations and terminal
|
|
24
|
+
rows use the engine's observed model. Saved model assignments remain keyed by the full role.
|
|
25
|
+
- Terminal-result capture survives reactivation of the same extension and tracker. Shutdown
|
|
26
|
+
revokes the old archive owner without removing the capture observers or changing their order
|
|
27
|
+
before completion notifications; late results cannot enter another session's archive.
|
|
28
|
+
- Child broker contacts confirm one-way delivery only after admission to the host bus.
|
|
29
|
+
Full inboxes, disconnects, cancellation and missing receipts no longer report `delivered: true`.
|
|
30
|
+
Receipts are bounded and never count as a supervisor answer or authorization.
|
|
31
|
+
Unconfirmed sends and errors after admission no longer claim that the message was dropped;
|
|
32
|
+
the supervisor may already have received it.
|
|
33
|
+
- Reference-flow diagnostics fence worker excerpts even on the defensive missing-reference path.
|
|
34
|
+
- A worker-provided patch remains usable after worktree capture fails, with an explicit warning
|
|
35
|
+
that the patch was not verified against the final worktree state.
|
|
36
|
+
|
|
7
37
|
## [1.17.4](https://github.com/AeonDave/pi-persona/releases/tag/v1.17.4) - 2026-10-25
|
|
8
38
|
|
|
9
39
|
### Added
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pi-persona
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="assets/banner.png" alt="pi-persona — supervised multi-agent orchestration for Pi">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/AeonDave/pi-persona/v1.17.5/assets/banner.png" alt="pi-persona — supervised multi-agent orchestration for Pi">
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
[](https://github.com/AeonDave/pi-persona/actions/workflows/test.yml) [](https://scorecard.dev/viewer/?uri=github.com/AeonDave/pi-persona) [](https://www.npmjs.com/package/@aeondave/pi-persona) [](LICENSE)
|
|
@@ -40,9 +40,9 @@ your customizations.
|
|
|
40
40
|
| Install option | Command |
|
|
41
41
|
|---|---|
|
|
42
42
|
| This project only | `pi install -l npm:@aeondave/pi-persona` |
|
|
43
|
-
| Pin this release | `pi install npm:@aeondave/pi-persona@1.17.
|
|
43
|
+
| Pin this release | `pi install npm:@aeondave/pi-persona@1.17.5` |
|
|
44
44
|
| Update an unpinned install | `pi update npm:@aeondave/pi-persona` |
|
|
45
|
-
| Git tag 1.17.
|
|
45
|
+
| Git tag 1.17.5 | `pi install git:github.com/AeonDave/pi-persona@v1.17.5` |
|
|
46
46
|
|
|
47
47
|
Restart Pi after updating. For a pinned install, install the new version or tag explicitly.
|
|
48
48
|
If switching from a Git install, remove that source first with
|
|
@@ -180,14 +180,20 @@ to those files. It does not delete additional custom files: a persona `pippo.md`
|
|
|
180
180
|
`pluto.md` remains untouched. A custom file using the same destination as a bundled default is
|
|
181
181
|
replaced. Settings, credentials, sessions and project-local overrides are not reset.
|
|
182
182
|
|
|
183
|
-
For optional long-term memory, add [pi-persona-mind](https://github.com/AeonDave/pi-persona-mind).
|
|
184
|
-
|
|
185
183
|
`/persona-settings` is separate from Pi's `/settings`: it edits pi-persona preferences and saved
|
|
186
184
|
persona model assignments, not native Pi or compaction settings. In the TUI, `/` filters settings,
|
|
187
185
|
`r` resets the selected saved override, and `m` opens model assignment editing. Scalar preference
|
|
188
186
|
changes apply after restarting Pi or Pi's native `/reload`, not `/new`; `/persona reload` only
|
|
189
187
|
refreshes persona definitions. Model changes apply to future runs. See [configuration and settings](docs/REFERENCE.md#configuration-and-data).
|
|
190
188
|
|
|
189
|
+
## Optional memory companion
|
|
190
|
+
|
|
191
|
+
[pi-persona-mind 0.8.4](https://github.com/AeonDave/pi-persona-mind/releases/tag/v0.8.4) adds durable
|
|
192
|
+
project/persona memory, session notes and a visible automatic consolidation worker. It requires
|
|
193
|
+
pi-persona 1.17.5 or later. Install it with `pi install npm:@aeondave/pi-persona-mind`, then restart
|
|
194
|
+
Pi or run `/reload`. See its [README](https://github.com/AeonDave/pi-persona-mind#readme) for scope,
|
|
195
|
+
automatic-review limits and undo.
|
|
196
|
+
|
|
191
197
|
## Documentation
|
|
192
198
|
|
|
193
199
|
| Guide | What's inside |
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -288,13 +288,18 @@ up to 16 names. Same idea, two layers, two caps — see docs/REFERENCE.md's exam
|
|
|
288
288
|
|
|
289
289
|
`isolation: worktree` is fail-closed. It requires a clean Git checkout so the detached `HEAD` view
|
|
290
290
|
cannot silently omit staged/unstaged/untracked supervisor work; a non-repository, dirty checkout,
|
|
291
|
-
worktree creation failure,
|
|
292
|
-
the base engine is never invoked against the real tree as a fallback. The
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
291
|
+
worktree creation failure, or a successful leg with neither a captured diff nor a returned unified
|
|
292
|
+
patch fails the leg — the base engine is never invoked against the real tree as a fallback. The
|
|
293
|
+
created worktree starts from the committed `HEAD` only: local ignored files are not copied into it,
|
|
294
|
+
and untracked ignored outputs are not exported. The original checkout's ignored files remain
|
|
295
|
+
untouched; tasks that need local-only inputs must recreate or provide them explicitly. The
|
|
296
|
+
worktree's initial commit is pinned before the worker starts. Export compares the final file state
|
|
297
|
+
against that immutable commit, so worker commits, later unstaged changes and new files are included
|
|
298
|
+
together. The diff is captured before the temporary tree is removed, then returned to the supervisor;
|
|
299
|
+
commit history is not applied to the main checkout. If Git cannot capture the diff but the worker
|
|
300
|
+
returned a unified patch, that patch is retained with a warning that it was worker-provided and not
|
|
301
|
+
verified against the worktree state. Every git call is asynchronous so the extension host keeps
|
|
302
|
+
rendering, ticking timers, and serving broker sockets while a large checkout runs.
|
|
298
303
|
|
|
299
304
|
Transient retries inside one agent session belong to the host **Pi runtime**'s `retry.*` settings, not
|
|
300
305
|
a persona contract — pi-persona neither parses retry notation nor schedules its own backoff. Both
|
|
@@ -317,10 +322,19 @@ models, repeated same-model attempts, stop/timeout/contract/agent/infrastructure
|
|
|
317
322
|
child/token budgets do not recover. Both attempts are accounted, and a warning plus result metadata
|
|
318
323
|
identify the switch. A per-role model assignment can supplement the legacy per-agent assignment;
|
|
319
324
|
preflight model selection includes external arbiters and other declared agent-valued parameters.
|
|
320
|
-
Live tree and telemetry labels
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
325
|
+
Live tree and telemetry labels resolve auxiliary actors and every repeated roster occurrence from
|
|
326
|
+
the SDK's exact, immutable attempt identity (`agent`, full `role`, explicit `model` and `thinking`),
|
|
327
|
+
alongside its unique node key. Short role hints and `#N` suffixes are presentation only: colliding
|
|
328
|
+
hints never discard a known model or borrow another occurrence's inline pins. Model assignments
|
|
329
|
+
and agent defaults are copied after setup when the engine is constructed, so a queued leg's display
|
|
330
|
+
cannot change with later persona/settings edits. Seeds use each actual roster member; lifecycle
|
|
331
|
+
updates use the actual attempt, including custom strategy calls and auxiliary actors in any order.
|
|
332
|
+
A recovery keeps the node key, changes its requested model, and clears the preceding attempt's Pi
|
|
333
|
+
effort observation. Terminal `modelUsed` and validated effort observations override intent only when
|
|
334
|
+
reported; an absent observation retains the current attempt's intent. Initial effort remains the
|
|
335
|
+
explicit spec request, never inferred provider cognition. Clearing an unknown model emits an empty
|
|
336
|
+
`model` string in a telemetry update, preserving the existing string patch contract without leaving
|
|
337
|
+
a stale previous model in consumers.
|
|
324
338
|
|
|
325
339
|
**Model assignment repair is configuration, not execution recovery.** `/persona models [profile]`
|
|
326
340
|
is an explicit user edit in the Pi adapter: it reuses `resolveCouncilInvocation`,
|
|
@@ -497,8 +511,9 @@ never simultaneously a telemetry event, a routed message, and a UI source of tru
|
|
|
497
511
|
usage`). Feeds derived UI; never routed as an agent message.
|
|
498
512
|
- **Bus Msg** — semantic agent↔agent communication (`progress · decision · interview`, and sibling
|
|
499
513
|
peer sends). `src/bus`. `progress_update` is a Bus message only, not an EngineEvent.
|
|
500
|
-
- **ProgressView** — a derived, read-only UI view computed
|
|
501
|
-
|
|
514
|
+
- **ProgressView** — a derived, read-only UI view computed from observed runtime lifecycles:
|
|
515
|
+
EngineEvents, wake-source lifecycles, or the bounded local companion lifecycle below. Never a
|
|
516
|
+
source of truth. The `f9` agent tree and `peek` digest are ProgressViews.
|
|
502
517
|
|
|
503
518
|
Steering is a supervisor-to-child control action, distinct from semantic Bus messages; requesting
|
|
504
519
|
steering does not establish that a blocking headless delegate call has been interrupted. The peek digest is
|
|
@@ -508,6 +523,69 @@ External telemetry is not a fourth communication plane: it is a generic observer
|
|
|
508
523
|
future plugins may consume or produce, not an exclusive pi-persona protocol. It observes projected
|
|
509
524
|
events and cannot route, reply, steer, or otherwise control agents.
|
|
510
525
|
|
|
526
|
+
### Loaded companions and local activity
|
|
527
|
+
|
|
528
|
+
The local EventBus request `pi-persona:companion:v1` accepts `{ version: 1, respond }` and calls
|
|
529
|
+
`respond` synchronously with `{ version: 1, provider: "pi-persona", role, capabilities }`.
|
|
530
|
+
`role` is `supervisor`, `leg`, or `disabled`; `capabilities.memory` is `1` for a loaded supervisor or
|
|
531
|
+
delegated leg and `false` for a disabled supervisor. `capabilities.activity` is `1` only for an enabled
|
|
532
|
+
supervisor. Persona registers this responder before its bridge and disable guards. Pi owns the
|
|
533
|
+
subscription lifetime and revokes it on disposal/reload. Companions query the loaded extension;
|
|
534
|
+
settings files, package installation, and environment flags cannot establish its presence. This is
|
|
535
|
+
a cooperative local extension contract, not authentication or an OS permission boundary. The
|
|
536
|
+
contract is available from Persona 1.17.5, paired with Mind 0.8.4 or later; qualify both together.
|
|
537
|
+
|
|
538
|
+
Memory ownership is queried through `pi-persona:memory-owner:v1` with
|
|
539
|
+
`{ version: 1, sessionId, respond }`. Its synchronous response is
|
|
540
|
+
`{ version: 1, provider: "pi-persona", sessionId, ready, persona }`, where `persona` is the actual
|
|
541
|
+
live selection or `null` for Persona off. The native manager/id/file must still match, and `ready`
|
|
542
|
+
remains false until Persona's session startup finishes. `pi-persona:memory-owner-changed:v1`
|
|
543
|
+
broadcasts that same descriptor on readiness, selection and shutdown. A companion loaded first
|
|
544
|
+
must not wait inside its startup hook for a later extension's hook; it can activate on the readiness
|
|
545
|
+
broadcast or its next fresh query. Canonical settings retain startup preferences, not live ownership:
|
|
546
|
+
`persist: false` selections still change this descriptor. Unavailable ownership never authorizes a
|
|
547
|
+
fallback to another persona's saved selection.
|
|
548
|
+
|
|
549
|
+
Each engine freezes the parent's memory persona when it is built. In-process children receive a
|
|
550
|
+
private ResourceLoader factory and their own native manager/EventBus responder; child processes
|
|
551
|
+
receive the versioned `PI_PERSONA_MEMORY_PERSONA` record only in that spawn's environment. Inherited
|
|
552
|
+
values are scrubbed before spawning. Neither route mutates a shared process-wide persona value.
|
|
553
|
+
Delegated legs expose this frozen owner while retaining their lean memory-only role, including an
|
|
554
|
+
explicit `null` owner when the parent had Persona off.
|
|
555
|
+
|
|
556
|
+
`pi-persona:activity:v1` contributes operator-only local-work rows through a closed record:
|
|
557
|
+
`{ version: 1, producerId: "pi-persona-mind", activityId, sessionId, phase, state, target }`.
|
|
558
|
+
`phase` is `inspect|stage|apply`, `state` is `running|staged|done|failed`, and `target` is
|
|
559
|
+
`project|persona|shared`; `staged` is valid only for `stage`. No memory text, paths, models, token
|
|
560
|
+
counts, or free-form output are accepted. Ingress matches the active native session and checks its
|
|
561
|
+
manager's current id/file. The producer also captures its native owner and scope across awaits.
|
|
562
|
+
The resulting `Mind` row uses `kind: "activity"`: it has no agent count, steer/stop handle, wake,
|
|
563
|
+
stall badge, or `agent.*` telemetry. The F9 drill-in explains the local phase. Up to eight rows are
|
|
564
|
+
retained, with completed/staged rows evicted before live work; teardown and explicit persona
|
|
565
|
+
selection revoke old rows without cancelling workers. A staged row states that a proposal was
|
|
566
|
+
prepared and requires explicit apply; it does not claim that the plan remains valid indefinitely or
|
|
567
|
+
that model review is running.
|
|
568
|
+
Receipts remain in Mind's session-native cards; local activity events are not replayed on restart.
|
|
569
|
+
|
|
570
|
+
Mind may request one real async maintenance worker through `pi-persona:mind-worker:v1` with
|
|
571
|
+
the closed record `{ version: 1, sessionId, persona, task, respond }` (task at most 48,000 characters).
|
|
572
|
+
Persona checks the current native session and ready memory owner, actual persona, delegation and
|
|
573
|
+
memory permissions, the persona's raw delegate target rules for `mind`, and idle/no-pending-message
|
|
574
|
+
state. A strict target allowlist must include `mind`; explicit memory/delegate/target denies win.
|
|
575
|
+
No installed agent is required. Accepted
|
|
576
|
+
responses are synchronous `{ ok: true, runId, model, thinking, completion, cancel }`; rejection is
|
|
577
|
+
`{ ok: false, reason }`. `completion` resolves with `{ ok, output, error?, resultRef?, model?, thinking? }`.
|
|
578
|
+
Output remains untrusted data for Mind's bounded proposal parser, never executable code.
|
|
579
|
+
|
|
580
|
+
This is an ordinary tracked `Mind` sub-agent in the tree and F9, sharing the launch pool, usage,
|
|
581
|
+
progress, stop/steer controls and terminal-result archive. Its private definition has an explicit empty
|
|
582
|
+
tool allowlist and no worker spine. Provider/model and thinking are pinned to the accepting session;
|
|
583
|
+
provider fallback is disabled. The worker has a 180-second cancellation ceiling; owner readiness or
|
|
584
|
+
selection changes cancel it. Mind can also cancel through its accepted handle. Settlement returns
|
|
585
|
+
to the requesting companion and retains operator failure reporting, but never wakes a supervisor
|
|
586
|
+
model turn. Native session teardown cancels it through the ordinary stop registry. Local inspect,
|
|
587
|
+
stage and apply operations continue to use activity rows; model work uses the real agent view.
|
|
588
|
+
|
|
511
589
|
## The comm plane in practice
|
|
512
590
|
|
|
513
591
|
- **In-process bus** (`bus/inproc.ts`) — a handle-based mailbox: `send` (one-way), `ask` (blocks for a
|
|
@@ -539,10 +617,14 @@ events and cannot route, reply, steer, or otherwise control agents.
|
|
|
539
617
|
notifier, f9, peek) is unchanged by construction. Off ⇒ the host never starts and the child spawns
|
|
540
618
|
byte-identical to pre-broker pi-persona. Ask failures echo the originating `msgId` so the client
|
|
541
619
|
rejects the right request immediately, and a client cancellation propagates only to that connection's
|
|
542
|
-
own host ask.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
620
|
+
own host ask. A one-way send gets a UUID-correlated host receipt; `delivered: true` means admitted
|
|
621
|
+
to the bus, not read or acted on by the peer. Each child is limited to 64 pending receipts with a
|
|
622
|
+
maximum 10-second wait. Abort, disconnect, a full or missing inbox, or a lost receipt reports
|
|
623
|
+
`delivered: false`. It never waits for a recipient answer or approval and does not replay.
|
|
624
|
+
`extension/broker-host.ts`'s `SupervisorBroker` owns the lifecycle: it starts
|
|
625
|
+
the host lazily on the first child-engine build and warns once on a failed bind. Children built while
|
|
626
|
+
it's down spawn without a bus endpoint, so they never burn connect backoff against a dead socket;
|
|
627
|
+
the next build retries; `/doctor` surfaces its state on one line. A dropped client connection flips the
|
|
546
628
|
child-side bridge to `⇄ offline` and fails a fresh ask fast rather than hanging.
|
|
547
629
|
A connected child exposes `contact_supervisor` when `canUseBus` is allowed; `contact_peer` also
|
|
548
630
|
requires the run's peer opt-in. Granted custom tool names join a nonempty plain allowlist so Pi's
|
|
@@ -659,7 +741,16 @@ retention is labelled as such. Retrieval is read-only: `intercom results` lists,
|
|
|
659
741
|
{ ref, offset, limit }` pages (8,000 default / 16,000 max units, surrogate-safe), with no respawn,
|
|
660
742
|
rebilling, collection or extra wake; reading a page never returns the whole run hidden in metadata.
|
|
661
743
|
A saved result is not a live worker: the restore notice says runtime handles are gone while retained
|
|
662
|
-
reports remain readable.
|
|
744
|
+
reports remain readable. Archive listeners belong to the extension factory/tracker lifetime and are
|
|
745
|
+
installed exactly once. `session_shutdown` revokes that session's archive owner and clears its
|
|
746
|
+
captures. A later activation creates a fresh owner and capture context before the ordinary completion
|
|
747
|
+
listener observes terminal results for that activation; a late callback bound to the old owner cannot
|
|
748
|
+
append into the new session.
|
|
749
|
+
|
|
750
|
+
Flow reference handoff needs durable session-entry storage when a phase's result feeds a dependent
|
|
751
|
+
phase. In a no-session run, a source phase can finish with a memory-only receipt, but the dependent's
|
|
752
|
+
reader preflight rejects it before that dependent is spawned. Use a persisted session for reference
|
|
753
|
+
handoff across phases, or use inline handoff when no durable reader is available.
|
|
663
754
|
|
|
664
755
|
This is a UI invariant only: truncating a collapsed card must never be confused with truncating the
|
|
665
756
|
underlying result or changing a strategy's contract.
|
package/docs/STRATEGIES.md
CHANGED
|
@@ -80,6 +80,12 @@ Same-agent members are disambiguated in the live `f9` tree by a role hint (`revi
|
|
|
80
80
|
`rosterNodeKeys`/`roleHint` (`roster.ts`) + the SDK's per-run key (`sdk.ts`). If you touch either, keep
|
|
81
81
|
the seeding loop and the SDK key derivation in lockstep, and inject any per-run protocol via the TASK
|
|
82
82
|
text, never `role` (or the derived tree key drifts from the seeded one).
|
|
83
|
+
The short hint is never used to recover a participant's model or effort. SDK lifecycle callbacks
|
|
84
|
+
carry a frozen `AgentAttemptIdentity` beside that key, containing only the actual agent, full role,
|
|
85
|
+
explicit model and requested thinking. This preserves distinct inline selections for repeated roles,
|
|
86
|
+
auxiliary actors and custom strategies that invoke members in a different order. Recovery updates the
|
|
87
|
+
same logical key with the new attempt's model; observed terminal metadata remains separate from the
|
|
88
|
+
request. Tasks and result grants are excluded from this display identity.
|
|
83
89
|
|
|
84
90
|
## The built-in strategies
|
|
85
91
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aeondave/pi-persona",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Supervised multi-agent orchestration for Pi — async workers, live steering, cross-session collaboration, and switchable personas.",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
}
|
|
112
112
|
},
|
|
113
113
|
"pi": {
|
|
114
|
-
"image": "https://raw.githubusercontent.com/AeonDave/pi-persona/v1.17.
|
|
114
|
+
"image": "https://raw.githubusercontent.com/AeonDave/pi-persona/v1.17.5/assets/banner.png",
|
|
115
115
|
"video": "https://cdn.jsdelivr.net/gh/AeonDave/pi-persona@6655d281ce7965a75303dd704bd29db73db04070/assets/demo1.mp4",
|
|
116
116
|
"extensions": [
|
|
117
117
|
"./src/extension.ts"
|
package/src/bridge.ts
CHANGED
|
@@ -20,7 +20,8 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
20
20
|
|
|
21
21
|
import { type BrokerClient, type DeliverEvent, makeBrokerClient, type MakeBrokerClientDeps } from "./bus/broker/client.ts";
|
|
22
22
|
import { makeContactSupervisorTool } from "./bus/contact.ts";
|
|
23
|
-
import {
|
|
23
|
+
import type { MsgKind } from "./bus/inproc.ts";
|
|
24
|
+
import type { ContactTransport } from "./bus/contact-transport.ts";
|
|
24
25
|
import { makeContactPeerTool, type PeerInfo } from "./bus/peers.ts";
|
|
25
26
|
import { attributeInbound } from "./core/fence.ts";
|
|
26
27
|
import { isReaderToken } from "./bus/broker/messages.ts";
|
|
@@ -49,30 +50,22 @@ export function isBridgeMode(env: Env = process.env): boolean {
|
|
|
49
50
|
return !!env.PI_PERSONA_BUS;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
* object) so it satisfies the factories' concrete parameter type without touching them.
|
|
56
|
-
* `connected` gates both overrides so the tools' existing "no supervisor reachable" /
|
|
57
|
-
* "dropped" wording stays honest before the handshake completes — the client's own
|
|
58
|
-
* `send`/`ask` can't report that (a write to a not-yet-open socket silently no-ops, per
|
|
59
|
-
* spec's "sends report undelivered"). */
|
|
60
|
-
class BrokerBus extends InProcessBus {
|
|
53
|
+
/** Contact transport over the broker. A connected socket is not delivery evidence:
|
|
54
|
+
* send resolves only after the host acknowledges admission to its local bus. */
|
|
55
|
+
class BrokerBus implements ContactTransport {
|
|
61
56
|
private readonly client: BrokerClient;
|
|
62
57
|
connected = false;
|
|
63
58
|
|
|
64
59
|
constructor(client: BrokerClient) {
|
|
65
|
-
super();
|
|
66
60
|
this.client = client;
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
|
|
63
|
+
send(_from: string, to: string, text: string, kind: MsgKind = "progress", signal?: AbortSignal): boolean | Promise<boolean> {
|
|
70
64
|
if (!this.connected) return false;
|
|
71
|
-
this.client.send(to, kind, text);
|
|
72
|
-
return true;
|
|
65
|
+
return this.client.send(to, kind, text, signal);
|
|
73
66
|
}
|
|
74
67
|
|
|
75
|
-
|
|
68
|
+
ask(
|
|
76
69
|
_from: string,
|
|
77
70
|
to: string,
|
|
78
71
|
text: string,
|
package/src/bus/broker/client.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Broker client — the child-side half of the relay (spec B3/B6): connects to the
|
|
3
3
|
* session-scoped endpoint (`paths.ts`), performs the `register`/`registered` handshake,
|
|
4
4
|
* and exposes the same shape a local `contact_supervisor`/`contact_peer` binding needs —
|
|
5
|
-
* `send` (
|
|
5
|
+
* `send` (one-way with a bounded host-admission receipt), `ask` (a blocking `send{expectsReply}` correlated to a
|
|
6
6
|
* `replied{askId}`, 10-minute cap, mirrors `bus.ask`, and sends `cancel{msgId}` when
|
|
7
7
|
* aborted), `reply` (answers a host-issued
|
|
8
8
|
* blocking `deliver`), `list` (the engine-scoped peer roster, B7), and `onDeliver`/`onSteer`
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* exponential backoff (250ms–5s) on the INITIAL connect only (comtac's presence tracking,
|
|
15
15
|
* roster cache, and endless reconnect-after-disconnect "storm" are dropped — YAGNI here,
|
|
16
16
|
* see spec B6/Task 4 brief). An unexpected disconnect (host died
|
|
17
|
-
* mid-run) is NOT auto-reconnected: sends
|
|
17
|
+
* mid-run) is NOT auto-reconnected: sends resolve false, while in-flight asks/lists
|
|
18
18
|
* reject promptly and clean up their timers. Authenticated result readers are stricter:
|
|
19
19
|
* one connect attempt, bounded handshake/reads, closed failure replies, never mute success.
|
|
20
20
|
*
|
|
@@ -46,8 +46,9 @@ export interface BrokerClient {
|
|
|
46
46
|
register(): Promise<void>;
|
|
47
47
|
/** Authenticated bounded read. Failures are fixed closed replies; never a mute success. */
|
|
48
48
|
read(request: ResultReadRequest, signal?: AbortSignal): Promise<ReadReply>;
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/** One-way message; true only after host bus admission, never a recipient answer.
|
|
50
|
+
* Missing receipt, cancellation, saturation or disconnect resolve false without replay. */
|
|
51
|
+
send(to: string, kind: MsgKind, text: string, signal?: AbortSignal): Promise<boolean>;
|
|
51
52
|
/** Blocking request; resolves on a matching `replied`, rejects on `signal` abort or the
|
|
52
53
|
* 10-minute cap (mirrors `bus.ask`'s default). */
|
|
53
54
|
ask(to: string, kind: MsgKind, text: string, signal?: AbortSignal): Promise<string>;
|
|
@@ -81,6 +82,8 @@ const ASK_TIMEOUT_MS = 600_000;
|
|
|
81
82
|
// A roster lookup is one local socket/pipe round-trip, unlike a blocking supervisor ask. Do not
|
|
82
83
|
// let a half-open broker pin `contact_peer list/send` until the child idle watchdog fires.
|
|
83
84
|
const LIST_TIMEOUT_MS = 10_000;
|
|
85
|
+
export const SEND_TIMEOUT_MS = 10_000;
|
|
86
|
+
export const MAX_PENDING_SENDS = 64;
|
|
84
87
|
|
|
85
88
|
export function makeBrokerClient(deps: MakeBrokerClientDeps): BrokerClient {
|
|
86
89
|
const netImpl = deps.net ?? nodeNet;
|
|
@@ -93,6 +96,7 @@ export function makeBrokerClient(deps: MakeBrokerClientDeps): BrokerClient {
|
|
|
93
96
|
let cancelConnect: (() => void) | undefined;
|
|
94
97
|
let cancelBackoff: (() => void) | undefined;
|
|
95
98
|
const pendingReads = new Map<string, { request: ResultReadRequest; resolve: (result: ReadReply) => void }>();
|
|
99
|
+
const pendingSends = new Map<string, (delivered: boolean) => void>();
|
|
96
100
|
let registerSettle: { resolve: () => void; reject: (err: Error) => void } | undefined;
|
|
97
101
|
|
|
98
102
|
const deliverListeners = new Set<(evt: DeliverEvent) => void>();
|
|
@@ -101,9 +105,7 @@ export function makeBrokerClient(deps: MakeBrokerClientDeps): BrokerClient {
|
|
|
101
105
|
const pendingAsks = new Map<string, { resolve: (text: string) => void; reject: (err: Error) => void }>();
|
|
102
106
|
const pendingLists = new Map<string, { resolve: (peers: Array<{ handle: string; label: string }>) => void; reject: (err: Error) => void }>();
|
|
103
107
|
|
|
104
|
-
/** Best-effort write
|
|
105
|
-
* the design's "sends report undelivered" (no throw the caller could even act on,
|
|
106
|
-
* `send`/`reply` are `void`). */
|
|
108
|
+
/** Best-effort control frame write. Confirmed sends use their own guarded write and receipt. */
|
|
107
109
|
function write(frame: Frame): void {
|
|
108
110
|
if (!socket) return;
|
|
109
111
|
try {
|
|
@@ -126,6 +128,9 @@ export function makeBrokerClient(deps: MakeBrokerClientDeps): BrokerClient {
|
|
|
126
128
|
|
|
127
129
|
function handleFrame(frame: Frame): void {
|
|
128
130
|
switch (frame.t) {
|
|
131
|
+
case "sent":
|
|
132
|
+
pendingSends.get(frame.msgId)?.(frame.delivered);
|
|
133
|
+
return;
|
|
129
134
|
case "registered":
|
|
130
135
|
if (!registerSettle) return;
|
|
131
136
|
if (frame.handle !== deps.handle || (deps.readerToken !== undefined && (!frame.readerToken
|
|
@@ -146,6 +151,7 @@ export function makeBrokerClient(deps: MakeBrokerClientDeps): BrokerClient {
|
|
|
146
151
|
// A correlated error belongs to one request on this connection. Do not let an
|
|
147
152
|
// unknown/stale correlation id affect the register handshake or another ask.
|
|
148
153
|
if (frame.msgId !== undefined) {
|
|
154
|
+
pendingSends.get(frame.msgId)?.(false);
|
|
149
155
|
pendingAsks.get(frame.msgId)?.reject(new Error(frame.reason));
|
|
150
156
|
return;
|
|
151
157
|
}
|
|
@@ -254,6 +260,7 @@ export function makeBrokerClient(deps: MakeBrokerClientDeps): BrokerClient {
|
|
|
254
260
|
}
|
|
255
261
|
|
|
256
262
|
function rejectAllPending(reason: string): void {
|
|
263
|
+
for (const settle of pendingSends.values()) settle(false);
|
|
257
264
|
for (const p of pendingReads.values()) p.resolve(readFailure("unavailable"));
|
|
258
265
|
for (const p of pendingAsks.values()) p.reject(new Error(reason));
|
|
259
266
|
pendingAsks.clear();
|
|
@@ -354,8 +361,24 @@ export function makeBrokerClient(deps: MakeBrokerClientDeps): BrokerClient {
|
|
|
354
361
|
});
|
|
355
362
|
}
|
|
356
363
|
|
|
357
|
-
function send(to: string, kind: MsgKind, text: string):
|
|
358
|
-
|
|
364
|
+
function send(to: string, kind: MsgKind, text: string, signal?: AbortSignal): Promise<boolean> {
|
|
365
|
+
if (signal?.aborted || closed || disconnected || !registered || !socket || pendingSends.size >= MAX_PENDING_SENDS) return Promise.resolve(false);
|
|
366
|
+
const msgId = randomUUID();
|
|
367
|
+
return new Promise((resolve) => {
|
|
368
|
+
const finish = (delivered: boolean): void => {
|
|
369
|
+
if (!pendingSends.delete(msgId)) return;
|
|
370
|
+
clearTimeout(timer);
|
|
371
|
+
signal?.removeEventListener("abort", onAbort);
|
|
372
|
+
resolve(delivered);
|
|
373
|
+
};
|
|
374
|
+
const onAbort = (): void => finish(false);
|
|
375
|
+
const timer = setTimeout(() => finish(false), SEND_TIMEOUT_MS);
|
|
376
|
+
timer.unref?.();
|
|
377
|
+
pendingSends.set(msgId, finish);
|
|
378
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
379
|
+
try { socket!.write(encodeFrame({ t: "send", to, kind, text, msgId, expectsReply: false })); }
|
|
380
|
+
catch { finish(false); }
|
|
381
|
+
});
|
|
359
382
|
}
|
|
360
383
|
|
|
361
384
|
/** Why `ask`/`list` refuse to send right now — distinguishes a disposed client from one that
|
package/src/bus/broker/host.ts
CHANGED
|
@@ -373,7 +373,7 @@ export async function startBrokerHost(deps: StartBrokerHostDeps): Promise<Broker
|
|
|
373
373
|
}
|
|
374
374
|
case "send": {
|
|
375
375
|
if (!handle) {
|
|
376
|
-
write({ t: "error", reason: "send before register" });
|
|
376
|
+
write({ t: "error", reason: "send before register", msgId: frame.msgId });
|
|
377
377
|
return;
|
|
378
378
|
}
|
|
379
379
|
const from = handle;
|
|
@@ -395,8 +395,12 @@ export async function startBrokerHost(deps: StartBrokerHostDeps): Promise<Broker
|
|
|
395
395
|
forgetAsk(frame.msgId, ac);
|
|
396
396
|
write({ t: "error", reason: errorReason(error), msgId: frame.msgId });
|
|
397
397
|
}
|
|
398
|
-
} else
|
|
399
|
-
|
|
398
|
+
} else {
|
|
399
|
+
// Admission is not an answer. Unknown/full inboxes both return false;
|
|
400
|
+
// never label a full inbox "unknown peer" or a socket write "delivered".
|
|
401
|
+
let delivered = false;
|
|
402
|
+
try { delivered = bus.send(from, frame.to, frame.text, frame.kind); } catch { /* no confirmation */ }
|
|
403
|
+
write({ t: "sent", msgId: frame.msgId, delivered });
|
|
400
404
|
}
|
|
401
405
|
return;
|
|
402
406
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slimmed wire-frame catalog + validators (spec B6) — comtac's protocol trimmed to the
|
|
3
3
|
* frames pi-persona actually needs: register/send/cancel/reply/list/bye (client→host) and
|
|
4
|
-
* registered/deliver/steer/peers/replied/error (host→client). Mirrors comtac's
|
|
4
|
+
* registered/sent/deliver/steer/peers/replied/error (host→client). Mirrors comtac's
|
|
5
5
|
* `messages.ts` validation STYLE (structural per-field checks, one explicit assert per
|
|
6
6
|
* case) but drops presence/sessions/attachments/broadcasts entirely (YAGNI here).
|
|
7
7
|
*
|
|
@@ -28,6 +28,7 @@ export type Frame =
|
|
|
28
28
|
| { t: "result_read_reply"; reqId: string; result: ReadReply }
|
|
29
29
|
| { t: "result_read_cancel"; reqId: string }
|
|
30
30
|
| { t: "send"; to: string; kind: MsgKind; text: string; msgId: string; expectsReply: boolean }
|
|
31
|
+
| { t: "sent"; msgId: string; delivered: boolean }
|
|
31
32
|
| { t: "cancel"; msgId: string }
|
|
32
33
|
| { t: "deliver"; from: string; fromLabel?: string; kind: MsgKind; text: string; msgId: string; expectsReply: boolean }
|
|
33
34
|
| { t: "reply"; askId: string; text: string }
|
|
@@ -84,6 +85,8 @@ export function isFrame(value: unknown): value is Frame {
|
|
|
84
85
|
);
|
|
85
86
|
case "cancel":
|
|
86
87
|
return typeof value.msgId === "string";
|
|
88
|
+
case "sent":
|
|
89
|
+
return closedRecord(value, ["t", "msgId", "delivered"]) && typeof value.msgId === "string" && typeof value.delivered === "boolean";
|
|
87
90
|
case "deliver": {
|
|
88
91
|
if (typeof value.from !== "string") return false;
|
|
89
92
|
if (value.fromLabel !== undefined && typeof value.fromLabel !== "string") return false;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { InProcessBus, MsgKind } from "./inproc.ts";
|
|
2
|
+
|
|
3
|
+
/** Child contact tools need delivery admission, not a concrete mailbox implementation.
|
|
4
|
+
* A remote send awaits only the host's bounded receipt, never the recipient's answer. */
|
|
5
|
+
export interface ContactTransport {
|
|
6
|
+
send(from: string, to: string, text: string, kind?: MsgKind, signal?: AbortSignal): boolean | Promise<boolean>;
|
|
7
|
+
ask: InProcessBus["ask"];
|
|
8
|
+
}
|
package/src/bus/contact.ts
CHANGED
|
@@ -21,7 +21,8 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
21
21
|
|
|
22
22
|
import { compactInlineText } from "../ui/presentation.ts";
|
|
23
23
|
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
24
|
-
import type {
|
|
24
|
+
import type { MsgKind } from "./inproc.ts";
|
|
25
|
+
import type { ContactTransport } from "./contact-transport.ts";
|
|
25
26
|
|
|
26
27
|
const ContactParams = Type.Object({
|
|
27
28
|
kind: Type.Union([Type.Literal("progress"), Type.Literal("decision"), Type.Literal("interview")], {
|
|
@@ -94,7 +95,7 @@ function result(t: string, details: ContactDetails): { content: [{ type: "text";
|
|
|
94
95
|
* the supervisor is busy (a sync run holds the turn), so the question is posted one-way and
|
|
95
96
|
* returns unresolved. This prevents a sync delegate/council from deadlocking on a child ask. */
|
|
96
97
|
export function makeContactSupervisorTool(
|
|
97
|
-
bus:
|
|
98
|
+
bus: ContactTransport,
|
|
98
99
|
fromHandle: string,
|
|
99
100
|
supervisorHandle: string,
|
|
100
101
|
opts: { allowBlocking?: boolean } = {},
|
|
@@ -125,13 +126,14 @@ export function makeContactSupervisorTool(
|
|
|
125
126
|
const message = boundContactMessage(params.message);
|
|
126
127
|
if (kind === "progress") {
|
|
127
128
|
try {
|
|
128
|
-
const delivered = bus.send(fromHandle, supervisorHandle, message, "progress");
|
|
129
|
+
const delivered = await bus.send(fromHandle, supervisorHandle, message, "progress", signal);
|
|
130
|
+
if (signal?.aborted) return result("The progress update was cancelled; delivery could not be confirmed.", { kind, delivered: false, outcome: "cancelled" });
|
|
129
131
|
return result(
|
|
130
|
-
delivered ? "Progress reported to the supervisor." : "
|
|
132
|
+
delivered ? "Progress reported to the supervisor." : "Progress delivery could not be confirmed. The supervisor may already have received the update.",
|
|
131
133
|
{ kind, delivered, outcome: delivered ? "reported" : "unavailable" },
|
|
132
134
|
);
|
|
133
135
|
} catch {
|
|
134
|
-
return result("
|
|
136
|
+
return result("Progress delivery could not be confirmed. The supervisor may already have received the update.", { kind, delivered: false, outcome: "failed" });
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
|
|
@@ -140,13 +142,14 @@ export function makeContactSupervisorTool(
|
|
|
140
142
|
if (!allowBlocking) {
|
|
141
143
|
let delivered: boolean;
|
|
142
144
|
try {
|
|
143
|
-
delivered = bus.send(fromHandle, supervisorHandle, message, kind);
|
|
145
|
+
delivered = await bus.send(fromHandle, supervisorHandle, message, kind, signal);
|
|
144
146
|
} catch {
|
|
145
147
|
return result(
|
|
146
|
-
`
|
|
148
|
+
`Delivery of the question could not be confirmed. The supervisor may already have received it; no answer is possible in this path. ${unresolvedGuidance(kind)}`,
|
|
147
149
|
{ kind, delivered: false, outcome: "failed" },
|
|
148
150
|
);
|
|
149
151
|
}
|
|
152
|
+
if (signal?.aborted) return result(`The request was cancelled. ${unresolvedGuidance(kind)}`, { kind, delivered: false, outcome: "cancelled" });
|
|
150
153
|
const delivery = delivered ? "question sent one-way" : "question delivery could not be confirmed";
|
|
151
154
|
return result(
|
|
152
155
|
`The supervisor is busy right now; ${delivery}, and no answer is possible in this path. ${unresolvedGuidance(kind)}`,
|