pi-subagents 0.27.0 → 0.28.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/CHANGELOG.md +13 -0
- package/README.md +46 -2
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +29 -1
- package/src/agents/agent-management.ts +14 -0
- package/src/agents/agent-serializer.ts +10 -0
- package/src/agents/agents.ts +41 -1
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +4 -2
- package/src/extension/schemas.ts +4 -11
- package/src/intercom/result-intercom.ts +5 -0
- package/src/runs/background/async-execution.ts +4 -0
- package/src/runs/background/subagent-runner.ts +65 -8
- package/src/runs/foreground/chain-execution.ts +45 -1
- package/src/runs/foreground/execution.ts +171 -10
- package/src/runs/foreground/subagent-executor.ts +59 -3
- package/src/runs/shared/acceptance-contract.ts +27 -0
- package/src/runs/shared/acceptance-finalization.ts +12 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/workflow-graph.ts +6 -2
- package/src/shared/types.ts +16 -2
- package/src/shared/utils.ts +7 -0
- package/src/tui/render.ts +18 -13
|
@@ -121,6 +121,8 @@ export interface SubagentParamsLike {
|
|
|
121
121
|
chain?: ChainStep[];
|
|
122
122
|
tasks?: TaskParam[];
|
|
123
123
|
concurrency?: number;
|
|
124
|
+
timeoutMs?: number;
|
|
125
|
+
maxRuntimeMs?: number;
|
|
124
126
|
worktree?: boolean;
|
|
125
127
|
context?: "fresh" | "fork";
|
|
126
128
|
async?: boolean;
|
|
@@ -169,6 +171,7 @@ interface ExecutionContextData {
|
|
|
169
171
|
artifactsDir: string;
|
|
170
172
|
backgroundRequestedWhileClarifying: boolean;
|
|
171
173
|
effectiveAsync: boolean;
|
|
174
|
+
foregroundTimeoutMs?: number;
|
|
172
175
|
controlConfig: ResolvedControlConfig;
|
|
173
176
|
intercomBridge: IntercomBridgeState;
|
|
174
177
|
nestedRoute?: NestedRouteInfo;
|
|
@@ -250,7 +253,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
|
|
|
250
253
|
children: input.results.map((result, index) => ({
|
|
251
254
|
agent: result.agent,
|
|
252
255
|
index,
|
|
253
|
-
status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
|
|
256
|
+
status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached, timedOut: result.timedOut }),
|
|
254
257
|
...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
|
|
255
258
|
})),
|
|
256
259
|
});
|
|
@@ -716,6 +719,7 @@ async function emitForegroundResultIntercom(input: {
|
|
|
716
719
|
exitCode: result.exitCode,
|
|
717
720
|
interrupted: result.interrupted,
|
|
718
721
|
detached: result.detached,
|
|
722
|
+
timedOut: result.timedOut,
|
|
719
723
|
}),
|
|
720
724
|
summary: resultSummaryForIntercom(result),
|
|
721
725
|
index,
|
|
@@ -765,6 +769,21 @@ function validationErrorResult(mode: Details["mode"], text: string): AgentToolRe
|
|
|
765
769
|
return { content: [{ type: "text", text }], isError: true, details: { mode, results: [] } };
|
|
766
770
|
}
|
|
767
771
|
|
|
772
|
+
function resolveForegroundTimeoutMs(params: SubagentParamsLike): { timeoutMs?: number; error?: string } {
|
|
773
|
+
const rawTimeout = (params as { timeoutMs?: unknown }).timeoutMs;
|
|
774
|
+
const rawMaxRuntime = (params as { maxRuntimeMs?: unknown }).maxRuntimeMs;
|
|
775
|
+
for (const [name, value] of [["timeoutMs", rawTimeout], ["maxRuntimeMs", rawMaxRuntime]] as const) {
|
|
776
|
+
if (value !== undefined && (typeof value !== "number" || !Number.isInteger(value) || value < 1)) {
|
|
777
|
+
return { error: `${name} must be a positive integer.` };
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (rawTimeout !== undefined && rawMaxRuntime !== undefined && rawTimeout !== rawMaxRuntime) {
|
|
781
|
+
return { error: "timeoutMs and maxRuntimeMs are aliases; provide only one or use identical values." };
|
|
782
|
+
}
|
|
783
|
+
const timeoutMs = rawTimeout ?? rawMaxRuntime;
|
|
784
|
+
return timeoutMs === undefined ? {} : { timeoutMs };
|
|
785
|
+
}
|
|
786
|
+
|
|
768
787
|
function validateAcceptanceForExecution(params: SubagentParamsLike): AgentToolResult<Details> | null {
|
|
769
788
|
const topLevelErrors = validateAcceptanceInput(params.acceptance);
|
|
770
789
|
if (topLevelErrors.length > 0) return validationErrorResult("single", topLevelErrors.join(" "));
|
|
@@ -815,6 +834,9 @@ function validateExecutionInput(
|
|
|
815
834
|
};
|
|
816
835
|
}
|
|
817
836
|
|
|
837
|
+
const timeoutResolution = resolveForegroundTimeoutMs(params);
|
|
838
|
+
if (timeoutResolution.error) return validationErrorResult(getRequestedModeLabel(params), timeoutResolution.error);
|
|
839
|
+
|
|
818
840
|
if (hasSingle && params.agent && !agents.find((agent) => agent.name === params.agent)) {
|
|
819
841
|
return {
|
|
820
842
|
content: [{ type: "text", text: `Unknown agent: ${params.agent}` }],
|
|
@@ -1288,6 +1310,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1288
1310
|
onUpdate,
|
|
1289
1311
|
onControlEvent,
|
|
1290
1312
|
controlConfig,
|
|
1313
|
+
...(data.foregroundTimeoutMs !== undefined ? { timeoutMs: data.foregroundTimeoutMs } : {}),
|
|
1291
1314
|
childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
|
|
1292
1315
|
orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
|
|
1293
1316
|
foregroundControl,
|
|
@@ -1344,7 +1367,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1344
1367
|
const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
|
|
1345
1368
|
if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
|
|
1346
1369
|
if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
|
|
1347
|
-
const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
|
|
1370
|
+
const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached || result.timedOut)
|
|
1348
1371
|
? await maybeBuildForegroundIntercomReceipt({
|
|
1349
1372
|
pi: deps.pi,
|
|
1350
1373
|
intercomBridge: data.intercomBridge,
|
|
@@ -1379,6 +1402,8 @@ interface ForegroundParallelRunInput {
|
|
|
1379
1402
|
artifactConfig: ArtifactConfig;
|
|
1380
1403
|
artifactsDir: string;
|
|
1381
1404
|
maxOutput?: MaxOutputConfig;
|
|
1405
|
+
timeoutMs?: number;
|
|
1406
|
+
timeoutAt?: number;
|
|
1382
1407
|
paramsCwd: string;
|
|
1383
1408
|
maxSubagentDepths: number[];
|
|
1384
1409
|
availableModels: ModelInfo[];
|
|
@@ -1531,6 +1556,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1531
1556
|
cwd: taskCwd,
|
|
1532
1557
|
signal: input.signal,
|
|
1533
1558
|
interruptSignal: interruptController.signal,
|
|
1559
|
+
...(input.timeoutMs !== undefined && input.timeoutAt !== undefined ? { timeoutMs: input.timeoutMs, timeoutAt: input.timeoutAt } : {}),
|
|
1534
1560
|
allowIntercomDetach: agentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
|
|
1535
1561
|
intercomEvents: input.intercomEvents,
|
|
1536
1562
|
runId: input.runId,
|
|
@@ -1544,6 +1570,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1544
1570
|
outputPath,
|
|
1545
1571
|
outputMode: behavior?.outputMode,
|
|
1546
1572
|
maxSubagentDepth: input.maxSubagentDepths[index],
|
|
1573
|
+
maxExecutionTimeMs: agentConfig?.maxExecutionTimeMs,
|
|
1574
|
+
maxTokens: agentConfig?.maxTokens,
|
|
1547
1575
|
controlConfig: input.controlConfig,
|
|
1548
1576
|
onControlEvent: input.onControlEvent,
|
|
1549
1577
|
intercomSessionName: input.childIntercomTarget?.(task.agent, index),
|
|
@@ -1811,6 +1839,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1811
1839
|
}
|
|
1812
1840
|
}
|
|
1813
1841
|
|
|
1842
|
+
const timeoutAt = data.foregroundTimeoutMs !== undefined ? Date.now() + data.foregroundTimeoutMs : undefined;
|
|
1814
1843
|
const results = await runForegroundParallelTasks({
|
|
1815
1844
|
tasks,
|
|
1816
1845
|
taskTexts,
|
|
@@ -1825,6 +1854,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1825
1854
|
artifactConfig,
|
|
1826
1855
|
artifactsDir,
|
|
1827
1856
|
maxOutput: params.maxOutput,
|
|
1857
|
+
...(data.foregroundTimeoutMs !== undefined && timeoutAt !== undefined ? { timeoutMs: data.foregroundTimeoutMs, timeoutAt } : {}),
|
|
1828
1858
|
paramsCwd: effectiveCwd,
|
|
1829
1859
|
availableModels,
|
|
1830
1860
|
modelOverrides,
|
|
@@ -1852,6 +1882,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1852
1882
|
if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
|
|
1853
1883
|
}
|
|
1854
1884
|
|
|
1885
|
+
const timedOut = results.find((result) => result.timedOut);
|
|
1855
1886
|
const interrupted = results.find((result) => result.interrupted);
|
|
1856
1887
|
const details = compactForegroundDetails({
|
|
1857
1888
|
mode: "parallel",
|
|
@@ -1861,6 +1892,13 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1861
1892
|
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1862
1893
|
});
|
|
1863
1894
|
rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
|
|
1895
|
+
if (timedOut) {
|
|
1896
|
+
return {
|
|
1897
|
+
content: [{ type: "text", text: `Parallel run timed out (${timedOut.agent}): ${timedOut.error ?? "timeout expired"}` }],
|
|
1898
|
+
details,
|
|
1899
|
+
isError: true,
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1864
1902
|
if (interrupted) {
|
|
1865
1903
|
return {
|
|
1866
1904
|
content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
|
|
@@ -2091,10 +2129,12 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2091
2129
|
}
|
|
2092
2130
|
: undefined;
|
|
2093
2131
|
|
|
2132
|
+
const timeoutAt = data.foregroundTimeoutMs !== undefined ? Date.now() + data.foregroundTimeoutMs : undefined;
|
|
2094
2133
|
const r = await runSync(ctx.cwd, agents, params.agent!, task, {
|
|
2095
2134
|
cwd: effectiveCwd,
|
|
2096
2135
|
signal,
|
|
2097
2136
|
interruptSignal: interruptController.signal,
|
|
2137
|
+
...(data.foregroundTimeoutMs !== undefined && timeoutAt !== undefined ? { timeoutMs: data.foregroundTimeoutMs, timeoutAt } : {}),
|
|
2098
2138
|
allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
|
|
2099
2139
|
intercomEvents: deps.pi.events,
|
|
2100
2140
|
runId,
|
|
@@ -2107,6 +2147,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2107
2147
|
outputPath,
|
|
2108
2148
|
outputMode: effectiveOutputMode,
|
|
2109
2149
|
maxSubagentDepth,
|
|
2150
|
+
maxExecutionTimeMs: agentConfig.maxExecutionTimeMs,
|
|
2151
|
+
maxTokens: agentConfig.maxTokens,
|
|
2110
2152
|
onUpdate: forwardSingleUpdate,
|
|
2111
2153
|
controlConfig,
|
|
2112
2154
|
onControlEvent,
|
|
@@ -2159,7 +2201,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2159
2201
|
});
|
|
2160
2202
|
rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
|
|
2161
2203
|
|
|
2162
|
-
if (!r.detached && !r.interrupted) {
|
|
2204
|
+
if (!r.detached && !r.interrupted && !r.timedOut) {
|
|
2163
2205
|
if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
|
|
2164
2206
|
const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
|
|
2165
2207
|
pi: deps.pi,
|
|
@@ -2185,6 +2227,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2185
2227
|
};
|
|
2186
2228
|
}
|
|
2187
2229
|
|
|
2230
|
+
if (r.timedOut) {
|
|
2231
|
+
return {
|
|
2232
|
+
content: [{ type: "text", text: `Run timed out (${params.agent}): ${r.error ?? "timeout expired"}` }],
|
|
2233
|
+
details,
|
|
2234
|
+
isError: true,
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2188
2238
|
if (r.interrupted) {
|
|
2189
2239
|
return {
|
|
2190
2240
|
content: [{ type: "text", text: `Run paused after interrupt (${params.agent}). Waiting for explicit next action.` }],
|
|
@@ -2412,6 +2462,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2412
2462
|
const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
|
|
2413
2463
|
const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
|
|
2414
2464
|
const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
|
|
2465
|
+
const foregroundTimeout = resolveForegroundTimeoutMs(effectiveParams);
|
|
2466
|
+
if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
|
|
2467
|
+
if (effectiveAsync && foregroundTimeout.timeoutMs !== undefined) {
|
|
2468
|
+
return buildRequestedModeError(effectiveParams, "timeoutMs/maxRuntimeMs only applies to foreground subagent runs. Omit async:true or use action:'interrupt' for background runs.");
|
|
2469
|
+
}
|
|
2415
2470
|
const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
|
|
2416
2471
|
|
|
2417
2472
|
const artifactConfig: ArtifactConfig = {
|
|
@@ -2463,6 +2518,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2463
2518
|
artifactsDir,
|
|
2464
2519
|
backgroundRequestedWhileClarifying,
|
|
2465
2520
|
effectiveAsync,
|
|
2521
|
+
...(foregroundTimeout.timeoutMs !== undefined ? { foregroundTimeoutMs: foregroundTimeout.timeoutMs } : {}),
|
|
2466
2522
|
controlConfig,
|
|
2467
2523
|
intercomBridge,
|
|
2468
2524
|
nestedRoute,
|
|
@@ -34,6 +34,22 @@ const ACCEPTANCE_KEYS = new Set([
|
|
|
34
34
|
|
|
35
35
|
const REMOVED_ACCEPTANCE_KEYS = new Set(["level", "finalization", "reason"]);
|
|
36
36
|
|
|
37
|
+
const EVIDENCE_REPORT_FIELDS: Record<AcceptanceEvidenceKind, string> = {
|
|
38
|
+
"changed-files": "changedFiles: array of changed file paths",
|
|
39
|
+
"tests-added": "testsAddedOrUpdated: array of test files, suites, or cases added/updated",
|
|
40
|
+
"commands-run": "commandsRun: array of commands with result passed/failed/not-run and a short summary",
|
|
41
|
+
"validation-output": "validationOutput: array of relevant validation output summaries",
|
|
42
|
+
"residual-risks": "residualRisks: array of remaining risks or blockers; use [] when none remain",
|
|
43
|
+
"no-staged-files": "noStagedFiles: boolean",
|
|
44
|
+
"diff-summary": "diffSummary: non-empty string summarizing changed behavior and important files",
|
|
45
|
+
"review-findings": "reviewFindings: array of reviewer findings; use [] when no findings remain",
|
|
46
|
+
"manual-notes": "manualNotes: string for manual notes or external evidence",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function formatEvidenceReportFieldMapping(evidence: AcceptanceEvidenceKind[]): string[] {
|
|
50
|
+
return evidence.map((kind) => `- ${kind} -> ${EVIDENCE_REPORT_FIELDS[kind]}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
function hasArrayItems(value: unknown): boolean {
|
|
38
54
|
return Array.isArray(value) && value.length > 0;
|
|
39
55
|
}
|
|
@@ -260,6 +276,14 @@ export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig): st
|
|
|
260
276
|
"",
|
|
261
277
|
`Required evidence: ${acceptance.evidence.join(", ") || "none explicitly requested"}`,
|
|
262
278
|
];
|
|
279
|
+
if (acceptance.evidence.length > 0) {
|
|
280
|
+
lines.push(
|
|
281
|
+
"",
|
|
282
|
+
"Structured evidence must be present in the `acceptance-report` JSON fields. Markdown sections in your visible answer do not satisfy required evidence by themselves. If you already described evidence in prose, copy or summarize it into the matching JSON field.",
|
|
283
|
+
"Evidence field mapping:",
|
|
284
|
+
...formatEvidenceReportFieldMapping(acceptance.evidence),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
263
287
|
if (acceptance.verify.length > 0) {
|
|
264
288
|
lines.push("", "Runtime verification commands configured by parent:");
|
|
265
289
|
for (const command of acceptance.verify) lines.push(`- ${command.id}: ${command.command}`);
|
|
@@ -283,6 +307,9 @@ export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig): st
|
|
|
283
307
|
validationOutput: [],
|
|
284
308
|
residualRisks: [],
|
|
285
309
|
noStagedFiles: true,
|
|
310
|
+
diffSummary: "concise summary of changed behavior and important files",
|
|
311
|
+
reviewFindings: [],
|
|
312
|
+
manualNotes: "manual notes or external evidence, if any",
|
|
286
313
|
notes: "anything else the parent should know",
|
|
287
314
|
}, null, 2),
|
|
288
315
|
"```",
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ResolvedAcceptanceConfig,
|
|
5
5
|
} from "../../shared/types.ts";
|
|
6
6
|
import { acceptanceFailureMessage } from "./acceptance-evaluation.ts";
|
|
7
|
+
import { formatEvidenceReportFieldMapping } from "./acceptance-contract.ts";
|
|
7
8
|
import { stripAcceptanceReport } from "./acceptance-reports.ts";
|
|
8
9
|
|
|
9
10
|
const INITIAL_OUTPUT_LIMIT = 8_000;
|
|
@@ -42,6 +43,14 @@ export function formatAcceptanceFinalizationPrompt(input: {
|
|
|
42
43
|
"",
|
|
43
44
|
`Required evidence: ${input.acceptance.evidence.join(", ") || "none explicitly requested"}`,
|
|
44
45
|
];
|
|
46
|
+
if (input.acceptance.evidence.length > 0) {
|
|
47
|
+
lines.push(
|
|
48
|
+
"",
|
|
49
|
+
"Structured evidence must be present in the final `acceptance-report` JSON fields. Markdown sections in the visible answer do not satisfy required evidence by themselves. If the previous visible output already included the evidence, copy or summarize it into the matching JSON field.",
|
|
50
|
+
"Evidence field mapping:",
|
|
51
|
+
...formatEvidenceReportFieldMapping(input.acceptance.evidence),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
45
54
|
if (input.acceptance.verify.length > 0) {
|
|
46
55
|
lines.push("", "Runtime verification commands that must pass:", ...input.acceptance.verify.map((command) => `- ${command.id}: ${command.command}`));
|
|
47
56
|
}
|
|
@@ -74,6 +83,9 @@ export function formatAcceptanceFinalizationPrompt(input: {
|
|
|
74
83
|
validationOutput: [],
|
|
75
84
|
residualRisks: [],
|
|
76
85
|
noStagedFiles: true,
|
|
86
|
+
diffSummary: "concise summary of changed behavior and important files",
|
|
87
|
+
reviewFindings: [],
|
|
88
|
+
manualNotes: "manual notes or external evidence, if any",
|
|
77
89
|
notes: "final self-review summary",
|
|
78
90
|
}, null, 2),
|
|
79
91
|
"```",
|
|
@@ -25,6 +25,8 @@ export interface RunnerSubagentStep {
|
|
|
25
25
|
outputMode?: "inline" | "file-only";
|
|
26
26
|
sessionFile?: string;
|
|
27
27
|
maxSubagentDepth?: number;
|
|
28
|
+
maxExecutionTimeMs?: number;
|
|
29
|
+
maxTokens?: number;
|
|
28
30
|
structuredOutput?: {
|
|
29
31
|
schema: JsonSchemaObject;
|
|
30
32
|
schemaPath: string;
|
|
@@ -5,7 +5,7 @@ export interface WorkflowGraphBuildInput {
|
|
|
5
5
|
runId: string;
|
|
6
6
|
mode?: SubagentRunMode;
|
|
7
7
|
steps: ChainStep[];
|
|
8
|
-
results?: Array<Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "error" | "acceptance">>;
|
|
8
|
+
results?: Array<Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "timedOut" | "error" | "acceptance">>;
|
|
9
9
|
currentFlatIndex?: number;
|
|
10
10
|
currentStepIndex?: number;
|
|
11
11
|
stepStatuses?: Array<{ status?: string; error?: string }>;
|
|
@@ -26,6 +26,8 @@ function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undef
|
|
|
26
26
|
return "paused";
|
|
27
27
|
case "detached":
|
|
28
28
|
return "detached";
|
|
29
|
+
case "timed-out":
|
|
30
|
+
return "timed-out";
|
|
29
31
|
case "pending":
|
|
30
32
|
return "pending";
|
|
31
33
|
default:
|
|
@@ -33,9 +35,10 @@ function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undef
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
function resultStatus(result: Pick<SingleResult, "exitCode" | "detached" | "interrupted"> | undefined): WorkflowNodeStatus | undefined {
|
|
38
|
+
function resultStatus(result: Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "timedOut"> | undefined): WorkflowNodeStatus | undefined {
|
|
37
39
|
if (!result) return undefined;
|
|
38
40
|
if (result.detached) return "detached";
|
|
41
|
+
if (result.timedOut) return "timed-out";
|
|
39
42
|
if (result.interrupted) return "paused";
|
|
40
43
|
return result.exitCode === 0 ? "completed" : "failed";
|
|
41
44
|
}
|
|
@@ -63,6 +66,7 @@ function seqLabel(step: SequentialStep, stepIndex: number): string {
|
|
|
63
66
|
function summarizeParallelStatuses(statuses: WorkflowNodeStatus[]): WorkflowNodeStatus {
|
|
64
67
|
if (statuses.some((status) => status === "running")) return "running";
|
|
65
68
|
if (statuses.some((status) => status === "failed")) return "failed";
|
|
69
|
+
if (statuses.some((status) => status === "timed-out")) return "timed-out";
|
|
66
70
|
if (statuses.some((status) => status === "paused")) return "paused";
|
|
67
71
|
if (statuses.some((status) => status === "detached")) return "detached";
|
|
68
72
|
if (statuses.length > 0 && statuses.every((status) => status === "completed")) return "completed";
|
package/src/shared/types.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface ChainOutputMapEntry {
|
|
|
30
30
|
|
|
31
31
|
export type ChainOutputMap = Record<string, ChainOutputMapEntry>;
|
|
32
32
|
|
|
33
|
-
export type WorkflowNodeStatus = "pending" | "running" | "completed" | "failed" | "paused" | "detached";
|
|
33
|
+
export type WorkflowNodeStatus = "pending" | "running" | "completed" | "failed" | "paused" | "detached" | "timed-out";
|
|
34
34
|
|
|
35
35
|
export interface WorkflowGraphNode {
|
|
36
36
|
id: string;
|
|
@@ -142,7 +142,7 @@ export interface ControlEvent {
|
|
|
142
142
|
recentFailureSummary?: string;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached";
|
|
145
|
+
export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached" | "timed-out";
|
|
146
146
|
export type SubagentRunMode = "single" | "parallel" | "chain";
|
|
147
147
|
|
|
148
148
|
export type PublicNestedStepSummary = Pick<
|
|
@@ -408,6 +408,13 @@ export interface AcceptanceLedger {
|
|
|
408
408
|
};
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
+
export interface ResourceLimitExceeded {
|
|
412
|
+
kind: "maxExecutionTimeMs" | "maxTokens";
|
|
413
|
+
limit: number;
|
|
414
|
+
observed?: number;
|
|
415
|
+
message: string;
|
|
416
|
+
}
|
|
417
|
+
|
|
411
418
|
export interface SingleResult {
|
|
412
419
|
agent: string;
|
|
413
420
|
task: string;
|
|
@@ -415,6 +422,8 @@ export interface SingleResult {
|
|
|
415
422
|
detached?: boolean;
|
|
416
423
|
detachedReason?: string;
|
|
417
424
|
interrupted?: boolean;
|
|
425
|
+
timedOut?: boolean;
|
|
426
|
+
resourceLimitExceeded?: ResourceLimitExceeded;
|
|
418
427
|
messages?: Message[];
|
|
419
428
|
usage: Usage;
|
|
420
429
|
model?: string;
|
|
@@ -639,6 +648,7 @@ export interface AsyncStatus {
|
|
|
639
648
|
structuredOutputPath?: string;
|
|
640
649
|
structuredOutputSchemaPath?: string;
|
|
641
650
|
acceptance?: AcceptanceLedger;
|
|
651
|
+
resourceLimitExceeded?: ResourceLimitExceeded;
|
|
642
652
|
}>;
|
|
643
653
|
sessionDir?: string;
|
|
644
654
|
outputFile?: string;
|
|
@@ -780,6 +790,8 @@ export interface RunSyncOptions {
|
|
|
780
790
|
cwd?: string;
|
|
781
791
|
signal?: AbortSignal;
|
|
782
792
|
interruptSignal?: AbortSignal;
|
|
793
|
+
timeoutMs?: number;
|
|
794
|
+
timeoutAt?: number;
|
|
783
795
|
allowIntercomDetach?: boolean;
|
|
784
796
|
intercomEvents?: IntercomEventBus;
|
|
785
797
|
onUpdate?: (r: import("@earendil-works/pi-agent-core").AgentToolResult<Details>) => void;
|
|
@@ -798,6 +810,8 @@ export interface RunSyncOptions {
|
|
|
798
810
|
outputPath?: string;
|
|
799
811
|
outputMode?: OutputMode;
|
|
800
812
|
maxSubagentDepth?: number;
|
|
813
|
+
maxExecutionTimeMs?: number;
|
|
814
|
+
maxTokens?: number;
|
|
801
815
|
nestedRoute?: NestedRouteInfo;
|
|
802
816
|
/** Override the agent's default model (format: "provider/id" or just "id") */
|
|
803
817
|
modelOverride?: string;
|
package/src/shared/utils.ts
CHANGED
|
@@ -204,6 +204,13 @@ export function getSingleResultOutput(result: Pick<SingleResult, "finalOutput" |
|
|
|
204
204
|
return result.finalOutput ?? getFinalOutput(result.messages ?? []);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
export function formatResourceLimitExceeded(input: { agent: string; kind: "maxExecutionTimeMs" | "maxTokens"; limit: number; observed?: number }): string {
|
|
208
|
+
if (input.kind === "maxExecutionTimeMs") {
|
|
209
|
+
return `Resource limit exceeded for ${input.agent}: maxExecutionTimeMs ${input.limit}ms.`;
|
|
210
|
+
}
|
|
211
|
+
return `Resource limit exceeded for ${input.agent}: maxTokens ${input.limit}${input.observed !== undefined ? ` (observed ${input.observed})` : ""}.`;
|
|
212
|
+
}
|
|
213
|
+
|
|
207
214
|
/**
|
|
208
215
|
* Extract display items (text and tool calls) from messages
|
|
209
216
|
*/
|
package/src/tui/render.ts
CHANGED
|
@@ -233,6 +233,7 @@ function formatAcceptanceStatus(result: Details["results"][number]): string | un
|
|
|
233
233
|
|
|
234
234
|
function resultStatusLine(result: Details["results"][number], output: string): string {
|
|
235
235
|
if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
|
|
236
|
+
if (result.timedOut) return `Timed out${result.error ? `: ${result.error}` : ""}`;
|
|
236
237
|
if (result.interrupted) return "Paused";
|
|
237
238
|
if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
|
|
238
239
|
const acceptance = formatAcceptanceStatus(result);
|
|
@@ -244,6 +245,7 @@ function resultStatusLine(result: Details["results"][number], output: string): s
|
|
|
244
245
|
function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary)): string {
|
|
245
246
|
if (running) return theme.fg("accent", runningGlyph(seed));
|
|
246
247
|
if (result.detached) return theme.fg("warning", "■");
|
|
248
|
+
if (result.timedOut) return theme.fg("error", "✗");
|
|
247
249
|
if (result.interrupted) return theme.fg("warning", "■");
|
|
248
250
|
if (result.exitCode !== 0) return theme.fg("error", "✗");
|
|
249
251
|
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return theme.fg("warning", "✓");
|
|
@@ -363,18 +365,19 @@ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
|
363
365
|
return theme.fg("error", "✗");
|
|
364
366
|
}
|
|
365
367
|
|
|
366
|
-
function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number): string {
|
|
368
|
+
function widgetStepGlyph(status: AsyncJobStep["status"] | WorkflowNodeStatus, theme: Theme, seed?: number): string {
|
|
367
369
|
if (status === "running") return theme.fg("accent", runningGlyph(seed));
|
|
368
370
|
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
369
|
-
if (status === "failed") return theme.fg("error", "✗");
|
|
371
|
+
if (status === "failed" || status === "timed-out") return theme.fg("error", "✗");
|
|
370
372
|
if (status === "paused") return theme.fg("warning", "■");
|
|
371
373
|
return theme.fg("muted", "◦");
|
|
372
374
|
}
|
|
373
375
|
|
|
374
|
-
function widgetStepStatus(status: AsyncJobStep["status"], theme: Theme): string {
|
|
376
|
+
function widgetStepStatus(status: AsyncJobStep["status"] | WorkflowNodeStatus, theme: Theme): string {
|
|
375
377
|
if (status === "running") return theme.fg("accent", "running");
|
|
376
378
|
if (status === "complete" || status === "completed") return theme.fg("success", "complete");
|
|
377
379
|
if (status === "failed") return theme.fg("error", "failed");
|
|
380
|
+
if (status === "timed-out") return theme.fg("error", "timed out");
|
|
378
381
|
if (status === "paused") return theme.fg("warning", "paused");
|
|
379
382
|
return theme.fg("dim", status);
|
|
380
383
|
}
|
|
@@ -511,7 +514,7 @@ function isDoneResult(result: Details["results"][number]): boolean {
|
|
|
511
514
|
const status = result.progress?.status;
|
|
512
515
|
if (status === "completed") return true;
|
|
513
516
|
if (status === "running" || status === "pending") return false;
|
|
514
|
-
if (result.interrupted || result.detached) return false;
|
|
517
|
+
if (result.interrupted || result.detached || result.timedOut) return false;
|
|
515
518
|
return result.exitCode === 0;
|
|
516
519
|
}
|
|
517
520
|
|
|
@@ -583,7 +586,7 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
583
586
|
|
|
584
587
|
if (details.mode === "parallel") {
|
|
585
588
|
const totalCount = details.totalSteps ?? details.results.length;
|
|
586
|
-
const statuses = new Array(totalCount).fill("pending") as
|
|
589
|
+
const statuses = new Array(totalCount).fill("pending") as WorkflowNodeStatus[];
|
|
587
590
|
for (const progress of details.progress ?? []) {
|
|
588
591
|
if (progress.index >= 0 && progress.index < totalCount) statuses[progress.index] = progress.status;
|
|
589
592
|
}
|
|
@@ -594,11 +597,13 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
594
597
|
const index = result.progress?.index ?? progressFromArray?.index ?? i;
|
|
595
598
|
if (index < 0 || index >= totalCount) continue;
|
|
596
599
|
const status = result.progress?.status
|
|
597
|
-
?? (result.
|
|
598
|
-
? "
|
|
599
|
-
: result.
|
|
600
|
-
? "
|
|
601
|
-
:
|
|
600
|
+
?? (result.timedOut
|
|
601
|
+
? "timed-out"
|
|
602
|
+
: result.interrupted || result.detached
|
|
603
|
+
? "detached"
|
|
604
|
+
: result.exitCode === 0
|
|
605
|
+
? "completed"
|
|
606
|
+
: "failed");
|
|
602
607
|
statuses[index] = status;
|
|
603
608
|
}
|
|
604
609
|
const running = statuses.filter((status) => status === "running").length;
|
|
@@ -1060,7 +1065,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
1060
1065
|
|| d.results.some((r) => r.progress?.status === "running")
|
|
1061
1066
|
|| workflowGraphHasStatus(d, ["running"]);
|
|
1062
1067
|
const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running")
|
|
1063
|
-
|| workflowGraphHasStatus(d, ["failed"]);
|
|
1068
|
+
|| workflowGraphHasStatus(d, ["failed", "timed-out"]);
|
|
1064
1069
|
const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running")
|
|
1065
1070
|
|| workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
1066
1071
|
let totalSummary = d.progressSummary;
|
|
@@ -1136,7 +1141,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
1136
1141
|
const activity = compactCurrentActivity(rProg);
|
|
1137
1142
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
1138
1143
|
c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
|
|
1139
|
-
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
|
|
1144
|
+
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || r.timedOut || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
|
|
1140
1145
|
c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
|
|
1141
1146
|
}
|
|
1142
1147
|
const outputTarget = extractOutputTarget(r.task);
|
|
@@ -1273,7 +1278,7 @@ export function renderSubagentResult(
|
|
|
1273
1278
|
&& r.progress?.status !== "running"
|
|
1274
1279
|
&& hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
|
|
1275
1280
|
);
|
|
1276
|
-
const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed"]);
|
|
1281
|
+
const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed", "timed-out"]);
|
|
1277
1282
|
const hasWorkflowPause = workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
1278
1283
|
const icon = hasRunning
|
|
1279
1284
|
? theme.fg("warning", "running")
|