@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.
- package/AGENTS.md +30 -6
- package/CHANGELOG.md +82 -0
- package/README.md +27 -8
- package/SECURITY.md +42 -0
- package/docs/ARCHITECTURE.md +52 -7
- package/docs/EXPERIENCE.md +21 -5
- package/docs/README.md +2 -0
- package/docs/REFERENCE.md +84 -14
- package/docs/STRATEGIES.md +58 -31
- package/docs/TELEMETRY.md +8 -0
- package/package.json +11 -9
- package/src/bus/broker/paths.ts +3 -2
- package/src/core/brief.ts +1 -1
- package/src/core/nudge.ts +3 -4
- package/src/core/pi-compat.ts +3 -3
- package/src/engine/adapter.ts +11 -6
- package/src/engine/async.ts +12 -6
- package/src/engine/inproc.ts +14 -6
- package/src/extension/engine.ts +25 -4
- package/src/extension/hooks.ts +68 -21
- package/src/extension.ts +209 -49
- package/src/orchestration/model-retry.ts +32 -61
- package/src/orchestration/params.ts +27 -1
- package/src/orchestration/roster.ts +80 -3
- package/src/orchestration/sdk.ts +150 -36
- package/src/orchestration/strategies/compete.ts +14 -8
- package/src/orchestration/strategies/council-rounds.ts +8 -1
- package/src/orchestration/strategies/critic-loop.ts +25 -11
- package/src/orchestration/strategies/judge.ts +18 -9
- package/src/orchestration/strategies/magi.ts +42 -93
- package/src/orchestration/strategies/map.ts +41 -19
- package/src/orchestration/strategies/pipeline.ts +2 -2
- package/src/orchestration/strategies/synthesize.ts +18 -11
- package/src/orchestration/types.ts +15 -0
- package/src/persona/model-participants.ts +240 -0
- package/src/persona/orchestrate.ts +44 -2
- package/src/persona/persona.ts +86 -14
- package/src/telemetry/contract.ts +1 -1
- package/src/telemetry/producer.ts +241 -8
- package/src/tools/council.ts +36 -8
- package/src/tools/delegate-tool.ts +162 -24
- package/src/tools/intercom-tool.ts +119 -32
- package/src/ui/agent-overlay.ts +15 -6
- package/src/ui/agent-tree.ts +39 -2
package/src/extension/hooks.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { buildExocomBrief, RUNTIME_CONTEXT_GUIDANCE } from "../core/brief.ts";
|
|
|
10
10
|
import { canCallTool, canDelegateTo, canFanOut } from "../core/capabilities.ts";
|
|
11
11
|
import type { PiPersonaConfig } from "../core/config.ts";
|
|
12
12
|
import { fenceUntrusted } from "../core/fence.ts";
|
|
13
|
-
import type
|
|
13
|
+
import { HANDOFF_TOOL_NAMES, type DelegationNudge, type PersistenceNudge } from "../core/nudge.ts";
|
|
14
14
|
import { buildSessionAnchor } from "../core/time.ts";
|
|
15
15
|
import { readClockSnapshot } from "./clock.ts";
|
|
16
16
|
import { type AsyncRun, type IdleCoalescingNotifier, type PeekWatcher } from "../engine/async.ts";
|
|
@@ -34,6 +34,7 @@ import type { PersonaConfigStore } from "../persona/config-store.ts";
|
|
|
34
34
|
import { TELEMETRY_EVENT_NAME, type TelemetryEvent } from "../telemetry/contract.ts";
|
|
35
35
|
import { TelemetryProducer, type TelemetryAgentInput } from "../telemetry/producer.ts";
|
|
36
36
|
import type { AgentTree, AgentNode } from "../ui/agent-tree.ts";
|
|
37
|
+
import { compactInlineText } from "../ui/presentation.ts";
|
|
37
38
|
import type { FailureKind } from "../orchestration/types.ts";
|
|
38
39
|
import type { ExocomInstall } from "../exocom/install.ts";
|
|
39
40
|
import type { SessionIdentity } from "./identity.ts";
|
|
@@ -113,8 +114,19 @@ export interface HookHost {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
export function installHooks(pi: ExtensionAPI, h: HookHost, exocom: ExocomInstall): void {
|
|
117
|
+
// Only parent ids are retained, never child output. Propagate through wrappers only when they
|
|
118
|
+
// actually relay a surrender marker; the outer model-visible result owns the note and card.
|
|
119
|
+
const pendingSurrenderRelays = new Set<string>();
|
|
120
|
+
const rememberSurrenderRelay = (id: string): void => {
|
|
121
|
+
if (pendingSurrenderRelays.size >= 256) {
|
|
122
|
+
const oldest = pendingSurrenderRelays.values().next().value;
|
|
123
|
+
if (oldest !== undefined) pendingSurrenderRelays.delete(oldest);
|
|
124
|
+
}
|
|
125
|
+
pendingSurrenderRelays.add(id);
|
|
126
|
+
};
|
|
116
127
|
// ── lifecycle ─────────────────────────────────────────────────────────────
|
|
117
128
|
pi.on("session_start", async (_event, ctx) => {
|
|
129
|
+
pendingSurrenderRelays.clear();
|
|
118
130
|
h.lastCtx = ctx;
|
|
119
131
|
h.delegationNudge.reset(); // a fresh session starts with a clean by-hand run
|
|
120
132
|
h.childUsage.reset();
|
|
@@ -174,29 +186,48 @@ export function installHooks(pi: ExtensionAPI, h: HookHost, exocom: ExocomInstal
|
|
|
174
186
|
const sessionId = sessionManager?.getSessionId?.();
|
|
175
187
|
if (sessionId) {
|
|
176
188
|
const eventBus = (pi as unknown as { events?: { emit?: (name: string, event: TelemetryEvent) => void } }).events;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
try {
|
|
190
|
+
h.telemetry = new TelemetryProducer({
|
|
191
|
+
agentDir: h.userAgentDir(),
|
|
192
|
+
cwd: ctx.cwd,
|
|
193
|
+
sessionId,
|
|
194
|
+
emit: (event) => eventBus?.emit?.(TELEMETRY_EVENT_NAME, event),
|
|
195
|
+
heartbeat: () => exocom.currentTelemetryInstance(h.lastCtx ?? ctx),
|
|
196
|
+
onError: (error: unknown) => {
|
|
197
|
+
// Retention and writer failures must be observable during normal operation too.
|
|
198
|
+
// A broken UI/logger must never turn best-effort telemetry into a session failure.
|
|
199
|
+
try {
|
|
200
|
+
const message = `pi-persona: telemetry: ${compactInlineText(error instanceof Error ? error.message : String(error), { maxChars: 400 })}`;
|
|
201
|
+
const current = h.lastCtx ?? ctx;
|
|
202
|
+
if (current.hasUI) current.ui.notify(message, "warning");
|
|
203
|
+
else process.stderr.write(`${message}\n`);
|
|
204
|
+
} catch { /* diagnostics are non-fatal */ }
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
h.telemetry.start(exocom.currentTelemetryInstance(ctx));
|
|
208
|
+
} catch (error) {
|
|
209
|
+
h.telemetry = undefined;
|
|
210
|
+
const message = "pi-persona: telemetry unavailable (safe session admission failed)";
|
|
211
|
+
try {
|
|
212
|
+
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
213
|
+
else process.stderr.write(`${message}\n`);
|
|
214
|
+
} catch { /* admission diagnostics are non-fatal too */ }
|
|
215
|
+
}
|
|
216
|
+
if (h.telemetry) {
|
|
217
|
+
// Re-seeded with the replay, so the dedupe below always mirrors what THIS producer last
|
|
218
|
+
// published — a session start that follows no teardown carries no stale projection.
|
|
219
|
+
h.lastAgentProjection.clear();
|
|
220
|
+
for (const node of h.agentTree.snapshot()) {
|
|
221
|
+
h.lastAgentProjection.set(node.id, JSON.stringify(h.telemetryAgent(node)));
|
|
222
|
+
h.telemetry.publishAgentAdded(h.telemetryAgent(node));
|
|
223
|
+
}
|
|
224
|
+
exocom.publishTelemetryPeers();
|
|
194
225
|
}
|
|
195
|
-
exocom.publishTelemetryPeers();
|
|
196
226
|
}
|
|
197
227
|
});
|
|
198
228
|
|
|
199
229
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
230
|
+
pendingSurrenderRelays.clear();
|
|
200
231
|
h.lastCtx = ctx;
|
|
201
232
|
h.disposed = true; // gate any late async-run onComplete from touching the next session's instance
|
|
202
233
|
h.deferredOrchestrations.length = 0;
|
|
@@ -246,6 +277,7 @@ export function installHooks(pi: ExtensionAPI, h: HookHost, exocom: ExocomInstal
|
|
|
246
277
|
// idle window first; its replay starts the next run and the remaining FIFO entries follow one per
|
|
247
278
|
// settlement. With no deferred input, flush ordinary wakes immediately.
|
|
248
279
|
pi.on("agent_settled", async () => {
|
|
280
|
+
pendingSurrenderRelays.clear();
|
|
249
281
|
if (h.lastCtx) h.telemetry?.publish("instance.updated", { ...exocom.currentTelemetryInstance(h.lastCtx), status: "idle" });
|
|
250
282
|
// Pi's `prepareToolCall` returns {kind:"immediate"} when the abort signal is already set
|
|
251
283
|
// after beforeToolCall — skipping afterToolCall, and so our tool_result hook. Those calls
|
|
@@ -625,16 +657,31 @@ export function installHooks(pi: ExtensionAPI, h: HookHost, exocom: ExocomInstal
|
|
|
625
657
|
if (!h.config.nudge) return errorPatch;
|
|
626
658
|
// Only a supervisor that CAN delegate is nudged to — a persona without the tool can't act on it.
|
|
627
659
|
if (!h.controller.capabilities?.tools.has("delegate")) return errorPatch;
|
|
660
|
+
const text = event.content.reduce((s, c) => (c.type === "text" ? s + c.text : s), "");
|
|
661
|
+
const relayed = pendingSurrenderRelays.delete(event.toolCallId);
|
|
662
|
+
const surrender = relayed ? h.persistenceNudge.scan(text) : h.persistenceNudge.observe(event.toolName, text);
|
|
663
|
+
// A NESTED call (a codemode script's `ctx.executeTool`) is a real execution — the gate, the
|
|
664
|
+
// telemetry above and the error patch all already ran — but its content is not a model-facing
|
|
665
|
+
// tool result. The caller chooses what to print or discard; only that outer output is charged
|
|
666
|
+
// to the context-burn run. Appending a note here would modify programmatic data and could
|
|
667
|
+
// create an invisible reminder. `parentToolCallId` is the host's explicit marker; `<parent>/<n>`
|
|
668
|
+
// id shape is never inferred from the id string.
|
|
669
|
+
if (typeof event.parentToolCallId === "string" && event.parentToolCallId.length > 0) {
|
|
670
|
+
if (surrender) rememberSurrenderRelay(event.parentToolCallId);
|
|
671
|
+
// A hand-off that actually landed ends the run even when a script issued it, so the reset is
|
|
672
|
+
// NOT dropped here. A FAILED one does not reset, and its repair note would be invisible:
|
|
673
|
+
// the next model-visible result is what reports the streak.
|
|
674
|
+
if (!errorPatch && event.isError !== true && HANDOFF_TOOL_NAMES.has(event.toolName)) h.delegationNudge.reset();
|
|
675
|
+
return errorPatch;
|
|
676
|
+
}
|
|
628
677
|
const notes: string[] = [];
|
|
629
678
|
// Grinding-by-hand reminder: a RUN of substantive hands-on commands on the supervisor's own
|
|
630
679
|
// tools (delegate/council reset the run). `size` classifies substantive vs glue + fat dump.
|
|
631
|
-
const text = event.content.reduce((s, c) => (c.type === "text" ? s + c.text : s), "");
|
|
632
680
|
const size = text.length;
|
|
633
681
|
const sweepNote = h.delegationNudge.observe(event.toolName, size, !errorPatch && event.isError !== true, text);
|
|
634
682
|
if (sweepNote) notes.push(sweepNote);
|
|
635
683
|
// Premature-surrender reminder: a delegated leg that came back BLOCKED/UNKNOWN (delegate/council
|
|
636
684
|
// results only; because delegate/council reset the run the two never fire on one event).
|
|
637
|
-
const surrender = h.persistenceNudge.observe(event.toolName, text);
|
|
638
685
|
if (surrender) notes.push(surrender);
|
|
639
686
|
if (notes.length === 0) return errorPatch;
|
|
640
687
|
const joined = notes.join("\n\n");
|
package/src/extension.ts
CHANGED
|
@@ -96,13 +96,18 @@ import { type FlowSpec, flowHash, parseFlow, verifyFlowRefs } from "./orchestrat
|
|
|
96
96
|
import { journalFileName, journalWriter, readJournal } from "./orchestration/flow-journal.ts";
|
|
97
97
|
import { runFlow } from "./orchestration/flow-run.ts";
|
|
98
98
|
import type { FlowOutcome } from "./orchestration/flow-run.ts";
|
|
99
|
-
import { type RosterMember, rosterNodeKeys, rosterSpec } from "./orchestration/roster.ts";
|
|
99
|
+
import { type RosterMember, roleHint, rosterNodeKeys, rosterSpec } from "./orchestration/roster.ts";
|
|
100
100
|
import type { AgentProgress, AgentStatus, SteerFn } from "./orchestration/sdk.ts";
|
|
101
101
|
import { knownParams, strategyNames } from "./orchestration/strategy.ts";
|
|
102
102
|
import { compactMemberResult } from "./orchestration/render.ts";
|
|
103
103
|
import type { AgentResult, FailureKind } from "./orchestration/types.ts";
|
|
104
104
|
import { type ModelHandle, PersonaController, type PersonaHost } from "./persona/controller.ts";
|
|
105
|
-
import { resolveStrategyName, runPersonaStrategy } from "./persona/orchestrate.ts";
|
|
105
|
+
import { resolveStrategyName, resolveOrchestrationMembers, runPersonaStrategy } from "./persona/orchestrate.ts";
|
|
106
|
+
import {
|
|
107
|
+
collectModelParticipants,
|
|
108
|
+
pendingParticipants,
|
|
109
|
+
type ModelParticipant,
|
|
110
|
+
} from "./persona/model-participants.ts";
|
|
106
111
|
import { expandCouncilPreset, type OrchestrationGrammar, type Persona } from "./persona/persona.ts";
|
|
107
112
|
import { bundledSpinePath, bundledWorkerSpinePath, readSpineFile, resolveSpine, type SpineSources } from "./persona/spine.ts";
|
|
108
113
|
import { readLastPersona, writeLastPersona } from "./persona/state.ts";
|
|
@@ -119,7 +124,7 @@ import { shortModel } from "./tools/delegate.ts";
|
|
|
119
124
|
import { formatInbox } from "./tools/intercom.ts";
|
|
120
125
|
import { renderTimerFire, TimerScheduler, type TimerEntry } from "./core/timer.ts";
|
|
121
126
|
import { AgentOverlay } from "./ui/agent-overlay.ts";
|
|
122
|
-
import { type AddNodeInput, type AgentNode, type AgentTreeChange, AgentTree, type AgentNodeStatus, isQueuedMarker, progressPatch, renderAgentTreeSummary } from "./ui/agent-tree.ts";
|
|
127
|
+
import { type AddNodeInput, type AgentNode, type AgentTreeChange, AgentTree, type AgentNodeStatus, deriveWaitingNodeIds, isQueuedMarker, progressPatch, renderAgentTreeSummary } from "./ui/agent-tree.ts";
|
|
123
128
|
import { LiveClock } from "./ui/live-clock.ts";
|
|
124
129
|
import { filterModels, ModelPicker, orderModelRefs } from "./ui/model-picker.ts";
|
|
125
130
|
import { compactInlineText, compactVisibleText, sanitizeTerminalText } from "./ui/presentation.ts";
|
|
@@ -188,9 +193,14 @@ export function announceAsyncRunSettlement(
|
|
|
188
193
|
enqueue: (run: AsyncRun) => void,
|
|
189
194
|
): void {
|
|
190
195
|
const id = compactInlineText(run.id, { maxChars: 80 }) || "run";
|
|
191
|
-
const agent = compactInlineText(run.agent, { maxChars: 80 }) || "agent";
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
const agent = compactInlineText(run.label ?? run.agent, { maxChars: 80 }) || compactInlineText(run.agent, { maxChars: 80 }) || "agent";
|
|
197
|
+
try {
|
|
198
|
+
if (run.status === "done") notify?.(`async run ${id} (${agent}) completed`, "info");
|
|
199
|
+
else if (run.status === "stopped") notify?.(`async run ${id} (${agent}) stopped`, "info");
|
|
200
|
+
else if (run.status === "failed") notify?.(`async run ${id} (${agent}) failed: ${compactInlineText(run.error ?? "(no detail)", { maxChars: 240 })}`, "error");
|
|
201
|
+
} catch {
|
|
202
|
+
// Cosmetic notification only: a broken UI must never swallow the terminal report.
|
|
203
|
+
}
|
|
194
204
|
enqueue(run);
|
|
195
205
|
}
|
|
196
206
|
|
|
@@ -636,6 +646,12 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
636
646
|
|
|
637
647
|
// node id → steer that one agent (in-process engine only): inject a live user message.
|
|
638
648
|
const steerRegistry = new Map<string, SteerFn>();
|
|
649
|
+
function waitingTreeIds(nodes: readonly AgentNode[] = agentTree.snapshot()): Set<string> {
|
|
650
|
+
return deriveWaitingNodeIds(nodes, (id) => steerRegistry.get(id)?.isWaitingForSupervisor?.() === true);
|
|
651
|
+
}
|
|
652
|
+
function waitingAsyncRunIds(): Set<string> {
|
|
653
|
+
return new Set(tracker.running().filter((run) => steerRegistry.get(`async:${run.id}`)?.isWaitingForSupervisor?.() === true).map((run) => run.id));
|
|
654
|
+
}
|
|
639
655
|
const clearSteers = (prefix: string): void => {
|
|
640
656
|
for (const k of [...steerRegistry.keys()]) if (k === prefix || k.startsWith(`${prefix}/`)) steerRegistry.delete(k);
|
|
641
657
|
};
|
|
@@ -670,7 +686,7 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
670
686
|
function renderAgentWidget(nodes: readonly AgentNode[] = agentTree.snapshot()): void {
|
|
671
687
|
if (!lastCtx) return;
|
|
672
688
|
const empty = nodes.length === 0;
|
|
673
|
-
const lines = empty ? undefined : renderAgentTreeSummary([...nodes], 8, { now: Date.now(), stallMs: STALL_FLAG_MS });
|
|
689
|
+
const lines = empty ? undefined : renderAgentTreeSummary([...nodes], 8, { now: Date.now(), stallMs: STALL_FLAG_MS, waitingForSupervisor: waitingTreeIds(nodes) });
|
|
674
690
|
const status = empty ? undefined : String(inFlightAgentCount(nodes));
|
|
675
691
|
try {
|
|
676
692
|
if (!widgetLinesInitialized || !sameWidgetLines(lastWidgetLines, lines)) {
|
|
@@ -783,6 +799,7 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
783
799
|
canSteer: (id) => steerRegistry.has(id),
|
|
784
800
|
canStop: (id) => stopRegistry.has(id),
|
|
785
801
|
stallMs: STALL_FLAG_MS,
|
|
802
|
+
isWaitingForSupervisor: (id) => steerRegistry.get(id)?.isWaitingForSupervisor?.() === true,
|
|
786
803
|
}),
|
|
787
804
|
// Near-fullscreen: watching sub-agents work is a reading surface, not a popup.
|
|
788
805
|
{ overlay: true, overlayOptions: { width: "90%", maxHeight: "90%" } },
|
|
@@ -813,7 +830,20 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
813
830
|
return readPersonaConfigs(configFile);
|
|
814
831
|
};
|
|
815
832
|
let personaConfigs: PersonaConfigStore = {};
|
|
816
|
-
|
|
833
|
+
// A participant is asked for a model at most ONCE per session — dismissal included, so a
|
|
834
|
+
// closed picker never becomes a nag. Keyed by persona + assignment key, NOT by persona: a new
|
|
835
|
+
// roster (or a new strategy's arbiter) is a genuinely new participant and may ask the first time
|
|
836
|
+
// it appears, while everyone already known stays silent.
|
|
837
|
+
const promptedParticipants = new Set<string>();
|
|
838
|
+
/** The subset of this run's participants already asked this session, keyed for the pure
|
|
839
|
+
* `pendingParticipants` gate (which speaks bare assignment keys). */
|
|
840
|
+
const promptedFor = (persona: string, participants: ModelParticipant[]): Set<string> => {
|
|
841
|
+
const asked = new Set<string>();
|
|
842
|
+
for (const p of participants) {
|
|
843
|
+
if (promptedParticipants.has(`${persona}\0${p.key}`)) asked.add(p.key);
|
|
844
|
+
}
|
|
845
|
+
return asked;
|
|
846
|
+
};
|
|
817
847
|
|
|
818
848
|
// Personas/agents load ONLY from the user dir (populated by `/persona seed|restore`) and the
|
|
819
849
|
// project `.pi/agents`. The bundled defaults are a *seed source*, NOT a live discovery layer,
|
|
@@ -1172,7 +1202,8 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1172
1202
|
// the full status view stays on demand via `/peek`.
|
|
1173
1203
|
const now = Date.now();
|
|
1174
1204
|
const fast = config.peekEveryMs > 0;
|
|
1175
|
-
const
|
|
1205
|
+
const waitingRuns = waitingAsyncRunIds();
|
|
1206
|
+
const stuck = fast ? peekWatcher.poll(runs, now, STALL_FLAG_MS, waitingRuns) : [];
|
|
1176
1207
|
// Drain only progress messages; blocking asks (expectsReply) are surfaced by the intercom
|
|
1177
1208
|
// notifier and left for the `intercom inbox` tool — so peek never double-shows them.
|
|
1178
1209
|
const unread = fast ? bus.takeWhere(SUPERVISOR, (e) => !e.expectsReply) : [];
|
|
@@ -1180,7 +1211,7 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1180
1211
|
if (stuck.length === 0 && unread.length === 0 && !dueCheckIn) return; // healthy + quiet ⇒ stay silent
|
|
1181
1212
|
const parts: string[] = [];
|
|
1182
1213
|
if (stuck.length > 0) parts.push(buildPeekAlert(stuck, { now }));
|
|
1183
|
-
else if (dueCheckIn) parts.push(buildCheckIn(runs, { now, stallMs: STALL_FLAG_MS }));
|
|
1214
|
+
else if (dueCheckIn) parts.push(buildCheckIn(runs, { now, stallMs: STALL_FLAG_MS, waitingForSupervisor: waitingRuns }));
|
|
1184
1215
|
if (unread.length > 0) parts.push(`📨 from sub-agents:\n${fenceUntrusted(formatInbox(unread))}`);
|
|
1185
1216
|
// Reset the check-in cadence only on a PROGRESS surfacing (a stall alert or a check-in), NOT on a
|
|
1186
1217
|
// message-only wake — else a chatty child would postpone the routine off-track glance forever.
|
|
@@ -1283,27 +1314,54 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1283
1314
|
};
|
|
1284
1315
|
};
|
|
1285
1316
|
|
|
1286
|
-
|
|
1317
|
+
/**
|
|
1318
|
+
* Every participant a run really spawns — roster members (inline ones included) plus the
|
|
1319
|
+
* auxiliary actors its strategy declares — with the model each one resolves to.
|
|
1320
|
+
*/
|
|
1321
|
+
function runParticipants(ctx: ExtensionContext, opts: { strategy: string; params?: Record<string, unknown>; members: RosterMember[] }): ModelParticipant[] {
|
|
1322
|
+
const persona = controller.activePersona?.name;
|
|
1323
|
+
return collectModelParticipants({
|
|
1324
|
+
members: opts.members,
|
|
1325
|
+
strategy: opts.strategy,
|
|
1326
|
+
...(opts.params ? { params: opts.params } : {}),
|
|
1327
|
+
...(persona ? { assigned: personaModels(personaConfigs, persona) } : {}),
|
|
1328
|
+
agentModel: (agent) => agents.find((a) => a.name === agent)?.model,
|
|
1329
|
+
...(ctx.model ? { sessionModel: `${ctx.model.provider}/${ctx.model.id}` } : {}),
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Offer a model for each participant that nobody has chosen one for yet. A participant is
|
|
1335
|
+
* skipped when a model is already assigned for it — an explicit inline member model, a saved
|
|
1336
|
+
* per-persona assignment (role key or agent key), or the agent's own frontmatter model. A saved
|
|
1337
|
+
* ref that is not currently authenticated is NOT re-asked either: that is a runtime recovery
|
|
1338
|
+
* question (the SDK reroutes such a leg to the main model), not a reason to nag every session.
|
|
1339
|
+
*
|
|
1340
|
+
* Only a live UI ever sees a dialog (`ctx.hasUI`), and only when there is more than one
|
|
1341
|
+
* authenticated model to choose between.
|
|
1342
|
+
*/
|
|
1343
|
+
async function ensurePersonaModels(ctx: ExtensionContext, participants: ModelParticipant[]): Promise<void> {
|
|
1287
1344
|
const persona = controller.activePersona?.name;
|
|
1288
1345
|
if (!persona || !ctx.hasUI) return;
|
|
1289
|
-
|
|
1290
|
-
const pickable = [...new Set(roster.map((m) => rosterSpec(m)).filter((s) => !s.model).map((s) => s.agent))];
|
|
1291
|
-
if (pickable.length < 2) return; // an ensemble of one distinct core can't be diversified
|
|
1292
|
-
if (modelsPrompted.has(persona)) return;
|
|
1293
|
-
const configured = personaConfigs[persona]?.models ?? {};
|
|
1294
|
-
const missing = pickable.filter((a) => !configured[a]);
|
|
1346
|
+
const missing = pendingParticipants(participants, { prompted: promptedFor(persona, participants) });
|
|
1295
1347
|
if (missing.length === 0) return;
|
|
1296
1348
|
const available = configuredModels(ctx);
|
|
1297
1349
|
if (available.length < 2) return; // can't diversify with a single configured model
|
|
1298
|
-
modelsPrompted.add(persona);
|
|
1299
1350
|
const options = orderModelRefs(available.map((m) => `${m.provider}/${m.id}`), ctx.model?.provider);
|
|
1300
1351
|
const chosen: Record<string, string> = {};
|
|
1301
1352
|
try {
|
|
1302
|
-
for (const
|
|
1353
|
+
for (const p of missing) {
|
|
1354
|
+
// Recorded BEFORE the prompt: a dismissal (or a picker that dies) must not be retried
|
|
1355
|
+
// for this session, exactly like an answer is not asked twice.
|
|
1356
|
+
promptedParticipants.add(`${persona}\0${p.key}`);
|
|
1303
1357
|
// Name the core's verticalization in the picker too: choosing a model for "the
|
|
1304
1358
|
// Conservatore" is a different judgement than choosing one for a bare "balthasar".
|
|
1305
|
-
|
|
1306
|
-
const
|
|
1359
|
+
// An auxiliary actor names the param that declared it ("arbiter judge").
|
|
1360
|
+
const purpose = corePurpose(p.agent);
|
|
1361
|
+
const role = p.param ? ` · ${p.param}` : "";
|
|
1362
|
+
// A participant label carries a role prompt (free prose from a team file or a
|
|
1363
|
+
// caller) — sanitised here so a title can never repaint the TUI or forge a row.
|
|
1364
|
+
const title = compactInlineText(`Model for "${p.label}"${purpose ? ` (${purpose})` : ""}${role} · ${persona}`, { maxChars: 120 });
|
|
1307
1365
|
// In the TUI: a searchable picker (type to filter) whose viewport follows the
|
|
1308
1366
|
// selection — the built-in select can't scroll a hundreds-long provider list
|
|
1309
1367
|
// usefully. Outside the TUI (RPC), fall back to the built-in select.
|
|
@@ -1315,7 +1373,7 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1315
1373
|
{ overlay: true },
|
|
1316
1374
|
)
|
|
1317
1375
|
: await ctx.ui.select(title, options);
|
|
1318
|
-
if (pick) chosen[
|
|
1376
|
+
if (pick) chosen[p.key] = pick;
|
|
1319
1377
|
}
|
|
1320
1378
|
} catch {
|
|
1321
1379
|
/* dismissed / no UI → fall back to the default model */
|
|
@@ -1342,24 +1400,40 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1342
1400
|
return agents.find((a) => a.name === agent)?.purpose;
|
|
1343
1401
|
}
|
|
1344
1402
|
|
|
1345
|
-
/** The session's own model, for a strategy's
|
|
1346
|
-
*
|
|
1347
|
-
|
|
1348
|
-
|
|
1403
|
+
/** The session's own model, for a strategy's main-only recovery of a member whose model broke
|
|
1404
|
+
* (`orchestration/model-retry.ts`). The GETTER is the live
|
|
1405
|
+
* one: a leg can fail minutes after the run started, and the main model may have changed since
|
|
1406
|
+
* the snapshot was taken — the legacy string stays alongside it for callers that want the
|
|
1407
|
+
* value the run started with. */
|
|
1408
|
+
function sessionModelDep(ctx: ExtensionContext): { sessionModel?: string; getSessionModel?: () => string | undefined } {
|
|
1409
|
+
const ref = (c?: ExtensionContext): string | undefined => {
|
|
1410
|
+
const model = (c ?? lastCtx)?.model;
|
|
1411
|
+
return model ? `${model.provider}/${model.id}` : undefined;
|
|
1412
|
+
};
|
|
1413
|
+
const snapshot = ref(ctx);
|
|
1414
|
+
return {
|
|
1415
|
+
...(snapshot ? { sessionModel: snapshot } : {}),
|
|
1416
|
+
getSessionModel: () => ref(ctx) ?? ref(),
|
|
1417
|
+
};
|
|
1349
1418
|
}
|
|
1350
1419
|
|
|
1351
|
-
// Each core's model beside its name
|
|
1352
|
-
|
|
1420
|
+
// Each core's model beside its name, in the ONE precedence the picker saved under: the
|
|
1421
|
+
// participant's own assignment key (a role member's `agent#hash`, else the bare agent key) →
|
|
1422
|
+
// the agent's declared default → the session model. An inline model is already the participant's
|
|
1423
|
+
// explicit engine pin and wins before any saved assignment.
|
|
1424
|
+
function coreModel(ctx: ExtensionContext, agent: string, assignmentKey: string = agent, participant?: ModelParticipant | null): string | undefined {
|
|
1425
|
+
if (participant === null) return undefined; // a short-hint collision cannot identify one role safely
|
|
1426
|
+
if (participant?.source === "inline" && participant.model) return participant.model;
|
|
1353
1427
|
const persona = controller.activePersona?.name;
|
|
1354
1428
|
const configured = persona ? personaModels(personaConfigs, persona) : {};
|
|
1355
|
-
return configured[agent] ?? agents.find((a) => a.name === agent)?.model ?? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
|
|
1429
|
+
return configured[assignmentKey] ?? configured[agent] ?? agents.find((a) => a.name === agent)?.model ?? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
|
|
1356
1430
|
}
|
|
1357
|
-
function coreTelemetryMeta(ctx: ExtensionContext, agent: string): { agent: string; model?: string } {
|
|
1358
|
-
const model = coreModel(ctx, agent);
|
|
1431
|
+
function coreTelemetryMeta(ctx: ExtensionContext, agent: string, assignmentKey?: string, participant?: ModelParticipant | null): { agent: string; model?: string } {
|
|
1432
|
+
const model = coreModel(ctx, agent, assignmentKey, participant);
|
|
1359
1433
|
return { agent, ...(model ? { model } : {}) };
|
|
1360
1434
|
}
|
|
1361
|
-
function coreLabel(ctx: ExtensionContext, agent: string, key: string = agent): string {
|
|
1362
|
-
const short = shortModel(coreModel(ctx, agent));
|
|
1435
|
+
function coreLabel(ctx: ExtensionContext, agent: string, key: string = agent, assignmentKey?: string, participant?: ModelParticipant | null): string {
|
|
1436
|
+
const short = shortModel(coreModel(ctx, agent, assignmentKey, participant));
|
|
1363
1437
|
// `key` is the disambiguated node id (`agent` for a solo member, `agent · HINT` for a
|
|
1364
1438
|
// roster-role one) — display that + what the core is FOR + the model, so a watcher reads the
|
|
1365
1439
|
// council as roles rather than names, and three `reviewer` lenses still read distinctly.
|
|
@@ -1367,13 +1441,69 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1367
1441
|
// purpose would repeat it — but a plain repeat (`melchior#2`) is the same core twice and
|
|
1368
1442
|
// still wants its lens, so key off the role separator, not off `key !== agent`.
|
|
1369
1443
|
const purpose = key.includes(" · ") ? undefined : corePurpose(agent);
|
|
1370
|
-
|
|
1444
|
+
// `key` embeds the member's role HINT (derived from its role prompt), and `short` embeds a
|
|
1445
|
+
// model ref — both are dynamic, so the whole row is collapsed to one escape-free line.
|
|
1446
|
+
return compactInlineText([key, purpose, short].filter(Boolean).join(" · "), { maxChars: 120 });
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/** The assignment key for a tree node key when no participant was seeded under it: strip the lens
|
|
1450
|
+
* hint and the occurrence suffix, leaving the bare agent — the legacy key shape, which always
|
|
1451
|
+
* resolves (a role-less agent is exactly how those assignments were saved). */
|
|
1452
|
+
function bareAssignmentKey(nodeKey: string): string {
|
|
1453
|
+
return (nodeKey.split(" · ")[0] ?? nodeKey).replace(/#\d+$/, "");
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/** Map every picker participant to the SDK's display base. A short role hint is not unique when
|
|
1457
|
+
* different role prompts share it, so ambiguous nodes omit the model rather than borrowing
|
|
1458
|
+
* another lens' assignment. Repeated SDK keys (`#2`, …) resolve through their base and share the
|
|
1459
|
+
* same participant. */
|
|
1460
|
+
function participantByNode(participants: ModelParticipant[]): Map<string, ModelParticipant | null> {
|
|
1461
|
+
const groups = new Map<string, Map<string, ModelParticipant>>();
|
|
1462
|
+
for (const participant of participants) {
|
|
1463
|
+
const base = participant.role ? `${participant.agent} · ${roleHint(participant.role)}` : participant.agent;
|
|
1464
|
+
const group = groups.get(base) ?? new Map<string, ModelParticipant>();
|
|
1465
|
+
group.set(participant.key, participant);
|
|
1466
|
+
groups.set(base, group);
|
|
1467
|
+
}
|
|
1468
|
+
const out = new Map<string, ModelParticipant | null>();
|
|
1469
|
+
for (const [base, group] of groups) {
|
|
1470
|
+
if (group.size !== 1) {
|
|
1471
|
+
out.set(base, null);
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
const participant = group.values().next().value as ModelParticipant | undefined;
|
|
1475
|
+
if (participant) out.set(base, participant);
|
|
1476
|
+
}
|
|
1477
|
+
return out;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function participantForNode(nodeParticipants: ReadonlyMap<string, ModelParticipant | null>, nodeKey: string): ModelParticipant | null | undefined {
|
|
1481
|
+
const exact = nodeParticipants.get(nodeKey);
|
|
1482
|
+
if (exact !== undefined) return exact;
|
|
1483
|
+
const base = nodeKey.replace(/#\d+$/, "");
|
|
1484
|
+
return base === nodeKey ? undefined : nodeParticipants.get(base);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/** Surface the SDK's main-model recovery in the UI: a broken member is re-run on the session's own
|
|
1488
|
+
* model rather than dropped from the council, and the operator deserves to see that switch.
|
|
1489
|
+
* Strictly cosmetic — a toast that throws must never corrupt a run's result. */
|
|
1490
|
+
function modelFallbackDep(ctx: ExtensionContext): { onModelFallback?: (info: { agent: string; from?: string; to: string; key: string }) => void } {
|
|
1491
|
+
return {
|
|
1492
|
+
onModelFallback: (info) => {
|
|
1493
|
+
if (!ctx.hasUI) return;
|
|
1494
|
+
try {
|
|
1495
|
+
ctx.ui.notify(compactInlineText(`${info.agent}: ${info.from ?? "its model"} failed, re-running on ${info.to}`, { maxChars: 120 }), "warning");
|
|
1496
|
+
} catch {
|
|
1497
|
+
/* cosmetic */
|
|
1498
|
+
}
|
|
1499
|
+
},
|
|
1500
|
+
};
|
|
1371
1501
|
}
|
|
1372
1502
|
|
|
1373
1503
|
// The SDK lifecycle callbacks that drive the unified tree for one strategy run rooted at
|
|
1374
1504
|
// `rootId` — shared by /orchestrate, the council tool, and each flow phase. Seeds running
|
|
1375
1505
|
// cores, flips ⏳ → ✓/✗ with usage, streams progress, and registers stop/steer handles.
|
|
1376
|
-
function strategyTreeDeps(ctx: ExtensionContext, rootId: string) {
|
|
1506
|
+
function strategyTreeDeps(ctx: ExtensionContext, rootId: string, nodeParticipants: ReadonlyMap<string, ModelParticipant | null> = new Map()) {
|
|
1377
1507
|
return {
|
|
1378
1508
|
onAgentStart: (agent: string, abort: () => void, key?: string) => {
|
|
1379
1509
|
stopRegistry.set(`${rootId}/${key ?? agent}`, abort);
|
|
@@ -1390,7 +1520,9 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1390
1520
|
// time, not run time, so a queued leg would otherwise show elapsed time it never
|
|
1391
1521
|
// spent running and could raise a false stall badge once it goes live.
|
|
1392
1522
|
const now = Date.now();
|
|
1393
|
-
|
|
1523
|
+
const participant = participantForNode(nodeParticipants, nodeKey);
|
|
1524
|
+
const assignmentKey = participant?.key ?? bareAssignmentKey(nodeKey);
|
|
1525
|
+
agentTree.add({ id, label: coreLabel(ctx, agent, nodeKey, assignmentKey, participant), parentId: rootId, status: "running", kind: "subagent", ...coreTelemetryMeta(ctx, agent, assignmentKey, participant), detail: "", startedAt: now, lastAdvanceAt: now });
|
|
1394
1526
|
return;
|
|
1395
1527
|
}
|
|
1396
1528
|
stopRegistry.delete(id);
|
|
@@ -1406,7 +1538,10 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1406
1538
|
onAgentProgress: (agent: string, p: AgentProgress, key?: string) => {
|
|
1407
1539
|
const id = `${rootId}/${key ?? agent}`;
|
|
1408
1540
|
if (p.toolEvent) publishAgentTool(id, p.toolEvent);
|
|
1409
|
-
|
|
1541
|
+
const now = Date.now();
|
|
1542
|
+
agentTree.update(id, progressPatch(p, now));
|
|
1543
|
+
// Child engine events refresh liveness, not proof of useful work; repaint alone never advances it.
|
|
1544
|
+
agentTree.update(rootId, { lastAdvanceAt: now });
|
|
1410
1545
|
},
|
|
1411
1546
|
};
|
|
1412
1547
|
}
|
|
@@ -1426,8 +1561,10 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1426
1561
|
signal?: AbortSignal,
|
|
1427
1562
|
) {
|
|
1428
1563
|
const label = resolveStrategyName(orch) ?? "strategy";
|
|
1429
|
-
const roster = orch
|
|
1430
|
-
|
|
1564
|
+
const roster = resolveOrchestrationMembers(orch, teams);
|
|
1565
|
+
const participants = runParticipants(ctx, { strategy: label, ...(orch.params ? { params: orch.params } : {}), members: roster });
|
|
1566
|
+
await ensurePersonaModels(ctx, participants);
|
|
1567
|
+
const participantsByNode = participantByNode(participants);
|
|
1431
1568
|
const rootId = nextRootId(`${idPrefix}:${label}`);
|
|
1432
1569
|
agentTree.add({ id: rootId, label, status: "running", kind: "council" });
|
|
1433
1570
|
// Seed the whole roster at once (cores show by name immediately); "queued" until the
|
|
@@ -1436,13 +1573,15 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1436
1573
|
roster.forEach((m, i) => {
|
|
1437
1574
|
const a = rosterSpec(m).agent;
|
|
1438
1575
|
const key = seedKeys[i] ?? a;
|
|
1439
|
-
|
|
1576
|
+
const participant = participantForNode(participantsByNode, key);
|
|
1577
|
+
const assignmentKey = participant?.key ?? bareAssignmentKey(key);
|
|
1578
|
+
agentTree.add({ id: `${rootId}/${key}`, label: coreLabel(ctx, a, key, assignmentKey, participant), parentId: rootId, status: "running", kind: "subagent", ...coreTelemetryMeta(ctx, a, assignmentKey, participant), detail: "queued" });
|
|
1440
1579
|
});
|
|
1441
1580
|
try {
|
|
1442
1581
|
// The signal goes to the STRATEGY as well as the engine: a multi-round strategy checks it
|
|
1443
1582
|
// cooperatively between rounds, so an aborted run stops convening instead of running every
|
|
1444
1583
|
// remaining round against an already-cancelled engine (docs/STRATEGIES.md).
|
|
1445
|
-
const result = await runPersonaStrategy(orch, task, { engine: buildEngine(signal), teams, limits: RUN_LIMITS, ...sessionModelDep(ctx), ...(signal ? { signal } : {}), ...strategyTreeDeps(ctx, rootId), ...strategySpawnGate() });
|
|
1584
|
+
const result = await runPersonaStrategy(orch, task, { engine: buildEngine(signal, undefined, { providerFallback: false }), teams, limits: RUN_LIMITS, ...sessionModelDep(ctx), ...(signal ? { signal } : {}), ...strategyTreeDeps(ctx, rootId, participantsByNode), ...strategySpawnGate(), ...modelFallbackDep(ctx) });
|
|
1446
1585
|
agentTree.update(rootId, { status: signal?.aborted ? "stopped" : result?.ok === false ? "failed" : "done" });
|
|
1447
1586
|
return result ?? undefined;
|
|
1448
1587
|
} catch (error) {
|
|
@@ -1500,8 +1639,23 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1500
1639
|
const journalPath = join(journalDir, journalFileName(spec)); // encodes the name — any name is storable
|
|
1501
1640
|
const resume = readJournal(journalPath, hash);
|
|
1502
1641
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1642
|
+
// Preflight: every participant the flow's phases will spawn — roster members AND the
|
|
1643
|
+
// auxiliary actors each phase's strategy declares (an arbiter, a synthesiser) — resolved
|
|
1644
|
+
// once, up front, so the whole run's models are settled before the first leg starts.
|
|
1645
|
+
// An unknown team resolves to nothing here; `verifyFlowRefs` reports it properly below.
|
|
1646
|
+
const phasePlans = new Map(
|
|
1647
|
+
spec.phases.map((phase) => {
|
|
1648
|
+
let members: RosterMember[] = [];
|
|
1649
|
+
try {
|
|
1650
|
+
members = resolveOrchestrationMembers(phase.roster ? { roster: phase.roster } : {}, teams);
|
|
1651
|
+
} catch {
|
|
1652
|
+
members = [];
|
|
1653
|
+
}
|
|
1654
|
+
const participants = runParticipants(ctx, { strategy: phase.strategy, ...(phase.params ? { params: phase.params } : {}), members });
|
|
1655
|
+
return [phase.id, { members, participants }] as const;
|
|
1656
|
+
}),
|
|
1657
|
+
);
|
|
1658
|
+
await ensurePersonaModels(ctx, spec.phases.flatMap((p) => phasePlans.get(p.id)?.participants ?? []));
|
|
1505
1659
|
|
|
1506
1660
|
const flowRoot = nextRootId(`flow:${spec.name}`);
|
|
1507
1661
|
agentTree.add({ id: flowRoot, label: `flow ${spec.name}`, status: "running", kind: "flow" });
|
|
@@ -1546,23 +1700,28 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1546
1700
|
},
|
|
1547
1701
|
runPhase: async ({ phase, task }) => {
|
|
1548
1702
|
const pid = `${flowRoot}/${phase.id}`;
|
|
1549
|
-
const
|
|
1703
|
+
const plan = phasePlans.get(phase.id) ?? { members: [], participants: [] };
|
|
1704
|
+
const roster = plan.members;
|
|
1550
1705
|
const seedKeys = rosterNodeKeys(roster);
|
|
1706
|
+
const participantsByNode = participantByNode(plan.participants);
|
|
1551
1707
|
roster.forEach((m, i) => {
|
|
1552
1708
|
const a = rosterSpec(m).agent;
|
|
1553
1709
|
const key = seedKeys[i] ?? a;
|
|
1554
|
-
|
|
1710
|
+
const participant = participantForNode(participantsByNode, key);
|
|
1711
|
+
const assignmentKey = participant?.key ?? bareAssignmentKey(key);
|
|
1712
|
+
agentTree.add({ id: `${pid}/${key}`, label: coreLabel(ctx, a, key, assignmentKey, participant), parentId: pid, status: "running", kind: "subagent", ...coreTelemetryMeta(ctx, a, assignmentKey, participant), detail: "queued" });
|
|
1555
1713
|
});
|
|
1556
1714
|
const orch: OrchestrationGrammar = { mode: "strategy", strategy: phase.strategy, params: phase.params ?? {} };
|
|
1557
1715
|
if (phase.roster) orch.roster = phase.roster;
|
|
1558
1716
|
const r = await runPersonaStrategy(orch, task, {
|
|
1559
|
-
engine: buildEngine(signal),
|
|
1717
|
+
engine: buildEngine(signal, undefined, { providerFallback: false }),
|
|
1560
1718
|
teams,
|
|
1561
1719
|
limits: RUN_LIMITS,
|
|
1562
1720
|
...sessionModelDep(ctx),
|
|
1563
1721
|
...(signal ? { signal } : {}), // cooperative per-round abort inside the phase's strategy, not just the engine
|
|
1564
|
-
...strategyTreeDeps(ctx, pid),
|
|
1722
|
+
...strategyTreeDeps(ctx, pid, participantsByNode),
|
|
1565
1723
|
...strategySpawnGate(),
|
|
1724
|
+
...modelFallbackDep(ctx),
|
|
1566
1725
|
});
|
|
1567
1726
|
return r ?? { agent: phase.id, output: `unknown strategy: ${phase.strategy}`, usage: emptyUsage(), ok: false, error: "unknown strategy" };
|
|
1568
1727
|
},
|
|
@@ -1874,6 +2033,7 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
1874
2033
|
bus,
|
|
1875
2034
|
SUPERVISOR,
|
|
1876
2035
|
STALL_FLAG_MS,
|
|
2036
|
+
waitingRunIds: waitingAsyncRunIds,
|
|
1877
2037
|
missingRunMessage,
|
|
1878
2038
|
stopAgent,
|
|
1879
2039
|
steerAgent,
|
|
@@ -2109,9 +2269,9 @@ export default function piPersona(pi: ExtensionAPI, options: PiPersonaOptions =
|
|
|
2109
2269
|
const id = args.trim();
|
|
2110
2270
|
if (id) {
|
|
2111
2271
|
const run = tracker.peek(id);
|
|
2112
|
-
ctx.ui.notify(run ? buildPeekDigest([run], { now: Date.now(), stallMs: STALL_FLAG_MS }) : `peek: no run "${id}"`, run ? "info" : "warning");
|
|
2272
|
+
ctx.ui.notify(run ? buildPeekDigest([run], { now: Date.now(), stallMs: STALL_FLAG_MS, waitingForSupervisor: waitingAsyncRunIds() }) : `peek: no run "${id}"`, run ? "info" : "warning");
|
|
2113
2273
|
} else {
|
|
2114
|
-
ctx.ui.notify(buildPeekDigest(tracker.list(), { now: Date.now(), stallMs: STALL_FLAG_MS }), "info");
|
|
2274
|
+
ctx.ui.notify(buildPeekDigest(tracker.list(), { now: Date.now(), stallMs: STALL_FLAG_MS, waitingForSupervisor: waitingAsyncRunIds() }), "info");
|
|
2115
2275
|
}
|
|
2116
2276
|
},
|
|
2117
2277
|
});
|