@ferris1225/pi-subagents 4.1.3 → 4.1.4
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 +55 -32
- package/agents/cleaner.md +2 -2
- package/agents/documenter.md +3 -3
- package/agents/reviewer.md +2 -2
- package/agents/worker.md +2 -2
- package/package.json +1 -1
- package/src/completion.ts +160 -160
- package/src/config.ts +5 -5
- package/src/dispatch.ts +63 -50
- package/src/fixloop.ts +75 -95
- package/src/models.ts +189 -189
- package/src/prompt.ts +3 -3
- package/src/recovery.ts +145 -145
- package/src/session-fork.ts +80 -80
- package/src/thread-lifecycle.ts +3 -3
package/README.md
CHANGED
|
@@ -23,12 +23,12 @@ losing retained context.
|
|
|
23
23
|
You
|
|
24
24
|
└─ pi main agent
|
|
25
25
|
├─ explorer ─── maps the codebase
|
|
26
|
-
├─ worker ───── implements ─┬─▶
|
|
26
|
+
├─ worker ───── implements ─┬─▶ reviewer ─▶ documenter
|
|
27
27
|
├─ cleaner ──── cleans up ──┘ (enabled roles only)
|
|
28
28
|
├─ documenter ─ synchronizes docs ─▶ reviewer
|
|
29
29
|
└─ reviewer ─── advisory report (no VERDICT), or managed gate
|
|
30
|
-
├─ REVIEW_PASS + documenter →
|
|
31
|
-
└─ REVIEW_FAIL → worker →
|
|
30
|
+
├─ REVIEW_PASS + documenter → final documentation sync
|
|
31
|
+
└─ REVIEW_FAIL → worker → reviewer (fix rounds) → final docs
|
|
32
32
|
|
|
33
33
|
The stable parent run returns one final result when the complete workflow settles.
|
|
34
34
|
```
|
|
@@ -49,11 +49,12 @@ more of it.
|
|
|
49
49
|
checkouts (worktrees) by default, then apply their changes back without
|
|
50
50
|
touching your index.
|
|
51
51
|
- **Documentation stops drifting.** Enabled `documenter` runs automatically
|
|
52
|
-
after
|
|
53
|
-
run an explicitly requested
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
hard limit
|
|
52
|
+
once after the review gate settles — after successful workers/cleaners or
|
|
53
|
+
fix rounds, never per fix round. It can also run an explicitly requested
|
|
54
|
+
whole-codebase maintenance pass.
|
|
55
|
+
- **Review can close the loop.** A failed gate can automatically dispatch a
|
|
56
|
+
worker, request another independent review, and repeat up to a hard limit;
|
|
57
|
+
one final documentation sync follows the settled chain.
|
|
57
58
|
- **Agents remain controllable.** Every run has a stable id and retained session,
|
|
58
59
|
so you can change direction or continue later without starting from zero.
|
|
59
60
|
- **Failures are handled, not hidden.** Model failures can hand the same session to
|
|
@@ -67,7 +68,7 @@ more of it.
|
|
|
67
68
|
| One generic child role | Five focused engineering roles |
|
|
68
69
|
| A one-shot prompt | Retained, steerable, resumable, forkable threads |
|
|
69
70
|
| Concurrent writers in one checkout | Git worktree isolation for parallel workers |
|
|
70
|
-
| A review report you must act on manually | Automatic writer →
|
|
71
|
+
| A review report you must act on manually | Automatic writer → reviewer → documenter delivery and bounded fix rounds |
|
|
71
72
|
| Manual polling or follow-up | Automatic result delivery that resumes the main agent |
|
|
72
73
|
| A hard failure when the selected model is unavailable | Direct handoff to the current main model |
|
|
73
74
|
| Synchronized retries during startup contention | Extended jittered backoff that reduces retry collisions |
|
|
@@ -108,6 +109,25 @@ Compare screenshots/settings.png with design.png and report every visual mismatc
|
|
|
108
109
|
The main agent decides when delegation is useful. You can also call the tools
|
|
109
110
|
explicitly when you want exact control.
|
|
110
111
|
|
|
112
|
+
## What changed in 4.1.4
|
|
113
|
+
|
|
114
|
+
### Documentation sync moved after the review gate
|
|
115
|
+
|
|
116
|
+
`documenter` now runs once at the end of a managed chain instead of before the
|
|
117
|
+
reviewer and once per fix round:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
before: worker → documenter → reviewer → (worker → documenter → reviewer) × N
|
|
121
|
+
after: worker → reviewer → (worker → reviewer) × N → documenter
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Code fixes no longer invalidate a docs pass written moments earlier, fix rounds
|
|
125
|
+
stop paying for documenter runs and re-reviewing their churn, and a direct
|
|
126
|
+
passing gate gets one final documentation sync instead of a second full review.
|
|
127
|
+
Gate reviewers record documentation drift as non-gating `## Documentation notes`
|
|
128
|
+
that the final documenter applies; when `documenter` is disabled, drift stays a
|
|
129
|
+
normal gate finding. The sync is skipped when a chain ends on a failing gate.
|
|
130
|
+
|
|
111
131
|
## What changed in 4.1.2
|
|
112
132
|
|
|
113
133
|
### Documentation sync as a real workflow stage
|
|
@@ -225,22 +245,25 @@ subagent({
|
|
|
225
245
|
});
|
|
226
246
|
```
|
|
227
247
|
|
|
228
|
-
A gate reviewer ends with `REVIEW_PASS` or `REVIEW_FAIL`. A direct pass is
|
|
229
|
-
|
|
230
|
-
|
|
248
|
+
A gate reviewer ends with `REVIEW_PASS` or `REVIEW_FAIL`. A direct pass is
|
|
249
|
+
final for the code: runtime runs the final documentation sync once (when
|
|
250
|
+
`documenter` is enabled) and delivers. A failure uses the bounded loop:
|
|
231
251
|
|
|
232
252
|
```text
|
|
233
|
-
reviewer → worker fixes every open finding →
|
|
253
|
+
reviewer → worker fixes every open finding → reviewer checks again → … → final documentation sync
|
|
234
254
|
```
|
|
235
255
|
|
|
236
256
|
Each step gets a fresh model context. The chain shares the same code state and
|
|
237
|
-
passes every full reviewer
|
|
238
|
-
|
|
239
|
-
|
|
257
|
+
passes every full reviewer and worker report forward; it does not reuse one
|
|
258
|
+
context window. Internal children bypass top-level lifecycle policy, so they
|
|
259
|
+
cannot recursively start another chain. Gate reviewers keep documentation drift
|
|
260
|
+
out of the verdict while `documenter` is enabled by recording it as
|
|
261
|
+
`## Documentation notes` for the final documenter.
|
|
240
262
|
|
|
241
|
-
`maxFixRounds` limits worker fix attempts only.
|
|
242
|
-
|
|
243
|
-
are advisory: they omit `VERDICT`, remain read-only, and
|
|
263
|
+
`maxFixRounds` limits worker fix attempts only. The post-writer review gate and
|
|
264
|
+
the final documentation sync still run when it is `0`. Generic audits and
|
|
265
|
+
read-only reviews are advisory: they omit `VERDICT`, remain read-only, and
|
|
266
|
+
never trigger edits.
|
|
244
267
|
|
|
245
268
|
### Clean up without guessing
|
|
246
269
|
|
|
@@ -258,7 +281,7 @@ axes genuinely differ, avoiding a generic abstraction that is worse than the
|
|
|
258
281
|
duplication.
|
|
259
282
|
|
|
260
283
|
```text
|
|
261
|
-
explicit cleanup request → cleaner applies proven cuts →
|
|
284
|
+
explicit cleanup request → cleaner applies proven cuts → reviewer gates the diff → documenter syncs docs
|
|
262
285
|
read-only cleanup audit → reviewer reports candidates only
|
|
263
286
|
```
|
|
264
287
|
|
|
@@ -269,10 +292,11 @@ changes, and asking for cleanup does not reward speculative deletion.
|
|
|
269
292
|
|
|
270
293
|
`documenter` has two deliberate launch paths.
|
|
271
294
|
|
|
272
|
-
**For a pending worker or cleaner change**, enable the role. Runtime schedules
|
|
273
|
-
automatically against the actual diff
|
|
274
|
-
a duplicate manual sync. If reviewer is disabled,
|
|
275
|
-
|
|
295
|
+
**For a pending worker or cleaner change**, enable the role. Runtime schedules
|
|
296
|
+
one final sync automatically against the actual diff after the review gate
|
|
297
|
+
settles; do not dispatch a duplicate manual sync. If reviewer is disabled,
|
|
298
|
+
documenter becomes the final managed stage directly after the writer. If
|
|
299
|
+
documenter is disabled, reviewer follows the writer directly.
|
|
276
300
|
|
|
277
301
|
**For an existing project**, explicitly authorize a broad maintenance pass:
|
|
278
302
|
|
|
@@ -308,11 +332,11 @@ filesystem isolation:
|
|
|
308
332
|
not need a writable checkout.
|
|
309
333
|
|
|
310
334
|
Worktree mode requires a Git repository with a committed `HEAD`. For an isolated
|
|
311
|
-
writer, automatic documenter
|
|
312
|
-
Those isolated stages can still run in parallel; writer and documentation
|
|
313
|
-
are integrated only after the final
|
|
314
|
-
and binary changes are then carried back to the original
|
|
315
|
-
or modifying the parent index.
|
|
335
|
+
writer, automatic reviewer/documenter children run inside that same worktree.
|
|
336
|
+
Those isolated stages can still run in parallel; writer, fix, and documentation
|
|
337
|
+
changes are integrated only after the final managed stage settles. Tracked,
|
|
338
|
+
deleted, untracked, and binary changes are then carried back to the original
|
|
339
|
+
checkout without staging or modifying the parent index.
|
|
316
340
|
|
|
317
341
|
Repository-lane discovery uses the Git top-level even in an empty repository, so
|
|
318
342
|
root and nested paths share one lane before the first commit. Every shared
|
|
@@ -384,8 +408,7 @@ The active TUI widget shows queued and running work as a compact tree:
|
|
|
384
408
|
● reviewer workflow · review diff of src/cache.ts · 42s
|
|
385
409
|
├ ● worker · fix round 1 · src/cache.ts · claude-sonnet-4-5/high · 10s
|
|
386
410
|
│ grep cacheKey
|
|
387
|
-
|
|
388
|
-
└ ○ reviewer · re-review round 1 · claude-sonnet-4-5/high · 3s
|
|
411
|
+
└ ○ documenter · final documentation sync · claude-haiku-4-5/low · 3s
|
|
389
412
|
```
|
|
390
413
|
|
|
391
414
|
A managed root keeps its original top-level role and workflow-wide elapsed
|
|
@@ -506,7 +529,7 @@ Configuration is stored at `~/.pi/agent/pi-subagents.json` and follows
|
|
|
506
529
|
| `proactiveInjection` | Teach the main model when and how to delegate. Default `true`. |
|
|
507
530
|
| `agentScope` | Discover `user`, `project`, or `both` agent directories. Default `user`. |
|
|
508
531
|
| `maxConcurrency` | Running process limit and maximum tasks in one parallel call, from `1` to `16`. Default `4`. |
|
|
509
|
-
| `maxFixRounds` | Maximum worker fixes after `REVIEW_FAIL`; each fix is
|
|
532
|
+
| `maxFixRounds` | Maximum worker fixes after `REVIEW_FAIL`; each fix is re-reviewed by a reviewer, and one final documentation sync runs after the chain settles. `0` disables fixes but not the post-writer review gate or final docs. Default `2`. |
|
|
510
533
|
| `idleTimeoutSec` | Seconds without RPC output before termination. `0` disables the watchdog. Default `90`. |
|
|
511
534
|
|
|
512
535
|
Invalid values fall back safely. Older configs are normalized automatically. The
|
package/agents/cleaner.md
CHANGED
|
@@ -43,9 +43,9 @@ Never simplify away authorization, validation at trust boundaries, security cont
|
|
|
43
43
|
- Do not weaken a meaningful check to force a cut through. Repair or revert only the current batch when evidence fails.
|
|
44
44
|
|
|
45
45
|
## Release boundary
|
|
46
|
-
Never commit, push, publish, tag, release, or bump a package version. The parent workflow owns
|
|
46
|
+
Never commit, push, publish, tag, release, or bump a package version. The parent workflow owns the independent review gate, the final documentation sync, and every release action—even when repository instructions normally automate release after green checks.
|
|
47
47
|
|
|
48
48
|
## Final response
|
|
49
49
|
Return only the cleanup outcome: exact files/contracts removed or consolidated, measurable net reduction, behavior tradeoffs, and checks actually run. Mention a kept candidate only when the caller must make a product decision or it blocks an otherwise safe cut. If no safe cut was proved, say so and make no edits. Do not repeat the task brief or evidence-gathering chronology. Omit transient tool failures that were recovered; report only unresolved blockers and checks that remain failed. Keep the final response comfortably below the 80-line delivery cap unless the result genuinely requires more. Never equate green tests with proof, or deletion volume with value.
|
|
50
50
|
|
|
51
|
-
The parent runtime automatically runs enabled `
|
|
51
|
+
The parent runtime automatically runs the enabled `reviewer` gate and final `documenter` sync after a successful top-level cleaner. Provide a complete handoff without asking the caller to dispatch duplicate downstream roles. Reviewer gates the code; documenter is the final stage before delivery.
|
package/agents/documenter.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: documenter
|
|
3
|
-
description: "Write-capable documentation synchronizer with two modes:
|
|
3
|
+
description: "Write-capable documentation synchronizer with two modes: final diff sync after the review gate settles, or an explicitly requested whole-codebase comment/README/docs maintenance pass. May make zero edits and never changes runtime behavior."
|
|
4
4
|
tools: read, grep, find, ls, bash, edit, write
|
|
5
5
|
# At launch, this shell slot follows the parent and parent-active plugin tools
|
|
6
6
|
# are appended; the listed non-shell Pi built-ins remain the permission boundary.
|
|
@@ -15,7 +15,7 @@ You are a documenter agent: a write-capable specialist for keeping comments, REA
|
|
|
15
15
|
You may edit documentation and comments, but you must never change runtime behavior to make the documentation true. Finding no drift and making zero edits is valid.
|
|
16
16
|
|
|
17
17
|
## Choose the mode
|
|
18
|
-
- **Pre-commit diff sync (default for a concrete change):** run after implementation, cleanup, or
|
|
18
|
+
- **Pre-commit diff sync (default for a concrete change):** run after the code review gate settles — following implementation, cleanup, or auto-fix rounds — as the final managed stage before delivery. Inspect the complete pending diff, apply every documentation note the reviewers recorded, and synchronize every documentation surface affected by it.
|
|
19
19
|
- **Whole-codebase maintenance:** run only when the user explicitly asks to refresh, re-document, or audit-and-update comments/README/docs across an existing project. Inspect the whole requested codebase or scope, prove each stale statement against implementation, and apply every safe in-scope correction. Do not trigger this broad mode merely because a diff is large or a PR exists.
|
|
20
20
|
- If the brief does not explicitly authorize a whole-codebase pass, stay in diff mode. A read-only documentation audit belongs to `reviewer`, not this write-capable role.
|
|
21
21
|
|
|
@@ -43,4 +43,4 @@ Return only the documentation outcome:
|
|
|
43
43
|
|
|
44
44
|
Do not repeat the task brief, diff walkthrough, generic root-cause explanation, or tool chronology. Omit transient tool failures that were recovered; report only checks that remain failed or blockers that remain unresolved. Mention diff mode versus whole-codebase mode only when it materially clarifies scope. Keep the final response comfortably below the 80-line delivery cap unless the result genuinely requires more.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
A successful top-level `documenter` still gets a fresh read-only `reviewer` gate when that role is enabled; report a complete handoff without requesting a duplicate dispatch. When you run as the final managed stage after a settled gate, the workflow delivers directly after you and no fresh reviewer runs. You are always the last writer, never the code approver.
|
package/agents/reviewer.md
CHANGED
|
@@ -41,8 +41,8 @@ You are a senior, adversarial code reviewer. Find genuine defects and risks rath
|
|
|
41
41
|
## Reporting discipline
|
|
42
42
|
- Report only defensible defects or risks with file:line evidence; omit preferences and optional nits.
|
|
43
43
|
- Return only the review result. Do not repeat the task brief, summarize the implementation, narrate inspection/tool chronology, or explain a root cause when no finding depends on it. Omit transient tool failures that were recovered; report only unresolved coverage gaps.
|
|
44
|
-
- Stay independent of `worker`, `cleaner`, and `documenter`; fix nothing yourself. When
|
|
45
|
-
- In a gate, every finding enters auto-fix, with no severity tiers. A direct REVIEW_PASS is
|
|
44
|
+
- Stay independent of `worker`, `cleaner`, and `documenter`; fix nothing yourself. When the final documenter is pending (documenter is enabled), documentation sync runs AFTER this gate: documentation drift is not a gate finding — record needed updates as a short "## Documentation notes" list and carry it forward on re-review so the final documenter applies it. When documenter is disabled, documentation drift is a normal finding.
|
|
45
|
+
- In a gate, every finding enters auto-fix, with no severity tiers. A direct REVIEW_PASS is final for code: runtime runs the final documentation sync once, then delivers; those documentation edits never reopen the gate. On re-review, rule on each open finding once, concretely adjudicate worker rejections, add only defects the fix introduced or exposed, and never re-open a verified resolution.
|
|
46
46
|
- Advisory findings never enter auto-fix; the caller decides whether to authorize later implementation or cleanup.
|
|
47
47
|
|
|
48
48
|
## Output
|
package/agents/worker.md
CHANGED
|
@@ -28,13 +28,13 @@ Run the project's format/build/tests when they exist (e.g. `tsc --noEmit`, the t
|
|
|
28
28
|
Return only the concrete outcome so the caller can verify it and, if needed, hand it to a `reviewer`. Do not repeat the task brief, plan, root-cause investigation, or tool chronology. Omit transient tool failures that were recovered; report only checks that remain failed or blockers that remain unresolved.
|
|
29
29
|
|
|
30
30
|
## Release boundary
|
|
31
|
-
Never commit, push, publish, tag, release, or bump a package version. The parent workflow owns
|
|
31
|
+
Never commit, push, publish, tag, release, or bump a package version. The parent workflow owns the independent review gate, the final documentation sync, and every release action—even when repository instructions normally automate release after green checks.
|
|
32
32
|
|
|
33
33
|
## Collaboration
|
|
34
34
|
- You cannot dispatch sub-agents (children are leaf processes with no `subagent` tool). When the
|
|
35
35
|
brief lacks context that needs broad code discovery, state concretely what an `explorer` should
|
|
36
36
|
retrieve for the caller — do not guess.
|
|
37
|
-
- The parent runtime automatically runs enabled `
|
|
37
|
+
- The parent runtime automatically runs the enabled `reviewer` gate and final `documenter` sync after a successful top-level worker. Report a complete handoff, but do not ask the caller to dispatch duplicate downstream roles. Never treat your own verification as the final gate.
|
|
38
38
|
|
|
39
39
|
## Output format
|
|
40
40
|
## Completed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.4",
|
|
4
4
|
"description": "A managed sub-agent team for pi: specialized roles, pre-commit documentation sync, retained threads, auto-fix chains, model fallback, and Git worktree isolation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/completion.ts
CHANGED
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Smart batching for successful background completions.
|
|
3
|
-
*
|
|
4
|
-
* A short debounce coalesces sibling runs while a max-wait timer, measured from
|
|
5
|
-
* the first item in the open group, bounds delivery latency. Failures are
|
|
6
|
-
* intentionally handled by the caller: flush held successes, then emit the
|
|
7
|
-
* failure directly so it is never delayed.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
11
|
-
import { formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
12
|
-
import type { UsageStats } from "./rpc-run.ts";
|
|
13
|
-
|
|
14
|
-
export interface CompletionBatchTimings {
|
|
15
|
-
debounceMs: number;
|
|
16
|
-
maxWaitMs: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const DEFAULT_COMPLETION_BATCH_TIMINGS: CompletionBatchTimings = {
|
|
20
|
-
debounceMs: 150,
|
|
21
|
-
maxWaitMs: 1_000,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
25
|
-
|
|
26
|
-
function unrefHandle(handle: TimerHandle): void {
|
|
27
|
-
if (
|
|
28
|
-
handle &&
|
|
29
|
-
typeof handle === "object" &&
|
|
30
|
-
"unref" in handle &&
|
|
31
|
-
typeof (handle as { unref: unknown }).unref === "function"
|
|
32
|
-
) {
|
|
33
|
-
(handle as { unref: () => void }).unref();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface CompletionBatcherOptions<T> {
|
|
38
|
-
emit: (items: T[]) => void;
|
|
39
|
-
timings?: Partial<CompletionBatchTimings>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface CompletionBatcher<T> {
|
|
43
|
-
/** Add an item to the current debounced group. */
|
|
44
|
-
push(item: T): void;
|
|
45
|
-
/** Emit any held items immediately as one group. */
|
|
46
|
-
flush(): void;
|
|
47
|
-
/** Clear timers and return held items without emitting them. */
|
|
48
|
-
dispose(): T[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
|
|
52
|
-
const timings = { ...DEFAULT_COMPLETION_BATCH_TIMINGS, ...options.timings };
|
|
53
|
-
let pending: T[] = [];
|
|
54
|
-
let debounceTimer: TimerHandle | null = null;
|
|
55
|
-
let maxWaitTimer: TimerHandle | null = null;
|
|
56
|
-
|
|
57
|
-
const clearTimers = (): void => {
|
|
58
|
-
if (debounceTimer !== null) {
|
|
59
|
-
clearTimeout(debounceTimer);
|
|
60
|
-
debounceTimer = null;
|
|
61
|
-
}
|
|
62
|
-
if (maxWaitTimer !== null) {
|
|
63
|
-
clearTimeout(maxWaitTimer);
|
|
64
|
-
maxWaitTimer = null;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const emitGroup = (): void => {
|
|
69
|
-
clearTimers();
|
|
70
|
-
if (pending.length === 0) return;
|
|
71
|
-
const items = pending;
|
|
72
|
-
pending = [];
|
|
73
|
-
options.emit(items);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
push(item: T): void {
|
|
78
|
-
pending.push(item);
|
|
79
|
-
|
|
80
|
-
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
81
|
-
debounceTimer = setTimeout(emitGroup, timings.debounceMs);
|
|
82
|
-
unrefHandle(debounceTimer);
|
|
83
|
-
|
|
84
|
-
if (maxWaitTimer === null) {
|
|
85
|
-
maxWaitTimer = setTimeout(emitGroup, timings.maxWaitMs);
|
|
86
|
-
unrefHandle(maxWaitTimer);
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
flush: emitGroup,
|
|
90
|
-
dispose(): T[] {
|
|
91
|
-
clearTimers();
|
|
92
|
-
const abandoned = pending;
|
|
93
|
-
pending = [];
|
|
94
|
-
return abandoned;
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface CompletionMessageItem {
|
|
100
|
-
agent: string;
|
|
101
|
-
block: string;
|
|
102
|
-
triggerTurn: boolean;
|
|
103
|
-
/** Final usage of the underlying run (or chain); aggregated into the group totals. */
|
|
104
|
-
usage?: UsageStats;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Keep the established single-result shape; add a group header and an aggregate
|
|
108
|
-
* token/cost footer only for real groups. */
|
|
109
|
-
export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
|
|
110
|
-
if (items.length === 0) return "";
|
|
111
|
-
if (items.length === 1) return items[0].block;
|
|
112
|
-
const agents = items.map((item) => item.agent).join(", ");
|
|
113
|
-
const withUsage = items.filter((item) => item.usage !== undefined);
|
|
114
|
-
const totals = withUsage.length > 0 ? formatUsageCompact(sumUsage(withUsage.map((item) => item.usage!))) : "";
|
|
115
|
-
const footer = totals ? `\n\nTotals: ${items.length} runs · ${totals}` : "";
|
|
116
|
-
return `### Subagents completed (${items.length}): ${agents}\n\n${items.map((item) => item.block).join("\n\n")}${footer}`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** A grouped completion wakes the main agent when any member requires a turn. */
|
|
120
|
-
export function completionGroupTriggersTurn(items: readonly CompletionMessageItem[]): boolean {
|
|
121
|
-
return items.some((item) => item.triggerTurn);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Passing reviewer notifications may opt out of waking; every other result wakes. */
|
|
125
|
-
export function completionTriggersTurn(result: SingleResult, notifyOnReviewPass: boolean): boolean {
|
|
126
|
-
if (isFailedResult(result)) return true;
|
|
127
|
-
return !(
|
|
128
|
-
notifyOnReviewPass &&
|
|
129
|
-
result.agent === "reviewer" &&
|
|
130
|
-
reviewVerdict(getResultOutput(result)) === "pass"
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Minimal shape of an active run, for the "others still running" footer. Kept
|
|
135
|
-
* decoupled from the monitor's RunView so this stays a pure, easily tested
|
|
136
|
-
* formatter; the caller maps its live runs into this shape. */
|
|
137
|
-
export interface ActiveRunFoot {
|
|
138
|
-
id: number;
|
|
139
|
-
agent: string;
|
|
140
|
-
/** Optional content label (task-derived) shown next to the agent name. */
|
|
141
|
-
label?: string;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Footer appended to a completion message when OTHER runs are still active, so
|
|
146
|
-
* the main agent does not declare the overall task done prematurely. A result
|
|
147
|
-
* arriving for one run does not mean sibling runs are finished; naming them
|
|
148
|
-
* gives the main agent concrete, in-context awareness to keep waiting.
|
|
149
|
-
*
|
|
150
|
-
* Returns "" when nothing is active (the common, single-run case stays quiet).
|
|
151
|
-
*/
|
|
152
|
-
export function formatActiveRunsFooter(runs: readonly ActiveRunFoot[], maxListed = 4): string {
|
|
153
|
-
if (runs.length === 0) return "";
|
|
154
|
-
const listed = runs.slice(0, maxListed);
|
|
155
|
-
const items = listed
|
|
156
|
-
.map((run) => `#${run.id} ${run.agent}${run.label ? `·${run.label}` : ""}`)
|
|
157
|
-
.join(", ");
|
|
158
|
-
const more = runs.length > listed.length ? `, +${runs.length - listed.length} more` : "";
|
|
159
|
-
return `\n\n⚠ ${runs.length} other run${runs.length === 1 ? "" : "s"} still active: ${items}${more}. Do not conclude the overall task yet — wait for their results (they wake you automatically) or check subagent_status.`;
|
|
160
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Smart batching for successful background completions.
|
|
3
|
+
*
|
|
4
|
+
* A short debounce coalesces sibling runs while a max-wait timer, measured from
|
|
5
|
+
* the first item in the open group, bounds delivery latency. Failures are
|
|
6
|
+
* intentionally handled by the caller: flush held successes, then emit the
|
|
7
|
+
* failure directly so it is never delayed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
11
|
+
import { formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
12
|
+
import type { UsageStats } from "./rpc-run.ts";
|
|
13
|
+
|
|
14
|
+
export interface CompletionBatchTimings {
|
|
15
|
+
debounceMs: number;
|
|
16
|
+
maxWaitMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_COMPLETION_BATCH_TIMINGS: CompletionBatchTimings = {
|
|
20
|
+
debounceMs: 150,
|
|
21
|
+
maxWaitMs: 1_000,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
25
|
+
|
|
26
|
+
function unrefHandle(handle: TimerHandle): void {
|
|
27
|
+
if (
|
|
28
|
+
handle &&
|
|
29
|
+
typeof handle === "object" &&
|
|
30
|
+
"unref" in handle &&
|
|
31
|
+
typeof (handle as { unref: unknown }).unref === "function"
|
|
32
|
+
) {
|
|
33
|
+
(handle as { unref: () => void }).unref();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CompletionBatcherOptions<T> {
|
|
38
|
+
emit: (items: T[]) => void;
|
|
39
|
+
timings?: Partial<CompletionBatchTimings>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CompletionBatcher<T> {
|
|
43
|
+
/** Add an item to the current debounced group. */
|
|
44
|
+
push(item: T): void;
|
|
45
|
+
/** Emit any held items immediately as one group. */
|
|
46
|
+
flush(): void;
|
|
47
|
+
/** Clear timers and return held items without emitting them. */
|
|
48
|
+
dispose(): T[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
|
|
52
|
+
const timings = { ...DEFAULT_COMPLETION_BATCH_TIMINGS, ...options.timings };
|
|
53
|
+
let pending: T[] = [];
|
|
54
|
+
let debounceTimer: TimerHandle | null = null;
|
|
55
|
+
let maxWaitTimer: TimerHandle | null = null;
|
|
56
|
+
|
|
57
|
+
const clearTimers = (): void => {
|
|
58
|
+
if (debounceTimer !== null) {
|
|
59
|
+
clearTimeout(debounceTimer);
|
|
60
|
+
debounceTimer = null;
|
|
61
|
+
}
|
|
62
|
+
if (maxWaitTimer !== null) {
|
|
63
|
+
clearTimeout(maxWaitTimer);
|
|
64
|
+
maxWaitTimer = null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const emitGroup = (): void => {
|
|
69
|
+
clearTimers();
|
|
70
|
+
if (pending.length === 0) return;
|
|
71
|
+
const items = pending;
|
|
72
|
+
pending = [];
|
|
73
|
+
options.emit(items);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
push(item: T): void {
|
|
78
|
+
pending.push(item);
|
|
79
|
+
|
|
80
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
81
|
+
debounceTimer = setTimeout(emitGroup, timings.debounceMs);
|
|
82
|
+
unrefHandle(debounceTimer);
|
|
83
|
+
|
|
84
|
+
if (maxWaitTimer === null) {
|
|
85
|
+
maxWaitTimer = setTimeout(emitGroup, timings.maxWaitMs);
|
|
86
|
+
unrefHandle(maxWaitTimer);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
flush: emitGroup,
|
|
90
|
+
dispose(): T[] {
|
|
91
|
+
clearTimers();
|
|
92
|
+
const abandoned = pending;
|
|
93
|
+
pending = [];
|
|
94
|
+
return abandoned;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface CompletionMessageItem {
|
|
100
|
+
agent: string;
|
|
101
|
+
block: string;
|
|
102
|
+
triggerTurn: boolean;
|
|
103
|
+
/** Final usage of the underlying run (or chain); aggregated into the group totals. */
|
|
104
|
+
usage?: UsageStats;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Keep the established single-result shape; add a group header and an aggregate
|
|
108
|
+
* token/cost footer only for real groups. */
|
|
109
|
+
export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
|
|
110
|
+
if (items.length === 0) return "";
|
|
111
|
+
if (items.length === 1) return items[0].block;
|
|
112
|
+
const agents = items.map((item) => item.agent).join(", ");
|
|
113
|
+
const withUsage = items.filter((item) => item.usage !== undefined);
|
|
114
|
+
const totals = withUsage.length > 0 ? formatUsageCompact(sumUsage(withUsage.map((item) => item.usage!))) : "";
|
|
115
|
+
const footer = totals ? `\n\nTotals: ${items.length} runs · ${totals}` : "";
|
|
116
|
+
return `### Subagents completed (${items.length}): ${agents}\n\n${items.map((item) => item.block).join("\n\n")}${footer}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A grouped completion wakes the main agent when any member requires a turn. */
|
|
120
|
+
export function completionGroupTriggersTurn(items: readonly CompletionMessageItem[]): boolean {
|
|
121
|
+
return items.some((item) => item.triggerTurn);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Passing reviewer notifications may opt out of waking; every other result wakes. */
|
|
125
|
+
export function completionTriggersTurn(result: SingleResult, notifyOnReviewPass: boolean): boolean {
|
|
126
|
+
if (isFailedResult(result)) return true;
|
|
127
|
+
return !(
|
|
128
|
+
notifyOnReviewPass &&
|
|
129
|
+
result.agent === "reviewer" &&
|
|
130
|
+
reviewVerdict(getResultOutput(result)) === "pass"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Minimal shape of an active run, for the "others still running" footer. Kept
|
|
135
|
+
* decoupled from the monitor's RunView so this stays a pure, easily tested
|
|
136
|
+
* formatter; the caller maps its live runs into this shape. */
|
|
137
|
+
export interface ActiveRunFoot {
|
|
138
|
+
id: number;
|
|
139
|
+
agent: string;
|
|
140
|
+
/** Optional content label (task-derived) shown next to the agent name. */
|
|
141
|
+
label?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Footer appended to a completion message when OTHER runs are still active, so
|
|
146
|
+
* the main agent does not declare the overall task done prematurely. A result
|
|
147
|
+
* arriving for one run does not mean sibling runs are finished; naming them
|
|
148
|
+
* gives the main agent concrete, in-context awareness to keep waiting.
|
|
149
|
+
*
|
|
150
|
+
* Returns "" when nothing is active (the common, single-run case stays quiet).
|
|
151
|
+
*/
|
|
152
|
+
export function formatActiveRunsFooter(runs: readonly ActiveRunFoot[], maxListed = 4): string {
|
|
153
|
+
if (runs.length === 0) return "";
|
|
154
|
+
const listed = runs.slice(0, maxListed);
|
|
155
|
+
const items = listed
|
|
156
|
+
.map((run) => `#${run.id} ${run.agent}${run.label ? `·${run.label}` : ""}`)
|
|
157
|
+
.join(", ");
|
|
158
|
+
const more = runs.length > listed.length ? `, +${runs.length - listed.length} more` : "";
|
|
159
|
+
return `\n\n⚠ ${runs.length} other run${runs.length === 1 ? "" : "s"} still active: ${items}${more}. Do not conclude the overall task yet — wait for their results (they wake you automatically) or check subagent_status.`;
|
|
160
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -72,9 +72,9 @@ export const DEFAULT_MAX_CONCURRENCY = 4;
|
|
|
72
72
|
/** Upper bound accepted for maxConcurrency (defensive clamp). */
|
|
73
73
|
export const MAX_CONCURRENCY_LIMIT = 16;
|
|
74
74
|
/**
|
|
75
|
-
* Maximum worker fixes after REVIEW_FAIL. Each fix is followed by
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* Maximum worker fixes after REVIEW_FAIL. Each fix is followed by a reviewer
|
|
76
|
+
* re-review; this cap does not suppress the post-writer review gate or the
|
|
77
|
+
* final documentation sync. 0 disables fixes. Default: 2.
|
|
78
78
|
*/
|
|
79
79
|
export const DEFAULT_MAX_FIX_ROUNDS = 2;
|
|
80
80
|
/** Upper bound accepted for maxFixRounds (defensive clamp). 0 disables the loop. */
|
|
@@ -116,8 +116,8 @@ export interface SubagentsConfig {
|
|
|
116
116
|
maxConcurrency: number;
|
|
117
117
|
/**
|
|
118
118
|
* Maximum worker fixes after REVIEW_FAIL. Every fix receives the full review,
|
|
119
|
-
* then
|
|
120
|
-
*
|
|
119
|
+
* then a re-review runs; one final documentation sync follows the settled
|
|
120
|
+
* chain and does not consume this budget. 0 disables fixes. Default: 2.
|
|
121
121
|
*/
|
|
122
122
|
maxFixRounds: number;
|
|
123
123
|
/**
|