@ferris1225/pi-subagents 4.1.2 → 4.1.3
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/README.md +561 -506
- package/agents/cleaner.md +2 -2
- package/agents/documenter.md +46 -44
- package/agents/explorer.md +15 -11
- package/agents/reviewer.md +5 -2
- package/agents/worker.md +5 -3
- package/package.json +55 -55
- package/src/agents.ts +42 -1
- package/src/completion.ts +160 -160
- package/src/dispatch.ts +634 -637
- package/src/fixloop.ts +15 -32
- package/src/models.ts +189 -189
- package/src/monitor.ts +97 -27
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +6 -3
- package/src/runtime.ts +5 -0
- package/src/session-fork.ts +80 -80
- package/src/setup.ts +151 -136
- package/src/spawn.ts +8 -2
- package/src/thread-lifecycle.ts +43 -11
- package/src/tools.ts +44 -30
- package/src/widget.ts +65 -19
package/src/thread-lifecycle.ts
CHANGED
|
@@ -11,7 +11,12 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
12
|
import { rm } from "node:fs/promises";
|
|
13
13
|
import { resolve } from "node:path";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
discoverAgents,
|
|
16
|
+
isWriteCapableAgent,
|
|
17
|
+
resolveAgentTools,
|
|
18
|
+
type AgentConfig,
|
|
19
|
+
} from "./agents.ts";
|
|
15
20
|
import { completionTriggersTurn, type CompletionMessageItem } from "./completion.ts";
|
|
16
21
|
import {
|
|
17
22
|
DEFAULT_THINKING_LEVEL,
|
|
@@ -43,7 +48,7 @@ import {
|
|
|
43
48
|
resolveAgentModelRoute,
|
|
44
49
|
resolveThinkingLevel,
|
|
45
50
|
} from "./models.ts";
|
|
46
|
-
import { monitor, sumUsage } from "./monitor.ts";
|
|
51
|
+
import { monitor, sumUsage, type ContinuationKind } from "./monitor.ts";
|
|
47
52
|
import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
|
|
48
53
|
import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts";
|
|
49
54
|
import { forkRetainedSession } from "./session-fork.ts";
|
|
@@ -185,6 +190,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
185
190
|
prompt?: string;
|
|
186
191
|
worktree?: WorktreeIsolation;
|
|
187
192
|
forkedFromRunId?: number;
|
|
193
|
+
continuationKind?: ContinuationKind;
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
interface ResumeReservation {
|
|
@@ -225,7 +231,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
225
231
|
cwd: string | undefined,
|
|
226
232
|
isolation: IsolationMode = "shared",
|
|
227
233
|
existingThread?: SubagentThread,
|
|
228
|
-
|
|
234
|
+
appendedObjectiveOnResume = false,
|
|
229
235
|
environment?: DispatchEnvironment,
|
|
230
236
|
seed?: SessionSeed,
|
|
231
237
|
resumeReservation?: ResumeReservation,
|
|
@@ -239,8 +245,11 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
239
245
|
const runCtx = environment?.ctx ?? ctx;
|
|
240
246
|
const runConfig = environment?.config ?? config;
|
|
241
247
|
const runAgents = environment?.agents ?? agents;
|
|
242
|
-
const
|
|
243
|
-
if (!
|
|
248
|
+
const discoveredAgent = runAgents.find((candidate) => candidate.name === agentName);
|
|
249
|
+
if (!discoveredAgent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
250
|
+
const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
|
|
251
|
+
resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
|
|
252
|
+
const agent = resolveLiveAgentTools(discoveredAgent);
|
|
244
253
|
if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
|
|
245
254
|
return {
|
|
246
255
|
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker, cleaner, or documenter.`),
|
|
@@ -288,6 +297,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
288
297
|
const runId = existingThread?.id ?? monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
|
|
289
298
|
isolation,
|
|
290
299
|
...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
|
|
300
|
+
...(seed?.continuationKind ? { continuationKind: seed.continuationKind } : {}),
|
|
291
301
|
});
|
|
292
302
|
const generation = (existingThread?.generation ?? 0) + 1;
|
|
293
303
|
const pending: SingleResult = {
|
|
@@ -302,7 +312,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
302
312
|
...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
|
|
303
313
|
};
|
|
304
314
|
if (existingThread) {
|
|
305
|
-
monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation
|
|
315
|
+
monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation, {
|
|
316
|
+
elapsedMs: existingThread.elapsedMs,
|
|
317
|
+
continuationKind: appendedObjectiveOnResume ? "resume-appended" : "resume-retained",
|
|
318
|
+
});
|
|
306
319
|
runtime.settledRuns.delete(runId);
|
|
307
320
|
}
|
|
308
321
|
|
|
@@ -367,6 +380,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
367
380
|
control,
|
|
368
381
|
generationCompletion: Promise.resolve(),
|
|
369
382
|
lifecycleVersion: 0,
|
|
383
|
+
elapsedMs: 0,
|
|
370
384
|
sessionId: seed?.sessionId,
|
|
371
385
|
sessionDir: seed?.sessionDir,
|
|
372
386
|
forkedFromRunId: seed?.forkedFromRunId,
|
|
@@ -504,6 +518,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
504
518
|
});
|
|
505
519
|
};
|
|
506
520
|
|
|
521
|
+
const persistElapsedTime = (): void => {
|
|
522
|
+
thread.elapsedMs = monitor.getElapsedMs(runId) ?? thread.elapsedMs;
|
|
523
|
+
};
|
|
524
|
+
|
|
507
525
|
thread.park = async (): Promise<"queued" | "active"> => {
|
|
508
526
|
if (thread.retired) throw new Error(`Run #${runId} was retired by subagent_stop.`);
|
|
509
527
|
if (thread.lifecycleOperation) throw new Error(`Run #${runId} is already handling ${thread.lifecycleOperation}.`);
|
|
@@ -544,7 +562,14 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
544
562
|
thread.state = "parked";
|
|
545
563
|
thread.queueController = undefined;
|
|
546
564
|
runtime.runControllers.delete(runId);
|
|
565
|
+
const parkedRun = monitor.findRun(runId);
|
|
566
|
+
if (parkedRun?.managedWorkflow && parkedRun.task !== thread.task) {
|
|
567
|
+
// The active child row previously showed this stage objective. Once it
|
|
568
|
+
// disappears, keep the parked parent aligned with what resume retains.
|
|
569
|
+
monitor.setTask(runId, thread.task);
|
|
570
|
+
}
|
|
547
571
|
monitor.setStatus(runId, "parked");
|
|
572
|
+
persistElapsedTime();
|
|
548
573
|
return queued ? "queued" : "active";
|
|
549
574
|
} finally {
|
|
550
575
|
if (thread.lifecycleVersion === version && thread.lifecycleOperation === "park") {
|
|
@@ -822,6 +847,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
822
847
|
prompt: forkObjective ?? FORK_CONTINUATION_PROMPT,
|
|
823
848
|
worktree: childWorktree,
|
|
824
849
|
forkedFromRunId: runId,
|
|
850
|
+
continuationKind: forkObjective ? "fork-appended" : "fork-retained",
|
|
825
851
|
},
|
|
826
852
|
);
|
|
827
853
|
if (child.exitCode !== -1 || child.runId === undefined) {
|
|
@@ -877,6 +903,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
877
903
|
{
|
|
878
904
|
defaultCwd: executionCwd,
|
|
879
905
|
agent: route.agent,
|
|
906
|
+
resolveAgentForAttempt: resolveLiveAgentTools,
|
|
880
907
|
agentName,
|
|
881
908
|
task,
|
|
882
909
|
cwd: executionCwd,
|
|
@@ -891,7 +918,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
891
918
|
? {
|
|
892
919
|
sessionId: priorSessionId,
|
|
893
920
|
sessionDir: priorSessionDir,
|
|
894
|
-
stdinText: seed?.prompt ?? (
|
|
921
|
+
stdinText: seed?.prompt ?? (appendedObjectiveOnResume
|
|
895
922
|
? task
|
|
896
923
|
: buildResumePrompt(priorTask ?? task, "the retained thread was resumed")),
|
|
897
924
|
}
|
|
@@ -955,9 +982,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
955
982
|
const workflowPlan = getManagedWorkflowPlan(result, runConfig, workflowAvailability);
|
|
956
983
|
if (workflowPlan && runtime.sessionActive) {
|
|
957
984
|
thread.state = "running";
|
|
958
|
-
// The stable parent row
|
|
959
|
-
//
|
|
960
|
-
//
|
|
985
|
+
// The stable parent row now represents workflow ownership, not whichever
|
|
986
|
+
// model stage ran most recently. Internal rows own their exact role/model/
|
|
987
|
+
// thinking/timing telemetry and remain independently queryable.
|
|
988
|
+
monitor.setManagedWorkflow(runId, true);
|
|
961
989
|
monitor.setStatus(runId, "running");
|
|
962
990
|
monitor.setActivity(
|
|
963
991
|
runId,
|
|
@@ -978,8 +1006,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
978
1006
|
rememberLatest: (latest) => {
|
|
979
1007
|
if (runtime.threads.get(runId) !== thread || thread.generation !== generation) return;
|
|
980
1008
|
thread.lastResult = latest;
|
|
1009
|
+
// Retained control follows the newest child session, but the live parent
|
|
1010
|
+
// row keeps the original top-level role/model/usage. The active internal
|
|
1011
|
+
// row already owns the current stage's role and telemetry.
|
|
981
1012
|
thread.agentName = latest.agent;
|
|
982
|
-
monitor.setAgent(runId, latest.agent);
|
|
983
1013
|
thread.task = latest.task;
|
|
984
1014
|
thread.sessionId = latest.sessionId;
|
|
985
1015
|
thread.sessionDir = latest.sessionDir;
|
|
@@ -1041,6 +1071,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
1041
1071
|
// Stamp the terminal monitor state before projecting it. This gives every
|
|
1042
1072
|
// path a fixed endedAt even when the row is removed immediately.
|
|
1043
1073
|
monitor.setStatus(runId, failed ? "failed" : "done");
|
|
1074
|
+
persistElapsedTime();
|
|
1044
1075
|
if (!runtime.sessionActive || !ownsSettlement()) return;
|
|
1045
1076
|
|
|
1046
1077
|
const modelLevel = failed && isModelLevelFailure(result);
|
|
@@ -1175,6 +1206,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
1175
1206
|
if (!ownsSettlement()) return;
|
|
1176
1207
|
thread.state = "failed";
|
|
1177
1208
|
monitor.setStatus(runId, "failed");
|
|
1209
|
+
persistElapsedTime();
|
|
1178
1210
|
finishRun(runId, "failed", { silent: true });
|
|
1179
1211
|
runtime.registerRunResult(runId, crashed);
|
|
1180
1212
|
runtime.runControllers.delete(runId);
|
package/src/tools.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thread controls and lookup tools around the subagent runtime:
|
|
3
|
-
* subagent_control (steer/retarget/park/resume), subagent_wait (in-turn
|
|
4
|
-
* lookup), subagent_status, and destructive subagent_stop.
|
|
3
|
+
* subagent_control (steer/retarget/park/resume/fork), subagent_wait (in-turn
|
|
4
|
+
* result lookup), subagent_status, and destructive subagent_stop.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
@@ -12,8 +12,7 @@ import { DEFAULT_MAX_RESULT_LINES, loadConfig } from "./config.ts";
|
|
|
12
12
|
import { formatCompletionBlock, formatUsage, matchRunIds } from "./format.ts";
|
|
13
13
|
import { emptyUsage } from "./rpc-run.ts";
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
formatUsageCompact,
|
|
15
|
+
formatTaskSummary,
|
|
17
16
|
isRunActiveStatus,
|
|
18
17
|
monitor,
|
|
19
18
|
runLabel,
|
|
@@ -51,7 +50,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
51
50
|
Type.String({ description: "Instruction queued by steer after the current child tool batch." }),
|
|
52
51
|
),
|
|
53
52
|
objective: Type.Optional(
|
|
54
|
-
Type.String({ description: "Replacement objective for retarget
|
|
53
|
+
Type.String({ description: "Replacement objective for retarget; optional appended objective for resume/fork. Omit on resume/fork to continue the current retained objective." }),
|
|
55
54
|
),
|
|
56
55
|
});
|
|
57
56
|
|
|
@@ -64,14 +63,14 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
64
63
|
"retarget replaces the objective in that same active top-level child.",
|
|
65
64
|
"Managed downstream documenter/reviewer/fix stages are controlled by the parent queue rather than its settled RPC control: use park or stop there, then resume with an objective to redirect retained context.",
|
|
66
65
|
"park aborts to a stable checkpoint, terminates the child, preserves context, and releases its concurrency slot.",
|
|
67
|
-
"resume restarts a parked, completed, or failed retained thread with the same run id; objective
|
|
68
|
-
"fork copies a parked/completed/failed retained session branch into a new logical thread and run id; an isolated checkpoint must be settled and integrated first; objective
|
|
66
|
+
"resume restarts a parked, completed, or failed retained thread with the same run id and cumulative active time; omit objective to continue the current goal, or provide one to append it to retained context and make it the displayed current goal.",
|
|
67
|
+
"fork copies a parked/completed/failed retained session branch into a new logical thread and run id; an isolated checkpoint must be settled and integrated first; omit objective to continue the current goal, or provide one to append a new branch goal.",
|
|
69
68
|
].join(" "),
|
|
70
69
|
promptSnippet: "Control a subagent thread: steer/retarget an active top-level child; park/stop a managed downstream stage; resume or fork retained context.",
|
|
71
70
|
promptGuidelines: [
|
|
72
71
|
"Use subagent_control steer to refine an active top-level RPC child without restarting it; the instruction is delivered after its current tool batch.",
|
|
73
72
|
"Use subagent_control retarget only while that top-level child is active. During a managed downstream stage, park it and resume with a replacement objective instead.",
|
|
74
|
-
"Use subagent_control park to checkpoint useful context while releasing the process/concurrency slot, and resume to continue the same run id later.",
|
|
73
|
+
"Use subagent_control park to checkpoint useful context while releasing the process/concurrency slot, and resume to continue the same run id later. Resume without objective keeps the current goal; resume with objective appends that goal to retained context.",
|
|
75
74
|
"Use subagent_control fork only on a parked or settled retained thread; isolated work must settle and integrate before it can fork. Fork creates a new run id while leaving the source untouched.",
|
|
76
75
|
"Use subagent_stop only for destructive cancellation; it retires that thread's retained session without retiring independent forks.",
|
|
77
76
|
],
|
|
@@ -110,13 +109,15 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
110
109
|
thread.task = objective;
|
|
111
110
|
thread.control.retargetPending(objective);
|
|
112
111
|
monitor.setTask(thread.id, objective);
|
|
113
|
-
|
|
112
|
+
monitor.setContinuationKind(thread.id, "retarget");
|
|
113
|
+
return { content: [{ type: "text", text: `Updated queued run #${thread.id} to the replacement objective; no child was spawned by this control action.` }], details: {} };
|
|
114
114
|
}
|
|
115
115
|
if (!(["starting", "running", "steering", "interrupting", "retrying"] as const).includes(phase as any)) {
|
|
116
116
|
return { content: [{ type: "text", text: `Run #${thread.id} is ${thread.state}; use resume with objective to restart retained context.` }], details: {} };
|
|
117
117
|
}
|
|
118
118
|
thread.task = objective;
|
|
119
119
|
monitor.setTask(thread.id, objective);
|
|
120
|
+
monitor.setContinuationKind(thread.id, "retarget");
|
|
120
121
|
await thread.control.retarget(objective);
|
|
121
122
|
return { content: [{ type: "text", text: `Retargeted run #${thread.id} in the same session; the aborted objective will not be delivered as a completion.` }], details: {} };
|
|
122
123
|
}
|
|
@@ -146,11 +147,19 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
146
147
|
if (params.objective !== undefined && !objective) {
|
|
147
148
|
return { content: [{ type: "text", text: "resume objective must be non-blank when provided." }], details: {} };
|
|
148
149
|
}
|
|
150
|
+
const hadRetainedSession = Boolean(thread.sessionId && thread.sessionDir);
|
|
149
151
|
const pending = await thread.resume(objective, ctx);
|
|
150
152
|
if (pending.exitCode !== -1) {
|
|
151
153
|
return { content: [{ type: "text", text: getResultOutput(pending) }], details: {} };
|
|
152
154
|
}
|
|
153
|
-
|
|
155
|
+
const currentObjective = formatTaskSummary(objective ?? thread.task, 80, false);
|
|
156
|
+
const mode = objective
|
|
157
|
+
? `appended objective: ${currentObjective}`
|
|
158
|
+
: `continuing current objective: ${currentObjective}`;
|
|
159
|
+
const context = hadRetainedSession
|
|
160
|
+
? "the same retained session and prior context are preserved"
|
|
161
|
+
: "no prior child session existed, so only the logical run and objective are continued";
|
|
162
|
+
return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved. Completion will arrive automatically.` }], details: {}, terminate: true };
|
|
154
163
|
}
|
|
155
164
|
case "fork": {
|
|
156
165
|
const objective = params.objective === undefined ? undefined : nonBlank(params.objective);
|
|
@@ -164,7 +173,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
164
173
|
return {
|
|
165
174
|
content: [{
|
|
166
175
|
type: "text",
|
|
167
|
-
text: `Forked run #${thread.id} into new run #${pending.runId}${objective ?
|
|
176
|
+
text: `Forked run #${thread.id} into new run #${pending.runId}; ${objective ? `appended branch objective: ${formatTaskSummary(objective, 80, false)}` : `continuing current objective: ${formatTaskSummary(thread.task, 80, false)}`}. Retained context is copied, the source is unchanged, and child completion will arrive automatically.`,
|
|
168
177
|
}],
|
|
169
178
|
details: { sourceRunId: thread.id, childRunId: pending.runId, result: pending },
|
|
170
179
|
terminate: true,
|
|
@@ -352,10 +361,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
352
361
|
});
|
|
353
362
|
|
|
354
363
|
// Status overview: what is running right now and what finished this session,
|
|
355
|
-
// with per-run details (id,
|
|
356
|
-
// main agent can decide whether to wait, stop, or re-dispatch. Learned from
|
|
357
|
-
// nicobailon/pi-subagents ({action:"status"} + status files): inspect before
|
|
358
|
-
// you act, and report run ids when handing off.
|
|
364
|
+
// with per-run details (id, role, model, usage, elapsed, activity).
|
|
359
365
|
const SubagentStatusParams = Type.Object({
|
|
360
366
|
id: Type.Optional(
|
|
361
367
|
Type.String({
|
|
@@ -368,7 +374,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
368
374
|
name: "subagent_status",
|
|
369
375
|
label: "Subagent Status",
|
|
370
376
|
description: [
|
|
371
|
-
"List active background sub-agent runs (id,
|
|
377
|
+
"List active background sub-agent runs (id, role, model, thinking, usage, elapsed, current activity) and recently finished results.",
|
|
372
378
|
"Pass id to read the full result of a finished run; pass no id for the overview.",
|
|
373
379
|
"Use it to decide whether to subagent_wait, subagent_stop, or re-dispatch — never to poll: results arrive by themselves.",
|
|
374
380
|
].join(" "),
|
|
@@ -412,20 +418,30 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
412
418
|
const activeThread = runtime.threads.get(active.id);
|
|
413
419
|
const managedDownstream =
|
|
414
420
|
activeThread?.state === "running" && activeThread.control.getPhase() === "settled";
|
|
421
|
+
const activeChild = runs.find((run) =>
|
|
422
|
+
run.parentRunId === active.id && isRunActiveStatus(run.status)
|
|
423
|
+
);
|
|
424
|
+
const owner = active.managedWorkflow ? `${active.agent} workflow` : active.agent;
|
|
425
|
+
const retainedStage = active.managedWorkflow && activeThread?.agentName !== active.agent
|
|
426
|
+
? activeThread?.agentName
|
|
427
|
+
: undefined;
|
|
415
428
|
const metadata = [
|
|
416
429
|
activeThread?.isolation === "worktree" ? `worktree ${active.integrationStatus ?? activeThread.worktree?.state ?? "active"}` : undefined,
|
|
417
430
|
activeThread?.forkedFromRunId !== undefined ? `forked from #${activeThread.forkedFromRunId}` : undefined,
|
|
418
431
|
(activeThread?.forkChildRunIds.length ?? 0) > 0 ? `forks ${activeThread!.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
|
|
419
432
|
].filter(Boolean).join(" · ");
|
|
433
|
+
const stageStatus = activeChild
|
|
434
|
+
? monitor.summarize(activeChild)
|
|
435
|
+
: active.activity ?? statusLabel(active.status);
|
|
420
436
|
return {
|
|
421
437
|
content: [
|
|
422
438
|
{
|
|
423
439
|
type: "text",
|
|
424
440
|
text: parked
|
|
425
|
-
? `Run #${active.id} ${
|
|
441
|
+
? `Run #${active.id} ${owner} is parked with retained${retainedStage ? ` ${retainedStage} stage` : ""} context${metadata ? ` (${metadata})` : ""}. Use subagent_control resume to restart it, or subagent_stop to retire it.`
|
|
426
442
|
: managedDownstream
|
|
427
|
-
? `Run #${active.id} ${
|
|
428
|
-
: `Run #${active.id} ${
|
|
443
|
+
? `Run #${active.id} ${owner} is in a managed downstream stage (${stageStatus}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait for its result, subagent_control park to checkpoint it, or subagent_stop to cancel it. Steer/retarget are unavailable until you park and resume the retained stage.`
|
|
444
|
+
: `Run #${active.id} ${owner} is still active (${active.activity ?? statusLabel(active.status)}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait to block for its result, subagent_control to steer/park it, or subagent_stop to cancel it.`,
|
|
429
445
|
},
|
|
430
446
|
],
|
|
431
447
|
details: {},
|
|
@@ -434,36 +450,34 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
434
450
|
return { content: [{ type: "text", text: `No subagent run matches "${requested}".` }], details: {} };
|
|
435
451
|
}
|
|
436
452
|
|
|
437
|
-
const now = Date.now();
|
|
438
453
|
const activeRuns = monitor.getRuns().filter(
|
|
439
454
|
(run) => isRunActiveStatus(run.status),
|
|
440
455
|
);
|
|
441
456
|
const activeLines = activeRuns.map((run) => {
|
|
442
457
|
const thread = runtime.threads.get(run.id);
|
|
443
|
-
const model = run.modelFallbackFrom
|
|
444
|
-
? `${run.model ?? "?"} (main after ${run.modelFallbackFrom} failed)`
|
|
445
|
-
: (run.model ?? "?");
|
|
446
458
|
const parts = [
|
|
447
|
-
`#${run.id} ${run
|
|
459
|
+
`#${run.id} ${monitor.summarize(run)}`,
|
|
448
460
|
run.label,
|
|
449
|
-
model,
|
|
450
|
-
formatUsageCompact(run.usage),
|
|
451
|
-
formatElapsed(run, now),
|
|
452
|
-
thread?.isolation === "worktree" ? `worktree ${run.integrationStatus ?? thread.worktree?.state ?? "active"}` : undefined,
|
|
453
461
|
thread?.forkedFromRunId !== undefined ? `forked from #${thread.forkedFromRunId}` : undefined,
|
|
454
462
|
(thread?.forkChildRunIds.length ?? 0) > 0 ? `forks ${thread!.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
|
|
463
|
+
run.activity ?? statusLabel(run.status),
|
|
455
464
|
].filter(Boolean);
|
|
456
|
-
return `- ${parts.join(" · ")}
|
|
465
|
+
return `- ${parts.join(" · ")}`;
|
|
457
466
|
});
|
|
458
467
|
const parkedThreads = [...runtime.threads.values()].filter((thread) => thread.state === "parked");
|
|
459
468
|
const parkedLines = parkedThreads.map((thread) => {
|
|
469
|
+
const run = monitor.findRun(thread.id);
|
|
470
|
+
const owner = run?.managedWorkflow ? `${run.agent} workflow` : run?.agent ?? thread.agentName;
|
|
471
|
+
const retainedStage = run?.managedWorkflow && thread.agentName !== run.agent
|
|
472
|
+
? ` · retained stage ${thread.agentName}`
|
|
473
|
+
: "";
|
|
460
474
|
const relations = [
|
|
461
475
|
thread.forkedFromRunId !== undefined ? `forked from #${thread.forkedFromRunId}` : undefined,
|
|
462
476
|
thread.forkChildRunIds.length > 0 ? `forks ${thread.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
|
|
463
477
|
].filter(Boolean);
|
|
464
478
|
const relation = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
|
|
465
479
|
const isolation = thread.isolation === "worktree" ? ` · worktree ${thread.worktree?.state ?? "active"}` : "";
|
|
466
|
-
return `- #${thread.id} ${
|
|
480
|
+
return `- #${thread.id} ${owner} · ${run?.label ?? runLabel(thread.task)} · parked${thread.sessionDir ? " · context retained" : " · not started"}${retainedStage}${isolation}${relation}`;
|
|
467
481
|
});
|
|
468
482
|
const completed = [...runtime.settledRuns.entries()].slice(-5);
|
|
469
483
|
const completedLines = completed.map(([id, result]) => {
|
package/src/widget.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
import {
|
|
6
|
+
continuationLabel,
|
|
6
7
|
formatElapsed,
|
|
7
8
|
formatTaskSummary,
|
|
8
9
|
isRunActiveStatus,
|
|
@@ -25,10 +26,22 @@ function compactLine(left: string, right: string, width: number): string {
|
|
|
25
26
|
return `${truncateToWidth(left, leftWidth, "…")}${separator}${right}`;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/** Keep continuation semantics intact while independently compacting the task. */
|
|
30
|
+
function formatContinuationTask(label: string, task: string, width: number): string {
|
|
31
|
+
if (width <= 0) return "";
|
|
32
|
+
const labelWidth = visibleWidth(label);
|
|
33
|
+
if (labelWidth >= width) return truncateToWidth(label, width, "");
|
|
34
|
+
const separator = " · ";
|
|
35
|
+
const taskWidth = width - labelWidth - visibleWidth(separator);
|
|
36
|
+
if (taskWidth <= 0) return label;
|
|
37
|
+
const summary = formatTaskSummary(task, taskWidth);
|
|
38
|
+
return summary ? `${label}${separator}${summary}` : label;
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
/** One compact primary line per genuinely active run, plus an optional indented
|
|
29
|
-
* activity line. The primary line reserves
|
|
30
|
-
* width before truncating the task. Settled and parked threads never
|
|
31
|
-
* elapsed time cannot keep ticking beside a terminal status. */
|
|
42
|
+
* activity line. The primary line reserves stage model/thinking when present and
|
|
43
|
+
* elapsed width before truncating the task. Settled and parked threads never
|
|
44
|
+
* appear, so elapsed time cannot keep ticking beside a terminal status. */
|
|
32
45
|
function runPrimaryLine(
|
|
33
46
|
run: RunView,
|
|
34
47
|
theme: Theme,
|
|
@@ -38,22 +51,31 @@ function runPrimaryLine(
|
|
|
38
51
|
): string {
|
|
39
52
|
const dim = (text: string): string => theme.fg("dim", text);
|
|
40
53
|
const icon = statusIcon(run.status, theme);
|
|
41
|
-
const
|
|
54
|
+
const displayName = run.managedWorkflow ? `${run.agent} workflow` : run.agent;
|
|
55
|
+
const name = theme.fg("accent", theme.bold(displayName));
|
|
42
56
|
const identity = `${prefix}${icon} ${name}`;
|
|
43
57
|
const elapsed = formatElapsed(run, now);
|
|
44
58
|
// Render only the resolved model id plus thinking level. Provider auth and
|
|
45
59
|
// other configuration never enter monitor state or this line.
|
|
46
60
|
const modelId = run.model?.split("/").at(-1);
|
|
47
|
-
const modelSource =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
const modelSource = run.managedWorkflow
|
|
62
|
+
? ""
|
|
63
|
+
: formatTaskSummary(
|
|
64
|
+
modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
|
|
65
|
+
64,
|
|
66
|
+
false,
|
|
67
|
+
);
|
|
52
68
|
// A chain child shows its role in the chain plus a task-derived label; the
|
|
53
69
|
// templated fix brief itself would only repeat the parent review's content.
|
|
70
|
+
const continuation = run.parentRunId === undefined
|
|
71
|
+
? continuationLabel(run.continuationKind, run.forkedFromRunId)
|
|
72
|
+
: undefined;
|
|
54
73
|
const taskSource = run.parentRunId !== undefined
|
|
55
74
|
? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(" · ")
|
|
56
75
|
: formatTaskSummary(run.task, 64);
|
|
76
|
+
const taskDesiredSource = [continuation, taskSource]
|
|
77
|
+
.filter((part): part is string => Boolean(part))
|
|
78
|
+
.join(" · ");
|
|
57
79
|
const primaryPartCount = 2 + (modelSource ? 1 : 0) + (elapsed ? 1 : 0);
|
|
58
80
|
const contentWidth = Math.max(
|
|
59
81
|
0,
|
|
@@ -64,15 +86,40 @@ function runPrimaryLine(
|
|
|
64
86
|
);
|
|
65
87
|
const modelDesired = visibleWidth(modelSource);
|
|
66
88
|
const modelFloor = Math.min(modelDesired, Math.min(12, contentWidth));
|
|
67
|
-
let modelWidth =
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
let modelWidth = 0;
|
|
90
|
+
if (modelSource) {
|
|
91
|
+
if (continuation) {
|
|
92
|
+
const continuationWidth = visibleWidth(continuation);
|
|
93
|
+
// Preserve the full semantic label and the usual eight-column task tail
|
|
94
|
+
// before giving the remaining space to model/thinking.
|
|
95
|
+
const taskFloor = Math.min(
|
|
96
|
+
visibleWidth(taskDesiredSource),
|
|
97
|
+
continuationWidth +
|
|
98
|
+
(taskSource ? visibleWidth(" · ") + Math.min(8, visibleWidth(taskSource)) : 0),
|
|
99
|
+
contentWidth,
|
|
100
|
+
);
|
|
101
|
+
const effectiveModelFloor = Math.min(
|
|
102
|
+
modelFloor,
|
|
103
|
+
Math.max(0, contentWidth - continuationWidth),
|
|
104
|
+
);
|
|
105
|
+
modelWidth = Math.min(
|
|
106
|
+
modelDesired,
|
|
107
|
+
Math.max(effectiveModelFloor, contentWidth - taskFloor),
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
modelWidth = Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
70
113
|
let taskWidth = contentWidth - modelWidth;
|
|
71
|
-
if (visibleWidth(
|
|
72
|
-
modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(
|
|
114
|
+
if (visibleWidth(taskDesiredSource) < taskWidth) {
|
|
115
|
+
modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskDesiredSource));
|
|
73
116
|
taskWidth = contentWidth - modelWidth;
|
|
74
117
|
}
|
|
75
|
-
const task = taskWidth
|
|
118
|
+
const task = taskWidth <= 0
|
|
119
|
+
? ""
|
|
120
|
+
: continuation
|
|
121
|
+
? formatContinuationTask(continuation, taskSource, taskWidth)
|
|
122
|
+
: formatTaskSummary(taskSource, taskWidth, run.parentRunId === undefined);
|
|
76
123
|
const modelThinking = modelWidth > 0 ? formatTaskSummary(modelSource, modelWidth, false) : "";
|
|
77
124
|
const primaryLeft = [
|
|
78
125
|
identity,
|
|
@@ -94,9 +141,8 @@ function runActivityLine(run: RunView, theme: Theme, width: number, indent: stri
|
|
|
94
141
|
}
|
|
95
142
|
|
|
96
143
|
/** Render active runs as a tree: main-agent dispatches are roots and managed
|
|
97
|
-
* documenter/reviewer/fix steps nest under the stable parent row.
|
|
98
|
-
*
|
|
99
|
-
* available through subagent_status when a thread must be controlled. */
|
|
144
|
+
* documenter/reviewer/fix steps nest under the stable parent row. Fork labels
|
|
145
|
+
* include their source id; other control ids remain available through status. */
|
|
100
146
|
export function formatActiveRunLines(
|
|
101
147
|
runs: readonly RunView[],
|
|
102
148
|
theme: Theme,
|
|
@@ -134,7 +180,7 @@ export function formatActiveRunLines(
|
|
|
134
180
|
|
|
135
181
|
function hasTickingRun(): boolean {
|
|
136
182
|
return monitor.getRuns().some(
|
|
137
|
-
(run) => isRunActiveStatus(run.status) && run.
|
|
183
|
+
(run) => isRunActiveStatus(run.status) && run.activeSince !== undefined,
|
|
138
184
|
);
|
|
139
185
|
}
|
|
140
186
|
|