@d3ara1n/pi-subagent 0.10.1 → 0.10.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 +17 -10
- package/package.json +1 -1
- package/src/config.test.ts +2 -7
- package/src/config.ts +0 -1
- package/src/index.ts +15 -32
- package/src/roles.ts +4 -0
- package/src/types.ts +1 -4
- package/src/utils.test.ts +50 -8
- package/src/utils.ts +15 -7
package/README.md
CHANGED
|
@@ -30,12 +30,12 @@ This means:
|
|
|
30
30
|
|
|
31
31
|
## Built-in Roles
|
|
32
32
|
|
|
33
|
-
| Role | Model Role | Tools | Can Delegate To | Description |
|
|
34
|
-
|
|
35
|
-
| `explorer` | fast | read, find, grep | — | Fast code search (read-only, no bash) |
|
|
36
|
-
| `reviewer` | heavy | read, bash, grep, find | — | Deep code review (read-only, bash for git/log) |
|
|
37
|
-
| `worker` | default | read, bash, edit, write, grep, find, delegate | explorer, researcher | Implementation — the only role that can modify files |
|
|
38
|
-
| `researcher` | fast | web_search, fetch_content, read, bash, delegate | explorer | Web research + GitHub repo analysis |
|
|
33
|
+
| Role | Model Role | Timeout | Tools | Can Delegate To | Description |
|
|
34
|
+
|------|-----------|---------|-------|-----------------|-------------|
|
|
35
|
+
| `explorer` | fast | 900s | read, find, grep | — | Fast code search (read-only, no bash) |
|
|
36
|
+
| `reviewer` | heavy | 3600s | read, bash, grep, find | — | Deep code review (read-only, bash for git/log) |
|
|
37
|
+
| `worker` | default | 2400s | read, bash, edit, write, grep, find, delegate | explorer, researcher | Implementation — the only role that can modify files |
|
|
38
|
+
| `researcher` | fast | 2400s | web_search, fetch_content, read, bash, delegate | explorer | Web research + GitHub repo analysis |
|
|
39
39
|
|
|
40
40
|
**Nested delegation**: `worker` and `researcher` can spawn their own subagents. This keeps the main model's context clean — a worker can explore unfamiliar code via an `explorer` subagent without returning intermediate results to the main model.
|
|
41
41
|
|
|
@@ -47,6 +47,12 @@ This means:
|
|
|
47
47
|
- **Collapsed result**: `✓ explorer · Found login, registration, and token logic` + recent tool calls + usage stats
|
|
48
48
|
- **Expanded result** (Ctrl+O): Full task text, all tool calls, final output as rendered Markdown, and usage details
|
|
49
49
|
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
| Command | Description |
|
|
53
|
+
|---------|-------------|
|
|
54
|
+
| `/subagent:doctor` | Diagnose pi invocation, model-role resolution, configuration, and role references |
|
|
55
|
+
|
|
50
56
|
## Dependencies
|
|
51
57
|
|
|
52
58
|
- [`@d3ara1n/pi-model-roles`](../pi-model-roles) — model role resolution
|
|
@@ -76,7 +82,6 @@ Edit `~/.pi/agent/settings.json`:
|
|
|
76
82
|
```json
|
|
77
83
|
{
|
|
78
84
|
"subagent": {
|
|
79
|
-
"timeout": 1500,
|
|
80
85
|
"maxConcurrency": 4,
|
|
81
86
|
"maxDepth": 3,
|
|
82
87
|
"maxTurns": 0,
|
|
@@ -92,9 +97,11 @@ Edit `~/.pi/agent/settings.json`:
|
|
|
92
97
|
}
|
|
93
98
|
```
|
|
94
99
|
|
|
95
|
-
All fields are optional. Defaults: `
|
|
100
|
+
All fields are optional. Defaults: `maxConcurrency: 4`, `maxDepth: 3`, `maxTurns: 0` (unlimited), `maxCost: 0` (unlimited), `history.enabled: true`, `summary.role: "utility"`, `summary.enabled: true`.
|
|
101
|
+
|
|
102
|
+
Timeouts are defined per role. Built-in defaults are `explorer: 900`, `reviewer: 3600`, `worker: 2400`, and `researcher: 2400` seconds. The timeout is active time — the clock pauses while the child is inside a nested `delegate` call, so delegate-capable roles need no extra headroom.
|
|
96
103
|
|
|
97
|
-
All numeric limits accept `0` for unlimited: `
|
|
104
|
+
All numeric limits accept `0` for unlimited: `maxConcurrency`, `maxDepth`, `maxTurns`, `maxCost`, and per-role `timeout`. Negative values are normalized to `0`; non-numeric or non-finite values fall back to their defaults. `maxConcurrency: 0` runs delegates without queuing, and `maxDepth: 0` permits unrestricted nesting.
|
|
98
105
|
|
|
99
106
|
### Agent Overrides
|
|
100
107
|
|
|
@@ -131,7 +138,7 @@ Override, disable, or add subagent roles via `agentOverrides`. Built-in and cust
|
|
|
131
138
|
|
|
132
139
|
**Required fields for custom roles:** `role`, `description`, `examples`, `decisionTrigger`, `tools`, `systemPrompt`.
|
|
133
140
|
|
|
134
|
-
**Optional fields:** `subagentRoles` (roles this role can spawn via delegate), `timeout` (per-role active-time timeout in seconds; unset
|
|
141
|
+
**Optional fields:** `subagentRoles` (roles this role can spawn via delegate), `timeout` (per-role active-time timeout in seconds; unset or `0` is unlimited, negative values normalize to `0`), `maxTurns` / `maxCost` (per-role budget overrides; unset uses the top-level `maxTurns` / `maxCost` setting, `0` is unlimited, negative values normalize to `0`), `fallbackRole` (backup pi-model-roles role on provider errors).
|
|
135
142
|
|
|
136
143
|
Invalid custom roles (missing required fields) are silently skipped with an error notification at session start.
|
|
137
144
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3ara1n/pi-subagent",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Role-based subagent orchestration for pi — delegates tasks to specialized pi child processes with configurable model roles",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/config.test.ts
CHANGED
|
@@ -47,7 +47,6 @@ describe("loadSubagentConfig", () => {
|
|
|
47
47
|
const { agentDir } = makeRoot();
|
|
48
48
|
writeSettings(agentDir, {
|
|
49
49
|
subagent: {
|
|
50
|
-
timeout: 0,
|
|
51
50
|
maxConcurrency: -1,
|
|
52
51
|
maxDepth: -2,
|
|
53
52
|
maxTurns: 0,
|
|
@@ -56,7 +55,6 @@ describe("loadSubagentConfig", () => {
|
|
|
56
55
|
});
|
|
57
56
|
|
|
58
57
|
const config = loadSubagentConfig();
|
|
59
|
-
assert.equal(config.timeout, 0);
|
|
60
58
|
assert.equal(config.maxConcurrency, 0);
|
|
61
59
|
assert.equal(config.maxDepth, 0);
|
|
62
60
|
assert.equal(config.maxTurns, 0);
|
|
@@ -69,11 +67,10 @@ describe("loadSubagentConfig", () => {
|
|
|
69
67
|
// would serialize it as null. Exercise both non-finite and wrong-type inputs.
|
|
70
68
|
writeSettingsText(
|
|
71
69
|
agentDir,
|
|
72
|
-
'{"subagent":{"
|
|
70
|
+
'{"subagent":{"maxConcurrency":-1e999,"maxDepth":{},"maxTurns":false,"maxCost":"NaN"}}',
|
|
73
71
|
);
|
|
74
72
|
|
|
75
73
|
const config = loadSubagentConfig();
|
|
76
|
-
assert.equal(config.timeout, DEFAULT_CONFIG.timeout);
|
|
77
74
|
assert.equal(config.maxConcurrency, DEFAULT_CONFIG.maxConcurrency);
|
|
78
75
|
assert.equal(config.maxDepth, DEFAULT_CONFIG.maxDepth);
|
|
79
76
|
assert.equal(config.maxTurns, DEFAULT_CONFIG.maxTurns);
|
|
@@ -96,7 +93,6 @@ describe("loadSubagentConfig", () => {
|
|
|
96
93
|
const { agentDir, projectDir } = makeRoot();
|
|
97
94
|
writeSettings(agentDir, {
|
|
98
95
|
subagent: {
|
|
99
|
-
timeout: 999,
|
|
100
96
|
maxConcurrency: 8,
|
|
101
97
|
maxDepth: 7,
|
|
102
98
|
maxTurns: 6,
|
|
@@ -107,11 +103,10 @@ describe("loadSubagentConfig", () => {
|
|
|
107
103
|
},
|
|
108
104
|
});
|
|
109
105
|
writeSettings(path.join(projectDir, ".pi"), {
|
|
110
|
-
subagent: {
|
|
106
|
+
subagent: { summary: { role: "project-summary" } },
|
|
111
107
|
});
|
|
112
108
|
|
|
113
109
|
const config = loadSubagentConfig(projectDir);
|
|
114
|
-
assert.equal(config.timeout, 12);
|
|
115
110
|
assert.equal(config.maxConcurrency, DEFAULT_CONFIG.maxConcurrency);
|
|
116
111
|
assert.equal(config.maxDepth, DEFAULT_CONFIG.maxDepth);
|
|
117
112
|
assert.equal(config.maxTurns, DEFAULT_CONFIG.maxTurns);
|
package/src/config.ts
CHANGED
|
@@ -46,7 +46,6 @@ export function loadSubagentConfig(cwd?: string): SubagentConfig {
|
|
|
46
46
|
const rawSummary = raw?.summary;
|
|
47
47
|
const rawHistory = raw?.history;
|
|
48
48
|
return {
|
|
49
|
-
timeout: normalizeNonNegativeNumber(raw.timeout, DEFAULT_CONFIG.timeout),
|
|
50
49
|
maxConcurrency: normalizeNonNegativeInteger(raw.maxConcurrency, DEFAULT_CONFIG.maxConcurrency),
|
|
51
50
|
maxDepth: normalizeNonNegativeInteger(raw.maxDepth, DEFAULT_CONFIG.maxDepth),
|
|
52
51
|
maxTurns: normalizeNonNegativeInteger(raw.maxTurns, DEFAULT_CONFIG.maxTurns),
|
package/src/index.ts
CHANGED
|
@@ -23,8 +23,7 @@ import {
|
|
|
23
23
|
AsyncSemaphore,
|
|
24
24
|
isProviderError,
|
|
25
25
|
effectiveTimeout,
|
|
26
|
-
|
|
27
|
-
normalizeNonNegativeNumber,
|
|
26
|
+
hasFailedSubagentResult,
|
|
28
27
|
} from "./utils.ts";
|
|
29
28
|
import { persistSubagentHistory } from "./history.ts";
|
|
30
29
|
import { compressOutput, generateSummary } from "./output.ts";
|
|
@@ -171,6 +170,11 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
171
170
|
rebuildGuidelines(availableRoles);
|
|
172
171
|
});
|
|
173
172
|
|
|
173
|
+
pi.on("tool_result", (event) => {
|
|
174
|
+
if (event.toolName !== "delegate") return;
|
|
175
|
+
if (hasFailedSubagentResult(event.details)) return { isError: true };
|
|
176
|
+
});
|
|
177
|
+
|
|
174
178
|
pi.registerTool({
|
|
175
179
|
name: "delegate",
|
|
176
180
|
label: "Delegate to subagent",
|
|
@@ -220,16 +224,9 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
220
224
|
|
|
221
225
|
// Guard against bounded subagent nesting. A configured depth of 0 is unlimited.
|
|
222
226
|
if (config.maxDepth > 0 && CURRENT_DEPTH >= config.maxDepth) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
type: "text",
|
|
227
|
-
text: `Cannot delegate: maximum nesting depth (${config.maxDepth}) reached (current depth ${CURRENT_DEPTH}). Return a result to the caller instead of delegating further.`,
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
details: undefined as any,
|
|
231
|
-
isError: true,
|
|
232
|
-
};
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Cannot delegate: maximum nesting depth (${config.maxDepth}) reached (current depth ${CURRENT_DEPTH}). Return a result to the caller instead of delegating further.`,
|
|
229
|
+
);
|
|
233
230
|
}
|
|
234
231
|
|
|
235
232
|
// Throttle state hoisted to the execute scope so the finally block can clear it.
|
|
@@ -287,13 +284,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
287
284
|
try {
|
|
288
285
|
await gate.acquire(signal);
|
|
289
286
|
} catch {
|
|
290
|
-
|
|
291
|
-
content: [
|
|
292
|
-
{ type: "text", text: `Subagent (${params.role}) was cancelled while queued.` },
|
|
293
|
-
],
|
|
294
|
-
details: { mode: "single", results: [] },
|
|
295
|
-
isError: true,
|
|
296
|
-
};
|
|
287
|
+
throw new Error(`Subagent (${params.role}) was cancelled while queued.`);
|
|
297
288
|
}
|
|
298
289
|
|
|
299
290
|
try {
|
|
@@ -336,12 +327,9 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
336
327
|
const startTime = Date.now();
|
|
337
328
|
// Total active-time budget for this run (ms). The clock pauses while the
|
|
338
329
|
// child delegates, so this caps *active* time, not wall time.
|
|
339
|
-
const timeoutBudgetMs = effectiveTimeout(roleDef
|
|
340
|
-
const maxTurns =
|
|
341
|
-
|
|
342
|
-
config.maxTurns,
|
|
343
|
-
);
|
|
344
|
-
const maxCost = normalizeNonNegativeNumber(roleDef.maxCost ?? config.maxCost, config.maxCost);
|
|
330
|
+
const timeoutBudgetMs = effectiveTimeout(roleDef) * 1000;
|
|
331
|
+
const maxTurns = roleDef.maxTurns ?? config.maxTurns;
|
|
332
|
+
const maxCost = roleDef.maxCost ?? config.maxCost;
|
|
345
333
|
|
|
346
334
|
// Throttled progress: coalesces bursty thinking/tool events so the TUI
|
|
347
335
|
// repaints at most ~every PROGRESS_THROTTLE_MS, always keeping the latest state.
|
|
@@ -529,7 +517,6 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
529
517
|
return {
|
|
530
518
|
content: [{ type: "text", text: failedText }],
|
|
531
519
|
details: { mode: "single", results: [result] },
|
|
532
|
-
isError: true,
|
|
533
520
|
};
|
|
534
521
|
}
|
|
535
522
|
|
|
@@ -552,11 +539,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
552
539
|
} catch (err: any) {
|
|
553
540
|
const errorText = `Subagent (${params.role}) error: ${err.message || err}`;
|
|
554
541
|
emitFinal([], errorText);
|
|
555
|
-
|
|
556
|
-
content: [{ type: "text", text: errorText }],
|
|
557
|
-
details: { mode: "single", results: [] },
|
|
558
|
-
isError: true,
|
|
559
|
-
};
|
|
542
|
+
throw new Error(errorText);
|
|
560
543
|
} finally {
|
|
561
544
|
// Cancel any trailing throttled onUpdate regardless of how we exited
|
|
562
545
|
// (success / fallback / budget / error). A stale "still running" progress
|
|
@@ -592,7 +575,7 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
592
575
|
try {
|
|
593
576
|
const cfg = loadSubagentConfig(ctx.cwd);
|
|
594
577
|
lines.push(
|
|
595
|
-
`[\u2713] config:
|
|
578
|
+
`[\u2713] config: concurrency=${cfg.maxConcurrency || "∞"} depth=${cfg.maxDepth || "∞"} turns=${cfg.maxTurns || "∞"} cost=$${cfg.maxCost || "∞"} summary=${cfg.summary.enabled ? cfg.summary.role : "off"} history=${cfg.history.enabled}`,
|
|
596
579
|
);
|
|
597
580
|
} catch {
|
|
598
581
|
lines.push("[\u2717] config: failed to load");
|
package/src/roles.ts
CHANGED
|
@@ -12,6 +12,7 @@ export const BUILTIN_ROLES: Record<string, SubagentRole> = {
|
|
|
12
12
|
explorer: {
|
|
13
13
|
role: "fast",
|
|
14
14
|
fallbackRole: "default",
|
|
15
|
+
timeout: 900,
|
|
15
16
|
description:
|
|
16
17
|
"READ-ONLY codebase exploration — locate files, grep symbols, trace imports, explain structures. Tools: read, find, grep. NO bash, NO edits, NO web access.",
|
|
17
18
|
examples: ["Find where auth middleware is implemented", "Map the routing structure"],
|
|
@@ -31,6 +32,7 @@ export const BUILTIN_ROLES: Record<string, SubagentRole> = {
|
|
|
31
32
|
reviewer: {
|
|
32
33
|
role: "heavy",
|
|
33
34
|
fallbackRole: "default",
|
|
35
|
+
timeout: 3600,
|
|
34
36
|
description:
|
|
35
37
|
"READ-ONLY code review & analysis — audit code, assess architecture, review diffs. Tools: read, bash, grep, find. Has bash (git diff/log, test runs). NO edits, NO web access.",
|
|
36
38
|
examples: [
|
|
@@ -52,6 +54,7 @@ export const BUILTIN_ROLES: Record<string, SubagentRole> = {
|
|
|
52
54
|
},
|
|
53
55
|
worker: {
|
|
54
56
|
role: "default",
|
|
57
|
+
timeout: 2400,
|
|
55
58
|
description:
|
|
56
59
|
"the ONLY role that can MODIFY files — edit, write, refactor, fix, implement. Tools: read, bash, edit, write, grep, find, delegate. Can delegate to explorer/researcher.",
|
|
57
60
|
examples: ["Rename all snake_case fields to camelCase", "Add input validation to POST /login"],
|
|
@@ -77,6 +80,7 @@ export const BUILTIN_ROLES: Record<string, SubagentRole> = {
|
|
|
77
80
|
researcher: {
|
|
78
81
|
role: "fast",
|
|
79
82
|
fallbackRole: "default",
|
|
83
|
+
timeout: 2400,
|
|
80
84
|
description:
|
|
81
85
|
"the ONLY role with WEB ACCESS — search docs, fetch pages, analyze GitHub repos. Tools: web_search, fetch_content, read, bash, delegate. Can clone repos & delegate to explorer.",
|
|
82
86
|
examples: ["Find the React 19 migration guide", "Check GitHub issue #1234 for context"],
|
package/src/types.ts
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
/** Configuration for the subagent extension. */
|
|
6
6
|
export interface SubagentConfig {
|
|
7
|
-
/** Per-subagent active-time timeout in seconds. `0` means unlimited; negative values are normalized to `0`. The clock pauses while the child is inside a nested `delegate` call, so no widening is needed for delegate-capable roles. */
|
|
8
|
-
timeout: number;
|
|
9
7
|
/** Max concurrent subagents. `0` means unlimited; negative values are normalized to `0`. Extras queue with a TUI hint when this is positive. */
|
|
10
8
|
maxConcurrency: number;
|
|
11
9
|
/** Max subagent nesting depth (the top-level session is depth 0). `0` means unlimited; negative values are normalized to `0`. */
|
|
@@ -35,7 +33,6 @@ export interface SubagentSummaryConfig {
|
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
export const DEFAULT_CONFIG: SubagentConfig = {
|
|
38
|
-
timeout: 1500,
|
|
39
36
|
maxConcurrency: 4,
|
|
40
37
|
maxDepth: 3,
|
|
41
38
|
maxTurns: 0,
|
|
@@ -61,7 +58,7 @@ export interface SubagentRole {
|
|
|
61
58
|
tools: string[];
|
|
62
59
|
/** If this role has `delegate`, restrict which roles it may spawn. undefined = no restriction. */
|
|
63
60
|
subagentRoles?: string[];
|
|
64
|
-
/** Per-role active-time timeout
|
|
61
|
+
/** Per-role active-time timeout in seconds. `0` or unset means unlimited; negative values are normalized to `0`. */
|
|
65
62
|
timeout?: number;
|
|
66
63
|
/** Max assistant turns before the run is killed. `0` means unlimited; negative values are normalized to `0`. */
|
|
67
64
|
maxTurns?: number;
|
package/src/utils.test.ts
CHANGED
|
@@ -21,9 +21,50 @@ import {
|
|
|
21
21
|
formatTokens,
|
|
22
22
|
effectiveTimeout,
|
|
23
23
|
elapsedSeconds,
|
|
24
|
+
hasFailedSubagentResult,
|
|
24
25
|
} from "./utils.ts";
|
|
25
26
|
import type { SubagentResult, SubagentRole } from "./types.ts";
|
|
26
27
|
|
|
28
|
+
describe("subagent failure details", () => {
|
|
29
|
+
const baseResult = (overrides: Partial<SubagentResult> = {}): SubagentResult => ({
|
|
30
|
+
role: "worker",
|
|
31
|
+
task: "test task",
|
|
32
|
+
exitCode: 0,
|
|
33
|
+
messages: [],
|
|
34
|
+
output: "ok",
|
|
35
|
+
stderr: "",
|
|
36
|
+
usage: {
|
|
37
|
+
input: 0,
|
|
38
|
+
output: 0,
|
|
39
|
+
cacheRead: 0,
|
|
40
|
+
cacheWrite: 0,
|
|
41
|
+
cost: 0,
|
|
42
|
+
contextTokens: 0,
|
|
43
|
+
turns: 0,
|
|
44
|
+
},
|
|
45
|
+
activityLog: [],
|
|
46
|
+
...overrides,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("detects failed delegate results for tool_result error marking", () => {
|
|
50
|
+
assert.equal(
|
|
51
|
+
hasFailedSubagentResult({ mode: "single", results: [baseResult({ exitCode: 1 })] }),
|
|
52
|
+
true,
|
|
53
|
+
);
|
|
54
|
+
assert.equal(
|
|
55
|
+
hasFailedSubagentResult({ mode: "single", results: [baseResult({ stopReason: "timeout" })] }),
|
|
56
|
+
true,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("does not mark successful or malformed details as failed", () => {
|
|
61
|
+
assert.equal(hasFailedSubagentResult({ mode: "single", results: [baseResult()] }), false);
|
|
62
|
+
assert.equal(hasFailedSubagentResult({ mode: "single", results: [] }), false);
|
|
63
|
+
assert.equal(hasFailedSubagentResult(undefined), false);
|
|
64
|
+
assert.equal(hasFailedSubagentResult({}), false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
27
68
|
// ── sanitizeFilename: guards the path-injection fix ──
|
|
28
69
|
describe("sanitizeFilename", () => {
|
|
29
70
|
test("never yields a path separator (no directory traversal)", () => {
|
|
@@ -248,17 +289,18 @@ describe("effectiveTimeout", () => {
|
|
|
248
289
|
timeout,
|
|
249
290
|
}) as unknown as SubagentRole;
|
|
250
291
|
|
|
251
|
-
test("
|
|
252
|
-
assert.equal(effectiveTimeout(role(["read", "grep"])
|
|
292
|
+
test("role without timeout is unlimited", () => {
|
|
293
|
+
assert.equal(effectiveTimeout(role(["read", "grep"])), 0);
|
|
253
294
|
});
|
|
254
|
-
test("delegate role
|
|
255
|
-
assert.equal(effectiveTimeout(role(["read", "delegate"])
|
|
295
|
+
test("delegate-capable role without timeout is also unlimited", () => {
|
|
296
|
+
assert.equal(effectiveTimeout(role(["read", "delegate"])), 0);
|
|
256
297
|
});
|
|
257
|
-
test("explicit
|
|
258
|
-
assert.equal(effectiveTimeout(role(["read", "delegate"], 300)
|
|
298
|
+
test("explicit role timeout is honored", () => {
|
|
299
|
+
assert.equal(effectiveTimeout(role(["read", "delegate"], 300)), 300);
|
|
259
300
|
});
|
|
260
|
-
test("
|
|
261
|
-
assert.equal(effectiveTimeout(role(["read"]
|
|
301
|
+
test("negative and non-finite values normalize to unlimited", () => {
|
|
302
|
+
assert.equal(effectiveTimeout(role(["read"], -1)), 0);
|
|
303
|
+
assert.equal(effectiveTimeout(role(["read"], Number.POSITIVE_INFINITY)), 0);
|
|
262
304
|
});
|
|
263
305
|
});
|
|
264
306
|
|
package/src/utils.ts
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
ActivityEntry,
|
|
9
|
+
SubagentDetails,
|
|
10
|
+
SubagentRole,
|
|
11
|
+
SubagentResult,
|
|
12
|
+
ToolStatus,
|
|
13
|
+
} from "./types.ts";
|
|
8
14
|
|
|
9
15
|
/** Max output chars fed to the main model and the expanded TUI. Larger outputs are compressed (or truncated) to fit. */
|
|
10
16
|
export const MAX_OUTPUT_CHARS = 50_000;
|
|
@@ -194,6 +200,11 @@ export function isFailedResult(r: SubagentResult): boolean {
|
|
|
194
200
|
);
|
|
195
201
|
}
|
|
196
202
|
|
|
203
|
+
export function hasFailedSubagentResult(details: unknown): boolean {
|
|
204
|
+
const d = details as SubagentDetails | undefined;
|
|
205
|
+
return Array.isArray(d?.results) && d.results.some(isFailedResult);
|
|
206
|
+
}
|
|
207
|
+
|
|
197
208
|
/** Heuristic: does this result look like a provider-side failure worth retrying on the fallback role? */
|
|
198
209
|
export function isProviderError(result: SubagentResult): boolean {
|
|
199
210
|
const haystack = `${result.stderr || ""}\n${result.errorMessage || ""}`;
|
|
@@ -291,13 +302,10 @@ export class AsyncSemaphore {
|
|
|
291
302
|
|
|
292
303
|
/**
|
|
293
304
|
* Effective per-role timeout in SECONDS (convert to ms at the spawn boundary).
|
|
294
|
-
*
|
|
295
|
-
* pauses while the child is inside a nested `delegate` call, so the base
|
|
296
|
-
* budget is already enough. An explicit roleDef.timeout always wins.
|
|
297
|
-
* Non-finite values fall back to the base timeout; negative values become 0 (unlimited).
|
|
305
|
+
* `0` or unset means unlimited; non-finite and negative values normalize to 0.
|
|
298
306
|
*/
|
|
299
|
-
export function effectiveTimeout(roleDef: SubagentRole
|
|
300
|
-
return normalizeNonNegativeNumber(roleDef.timeout,
|
|
307
|
+
export function effectiveTimeout(roleDef: SubagentRole): number {
|
|
308
|
+
return normalizeNonNegativeNumber(roleDef.timeout, 0);
|
|
301
309
|
}
|
|
302
310
|
|
|
303
311
|
// ── Output truncation ────────────────────────────────────────
|