pi-subagents 0.65.0 → 0.66.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 +87 -0
- package/README.md +1 -1
- package/agents/researcher.md +23 -13
- package/docs/agents.md +17 -3
- package/docs/configuration.md +34 -0
- package/docs/extension-api.md +94 -0
- package/docs/models.md +58 -1
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +11 -5
- package/docs/workflows.md +22 -7
- package/package.json +4 -1
- package/runner-server-preload.mjs +13 -0
- package/skills/pi-subagents/SKILL.md +2 -1
- package/skills/pi-subagents/references/execution-controls.md +19 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- package/src/agents/advertised-agent-prompt.ts +63 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +8 -0
- package/src/api/preflight.ts +5 -1
- package/src/api/shared-types.ts +1 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/config.ts +4 -2
- package/src/extension/index.ts +71 -4
- package/src/extension/public-execution.ts +0 -1
- package/src/extension/rpc.ts +4 -21
- package/src/extension/schemas.ts +9 -7
- package/src/extension/tool-description.ts +10 -5
- package/src/integrations/pi-web-session-liveness.ts +73 -0
- package/src/intercom/native-supervisor-channel.ts +102 -88
- package/src/intercom/supervisor-ui.ts +3 -2
- package/src/missions/workflow-state.ts +37 -16
- package/src/runs/background/active-async-capacity.ts +18 -18
- package/src/runs/background/async-execution.ts +8 -1
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-retention.ts +9 -0
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +40 -29
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/notify.ts +88 -12
- package/src/runs/background/owned-process-tree.ts +6 -6
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/src/runs/background/run-child-session.ts +62 -33
- package/src/runs/background/run-status.ts +75 -5
- package/src/runs/background/runner-aliases.ts +46 -8
- package/src/runs/background/runner-child-launch.ts +86 -0
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/subagent-runner.ts +430 -208
- package/src/runs/background/subagent-wait.ts +3 -0
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/async-steering-action.ts +19 -0
- package/src/runs/foreground/execution.ts +116 -26
- package/src/runs/foreground/foreground-history.ts +3 -1
- package/src/runs/foreground/prompt-audit.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +531 -227
- package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
- package/src/runs/shared/acceptance.ts +16 -3
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +47 -47
- package/src/runs/shared/child-hooks.ts +151 -2
- package/src/runs/shared/child-launch.ts +18 -13
- package/src/runs/shared/child-session.ts +55 -24
- package/src/runs/shared/child-tool-plan.ts +2 -2
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +1 -0
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +30 -20
- package/src/runs/shared/model-exclusions.ts +2 -1
- package/src/runs/shared/model-fallback.ts +41 -8
- package/src/runs/shared/nested-events.ts +8 -8
- package/src/runs/shared/orca-progress-tabs.ts +6 -0
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +3 -2
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
- package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +403 -210
- package/src/shared/model-response-aliases.ts +13 -0
- package/src/shared/types.ts +89 -60
- package/src/shared/utils.ts +10 -2
- package/src/shared/watch-strategy.ts +2 -0
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/tui/fleet-status.ts +1 -1
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +44 -15
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +70 -16
- package/src/workflows/workflow-checklist.ts +15 -18
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
|
@@ -94,7 +94,7 @@ function workflowResultChildren(status: AsyncStatus, childRunId: string, result:
|
|
|
94
94
|
}));
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string, resolution: WorkflowTerminalResolution | undefined): WorkflowReceipt | undefined {
|
|
97
|
+
function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string, resolution: WorkflowTerminalResolution | undefined): { receipt: WorkflowReceipt; path: string } | undefined {
|
|
98
98
|
const receiptRoot = path.dirname(asyncDir);
|
|
99
99
|
const receiptPath = workflowReceiptPath(receiptRoot, status.runId);
|
|
100
100
|
if (!fs.existsSync(receiptPath)) return undefined;
|
|
@@ -155,8 +155,7 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
|
|
|
155
155
|
delete next.workflowResolution;
|
|
156
156
|
delete next.recovery;
|
|
157
157
|
}
|
|
158
|
-
writeWorkflowReceipt(asyncDir, next);
|
|
159
|
-
return next;
|
|
158
|
+
return { receipt: next, path: writeWorkflowReceipt(asyncDir, next) };
|
|
160
159
|
}
|
|
161
160
|
|
|
162
161
|
function appendDetachedWorkflowEvent(asyncDir: string, event: Record<string, unknown>): void {
|
|
@@ -209,10 +208,13 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
|
|
|
209
208
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
210
209
|
}
|
|
211
210
|
let receipt: WorkflowReceipt | undefined;
|
|
211
|
+
let receiptPath: string | undefined;
|
|
212
212
|
let receiptError: string | undefined;
|
|
213
213
|
const resolution = classifyWorkflowSettlement(next, input.result.interrupted);
|
|
214
214
|
try {
|
|
215
|
-
|
|
215
|
+
const publication = reconcileWorkflowReceipt(next, input.childRunId, input.result, asyncDir, resolution);
|
|
216
|
+
receipt = publication?.receipt;
|
|
217
|
+
receiptPath = publication?.path;
|
|
216
218
|
} catch (error) {
|
|
217
219
|
receiptError = `Failed to reconcile async workflow receipt: ${error instanceof Error ? error.message : String(error)}`;
|
|
218
220
|
}
|
|
@@ -243,7 +245,7 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
|
|
|
243
245
|
completionOwnerId: next.completionOwnerId,
|
|
244
246
|
},
|
|
245
247
|
receipt,
|
|
246
|
-
receiptPath
|
|
248
|
+
receiptPath,
|
|
247
249
|
receiptPersistenceError: receiptError,
|
|
248
250
|
resolution,
|
|
249
251
|
terminalOutcome: receipt?.terminalOutcome,
|
|
@@ -283,6 +285,7 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
|
|
|
283
285
|
agent: "workflow",
|
|
284
286
|
success: plan.status.state === "complete",
|
|
285
287
|
state: plan.status.state,
|
|
288
|
+
...(plan.publicResult.workflowReceipt ? { workflowReceipt: plan.publicResult.workflowReceipt } : {}),
|
|
286
289
|
...(resolution ? { workflowResolution: resolution } : {}),
|
|
287
290
|
...(plan.receipt?.terminalOutcome ? { terminalOutcome: plan.receipt.terminalOutcome } : {}),
|
|
288
291
|
recovery: plan.recovery,
|
|
@@ -3,8 +3,9 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
4
|
import type { Details, ForegroundRunControl, SubagentState } from "../../shared/types.ts";
|
|
5
5
|
import { readStatus } from "../../shared/utils.ts";
|
|
6
|
-
import { steeringReceipt } from "../background/steering.ts";
|
|
7
|
-
import type
|
|
6
|
+
import { steeringReceipt, waitForSteeringAction } from "../background/steering.ts";
|
|
7
|
+
import { requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
|
|
8
|
+
import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
|
|
8
9
|
|
|
9
10
|
export interface WorkflowForegroundSteeringTarget {
|
|
10
11
|
control: ForegroundRunControl;
|
|
@@ -66,6 +67,58 @@ function managementError(message: string): AgentToolResult<Details> {
|
|
|
66
67
|
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
/** Local controllers remain authoritative; recorded foreign ownership permits enqueue, not takeover or proof of liveness. */
|
|
71
|
+
export async function steerWorkflowRun(input: {
|
|
72
|
+
state: SubagentState;
|
|
73
|
+
runId: string;
|
|
74
|
+
asyncDir: string;
|
|
75
|
+
message: string;
|
|
76
|
+
mode?: SteerDeliveryMode;
|
|
77
|
+
index?: number;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
ackTimeoutMs?: number;
|
|
80
|
+
}): Promise<AgentToolResult<Details>> {
|
|
81
|
+
const { state, runId, asyncDir } = input;
|
|
82
|
+
const status = readStatus(asyncDir);
|
|
83
|
+
if (!status || status.mode !== "workflow" || status.runId !== runId || path.basename(asyncDir) !== runId) {
|
|
84
|
+
return managementError(`Workflow '${runId}' does not match its resolved run directory.`);
|
|
85
|
+
}
|
|
86
|
+
if (state.workflowControllers?.has(runId)) {
|
|
87
|
+
const route = resolveWorkflowForegroundSteeringTarget({ state, workflowRunId: runId, asyncDirRoot: path.dirname(asyncDir) });
|
|
88
|
+
if (!route.ok) return managementError(route.message);
|
|
89
|
+
return steerWorkflowForegroundTarget({ target: route.target, message: input.message, mode: input.mode, index: input.index });
|
|
90
|
+
}
|
|
91
|
+
if (!state.currentSessionId) return managementError("Workflow steering requires an active parent session.");
|
|
92
|
+
if (status.sessionId !== state.currentSessionId) return managementError(`Workflow '${runId}' was not found in the active session.`);
|
|
93
|
+
if (status.state !== "running" && status.state !== "queued") return managementError(`Workflow '${runId}' is not running or queued and cannot be steered.`);
|
|
94
|
+
if (typeof status.completionOwnerId !== "string" || !status.completionOwnerId.trim()
|
|
95
|
+
|| status.completionOwnerId === (state.completionOwnerId ?? currentCompletionOwnerId())
|
|
96
|
+
|| [...state.foregroundControls.values()].some((control) => control.parentWorkflowRunId === runId)) {
|
|
97
|
+
return managementError(`Workflow '${runId}' has no live foreground child and is not a recorded foreign workflow.`);
|
|
98
|
+
}
|
|
99
|
+
if (input.index !== undefined) {
|
|
100
|
+
const step = status.steps?.[input.index];
|
|
101
|
+
if (!Number.isInteger(input.index) || input.index < 0 || !step) return managementError(`Workflow '${runId}' index ${input.index} is out of range or invalid.`);
|
|
102
|
+
if (step.status !== "running" && step.status !== "pending") return managementError(`Workflow '${runId}' child ${input.index} is ${step.status} and cannot be steered.`);
|
|
103
|
+
if (typeof step.workflowKey !== "string" || !step.workflowKey.trim()) return managementError(`Workflow '${runId}' child ${input.index} has no projected workflow key.`);
|
|
104
|
+
}
|
|
105
|
+
const requestId = randomUUID();
|
|
106
|
+
try {
|
|
107
|
+
requestAsyncSteer(asyncDir, { id: requestId, message: input.message, mode: input.mode, ...(input.index !== undefined ? { targetIndex: input.index } : {}), source: "steer-action" });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return managementError(`Failed to queue steering for workflow ${runId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
}
|
|
111
|
+
// Only the owner writes the ledger. No acknowledgment leaves an honest, unaddressed queued receipt.
|
|
112
|
+
const steering = await waitForSteeringAction({ asyncDir, sourceRunId: runId, requestId, timeoutMs: input.ackTimeoutMs ?? 3_000, signal: input.signal })
|
|
113
|
+
?? { requestId, state: "pending" as const, deliveryStatus: "queued" as const, sourceRunId: runId, targets: input.index === undefined ? [] : [{ index: input.index, state: "pending" as const }] };
|
|
114
|
+
const failed = steering.state === "failed" || steering.state === "partial";
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: steeringReceipt(input.message, `Steering ${failed ? steering.state : steering.deliveryStatus === "delivered" ? "delivered" : "queued"} for workflow ${runId} (request ${requestId}).`) }],
|
|
117
|
+
...(failed ? { isError: true } : {}),
|
|
118
|
+
details: { mode: "management", results: [], steering },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
69
122
|
/**
|
|
70
123
|
* Steer a live workflow-owned foreground child through its in-process session.
|
|
71
124
|
* `steer` (and `auto`) interrupt the child at its next safe point; `follow_up`
|
|
@@ -91,6 +144,7 @@ export async function steerWorkflowForegroundTarget(input: {
|
|
|
91
144
|
|
|
92
145
|
const message = input.message.trim();
|
|
93
146
|
const requestId = randomUUID();
|
|
147
|
+
|
|
94
148
|
const outcome = await child.steer({ message, ...(input.mode && input.mode !== "steer" ? { mode: input.mode } : {}) });
|
|
95
149
|
const target = outcome.state === "delivered"
|
|
96
150
|
? { index, state: "delivered" as const, deliveredAt: Date.now() }
|
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
ChildWatchdogProgress,
|
|
25
25
|
} from "../../shared/types.ts";
|
|
26
26
|
import { unresolvedChildWatchdogBlockers } from "../../watchdog/child-status.ts";
|
|
27
|
-
import {
|
|
27
|
+
import { isAgentContract } from "./agent-contract.ts";
|
|
28
28
|
import { classifyTaskMutationIntent, stripSeverityCompounds, taskMayMutate } from "./task-intent.ts";
|
|
29
29
|
|
|
30
30
|
const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
|
|
@@ -202,10 +202,23 @@ export function normalizeGateAcceptance(gate: unknown, acceptance: AcceptanceInp
|
|
|
202
202
|
return normalized.error ? { ok: false, error: normalized.error } : { ok: true, acceptance: normalized.value as AcceptanceInput };
|
|
203
203
|
}
|
|
204
204
|
if (typeof gate !== "string" || !gate.trim()) return { ok: false, error: "gate must be a non-empty command string." };
|
|
205
|
-
if (acceptance !== undefined) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
|
|
205
|
+
if (acceptance !== undefined && acceptance !== false) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." + describeGateAcceptanceConflict(gate, acceptance) };
|
|
206
206
|
return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
export function describeGateAcceptanceConflict(gate: unknown, acceptance: unknown): string {
|
|
210
|
+
const render = (value: unknown): string => {
|
|
211
|
+
let encoded: string;
|
|
212
|
+
try {
|
|
213
|
+
encoded = JSON.stringify(value) ?? String(value);
|
|
214
|
+
} catch {
|
|
215
|
+
encoded = String(value);
|
|
216
|
+
}
|
|
217
|
+
return encoded.length > 120 ? `${encoded.slice(0, 120)}...` : encoded;
|
|
218
|
+
};
|
|
219
|
+
return ` Both fields were present: gate=${render(gate)} acceptance=${render(acceptance)}.`;
|
|
220
|
+
}
|
|
221
|
+
|
|
209
222
|
function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
|
|
210
223
|
return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
|
|
211
224
|
}
|
|
@@ -422,7 +435,7 @@ export function resolveEffectiveAcceptance(input: {
|
|
|
422
435
|
}): ResolvedAcceptanceConfig {
|
|
423
436
|
const explicit = normalizeAcceptanceInput(input.explicit);
|
|
424
437
|
const explicitLevel = normalizeLevel(explicit.level);
|
|
425
|
-
if (
|
|
438
|
+
if (isAgentContract(input.agentContract)) {
|
|
426
439
|
const level = explicitAcceptanceCanDisable(explicit) || explicitLevel === "auto"
|
|
427
440
|
? "none"
|
|
428
441
|
: explicitLevel;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentContract, EffectsProjection, ExecutionProjection, ReviewProjection, SingleResult } from "../../shared/types.ts";
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function isAgentContract(contract: AgentContract | undefined): boolean {
|
|
4
4
|
return contract?.version === 1;
|
|
5
5
|
}
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-text.ts";
|
|
2
2
|
import { formatModelThinking } from "../../shared/formatters.ts";
|
|
3
|
-
import type { AsyncJobState, AsyncJobStep,
|
|
3
|
+
import type { AsyncJobState, AsyncJobStep, HostStepFreshness, HostStepMonitorKind, HostStepNode, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentRunMode, WorkflowGraphSnapshot, WorkflowPreflightLane, WorkflowPreflight } from "../../shared/types.ts";
|
|
4
4
|
import { HOST_STEP_MAX_COUNT, HOST_STEP_MAX_DETAIL_CHARS, HOST_STEP_MAX_LABEL_CHARS, HOST_STEP_MAX_PROVIDER_CHARS, HOST_STEP_MAX_REASON_CHARS, HOST_STEP_MAX_REF_CHARS, HOST_STEP_MAX_ROLE_CHARS, HOST_STEP_MAX_TARGET_CHARS, hostStepReportName, parseHostStepNode, validHostStepNodes } from "./host-step-status.ts";
|
|
5
5
|
import { workflowPreflightLaneForRuntimeKey } from "../../workflows/workflow-preflight.ts";
|
|
6
6
|
import { workflowGraphStageNodes } from "./workflow-graph.ts";
|
|
@@ -32,7 +32,7 @@ function isAsyncStatusSnapshotState(value: string): value is AsyncStatusSnapshot
|
|
|
32
32
|
return Object.hasOwn(ASYNC_STATUS_SNAPSHOT_STATES, value);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export interface
|
|
35
|
+
export interface AsyncStatusSnapshotActivity {
|
|
36
36
|
state?: string;
|
|
37
37
|
currentTool?: string;
|
|
38
38
|
lastActivityAt?: number;
|
|
@@ -41,7 +41,7 @@ export interface AsyncStatusSnapshotActivityV1 {
|
|
|
41
41
|
toolCount?: number;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export interface
|
|
44
|
+
export interface AsyncStatusSnapshotHostStep {
|
|
45
45
|
kind: HostStepMonitorKind;
|
|
46
46
|
provider?: string;
|
|
47
47
|
role?: string;
|
|
@@ -54,7 +54,7 @@ export interface AsyncStatusSnapshotHostStepV1 {
|
|
|
54
54
|
report?: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export interface
|
|
57
|
+
export interface AsyncStatusSnapshotNode {
|
|
58
58
|
id: string;
|
|
59
59
|
kind: AsyncStatusSnapshotKind | "host-step";
|
|
60
60
|
label: string;
|
|
@@ -62,12 +62,12 @@ export interface AsyncStatusSnapshotNodeV1 {
|
|
|
62
62
|
startedAt?: number;
|
|
63
63
|
updatedAt?: number;
|
|
64
64
|
endedAt?: number;
|
|
65
|
-
activity?:
|
|
66
|
-
hostStep?:
|
|
67
|
-
children?:
|
|
65
|
+
activity?: AsyncStatusSnapshotActivity;
|
|
66
|
+
hostStep?: AsyncStatusSnapshotHostStep;
|
|
67
|
+
children?: AsyncStatusSnapshotNode[];
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export interface
|
|
70
|
+
export interface AsyncStatusSnapshotCaps {
|
|
71
71
|
maxRuns: number;
|
|
72
72
|
maxChildrenPerNode: number;
|
|
73
73
|
maxDepth: number;
|
|
@@ -75,19 +75,19 @@ export interface AsyncStatusSnapshotCapsV1 {
|
|
|
75
75
|
maxSerializedBytes: number;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
export interface
|
|
78
|
+
export interface AsyncStatusSnapshotOmitted {
|
|
79
79
|
runs: number;
|
|
80
80
|
children: number;
|
|
81
81
|
byteLimitExceeded: boolean;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export interface
|
|
84
|
+
export interface AsyncStatusSnapshot {
|
|
85
85
|
kind: typeof ASYNC_STATUS_SNAPSHOT_KIND;
|
|
86
86
|
version: typeof ASYNC_STATUS_SNAPSHOT_VERSION;
|
|
87
87
|
generatedAt: number;
|
|
88
|
-
caps:
|
|
89
|
-
omitted:
|
|
90
|
-
runs:
|
|
88
|
+
caps: AsyncStatusSnapshotCaps;
|
|
89
|
+
omitted: AsyncStatusSnapshotOmitted;
|
|
90
|
+
runs: AsyncStatusSnapshotNode[];
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export interface AsyncStatusSnapshotOptions {
|
|
@@ -117,19 +117,19 @@ export interface AsyncStatusWorkflowRow {
|
|
|
117
117
|
reasonCode?: string;
|
|
118
118
|
detail?: string;
|
|
119
119
|
target?: string;
|
|
120
|
-
freshness?:
|
|
120
|
+
freshness?: HostStepFreshness;
|
|
121
121
|
reportPath?: string;
|
|
122
|
-
preflight?:
|
|
122
|
+
preflight?: WorkflowPreflightLane;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
interface ProjectionContext {
|
|
126
|
-
caps:
|
|
127
|
-
omitted:
|
|
126
|
+
caps: AsyncStatusSnapshotCaps;
|
|
127
|
+
omitted: AsyncStatusSnapshotOmitted;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
function validHostStepList(source: readonly
|
|
130
|
+
function validHostStepList(source: readonly HostStepNode[] | WorkflowGraphSnapshot | undefined): HostStepNode[] {
|
|
131
131
|
if (source && "nodes" in source) return validHostStepNodes(source);
|
|
132
|
-
const hostSteps:
|
|
132
|
+
const hostSteps: HostStepNode[] = [];
|
|
133
133
|
for (const [index, value] of (source ?? []).slice(0, HOST_STEP_MAX_COUNT).entries()) {
|
|
134
134
|
try {
|
|
135
135
|
hostSteps.push(parseHostStepNode(value, `hostSteps[${index}]`));
|
|
@@ -140,7 +140,7 @@ function validHostStepList(source: readonly HostStepNodeV1[] | WorkflowGraphSnap
|
|
|
140
140
|
return hostSteps;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
function resolveCaps(options: AsyncStatusSnapshotOptions):
|
|
143
|
+
function resolveCaps(options: AsyncStatusSnapshotOptions): AsyncStatusSnapshotCaps {
|
|
144
144
|
return {
|
|
145
145
|
maxRuns: Math.max(0, Math.floor(options.maxRuns ?? DEFAULT_MAX_RUNS)),
|
|
146
146
|
maxChildrenPerNode: Math.max(0, Math.floor(options.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE)),
|
|
@@ -199,13 +199,13 @@ function activityFor(source: {
|
|
|
199
199
|
currentToolStartedAt?: unknown;
|
|
200
200
|
turnCount?: unknown;
|
|
201
201
|
toolCount?: unknown;
|
|
202
|
-
}, ctx: ProjectionContext):
|
|
202
|
+
}, ctx: ProjectionContext): AsyncStatusSnapshotActivity | undefined {
|
|
203
203
|
const currentTool = publicOptionalText(source.currentTool, ctx.caps.maxStringLength);
|
|
204
204
|
const lastActivityAt = publicTime(source.lastActivityAt);
|
|
205
205
|
const currentToolStartedAt = publicTime(source.currentToolStartedAt);
|
|
206
206
|
const turnCount = publicCount(source.turnCount);
|
|
207
207
|
const toolCount = publicCount(source.toolCount);
|
|
208
|
-
const activity:
|
|
208
|
+
const activity: AsyncStatusSnapshotActivity = {
|
|
209
209
|
...(typeof source.activityState === "string" ? { state: publicText(source.activityState, "unknown", ctx.caps.maxStringLength) } : {}),
|
|
210
210
|
...(currentTool ? { currentTool } : {}),
|
|
211
211
|
...(lastActivityAt !== undefined ? { lastActivityAt } : {}),
|
|
@@ -216,19 +216,19 @@ function activityFor(source: {
|
|
|
216
216
|
return Object.keys(activity).length ? activity : undefined;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
function appendBoundedChildren(children:
|
|
219
|
+
function appendBoundedChildren(children: AsyncStatusSnapshotNode[], source: readonly AsyncStatusSnapshotNode[], ctx: ProjectionContext): void {
|
|
220
220
|
const remaining = Math.max(0, ctx.caps.maxChildrenPerNode - children.length);
|
|
221
221
|
children.push(...source.slice(0, remaining));
|
|
222
222
|
ctx.omitted.children += Math.max(0, source.length - remaining);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: ProjectionContext):
|
|
225
|
+
function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNode {
|
|
226
226
|
const state = normalizeState(step.status);
|
|
227
227
|
const startedAt = publicTime(step.startedAt);
|
|
228
228
|
const endedAt = publicTime(step.endedAt);
|
|
229
229
|
const updatedAt = endedAt ?? publicTime(step.lastActivityAt) ?? startedAt;
|
|
230
230
|
const activity = activityFor(step, ctx);
|
|
231
|
-
const node:
|
|
231
|
+
const node: AsyncStatusSnapshotNode = {
|
|
232
232
|
id: publicText("workflowKey" in step && step.workflowKey ? step.workflowKey : "runId" in step && step.runId ? step.runId : `step:${index}`, `step:${index}`, ctx.caps.maxStringLength),
|
|
233
233
|
kind: "step",
|
|
234
234
|
label: publicText("label" in step && step.label ? step.label : step.agent, "step", ctx.caps.maxStringLength),
|
|
@@ -240,7 +240,7 @@ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, dept
|
|
|
240
240
|
};
|
|
241
241
|
if (depth < ctx.caps.maxDepth && step.children?.length) {
|
|
242
242
|
const nested = step.children.map((child, childIndex) => projectNestedRun(child, childIndex, depth + 1, ctx));
|
|
243
|
-
const bounded:
|
|
243
|
+
const bounded: AsyncStatusSnapshotNode[] = [];
|
|
244
244
|
appendBoundedChildren(bounded, nested, ctx);
|
|
245
245
|
if (bounded.length) node.children = bounded;
|
|
246
246
|
} else if (step.children?.length) {
|
|
@@ -249,7 +249,7 @@ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, dept
|
|
|
249
249
|
return node;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number], index: number, depth: number, ctx: ProjectionContext):
|
|
252
|
+
function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number], index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNode {
|
|
253
253
|
const step: AsyncJobStep = {
|
|
254
254
|
agent: node.agent ?? node.label,
|
|
255
255
|
status: workflowGraphStepStatus(node.status),
|
|
@@ -259,13 +259,13 @@ function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number],
|
|
|
259
259
|
return projectStep(step, node.flatIndex ?? index, depth, ctx);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
function projectNestedRun(child: NestedRunSummary, index: number, depth: number, ctx: ProjectionContext):
|
|
262
|
+
function projectNestedRun(child: NestedRunSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNode {
|
|
263
263
|
const state = normalizeState(child.state);
|
|
264
264
|
const startedAt = publicTime(child.startedAt);
|
|
265
265
|
const endedAt = publicTime(child.endedAt);
|
|
266
266
|
const updatedAt = publicTime(child.lastUpdate) ?? endedAt ?? publicTime(child.lastActivityAt) ?? startedAt;
|
|
267
267
|
const activity = activityFor(child, ctx);
|
|
268
|
-
const node:
|
|
268
|
+
const node: AsyncStatusSnapshotNode = {
|
|
269
269
|
id: publicText(child.id, `nested:${index}`, ctx.caps.maxStringLength),
|
|
270
270
|
kind: kindForMode(child.mode),
|
|
271
271
|
label: child.agent ? publicText(child.agent, "subagent", ctx.caps.maxStringLength) : labelForAgents(child.agents, child.mode ?? "subagent", ctx.caps.maxStringLength),
|
|
@@ -278,7 +278,7 @@ function projectNestedRun(child: NestedRunSummary, index: number, depth: number,
|
|
|
278
278
|
if (depth < ctx.caps.maxDepth) {
|
|
279
279
|
const nestedSteps = child.steps?.map((step, stepIndex) => projectStep(step, stepIndex, depth + 1, ctx)) ?? [];
|
|
280
280
|
const nestedChildren = child.children?.map((nested, childIndex) => projectNestedRun(nested, childIndex, depth + 1, ctx)) ?? [];
|
|
281
|
-
const bounded:
|
|
281
|
+
const bounded: AsyncStatusSnapshotNode[] = [];
|
|
282
282
|
appendBoundedChildren(bounded, [...nestedSteps, ...nestedChildren], ctx);
|
|
283
283
|
if (bounded.length) node.children = bounded;
|
|
284
284
|
} else {
|
|
@@ -295,11 +295,11 @@ function hostStepSnapshotState(state: HostStepState, verdict: HostStepVerdict |
|
|
|
295
295
|
return verdict === undefined || verdict === "inconclusive" ? "partial" : "complete";
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
function projectHostStep(hostStep:
|
|
298
|
+
function projectHostStep(hostStep: HostStepNode, ctx: ProjectionContext): AsyncStatusSnapshotNode {
|
|
299
299
|
const state = hostStepSnapshotState(hostStep.state, hostStep.verdict);
|
|
300
300
|
const detail = publicOptionalText(hostStep.detail, ctx.caps.maxStringLength);
|
|
301
301
|
const report = publicOptionalText(hostStepReportName(hostStep.reportPath), ctx.caps.maxStringLength);
|
|
302
|
-
const hostMetadata:
|
|
302
|
+
const hostMetadata: AsyncStatusSnapshotHostStep = {
|
|
303
303
|
kind: hostStep.monitorKind,
|
|
304
304
|
state: hostStep.state,
|
|
305
305
|
...(hostStep.provider ? { provider: publicText(hostStep.provider, "provider", ctx.caps.maxStringLength) } : {}),
|
|
@@ -322,12 +322,12 @@ function projectHostStep(hostStep: HostStepNodeV1, ctx: ProjectionContext): Asyn
|
|
|
322
322
|
};
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
function projectRun(job: AsyncJobState, ctx: ProjectionContext):
|
|
325
|
+
function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnapshotNode {
|
|
326
326
|
const state = normalizeState(job.status);
|
|
327
327
|
const startedAt = publicTime(job.startedAt);
|
|
328
328
|
const updatedAt = publicTime(job.updatedAt) ?? startedAt;
|
|
329
329
|
const activity = activityFor(job, ctx);
|
|
330
|
-
const node:
|
|
330
|
+
const node: AsyncStatusSnapshotNode = {
|
|
331
331
|
id: publicText(job.asyncId, "async", ctx.caps.maxStringLength),
|
|
332
332
|
kind: kindForMode(job.mode),
|
|
333
333
|
label: labelForAgents(job.agents, job.mode ?? "subagent", ctx.caps.maxStringLength),
|
|
@@ -349,7 +349,7 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
|
|
|
349
349
|
const ordinaryChildren = [...stepChildren, ...graphChildren, ...nestedChildren];
|
|
350
350
|
const retainedHostSteps = hostStepChildren.slice(0, ctx.caps.maxChildrenPerNode);
|
|
351
351
|
const retainedOrdinaryChildren = ordinaryChildren.slice(0, ctx.caps.maxChildrenPerNode - retainedHostSteps.length);
|
|
352
|
-
const bounded:
|
|
352
|
+
const bounded: AsyncStatusSnapshotNode[] = [];
|
|
353
353
|
bounded.push(...retainedOrdinaryChildren, ...retainedHostSteps);
|
|
354
354
|
ctx.omitted.children += ordinaryChildren.length - retainedOrdinaryChildren.length + hostStepChildren.length - retainedHostSteps.length;
|
|
355
355
|
if (bounded.length) node.children = bounded;
|
|
@@ -362,11 +362,11 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
|
|
|
362
362
|
return node;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
function snapshotBytes(snapshot:
|
|
365
|
+
function snapshotBytes(snapshot: AsyncStatusSnapshot): number {
|
|
366
366
|
return Buffer.byteLength(JSON.stringify(snapshot), "utf8");
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
-
function enforceByteLimit(snapshot:
|
|
369
|
+
function enforceByteLimit(snapshot: AsyncStatusSnapshot): void {
|
|
370
370
|
if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) return;
|
|
371
371
|
snapshot.omitted.byteLimitExceeded = true;
|
|
372
372
|
const runs = snapshot.runs;
|
|
@@ -401,7 +401,7 @@ function workflowStepName(step: AsyncJobStep, index: number): string {
|
|
|
401
401
|
return `${phase}${key}${label} (${step.agent})`;
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
-
function hostStepRow(hostStep:
|
|
404
|
+
function hostStepRow(hostStep: HostStepNode): AsyncStatusWorkflowRow {
|
|
405
405
|
const freshness = hostStep.freshness
|
|
406
406
|
? {
|
|
407
407
|
expectedRef: publicText(hostStep.freshness.expectedRef, "ref", HOST_STEP_MAX_REF_CHARS),
|
|
@@ -425,11 +425,11 @@ function hostStepRow(hostStep: HostStepNodeV1): AsyncStatusWorkflowRow {
|
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
|
|
428
|
-
function isWorkflowPreflight(value: readonly
|
|
428
|
+
function isWorkflowPreflight(value: readonly HostStepNode[] | WorkflowGraphSnapshot | WorkflowPreflight | undefined): value is WorkflowPreflight {
|
|
429
429
|
return value !== undefined && !Array.isArray(value) && "lanes" in value;
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
-
function isWorkflowGraph(value: readonly
|
|
432
|
+
function isWorkflowGraph(value: readonly HostStepNode[] | WorkflowGraphSnapshot | WorkflowPreflight | undefined): value is WorkflowGraphSnapshot {
|
|
433
433
|
return value !== undefined && !Array.isArray(value) && "nodes" in value;
|
|
434
434
|
}
|
|
435
435
|
|
|
@@ -465,7 +465,7 @@ function workflowGraphRowName(node: WorkflowGraphSnapshot["nodes"][number]): str
|
|
|
465
465
|
return `${phase ? `${phase}: ` : ""}${key}${label && label !== node.id ? ` · ${label}` : ""}${agent ? ` (${agent})` : ""}`;
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], preflight?:
|
|
468
|
+
function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], preflight?: WorkflowPreflightLane): AsyncStatusWorkflowRow {
|
|
469
469
|
return {
|
|
470
470
|
name: workflowGraphRowName(node),
|
|
471
471
|
state: workflowGraphRowState(node.status),
|
|
@@ -476,14 +476,14 @@ function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], p
|
|
|
476
476
|
/** Project authoritative workflow facts into compact rows, annotated by preflight hints. */
|
|
477
477
|
export function projectAsyncWorkflowRows(
|
|
478
478
|
steps: readonly AsyncJobStep[] | undefined,
|
|
479
|
-
hostStepsOrPreflight?: readonly
|
|
480
|
-
preflightOverride?:
|
|
479
|
+
hostStepsOrPreflight?: readonly HostStepNode[] | WorkflowGraphSnapshot | WorkflowPreflight,
|
|
480
|
+
preflightOverride?: WorkflowPreflight,
|
|
481
481
|
): AsyncStatusWorkflowRow[] {
|
|
482
482
|
const preflight = preflightOverride ?? (isWorkflowPreflight(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined);
|
|
483
483
|
const graph = isWorkflowGraph(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined;
|
|
484
484
|
const hostSteps = isWorkflowPreflight(hostStepsOrPreflight) || graph ? undefined : hostStepsOrPreflight;
|
|
485
485
|
const loaded = steps ?? [];
|
|
486
|
-
const preflightForKey = (key: string, groupKeys: readonly (string | undefined)[] = []):
|
|
486
|
+
const preflightForKey = (key: string, groupKeys: readonly (string | undefined)[] = []): WorkflowPreflightLane | undefined =>
|
|
487
487
|
workflowPreflightLaneForRuntimeKey(preflight, key, groupKeys);
|
|
488
488
|
if (graph) {
|
|
489
489
|
const loadedIndexesByKey = new Map<string, number[]>();
|
|
@@ -526,7 +526,7 @@ export function projectAsyncWorkflowRows(
|
|
|
526
526
|
];
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
-
function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?:
|
|
529
|
+
function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?: WorkflowPreflightLane): AsyncStatusWorkflowRow {
|
|
530
530
|
const modelThinking = formatModelThinking(step.model, step.thinking) || undefined;
|
|
531
531
|
const activity = workflowStepActivity(step);
|
|
532
532
|
return {
|
|
@@ -543,7 +543,7 @@ function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?:
|
|
|
543
543
|
}
|
|
544
544
|
|
|
545
545
|
/** Project already-loaded async status facts into the bounded public snapshot shape. */
|
|
546
|
-
export function projectAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}):
|
|
546
|
+
export function projectAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshot {
|
|
547
547
|
const caps = resolveCaps(options);
|
|
548
548
|
const ctx: ProjectionContext = { caps, omitted: { runs: 0, children: 0, byteLimitExceeded: false } };
|
|
549
549
|
const sorted = [...jobs].sort((left, right) => {
|
|
@@ -552,7 +552,7 @@ export function projectAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, option
|
|
|
552
552
|
return rightUpdated - leftUpdated || left.asyncId.localeCompare(right.asyncId);
|
|
553
553
|
});
|
|
554
554
|
ctx.omitted.runs += Math.max(0, sorted.length - caps.maxRuns);
|
|
555
|
-
const snapshot:
|
|
555
|
+
const snapshot: AsyncStatusSnapshot = {
|
|
556
556
|
kind: ASYNC_STATUS_SNAPSHOT_KIND,
|
|
557
557
|
version: ASYNC_STATUS_SNAPSHOT_VERSION,
|
|
558
558
|
generatedAt: options.generatedAt ?? Date.now(),
|