pi-subagents 0.48.0 → 0.50.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 +82 -19
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/docs/agents.md +3 -0
- package/docs/configuration.md +75 -8
- package/docs/extension-api.md +36 -0
- package/docs/missions.md +5 -3
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +19 -2
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +37 -12
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +13 -7
- package/src/extension/config.ts +53 -0
- package/src/extension/index.ts +62 -9
- package/src/extension/public-execution.ts +32 -4
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +10 -8
- package/src/extension/tool-description.ts +13 -9
- package/src/inspectors/herdr/actions.ts +13 -8
- package/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/src/inspectors/herdr/project-panes.ts +2 -6
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +5 -4
- package/src/intercom/native-supervisor-channel.ts +19 -42
- package/src/missions/goal-driver.ts +3 -1
- package/src/missions/store.ts +8 -3
- package/src/runs/background/active-async-capacity.ts +82 -25
- package/src/runs/background/active-run-index.ts +71 -1
- package/src/runs/background/async-execution.ts +73 -43
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-resume.ts +14 -6
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +8 -3
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/fleet-view.ts +21 -6
- package/src/runs/background/result-files.ts +437 -0
- package/src/runs/background/result-watcher.ts +188 -41
- package/src/runs/background/resume-guidance.ts +27 -7
- package/src/runs/background/retained-children.ts +75 -18
- package/src/runs/background/run-id-resolver.ts +30 -24
- package/src/runs/background/run-status.ts +101 -4
- package/src/runs/background/scheduled-runs.ts +54 -28
- package/src/runs/background/stale-run-reconciler.ts +27 -13
- package/src/runs/background/subagent-runner.ts +298 -33
- package/src/runs/background/subagent-wait.ts +2 -0
- package/src/runs/background/wait-completions.ts +5 -2
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/chain-execution.ts +16 -0
- package/src/runs/foreground/execution.ts +219 -15
- package/src/runs/foreground/prompt-audit.ts +4 -3
- package/src/runs/foreground/subagent-executor.ts +328 -43
- package/src/runs/shared/completion-guard.ts +107 -1
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/src/runs/shared/model-fallback.ts +16 -2
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +375 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/subagent-control.ts +15 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/shared/agent-stream-options.ts +5 -0
- package/src/shared/artifacts.ts +2 -6
- package/src/shared/display-text.ts +50 -0
- package/src/shared/node-executable.ts +21 -0
- package/src/shared/types.ts +51 -5
- package/src/slash/slash-commands.ts +34 -25
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet-transcript.ts +1 -48
- package/src/tui/fleet.ts +128 -16
- package/src/tui/render.ts +122 -44
- package/src/watchdog/permission-arbiter.ts +2 -1
- package/src/watchdog/review.ts +4 -3
- package/src/workflows/chat-progress.ts +10 -2
- package/src/workflows/scripted-workflow.ts +272 -76
|
@@ -19,10 +19,12 @@ import { buildChainInstructions, isCheckpointStep, isDynamicParallelStep, isPara
|
|
|
19
19
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
20
20
|
import type { ContextMode } from "../shared/context-mode.ts";
|
|
21
21
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
22
|
+
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
22
23
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
23
24
|
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
24
25
|
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
|
|
25
26
|
import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
|
|
27
|
+
import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
|
|
26
28
|
import type { ModelScopeConfig } from "../shared/model-scope.ts";
|
|
27
29
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
28
30
|
import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
|
|
@@ -37,6 +39,7 @@ import {
|
|
|
37
39
|
type AsyncStatus,
|
|
38
40
|
type ArtifactConfig,
|
|
39
41
|
type Details,
|
|
42
|
+
type IntercomBridgeConfig,
|
|
40
43
|
type JsonSchemaObject,
|
|
41
44
|
type MaxOutputConfig,
|
|
42
45
|
type NestedRouteInfo,
|
|
@@ -56,6 +59,7 @@ import {
|
|
|
56
59
|
resolveChildMaxSubagentDepth,
|
|
57
60
|
} from "../../shared/types.ts";
|
|
58
61
|
import { nestedResultsPath, nestedSummaryFromAsyncStatus, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
|
|
62
|
+
import { resultFilePath } from "./result-files.ts";
|
|
59
63
|
import { appendTurnBudgetSystemPrompt, initialTurnBudgetState } from "../shared/turn-budget.ts";
|
|
60
64
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
61
65
|
import { usageBudgetState } from "../shared/usage-budget.ts";
|
|
@@ -172,6 +176,12 @@ interface AsyncChainParams {
|
|
|
172
176
|
toolBudget?: ResolvedToolBudget;
|
|
173
177
|
usageBudget?: UsageBudgetConfig;
|
|
174
178
|
configToolBudget?: ResolvedToolBudget;
|
|
179
|
+
/** Optional per-call hard toolTimeoutMs override (highest precedence). */
|
|
180
|
+
callToolTimeoutMs?: number;
|
|
181
|
+
/** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
|
|
182
|
+
configToolTimeoutMs?: number;
|
|
183
|
+
/** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
|
|
184
|
+
toolTimeoutMsEnv?: string | undefined;
|
|
175
185
|
/** Global cap on simultaneously-running subagent tasks within the async run. */
|
|
176
186
|
globalConcurrencyLimit?: number;
|
|
177
187
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
@@ -187,6 +197,8 @@ interface AsyncSingleParams {
|
|
|
187
197
|
/** Raw caller-facing goal used only by the started event. */
|
|
188
198
|
goal?: string;
|
|
189
199
|
agentConfig: AgentConfig;
|
|
200
|
+
/** Agent contract before per-run bridge injection, used only for recovery persistence. */
|
|
201
|
+
recoveryAgentConfig?: AgentConfig;
|
|
190
202
|
ctx: AsyncExecutionContext;
|
|
191
203
|
cwd?: string;
|
|
192
204
|
maxOutput?: MaxOutputConfig;
|
|
@@ -214,16 +226,23 @@ interface AsyncSingleParams {
|
|
|
214
226
|
worktreeSetupHookTimeoutMs?: number;
|
|
215
227
|
worktreeBaseDir?: string;
|
|
216
228
|
controlConfig?: ResolvedControlConfig;
|
|
229
|
+
intercomBridge?: IntercomBridgeConfig;
|
|
217
230
|
controlIntercomTarget?: string;
|
|
218
231
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
219
232
|
nestedRoute?: NestedRouteInfo;
|
|
220
233
|
acceptance?: AcceptanceInput;
|
|
221
234
|
timeoutMs?: number;
|
|
222
235
|
absoluteDeadlineAt?: number;
|
|
236
|
+
/** Optional per-call hard toolTimeoutMs override (highest precedence). */
|
|
237
|
+
toolTimeoutMs?: number;
|
|
223
238
|
turnBudget?: { maxTurns: number; graceTurns?: number };
|
|
224
239
|
toolBudget?: ResolvedToolBudget | ToolBudgetConfig;
|
|
225
240
|
usageBudget?: UsageBudgetConfig;
|
|
226
241
|
configToolBudget?: ResolvedToolBudget;
|
|
242
|
+
/** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
|
|
243
|
+
configToolTimeoutMs?: number;
|
|
244
|
+
/** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
|
|
245
|
+
toolTimeoutMsEnv?: string | undefined;
|
|
227
246
|
allowZeroToolBudget?: boolean;
|
|
228
247
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
229
248
|
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
@@ -262,6 +281,12 @@ export interface AsyncRunnerStepBuildParams {
|
|
|
262
281
|
validateOutputBindings?: boolean;
|
|
263
282
|
toolBudget?: ResolvedToolBudget;
|
|
264
283
|
configToolBudget?: ResolvedToolBudget;
|
|
284
|
+
/** Optional per-call hard toolTimeoutMs override from the subagent invocation. */
|
|
285
|
+
callToolTimeoutMs?: number;
|
|
286
|
+
/** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
|
|
287
|
+
configToolTimeoutMs?: number;
|
|
288
|
+
/** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
|
|
289
|
+
toolTimeoutMsEnv?: string | undefined;
|
|
265
290
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
266
291
|
}
|
|
267
292
|
|
|
@@ -299,27 +324,6 @@ export function isAsyncAvailable(): boolean {
|
|
|
299
324
|
return jitiCliPath !== undefined;
|
|
300
325
|
}
|
|
301
326
|
|
|
302
|
-
function isNodeExecutableName(execPath: string): boolean {
|
|
303
|
-
const basename = path.basename(execPath).toLowerCase();
|
|
304
|
-
return basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe";
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function canUseCurrentNodeExecutable(execPath: string): boolean {
|
|
308
|
-
try {
|
|
309
|
-
fs.accessSync(execPath, process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
|
|
310
|
-
return true;
|
|
311
|
-
} catch {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function resolveAsyncRunnerNodeCommand(): string {
|
|
317
|
-
if (isNodeExecutableName(process.execPath) && canUseCurrentNodeExecutable(process.execPath)) {
|
|
318
|
-
return process.execPath;
|
|
319
|
-
}
|
|
320
|
-
return process.platform === "win32" ? "node.exe" : "node";
|
|
321
|
-
}
|
|
322
|
-
|
|
323
327
|
export function resolveAsyncRunnerLogPaths(cfg: object): { stdoutPath: string; stderrPath: string } | undefined {
|
|
324
328
|
const asyncDir = typeof (cfg as { asyncDir?: unknown }).asyncDir === "string"
|
|
325
329
|
? (cfg as { asyncDir: string }).asyncDir
|
|
@@ -486,7 +490,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
|
|
|
486
490
|
const launchConfig = { ...cfg, runnerProcessInstanceId };
|
|
487
491
|
fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
|
|
488
492
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
489
|
-
const nodeCommand =
|
|
493
|
+
const nodeCommand = resolveNodeExecutable();
|
|
490
494
|
const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; revivalLease?: unknown };
|
|
491
495
|
const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
|
|
492
496
|
const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
|
|
@@ -730,6 +734,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
730
734
|
const toolBudgetInput = s.toolBudget ?? params.toolBudget ?? a.toolBudget ?? params.configToolBudget;
|
|
731
735
|
const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, s.toolBudget ? "toolBudget" : a.toolBudget ? "agent.toolBudget" : "config.toolBudget");
|
|
732
736
|
if (resolvedToolBudget.error) throw new AsyncStartValidationError(resolvedToolBudget.error);
|
|
737
|
+
const resolvedToolTimeout = resolveToolTimeoutMs({
|
|
738
|
+
callValue: params.callToolTimeoutMs,
|
|
739
|
+
agentValue: a.defaultToolTimeoutMs,
|
|
740
|
+
configValue: params.configToolTimeoutMs,
|
|
741
|
+
envValue: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
|
|
742
|
+
});
|
|
743
|
+
if (resolvedToolTimeout.error) throw new AsyncStartValidationError(resolvedToolTimeout.error);
|
|
733
744
|
const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
|
|
734
745
|
const instructionCwd = behaviorCwd ?? stepCwd;
|
|
735
746
|
const readExistenceCwd = behaviorCwd ? stepCwd : instructionCwd;
|
|
@@ -843,6 +854,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
843
854
|
sessionFile,
|
|
844
855
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
|
|
845
856
|
timeoutMs: a.defaultTimeoutMs ?? DEFAULT_ASYNC_TIMEOUT_MS,
|
|
857
|
+
toolTimeoutMs: resolvedToolTimeout.toolTimeoutMs,
|
|
846
858
|
waitToolEnabled: params.waitToolEnabled,
|
|
847
859
|
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
848
860
|
explicit: s.acceptance,
|
|
@@ -1066,6 +1078,9 @@ export function executeAsyncChain(
|
|
|
1066
1078
|
asyncDir,
|
|
1067
1079
|
toolBudget: params.toolBudget,
|
|
1068
1080
|
configToolBudget: params.configToolBudget,
|
|
1081
|
+
callToolTimeoutMs: params.callToolTimeoutMs,
|
|
1082
|
+
configToolTimeoutMs: params.configToolTimeoutMs,
|
|
1083
|
+
toolTimeoutMsEnv: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
|
|
1069
1084
|
capabilityCeiling,
|
|
1070
1085
|
});
|
|
1071
1086
|
if ("error" in built) {
|
|
@@ -1102,7 +1117,7 @@ export function executeAsyncChain(
|
|
|
1102
1117
|
{
|
|
1103
1118
|
id,
|
|
1104
1119
|
steps,
|
|
1105
|
-
resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) :
|
|
1120
|
+
resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : resultFilePath(DIRS.results, id),
|
|
1106
1121
|
cwd: runnerCwd,
|
|
1107
1122
|
placeholder: "{previous}",
|
|
1108
1123
|
maxOutput,
|
|
@@ -1251,6 +1266,7 @@ export function executeAsyncChain(
|
|
|
1251
1266
|
workflowGraph,
|
|
1252
1267
|
cwd: runnerCwd,
|
|
1253
1268
|
asyncDir,
|
|
1269
|
+
...(sessionRoot ? { sessionRoot } : {}),
|
|
1254
1270
|
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
|
|
1255
1271
|
...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
|
|
1256
1272
|
...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}),
|
|
@@ -1399,17 +1415,27 @@ export function executeAsyncSingle(
|
|
|
1399
1415
|
? deadlineAt - Date.now()
|
|
1400
1416
|
: params.timeoutMs;
|
|
1401
1417
|
if (timeoutMs !== undefined && timeoutMs <= 0) return formatAsyncStartError("single", "The source run's absolute deadline expired before recovery could launch.");
|
|
1418
|
+
const resolvedToolTimeout = resolveToolTimeoutMs({
|
|
1419
|
+
callValue: params.toolTimeoutMs,
|
|
1420
|
+
agentValue: agentConfig.defaultToolTimeoutMs,
|
|
1421
|
+
configValue: params.configToolTimeoutMs,
|
|
1422
|
+
envValue: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
|
|
1423
|
+
});
|
|
1424
|
+
if (resolvedToolTimeout.error) return formatAsyncStartError("single", resolvedToolTimeout.error);
|
|
1425
|
+
const toolTimeoutMs = resolvedToolTimeout.toolTimeoutMs;
|
|
1402
1426
|
const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
|
|
1403
1427
|
const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
|
|
1404
1428
|
const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
|
|
1405
1429
|
const structuredOutput = params.structuredOutputSchema
|
|
1406
1430
|
? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
|
|
1407
1431
|
: undefined;
|
|
1408
|
-
const modelCandidates =
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1432
|
+
const modelCandidates = externalRunner
|
|
1433
|
+
? []
|
|
1434
|
+
: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope })
|
|
1435
|
+
.flatMap((candidate) => {
|
|
1436
|
+
const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
|
|
1437
|
+
return resolved ? [resolved] : [];
|
|
1438
|
+
});
|
|
1413
1439
|
const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
|
|
1414
1440
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
1415
1441
|
tools: agentConfig.tools,
|
|
@@ -1451,6 +1477,7 @@ export function executeAsyncSingle(
|
|
|
1451
1477
|
async: true,
|
|
1452
1478
|
agentContract: params.agentContract,
|
|
1453
1479
|
});
|
|
1480
|
+
const recoveryAgentConfig = params.recoveryAgentConfig ?? agentConfig;
|
|
1454
1481
|
const recoveryDescriptor: SteeringRecoveryDescriptor = {
|
|
1455
1482
|
version: 1,
|
|
1456
1483
|
launchContractDigest,
|
|
@@ -1462,30 +1489,31 @@ export function executeAsyncSingle(
|
|
|
1462
1489
|
...(sessionFile ? { sessionFile } : {}),
|
|
1463
1490
|
cwd: runnerCwd,
|
|
1464
1491
|
...(model ? { model } : {}),
|
|
1465
|
-
...(
|
|
1492
|
+
...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
|
|
1466
1493
|
...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
|
|
1467
|
-
...(
|
|
1468
|
-
...(
|
|
1469
|
-
...(
|
|
1470
|
-
...(
|
|
1471
|
-
...(
|
|
1472
|
-
systemPromptMode:
|
|
1473
|
-
inheritProjectContext:
|
|
1474
|
-
inheritSkills:
|
|
1494
|
+
...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
|
|
1495
|
+
...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
|
|
1496
|
+
...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
|
|
1497
|
+
...(recoveryAgentConfig.mcpDirectTools ? { mcpDirectTools: [...recoveryAgentConfig.mcpDirectTools] } : {}),
|
|
1498
|
+
...(recoveryAgentConfig.systemPrompt ? { systemPrompt: recoveryAgentConfig.systemPrompt } : {}),
|
|
1499
|
+
systemPromptMode: recoveryAgentConfig.systemPromptMode,
|
|
1500
|
+
inheritProjectContext: recoveryAgentConfig.inheritProjectContext,
|
|
1501
|
+
inheritSkills: recoveryAgentConfig.inheritSkills,
|
|
1475
1502
|
...(resolvedSkills.length ? { skills: resolvedSkills.map((skill) => skill.name) } : {}),
|
|
1476
|
-
...(
|
|
1477
|
-
...(
|
|
1478
|
-
...(
|
|
1479
|
-
...(
|
|
1503
|
+
...(recoveryAgentConfig.skillPath ? { skillPath: [...recoveryAgentConfig.skillPath] } : {}),
|
|
1504
|
+
...(recoveryAgentConfig.filePath ? { agentFilePath: recoveryAgentConfig.filePath } : {}),
|
|
1505
|
+
...(recoveryAgentConfig.completionGuard !== undefined ? { completionGuard: recoveryAgentConfig.completionGuard } : {}),
|
|
1506
|
+
...(recoveryAgentConfig.memory ? { memory: { ...recoveryAgentConfig.memory } } : {}),
|
|
1480
1507
|
...(outputPath ? { outputPath } : {}),
|
|
1481
1508
|
outputMode,
|
|
1482
1509
|
...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
|
|
1483
1510
|
...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
|
|
1484
1511
|
...(controlConfig ? { controlConfig } : {}),
|
|
1512
|
+
...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
|
|
1485
1513
|
...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
|
|
1486
1514
|
...(initialTurnBudget ? { initialTurnBudget: { maxTurns: initialTurnBudget.maxTurns, graceTurns: initialTurnBudget.graceTurns } } : {}),
|
|
1487
1515
|
...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
|
|
1488
|
-
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth,
|
|
1516
|
+
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, recoveryAgentConfig.maxSubagentDepth),
|
|
1489
1517
|
...(maxOutput ? { maxOutput } : {}),
|
|
1490
1518
|
share: shareEnabled,
|
|
1491
1519
|
...(resolvedSessionDir ? { sessionDir: resolvedSessionDir } : {}),
|
|
@@ -1546,7 +1574,7 @@ export function executeAsyncSingle(
|
|
|
1546
1574
|
],
|
|
1547
1575
|
resultPath: params.parentWorkflowRunId !== undefined && params.revivalLease !== undefined
|
|
1548
1576
|
? workflowAwaitedAsyncResultPath(asyncDir)
|
|
1549
|
-
: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) :
|
|
1577
|
+
: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : resultFilePath(DIRS.results, id),
|
|
1550
1578
|
cwd: runnerCwd,
|
|
1551
1579
|
placeholder: "{previous}",
|
|
1552
1580
|
maxOutput,
|
|
@@ -1565,6 +1593,7 @@ export function executeAsyncSingle(
|
|
|
1565
1593
|
controlConfig,
|
|
1566
1594
|
timeoutMs,
|
|
1567
1595
|
deadlineAt,
|
|
1596
|
+
toolTimeoutMs,
|
|
1568
1597
|
turnBudget: params.turnBudget,
|
|
1569
1598
|
toolBudget: params.toolBudget,
|
|
1570
1599
|
usageBudget: params.usageBudget,
|
|
@@ -1654,6 +1683,7 @@ export function executeAsyncSingle(
|
|
|
1654
1683
|
goal: (params.goal ?? task).trim() ? PROMPT_REDACTED : undefined,
|
|
1655
1684
|
cwd: runnerCwd,
|
|
1656
1685
|
asyncDir,
|
|
1686
|
+
...(sessionRoot ? { sessionRoot } : {}),
|
|
1657
1687
|
launchContractDigest,
|
|
1658
1688
|
launchResolvedExtensions,
|
|
1659
1689
|
...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
|
|
@@ -125,6 +125,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
125
125
|
return {
|
|
126
126
|
asyncId: run.id,
|
|
127
127
|
asyncDir: run.asyncDir,
|
|
128
|
+
toolCallId: run.toolCallId,
|
|
128
129
|
status: run.state,
|
|
129
130
|
sessionId: run.sessionId,
|
|
130
131
|
activityState: run.activityState,
|
|
@@ -557,10 +558,13 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
557
558
|
const agents = firstGroupCount && firstGroupCount > 0
|
|
558
559
|
? rawAgents?.slice(0, firstGroupCount)
|
|
559
560
|
: rawAgents;
|
|
561
|
+
const sessionRoot = state.liveAsyncSessionRoots?.get(info.id);
|
|
562
|
+
state.liveAsyncSessionRoots?.delete(info.id);
|
|
560
563
|
state.asyncJobs.set(info.id, {
|
|
561
564
|
asyncId: info.id,
|
|
562
565
|
asyncDir,
|
|
563
566
|
...(typeof info.cwd === "string" ? { cwd: path.resolve(info.cwd) } : {}),
|
|
567
|
+
...(sessionRoot ? { sessionRoot } : {}),
|
|
564
568
|
status: "queued",
|
|
565
569
|
pid: typeof info.pid === "number" ? info.pid : undefined,
|
|
566
570
|
...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
|
|
@@ -637,6 +641,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
637
641
|
state.asyncJobs.clear();
|
|
638
642
|
state.fleetJobs?.clear();
|
|
639
643
|
state.foregroundControls?.clear();
|
|
644
|
+
state.liveAsyncSessionRoots?.clear();
|
|
640
645
|
state.lastForegroundControlId = null;
|
|
641
646
|
state.resultFileCoalescer.clear();
|
|
642
647
|
if (ctx?.hasUI) {
|
|
@@ -8,6 +8,7 @@ import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, ty
|
|
|
8
8
|
import { validateRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
9
9
|
import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
|
|
10
10
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
11
|
+
import { resultFilePath } from "./result-files.ts";
|
|
11
12
|
|
|
12
13
|
export interface AsyncResumeParams {
|
|
13
14
|
id?: string;
|
|
@@ -172,7 +173,7 @@ function prefixedRunIds(dir: string, prefix: string, suffix = ""): string[] {
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
function exactResultPath(resultsDir: string, runId: string): string | null {
|
|
175
|
-
const resultPath =
|
|
176
|
+
const resultPath = resultFilePath(resultsDir, runId);
|
|
176
177
|
assertInsideRoot(resultsDir, resultPath, "Async result file");
|
|
177
178
|
return fs.existsSync(resultPath) ? resultPath : null;
|
|
178
179
|
}
|
|
@@ -182,10 +183,7 @@ export function findAsyncRunPrefixMatches(prefix: string, asyncDirRoot: string,
|
|
|
182
183
|
if (!requestedId) return [];
|
|
183
184
|
const asyncRoot = path.resolve(asyncDirRoot);
|
|
184
185
|
const resultRoot = path.resolve(resultsDir);
|
|
185
|
-
const matchingIds =
|
|
186
|
-
...prefixedRunIds(asyncRoot, requestedId),
|
|
187
|
-
...prefixedRunIds(resultRoot, requestedId, ".json"),
|
|
188
|
-
])].sort();
|
|
186
|
+
const matchingIds = prefixedRunIds(asyncRoot, requestedId).sort();
|
|
189
187
|
return matchingIds.map((id) => {
|
|
190
188
|
const asyncDir = path.join(asyncRoot, id);
|
|
191
189
|
assertInsideRoot(asyncRoot, asyncDir, "Async run directory");
|
|
@@ -307,7 +305,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
307
305
|
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
|
|
308
306
|
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
309
307
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
310
|
-
"artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
308
|
+
"artifactsDir", "maxOutput", "controlConfig", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
311
309
|
"launchResolvedExtensions", "runFanoutBudget",
|
|
312
310
|
]);
|
|
313
311
|
for (const field of Object.keys(parsed)) {
|
|
@@ -374,6 +372,16 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
374
372
|
if (artifact.includeTranscript !== undefined && typeof artifact.includeTranscript !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.includeTranscript must be a boolean.`);
|
|
375
373
|
if (!Number.isInteger(artifact.cleanupDays) || (artifact.cleanupDays as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.cleanupDays must be a non-negative integer.`);
|
|
376
374
|
}
|
|
375
|
+
if (parsed.intercomBridge !== undefined) {
|
|
376
|
+
if (!parsed.intercomBridge || typeof parsed.intercomBridge !== "object" || Array.isArray(parsed.intercomBridge)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge must be an object.`);
|
|
377
|
+
const bridge = parsed.intercomBridge as Record<string, unknown>;
|
|
378
|
+
for (const field of Object.keys(bridge)) {
|
|
379
|
+
if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.${field} is not supported.`);
|
|
380
|
+
}
|
|
381
|
+
if (bridge.mode !== undefined && bridge.mode !== "off" && bridge.mode !== "fork-only" && bridge.mode !== "always") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.mode is invalid.`);
|
|
382
|
+
if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.instructionFile must be a string.`);
|
|
383
|
+
if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.resultDelivery must be a boolean.`);
|
|
384
|
+
}
|
|
377
385
|
if (parsed.controlConfig !== undefined) {
|
|
378
386
|
if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
|
|
379
387
|
const control = parsed.controlConfig as Record<string, unknown>;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-text.ts";
|
|
2
|
+
import type { AsyncJobState, AsyncJobStep, NestedRunSummary, NestedStepSummary, SubagentRunMode, SubagentState } from "../../shared/types.ts";
|
|
3
|
+
|
|
4
|
+
export const ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
|
|
5
|
+
export const ASYNC_STATUS_SNAPSHOT_VERSION = 1;
|
|
6
|
+
export const ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX = "PI_SUBAGENT_ASYNC_JSON:";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_RUNS = 20;
|
|
9
|
+
const DEFAULT_MAX_CHILDREN_PER_NODE = 8;
|
|
10
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
11
|
+
const DEFAULT_MAX_STRING_LENGTH = 160;
|
|
12
|
+
const DEFAULT_MAX_SERIALIZED_BYTES = 32 * 1024;
|
|
13
|
+
|
|
14
|
+
export type AsyncStatusSnapshotState = "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
|
|
15
|
+
export type AsyncStatusSnapshotKind = "subagent" | "workflow" | "step";
|
|
16
|
+
|
|
17
|
+
export interface AsyncStatusSnapshotActivityV1 {
|
|
18
|
+
state?: string;
|
|
19
|
+
currentTool?: string;
|
|
20
|
+
lastActivityAt?: number;
|
|
21
|
+
currentToolStartedAt?: number;
|
|
22
|
+
turnCount?: number;
|
|
23
|
+
toolCount?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AsyncStatusSnapshotNodeV1 {
|
|
27
|
+
id: string;
|
|
28
|
+
kind: AsyncStatusSnapshotKind;
|
|
29
|
+
label: string;
|
|
30
|
+
state: AsyncStatusSnapshotState;
|
|
31
|
+
startedAt?: number;
|
|
32
|
+
updatedAt?: number;
|
|
33
|
+
endedAt?: number;
|
|
34
|
+
activity?: AsyncStatusSnapshotActivityV1;
|
|
35
|
+
children?: AsyncStatusSnapshotNodeV1[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AsyncStatusSnapshotCapsV1 {
|
|
39
|
+
maxRuns: number;
|
|
40
|
+
maxChildrenPerNode: number;
|
|
41
|
+
maxDepth: number;
|
|
42
|
+
maxStringLength: number;
|
|
43
|
+
maxSerializedBytes: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AsyncStatusSnapshotOmittedV1 {
|
|
47
|
+
runs: number;
|
|
48
|
+
children: number;
|
|
49
|
+
byteLimitExceeded: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AsyncStatusSnapshotV1 {
|
|
53
|
+
kind: typeof ASYNC_STATUS_SNAPSHOT_KIND;
|
|
54
|
+
version: typeof ASYNC_STATUS_SNAPSHOT_VERSION;
|
|
55
|
+
generatedAt: number;
|
|
56
|
+
caps: AsyncStatusSnapshotCapsV1;
|
|
57
|
+
omitted: AsyncStatusSnapshotOmittedV1;
|
|
58
|
+
runs: AsyncStatusSnapshotNodeV1[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AsyncStatusSnapshotOptions {
|
|
62
|
+
generatedAt?: number;
|
|
63
|
+
maxRuns?: number;
|
|
64
|
+
maxChildrenPerNode?: number;
|
|
65
|
+
maxDepth?: number;
|
|
66
|
+
maxStringLength?: number;
|
|
67
|
+
maxSerializedBytes?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface BuildContext {
|
|
71
|
+
caps: AsyncStatusSnapshotCapsV1;
|
|
72
|
+
omitted: AsyncStatusSnapshotOmittedV1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveCaps(options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotCapsV1 {
|
|
76
|
+
return {
|
|
77
|
+
maxRuns: Math.max(0, Math.floor(options.maxRuns ?? DEFAULT_MAX_RUNS)),
|
|
78
|
+
maxChildrenPerNode: Math.max(0, Math.floor(options.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE)),
|
|
79
|
+
maxDepth: Math.max(0, Math.floor(options.maxDepth ?? DEFAULT_MAX_DEPTH)),
|
|
80
|
+
maxStringLength: Math.max(0, Math.floor(options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH)),
|
|
81
|
+
maxSerializedBytes: Math.max(256, Math.floor(options.maxSerializedBytes ?? DEFAULT_MAX_SERIALIZED_BYTES)),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function publicText(value: unknown, fallback: string, maxLength: number): string {
|
|
86
|
+
if (typeof value !== "string") return fallback;
|
|
87
|
+
const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
|
|
88
|
+
return truncateDisplayText(normalized || fallback, maxLength);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function publicOptionalText(value: unknown, maxLength: number): string | undefined {
|
|
92
|
+
if (typeof value !== "string") return undefined;
|
|
93
|
+
const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
|
|
94
|
+
return normalized ? truncateDisplayText(normalized, maxLength) : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function publicTime(value: unknown): number | undefined {
|
|
98
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function publicCount(value: unknown): number | undefined {
|
|
102
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeState(value: string): AsyncStatusSnapshotState {
|
|
106
|
+
if (value === "completed") return "complete";
|
|
107
|
+
if (value === "pending") return "queued";
|
|
108
|
+
return value as AsyncStatusSnapshotState;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function terminalState(state: AsyncStatusSnapshotState): boolean {
|
|
112
|
+
return state === "complete" || state === "failed" || state === "paused" || state === "stopped" || state === "rejected";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function kindForMode(mode: SubagentRunMode | undefined): AsyncStatusSnapshotKind {
|
|
116
|
+
return mode === "workflow" ? "workflow" : "subagent";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function labelForAgents(agents: readonly string[] | undefined, fallback: string, maxLength: number): string {
|
|
120
|
+
if (!agents?.length) return publicText(fallback, fallback, maxLength);
|
|
121
|
+
const visible = agents.slice(0, 3).map((agent) => publicText(agent, "agent", maxLength)).join(", ");
|
|
122
|
+
const suffix = agents.length > 3 ? `, +${agents.length - 3} more` : "";
|
|
123
|
+
return truncateDisplayText(`${visible}${suffix}`, maxLength);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function activityFor(source: {
|
|
127
|
+
activityState?: unknown;
|
|
128
|
+
currentTool?: unknown;
|
|
129
|
+
lastActivityAt?: unknown;
|
|
130
|
+
currentToolStartedAt?: unknown;
|
|
131
|
+
turnCount?: unknown;
|
|
132
|
+
toolCount?: unknown;
|
|
133
|
+
}, ctx: BuildContext): AsyncStatusSnapshotActivityV1 | undefined {
|
|
134
|
+
const activity: AsyncStatusSnapshotActivityV1 = {
|
|
135
|
+
...(typeof source.activityState === "string" ? { state: publicText(source.activityState, "unknown", ctx.caps.maxStringLength) } : {}),
|
|
136
|
+
...(publicOptionalText(source.currentTool, ctx.caps.maxStringLength) ? { currentTool: publicOptionalText(source.currentTool, ctx.caps.maxStringLength) } : {}),
|
|
137
|
+
...(publicTime(source.lastActivityAt) !== undefined ? { lastActivityAt: publicTime(source.lastActivityAt) } : {}),
|
|
138
|
+
...(publicTime(source.currentToolStartedAt) !== undefined ? { currentToolStartedAt: publicTime(source.currentToolStartedAt) } : {}),
|
|
139
|
+
...(publicCount(source.turnCount) !== undefined ? { turnCount: publicCount(source.turnCount) } : {}),
|
|
140
|
+
...(publicCount(source.toolCount) !== undefined ? { toolCount: publicCount(source.toolCount) } : {}),
|
|
141
|
+
};
|
|
142
|
+
return Object.keys(activity).length ? activity : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function appendBoundedChildren(children: AsyncStatusSnapshotNodeV1[], source: readonly AsyncStatusSnapshotNodeV1[], ctx: BuildContext): void {
|
|
146
|
+
const remaining = Math.max(0, ctx.caps.maxChildrenPerNode - children.length);
|
|
147
|
+
children.push(...source.slice(0, remaining));
|
|
148
|
+
ctx.omitted.children += Math.max(0, source.length - remaining);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildStepNode(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
|
|
152
|
+
const state = normalizeState(step.status);
|
|
153
|
+
const updatedAt = publicTime(step.endedAt) ?? publicTime(step.lastActivityAt) ?? publicTime(step.startedAt);
|
|
154
|
+
const node: AsyncStatusSnapshotNodeV1 = {
|
|
155
|
+
id: publicText("workflowKey" in step && step.workflowKey ? step.workflowKey : "runId" in step && step.runId ? step.runId : `step:${index}`, `step:${index}`, ctx.caps.maxStringLength),
|
|
156
|
+
kind: "step",
|
|
157
|
+
label: publicText("label" in step && step.label ? step.label : step.agent, "step", ctx.caps.maxStringLength),
|
|
158
|
+
state,
|
|
159
|
+
...(publicTime(step.startedAt) !== undefined ? { startedAt: publicTime(step.startedAt) } : {}),
|
|
160
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
161
|
+
...(terminalState(state) && publicTime(step.endedAt) !== undefined ? { endedAt: publicTime(step.endedAt) } : {}),
|
|
162
|
+
...(activityFor(step, ctx) ? { activity: activityFor(step, ctx) } : {}),
|
|
163
|
+
};
|
|
164
|
+
if (depth < ctx.caps.maxDepth && step.children?.length) {
|
|
165
|
+
const nested = step.children.map((child, childIndex) => buildNestedNode(child, childIndex, depth + 1, ctx));
|
|
166
|
+
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
167
|
+
appendBoundedChildren(bounded, nested, ctx);
|
|
168
|
+
if (bounded.length) node.children = bounded;
|
|
169
|
+
} else if (step.children?.length) {
|
|
170
|
+
ctx.omitted.children += step.children.length;
|
|
171
|
+
}
|
|
172
|
+
return node;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildNestedNode(child: NestedRunSummary, index: number, depth: number, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
|
|
176
|
+
const state = normalizeState(child.state);
|
|
177
|
+
const updatedAt = publicTime(child.lastUpdate) ?? publicTime(child.endedAt) ?? publicTime(child.lastActivityAt) ?? publicTime(child.startedAt);
|
|
178
|
+
const node: AsyncStatusSnapshotNodeV1 = {
|
|
179
|
+
id: publicText(child.id, `nested:${index}`, ctx.caps.maxStringLength),
|
|
180
|
+
kind: kindForMode(child.mode),
|
|
181
|
+
label: child.agent ? publicText(child.agent, "subagent", ctx.caps.maxStringLength) : labelForAgents(child.agents, child.mode ?? "subagent", ctx.caps.maxStringLength),
|
|
182
|
+
state,
|
|
183
|
+
...(publicTime(child.startedAt) !== undefined ? { startedAt: publicTime(child.startedAt) } : {}),
|
|
184
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
185
|
+
...(terminalState(state) && publicTime(child.endedAt) !== undefined ? { endedAt: publicTime(child.endedAt) } : {}),
|
|
186
|
+
...(activityFor(child, ctx) ? { activity: activityFor(child, ctx) } : {}),
|
|
187
|
+
};
|
|
188
|
+
if (depth < ctx.caps.maxDepth) {
|
|
189
|
+
const nestedSteps = child.steps?.map((step, stepIndex) => buildStepNode(step, stepIndex, depth + 1, ctx)) ?? [];
|
|
190
|
+
const nestedChildren = child.children?.map((nested, childIndex) => buildNestedNode(nested, childIndex, depth + 1, ctx)) ?? [];
|
|
191
|
+
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
192
|
+
appendBoundedChildren(bounded, [...nestedSteps, ...nestedChildren], ctx);
|
|
193
|
+
if (bounded.length) node.children = bounded;
|
|
194
|
+
} else {
|
|
195
|
+
ctx.omitted.children += (child.steps?.length ?? 0) + (child.children?.length ?? 0);
|
|
196
|
+
}
|
|
197
|
+
return node;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildRunNode(job: AsyncJobState, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
|
|
201
|
+
const state = normalizeState(job.status);
|
|
202
|
+
const updatedAt = publicTime(job.updatedAt) ?? publicTime(job.startedAt);
|
|
203
|
+
const node: AsyncStatusSnapshotNodeV1 = {
|
|
204
|
+
id: publicText(job.asyncId, "async", ctx.caps.maxStringLength),
|
|
205
|
+
kind: kindForMode(job.mode),
|
|
206
|
+
label: labelForAgents(job.agents, job.mode ?? "subagent", ctx.caps.maxStringLength),
|
|
207
|
+
state,
|
|
208
|
+
...(publicTime(job.startedAt) !== undefined ? { startedAt: publicTime(job.startedAt) } : {}),
|
|
209
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
210
|
+
...(terminalState(state) && updatedAt !== undefined ? { endedAt: updatedAt } : {}),
|
|
211
|
+
...(activityFor(job, ctx) ? { activity: activityFor(job, ctx) } : {}),
|
|
212
|
+
};
|
|
213
|
+
if (ctx.caps.maxDepth > 0) {
|
|
214
|
+
const stepChildren = job.steps?.map((step, index) => buildStepNode(step, step.index ?? index, 1, ctx)) ?? [];
|
|
215
|
+
const nestedChildren = job.nestedChildren?.map((child, index) => buildNestedNode(child, index, 1, ctx)) ?? [];
|
|
216
|
+
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
217
|
+
appendBoundedChildren(bounded, [...stepChildren, ...nestedChildren], ctx);
|
|
218
|
+
if (bounded.length) node.children = bounded;
|
|
219
|
+
} else {
|
|
220
|
+
ctx.omitted.children += (job.steps?.length ?? 0) + (job.nestedChildren?.length ?? 0);
|
|
221
|
+
}
|
|
222
|
+
return node;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function snapshotBytes(snapshot: AsyncStatusSnapshotV1): number {
|
|
226
|
+
return Buffer.byteLength(JSON.stringify(snapshot), "utf8");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function enforceByteLimit(snapshot: AsyncStatusSnapshotV1): void {
|
|
230
|
+
while (snapshot.runs.length > 0 && snapshotBytes(snapshot) > snapshot.caps.maxSerializedBytes) {
|
|
231
|
+
snapshot.runs.pop();
|
|
232
|
+
snapshot.omitted.runs += 1;
|
|
233
|
+
snapshot.omitted.byteLimitExceeded = true;
|
|
234
|
+
}
|
|
235
|
+
if (snapshotBytes(snapshot) > snapshot.caps.maxSerializedBytes) snapshot.omitted.byteLimitExceeded = true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function buildAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
|
|
239
|
+
const caps = resolveCaps(options);
|
|
240
|
+
const ctx: BuildContext = { caps, omitted: { runs: 0, children: 0, byteLimitExceeded: false } };
|
|
241
|
+
const sorted = [...jobs].sort((left, right) => {
|
|
242
|
+
const leftUpdated = left.updatedAt ?? left.startedAt ?? 0;
|
|
243
|
+
const rightUpdated = right.updatedAt ?? right.startedAt ?? 0;
|
|
244
|
+
return rightUpdated - leftUpdated || left.asyncId.localeCompare(right.asyncId);
|
|
245
|
+
});
|
|
246
|
+
ctx.omitted.runs += Math.max(0, sorted.length - caps.maxRuns);
|
|
247
|
+
const snapshot: AsyncStatusSnapshotV1 = {
|
|
248
|
+
kind: ASYNC_STATUS_SNAPSHOT_KIND,
|
|
249
|
+
version: ASYNC_STATUS_SNAPSHOT_VERSION,
|
|
250
|
+
generatedAt: options.generatedAt ?? Date.now(),
|
|
251
|
+
caps,
|
|
252
|
+
omitted: ctx.omitted,
|
|
253
|
+
runs: sorted.slice(0, caps.maxRuns).map((job) => buildRunNode(job, ctx)),
|
|
254
|
+
};
|
|
255
|
+
enforceByteLimit(snapshot);
|
|
256
|
+
return snapshot;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined, sessionId: string | null | undefined): AsyncJobState[] {
|
|
260
|
+
if (!state || !sessionId || state.currentSessionId !== sessionId) return [];
|
|
261
|
+
const jobs = new Map<string, AsyncJobState>();
|
|
262
|
+
for (const job of state.asyncJobs.values()) {
|
|
263
|
+
if (job.sessionId === sessionId) jobs.set(job.asyncId, job);
|
|
264
|
+
}
|
|
265
|
+
for (const job of state.fleetJobs?.values() ?? []) {
|
|
266
|
+
if (job.sessionId === sessionId && !jobs.has(job.asyncId)) jobs.set(job.asyncId, job);
|
|
267
|
+
}
|
|
268
|
+
return [...jobs.values()];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
|
|
272
|
+
return buildAsyncStatusSnapshot(asyncStatusSnapshotJobsForState(state, sessionId), options);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function encodeAsyncStatusSnapshotWidget(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): string[] {
|
|
276
|
+
return [`${ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX}${JSON.stringify(buildAsyncStatusSnapshot(jobs, options))}`];
|
|
277
|
+
}
|