@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.
@@ -7,11 +7,8 @@
7
7
  import type { RunLimits } from "../core/capabilities.ts";
8
8
  import { makeRoster, type RosterMember } from "../orchestration/roster.ts";
9
9
  import {
10
- type AgentProgress,
11
- type AgentStatus,
12
10
  makeSDK,
13
11
  type SDKDeps,
14
- type SteerFn,
15
12
  type StrategyEngine,
16
13
  type StrategyInput,
17
14
  } from "../orchestration/sdk.ts";
@@ -72,14 +69,14 @@ export interface RunStrategyDeps {
72
69
  log?: (message: string) => void;
73
70
  /** Per-agent lifecycle, for live UI (which roster agent is running/done + its result).
74
71
  * `key` is a run-unique display id (disambiguates same-agent roster-role members). */
75
- onAgentStatus?: (agent: string, status: AgentStatus, result?: AgentResult, key?: string) => void;
72
+ onAgentStatus?: SDKDeps["onAgentStatus"];
76
73
  onAgentResult?: SDKDeps["onAgentResult"];
77
74
  /** Per-agent streaming progress (rolling output), for live UI. */
78
- onAgentProgress?: (agent: string, progress: AgentProgress, key?: string) => void;
75
+ onAgentProgress?: SDKDeps["onAgentProgress"];
79
76
  /** Called as each agent starts with a handle to abort just that agent (UI stop). */
80
- onAgentStart?: (agent: string, abort: () => void, key?: string) => void;
77
+ onAgentStart?: SDKDeps["onAgentStart"];
81
78
  /** Called once an agent is live with a handle to steer it (in-process engine only). */
82
- onAgentSteerable?: (agent: string, steer: SteerFn, key?: string) => void;
79
+ onAgentSteerable?: SDKDeps["onAgentSteerable"];
83
80
  /** See `SDKDeps.canSpawn` — every strategy spawn, not only `delegate`. */
84
81
  canSpawn?: (agent: string) => boolean;
85
82
  }
@@ -4,7 +4,7 @@ export const TELEMETRY_EVENT_NAME = "pi:telemetry" as const;
4
4
  export const LEGACY_TELEMETRY_EVENT_NAME = "pi-persona:telemetry" as const;
5
5
  export const LEGACY_TELEMETRY_VERSION = 1 as const;
6
6
  export const TELEMETRY_PRODUCER_ID = "pi-persona" as const;
7
- export const TELEMETRY_PRODUCER_VERSION = "1.17.4" as const;
7
+ export const TELEMETRY_PRODUCER_VERSION = "1.17.5" as const;
8
8
 
9
9
  export type InstanceStatus = "active" | "idle" | "stopped" | "stale" | (string & {});
10
10
  export type AgentKind = "subagent" | "delegate" | "council" | "flow" | "phase" | (string & {});
@@ -0,0 +1,34 @@
1
+ /** Bounded local-work rows. A staged plan is waiting data, never a running agent. */
2
+ import type { CompanionActivity } from "../core/companion.ts";
3
+ import type { AddNodeInput } from "./agent-tree.ts";
4
+
5
+ export const MAX_COMPANION_ACTIVITIES = 8;
6
+ interface ActivityRecord { event: CompanionActivity; startedAt: number }
7
+
8
+ function description(event: CompanionActivity): string {
9
+ const scope = `${event.target} memory`;
10
+ if (event.state === "staged") return `${scope} · proposal prepared; explicit apply required`;
11
+ if (event.state === "failed") return `${scope} · ${event.phase} failed; see the memory result`;
12
+ if (event.state === "done") return `${scope} · ${event.phase === "inspect" ? "inspection complete; ready for review" : event.phase === "apply" ? "staged plan applied" : "plan validation complete"}`;
13
+ return `${scope} · ${event.phase === "inspect" ? "inspecting" : event.phase === "stage" ? "validating consolidation" : "applying staged plan"}`;
14
+ }
15
+
16
+ export class ActivityRows {
17
+ private readonly records = new Map<string, ActivityRecord>();
18
+ accept(event: CompanionActivity, now: number): { node: AddNodeInput; removed: string[] } | undefined {
19
+ const id = `activity:pi-persona-mind:${event.activityId}`;
20
+ const existing = this.records.get(id);
21
+ if (existing && (existing.event.phase !== event.phase || existing.event.target !== event.target || existing.event.state !== "running")) return undefined;
22
+ const removed: string[] = [];
23
+ if (!existing && this.records.size >= MAX_COMPANION_ACTIVITIES) {
24
+ const victim = [...this.records].find(([, record]) => record.event.state !== "running");
25
+ if (!victim) return undefined; // never silently evict active local work
26
+ this.records.delete(victim[0]); removed.push(victim[0]);
27
+ }
28
+ const record = { event, startedAt: existing?.startedAt ?? now };
29
+ this.records.set(id, record);
30
+ const detail = description(event);
31
+ return { removed, node: { id, label: "Mind", kind: "activity", status: event.state === "running" ? "running" : event.state === "failed" ? "failed" : "done", detail, output: `${detail}.\nLocal memory operation; no sub-agent or model execution is implied.\nInspect the memory tool or command result for the full receipt.`, startedAt: record.startedAt, lastAdvanceAt: now } };
32
+ }
33
+ clear(): string[] { const ids = [...this.records.keys()]; this.records.clear(); return ids; }
34
+ }
@@ -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, deriveWaitingNodeIds, effortTag, flattenTree, isWakeSource, nodeGlyph, runningAnnotation, thinkingDetailLine } from "./agent-tree.ts";
26
+ import { type AgentNode, type AgentTree, type FlatRow, deriveWaitingNodeIds, effortTag, flattenTree, isWakeSource, isNonAgentNode, nodeGlyph, runningAnnotation, thinkingDetailLine } 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";
@@ -234,7 +234,7 @@ export class AgentOverlay extends Container {
234
234
  */
235
235
  private selectedLeaf(leaves: FlatRow[]): FlatRow | undefined {
236
236
  const anchored = this.selectedId !== undefined ? leaves.find((r) => r.node.id === this.selectedId) : undefined;
237
- const leaf = anchored ?? leaves.find((r) => !isWakeSource(r.node)) ?? leaves[0];
237
+ const leaf = anchored ?? leaves.find((r) => !isNonAgentNode(r.node)) ?? leaves[0];
238
238
  if (this.selectedId !== undefined && anchored === undefined && leaf !== undefined) this.aimLost = true;
239
239
  this.selectedId = leaf?.node.id;
240
240
  return leaf;
@@ -305,7 +305,7 @@ export class AgentOverlay extends Container {
305
305
  }
306
306
  this.addChild(new Spacer(1));
307
307
  const stoppable = selected !== undefined && this.canStopNode(selected);
308
- const steerable = selected !== undefined && selected.status === "running" && (this.actions.canSteer?.(selected.id) ?? false);
308
+ const steerable = selected !== undefined && !isNonAgentNode(selected) && selected.status === "running" && (this.actions.canSteer?.(selected.id) ?? false);
309
309
  this.addChild(new Text(t.fg("dim", `↑↓ navigate ⏎ open${this.hintLine("list", stoppable, steerable, this.stoppableIds(nodes).length)} esc close`), 1, 0));
310
310
  this.renderNotice();
311
311
  }
@@ -383,8 +383,9 @@ export class AgentOverlay extends Container {
383
383
  // A wake source is armed, not a live agent: it has no steer handle, so the agent-only "● live"
384
384
  // badge and the steer-unavailable hint below would both be wrong for it.
385
385
  const wake = isWakeSource(node);
386
+ const localActivity = node.kind === "activity";
386
387
  this.addChild(
387
- new Text(`${nodeGlyph(node)} ${t.fg("accent", t.bold(safeInline(node.label) || "agent"))}${live ? t.fg("success", wake ? " ● armed" : " ● live") : ""}`, 1, 0),
388
+ new Text(`${nodeGlyph(node)} ${t.fg("accent", t.bold(safeInline(node.label) || "agent"))}${live ? t.fg("success", localActivity ? " ● local work" : wake ? " ● armed" : " ● live") : ""}`, 1, 0),
388
389
  );
389
390
  if (node.detail) this.addChild(new Text(t.fg("dim", safeInline(node.detail)), 1, 0));
390
391
  // The drill-in has room for FULL effort names (the rows stay compact): an observed level
@@ -409,7 +410,7 @@ export class AgentOverlay extends Container {
409
410
  for (const line of all.slice(start, end)) this.addChild(new Text(t.fg("toolOutput", line), 1, 0));
410
411
  if (this.detailScroll > 0) this.addChild(new Text(t.fg("dim", `▼ ${this.detailScroll} newer`), 1, 0));
411
412
 
412
- const steerable = live && (this.actions.canSteer?.(node.id) ?? false);
413
+ const steerable = live && !isNonAgentNode(node) && (this.actions.canSteer?.(node.id) ?? false);
413
414
  const stoppable = this.canStopNode(node);
414
415
  if (this.steering && !steerable) this.steering = false; // agent finished mid-compose
415
416
  this.addChild(new Spacer(1));
@@ -421,7 +422,7 @@ export class AgentOverlay extends Container {
421
422
  } else {
422
423
  this.addChild(new Text(t.fg("dim", `esc back · ↑↓ scroll${this.hintLine("detail", stoppable, steerable)}`), 1, 0));
423
424
  this.renderNotice();
424
- if (live && !steerable && !wake) {
425
+ if (live && !steerable && !isNonAgentNode(node)) {
425
426
  this.addChild(new Text(t.fg("dim", "(steer unavailable: no live handle yet, or this engine/broker does not expose one)"), 1, 0));
426
427
  }
427
428
  }
@@ -456,7 +457,8 @@ export class AgentOverlay extends Container {
456
457
  } else if (hadNotice) this.refresh();
457
458
  return;
458
459
  }
459
- if (keyData === "s" && (this.actions.canSteer?.(this.detailId) ?? false)) {
460
+ const detailNode = this.tree.snapshot().find((node) => node.id === this.detailId);
461
+ if (keyData === "s" && detailNode && !isNonAgentNode(detailNode) && (this.actions.canSteer?.(this.detailId) ?? false)) {
460
462
  this.steering = true;
461
463
  this.steerBuffer = "";
462
464
  this.refresh();
@@ -492,7 +494,7 @@ export class AgentOverlay extends Container {
492
494
  // Steer straight from the list: drill into the selected agent with the
493
495
  // compose line already open (same gate as the detail view's `s`).
494
496
  const leaf = this.aimedLeaf(leaves);
495
- if (leaf && leaf.node.status === "running" && (this.actions.canSteer?.(leaf.node.id) ?? false)) {
497
+ if (leaf && !isNonAgentNode(leaf.node) && leaf.node.status === "running" && (this.actions.canSteer?.(leaf.node.id) ?? false)) {
496
498
  this.detailId = leaf.node.id;
497
499
  this.detailScroll = 0;
498
500
  this.steering = true;
@@ -512,8 +514,8 @@ export class AgentOverlay extends Container {
512
514
  /** Whether an agent can be stopped right now: it must be running, and the caller's `canStop`
513
515
  * gate (defaulting to "yes" when omitted) must agree. Shared by both hints and `tryStop`, so
514
516
  * the hint that advertises "x stop" and the handler that actually gates it never diverge. */
515
- private canStopNode(node: Pick<AgentNode, "id" | "status">): boolean {
516
- return node.status === "running" && (this.actions.canStop?.(node.id) ?? true);
517
+ private canStopNode(node: Pick<AgentNode, "id" | "status" | "kind">): boolean {
518
+ return node.kind !== "activity" && node.status === "running" && (this.actions.canStop?.(node.id) ?? true);
517
519
  }
518
520
 
519
521
  /** The agents `X` would stop: every leaf that `x` could stop, wake sources excluded. `X` means
@@ -521,7 +523,7 @@ export class AgentOverlay extends Container {
521
523
  * only by an explicit `x` on its own row. One list feeds the hint count, the confirmation prompt
522
524
  * and the bulk stop, so the three always agree. */
523
525
  private stoppableIds(snap: AgentNode[] = this.tree.snapshot()): string[] {
524
- return this.leafRows(snap).filter((r) => !isWakeSource(r.node) && this.canStopNode(r.node)).map((r) => r.node.id);
526
+ return this.leafRows(snap).filter((r) => !isNonAgentNode(r.node) && this.canStopNode(r.node)).map((r) => r.node.id);
525
527
  }
526
528
 
527
529
  /** The trailing "x stop"/"X stop all"/"s steer" hint fragment for an action line — the list's
@@ -15,7 +15,7 @@ import { formatUsage } from "./usage.ts";
15
15
  */
16
16
 
17
17
  export type AgentNodeStatus = "running" | "done" | "failed" | "stopped";
18
- export type AgentNodeKind = "subagent" | "delegate" | "council" | "flow" | "phase" | "monitor" | "timer";
18
+ export type AgentNodeKind = "subagent" | "delegate" | "council" | "flow" | "phase" | "monitor" | "timer" | "activity";
19
19
 
20
20
  export interface AgentNode {
21
21
  id: string;
@@ -78,10 +78,16 @@ export function isWakeSource(node: Pick<AgentNode, "kind">): boolean {
78
78
  return node.kind === "monitor" || node.kind === "timer";
79
79
  }
80
80
 
81
+ /** Local companion work shares the view but is neither an agent nor a wake source. */
82
+ export function isNonAgentNode(node: Pick<AgentNode, "kind">): boolean {
83
+ return isWakeSource(node) || node.kind === "activity";
84
+ }
85
+
81
86
  /** The kind-aware glyph shared by every surface (tree, overlay): a running timer is ⏰, a running
82
87
  * monitor ◉; everything else, settled wake rows included, uses the ordinary status glyph. */
83
88
  export function nodeGlyph(node: Pick<AgentNode, "status" | "kind">): string {
84
89
  if (node.status === "running") {
90
+ if (node.kind === "activity") return "◈";
85
91
  if (node.kind === "timer") return "⏰";
86
92
  if (node.kind === "monitor") return "◉";
87
93
  }
@@ -136,6 +142,7 @@ export function isQueuedMarker(detail: string | undefined): boolean {
136
142
  export function runningAnnotation(node: Pick<AgentNode, "status" | "startedAt" | "lastAdvanceAt" | "detail" | "kind" | "dueAt">, now: number, stallMs: number, waitingForSupervisor = false): string | undefined {
137
143
  if (node.status !== "running" || node.startedAt === undefined) return undefined;
138
144
  if (isQueuedMarker(node.detail)) return undefined;
145
+ if (node.kind === "activity") return formatDuration(now - node.startedAt);
139
146
  if (waitingForSupervisor) return "waiting for supervisor";
140
147
  if (node.dueAt !== undefined) {
141
148
  const left = node.dueAt - now;
@@ -236,6 +243,8 @@ function wakeDigestLine(hidden: readonly AgentNode[], now: number | undefined):
236
243
  const monitors = hidden.filter((node) => node.kind === "monitor").length;
237
244
  const timers = hidden.filter((node) => node.kind === "timer").length;
238
245
  const parts: string[] = [];
246
+ const activities = hidden.filter((node) => node.kind === "activity");
247
+ if (activities.length > 0) parts.push(`◈ Mind · ${safeInline(activities[activities.length - 1]!.detail ?? "local memory work")}`);
239
248
  if (monitors > 0) parts.push(`${nodeGlyph({ status: "running", kind: "monitor" })} ${monitors} monitor${monitors === 1 ? "" : "s"}`);
240
249
  if (timers > 0) parts.push(`${nodeGlyph({ status: "running", kind: "timer" })} ${timers} timer${timers === 1 ? "" : "s"}`);
241
250
  if (now !== undefined) {
@@ -277,7 +286,7 @@ export function renderAgentTreeSummary(nodes: AgentNode[], maxRows = 8, opts: Re
277
286
  // same parent→children order, so a row's flat index doubles as its position in the rendered list.
278
287
  const flat = flattenTree(nodes);
279
288
  const hiddenWake = (from: number): AgentNode[] =>
280
- flat.slice(from).map((row) => row.node).filter((node) => isWakeSource(node) && node.status === "running");
289
+ flat.slice(from).map((row) => row.node).filter((node) => node.kind === "activity" || (isWakeSource(node) && node.status === "running"));
281
290
  let visibleCount = Math.max(0, limit - (failureLine !== undefined ? 2 : 1));
282
291
  let hidden = hiddenWake(visibleCount);
283
292
  if (hidden.length > 0) {
@@ -300,9 +309,9 @@ export interface AddNodeInput {
300
309
  status?: AgentNodeStatus;
301
310
  kind?: AgentNodeKind;
302
311
  agent?: string;
303
- model?: string;
304
- thinking?: ThinkingLevel;
305
- thinkingEffective?: ThinkingLevel;
312
+ model?: string | undefined;
313
+ thinking?: ThinkingLevel | undefined;
314
+ thinkingEffective?: ThinkingLevel | undefined;
306
315
  detail?: string;
307
316
  output?: string;
308
317
  startedAt?: number;
@@ -314,9 +323,9 @@ export interface AgentNodePatch {
314
323
  status?: AgentNodeStatus;
315
324
  kind?: AgentNodeKind;
316
325
  agent?: string;
317
- model?: string;
318
- thinking?: ThinkingLevel;
319
- thinkingEffective?: ThinkingLevel;
326
+ model?: string | undefined;
327
+ thinking?: ThinkingLevel | undefined;
328
+ thinkingEffective?: ThinkingLevel | undefined;
320
329
  detail?: string;
321
330
  output?: string;
322
331
  startedAt?: number;
@@ -330,9 +339,19 @@ function applyPatch(node: AgentNode, patch: AgentNodePatch): void {
330
339
  if (patch.status) node.status = patch.status;
331
340
  if (patch.kind !== undefined) node.kind = patch.kind;
332
341
  if (patch.agent !== undefined) node.agent = patch.agent;
333
- if (patch.model !== undefined) node.model = patch.model;
334
- if (patch.thinking !== undefined) node.thinking = patch.thinking;
335
- if (patch.thinkingEffective !== undefined) node.thinkingEffective = patch.thinkingEffective;
342
+ // Omission preserves metadata; explicit undefined clears a previous attempt's intent/observation.
343
+ if (Object.hasOwn(patch, "model")) {
344
+ if (patch.model === undefined) delete node.model;
345
+ else node.model = patch.model;
346
+ }
347
+ if (Object.hasOwn(patch, "thinking")) {
348
+ if (patch.thinking === undefined) delete node.thinking;
349
+ else node.thinking = patch.thinking;
350
+ }
351
+ if (Object.hasOwn(patch, "thinkingEffective")) {
352
+ if (patch.thinkingEffective === undefined) delete node.thinkingEffective;
353
+ else node.thinkingEffective = patch.thinkingEffective;
354
+ }
336
355
  if (patch.detail !== undefined) node.detail = patch.detail;
337
356
  if (patch.output !== undefined) node.output = patch.output;
338
357
  if (patch.startedAt !== undefined) node.startedAt = patch.startedAt;
@@ -0,0 +1,40 @@
1
+ /** Model intent for live strategy rows, resolved from exact attempt identity and frozen run settings. */
2
+ import { isThinkingLevel, type ThinkingLevel } from "../core/types.ts";
3
+ import type { AgentAttemptIdentity } from "../orchestration/agent-attempt.ts";
4
+ import type { AgentResult } from "../orchestration/types.ts";
5
+ import { participantKey } from "../persona/model-participants.ts";
6
+
7
+ export interface StrategyModels {
8
+ readonly assigned: Readonly<Record<string, string>>;
9
+ readonly agents: Readonly<Record<string, string>>;
10
+ readonly sessionModel?: string;
11
+ }
12
+
13
+ export function snapshotStrategyModels(input: { assigned?: Record<string, string>; agents: readonly { name: string; model?: string }[]; sessionModel?: string }): StrategyModels {
14
+ return Object.freeze({ assigned: Object.freeze({ ...input.assigned }),
15
+ agents: Object.freeze(Object.fromEntries(input.agents.flatMap((a) => a.model ? [[a.name, a.model]] : []))),
16
+ ...(input.sessionModel ? { sessionModel: input.sessionModel } : {}) });
17
+ }
18
+
19
+ export interface StrategyLegMetadata {
20
+ agent: string;
21
+ model: string | undefined;
22
+ thinking: ThinkingLevel | undefined;
23
+ thinkingEffective: ThinkingLevel | undefined;
24
+ }
25
+
26
+ /** A bare observed id preserves a known provider only when it exactly names the same requested id. */
27
+ function observedModel(observed: string | undefined, requested: string | undefined): string | undefined {
28
+ const value = observed?.trim();
29
+ if (!value) return requested;
30
+ if (!value.includes("/") && requested?.includes("/") && requested.slice(requested.indexOf("/") + 1) === value) return requested;
31
+ return value;
32
+ }
33
+
34
+ export function strategyLegMetadata(identity: AgentAttemptIdentity, models: StrategyModels, result?: AgentResult): StrategyLegMetadata {
35
+ const requested = identity.model ?? models.assigned[participantKey(identity.agent, identity.role)]
36
+ ?? models.assigned[identity.agent] ?? models.agents[identity.agent] ?? models.sessionModel;
37
+ return { agent: identity.agent, model: observedModel(result?.modelUsed, requested),
38
+ thinking: isThinkingLevel(result?.thinkingRequested) ? result.thinkingRequested : identity.thinking,
39
+ thinkingEffective: isThinkingLevel(result?.thinkingEffective) ? result.thinkingEffective : undefined };
40
+ }