@aeondave/pi-persona 1.15.2 → 1.16.0

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 (44) hide show
  1. package/AGENTS.md +30 -6
  2. package/CHANGELOG.md +82 -0
  3. package/README.md +27 -8
  4. package/SECURITY.md +42 -0
  5. package/docs/ARCHITECTURE.md +52 -7
  6. package/docs/EXPERIENCE.md +21 -5
  7. package/docs/README.md +2 -0
  8. package/docs/REFERENCE.md +84 -14
  9. package/docs/STRATEGIES.md +58 -31
  10. package/docs/TELEMETRY.md +8 -0
  11. package/package.json +11 -9
  12. package/src/bus/broker/paths.ts +3 -2
  13. package/src/core/brief.ts +1 -1
  14. package/src/core/nudge.ts +3 -4
  15. package/src/core/pi-compat.ts +3 -3
  16. package/src/engine/adapter.ts +11 -6
  17. package/src/engine/async.ts +12 -6
  18. package/src/engine/inproc.ts +14 -6
  19. package/src/extension/engine.ts +25 -4
  20. package/src/extension/hooks.ts +68 -21
  21. package/src/extension.ts +209 -49
  22. package/src/orchestration/model-retry.ts +32 -61
  23. package/src/orchestration/params.ts +27 -1
  24. package/src/orchestration/roster.ts +80 -3
  25. package/src/orchestration/sdk.ts +150 -36
  26. package/src/orchestration/strategies/compete.ts +14 -8
  27. package/src/orchestration/strategies/council-rounds.ts +8 -1
  28. package/src/orchestration/strategies/critic-loop.ts +25 -11
  29. package/src/orchestration/strategies/judge.ts +18 -9
  30. package/src/orchestration/strategies/magi.ts +42 -93
  31. package/src/orchestration/strategies/map.ts +41 -19
  32. package/src/orchestration/strategies/pipeline.ts +2 -2
  33. package/src/orchestration/strategies/synthesize.ts +18 -11
  34. package/src/orchestration/types.ts +15 -0
  35. package/src/persona/model-participants.ts +240 -0
  36. package/src/persona/orchestrate.ts +44 -2
  37. package/src/persona/persona.ts +86 -14
  38. package/src/telemetry/contract.ts +1 -1
  39. package/src/telemetry/producer.ts +241 -8
  40. package/src/tools/council.ts +36 -8
  41. package/src/tools/delegate-tool.ts +162 -24
  42. package/src/tools/intercom-tool.ts +119 -32
  43. package/src/ui/agent-overlay.ts +15 -6
  44. package/src/ui/agent-tree.ts +39 -2
@@ -23,7 +23,7 @@ import {
23
23
  wrapTextWithAnsi,
24
24
  } from "@earendil-works/pi-tui";
25
25
 
26
- import { type AgentNode, type AgentTree, type FlatRow, flattenTree, GLYPH, runningAnnotation } from "./agent-tree.ts";
26
+ import { type AgentNode, type AgentTree, type FlatRow, deriveWaitingNodeIds, flattenTree, GLYPH, runningAnnotation } from "./agent-tree.ts";
27
27
  import { LiveClock } from "./live-clock.ts";
28
28
  import { visibleWindow } from "./model-picker.ts";
29
29
  import { compactInlineText, OPEN_SEQUENCE_TAIL, sanitizeTerminalText } from "./presentation.ts";
@@ -54,6 +54,7 @@ export interface AgentRowInput {
54
54
  inner: number;
55
55
  now: number;
56
56
  stallMs: number;
57
+ waitingForSupervisor?: boolean;
57
58
  theme: Theme;
58
59
  }
59
60
 
@@ -68,7 +69,7 @@ export interface AgentRowInput {
68
69
  export function composeAgentRow(input: AgentRowInput): string {
69
70
  const { node, depth, selected, inner, now, stallMs, theme: t } = input;
70
71
  const indent = " ".repeat(depth);
71
- const clock = runningAnnotation(node, now, stallMs);
72
+ const clock = runningAnnotation(node, now, stallMs, input.waitingForSupervisor);
72
73
  // Reserve the clock's exact width plus its widest separator (" · ") up front — this portion
73
74
  // of the row is never truncated.
74
75
  const clockCols = clock ? visibleWidth(clock) + visibleWidth(" · ") : 0;
@@ -98,6 +99,8 @@ export interface AgentOverlayActions {
98
99
  onSteer?: (nodeId: string, text: string) => boolean;
99
100
  canSteer?: (nodeId: string) => boolean;
100
101
  canStop?: (nodeId: string) => boolean;
102
+ /** Live predicate for the exact engine handle behind a row. */
103
+ isWaitingForSupervisor?: (nodeId: string) => boolean;
101
104
  /** Stall threshold for the ⚠ badge; 0/omitted ⇒ no badge. */
102
105
  stallMs?: number;
103
106
  /** Clock; tests inject a fixed one. */
@@ -202,8 +205,11 @@ export class AgentOverlay extends Container {
202
205
  return Math.max(6, termRows() - 10);
203
206
  }
204
207
 
205
- private leafRows(): FlatRow[] {
206
- const snap = this.tree.snapshot();
208
+ private waitingNodeIds(nodes: readonly AgentNode[] = this.tree.snapshot()): Set<string> {
209
+ return deriveWaitingNodeIds(nodes, (id) => this.actions.isWaitingForSupervisor?.(id) === true);
210
+ }
211
+
212
+ private leafRows(snap: AgentNode[] = this.tree.snapshot()): FlatRow[] {
207
213
  const parents = new Set(snap.map((n) => n.parentId).filter((p): p is string => p !== undefined));
208
214
  return flattenTree(snap).filter((r) => !parents.has(r.node.id));
209
215
  }
@@ -256,8 +262,9 @@ export class AgentOverlay extends Container {
256
262
 
257
263
  private renderList(): void {
258
264
  const t = this.theme;
259
- const rows = flattenTree(this.tree.snapshot());
260
- const selected = this.selectedLeaf(this.leafRows())?.node;
265
+ const nodes = this.tree.snapshot();
266
+ const rows = flattenTree(nodes);
267
+ const selected = this.selectedLeaf(this.leafRows(nodes))?.node;
261
268
  this.addChild(new Text(t.fg("accent", t.bold("Agents")), 1, 0));
262
269
  this.addChild(new Spacer(1));
263
270
  if (rows.length === 0) {
@@ -265,6 +272,7 @@ export class AgentOverlay extends Container {
265
272
  } else {
266
273
  // Scroll window over the rows (a big fan-out must not overflow the screen);
267
274
  // the window follows the selection, ▲/▼ markers show what's off-screen.
275
+ const waitingIds = this.waitingNodeIds(nodes);
268
276
  const vp = this.listViewport();
269
277
  const selIdx = Math.max(0, rows.findIndex((r) => r.node.id === selected?.id));
270
278
  this.listScroll = visibleWindow(selIdx, vp, rows.length, this.listScroll);
@@ -278,6 +286,7 @@ export class AgentOverlay extends Container {
278
286
  inner: this.inner(),
279
287
  now: this.now(),
280
288
  stallMs: this.actions.stallMs ?? 0,
289
+ waitingForSupervisor: waitingIds.has(row.node.id),
281
290
  theme: t,
282
291
  });
283
292
  this.addChild(new Text(composed, 1, 0));
@@ -65,6 +65,8 @@ export interface RenderOptions {
65
65
  now?: number;
66
66
  /** Quiet time after which a running row shows the stall badge; 0/omitted ⇒ never. */
67
67
  stallMs?: number;
68
+ /** Live ids whose exact engine handle currently has a pending supervisor ask. */
69
+ waitingForSupervisor?: ReadonlySet<string>;
68
70
  }
69
71
 
70
72
  /** Whether `detail` is the literal "queued" marker a seeded-but-not-yet-live leg carries — shared
@@ -78,14 +80,43 @@ export function isQueuedMarker(detail: string | undefined): boolean {
78
80
  * for `stallMs`. Undefined for settled nodes, nodes without clock data, or a node still carrying
79
81
  * the "queued" marker — it was seeded ahead of the concurrency gate and has not started running
80
82
  * yet, so its clock (stamped at seed time) has nothing true to report. Pure. */
81
- export function runningAnnotation(node: Pick<AgentNode, "status" | "startedAt" | "lastAdvanceAt" | "detail">, now: number, stallMs: number): string | undefined {
83
+ export function runningAnnotation(node: Pick<AgentNode, "status" | "startedAt" | "lastAdvanceAt" | "detail">, now: number, stallMs: number, waitingForSupervisor = false): string | undefined {
82
84
  if (node.status !== "running" || node.startedAt === undefined) return undefined;
83
85
  if (isQueuedMarker(node.detail)) return undefined;
86
+ if (waitingForSupervisor) return "waiting for supervisor";
84
87
  const quietSince = node.lastAdvanceAt ?? node.startedAt;
85
88
  if (stallMs > 0 && now - quietSince >= stallMs) return `⚠ stalled ${formatDuration(now - quietSince)}`;
86
89
  return formatDuration(now - node.startedAt);
87
90
  }
88
91
 
92
+ /** Derive waiting rows from exact live-handle predicates. A running parent is waiting only when
93
+ * every live leaf below it is waiting; a progressing or stalled sibling keeps the parent honest. */
94
+ export function deriveWaitingNodeIds(nodes: readonly AgentNode[], isWaiting: (id: string) => boolean): Set<string> {
95
+ const children = new Map<string, AgentNode[]>();
96
+ for (const node of nodes) {
97
+ if (node.parentId === undefined) continue;
98
+ const list = children.get(node.parentId) ?? [];
99
+ list.push(node);
100
+ children.set(node.parentId, list);
101
+ }
102
+ const waitingLeaves = new Map<string, { hasLive: boolean; allWaiting: boolean }>();
103
+ const visit = (node: AgentNode): { hasLive: boolean; allWaiting: boolean } => {
104
+ if (node.status === "running" && isQueuedMarker(node.detail)) return { hasLive: false, allWaiting: false };
105
+ const descendants = (children.get(node.id) ?? []).map(visit).filter((state) => state.hasLive);
106
+ if (node.status !== "running") return { hasLive: descendants.length > 0, allWaiting: descendants.every((state) => state.allWaiting) };
107
+ if (descendants.length === 0) {
108
+ const waiting = isWaiting(node.id);
109
+ if (waiting) waitingLeaves.set(node.id, { hasLive: true, allWaiting: true });
110
+ return { hasLive: true, allWaiting: waiting };
111
+ }
112
+ const allWaiting = descendants.every((state) => state.allWaiting);
113
+ if (allWaiting) waitingLeaves.set(node.id, { hasLive: true, allWaiting: true });
114
+ return { hasLive: true, allWaiting };
115
+ };
116
+ for (const node of nodes) if (node.parentId === undefined) visit(node);
117
+ return new Set(waitingLeaves.keys());
118
+ }
119
+
89
120
  /** The tree patch for one progress snapshot: every snapshot is an advance; activity wins over
90
121
  * a bare token count as the visible detail. One helper for every progress site. */
91
122
  export function progressPatch(snap: { output?: string; activity?: string; tokens?: number }, now: number): AgentNodePatch {
@@ -121,7 +152,7 @@ export function renderAgentTree(nodes: AgentNode[], opts: RenderOptions = {}): s
121
152
  const branch = isRoot ? "" : isLast ? "└─ " : "├─ ";
122
153
  const label = safeInline(node.label) || safeInline(node.id) || "agent";
123
154
  const detailText = node.detail ? safeInline(node.detail) : "";
124
- const clock = opts.now !== undefined ? runningAnnotation(node, opts.now, opts.stallMs ?? 0) : undefined;
155
+ const clock = opts.now !== undefined ? runningAnnotation(node, opts.now, opts.stallMs ?? 0, opts.waitingForSupervisor?.has(node.id) ?? false) : undefined;
125
156
  const tail = [detailText, clock].filter((s): s is string => Boolean(s)).join(" · ");
126
157
  const detail = tail ? ` ${tail}` : "";
127
158
  lines.push(`${prefix}${branch}${GLYPH[node.status]} ${label}${detail}`);
@@ -246,6 +277,12 @@ export class AgentTree {
246
277
  update(id: string, patch: AgentNodePatch): void {
247
278
  const node = this.nodes.find((n) => n.id === id);
248
279
  if (!node) return;
280
+ // Parent progress clocks are refreshed on every child tick. They never emit a change;
281
+ // avoid cloning/comparing the whole node for that common clock-only path.
282
+ if (Object.keys(patch).every((key) => CLOCK_KEYS.has(key as keyof AgentNode))) {
283
+ applyPatch(node, patch);
284
+ return;
285
+ }
249
286
  const before = { ...node };
250
287
  applyPatch(node, patch);
251
288
  if (!sameNode(before, node)) this.emit({ type: "updated", node: { ...node } });