@ferris1225/pi-subagents 4.1.24 → 4.2.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/README.md +47 -116
- package/agents/executor.md +53 -0
- package/package.json +55 -55
- package/src/agents.ts +2 -2
- package/src/announcements.ts +0 -26
- package/src/completion.ts +0 -11
- package/src/config.ts +1 -11
- package/src/dispatch.ts +16 -310
- package/src/durable.ts +0 -5
- package/src/format.ts +0 -8
- package/src/monitor.ts +7 -143
- package/src/prompt.ts +7 -30
- package/src/rpc-run.ts +993 -993
- package/src/runtime.ts +3 -10
- package/src/setup.ts +1 -6
- package/src/spawn.ts +0 -10
- package/src/thread-lifecycle.ts +51 -219
- package/src/widget.ts +33 -240
- package/agents/cleaner.md +0 -50
- package/agents/documenter.md +0 -40
- package/agents/reviewer.md +0 -82
- package/agents/synthesizer.md +0 -39
- package/agents/worker.md +0 -43
- package/src/workflow.ts +0 -215
package/src/monitor.ts
CHANGED
|
@@ -20,7 +20,6 @@ import type { IsolationMode, WorktreeFinalizationStatus } from "./worktree.ts";
|
|
|
20
20
|
|
|
21
21
|
export type RunStatus = "queued" | "running" | "interrupting" | "parked" | "done" | "failed";
|
|
22
22
|
export type ContinuationKind = "resume-retained" | "resume-appended";
|
|
23
|
-
export type WorkflowStageStatus = "done" | "active" | "pending" | "changes" | "failed";
|
|
24
23
|
|
|
25
24
|
/** Why a queued run has produced no output yet. Three genuinely different
|
|
26
25
|
* situations used to be reported as one "queued": waiting for a free process
|
|
@@ -32,19 +31,6 @@ export type WorkflowStageStatus = "done" | "active" | "pending" | "changes" | "f
|
|
|
32
31
|
* "queued"; cleared on every transition out of it. */
|
|
33
32
|
export type RunWaitReason = "process-slot" | "repository-lane" | "starting";
|
|
34
33
|
|
|
35
|
-
/** Ephemeral projection of one real or currently planned managed stage. It is
|
|
36
|
-
* live monitor state only; durable results remain the per-run chain records. */
|
|
37
|
-
export interface WorkflowStage {
|
|
38
|
-
agent: string;
|
|
39
|
-
relation: string;
|
|
40
|
-
status: WorkflowStageStatus;
|
|
41
|
-
/** Telemetry snapshot frozen when the stage settled (the live child row
|
|
42
|
-
* leaves the monitor at that moment); the active stage reads its live child. */
|
|
43
|
-
model?: string;
|
|
44
|
-
usage?: UsageStats;
|
|
45
|
-
elapsedMs?: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
34
|
export function isRunActiveStatus(status: RunStatus): boolean {
|
|
49
35
|
return status === "queued" || status === "running" || status === "interrupting";
|
|
50
36
|
}
|
|
@@ -88,45 +74,19 @@ export interface RunView {
|
|
|
88
74
|
endedAt?: number;
|
|
89
75
|
/** Why this generation reused retained context, shown in the widget/status. */
|
|
90
76
|
continuationKind?: ContinuationKind;
|
|
91
|
-
/** When set, this is an internal managed-workflow step. */
|
|
92
|
-
groupId?: string;
|
|
93
|
-
/** Human-readable role within a workflow, e.g. "final review" or "final documentation sync". */
|
|
94
|
-
relationLabel?: string;
|
|
95
|
-
/** Stable owning run whose row represents the whole managed workflow. */
|
|
96
|
-
parentRunId?: number;
|
|
97
|
-
/** This stable top-level row currently owns a multi-stage managed workflow.
|
|
98
|
-
* Its elapsed time is workflow-wide; active child rows own stage telemetry. */
|
|
99
|
-
managedWorkflow?: boolean;
|
|
100
|
-
/** Live-only stage timeline retained on the parent while completed internal
|
|
101
|
-
* child rows leave the monitor. */
|
|
102
|
-
workflowStages?: WorkflowStage[];
|
|
103
77
|
}
|
|
104
78
|
|
|
105
|
-
/**
|
|
79
|
+
/** Extra metadata for a run whose row must carry isolation or resume context. */
|
|
106
80
|
export interface RunChainMeta {
|
|
107
|
-
groupId?: string;
|
|
108
|
-
relationLabel?: string;
|
|
109
|
-
parentRunId?: number;
|
|
110
81
|
isolation?: IsolationMode;
|
|
111
82
|
worktreeId?: string;
|
|
112
83
|
continuationKind?: ContinuationKind;
|
|
113
84
|
/** Initial wait reason; defaults to "process-slot" (a fresh dispatch enters
|
|
114
|
-
* the process queue).
|
|
115
|
-
* they
|
|
85
|
+
* the process queue). Children spawned outside the queue pass "starting"
|
|
86
|
+
* because they never wait for a slot. */
|
|
116
87
|
waitReason?: RunWaitReason;
|
|
117
88
|
}
|
|
118
89
|
|
|
119
|
-
/** Ephemeral activity of the parent pi model while its agent loop runs: the
|
|
120
|
-
* live model/thinking ref and a one-line "what is it doing now". Not a run —
|
|
121
|
-
* no id, usage, or chain machinery; the view disappears when the loop settles. */
|
|
122
|
-
export interface MainActivity {
|
|
123
|
-
model?: string;
|
|
124
|
-
thinking?: string;
|
|
125
|
-
activity?: string;
|
|
126
|
-
/** Epoch ms when the current agent loop started. */
|
|
127
|
-
activeSince: number;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
90
|
// ---------------------------------------------------------------------------
|
|
131
91
|
// Formatting helpers
|
|
132
92
|
// ---------------------------------------------------------------------------
|
|
@@ -494,71 +454,6 @@ export class MonitorStore {
|
|
|
494
454
|
private runs: RunView[] = [];
|
|
495
455
|
private nextId = 1;
|
|
496
456
|
private subscribers = new Set<() => void>();
|
|
497
|
-
private mainModel?: string;
|
|
498
|
-
private mainThinking?: string;
|
|
499
|
-
private mainActivity?: string;
|
|
500
|
-
private mainActiveSince?: number;
|
|
501
|
-
|
|
502
|
-
// --- parent pi model activity ------------------------------------------
|
|
503
|
-
// Fed by the parent session's extension events (agent loop, streaming,
|
|
504
|
-
// tool executions); rendered as the widget's first line. Change-guarded so
|
|
505
|
-
// per-token streaming deltas do not flood subscribers.
|
|
506
|
-
|
|
507
|
-
setMainModel(model?: string): void {
|
|
508
|
-
if (!model || this.mainModel === model) return;
|
|
509
|
-
this.mainModel = model;
|
|
510
|
-
this.notify();
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
setMainThinking(thinking?: string): void {
|
|
514
|
-
if (!thinking || this.mainThinking === thinking) return;
|
|
515
|
-
this.mainThinking = thinking;
|
|
516
|
-
this.notify();
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
setMainActivity(text: string): void {
|
|
520
|
-
const activity = sanitizeActivityText(text) || undefined;
|
|
521
|
-
if (!activity || this.mainActivity === activity) return;
|
|
522
|
-
this.mainActivity = activity;
|
|
523
|
-
this.notify();
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/** Record the main model starting a tool; the activity shows the tool's
|
|
527
|
-
* most telling argument, same vocabulary as subagent rows. */
|
|
528
|
-
recordMainToolStart(toolName: string, activity: string): void {
|
|
529
|
-
const safeToolName = sanitizeActivityText(toolName) || "tool";
|
|
530
|
-
this.setMainActivity(activity || safeToolName);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/** Record a failed main-model tool; successful completions keep their last
|
|
534
|
-
* activity until the next model event supplies a better description. */
|
|
535
|
-
recordMainToolEnd(toolName: string, isError: boolean): void {
|
|
536
|
-
if (isError) this.setMainActivity(`✗ ${sanitizeActivityText(toolName) || "tool"} failed`);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/** Track the parent agent loop: started at agent_start, cleared when the
|
|
540
|
-
* loop settles (agent_end / agent_settled). */
|
|
541
|
-
setMainAgentActive(active: boolean): void {
|
|
542
|
-
if ((this.mainActiveSince !== undefined) === active) return;
|
|
543
|
-
this.mainActiveSince = active ? Date.now() : undefined;
|
|
544
|
-
this.mainActivity = undefined;
|
|
545
|
-
this.notify();
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/** Live view of the parent model while its agent loop runs; undefined when idle. */
|
|
549
|
-
getMainActivity(): MainActivity | undefined {
|
|
550
|
-
if (this.mainActiveSince === undefined) return undefined;
|
|
551
|
-
return {
|
|
552
|
-
...(this.mainModel ? { model: this.mainModel } : {}),
|
|
553
|
-
...(this.mainThinking ? { thinking: this.mainThinking } : {}),
|
|
554
|
-
...(this.mainActivity ? { activity: this.mainActivity } : {}),
|
|
555
|
-
activeSince: this.mainActiveSince,
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
isMainAgentActive(): boolean {
|
|
560
|
-
return this.mainActiveSince !== undefined;
|
|
561
|
-
}
|
|
562
457
|
|
|
563
458
|
beginTurn(): void {
|
|
564
459
|
// Clear finished runs from a previous turn, but keep active and parked
|
|
@@ -608,9 +503,6 @@ export class MonitorStore {
|
|
|
608
503
|
waitReason: meta?.waitReason ?? "process-slot",
|
|
609
504
|
usage: emptyUsage(),
|
|
610
505
|
elapsedMs: 0,
|
|
611
|
-
...(meta?.groupId ? { groupId: meta.groupId } : {}),
|
|
612
|
-
...(meta?.relationLabel ? { relationLabel: meta.relationLabel } : {}),
|
|
613
|
-
...(meta?.parentRunId !== undefined ? { parentRunId: meta.parentRunId } : {}),
|
|
614
506
|
...(meta?.isolation ? { isolation: meta.isolation, integrationStatus: meta.isolation === "worktree" ? "pending" : undefined, ...(meta.worktreeId ? { worktreeId: meta.worktreeId } : {}) } : {}),
|
|
615
507
|
...(meta?.continuationKind ? { continuationKind: meta.continuationKind } : {}),
|
|
616
508
|
});
|
|
@@ -649,27 +541,6 @@ export class MonitorStore {
|
|
|
649
541
|
this.notify();
|
|
650
542
|
}
|
|
651
543
|
|
|
652
|
-
/** Switch a stable top-level row from one model run to workflow ownership.
|
|
653
|
-
* The original role remains for identity; child rows show stage telemetry. */
|
|
654
|
-
setManagedWorkflow(id: number, active: boolean): void {
|
|
655
|
-
const run = this.find(id);
|
|
656
|
-
if (!run) return;
|
|
657
|
-
run.managedWorkflow = active || undefined;
|
|
658
|
-
if (!active) run.workflowStages = undefined;
|
|
659
|
-
this.notify();
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/** Replace the live workflow projection atomically so renderers never observe
|
|
663
|
-
* a half-updated fix/re-review plan. */
|
|
664
|
-
setWorkflowStages(id: number, stages: readonly WorkflowStage[]): void {
|
|
665
|
-
const run = this.find(id);
|
|
666
|
-
if (!run) return;
|
|
667
|
-
run.workflowStages = stages.length > 0
|
|
668
|
-
? stages.map((stage) => ({ ...stage }))
|
|
669
|
-
: undefined;
|
|
670
|
-
this.notify();
|
|
671
|
-
}
|
|
672
|
-
|
|
673
544
|
setUsage(id: number, usage: UsageStats, model?: string): void {
|
|
674
545
|
const run = this.find(id);
|
|
675
546
|
if (!run) return;
|
|
@@ -806,8 +677,6 @@ export class MonitorStore {
|
|
|
806
677
|
run.waitReason = "process-slot";
|
|
807
678
|
run.usage = emptyUsage();
|
|
808
679
|
run.activity = undefined;
|
|
809
|
-
run.managedWorkflow = undefined;
|
|
810
|
-
run.workflowStages = undefined;
|
|
811
680
|
run.activeSince = undefined;
|
|
812
681
|
run.endedAt = undefined;
|
|
813
682
|
run.elapsedMs = Math.max(run.elapsedMs, meta?.elapsedMs ?? 0);
|
|
@@ -825,10 +694,6 @@ export class MonitorStore {
|
|
|
825
694
|
* finishRun calls from the old session remain safe no-ops. */
|
|
826
695
|
clear(): void {
|
|
827
696
|
this.runs = [];
|
|
828
|
-
this.mainModel = undefined;
|
|
829
|
-
this.mainThinking = undefined;
|
|
830
|
-
this.mainActivity = undefined;
|
|
831
|
-
this.mainActiveSince = undefined;
|
|
832
697
|
this.notify();
|
|
833
698
|
}
|
|
834
699
|
|
|
@@ -854,14 +719,13 @@ export class MonitorStore {
|
|
|
854
719
|
|
|
855
720
|
summarize(run: RunView): string {
|
|
856
721
|
const usage = formatUsageCompact(run.usage);
|
|
857
|
-
const parts = [run.
|
|
722
|
+
const parts = [run.agent];
|
|
858
723
|
const continuation = continuationLabel(run.continuationKind);
|
|
859
724
|
if (continuation) parts.push(continuation);
|
|
860
|
-
if (run.
|
|
861
|
-
if (
|
|
862
|
-
if (!run.managedWorkflow && run.thinking) parts.push(`thinking ${run.thinking}`);
|
|
725
|
+
if (run.model) parts.push(run.model);
|
|
726
|
+
if (run.thinking) parts.push(`thinking ${run.thinking}`);
|
|
863
727
|
if (run.isolation === "worktree") parts.push(`worktree ${run.integrationStatus ?? "active"}`);
|
|
864
|
-
if (
|
|
728
|
+
if (usage) parts.push(usage);
|
|
865
729
|
const elapsed = formatElapsed(run);
|
|
866
730
|
if (elapsed) parts.push(elapsed);
|
|
867
731
|
return parts.join(" · ");
|
package/src/prompt.ts
CHANGED
|
@@ -20,38 +20,21 @@ export function buildDelegationDirective(
|
|
|
20
20
|
|
|
21
21
|
const catalog = agents.map(formatCatalogEntry).join("\n");
|
|
22
22
|
const hasExplorer = agents.some((agent) => agent.name === "explorer");
|
|
23
|
-
const
|
|
24
|
-
const hasCleaner = agents.some((agent) => agent.name === "cleaner");
|
|
25
|
-
const hasDocumenter = agents.some((agent) => agent.name === "documenter");
|
|
26
|
-
const hasReviewer = agents.some((agent) => agent.name === "reviewer");
|
|
27
|
-
const hasSynthesizer = agents.some((agent) => agent.name === "synthesizer");
|
|
28
|
-
const codeWriterNames = [
|
|
29
|
-
...(hasWorker ? ["worker"] : []),
|
|
30
|
-
...(hasCleaner ? ["cleaner"] : []),
|
|
31
|
-
];
|
|
23
|
+
const hasExecutor = agents.some((agent) => agent.name === "executor");
|
|
32
24
|
|
|
33
25
|
const dispatchRules = [
|
|
34
|
-
`Delegate aggressively: child contexts are cheap, yours is scarce. Inline only trivial work — a lookup, a single focused edit, an answer already in context${
|
|
26
|
+
`Delegate aggressively: child contexts are cheap, yours is scarce. Inline only trivial work — a lookup, a single focused edit, an answer already in context${hasExecutor ? "; default every non-trivial delegated task (implementation, fix, refactor, test, cleanup, docs sync, result merging) to `executor`" : ""}.`,
|
|
35
27
|
...(hasExplorer
|
|
36
28
|
? [
|
|
37
29
|
"`explorer`: split a broad question into parallel explorers with disjoint scopes. Its findings are leads, never proof — re-read load-bearing files before acting yourself (a child you brief re-verifies).",
|
|
38
30
|
]
|
|
39
31
|
: []),
|
|
40
|
-
...(
|
|
41
|
-
? ["`cleaner`: dispatch for requested cleanup AND proactively when finished work leaves dead code or duplication. Your brief is its edit authorization — every safe proven cut applies without per-item approval; never a gate. Scope: the uncommitted diff, or whatever your brief names (Git range, directory)."]
|
|
42
|
-
: []),
|
|
43
|
-
...(hasDocumenter
|
|
44
|
-
? ["`documenter`: standalone docs/comment work; dispatch it proactively when a change — yours or a child's — leaves README/docs/comment drift no writer already synced; cheap and may make zero edits."]
|
|
45
|
-
: []),
|
|
46
|
-
...(hasReviewer
|
|
32
|
+
...(hasExecutor
|
|
47
33
|
? [
|
|
48
|
-
|
|
34
|
+
"`executor`: brief it as the edit authorization. For cleanup, name the scope (uncommitted diff, Git range, directory) — every safe proven cut applies without per-item approval; finding no safe cut is a valid result. After a wide fan-out, pass the result-artifact paths to one executor and read its merged brief instead of every result yourself.",
|
|
49
35
|
]
|
|
50
36
|
: []),
|
|
51
|
-
|
|
52
|
-
? ["`synthesizer`: after a wide fan-out, pass the result-artifact paths to one synthesizer and read its brief instead of every result yourself."]
|
|
53
|
-
: []),
|
|
54
|
-
`Parallelize by default: map the todo list onto ONE \`tasks\` dispatch. One child owns one deliverable and its files; only genuinely dependent work waits for its prerequisite.`,
|
|
37
|
+
"Parallelize by default: map the todo list onto ONE `tasks` dispatch. One child owns one deliverable and its files; only genuinely dependent work waits for its prerequisite.",
|
|
55
38
|
"Brief each child completely — goal, exact paths, constraints, expected output; it has no conversation memory and cannot delegate. Resume parked threads with `subagent_control resume`.",
|
|
56
39
|
];
|
|
57
40
|
|
|
@@ -63,13 +46,7 @@ export function buildDelegationDirective(
|
|
|
63
46
|
|
|
64
47
|
const verificationRules = [
|
|
65
48
|
"Never report an unrun check as passed; surface unavailable checks and pre-existing failures, and inspect actual changes before reporting completion.",
|
|
66
|
-
|
|
67
|
-
? [
|
|
68
|
-
"A REVIEW_FAIL from a gate you dispatched directly returns its findings to you: fix them inline or via a briefed worker without waiting for the user, then re-verify ONCE. If the gate still fails, report the remaining findings and move on — never loop gate dispatches.",
|
|
69
|
-
"Multi-model cross-review only when explicitly requested or for high-risk security, FFI, migration, or concurrency changes.",
|
|
70
|
-
]
|
|
71
|
-
: []),
|
|
72
|
-
"Commit or push only when explicitly requested, applicable checks pass, and no review finding remains unresolved.",
|
|
49
|
+
"Commit or push only when explicitly requested and applicable checks pass.",
|
|
73
50
|
];
|
|
74
51
|
|
|
75
52
|
return `
|
|
@@ -86,6 +63,6 @@ ${bullets(dispatchRules)}
|
|
|
86
63
|
Result handoff:
|
|
87
64
|
${bullets(handoffRules)}
|
|
88
65
|
|
|
89
|
-
|
|
66
|
+
Verification:
|
|
90
67
|
${bullets(verificationRules)}`;
|
|
91
68
|
}
|