pi-subagents 0.66.0 → 0.68.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 +138 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +43 -15
- package/docs/configuration.md +67 -23
- package/docs/extension-api.md +38 -19
- package/docs/missions.md +10 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +22 -12
- package/docs/standalone-background.md +59 -0
- package/docs/tool-reference.md +33 -20
- package/docs/watchdog.md +39 -10
- package/docs/workflows.md +37 -13
- package/index.ts +5 -2
- package/inspector-runner.mjs +2 -2
- package/package.json +4 -2
- package/prompts/parallel-review.md +1 -1
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +32 -0
- package/skills/pi-subagents/SKILL.md +32 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +11 -9
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +190 -71
- package/src/agents/builtin-names.ts +1 -0
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +94 -59
- package/src/api/required-child-extensions.ts +6 -0
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +66 -4
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +62 -39
- package/src/extension/public-execution.ts +7 -5
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +81 -81
- package/src/extension/tool-description.ts +30 -82
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +44 -31
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +348 -176
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +17 -12
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +23 -10
- package/src/runs/background/binary-bootstrap.ts +38 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +105 -7
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +81 -33
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +12 -33
- package/src/runs/background/runner-child-launch.ts +6 -1
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +51 -18
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +441 -304
- package/src/runs/background/subagent-wait.ts +176 -28
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +5 -3
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +134 -248
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +374 -178
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +38 -11
- package/src/runs/shared/async-status-projection.ts +127 -33
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +28 -5
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +8 -1
- package/src/runs/shared/child-session.ts +127 -52
- package/src/runs/shared/child-tool-plan.ts +142 -11
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +45 -15
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +68 -8
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +74 -30
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -7
- package/src/slash/subagents-admin.ts +24 -13
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +168 -37
- package/src/watchdog/child-status.ts +28 -28
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +21 -1
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +10 -2
- package/src/watchdog/register-main.ts +39 -35
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +123 -74
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +100 -27
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +3 -0
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +5 -3
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +279 -10
- package/src/workflows/workflow-checklist.ts +2 -2
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/runner-server-preload.mjs +0 -13
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -52,11 +52,14 @@ const DEFAULT_LIVENESS_INTERVAL_MS = 5000;
|
|
|
52
52
|
const EVENT_REFRESH_DEBOUNCE_MS = 25;
|
|
53
53
|
const WATCH_ATTACHMENT_RETRY_MS = 100;
|
|
54
54
|
|
|
55
|
+
const isTerminalJobStatus = (status: AsyncJobState["status"]): boolean =>
|
|
56
|
+
status === "complete" || status === "failed" || status === "partial" || status === "paused" || status === "rejected" || status === "stopped";
|
|
57
|
+
|
|
55
58
|
function rememberFleetJob(state: SubagentState, job: AsyncJobState): void {
|
|
56
59
|
state.fleetJobs ??= new Map();
|
|
57
60
|
state.fleetJobs.set(job.asyncId, job);
|
|
58
61
|
const terminal = [...state.fleetJobs.values()]
|
|
59
|
-
.filter((candidate) => candidate.status
|
|
62
|
+
.filter((candidate) => isTerminalJobStatus(candidate.status))
|
|
60
63
|
.sort((left, right) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0));
|
|
61
64
|
for (const stale of terminal.slice(MAX_RECENT_FLEET_JOBS)) state.fleetJobs.delete(stale.asyncId);
|
|
62
65
|
}
|
|
@@ -100,7 +103,6 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
100
103
|
let nextWidgetAnimationAt = Date.now() + WIDGET_ANIMATION_INTERVAL_MS;
|
|
101
104
|
const watch = options.watch ?? fs.watch;
|
|
102
105
|
const useNativeWatcher = () => shouldUseNativeFsWatch("async-job-tracker", options.platform);
|
|
103
|
-
const terminalStatus = (status: string) => status === "complete" || status === "failed" || status === "paused" || status === "stopped";
|
|
104
106
|
const withLastUiContext = <T>(run: (ctx: ExtensionContext) => T): T | undefined => {
|
|
105
107
|
const cached = state.lastUiContext;
|
|
106
108
|
return withCachedUiContext(cached, () => {
|
|
@@ -445,10 +447,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
445
447
|
if (status) {
|
|
446
448
|
const previousStatus = job.status;
|
|
447
449
|
job.status = status.state;
|
|
448
|
-
if (!
|
|
450
|
+
if (!isTerminalJobStatus(job.status)) terminalPublications.delete(job.asyncId);
|
|
449
451
|
if (job.status === "running") runningJobIds.add(job.asyncId);
|
|
450
452
|
else runningJobIds.delete(job.asyncId);
|
|
451
|
-
if (job.status
|
|
453
|
+
if (!isTerminalJobStatus(job.status)) cancelCleanup(job.asyncId);
|
|
452
454
|
job.sessionId = status.sessionId ?? job.sessionId;
|
|
453
455
|
job.activityState = status.activityState;
|
|
454
456
|
job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
|
|
@@ -502,7 +504,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
502
504
|
job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
|
|
503
505
|
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
504
506
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
505
|
-
if (
|
|
507
|
+
if (isTerminalJobStatus(job.status)) {
|
|
506
508
|
let publication = terminalPublications.get(job.asyncId);
|
|
507
509
|
if (!publication && status.mode !== "workflow" && status.processTerminal?.state === "pending"
|
|
508
510
|
&& status.runId === job.asyncId && status.sessionId === state.currentSessionId
|
|
@@ -524,7 +526,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
524
526
|
} else cancelCleanup(job.asyncId);
|
|
525
527
|
}
|
|
526
528
|
// Scan on close too: publication may have raced the payload check.
|
|
527
|
-
if (!
|
|
529
|
+
if (!isTerminalJobStatus(previousStatus) || (wasPending && !publication?.pending)) options.onJobTerminal?.();
|
|
528
530
|
rememberFleetJob(state, job);
|
|
529
531
|
if (!publication?.pending && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
530
532
|
scheduleCleanup(job.asyncId);
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { DIRS, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor, type SubagentRunMode } from "../../shared/types.ts";
|
|
4
4
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
5
5
|
import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
|
|
6
|
+
import { snapshotRequiredChildExtensions } from "../../shared/required-child-extensions.ts";
|
|
6
7
|
import { normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
|
|
7
8
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
8
9
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
@@ -15,6 +16,7 @@ import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/paral
|
|
|
15
16
|
import { normalizeWorktreeBaseRef } from "../shared/worktree.ts";
|
|
16
17
|
import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
17
18
|
import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
|
|
19
|
+
import { validateIntercomBridgeConfig } from "../../intercom/intercom-bridge.ts";
|
|
18
20
|
import { validateModelResponseAliases } from "../../shared/model-response-aliases.ts";
|
|
19
21
|
|
|
20
22
|
export interface AsyncResumeParams {
|
|
@@ -318,12 +320,13 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
318
320
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
319
321
|
const parsed = value as Record<string, unknown>;
|
|
320
322
|
const allowedFields = new Set([
|
|
321
|
-
"modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "
|
|
323
|
+
"modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fast", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
322
324
|
"subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
|
|
323
325
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
324
326
|
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
325
327
|
"launchResolvedExtensions", "runFanoutBudget", "lane", "baseRef",
|
|
326
328
|
"extensionBindings",
|
|
329
|
+
"requiredExtensions",
|
|
327
330
|
]);
|
|
328
331
|
for (const field of Object.keys(parsed)) {
|
|
329
332
|
if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
|
|
@@ -342,6 +345,10 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
342
345
|
if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
|
|
343
346
|
if (parsed.thinkingCeiling !== undefined) parsed.thinkingCeiling = parseThinkingLevel(parsed.thinkingCeiling, `async recovery descriptor '${descriptorPath}' thinkingCeiling`);
|
|
344
347
|
if (parsed.extensionBindings !== undefined) parsed.extensionBindings = normalizeExtensionBindings(parsed.extensionBindings)!.value;
|
|
348
|
+
if (parsed.requiredExtensions !== undefined) {
|
|
349
|
+
try { parsed.requiredExtensions = snapshotRequiredChildExtensions(parsed.requiredExtensions, "requiredExtensions"); }
|
|
350
|
+
catch (error) { throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`); }
|
|
351
|
+
}
|
|
345
352
|
if (parsed.lane !== undefined) parsed.lane = normalizeWorkflowLaneMetadata(parsed.lane, `Invalid async recovery descriptor '${descriptorPath}': lane`);
|
|
346
353
|
if (parsed.agentContract !== undefined) {
|
|
347
354
|
if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
|
|
@@ -352,6 +359,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
352
359
|
if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
|
|
353
360
|
if (parsed.context !== undefined && parsed.context !== "fresh" && parsed.context !== "fork") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': context is invalid.`);
|
|
354
361
|
if (parsed.modelOverrideFromParent !== undefined && typeof parsed.modelOverrideFromParent !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOverrideFromParent must be a boolean.`);
|
|
362
|
+
if (parsed.fast !== undefined && typeof parsed.fast !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': fast must be a boolean.`);
|
|
355
363
|
if (parsed.modelOrigin !== undefined && parsed.modelOrigin !== "explicit" && parsed.modelOrigin !== "inherited" && parsed.modelOrigin !== "configured") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOrigin must be 'explicit', 'inherited', or 'configured'.`);
|
|
356
364
|
if (parsed.modelOrigin === undefined && parsed.model !== undefined) parsed.modelOrigin = parsed.modelOverrideFromParent ? "inherited" : "configured";
|
|
357
365
|
for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
|
|
@@ -361,7 +369,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
361
369
|
else if (typeof parsed.inheritGlobalContext !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': inheritGlobalContext must be a boolean.`);
|
|
362
370
|
if (parsed.allowNestedSubagents !== undefined && typeof parsed.allowNestedSubagents !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': allowNestedSubagents must be a boolean.`);
|
|
363
371
|
if (!Number.isInteger(parsed.maxSubagentDepth) || (parsed.maxSubagentDepth as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxSubagentDepth must be a non-negative integer.`);
|
|
364
|
-
for (const field of ["
|
|
372
|
+
for (const field of ["tools", "excludeTools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"] as const) {
|
|
365
373
|
const item = parsed[field];
|
|
366
374
|
if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
|
|
367
375
|
}
|
|
@@ -411,14 +419,8 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
411
419
|
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.`);
|
|
412
420
|
}
|
|
413
421
|
if (parsed.intercomBridge !== undefined) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
for (const field of Object.keys(bridge)) {
|
|
417
|
-
if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.${field} is not supported.`);
|
|
418
|
-
}
|
|
419
|
-
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.`);
|
|
420
|
-
if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.instructionFile must be a string.`);
|
|
421
|
-
if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.resultDelivery must be a boolean.`);
|
|
422
|
+
const bridge = validateIntercomBridgeConfig({ value: parsed.intercomBridge, label: "intercomBridge" });
|
|
423
|
+
if (!bridge.ok) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${bridge.error}`);
|
|
422
424
|
}
|
|
423
425
|
if (parsed.controlConfig !== undefined) {
|
|
424
426
|
if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
|
|
@@ -555,7 +557,11 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
555
557
|
const index = requestedIndex ?? 0;
|
|
556
558
|
if (!Number.isInteger(index)) throw new Error(`Async run '${runId}' index must be an integer.`);
|
|
557
559
|
if (index < 0 || index >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${index} is out of range.`);
|
|
558
|
-
const
|
|
560
|
+
const selectedStatusStep = statusSteps[index];
|
|
561
|
+
if (selectedStatusStep?.status === "stopped" || selectedStatusStep?.stopped === true) {
|
|
562
|
+
throw new Error(`Async run '${runId}' child ${index} was stopped and cannot be resumed. Start a new run instead.`);
|
|
563
|
+
}
|
|
564
|
+
const agent = selectedStatusStep?.agent ?? resultSteps[index]?.agent ?? result?.agent;
|
|
559
565
|
if (!agent) throw new Error(`Could not determine child agent for async run '${runId}'.`);
|
|
560
566
|
if (recoveryDescriptor && recoveryDescriptor.agent !== agent) throw new Error(`Async run '${runId}' has a recovery descriptor for '${recoveryDescriptor.agent}', not '${agent}'.`);
|
|
561
567
|
const sessionFile = statusSteps[index]?.sessionFile
|
|
@@ -600,7 +606,6 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
|
|
|
600
606
|
...agentConfig,
|
|
601
607
|
model: descriptor.model,
|
|
602
608
|
modelProvider: descriptor.modelProvider,
|
|
603
|
-
fallbackModels: descriptor.fallbackModels ? [...descriptor.fallbackModels] : undefined,
|
|
604
609
|
thinking: descriptor.thinking,
|
|
605
610
|
maxThinking: intersectThinkingCeilings(descriptor.thinkingCeiling, agentConfig.maxThinking),
|
|
606
611
|
tools: descriptor.tools ? [...descriptor.tools] : undefined,
|
|
@@ -26,6 +26,7 @@ import { workflowGraphStageNodes } from "../shared/workflow-graph.ts";
|
|
|
26
26
|
import { formatTimeoutRecoveryLines, projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
|
|
27
27
|
import { formatWorkflowChecklistText, projectWorkflowChecklist } from "../../workflows/workflow-checklist.ts";
|
|
28
28
|
import type { RawDrainStatusObserver } from "../shared/readonly-drain-observation.ts";
|
|
29
|
+
import { childWatchdogProgressForModel } from "../../watchdog/child-status.ts";
|
|
29
30
|
|
|
30
31
|
interface AsyncRunStepSummary {
|
|
31
32
|
index: number;
|
|
@@ -66,7 +67,7 @@ interface AsyncRunStepSummary {
|
|
|
66
67
|
model?: string;
|
|
67
68
|
contextLimit?: number;
|
|
68
69
|
thinking?: string;
|
|
69
|
-
|
|
70
|
+
requestedModel?: string;
|
|
70
71
|
sessionFile?: string;
|
|
71
72
|
transcriptPath?: string;
|
|
72
73
|
error?: string;
|
|
@@ -367,7 +368,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
367
368
|
...(step.contextLimit !== undefined ? { contextLimit: step.contextLimit } : {}),
|
|
368
369
|
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
369
370
|
...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
|
|
370
|
-
...(step.
|
|
371
|
+
...(step.requestedModel ? { requestedModel: step.requestedModel } : {}),
|
|
371
372
|
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
372
373
|
...(step.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
|
|
373
374
|
...(step.error ? { error: step.error } : {}),
|
|
@@ -387,7 +388,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
387
388
|
...(step.execution ? { execution: step.execution } : {}),
|
|
388
389
|
...(step.review ? { review: step.review } : {}),
|
|
389
390
|
...(step.effects ? { effects: step.effects } : {}),
|
|
390
|
-
...(step.watchdog ? { watchdog: step.watchdog } : {}),
|
|
391
|
+
...(step.watchdog ? { watchdog: childWatchdogProgressForModel(step.watchdog) } : {}),
|
|
391
392
|
...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path.join(asyncDir, "status.json")} step ${index}`) } : {}),
|
|
392
393
|
...(timeoutRecovery ? { timeoutRecovery } : {}),
|
|
393
394
|
...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
|
|
@@ -545,6 +546,14 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
545
546
|
const asyncDir = path.join(asyncDirRoot, entry);
|
|
546
547
|
let status: (AsyncStatus & { cwd?: string }) | null;
|
|
547
548
|
try {
|
|
549
|
+
// Reconciliation can rewrite state; session-scoped discovery does not own foreign runs.
|
|
550
|
+
if (options.sessionId !== undefined) {
|
|
551
|
+
const stored = readStatus(asyncDir);
|
|
552
|
+
if (stored && stored.sessionId !== options.sessionId) {
|
|
553
|
+
observeStatus?.(stored);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
548
557
|
const reconciliation = options.reconcile === false
|
|
549
558
|
? undefined
|
|
550
559
|
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now }, observeStatus);
|
|
@@ -563,7 +572,9 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
563
572
|
}
|
|
564
573
|
if (activeEntries.has(entry) && !isActiveAsyncState(status.state)) {
|
|
565
574
|
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
|
|
566
|
-
if (processTerminal?.state === "observed" || (activeRunMarkerAgeMs(asyncDir, options.now?.()) ?? 0) > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS)
|
|
575
|
+
if (processTerminal?.state === "observed" || (activeRunMarkerAgeMs(asyncDir, options.now?.()) ?? 0) > DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS) {
|
|
576
|
+
updateActiveRunIndex(asyncDir, status.state, status.toolCallId, { terminalIndexBeforeRelease: true });
|
|
577
|
+
}
|
|
567
578
|
}
|
|
568
579
|
if (status.displayDismissedAt !== undefined) continue;
|
|
569
580
|
// Filter before the nested-route lookup: the lookup builds an index over
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { snapshotBackgroundWork } from "../../api/background-work.ts";
|
|
3
|
-
import {
|
|
3
|
+
import type { Details, SubagentState } from "../../shared/types.ts";
|
|
4
4
|
import { listAsyncRuns } from "./async-status.ts";
|
|
5
5
|
import type { ReadonlyDrainObservation } from "../shared/readonly-drain-observation.ts";
|
|
6
|
-
import { waitForSubagents, type SubagentWaitDeps, type SubagentWaitParams, type WaitEventBus } from "./subagent-wait.ts";
|
|
6
|
+
import { waitForSubagents, waitRunScopes, type SubagentWaitDeps, type SubagentWaitParams, type WaitEventBus } from "./subagent-wait.ts";
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_AUTO_DRAIN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
9
9
|
|
|
10
|
-
export interface AutoDrainDeps {
|
|
10
|
+
export interface AutoDrainDeps extends Pick<SubagentWaitDeps, "asyncDirRoot" | "resultsDir" | "nestedRootRunId"> {
|
|
11
11
|
state: SubagentState;
|
|
12
12
|
events?: WaitEventBus;
|
|
13
13
|
timeoutMs?: number;
|
|
@@ -18,36 +18,43 @@ export interface AutoDrainDeps {
|
|
|
18
18
|
deps: SubagentWaitDeps,
|
|
19
19
|
) => Promise<AgentToolResult<Details>>;
|
|
20
20
|
hasWork?: (sessionId: string, nowMs: number) => boolean;
|
|
21
|
+
hasPendingSupervisorRequest?: () => boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function resultText(value: AgentToolResult<Details>): string {
|
|
24
25
|
return value.content.map((part) => part.type === "text" ? part.text : "").join(" ").trim();
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function hasOutstandingWork(sessionId: string, nowMs: number, observation?: ReadonlyDrainObservation): boolean {
|
|
28
|
-
const asyncRuns =
|
|
28
|
+
function hasOutstandingWork(deps: AutoDrainDeps, sessionId: string, nowMs: number, observation?: ReadonlyDrainObservation): boolean {
|
|
29
|
+
const asyncRuns = waitRunScopes(deps).flatMap(({ asyncDirRoot, resultsDir }) => listAsyncRuns(asyncDirRoot, {
|
|
29
30
|
states: ["queued", "running"],
|
|
30
31
|
sessionId,
|
|
31
|
-
resultsDir
|
|
32
|
+
resultsDir,
|
|
32
33
|
now: () => nowMs,
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
includeNested: false,
|
|
35
|
+
}, observation?.status));
|
|
36
|
+
const detachedForeground = [...(deps.state.foregroundRuns?.values() ?? [])].some((run) =>
|
|
37
|
+
run.sessionId === sessionId && run.children.some((child) => child.status === "detached")
|
|
38
|
+
);
|
|
39
|
+
return asyncRuns.length > 0 || snapshotBackgroundWork(sessionId, nowMs).items.length > 0 || detachedForeground;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
/** Drain all work owned by the current headless session, including work added while draining. */
|
|
38
43
|
export async function drainOutstandingWork(deps: AutoDrainDeps, observation?: ReadonlyDrainObservation): Promise<void> {
|
|
39
44
|
const sessionId = deps.state.currentSessionId;
|
|
40
|
-
observation?.begin(sessionId, !deps.hasWork && !deps.wait && !deps.now);
|
|
45
|
+
observation?.begin(sessionId, !deps.hasWork && !deps.wait && !deps.now && !deps.hasPendingSupervisorRequest);
|
|
41
46
|
try {
|
|
42
47
|
if (!sessionId) throw new Error("Cannot auto-drain background work without an active session identity.");
|
|
43
48
|
const now = deps.now ?? Date.now;
|
|
44
49
|
const timeoutMs = deps.timeoutMs ?? DEFAULT_AUTO_DRAIN_TIMEOUT_MS;
|
|
45
50
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Auto-drain timeoutMs must be a positive finite number.");
|
|
46
51
|
const deadlineAt = now() + timeoutMs;
|
|
47
|
-
const hasWork = deps.hasWork ?? (
|
|
52
|
+
const hasWork = deps.hasWork ?? ((id: string, time: number) => hasOutstandingWork(deps, id, time, observation));
|
|
48
53
|
const wait = deps.wait ?? waitForSubagents;
|
|
49
54
|
|
|
50
55
|
while (true) {
|
|
56
|
+
if (deps.state.currentSessionId !== sessionId) throw new Error("Auto-drain stopped because the active session changed.");
|
|
57
|
+
if (deps.hasPendingSupervisorRequest?.()) break;
|
|
51
58
|
const work = hasWork(sessionId, now());
|
|
52
59
|
observation?.predicate(work);
|
|
53
60
|
if (!work) break;
|
|
@@ -60,16 +67,22 @@ export async function drainOutstandingWork(deps: AutoDrainDeps, observation?: Re
|
|
|
60
67
|
undefined,
|
|
61
68
|
{
|
|
62
69
|
state: deps.state,
|
|
70
|
+
asyncDirRoot: deps.asyncDirRoot,
|
|
71
|
+
resultsDir: deps.resultsDir,
|
|
72
|
+
nestedRootRunId: deps.nestedRootRunId,
|
|
63
73
|
events: deps.events,
|
|
64
74
|
now,
|
|
65
75
|
stopOnAttention: false,
|
|
66
76
|
failOnFailedRuns: true,
|
|
67
77
|
failOnAttention: true,
|
|
78
|
+
hasPendingSupervisorRequest: deps.hasPendingSupervisorRequest,
|
|
68
79
|
},
|
|
69
80
|
);
|
|
70
81
|
if (waitResult.isError) {
|
|
71
82
|
throw new Error(`Auto-drain failed for session '${sessionId}': ${resultText(waitResult) || "bg_wait returned an error without details"}.`);
|
|
72
83
|
}
|
|
84
|
+
if (waitResult.details.wait?.reason === "supervisor_request") break;
|
|
85
|
+
if (deps.hasPendingSupervisorRequest?.()) break;
|
|
73
86
|
}
|
|
74
87
|
observation?.complete();
|
|
75
88
|
} catch (error) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as sdk from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { installRunnerHttpDispatcher } from "./runner-http-dispatcher.ts";
|
|
5
|
+
import { runConfiguredSubagent, type SubagentRunConfig } from "./subagent-runner.ts";
|
|
6
|
+
import { getAgentDir } from "../../shared/utils.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pi's extension loader supplies the embedded SDK. Bare Bun/Node cannot replace
|
|
10
|
+
* it: an apparent import success can be an auto-installed, different SDK.
|
|
11
|
+
* Await the configured runner inside extension initialization so the outer Pi
|
|
12
|
+
* session/RPC loop never starts. Exit only after shared disposal/lease release.
|
|
13
|
+
* Derived from xz-dev's PR #2049, commit 910807bfefcf9ee41d73fa25ec86dcd75ab8f4b2.
|
|
14
|
+
*/
|
|
15
|
+
export default async function runBinaryBootstrap(): Promise<never> {
|
|
16
|
+
const configPath = process.env.PI_SUBAGENT_RUNNER_CONFIG;
|
|
17
|
+
delete process.env.PI_SUBAGENT_RUNNER_CONFIG;
|
|
18
|
+
try {
|
|
19
|
+
if (!configPath || !path.isAbsolute(configPath)) throw new Error("Missing absolute PI_SUBAGENT_RUNNER_CONFIG path");
|
|
20
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as SubagentRunConfig;
|
|
21
|
+
if (!config || typeof config.id !== "string" || typeof config.asyncDir !== "string" || !Array.isArray(config.steps)) {
|
|
22
|
+
throw new Error("Invalid binary runner configuration");
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
fs.unlinkSync(configPath);
|
|
26
|
+
} catch {
|
|
27
|
+
// Temp-config cleanup is best effort, as in the Node entrypoint.
|
|
28
|
+
}
|
|
29
|
+
// Pi applies httpIdleTimeoutMs to its dispatcher only after extension
|
|
30
|
+
// factories return; this factory never does, so install the runner's own.
|
|
31
|
+
installRunnerHttpDispatcher({ agentDir: getAgentDir(), cwd: process.cwd() });
|
|
32
|
+
await runConfiguredSubagent(config, { loadPiCodingAgent: async () => sdk });
|
|
33
|
+
process.exit(0);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("Subagent binary runner error:", error);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -177,7 +177,7 @@ function statusStepForTask(task: RunnerSubagentStep): StatusStep {
|
|
|
177
177
|
model: task.model,
|
|
178
178
|
...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
|
|
179
179
|
thinking: task.thinking,
|
|
180
|
-
|
|
180
|
+
requestedModel: task.requestedModel,
|
|
181
181
|
recentTools: [],
|
|
182
182
|
recentOutput: [],
|
|
183
183
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { resultFilePath, resultPayloadPathForSessionRun } from "./result-files.ts";
|
|
4
|
-
import type { AcceptanceLedger, ArtifactPaths, AsyncStatus, CostSummary, EffectsProjection, ExecutionProjection,
|
|
4
|
+
import type { AcceptanceLedger, ArtifactPaths, AsyncStatus, CostSummary, EffectsProjection, ExecutionProjection, Usage } from "../../shared/types.ts";
|
|
5
5
|
import { readStatus } from "../../shared/utils.ts";
|
|
6
6
|
|
|
7
7
|
export interface ImportedAsyncRoot {
|
|
@@ -13,6 +13,7 @@ export interface ImportedAsyncRoot {
|
|
|
13
13
|
|
|
14
14
|
export interface ImportedAsyncRootResult {
|
|
15
15
|
agent: string;
|
|
16
|
+
importedPublication?: { sessionId?: string; toolCallId?: string };
|
|
16
17
|
/** Human-readable display name for the child session, when derived at launch. */
|
|
17
18
|
sessionName?: string;
|
|
18
19
|
output: string;
|
|
@@ -22,8 +23,7 @@ export interface ImportedAsyncRootResult {
|
|
|
22
23
|
sessionFile?: string;
|
|
23
24
|
intercomTarget?: string;
|
|
24
25
|
model?: string;
|
|
25
|
-
|
|
26
|
-
modelAttempts?: ModelAttempt[];
|
|
26
|
+
requestedModel?: string;
|
|
27
27
|
contextOverflow?: boolean;
|
|
28
28
|
totalCost?: CostSummary;
|
|
29
29
|
usage?: Usage;
|
|
@@ -43,6 +43,8 @@ export interface ImportedAsyncRootResult {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
interface AsyncResultFile {
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
toolCallId?: string;
|
|
46
48
|
state?: string;
|
|
47
49
|
success?: boolean;
|
|
48
50
|
summary?: string;
|
|
@@ -62,8 +64,7 @@ interface AsyncResultFile {
|
|
|
62
64
|
sessionFile?: string;
|
|
63
65
|
intercomTarget?: string;
|
|
64
66
|
model?: string;
|
|
65
|
-
|
|
66
|
-
modelAttempts?: ModelAttempt[];
|
|
67
|
+
requestedModel?: string;
|
|
67
68
|
contextOverflow?: boolean;
|
|
68
69
|
totalCost?: CostSummary;
|
|
69
70
|
usage?: Usage;
|
|
@@ -82,23 +83,6 @@ interface AsyncResultFile {
|
|
|
82
83
|
const TERMINAL_STATES = new Set(["complete", "failed", "partial", "paused", "stopped"]);
|
|
83
84
|
const TERMINAL_STEP_STATUSES = new Set(["complete", "completed", "failed", "partial", "paused", "stopped"]);
|
|
84
85
|
|
|
85
|
-
function usageFromAttempts(attempts: ModelAttempt[] | undefined): Usage | undefined {
|
|
86
|
-
if (!attempts?.length) return undefined;
|
|
87
|
-
const usage: Usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
88
|
-
for (const attempt of attempts) {
|
|
89
|
-
if (!attempt.usage) continue;
|
|
90
|
-
usage.input += attempt.usage.input;
|
|
91
|
-
usage.output += attempt.usage.output;
|
|
92
|
-
usage.cacheRead += attempt.usage.cacheRead;
|
|
93
|
-
usage.cacheWrite += attempt.usage.cacheWrite;
|
|
94
|
-
usage.cost += attempt.usage.cost;
|
|
95
|
-
usage.turns += attempt.usage.turns;
|
|
96
|
-
}
|
|
97
|
-
return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0
|
|
98
|
-
? usage
|
|
99
|
-
: undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
86
|
function readResultFile(resultPath: string): AsyncResultFile | undefined {
|
|
103
87
|
try {
|
|
104
88
|
return JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultFile;
|
|
@@ -153,7 +137,6 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
|
|
|
153
137
|
const execution = step?.execution ?? (partialStatus
|
|
154
138
|
? { status: "partial" as const, success: false, exitCode: 1, error: message }
|
|
155
139
|
: undefined);
|
|
156
|
-
const usage = usageFromAttempts(step?.modelAttempts);
|
|
157
140
|
return {
|
|
158
141
|
agent,
|
|
159
142
|
output: message,
|
|
@@ -165,11 +148,9 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
|
|
|
165
148
|
...(step?.sessionName ? { sessionName: step.sessionName } : {}),
|
|
166
149
|
...(step?.sessionFile ?? status.sessionFile ? { sessionFile: step?.sessionFile ?? status.sessionFile } : {}),
|
|
167
150
|
...(step?.model ? { model: step.model } : {}),
|
|
168
|
-
...(step?.
|
|
169
|
-
...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
|
|
151
|
+
...(step?.requestedModel ? { requestedModel: step.requestedModel } : {}),
|
|
170
152
|
...(step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
171
153
|
...(step?.totalCost ? { totalCost: step.totalCost } : {}),
|
|
172
|
-
...(usage ? { usage } : {}),
|
|
173
154
|
...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
|
|
174
155
|
...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
|
|
175
156
|
...(step?.structuredOutputSchemaPath ? { structuredOutputSchemaPath: step.structuredOutputSchemaPath } : {}),
|
|
@@ -182,7 +163,6 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
|
|
|
182
163
|
|
|
183
164
|
function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null, message: string): ImportedAsyncRootResult {
|
|
184
165
|
const step = selectedStatusStep(status, root.index);
|
|
185
|
-
const usage = usageFromAttempts(step?.modelAttempts);
|
|
186
166
|
return {
|
|
187
167
|
agent: step?.agent ?? status?.steps?.[root.index]?.agent ?? "subagent",
|
|
188
168
|
output: message,
|
|
@@ -193,11 +173,9 @@ function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null,
|
|
|
193
173
|
...(step?.sessionName ? { sessionName: step.sessionName } : {}),
|
|
194
174
|
...(step?.sessionFile ?? status?.sessionFile ? { sessionFile: step?.sessionFile ?? status?.sessionFile } : {}),
|
|
195
175
|
...(step?.model ? { model: step.model } : {}),
|
|
196
|
-
...(step?.
|
|
197
|
-
...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
|
|
176
|
+
...(step?.requestedModel ? { requestedModel: step.requestedModel } : {}),
|
|
198
177
|
...(step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
199
178
|
...(step?.totalCost ? { totalCost: step.totalCost } : {}),
|
|
200
|
-
...(usage ? { usage } : {}),
|
|
201
179
|
...(step?.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
|
|
202
180
|
};
|
|
203
181
|
}
|
|
@@ -213,9 +191,13 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
|
|
|
213
191
|
const success = state === "complete" && !timedOut && !stopped;
|
|
214
192
|
const error = child?.error ?? (success ? undefined : stopped ? "Subagent stopped by user." : result.error ?? result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
|
|
215
193
|
const execution = state ? { status: state === "complete" ? "completed" as const : state, success, exitCode: success ? 0 : 1, ...(error ? { error } : {}) } : undefined;
|
|
216
|
-
const usage = child?.usage
|
|
194
|
+
const usage = child?.usage;
|
|
217
195
|
return {
|
|
218
196
|
agent,
|
|
197
|
+
importedPublication: {
|
|
198
|
+
...(typeof result.sessionId === "string" ? { sessionId: result.sessionId } : {}),
|
|
199
|
+
...(typeof result.toolCallId === "string" ? { toolCallId: result.toolCallId } : {}),
|
|
200
|
+
},
|
|
219
201
|
output: success ? output : (output || error || ""),
|
|
220
202
|
success,
|
|
221
203
|
exitCode: success ? 0 : 1,
|
|
@@ -226,8 +208,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
|
|
|
226
208
|
...(child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile ? { sessionFile: child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile } : {}),
|
|
227
209
|
...(child?.intercomTarget ? { intercomTarget: child.intercomTarget } : {}),
|
|
228
210
|
...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
|
|
229
|
-
...(child?.
|
|
230
|
-
...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
|
|
211
|
+
...(child?.requestedModel ?? step?.requestedModel ? { requestedModel: child?.requestedModel ?? step?.requestedModel } : {}),
|
|
231
212
|
...(child?.contextOverflow || step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
232
213
|
...(child?.totalCost ?? step?.totalCost ? { totalCost: child?.totalCost ?? step?.totalCost } : {}),
|
|
233
214
|
...(usage ? { usage } : {}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
|
-
import { safeTerminalText } from "../../shared/display-text.ts";
|
|
4
|
+
import { safeTerminalText, truncateDisplayText } from "../../shared/display-text.ts";
|
|
5
5
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
6
6
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
7
7
|
import {
|
|
@@ -23,6 +23,10 @@ import { isTrustedRecordedSessionFile } from "../../shared/session-file-trust.ts
|
|
|
23
23
|
const DEFAULT_TRANSCRIPT_LINES = 80;
|
|
24
24
|
const MAX_TRANSCRIPT_LINES = 500;
|
|
25
25
|
const TRANSCRIPT_TAIL_BYTES = 256 * 1024;
|
|
26
|
+
const MAX_TRANSCRIPT_LINE_CHARS = 2000;
|
|
27
|
+
const MAX_TRANSCRIPT_BODY_BYTES = 32 * 1024;
|
|
28
|
+
const TRANSCRIPT_LINE_OMISSION = "… [content omitted]";
|
|
29
|
+
const TRANSCRIPT_BODY_OMISSION = " … [earlier lines omitted]";
|
|
26
30
|
|
|
27
31
|
type ForegroundControl = SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
|
|
28
32
|
type ForegroundRun = NonNullable<SubagentState["foregroundRuns"]> extends Map<string, infer T> ? T : never;
|
|
@@ -494,7 +498,31 @@ function appendTranscriptBody(lines: string[], sourceLabel: string, sourceLines:
|
|
|
494
498
|
lines.push(" (no transcript lines available yet)");
|
|
495
499
|
return;
|
|
496
500
|
}
|
|
497
|
-
|
|
501
|
+
// Bound the rendered body, not the raw input: terminal escaping can expand it.
|
|
502
|
+
// Keep line prefixes (roles/tool names), then prefer whole recent lines. The
|
|
503
|
+
// byte budget includes indentation, separators and omission markers, but not
|
|
504
|
+
// the source label, run metadata, warnings or artifact paths above the body.
|
|
505
|
+
const body: string[] = [];
|
|
506
|
+
let bytes = 0;
|
|
507
|
+
for (let index = sourceLines.length - 1; index >= 0; index--) {
|
|
508
|
+
// Preserve assembled-line binary detection (including indentation). A
|
|
509
|
+
// binary placeholder replaces the whole line and must not be reindented.
|
|
510
|
+
const safe = safeTerminalText(` ${sourceLines[index]!}`);
|
|
511
|
+
const lineLimit = MAX_TRANSCRIPT_LINE_CHARS + 2;
|
|
512
|
+
const line = safe.length <= lineLimit
|
|
513
|
+
? safe
|
|
514
|
+
: truncateDisplayText(safe, lineLimit - TRANSCRIPT_LINE_OMISSION.length) + TRANSCRIPT_LINE_OMISSION;
|
|
515
|
+
const size = Buffer.byteLength(line) + 1;
|
|
516
|
+
if (bytes + size > MAX_TRANSCRIPT_BODY_BYTES) {
|
|
517
|
+
const markerBytes = Buffer.byteLength(TRANSCRIPT_BODY_OMISSION) + 1;
|
|
518
|
+
while (bytes + markerBytes > MAX_TRANSCRIPT_BODY_BYTES) bytes -= Buffer.byteLength(body.pop()!) + 1;
|
|
519
|
+
body.push(TRANSCRIPT_BODY_OMISSION);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
body.push(line);
|
|
523
|
+
bytes += size;
|
|
524
|
+
}
|
|
525
|
+
lines.push(...body.reverse());
|
|
498
526
|
}
|
|
499
527
|
|
|
500
528
|
export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string, options: TranscriptOptions = {}): string {
|