@ferris1225/pi-subagents 4.1.3 → 4.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,10 @@ 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 optional
76
- * documenter and reviewer; this cap does not suppress the initial post-writer
77
- * documentation/final-review workflow. 0 disables fixes. Default: 2.
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 its
77
+ * conditional/reviewer-disabled documentation fallback. 0 disables fixes.
78
+ * Default: 2.
78
79
  */
79
80
  export const DEFAULT_MAX_FIX_ROUNDS = 2;
80
81
  /** Upper bound accepted for maxFixRounds (defensive clamp). 0 disables the loop. */
@@ -116,8 +117,8 @@ export interface SubagentsConfig {
116
117
  maxConcurrency: number;
117
118
  /**
118
119
  * Maximum worker fixes after REVIEW_FAIL. Every fix receives the full review,
119
- * then enabled documenter/reviewer stages run. Initial post-writer docs/review
120
- * do not consume this budget. 0 disables fixes. Default: 2.
120
+ * then a re-review runs; any documentation sync selected after the terminal
121
+ * healthy review does not consume this budget. 0 disables fixes. Default: 2.
121
122
  */
122
123
  maxFixRounds: number;
123
124
  /**
@@ -247,7 +248,8 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
247
248
  const maxConcurrency = clampCount(raw.maxConcurrency, MAX_CONCURRENCY_LIMIT);
248
249
  if (maxConcurrency !== undefined) config.maxConcurrency = maxConcurrency;
249
250
 
250
- // 0 disables worker fixes, not the initial managed docs/review workflow.
251
+ // 0 disables worker fixes, not the independent post-writer review gate or
252
+ // conditional/reviewer-disabled documentation fallback.
251
253
  if (typeof raw.maxFixRounds === "number" && Number.isFinite(raw.maxFixRounds)) {
252
254
  config.maxFixRounds = Math.max(0, Math.min(MAX_FIX_ROUNDS_LIMIT, Math.round(raw.maxFixRounds)));
253
255
  }