@ferris1225/pi-subagents 4.2.8 → 4.2.13
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/CHANGELOG.md +61 -0
- package/LICENSE +23 -23
- package/README.md +60 -4
- package/agents/executor.md +4 -3
- package/agents/explorer.md +37 -37
- package/package.json +11 -2
- package/src/background.ts +205 -205
- package/src/completion.ts +165 -165
- package/src/config.ts +308 -308
- package/src/dispatch.ts +10 -4
- package/src/format.ts +8 -6
- package/src/models.ts +203 -203
- package/src/monitor.ts +3 -2
- package/src/prompt.ts +1 -0
- package/src/recovery.ts +163 -163
- package/src/session-fork.ts +86 -86
- package/src/setup.ts +341 -341
- package/src/spawn.ts +663 -658
- package/src/status.ts +4 -3
- package/src/temp-hygiene.ts +230 -230
- package/src/tools.ts +1 -1
- package/src/ui.ts +248 -248
package/src/completion.ts
CHANGED
|
@@ -1,165 +1,165 @@
|
|
|
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 { formatUsageCompact, sumUsage, type RunWaitReason } from "./monitor.ts";
|
|
11
|
-
import type { UsageStats } from "./rpc-run.ts";
|
|
12
|
-
|
|
13
|
-
export interface CompletionBatchTimings {
|
|
14
|
-
debounceMs: number;
|
|
15
|
-
maxWaitMs: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const DEFAULT_COMPLETION_BATCH_TIMINGS: CompletionBatchTimings = {
|
|
19
|
-
debounceMs: 150,
|
|
20
|
-
maxWaitMs: 1_000,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
24
|
-
|
|
25
|
-
function unrefHandle(handle: TimerHandle): void {
|
|
26
|
-
if (
|
|
27
|
-
handle &&
|
|
28
|
-
typeof handle === "object" &&
|
|
29
|
-
"unref" in handle &&
|
|
30
|
-
typeof (handle as { unref: unknown }).unref === "function"
|
|
31
|
-
) {
|
|
32
|
-
(handle as { unref: () => void }).unref();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface CompletionBatcherOptions<T> {
|
|
37
|
-
emit: (items: T[]) => void;
|
|
38
|
-
timings?: Partial<CompletionBatchTimings>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface CompletionBatcher<T> {
|
|
42
|
-
/** Add an item to the current debounced group. */
|
|
43
|
-
push(item: T): void;
|
|
44
|
-
/** Emit any held items immediately as one group. */
|
|
45
|
-
flush(): void;
|
|
46
|
-
/** Clear timers and return held items without emitting them. */
|
|
47
|
-
dispose(): T[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
|
|
51
|
-
const timings = { ...DEFAULT_COMPLETION_BATCH_TIMINGS, ...options.timings };
|
|
52
|
-
let pending: T[] = [];
|
|
53
|
-
let debounceTimer: TimerHandle | null = null;
|
|
54
|
-
let maxWaitTimer: TimerHandle | null = null;
|
|
55
|
-
|
|
56
|
-
const clearTimers = (): void => {
|
|
57
|
-
if (debounceTimer !== null) {
|
|
58
|
-
clearTimeout(debounceTimer);
|
|
59
|
-
debounceTimer = null;
|
|
60
|
-
}
|
|
61
|
-
if (maxWaitTimer !== null) {
|
|
62
|
-
clearTimeout(maxWaitTimer);
|
|
63
|
-
maxWaitTimer = null;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const emitGroup = (): void => {
|
|
68
|
-
clearTimers();
|
|
69
|
-
if (pending.length === 0) return;
|
|
70
|
-
const items = pending;
|
|
71
|
-
pending = [];
|
|
72
|
-
options.emit(items);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
push(item: T): void {
|
|
77
|
-
pending.push(item);
|
|
78
|
-
|
|
79
|
-
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
80
|
-
debounceTimer = setTimeout(emitGroup, timings.debounceMs);
|
|
81
|
-
unrefHandle(debounceTimer);
|
|
82
|
-
|
|
83
|
-
if (maxWaitTimer === null) {
|
|
84
|
-
maxWaitTimer = setTimeout(emitGroup, timings.maxWaitMs);
|
|
85
|
-
unrefHandle(maxWaitTimer);
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
flush: emitGroup,
|
|
89
|
-
dispose(): T[] {
|
|
90
|
-
clearTimers();
|
|
91
|
-
const abandoned = pending;
|
|
92
|
-
pending = [];
|
|
93
|
-
return abandoned;
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface CompletionMessageItem {
|
|
99
|
-
agent: string;
|
|
100
|
-
block: string;
|
|
101
|
-
triggerTurn: boolean;
|
|
102
|
-
/** Final usage of the underlying run (or chain); aggregated into the group totals. */
|
|
103
|
-
usage?: UsageStats;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Keep the established single-result shape; add a group header and an aggregate
|
|
107
|
-
* token/cost footer only for real groups. */
|
|
108
|
-
export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
|
|
109
|
-
if (items.length === 0) return "";
|
|
110
|
-
if (items.length === 1) return items[0].block;
|
|
111
|
-
const agents = items.map((item) => item.agent).join(", ");
|
|
112
|
-
const withUsage = items.filter((item) => item.usage !== undefined);
|
|
113
|
-
const totals = withUsage.length > 0 ? formatUsageCompact(sumUsage(withUsage.map((item) => item.usage!))) : "";
|
|
114
|
-
const footer = totals ? `\n\nTotals: ${items.length} runs · ${totals}` : "";
|
|
115
|
-
return `### Subagents completed (${items.length}): ${agents}\n\n${items.map((item) => item.block).join("\n\n")}${footer}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** A grouped completion wakes the main agent when any member requires a turn. */
|
|
119
|
-
export function completionGroupTriggersTurn(items: readonly CompletionMessageItem[]): boolean {
|
|
120
|
-
return items.some((item) => item.triggerTurn);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Minimal shape of an active run, for the "others still running" footer. Kept
|
|
124
|
-
* decoupled from the monitor's RunView so this stays a pure, easily tested
|
|
125
|
-
* formatter; the caller maps its live runs into this shape. */
|
|
126
|
-
export interface ActiveRunFoot {
|
|
127
|
-
id: number;
|
|
128
|
-
agent: string;
|
|
129
|
-
/** Optional content label (task-derived) shown next to the agent name. */
|
|
130
|
-
label?: string;
|
|
131
|
-
/** Why a not-yet-executing run is waiting. Stated precisely so a repository
|
|
132
|
-
* lane wait or a starting child is never mistaken for an exhausted pool. */
|
|
133
|
-
wait?: RunWaitReason;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function activeRunWaitTag(wait: RunWaitReason | undefined): string {
|
|
137
|
-
switch (wait) {
|
|
138
|
-
case "process-slot":
|
|
139
|
-
return " (queued, starts when a process slot frees)";
|
|
140
|
-
case "repository-lane":
|
|
141
|
-
return " (waiting for the repository write lane, not for a slot)";
|
|
142
|
-
case "starting":
|
|
143
|
-
return " (starting)";
|
|
144
|
-
default:
|
|
145
|
-
return "";
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Footer appended to a completion message when OTHER runs are still active, so
|
|
151
|
-
* the main agent does not declare the overall task done prematurely. A result
|
|
152
|
-
* arriving for one run does not mean sibling runs are finished; naming them
|
|
153
|
-
* gives the main agent concrete, in-context awareness to keep waiting.
|
|
154
|
-
*
|
|
155
|
-
* Returns "" when nothing is active (the common, single-run case stays quiet).
|
|
156
|
-
*/
|
|
157
|
-
export function formatActiveRunsFooter(runs: readonly ActiveRunFoot[], maxListed = 4): string {
|
|
158
|
-
if (runs.length === 0) return "";
|
|
159
|
-
const listed = runs.slice(0, maxListed);
|
|
160
|
-
const items = listed
|
|
161
|
-
.map((run) => `#${run.id} ${run.agent}${run.label ? `·${run.label}` : ""}${activeRunWaitTag(run.wait)}`)
|
|
162
|
-
.join(", ");
|
|
163
|
-
const more = runs.length > listed.length ? `, +${runs.length - listed.length} more` : "";
|
|
164
|
-
return `\n\n⚠ ${runs.length} other run${runs.length === 1 ? "" : "s"} still active: ${items}${more}. Do not conclude the overall task yet — their results wake you automatically.`;
|
|
165
|
-
}
|
|
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 { formatUsageCompact, sumUsage, type RunWaitReason } from "./monitor.ts";
|
|
11
|
+
import type { UsageStats } from "./rpc-run.ts";
|
|
12
|
+
|
|
13
|
+
export interface CompletionBatchTimings {
|
|
14
|
+
debounceMs: number;
|
|
15
|
+
maxWaitMs: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_COMPLETION_BATCH_TIMINGS: CompletionBatchTimings = {
|
|
19
|
+
debounceMs: 150,
|
|
20
|
+
maxWaitMs: 1_000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
24
|
+
|
|
25
|
+
function unrefHandle(handle: TimerHandle): void {
|
|
26
|
+
if (
|
|
27
|
+
handle &&
|
|
28
|
+
typeof handle === "object" &&
|
|
29
|
+
"unref" in handle &&
|
|
30
|
+
typeof (handle as { unref: unknown }).unref === "function"
|
|
31
|
+
) {
|
|
32
|
+
(handle as { unref: () => void }).unref();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CompletionBatcherOptions<T> {
|
|
37
|
+
emit: (items: T[]) => void;
|
|
38
|
+
timings?: Partial<CompletionBatchTimings>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CompletionBatcher<T> {
|
|
42
|
+
/** Add an item to the current debounced group. */
|
|
43
|
+
push(item: T): void;
|
|
44
|
+
/** Emit any held items immediately as one group. */
|
|
45
|
+
flush(): void;
|
|
46
|
+
/** Clear timers and return held items without emitting them. */
|
|
47
|
+
dispose(): T[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
|
|
51
|
+
const timings = { ...DEFAULT_COMPLETION_BATCH_TIMINGS, ...options.timings };
|
|
52
|
+
let pending: T[] = [];
|
|
53
|
+
let debounceTimer: TimerHandle | null = null;
|
|
54
|
+
let maxWaitTimer: TimerHandle | null = null;
|
|
55
|
+
|
|
56
|
+
const clearTimers = (): void => {
|
|
57
|
+
if (debounceTimer !== null) {
|
|
58
|
+
clearTimeout(debounceTimer);
|
|
59
|
+
debounceTimer = null;
|
|
60
|
+
}
|
|
61
|
+
if (maxWaitTimer !== null) {
|
|
62
|
+
clearTimeout(maxWaitTimer);
|
|
63
|
+
maxWaitTimer = null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const emitGroup = (): void => {
|
|
68
|
+
clearTimers();
|
|
69
|
+
if (pending.length === 0) return;
|
|
70
|
+
const items = pending;
|
|
71
|
+
pending = [];
|
|
72
|
+
options.emit(items);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
push(item: T): void {
|
|
77
|
+
pending.push(item);
|
|
78
|
+
|
|
79
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
80
|
+
debounceTimer = setTimeout(emitGroup, timings.debounceMs);
|
|
81
|
+
unrefHandle(debounceTimer);
|
|
82
|
+
|
|
83
|
+
if (maxWaitTimer === null) {
|
|
84
|
+
maxWaitTimer = setTimeout(emitGroup, timings.maxWaitMs);
|
|
85
|
+
unrefHandle(maxWaitTimer);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
flush: emitGroup,
|
|
89
|
+
dispose(): T[] {
|
|
90
|
+
clearTimers();
|
|
91
|
+
const abandoned = pending;
|
|
92
|
+
pending = [];
|
|
93
|
+
return abandoned;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface CompletionMessageItem {
|
|
99
|
+
agent: string;
|
|
100
|
+
block: string;
|
|
101
|
+
triggerTurn: boolean;
|
|
102
|
+
/** Final usage of the underlying run (or chain); aggregated into the group totals. */
|
|
103
|
+
usage?: UsageStats;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Keep the established single-result shape; add a group header and an aggregate
|
|
107
|
+
* token/cost footer only for real groups. */
|
|
108
|
+
export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
|
|
109
|
+
if (items.length === 0) return "";
|
|
110
|
+
if (items.length === 1) return items[0].block;
|
|
111
|
+
const agents = items.map((item) => item.agent).join(", ");
|
|
112
|
+
const withUsage = items.filter((item) => item.usage !== undefined);
|
|
113
|
+
const totals = withUsage.length > 0 ? formatUsageCompact(sumUsage(withUsage.map((item) => item.usage!))) : "";
|
|
114
|
+
const footer = totals ? `\n\nTotals: ${items.length} runs · ${totals}` : "";
|
|
115
|
+
return `### Subagents completed (${items.length}): ${agents}\n\n${items.map((item) => item.block).join("\n\n")}${footer}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A grouped completion wakes the main agent when any member requires a turn. */
|
|
119
|
+
export function completionGroupTriggersTurn(items: readonly CompletionMessageItem[]): boolean {
|
|
120
|
+
return items.some((item) => item.triggerTurn);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Minimal shape of an active run, for the "others still running" footer. Kept
|
|
124
|
+
* decoupled from the monitor's RunView so this stays a pure, easily tested
|
|
125
|
+
* formatter; the caller maps its live runs into this shape. */
|
|
126
|
+
export interface ActiveRunFoot {
|
|
127
|
+
id: number;
|
|
128
|
+
agent: string;
|
|
129
|
+
/** Optional content label (task-derived) shown next to the agent name. */
|
|
130
|
+
label?: string;
|
|
131
|
+
/** Why a not-yet-executing run is waiting. Stated precisely so a repository
|
|
132
|
+
* lane wait or a starting child is never mistaken for an exhausted pool. */
|
|
133
|
+
wait?: RunWaitReason;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function activeRunWaitTag(wait: RunWaitReason | undefined): string {
|
|
137
|
+
switch (wait) {
|
|
138
|
+
case "process-slot":
|
|
139
|
+
return " (queued, starts when a process slot frees)";
|
|
140
|
+
case "repository-lane":
|
|
141
|
+
return " (waiting for the repository write lane, not for a slot)";
|
|
142
|
+
case "starting":
|
|
143
|
+
return " (starting)";
|
|
144
|
+
default:
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Footer appended to a completion message when OTHER runs are still active, so
|
|
151
|
+
* the main agent does not declare the overall task done prematurely. A result
|
|
152
|
+
* arriving for one run does not mean sibling runs are finished; naming them
|
|
153
|
+
* gives the main agent concrete, in-context awareness to keep waiting.
|
|
154
|
+
*
|
|
155
|
+
* Returns "" when nothing is active (the common, single-run case stays quiet).
|
|
156
|
+
*/
|
|
157
|
+
export function formatActiveRunsFooter(runs: readonly ActiveRunFoot[], maxListed = 4): string {
|
|
158
|
+
if (runs.length === 0) return "";
|
|
159
|
+
const listed = runs.slice(0, maxListed);
|
|
160
|
+
const items = listed
|
|
161
|
+
.map((run) => `#${run.id} ${run.agent}${run.label ? `·${run.label}` : ""}${activeRunWaitTag(run.wait)}`)
|
|
162
|
+
.join(", ");
|
|
163
|
+
const more = runs.length > listed.length ? `, +${runs.length - listed.length} more` : "";
|
|
164
|
+
return `\n\n⚠ ${runs.length} other run${runs.length === 1 ? "" : "s"} still active: ${items}${more}. Do not conclude the overall task yet — their results wake you automatically.`;
|
|
165
|
+
}
|