@aeondave/pi-persona 1.17.3 → 1.17.4

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.
Files changed (84) hide show
  1. package/AGENTS.md +44 -36
  2. package/CHANGELOG.md +76 -1
  3. package/README.md +23 -13
  4. package/docs/ARCHITECTURE.md +97 -37
  5. package/docs/COLLABORATION.md +110 -0
  6. package/docs/EXPERIENCE.md +33 -8
  7. package/docs/README.md +12 -0
  8. package/docs/REFERENCE.md +65 -14
  9. package/package.json +12 -6
  10. package/src/agents/agent.ts +6 -0
  11. package/src/bridge.ts +25 -8
  12. package/src/bus/broker/client.ts +119 -21
  13. package/src/bus/broker/host.ts +123 -8
  14. package/src/bus/broker/messages.ts +31 -4
  15. package/src/bus/contact.ts +113 -38
  16. package/src/bus/peers.ts +15 -0
  17. package/src/bus/result-reader.ts +88 -0
  18. package/src/core/brief.ts +14 -4
  19. package/src/core/capabilities.ts +1 -1
  20. package/src/core/leg-thinking.ts +57 -0
  21. package/src/core/ownership.ts +1 -2
  22. package/src/core/pi-compat.ts +5 -12
  23. package/src/core/session-runs.ts +1 -1
  24. package/src/core/types.ts +1 -1
  25. package/src/engine/adapter.ts +83 -19
  26. package/src/engine/async.ts +49 -10
  27. package/src/engine/child.ts +10 -2
  28. package/src/engine/inproc.ts +87 -46
  29. package/src/engine/result-grant.ts +62 -0
  30. package/src/engine/session-tools.ts +6 -0
  31. package/src/engine/stream.ts +6 -0
  32. package/src/engine/worktree.ts +17 -7
  33. package/src/exocom/collaboration-delivery.ts +29 -0
  34. package/src/exocom/collaboration.ts +148 -0
  35. package/src/exocom/envelope.ts +43 -4
  36. package/src/exocom/gate.ts +7 -2
  37. package/src/exocom/install.ts +45 -17
  38. package/src/exocom/ledger.ts +58 -8
  39. package/src/exocom/plane.ts +32 -1
  40. package/src/exocom/profile.ts +40 -0
  41. package/src/exocom/registry.ts +5 -0
  42. package/src/exocom/status.ts +11 -1
  43. package/src/exocom/write-guard.ts +7 -0
  44. package/src/extension/broker-host.ts +61 -9
  45. package/src/extension/engine.ts +20 -21
  46. package/src/extension/hooks.ts +952 -938
  47. package/src/extension/identity.ts +41 -75
  48. package/src/extension/results.ts +136 -0
  49. package/src/extension/shared.ts +17 -0
  50. package/src/extension.ts +2963 -2873
  51. package/src/loader.ts +11 -3
  52. package/src/orchestration/flow-journal.ts +7 -1
  53. package/src/orchestration/flow-run.ts +122 -19
  54. package/src/orchestration/flow.ts +11 -0
  55. package/src/orchestration/roster.ts +71 -23
  56. package/src/orchestration/sdk.ts +44 -6
  57. package/src/orchestration/types.ts +10 -0
  58. package/src/persona/model-participants.ts +20 -7
  59. package/src/persona/orchestrate.ts +2 -0
  60. package/src/persona/persona.ts +1 -1
  61. package/src/results/archive.ts +281 -0
  62. package/src/results/record.ts +166 -0
  63. package/src/results/retrieval.ts +142 -0
  64. package/src/results/types.ts +128 -0
  65. package/src/telemetry/contract.ts +1 -1
  66. package/src/tools/council.ts +40 -23
  67. package/src/tools/delegate-tool.ts +50 -14
  68. package/src/tools/delegate.ts +13 -6
  69. package/src/tools/exocom-collaboration-input.ts +168 -0
  70. package/src/tools/exocom-collaboration.ts +315 -0
  71. package/src/tools/exocom-profile.ts +102 -0
  72. package/src/tools/exocom.ts +20 -3
  73. package/src/tools/flow.ts +24 -8
  74. package/src/tools/intercom-tool.ts +98 -14
  75. package/src/tools/models.ts +15 -0
  76. package/src/tools/monitor.ts +20 -1
  77. package/src/tools/read-result.ts +52 -0
  78. package/src/tools/timer.ts +23 -2
  79. package/src/ui/agent-overlay.ts +11 -3
  80. package/src/ui/agent-tree.ts +43 -1
  81. package/src/ui/literal-tool-result.ts +1 -1
  82. package/src/ui/model-picker.ts +1 -1
  83. package/src/ui/orchestration-launch.ts +49 -0
  84. package/src/ui/session-cards.ts +191 -14
@@ -2,7 +2,8 @@
2
2
  * Supervisor-side broker lifecycle (spec B1-B7): the ONE cross-process host every child-engine
3
3
  * leg connects to. Started lazily on the first child-engine build; a failed bind is reported to
4
4
  * the user ONCE, children built while it is failed spawn without a bus endpoint (so they never
5
- * burn the connect backoff against a dead socket), and the next build retries.
5
+ * burn the connect backoff against a dead socket), and the next build retries. Result readers
6
+ * use the separate strict awaited preflight on this SAME host, never bus-less degradation.
6
7
  */
7
8
 
8
9
  import { mkdirSync } from "node:fs";
@@ -11,6 +12,7 @@ import { dirname } from "node:path";
11
12
  import type { EngineAdapterBroker } from "../engine/adapter.ts";
12
13
  import { type BrokerHost, startBrokerHost } from "../bus/broker/host.ts";
13
14
  import type { InProcessBus } from "../bus/inproc.ts";
15
+ import { RESULT_READ_TIMEOUT_MS, type PreparedResultReader, type ScopedReader } from "../bus/result-reader.ts";
14
16
 
15
17
  export interface SupervisorBrokerDeps {
16
18
  bus: InProcessBus;
@@ -49,6 +51,8 @@ export class SupervisorBroker {
49
51
  private readonly expected = new Set<string>();
50
52
  private readonly preHostSteers = new Map<string, string[]>();
51
53
  private readonly deps: SupervisorBrokerDeps;
54
+ private generation = 0;
55
+ private readonly preparingReaders = new Set<() => void>();
52
56
 
53
57
  constructor(deps: SupervisorBrokerDeps) {
54
58
  this.deps = deps;
@@ -82,6 +86,7 @@ export class SupervisorBroker {
82
86
  }
83
87
  }
84
88
  const start = this.deps.startHost ?? startBrokerHost;
89
+ const generation = this.generation;
85
90
  this.promise = start({
86
91
  bus: this.deps.bus,
87
92
  supervisorHandle: this.deps.supervisorHandle,
@@ -90,6 +95,7 @@ export class SupervisorBroker {
90
95
  });
91
96
  this.promise.then(
92
97
  (h) => {
98
+ if (generation !== this.generation) return; // close() owns the late host's teardown
93
99
  this.hostRef = h;
94
100
  this.lastError = undefined;
95
101
  this.warned = false;
@@ -98,6 +104,7 @@ export class SupervisorBroker {
98
104
  this.preHostSteers.clear();
99
105
  },
100
106
  (err) => {
107
+ if (generation !== this.generation) return;
101
108
  this.promise = undefined; // a later build retries
102
109
  this.lastError = err instanceof Error ? err.message : String(err);
103
110
  const dropped = [...this.preHostSteers.values()].reduce((n, texts) => n + texts.length, 0);
@@ -112,6 +119,46 @@ export class SupervisorBroker {
112
119
  );
113
120
  }
114
121
 
122
+ /** Strict reader preflight, unlike ordinary bus-less degradation. Call ensure(endpoint)
123
+ * first, then await this BEFORE adapter registration/spawn. The owner must revoke on
124
+ * settlement/spawn failure and close this broker when the owning session changes. */
125
+ async prepareResultReader(handle: string, reader: ScopedReader, signal?: AbortSignal): Promise<PreparedResultReader> {
126
+ const generation = this.generation;
127
+ let revoked = false;
128
+ const revoke = (): void => {
129
+ if (revoked) return;
130
+ revoked = true;
131
+ try { reader.revoke(); } catch { /* fixed diagnostic, even if cleanup throws */ }
132
+ };
133
+ const fail = (): Error => new Error("Result reader preparation failed.");
134
+ const hostPromise = this.promise;
135
+ if (!hostPromise || signal?.aborted) { revoke(); throw fail(); }
136
+ return new Promise<PreparedResultReader>((resolve, reject) => {
137
+ let settled = false;
138
+ const cleanup = (): void => {
139
+ settled = true;
140
+ clearTimeout(timer);
141
+ this.preparingReaders.delete(cancel);
142
+ signal?.removeEventListener("abort", cancel);
143
+ };
144
+ const cancel = (): void => {
145
+ if (settled) return;
146
+ revoke(); cleanup(); reject(fail());
147
+ };
148
+ const timer = setTimeout(cancel, RESULT_READ_TIMEOUT_MS);
149
+ timer.unref?.();
150
+ this.preparingReaders.add(cancel);
151
+ signal?.addEventListener("abort", cancel, { once: true });
152
+ void hostPromise.then(async (host) => {
153
+ if (settled) return;
154
+ const grant = await host.prepareResultReader(handle, { read: (request, s) => reader.read(request, s), revoke }, signal);
155
+ if (settled || generation !== this.generation || signal?.aborted) { grant.revoke(); cancel(); return; }
156
+ cleanup();
157
+ resolve(grant);
158
+ }).catch(cancel);
159
+ });
160
+ }
161
+
115
162
  /** The adapter-facing deps, or undefined while the last start failed (children spawn bus-less). */
116
163
  adapterDeps(endpoint: string): EngineAdapterBroker | undefined {
117
164
  const failedBefore = this.state() === "failed";
@@ -141,6 +188,7 @@ export class SupervisorBroker {
141
188
  return true;
142
189
  },
143
190
  hasPendingAskFrom: (handle) => this.deps.bus.hasPendingAskFrom(handle),
191
+ prepareResultReader: (handle, reader, signal) => this.prepareResultReader(handle, reader, signal),
144
192
  };
145
193
  }
146
194
 
@@ -164,18 +212,22 @@ export class SupervisorBroker {
164
212
  }
165
213
 
166
214
  async close(): Promise<void> {
167
- if (this.promise) {
168
- try {
169
- const h = await this.promise;
170
- await h.close();
171
- } catch {
172
- /* best-effort — never block shutdown on a broker teardown error */
173
- }
174
- }
215
+ this.generation++;
216
+ for (const cancel of this.preparingReaders) cancel();
217
+ const host = this.hostRef;
218
+ const pending = this.promise;
175
219
  this.hostRef = undefined;
176
220
  this.promise = undefined;
221
+ this.lastError = undefined;
177
222
  this.peers.clear();
178
223
  this.expected.clear();
179
224
  this.preHostSteers.clear();
225
+ try {
226
+ // Invoke immediately: bound readers revoke before the first await.
227
+ if (host) await host.close();
228
+ else if (pending) await (await pending).close();
229
+ } catch {
230
+ /* best-effort — never block shutdown on a broker teardown error */
231
+ }
180
232
  }
181
233
  }
@@ -21,6 +21,7 @@ import { emptyUsage, type ProgressSnapshot } from "../engine/stream.ts";
21
21
  import type { AgentRunSpec, StrategyEngine } from "../orchestration/sdk.ts";
22
22
  import type { AgentResult } from "../orchestration/types.ts";
23
23
  import type { InProcessBus } from "../bus/inproc.ts";
24
+ import type { ResultArchiveView } from "../results/retrieval.ts";
24
25
 
25
26
  export interface EngineFactories {
26
27
  makeEngine: typeof makeEngine;
@@ -45,6 +46,10 @@ export interface BuildEngineDeps {
45
46
  runLimits: RunLimits;
46
47
  bus: InProcessBus;
47
48
  supervisorHandle: string;
49
+ /** The live terminal-result archive (a GETTER: the owning session can change across runs).
50
+ * Feeds both engines' reference-handoff preflight + scoped `read_result` readers.
51
+ * Absent ⇒ reference-handoff legs fail closed on both engines (no archive to page). */
52
+ resultArchive?: () => ResultArchiveView | undefined;
48
53
  /** Breadcrumb for a provider reroute (toast + log); absent ⇒ silent, as before. */
49
54
  onFallback?: (info: { from: string; to: string; agent: string }) => void;
50
55
  /** Injected git runner for the non-worktree files-changed report (tests only); defaults to
@@ -69,7 +74,7 @@ export function createBuildEngine(d: () => BuildEngineDeps): BuildEngine {
69
74
  const {
70
75
  agents, contractDefs, controller, host, config, personaConfigs, lastCtx, workerSpineText,
71
76
  engineFactories, makeBrokerDeps, userAgentDir, childPiSettingsEnv, runLimits: RUN_LIMITS,
72
- bus, supervisorHandle: SUPERVISOR, onFallback,
77
+ bus, supervisorHandle: SUPERVISOR, onFallback, resultArchive,
73
78
  } = d();
74
79
  const resolveAgent = (n: string): AgentConfig | undefined => agents.find((a) => a.name === n);
75
80
  // A named contract file (contracts/<name>.contract.json) wins; "default" is the built-in.
@@ -111,7 +116,7 @@ export function createBuildEngine(d: () => BuildEngineDeps): BuildEngine {
111
116
  const getBrokerDeps = (): EngineAdapterBroker | undefined => {
112
117
  if (!brokerDepsBuilt) {
113
118
  brokerDepsBuilt = true;
114
- const sessionId = (lastCtx as { sessionManager?: { getSessionId?: () => string } } | undefined)?.sessionManager?.getSessionId?.();
119
+ const sessionId = lastCtx?.sessionManager.getSessionId();
115
120
  if (config.broker && lastCtx && sessionId) brokerDepsMemo = makeBrokerDeps(lastCtx);
116
121
  }
117
122
  return brokerDepsMemo;
@@ -137,6 +142,9 @@ export function createBuildEngine(d: () => BuildEngineDeps): BuildEngine {
137
142
  if (onProgress) deps.childOptions.onProgress = onProgress;
138
143
  const brokerDeps = getBrokerDeps();
139
144
  if (brokerDeps) deps.broker = brokerDeps;
145
+ // Reference-handoff legs page the owning session's terminal archive (strict preflight
146
+ // + scoped reader); the getter is read at LEG time so a mid-run session switch is seen.
147
+ if (resultArchive !== undefined) deps.resultArchive = resultArchive;
140
148
  // Peer messaging obeys the persona's bus capability, and blocking asks are honoured
141
149
  // only for async runs — same guards as the inproc engine below (spec B7 / §4.9).
142
150
  const caps = controller.capabilities;
@@ -164,6 +172,7 @@ export function createBuildEngine(d: () => BuildEngineDeps): BuildEngine {
164
172
  // can't answer, so blocking there would deadlock; the tool downgrades to one-way).
165
173
  ideps.bus = bus;
166
174
  ideps.supervisorHandle = SUPERVISOR;
175
+ if (resultArchive !== undefined) ideps.resultArchive = resultArchive;
167
176
  if (controller.activePersona?.coaching) ideps.coaching = true;
168
177
  // Peer messaging obeys the persona's bus capability (canUseBus; Task: sibling peer comm).
169
178
  const caps = controller.capabilities;
@@ -187,6 +196,7 @@ export function createBuildEngine(d: () => BuildEngineDeps): BuildEngine {
187
196
  if (onProgress) deps.childOptions.onProgress = onProgress;
188
197
  const brokerDeps = getBrokerDeps();
189
198
  if (brokerDeps) deps.broker = brokerDeps;
199
+ if (resultArchive !== undefined) deps.resultArchive = resultArchive;
190
200
  // Peer messaging obeys the persona's bus capability, and blocking asks are honoured
191
201
  // only for async runs — same guards as the inproc engine above (spec B7 / §4.9).
192
202
  const caps = controller.capabilities;
@@ -248,9 +258,9 @@ export function createBuildEngine(d: () => BuildEngineDeps): BuildEngine {
248
258
  const preflight = await worktreePreflight(root, defaultGitExec);
249
259
  if (!preflight.ok) return isolatedWorktreeFailure(spec, preflight.error);
250
260
  try {
251
- return await withWorktree(root, defaultGitExec, async (dir) => {
261
+ return await withWorktree(root, defaultGitExec, async (dir, baseCommit) => {
252
262
  const result = await childEngineAt(dir).run({ ...spec, isolation: "none" }, perProgress, perSignal, perSteer);
253
- const artifact = await captureWorktreeArtifact(dir, defaultGitExec);
263
+ const artifact = await captureWorktreeArtifact(dir, baseCommit, defaultGitExec);
254
264
  const hasOutputArtifact = hasUnifiedDiff(result.output);
255
265
  if (!result.ok) {
256
266
  return artifact.ok && artifact.diff.trim().length > 0
@@ -314,38 +324,27 @@ export function createBuildEngine(d: () => BuildEngineDeps): BuildEngine {
314
324
  // The host's model-level availability snapshot, plus the exact active model. A provider's
315
325
  // credentials do not grant every model in its catalog. Extension-native bridges can be
316
326
  // absent from an auth snapshot: restore only their explicit registered model lists in that
317
- // case, never every inherited model under their provider id. Older facades without an
318
- // availability API retain their historical getAll() fallback.
327
+ // case, never every inherited model under their provider id.
319
328
  export function configuredModels(ctx: ExtensionContext): ModelCostMetadata[] {
320
329
  const reg = ctx.modelRegistry;
321
330
  const all = reg.getAll();
322
- let available: typeof all;
323
- try {
324
- available = reg.getAvailable();
325
- } catch {
326
- return all;
327
- }
331
+ const available = reg.getAvailable();
328
332
  const byRef = new Map(available.map((m) => [`${m.provider}\0${m.id}`, m]));
329
333
  if (ctx.model) byRef.set(`${ctx.model.provider}\0${ctx.model.id}`, ctx.model);
330
334
  const availableProviders = new Set(available.map((m) => m.provider));
331
335
  const declaredRefs = new Set<string>();
332
- let registered: readonly string[] = [];
333
- try {
334
- registered = reg.getRegisteredProviderIds?.() ?? [];
335
- } catch {
336
- /* a third-party/older registry facade may not expose extension provider ids */
337
- }
336
+ const registered = reg.getRegisteredProviderIds();
338
337
  for (const provider of registered) {
339
338
  // A populated snapshot can intentionally exclude individual models for this credential.
340
339
  if (availableProviders.has(provider)) continue;
341
340
  let native: ReturnType<typeof reg.getRegisteredNativeProvider>;
342
341
  try {
343
- native = reg.getRegisteredNativeProvider?.(provider);
342
+ native = reg.getRegisteredNativeProvider(provider);
344
343
  // An empty credential-filtered list is authoritative too; it need not be a stale snapshot.
345
- if (reg.getProvider?.(provider)?.filterModels || native?.filterModels) continue;
344
+ if (reg.getProvider(provider)?.filterModels || native?.filterModels) continue;
346
345
  } catch { continue; }
347
346
  try {
348
- const models = reg.getRegisteredProviderConfig?.(provider)?.models;
347
+ const models = reg.getRegisteredProviderConfig(provider)?.models;
349
348
  if (Array.isArray(models)) {
350
349
  for (const model of models) {
351
350
  if ((model.type === undefined || model.type === "chat") && typeof model.id === "string") declaredRefs.add(`${provider}\0${model.id}`);