pi-subagents 0.65.1 → 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 +55 -0
- package/README.md +1 -1
- package/agents/researcher.md +23 -13
- package/docs/agents.md +16 -2
- package/docs/configuration.md +18 -0
- package/docs/extension-api.md +91 -0
- package/docs/models.md +58 -1
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +3 -3
- package/docs/workflows.md +14 -7
- package/package.json +2 -1
- package/skills/pi-subagents/references/execution-controls.md +14 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- 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/shared-types.ts +1 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/index.ts +40 -2
- package/src/extension/public-execution.ts +0 -1
- package/src/extension/rpc.ts +4 -21
- package/src/extension/schemas.ts +8 -6
- package/src/extension/tool-description.ts +6 -5
- package/src/intercom/native-supervisor-channel.ts +82 -54
- package/src/runs/background/active-async-capacity.ts +18 -18
- package/src/runs/background/async-job-tracker.ts +35 -3
- 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 +86 -12
- package/src/runs/background/owned-process-tree.ts +6 -6
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +60 -32
- package/src/runs/background/run-status.ts +75 -5
- package/src/runs/background/runner-aliases.ts +18 -7
- 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 +412 -209
- 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 +101 -25
- package/src/runs/foreground/subagent-executor.ts +474 -197
- 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 +2 -2
- 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 +42 -6
- 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 +10 -9
- package/src/runs/shared/model-fallback.ts +10 -6
- package/src/runs/shared/nested-events.ts +5 -5
- 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 +2 -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-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +332 -204
- package/src/shared/types.ts +81 -60
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +23 -5
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +38 -10
- package/src/workflows/workflow-checklist.ts +11 -15
- 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
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
|
-
import type { ExtensionAPI,
|
|
5
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import type { ChildSupervisorMetadata } from "../runs/shared/child-runtime-config.ts";
|
|
8
8
|
import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type ControlEvent, type IntercomEventBus, type SubagentState } from "../shared/types.ts";
|
|
@@ -74,7 +74,7 @@ interface ContactSupervisorParams {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
interface IntercomParams {
|
|
77
|
-
action: "list" | "
|
|
77
|
+
action: "list" | "pending" | "status" | "reply";
|
|
78
78
|
to?: string;
|
|
79
79
|
message?: string;
|
|
80
80
|
replyTo?: string;
|
|
@@ -83,6 +83,8 @@ interface IntercomParams {
|
|
|
83
83
|
type SupervisorWatch = (filename: fs.PathLike, listener: fs.WatchListener<string>) => fs.FSWatcher;
|
|
84
84
|
|
|
85
85
|
interface NativeSupervisorChannelDeps {
|
|
86
|
+
/** Retained scheduled states for the current runtime owner, never foreign owners. */
|
|
87
|
+
getCurrentOwnerStates?: () => Iterable<SubagentState>;
|
|
86
88
|
platform?: NodeJS.Platform;
|
|
87
89
|
watch?: SupervisorWatch;
|
|
88
90
|
timers?: Pick<typeof globalThis, "setInterval" | "clearInterval" | "setImmediate" | "clearImmediate">;
|
|
@@ -95,7 +97,7 @@ const ContactSupervisorParamsSchema = Type.Object({
|
|
|
95
97
|
}, { additionalProperties: false });
|
|
96
98
|
|
|
97
99
|
const IntercomParamsSchema = Type.Object({
|
|
98
|
-
action: Type.String({ enum: ["list", "
|
|
100
|
+
action: Type.String({ enum: ["list", "pending", "status", "reply"] }),
|
|
99
101
|
to: Type.Optional(Type.String()),
|
|
100
102
|
message: Type.Optional(Type.String()),
|
|
101
103
|
replyTo: Type.Optional(Type.String()),
|
|
@@ -381,18 +383,8 @@ function cleanupStaleEmptySupervisorChannels(nowMs = Date.now()): number {
|
|
|
381
383
|
return removed;
|
|
382
384
|
}
|
|
383
385
|
|
|
384
|
-
function
|
|
385
|
-
|
|
386
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
387
|
-
if (sessionId) return sessionId;
|
|
388
|
-
} catch {
|
|
389
|
-
// Fall through to the last known identity.
|
|
390
|
-
}
|
|
391
|
-
return state.currentSessionId ?? undefined;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function requestMatchesContext(request: SupervisorRequest, state: Pick<SubagentState, "currentSessionId">, ctx: ExtensionContext): boolean {
|
|
395
|
-
const currentSessionId = currentContextSessionId(state, ctx);
|
|
386
|
+
function requestMatchesOwner(request: SupervisorRequest, state: Pick<SubagentState, "supervisorOwnerSessionId">): boolean {
|
|
387
|
+
const currentSessionId = state.supervisorOwnerSessionId;
|
|
396
388
|
return Boolean(currentSessionId && request.orchestratorSessionId === currentSessionId);
|
|
397
389
|
}
|
|
398
390
|
|
|
@@ -462,12 +454,12 @@ function requestRunInactive(request: SupervisorRequest, state: SubagentState): b
|
|
|
462
454
|
return stepStatus === "complete" || stepStatus === "completed" || stepStatus === "failed" || stepStatus === "paused";
|
|
463
455
|
}
|
|
464
456
|
|
|
465
|
-
function requestLifecycle(request: PendingSupervisorRequest, state: SubagentState,
|
|
466
|
-
if (
|
|
457
|
+
function requestLifecycle(request: PendingSupervisorRequest, state: SubagentState, now: number, runState: SubagentState): SupervisorRequestLifecycle {
|
|
458
|
+
if (!requestMatchesOwner(request, state)) return "wrong-session";
|
|
467
459
|
if (!fs.existsSync(request.requestFile)) return "missing";
|
|
468
460
|
if (request.expectsReply && fs.existsSync(replyPath(request.channelDir, request.id))) return "resolved";
|
|
469
461
|
if (request.expectsReply && now > requestExpiresAt(request, now)) return "expired";
|
|
470
|
-
if (request.expectsReply && requestRunInactive(request,
|
|
462
|
+
if (request.expectsReply && requestRunInactive(request, runState)) return "inactive";
|
|
471
463
|
return "pending";
|
|
472
464
|
}
|
|
473
465
|
|
|
@@ -475,10 +467,10 @@ function cleanupRequestLifecycle(request: PendingSupervisorRequest, lifecycle: S
|
|
|
475
467
|
if (lifecycle === "resolved" || lifecycle === "expired" || lifecycle === "inactive") removeRequestFile(request.requestFile);
|
|
476
468
|
}
|
|
477
469
|
|
|
478
|
-
function refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState,
|
|
470
|
+
function refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, onLifecycle: SupervisorRequestLifecycleObserver, runState: (request: SupervisorRequest) => SubagentState): void {
|
|
479
471
|
const now = Date.now();
|
|
480
472
|
for (const request of pending.values()) {
|
|
481
|
-
const lifecycle = requestLifecycle(request, state,
|
|
473
|
+
const lifecycle = requestLifecycle(request, state, now, runState(request));
|
|
482
474
|
if (lifecycle === "pending") continue;
|
|
483
475
|
pending.delete(request.id);
|
|
484
476
|
onLifecycle(request, lifecycle);
|
|
@@ -498,7 +490,6 @@ function requestVisibleText(request: PendingSupervisorRequest): string {
|
|
|
498
490
|
`Agent: ${request.agent}`,
|
|
499
491
|
`Child index: ${request.childIndex}`,
|
|
500
492
|
];
|
|
501
|
-
if (request.childTarget) lines.push(`Child intercom target: ${request.childTarget}`);
|
|
502
493
|
lines.push("");
|
|
503
494
|
if (request.message) lines.push(request.message);
|
|
504
495
|
if (request.reason === "interview_request") {
|
|
@@ -509,6 +500,7 @@ function requestVisibleText(request: PendingSupervisorRequest): string {
|
|
|
509
500
|
if (request.interview !== undefined) lines.push(JSON.stringify(request.interview, null, "\t"));
|
|
510
501
|
}
|
|
511
502
|
if (request.expectsReply) lines.push("", `Reply with: ${supervisorReplyHint(request.id)}`);
|
|
503
|
+
lines.push("", `Live guidance: subagent({ action: "steer", id: ${JSON.stringify(request.runId)}, index: ${request.childIndex}, message: "..." })${request.expectsReply ? " (Reply to the pending request first.)" : ""}`);
|
|
512
504
|
return lines.join("\n").trimEnd();
|
|
513
505
|
}
|
|
514
506
|
|
|
@@ -563,6 +555,7 @@ function resolvePendingRequest(pending: Map<string, PendingSupervisorRequest>, p
|
|
|
563
555
|
);
|
|
564
556
|
if (matches.length === 1) return matches[0]!;
|
|
565
557
|
if (matches.length > 1) throw new Error(`Multiple pending supervisor requests match '${params.to}'. Use replyTo.`);
|
|
558
|
+
throw new Error(`No pending supervisor request matches '${params.to}'. Use replyTo.`);
|
|
566
559
|
}
|
|
567
560
|
if (requests.length === 1) return requests[0]!;
|
|
568
561
|
if (requests.length === 0) throw new Error("No pending supervisor requests need a reply.");
|
|
@@ -580,14 +573,16 @@ function publicPendingRequests(pending: Map<string, PendingSupervisorRequest>):
|
|
|
580
573
|
}));
|
|
581
574
|
}
|
|
582
575
|
|
|
583
|
-
function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, PendingSupervisorRequest>, state: SubagentState, onLifecycle: SupervisorRequestLifecycleObserver): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
|
|
576
|
+
function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, PendingSupervisorRequest>, state: SubagentState, onLifecycle: SupervisorRequestLifecycleObserver, discover: () => void, runState: (request: SupervisorRequest) => SubagentState): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
|
|
584
577
|
return {
|
|
585
578
|
name: NATIVE_SUPERVISOR_TOOL_NAME,
|
|
586
579
|
label: "Subagent Supervisor",
|
|
587
580
|
description: "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
|
|
588
581
|
parameters: IntercomParamsSchema,
|
|
589
582
|
async execute(_id, params) {
|
|
590
|
-
|
|
583
|
+
// Discover new request files even when demand-gated polling is idle.
|
|
584
|
+
discover();
|
|
585
|
+
refreshPendingRequests(pending, state, onLifecycle, runState);
|
|
591
586
|
const input = params as IntercomParams;
|
|
592
587
|
if (input.action === "status") {
|
|
593
588
|
return { content: [{ type: "text", text: `Native supervisor channel active. Pending replies: ${pending.size}.` }], details: { active: true, pending: pending.size, root: SUPERVISOR_CHANNEL_ROOT } };
|
|
@@ -605,9 +600,6 @@ function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, Pendin
|
|
|
605
600
|
clearForegroundSupervisorAttention(request, pending, state);
|
|
606
601
|
return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };
|
|
607
602
|
}
|
|
608
|
-
if (input.action === "send" || input.action === "ask") {
|
|
609
|
-
throw new Error("The native subagent supervisor handles replies only. Child agents initiate asks with contact_supervisor.");
|
|
610
|
-
}
|
|
611
603
|
throw new Error(`Unsupported supervisor action: ${input.action}`);
|
|
612
604
|
},
|
|
613
605
|
};
|
|
@@ -616,12 +608,19 @@ function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, Pendin
|
|
|
616
608
|
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): {
|
|
617
609
|
start: () => void;
|
|
618
610
|
activateTransport: () => void;
|
|
611
|
+
findPendingAsks: (target: { runId: string; agent: string; childIndex: number }) => string[];
|
|
619
612
|
dispose: () => void;
|
|
620
613
|
pending: Map<string, PendingSupervisorRequest>;
|
|
621
614
|
getSupervisorRequestState: (event: ControlEvent) => SupervisorRequestState;
|
|
622
615
|
} {
|
|
623
616
|
const watch = deps.watch ?? fs.watch;
|
|
624
617
|
const timers = deps.timers ?? globalThis;
|
|
618
|
+
const runState = (request: SupervisorRequest): SubagentState => {
|
|
619
|
+
for (const ownerState of deps.getCurrentOwnerStates?.() ?? []) {
|
|
620
|
+
if (ownerState.asyncJobs.has(request.runId)) return ownerState;
|
|
621
|
+
}
|
|
622
|
+
return state;
|
|
623
|
+
};
|
|
625
624
|
const pending = new Map<string, PendingSupervisorRequest>();
|
|
626
625
|
const requestCorrelations = new Map<string, SupervisorRequestCorrelation>();
|
|
627
626
|
const correlationKey = (request: { runId: string; agent: string; childIndex: number; toolCallId?: string }): string | undefined => {
|
|
@@ -667,7 +666,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
667
666
|
if (!fs.existsSync(correlation.request.requestFile)
|
|
668
667
|
|| fs.existsSync(replyPath(correlation.request.channelDir, correlation.request.id))
|
|
669
668
|
|| now > requestExpiresAt(correlation.request, now)
|
|
670
|
-
|| requestRunInactive(correlation.request,
|
|
669
|
+
|| requestRunInactive(correlation.request, runState(correlation.request))) {
|
|
671
670
|
rememberResolvedRequest(correlation.request);
|
|
672
671
|
return "resolved";
|
|
673
672
|
}
|
|
@@ -686,11 +685,17 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
686
685
|
const hasTransportDemand = () => {
|
|
687
686
|
if (pending.size > 0) return true;
|
|
688
687
|
if (state.foregroundControls.size > 0) return true;
|
|
689
|
-
|
|
688
|
+
if ([...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running")) return true;
|
|
689
|
+
for (const ownerState of deps.getCurrentOwnerStates?.() ?? []) {
|
|
690
|
+
for (const job of ownerState.asyncJobs.values()) {
|
|
691
|
+
if (job.status === "queued" || job.status === "running") return true;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return false;
|
|
690
695
|
};
|
|
691
696
|
|
|
692
697
|
const registerParentTools = (): void => {
|
|
693
|
-
if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pi, pending, state, observeRequestLifecycle));
|
|
698
|
+
if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pi, pending, state, observeRequestLifecycle, () => poll(), runState));
|
|
694
699
|
};
|
|
695
700
|
|
|
696
701
|
const cleanupStaleChannelsIfDue = (): void => {
|
|
@@ -706,15 +711,14 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
706
711
|
|
|
707
712
|
const poll = (): void => {
|
|
708
713
|
cleanupStaleChannelsIfDue();
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
refreshPendingRequests(pending, state, ctx, observeRequestLifecycle);
|
|
714
|
+
// Only display notifications require a live UI context, not request registration.
|
|
715
|
+
refreshPendingRequests(pending, state, observeRequestLifecycle, runState);
|
|
712
716
|
const now = Date.now();
|
|
713
717
|
for (const { channelDir, file } of listRequestFiles()) {
|
|
714
718
|
if (seenFiles.has(file)) continue;
|
|
715
719
|
const request = parseRequestFile(file, channelDir);
|
|
716
|
-
if (!request || !
|
|
717
|
-
const lifecycle = requestLifecycle(request, state,
|
|
720
|
+
if (!request || !requestMatchesOwner(request, state)) continue;
|
|
721
|
+
const lifecycle = requestLifecycle(request, state, now, runState(request));
|
|
718
722
|
if (lifecycle !== "pending") {
|
|
719
723
|
seenFiles.add(file);
|
|
720
724
|
observeRequestLifecycle(request, lifecycle);
|
|
@@ -727,27 +731,34 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
727
731
|
pending.set(request.id, request);
|
|
728
732
|
markForegroundSupervisorAttention(request, state);
|
|
729
733
|
}
|
|
730
|
-
|
|
731
|
-
|
|
734
|
+
// The ask is already queued above. A sendMessage failure (no UI, stale context) must not
|
|
735
|
+
// lose it, and must not abort the loop before the remaining asks register.
|
|
736
|
+
try {
|
|
737
|
+
pi.sendMessage({
|
|
738
|
+
customType: SUPERVISOR_REQUEST_MESSAGE_TYPE,
|
|
739
|
+
content: requestVisibleText(request),
|
|
740
|
+
display: true,
|
|
741
|
+
details: {
|
|
742
|
+
id: request.id,
|
|
743
|
+
requestId: request.id,
|
|
744
|
+
reason: request.reason,
|
|
745
|
+
expectsReply: request.expectsReply,
|
|
746
|
+
runId: request.runId,
|
|
747
|
+
agent: request.agent,
|
|
748
|
+
childIndex: request.childIndex,
|
|
749
|
+
...(request.childTarget ? { childTarget: request.childTarget } : {}),
|
|
750
|
+
...(request.interview !== undefined ? { interview: request.interview } : {}),
|
|
751
|
+
requestBody: request.message,
|
|
752
|
+
...(request.expectsReply ? { replyHint: supervisorReplyHint(request.id) } : {}),
|
|
753
|
+
},
|
|
754
|
+
}, { triggerTurn: true });
|
|
755
|
+
// sendMessage accepts synchronously; one-way updates stay on disk until it returns.
|
|
756
|
+
if (!request.expectsReply) removeRequestFile(request.requestFile);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
// Allow an existing later scan to retry an unaccepted one-way update.
|
|
759
|
+
if (!request.expectsReply) seenFiles.delete(file);
|
|
760
|
+
console.error(`Failed to surface supervisor request ${request.id} as a user turn:`, error);
|
|
732
761
|
}
|
|
733
|
-
pi.sendMessage({
|
|
734
|
-
customType: SUPERVISOR_REQUEST_MESSAGE_TYPE,
|
|
735
|
-
content: requestVisibleText(request),
|
|
736
|
-
display: true,
|
|
737
|
-
details: {
|
|
738
|
-
id: request.id,
|
|
739
|
-
requestId: request.id,
|
|
740
|
-
reason: request.reason,
|
|
741
|
-
expectsReply: request.expectsReply,
|
|
742
|
-
runId: request.runId,
|
|
743
|
-
agent: request.agent,
|
|
744
|
-
childIndex: request.childIndex,
|
|
745
|
-
...(request.childTarget ? { childTarget: request.childTarget } : {}),
|
|
746
|
-
...(request.interview !== undefined ? { interview: request.interview } : {}),
|
|
747
|
-
requestBody: request.message,
|
|
748
|
-
...(request.expectsReply ? { replyHint: supervisorReplyHint(request.id) } : {}),
|
|
749
|
-
},
|
|
750
|
-
}, { triggerTurn: true });
|
|
751
762
|
if (request.expectsReply) {
|
|
752
763
|
(pi as { events?: IntercomEventBus }).events?.emit(INTERCOM_DETACH_REQUEST_EVENT, {
|
|
753
764
|
requestId: request.id,
|
|
@@ -824,6 +835,23 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
824
835
|
poll();
|
|
825
836
|
if (!useNativeWatcher() && hasTransportDemand()) startPolling();
|
|
826
837
|
},
|
|
838
|
+
findPendingAsks: (target) => {
|
|
839
|
+
// Receipt-only discovery: no registration, notification, cleanup or reply writes.
|
|
840
|
+
const channelDir = resolveSupervisorChannelDir(target.runId, target.agent, target.childIndex);
|
|
841
|
+
let files: string[];
|
|
842
|
+
try { files = fs.readdirSync(path.join(channelDir, REQUESTS_DIR)); }
|
|
843
|
+
catch (error) {
|
|
844
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
845
|
+
throw error;
|
|
846
|
+
}
|
|
847
|
+
const now = Date.now();
|
|
848
|
+
return files.filter(file => file.endsWith(".json")).flatMap(file => {
|
|
849
|
+
const request = parseRequestFile(path.join(channelDir, REQUESTS_DIR, file), channelDir);
|
|
850
|
+
return request?.expectsReply && request.runId === target.runId
|
|
851
|
+
&& request.agent === target.agent && request.childIndex === target.childIndex
|
|
852
|
+
&& requestLifecycle(request, state, now, runState(request)) === "pending" ? [request.id] : [];
|
|
853
|
+
}).sort();
|
|
854
|
+
},
|
|
827
855
|
start: () => {
|
|
828
856
|
if (started) return;
|
|
829
857
|
started = true;
|
|
@@ -12,7 +12,7 @@ export const DEFAULT_ABANDONED_SLOT_RELEASE_AFTER_MS = 20 * 60 * 1000;
|
|
|
12
12
|
export const MIN_ABANDONED_SLOT_RELEASE_AFTER_MS = 5 * 60 * 1000;
|
|
13
13
|
export const MAX_ABANDONED_SLOT_RELEASE_AFTER_MS = 24 * 60 * 60 * 1000;
|
|
14
14
|
|
|
15
|
-
export interface
|
|
15
|
+
export interface ActiveAsyncCapacityOwner {
|
|
16
16
|
version: 1;
|
|
17
17
|
reservationToken: string;
|
|
18
18
|
ownerSessionId: string;
|
|
@@ -29,7 +29,7 @@ export interface ActiveAsyncCapacityOwnerV1 {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface ActiveAsyncCapacityHandle {
|
|
32
|
-
readonly owner:
|
|
32
|
+
readonly owner: ActiveAsyncCapacityOwner;
|
|
33
33
|
markStarted(runnerProcessInstanceId: string): void;
|
|
34
34
|
markWorkflowStarted(): void;
|
|
35
35
|
rollback(): boolean;
|
|
@@ -44,7 +44,7 @@ interface CapacityOptions {
|
|
|
44
44
|
abandonedSlotReleaseAfterMs?: number | false;
|
|
45
45
|
pidLiveness?: (pid: number) => PidLiveness;
|
|
46
46
|
afterSlotRename?: (releasedDir: string) => void;
|
|
47
|
-
writeOwner?: (filePath: string, owner:
|
|
47
|
+
writeOwner?: (filePath: string, owner: ActiveAsyncCapacityOwner) => void;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export interface ActiveAsyncCapacityReleaseEvidence {
|
|
@@ -61,7 +61,7 @@ export type ActiveAsyncCapacityReleaseVerdict =
|
|
|
61
61
|
| { state: "not-owned"; reason: string };
|
|
62
62
|
|
|
63
63
|
export interface ActiveAsyncCapacityInspection {
|
|
64
|
-
owner?:
|
|
64
|
+
owner?: ActiveAsyncCapacityOwner;
|
|
65
65
|
relation: "current" | "source" | "none";
|
|
66
66
|
slotDir?: string;
|
|
67
67
|
release: ActiveAsyncCapacityReleaseVerdict;
|
|
@@ -100,9 +100,9 @@ function slotDir(poolDir: string, slot: number): string {
|
|
|
100
100
|
return path.join(poolDir, `slot-${slot}`);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function parseOwner(value: unknown):
|
|
103
|
+
function parseOwner(value: unknown): ActiveAsyncCapacityOwner | undefined {
|
|
104
104
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
105
|
-
const owner = value as Partial<
|
|
105
|
+
const owner = value as Partial<ActiveAsyncCapacityOwner>;
|
|
106
106
|
if (owner.version !== 1
|
|
107
107
|
|| typeof owner.reservationToken !== "string" || !owner.reservationToken
|
|
108
108
|
|| typeof owner.ownerSessionId !== "string" || !owner.ownerSessionId
|
|
@@ -116,10 +116,10 @@ function parseOwner(value: unknown): ActiveAsyncCapacityOwnerV1 | undefined {
|
|
|
116
116
|
if (owner.sourceRunId !== undefined && typeof owner.sourceRunId !== "string") return undefined;
|
|
117
117
|
if (owner.runnerProcessInstanceId !== undefined && (typeof owner.runnerProcessInstanceId !== "string" || !owner.runnerProcessInstanceId)) return undefined;
|
|
118
118
|
if (owner.runnerStartedAt !== undefined && (typeof owner.runnerStartedAt !== "number" || !Number.isFinite(owner.runnerStartedAt))) return undefined;
|
|
119
|
-
return owner as
|
|
119
|
+
return owner as ActiveAsyncCapacityOwner;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function readOwner(dir: string):
|
|
122
|
+
function readOwner(dir: string): ActiveAsyncCapacityOwner | undefined {
|
|
123
123
|
try {
|
|
124
124
|
return parseOwner(JSON.parse(fs.readFileSync(path.join(dir, "owner.json"), "utf-8")));
|
|
125
125
|
} catch {
|
|
@@ -142,7 +142,7 @@ function snapshotFor(sessionId: string, limit: number | undefined, rootDir: stri
|
|
|
142
142
|
return { used: occupiedSlots(sessionDir(sessionId, rootDir)).length, limit: limit ?? 0 };
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function matchingOwner(dir: string, expected:
|
|
145
|
+
function matchingOwner(dir: string, expected: ActiveAsyncCapacityOwner): ActiveAsyncCapacityOwner | undefined {
|
|
146
146
|
const owner = readOwner(dir);
|
|
147
147
|
return owner?.reservationToken === expected.reservationToken
|
|
148
148
|
&& owner.runId === expected.runId
|
|
@@ -176,7 +176,7 @@ function withSlotClaim<T>(dir: string, operation: () => T): { acquired: true; va
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
function removeOwnedSlot(dir: string, expected:
|
|
179
|
+
function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwner, options: CapacityOptions, requireUnstarted = false, release?: ActiveAsyncCapacityReleaseVerdict): boolean {
|
|
180
180
|
if (requireUnstarted && (expected.runnerProcessInstanceId || expected.runnerStartedAt)) return false;
|
|
181
181
|
const claimed = withSlotClaim(dir, () => {
|
|
182
182
|
const current = matchingOwner(dir, expected);
|
|
@@ -193,7 +193,7 @@ function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwnerV1, opti
|
|
|
193
193
|
return claimed.acquired && claimed.value;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
function appendAbandonedReleaseEvent(asyncDir: string, owner:
|
|
196
|
+
function appendAbandonedReleaseEvent(asyncDir: string, owner: ActiveAsyncCapacityOwner, evidence: ActiveAsyncCapacityReleaseEvidence, now: number): void {
|
|
197
197
|
try {
|
|
198
198
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
199
199
|
fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
|
|
@@ -213,7 +213,7 @@ function terminalState(state: AsyncStatus["state"]): boolean {
|
|
|
213
213
|
return state !== "queued" && state !== "running" && state !== "paused";
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
function runnerReleaseVerdict(owner:
|
|
216
|
+
function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncStatus | null, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
|
|
217
217
|
if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
|
|
218
218
|
if (!owner.runnerProcessInstanceId) return { state: "retained", reason: "runner process identity has not been recorded" };
|
|
219
219
|
if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
|
|
@@ -262,7 +262,7 @@ function abandonedRunnerReleaseVerdict(status: AsyncStatus, proofState: string,
|
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
function workflowReleaseVerdict(owner:
|
|
265
|
+
function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncStatus | null, liveWorkflowRunIds: ReadonlySet<string>): ActiveAsyncCapacityReleaseVerdict {
|
|
266
266
|
if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
|
|
267
267
|
if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
|
|
268
268
|
if (status.runId !== owner.runId) return { state: "retained", reason: `status run ${status.runId} does not match owner run ${owner.runId}` };
|
|
@@ -289,7 +289,7 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: Async
|
|
|
289
289
|
return { state: "releasable", reason: "workflow is terminal, controller is gone, and async children have observed proof" };
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
function ownerReleaseVerdict(owner:
|
|
292
|
+
function ownerReleaseVerdict(owner: ActiveAsyncCapacityOwner, liveWorkflowRunIds: ReadonlySet<string>, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
|
|
293
293
|
const status = readStatus(owner.asyncDir);
|
|
294
294
|
return owner.kind === "runner"
|
|
295
295
|
? runnerReleaseVerdict(owner, status, options)
|
|
@@ -364,7 +364,7 @@ export function getActiveAsyncCapacitySnapshot(
|
|
|
364
364
|
return reconcileActiveAsyncCapacity(sessionId, limit, options);
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
function createSlot(poolDir: string, owner:
|
|
367
|
+
function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwner): boolean {
|
|
368
368
|
const destination = slotDir(poolDir, owner.slot);
|
|
369
369
|
fs.mkdirSync(poolDir, { recursive: true, mode: 0o700 });
|
|
370
370
|
try {
|
|
@@ -379,7 +379,7 @@ function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwnerV1): boolean
|
|
|
379
379
|
return true;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
function handleFor(owner:
|
|
382
|
+
function handleFor(owner: ActiveAsyncCapacityOwner, limit: number, options: CapacityOptions, rollbackOwner?: ActiveAsyncCapacityOwner): ActiveAsyncCapacityHandle {
|
|
383
383
|
const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
|
|
384
384
|
const writeOwner = options.writeOwner ?? writePrivateAtomicJson;
|
|
385
385
|
const dir = slotDir(sessionDir(owner.ownerSessionId, rootDir), owner.slot);
|
|
@@ -462,7 +462,7 @@ export function acquireActiveAsyncCapacity(
|
|
|
462
462
|
const poolDir = sessionDir(input.sessionId, rootDir);
|
|
463
463
|
const token = options.token?.() ?? randomUUID();
|
|
464
464
|
for (let slot = 0; slot < input.limit; slot++) {
|
|
465
|
-
const owner:
|
|
465
|
+
const owner: ActiveAsyncCapacityOwner = {
|
|
466
466
|
version: 1,
|
|
467
467
|
reservationToken: token,
|
|
468
468
|
ownerSessionId: input.sessionId,
|
|
@@ -495,7 +495,7 @@ export function transferActiveAsyncCapacity(
|
|
|
495
495
|
if (!current || !status || status.runId !== input.sourceRunId || status.state === "queued" || status.state === "running") {
|
|
496
496
|
throw new Error(`Active async capacity source '${input.sourceRunId}' is not transferable.`);
|
|
497
497
|
}
|
|
498
|
-
const next:
|
|
498
|
+
const next: ActiveAsyncCapacityOwner = {
|
|
499
499
|
...current,
|
|
500
500
|
runId: input.runId,
|
|
501
501
|
sourceRunId: input.sourceRunId,
|
|
@@ -26,6 +26,7 @@ import { EXTERNAL_JOB_BRIDGE_REQUEST_DIR, serviceExternalJobBridgeRequests } fro
|
|
|
26
26
|
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
27
27
|
import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
28
28
|
import { validHostStepNodes } from "../shared/host-step-status.ts";
|
|
29
|
+
import { readProcessTerminal } from "./process-terminal.ts";
|
|
29
30
|
import { withCachedUiContext } from "../../shared/extension-context.ts";
|
|
30
31
|
|
|
31
32
|
interface AsyncJobTrackerOptions {
|
|
@@ -78,6 +79,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
78
79
|
let widgetRerenderTimer: ReturnType<typeof setTimeout> | undefined;
|
|
79
80
|
const runningJobIds = new Set<string>();
|
|
80
81
|
const externalJobBridgeRuns = new Set<string>();
|
|
82
|
+
// Early native failure is visible before its publisher finishes. Retain only
|
|
83
|
+
// that scoped observation, using the existing liveness sweep to renew delivery.
|
|
84
|
+
const terminalPublications = new Map<string, { instanceId: string; pending: true } | { pending: false }>();
|
|
81
85
|
const externalJobBridgeEligibility = (steps: AsyncJobState["steps"]): "required" | "not-required" | "unknown" => {
|
|
82
86
|
if (!Array.isArray(steps)) return "unknown";
|
|
83
87
|
for (const step of steps) {
|
|
@@ -380,6 +384,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
380
384
|
refreshTimers.delete(asyncId);
|
|
381
385
|
runningJobIds.delete(asyncId);
|
|
382
386
|
externalJobBridgeRuns.delete(asyncId);
|
|
387
|
+
terminalPublications.delete(asyncId);
|
|
383
388
|
};
|
|
384
389
|
|
|
385
390
|
const refreshJob = (job: AsyncJobState): boolean => {
|
|
@@ -440,6 +445,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
440
445
|
if (status) {
|
|
441
446
|
const previousStatus = job.status;
|
|
442
447
|
job.status = status.state;
|
|
448
|
+
if (!terminalStatus(job.status)) terminalPublications.delete(job.asyncId);
|
|
443
449
|
if (job.status === "running") runningJobIds.add(job.asyncId);
|
|
444
450
|
else runningJobIds.delete(job.asyncId);
|
|
445
451
|
if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused" && job.status !== "stopped") cancelCleanup(job.asyncId);
|
|
@@ -497,9 +503,30 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
497
503
|
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
498
504
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
499
505
|
if (terminalStatus(job.status)) {
|
|
500
|
-
|
|
506
|
+
let publication = terminalPublications.get(job.asyncId);
|
|
507
|
+
if (!publication && status.mode !== "workflow" && status.processTerminal?.state === "pending"
|
|
508
|
+
&& status.runId === job.asyncId && status.sessionId === state.currentSessionId
|
|
509
|
+
&& status.completionOwnerId && status.completionOwnerId === state.completionOwnerId) {
|
|
510
|
+
publication = { instanceId: status.processTerminal.runnerProcessInstanceId, pending: true };
|
|
511
|
+
terminalPublications.set(job.asyncId, publication);
|
|
512
|
+
}
|
|
513
|
+
const wasPending = publication?.pending;
|
|
514
|
+
if (publication?.pending) {
|
|
515
|
+
// Reconciliation resolves pending as well as public payloads. The
|
|
516
|
+
// watcher already discovers exact tracked run IDs without promotion.
|
|
517
|
+
const published = reconciliation.resultPath && fs.existsSync(reconciliation.resultPath);
|
|
518
|
+
const closed = !published && readProcessTerminal(job.asyncDir, {
|
|
519
|
+
runId: job.asyncId, runnerProcessInstanceId: publication.instanceId,
|
|
520
|
+
})?.state === "observed";
|
|
521
|
+
if (published || closed) {
|
|
522
|
+
publication = { pending: false };
|
|
523
|
+
terminalPublications.set(job.asyncId, publication);
|
|
524
|
+
} else cancelCleanup(job.asyncId);
|
|
525
|
+
}
|
|
526
|
+
// Scan on close too: publication may have raced the payload check.
|
|
527
|
+
if (!terminalStatus(previousStatus) || (wasPending && !publication?.pending)) options.onJobTerminal?.();
|
|
501
528
|
rememberFleetJob(state, job);
|
|
502
|
-
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
529
|
+
if (!publication?.pending && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
503
530
|
scheduleCleanup(job.asyncId);
|
|
504
531
|
}
|
|
505
532
|
}
|
|
@@ -526,7 +553,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
526
553
|
}
|
|
527
554
|
runningJobIds.delete(job.asyncId);
|
|
528
555
|
rememberFleetJob(state, job);
|
|
529
|
-
if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) scheduleCleanup(job.asyncId);
|
|
556
|
+
if (!terminalPublications.get(job.asyncId)?.pending && !hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) scheduleCleanup(job.asyncId);
|
|
530
557
|
}
|
|
531
558
|
return widgetRenderKey(job, widgetExpanded) !== widgetStateBefore;
|
|
532
559
|
};
|
|
@@ -668,6 +695,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
668
695
|
const sessionRoot = state.liveAsyncSessionRoots?.get(info.id);
|
|
669
696
|
state.liveAsyncSessionRoots?.delete(info.id);
|
|
670
697
|
externalJobBridgeRuns.delete(info.id);
|
|
698
|
+
terminalPublications.delete(info.id);
|
|
671
699
|
state.asyncJobs.set(info.id, {
|
|
672
700
|
asyncId: info.id,
|
|
673
701
|
asyncDir,
|
|
@@ -721,6 +749,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
721
749
|
job.asyncDir = result.asyncDir;
|
|
722
750
|
watchJob(job);
|
|
723
751
|
}
|
|
752
|
+
// Delivery can precede the first terminal status refresh. Remember its
|
|
753
|
+
// settlement even when no pending publication has been observed yet.
|
|
754
|
+
terminalPublications.set(asyncId, { pending: false });
|
|
724
755
|
try {
|
|
725
756
|
updateAsyncJobNestedProjection(job);
|
|
726
757
|
} catch (error) {
|
|
@@ -745,6 +776,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
745
776
|
widgetRerenderTimer = undefined;
|
|
746
777
|
runningJobIds.clear();
|
|
747
778
|
externalJobBridgeRuns.clear();
|
|
779
|
+
terminalPublications.clear();
|
|
748
780
|
};
|
|
749
781
|
|
|
750
782
|
const resetJobs = (ctx?: ExtensionContext) => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AsyncJobState, SubagentState } from "../../shared/types.ts";
|
|
2
2
|
import {
|
|
3
|
-
projectAsyncStatusSnapshot,
|
|
3
|
+
projectAsyncStatusSnapshot as buildAsyncStatusSnapshot,
|
|
4
4
|
type AsyncStatusSnapshotOptions,
|
|
5
|
-
type
|
|
5
|
+
type AsyncStatusSnapshot,
|
|
6
6
|
} from "../shared/async-status-projection.ts";
|
|
7
7
|
|
|
8
8
|
export {
|
|
@@ -10,22 +10,20 @@ export {
|
|
|
10
10
|
ASYNC_STATUS_SNAPSHOT_VERSION,
|
|
11
11
|
} from "../shared/async-status-projection.ts";
|
|
12
12
|
export type {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
AsyncStatusSnapshotActivity,
|
|
14
|
+
AsyncStatusSnapshotCaps,
|
|
15
|
+
AsyncStatusSnapshotHostStep,
|
|
16
16
|
AsyncStatusSnapshotKind,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
AsyncStatusSnapshotNode,
|
|
18
|
+
AsyncStatusSnapshotOmitted,
|
|
19
19
|
AsyncStatusSnapshotOptions,
|
|
20
20
|
AsyncStatusSnapshotState,
|
|
21
|
-
|
|
21
|
+
AsyncStatusSnapshot,
|
|
22
22
|
} from "../shared/async-status-projection.ts";
|
|
23
23
|
|
|
24
24
|
export const ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX = "PI_SUBAGENT_ASYNC_JSON:";
|
|
25
25
|
|
|
26
|
-
export
|
|
27
|
-
return projectAsyncStatusSnapshot(jobs, options);
|
|
28
|
-
}
|
|
26
|
+
export { buildAsyncStatusSnapshot };
|
|
29
27
|
|
|
30
28
|
export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined, sessionId: string | null | undefined): AsyncJobState[] {
|
|
31
29
|
if (!state || !sessionId || state.currentSessionId !== sessionId) return [];
|
|
@@ -39,7 +37,7 @@ export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined
|
|
|
39
37
|
return [...jobs.values()];
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}):
|
|
40
|
+
export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshot {
|
|
43
41
|
return buildAsyncStatusSnapshot(asyncStatusSnapshotJobsForState(state, sessionId), options);
|
|
44
42
|
}
|
|
45
43
|
|