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
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { listBackgroundWorkProviders } from "../../api/background-work.ts";
|
|
3
|
+
import { ReadonlyDrainObservation } from "./readonly-drain-observation.ts";
|
|
2
4
|
import registerFanoutChildSubagentExtension from "../../extension/fanout-child.ts";
|
|
3
5
|
import registerSubagentFastModeExtension from "./fast-mode-extension.ts";
|
|
4
6
|
import registerSubagentPromptRuntime from "./subagent-prompt-runtime.ts";
|
|
5
7
|
import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
8
|
+
import type { ChildToolDiagnostic } from "./tool-availability.ts";
|
|
9
|
+
import type { ChildSessionLaunch } from "./child-session.ts";
|
|
10
|
+
import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
|
|
11
|
+
import { projectRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
|
|
6
12
|
|
|
7
13
|
/** Inline extension shape accepted by pi's resource loader (`extensionFactories`). */
|
|
8
14
|
export interface ChildHookExtension {
|
|
@@ -10,15 +16,158 @@ export interface ChildHookExtension {
|
|
|
10
16
|
factory: (pi: ExtensionAPI) => void;
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
type OwnedCapture = Required<Pick<ChildRuntimeConfig, "toolDiagnostic" | "runtimeAcknowledgements">>;
|
|
20
|
+
type PromptProof = { config: ChildRuntimeConfig; snapshot: string; factories?: ChildHookExtension["factory"][]; observeNext?: ReadonlyDrainObservation; observation?: ReadonlyDrainObservation; capture?: OwnedCapture; reporting?: { launch: ChildSessionLaunch; callback: ChildSessionLaunch["onExtensionError"] } };
|
|
21
|
+
const promptProofs = new WeakMap<ChildHookExtension["factory"], PromptProof>();
|
|
22
|
+
|
|
23
|
+
function ownedProof(hooks: ChildHookExtension[]): PromptProof | undefined {
|
|
24
|
+
const proof = hooks[0] && promptProofs.get(hooks[0].factory);
|
|
25
|
+
return proof?.factories?.length === hooks.length && proof.factories.every((factory, index) => hooks[index]?.factory === factory) ? proof : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The host's mandatory transcript reporting, owned by the existing hook certificate. */
|
|
29
|
+
export function withChildSessionErrorReporting(session: Omit<ChildSessionLaunch, "onExtensionError">, transcriptWriter: ChildTranscriptWriter | undefined): ChildSessionLaunch {
|
|
30
|
+
const launch: ChildSessionLaunch = { ...session, onExtensionError: (error) => {
|
|
31
|
+
transcriptWriter?.writeStderrLine(`Extension error (${error.extensionPath}, ${error.event}): ${error.error instanceof Error ? error.error.message : String(error.error)}`);
|
|
32
|
+
} };
|
|
33
|
+
const proof = ownedProof(launch.hooks);
|
|
34
|
+
if (proof?.capture && proof.config === launch.runtime) proof.reporting = { launch, callback: launch.onExtensionError };
|
|
35
|
+
return launch;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isReadonlyChildSessionReporting(launch: ChildSessionLaunch): boolean {
|
|
39
|
+
const proof = ownedProof(launch.hooks);
|
|
40
|
+
const descriptor = Object.getOwnPropertyDescriptor(launch, "onExtensionError");
|
|
41
|
+
if (descriptor && (!descriptor.enumerable || !("value" in descriptor))) return false;
|
|
42
|
+
return proof?.reporting ? proof.reporting.launch === launch && proof.reporting.callback === descriptor?.value : !descriptor && !("onExtensionError" in launch);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Admit only own enumerable data: inspecting descriptors must not invoke getters. */
|
|
46
|
+
function dataKeys(value: unknown): string[] | undefined {
|
|
47
|
+
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) return undefined;
|
|
48
|
+
const keys = Reflect.ownKeys(value);
|
|
49
|
+
for (const key of keys) {
|
|
50
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
|
|
51
|
+
if (typeof key !== "string" || !descriptor.enumerable || !("value" in descriptor)) return undefined;
|
|
52
|
+
}
|
|
53
|
+
return keys as string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Only reviewed data and the capture callbacks constructed below; no caller callback opt-in. */
|
|
57
|
+
function readonlyConfig(config: ChildRuntimeConfig, capture?: OwnedCapture): string | undefined {
|
|
58
|
+
const strings = ["runId", "agent", "sessionName", "forkCacheKey", "parentSessionId", "orchestratorSessionId"];
|
|
59
|
+
const numbers = ["childIndex", "depth", "maxDepth"];
|
|
60
|
+
const booleans = ["inheritProjectContext", "inheritGlobalContext", "inheritSkills"];
|
|
61
|
+
const keys = dataKeys(config);
|
|
62
|
+
if (!keys) return undefined;
|
|
63
|
+
if (capture && (config.toolDiagnostic !== capture.toolDiagnostic || config.runtimeAcknowledgements !== capture.runtimeAcknowledgements)) return undefined;
|
|
64
|
+
const waitKeys = dataKeys(config.waitTool);
|
|
65
|
+
if (!waitKeys || waitKeys.some((key) => key !== "enabled")) return undefined;
|
|
66
|
+
if (config.fast !== false || config.fanoutChild !== false || config.waitTool.enabled !== false) return undefined;
|
|
67
|
+
for (const key of keys) {
|
|
68
|
+
const value = Object.getOwnPropertyDescriptor(config, key)!.value;
|
|
69
|
+
if (["fast", "fanoutChild", "waitTool"].includes(key)) continue;
|
|
70
|
+
if (capture && (key === "toolDiagnostic" || key === "runtimeAcknowledgements")) continue;
|
|
71
|
+
if (capture && key === "requiredTools" && Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype) {
|
|
72
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
73
|
+
if (Reflect.ownKeys(descriptors).length !== value.length + 1) return undefined;
|
|
74
|
+
for (let i = 0; i < value.length; i++) {
|
|
75
|
+
const descriptor = descriptors[String(i)];
|
|
76
|
+
if (!descriptor?.enumerable || !("value" in descriptor) || !["read", "ls"].includes(descriptor.value)) return undefined;
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (strings.includes(key) && (value === undefined || typeof value === "string")) continue;
|
|
81
|
+
if (numbers.includes(key) && (value === undefined || (typeof value === "number" && Number.isInteger(value) && value >= 0))) continue;
|
|
82
|
+
if (booleans.includes(key) && (value === undefined || typeof value === "boolean")) continue;
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return JSON.stringify(config);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function noBackgroundProviders(): boolean {
|
|
89
|
+
try {
|
|
90
|
+
return listBackgroundWorkProviders().length === 0;
|
|
91
|
+
} catch { return false; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Internal proof of the captured closure/config, not its caller-controlled display name. */
|
|
95
|
+
export function isReadonlyChildHookProfile(hooks: ChildHookExtension[], config: ChildRuntimeConfig): boolean {
|
|
96
|
+
const proof = ownedProof(hooks);
|
|
97
|
+
if (!proof) return false;
|
|
98
|
+
const valid = proof.config === config && readonlyConfig(config, proof.capture) === proof.snapshot && noBackgroundProviders();
|
|
99
|
+
if (!valid) proof.observation?.deny();
|
|
100
|
+
return valid && (proof.observation?.check() ?? true);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Arm just the next installation; ordinary installations retain the original API/handlers. */
|
|
104
|
+
export function observeReadonlyChildHookDrain(hooks: ChildHookExtension[], enabled: boolean, sessionFile: string): void {
|
|
105
|
+
const proof = ownedProof(hooks);
|
|
106
|
+
if (!proof) return;
|
|
107
|
+
if (enabled) {
|
|
108
|
+
proof.observation?.deny();
|
|
109
|
+
proof.observation = undefined;
|
|
110
|
+
}
|
|
111
|
+
proof.observeNext = enabled ? new ReadonlyDrainObservation(sessionFile, () => readonlyConfig(proof.config, proof.capture) === proof.snapshot && noBackgroundProviders()) : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function captureReadonlyChildDrain(hooks: ChildHookExtension[]): (() => boolean) {
|
|
115
|
+
const proof = ownedProof(hooks);
|
|
116
|
+
const observation = proof?.observation;
|
|
117
|
+
return () => !!observation && proof?.observation === observation && observation.settled();
|
|
118
|
+
}
|
|
119
|
+
|
|
13
120
|
/**
|
|
14
121
|
* The child-side hooks pi-subagents installs in every child, keyed off the
|
|
15
122
|
* launch config. The registrations live in `subagent-prompt-runtime.ts`,
|
|
16
123
|
* `fast-mode-extension.ts`, and `fanout-child.ts`.
|
|
17
124
|
*/
|
|
18
125
|
export function createChildHooks(config: ChildRuntimeConfig): ChildHookExtension[] {
|
|
126
|
+
return childHooks(config);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Launch-owned bookkeeping, paired with the same private hook certificate (no callback registration API). */
|
|
130
|
+
export function createCapturedChildHooks(config: ChildRuntimeConfig, runner = false) {
|
|
131
|
+
let diagnostic: ChildToolDiagnostic | undefined;
|
|
132
|
+
let acknowledgedIds: string[] | undefined;
|
|
133
|
+
let completionIntentContext: Pick<ExtensionContext, "model" | "modelRegistry"> | undefined;
|
|
134
|
+
const capture: OwnedCapture = {
|
|
135
|
+
toolDiagnostic: (value) => { diagnostic = value; },
|
|
136
|
+
runtimeAcknowledgements: (ids) => { acknowledgedIds = ids; },
|
|
137
|
+
};
|
|
138
|
+
Object.assign(config, capture);
|
|
139
|
+
const hooks = childHooks(config, capture);
|
|
140
|
+
if (runner) {
|
|
141
|
+
hooks.push({ name: "pi-subagents:completion-intent", factory: (pi) => pi.on("session_start", (_event, childCtx) => {
|
|
142
|
+
// Retain only attempt model services, not the live child session.
|
|
143
|
+
completionIntentContext = { model: childCtx.model, modelRegistry: childCtx.modelRegistry };
|
|
144
|
+
}) });
|
|
145
|
+
const proof = promptProofs.get(hooks[0]!.factory);
|
|
146
|
+
if (proof) proof.factories = hooks.map((hook) => hook.factory);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
hooks,
|
|
150
|
+
completionIntentContext: () => completionIntentContext,
|
|
151
|
+
toolDiagnostic: () => diagnostic,
|
|
152
|
+
runtimeAcknowledgedExtensions: () => acknowledgedIds ? projectRuntimeAcknowledgedExtensions(acknowledgedIds) : undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function childHooks(config: ChildRuntimeConfig, capture?: OwnedCapture): ChildHookExtension[] {
|
|
157
|
+
const snapshot = readonlyConfig(config, capture);
|
|
158
|
+
const proof: PromptProof | undefined = snapshot === undefined ? undefined : { config, snapshot, capture };
|
|
19
159
|
const hooks: ChildHookExtension[] = [
|
|
20
|
-
{ name: "pi-subagents:prompt-runtime", factory:
|
|
160
|
+
{ name: "pi-subagents:prompt-runtime", factory: function promptRuntime(pi) {
|
|
161
|
+
if (!proof?.observeNext) return registerSubagentPromptRuntime(pi, config);
|
|
162
|
+
proof.observation = proof.observeNext;
|
|
163
|
+
proof.observeNext = undefined;
|
|
164
|
+
registerSubagentPromptRuntime(pi, config, proof.observation);
|
|
165
|
+
} },
|
|
21
166
|
];
|
|
167
|
+
if (proof) {
|
|
168
|
+
proof.factories = hooks.map((hook) => hook.factory);
|
|
169
|
+
promptProofs.set(hooks[0]!.factory, proof);
|
|
170
|
+
}
|
|
22
171
|
if (config.fast) hooks.push({ name: "pi-subagents:fast-mode", factory: (pi) => registerSubagentFastModeExtension(pi) });
|
|
23
172
|
if (config.fanoutChild) hooks.push({ name: "pi-subagents:fanout-child", factory: (pi) => registerFanoutChildSubagentExtension(pi, config) });
|
|
24
173
|
return hooks;
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import type { ChildWatchdogConfig, ChildWatchdogStatusEvent } from "../../watchdog/child-status.ts";
|
|
10
11
|
import type { ThinkingLevel } from "../../shared/model-info.ts";
|
|
11
12
|
import { intersectThinkingCeilings } from "../../shared/thinking-ceiling.ts";
|
|
12
13
|
import {
|
|
13
14
|
resolveChildDepth,
|
|
14
|
-
type
|
|
15
|
+
type LaunchResolvedChildExtensions,
|
|
15
16
|
type ResolvedToolBudget,
|
|
16
17
|
type RunFanoutBudgetDescriptor,
|
|
17
18
|
} from "../../shared/types.ts";
|
|
@@ -20,8 +21,7 @@ import type { McpRuntimeSnapshotHost } from "./mcp-direct-tool-allowlist.ts";
|
|
|
20
21
|
import type { PermissionRules } from "./permissions.ts";
|
|
21
22
|
import type { StructuredOutputRuntime } from "./structured-output.ts";
|
|
22
23
|
import type { ChildToolDiagnostic } from "./tool-availability.ts";
|
|
23
|
-
import type {
|
|
24
|
-
import { projectRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
|
|
24
|
+
import type { RuntimeAcknowledgedChildExtensions } from "../../shared/types.ts";
|
|
25
25
|
import { encodeExtensionBindings, PI_SUBAGENT_EXTENSION_BINDINGS_ENV, type ExtensionBindings } from "./extension-bindings.ts";
|
|
26
26
|
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "./capability-ceiling.ts";
|
|
27
27
|
import {
|
|
@@ -32,7 +32,8 @@ import {
|
|
|
32
32
|
type PiLaunchToolPlan,
|
|
33
33
|
} from "./child-tool-plan.ts";
|
|
34
34
|
import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
35
|
-
import {
|
|
35
|
+
import { createCapturedChildHooks, withChildSessionErrorReporting } from "./child-hooks.ts";
|
|
36
|
+
import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
|
|
36
37
|
import type { ChildSessionLaunch, ChildSessionStorage } from "./child-session.ts";
|
|
37
38
|
|
|
38
39
|
/** Environment variable pi-mcp-adapter reads for the tools a child may expose. */
|
|
@@ -114,9 +115,10 @@ export interface BuildInProcessChildLaunchInput {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
export interface InProcessChildCapture {
|
|
118
|
+
completionIntentContext?(): Pick<ExtensionContext, "model" | "modelRegistry"> | undefined;
|
|
117
119
|
structuredOutput(): { called: boolean; value?: unknown; acceptanceReport?: unknown; acceptanceReportProvided: boolean };
|
|
118
120
|
toolDiagnostic(): ChildToolDiagnostic | undefined;
|
|
119
|
-
runtimeAcknowledgedExtensions():
|
|
121
|
+
runtimeAcknowledgedExtensions(): RuntimeAcknowledgedChildExtensions | undefined;
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
export interface InProcessChildLaunch {
|
|
@@ -124,11 +126,16 @@ export interface InProcessChildLaunch {
|
|
|
124
126
|
config: ChildRuntimeConfig;
|
|
125
127
|
session: Omit<ChildSessionLaunch, "onExtensionError">;
|
|
126
128
|
capture: InProcessChildCapture;
|
|
127
|
-
launchResolvedExtensions:
|
|
129
|
+
launchResolvedExtensions: LaunchResolvedChildExtensions;
|
|
128
130
|
warnings: string[];
|
|
129
131
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
/** Actual host create-input boundary. Evidence remains explicitly opt-in and dormant in production. */
|
|
135
|
+
export function createReportedChildSessionInput(launch: InProcessChildLaunch, transcriptWriter?: ChildTranscriptWriter): ChildSessionLaunch {
|
|
136
|
+
return withChildSessionErrorReporting(launch.session, transcriptWriter);
|
|
137
|
+
}
|
|
138
|
+
|
|
132
139
|
/** Escape XML-significant characters in a string for safe attribute interpolation. */
|
|
133
140
|
function escapeXmlAttr(value: string): string {
|
|
134
141
|
return value
|
|
@@ -214,8 +221,6 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
214
221
|
let structuredAcceptanceReport: unknown;
|
|
215
222
|
let structuredCalled = false;
|
|
216
223
|
let structuredAcceptanceProvided = false;
|
|
217
|
-
let toolDiagnostic: ChildToolDiagnostic | undefined;
|
|
218
|
-
let acknowledgedIds: string[] | undefined;
|
|
219
224
|
|
|
220
225
|
const config: ChildRuntimeConfig = {
|
|
221
226
|
...(input.runId ? { runId: input.runId } : {}),
|
|
@@ -264,10 +269,9 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
264
269
|
: {}),
|
|
265
270
|
...(toolPlan.requiredChildTools.length > 0 ? { requiredTools: toolPlan.requiredChildTools } : {}),
|
|
266
271
|
...(toolPlan.effectiveMcpTools.length > 0 ? { mcpDirectTools: toolPlan.effectiveMcpTools } : {}),
|
|
267
|
-
toolDiagnostic: (diagnostic) => { toolDiagnostic = diagnostic; },
|
|
268
|
-
runtimeAcknowledgements: (ids) => { acknowledgedIds = ids; },
|
|
269
272
|
fast: input.fast === true,
|
|
270
273
|
};
|
|
274
|
+
const capturedHooks = createCapturedChildHooks(config, input.host === "runner");
|
|
271
275
|
|
|
272
276
|
const extensionPaths = toolPlan.extensionArgs.filter((extensionPath) => !isSubagentRuntimeExtensionPath(extensionPath));
|
|
273
277
|
const ambientExtensions = input.host === "runner" && !toolPlan.disableAmbientExtensions;
|
|
@@ -288,7 +292,7 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
288
292
|
...(!toolPlan.explicitToolAllowlist && toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
|
|
289
293
|
extensionPaths,
|
|
290
294
|
ambientExtensions,
|
|
291
|
-
hooks:
|
|
295
|
+
hooks: capturedHooks.hooks,
|
|
292
296
|
...(input.host === "runner" ? { processEnv: childProcessEnv(input, toolPlan) } : {}),
|
|
293
297
|
runtime: config,
|
|
294
298
|
noSkills: !input.inheritSkills,
|
|
@@ -303,9 +307,10 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
303
307
|
config,
|
|
304
308
|
session,
|
|
305
309
|
capture: {
|
|
310
|
+
completionIntentContext: capturedHooks.completionIntentContext,
|
|
306
311
|
structuredOutput: () => ({ called: structuredCalled, value: structuredValue, acceptanceReport: structuredAcceptanceReport, acceptanceReportProvided: structuredAcceptanceProvided }),
|
|
307
|
-
toolDiagnostic:
|
|
308
|
-
runtimeAcknowledgedExtensions:
|
|
312
|
+
toolDiagnostic: capturedHooks.toolDiagnostic,
|
|
313
|
+
runtimeAcknowledgedExtensions: capturedHooks.runtimeAcknowledgedExtensions,
|
|
309
314
|
},
|
|
310
315
|
launchResolvedExtensions,
|
|
311
316
|
warnings: toolPlan.warnings,
|
|
@@ -12,6 +12,14 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
|
12
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { getAgentDir } from "../../shared/utils.ts";
|
|
14
14
|
import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
15
|
+
import { prepareReadonlySessionEvidence } from "./readonly-session-evidence.ts";
|
|
16
|
+
import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
|
|
17
|
+
|
|
18
|
+
// Private runtime authority for host continuation planning; injected factories have none.
|
|
19
|
+
const readonlyModels = new WeakMap<ChildSession, { current: ModelInfo; resolve(reference: string): ModelInfo | undefined; requestBytes: number }>();
|
|
20
|
+
export function getReadonlyChildModels(child: ChildSession) {
|
|
21
|
+
return readonlyModels.get(child);
|
|
22
|
+
}
|
|
15
23
|
|
|
16
24
|
export interface ChildSessionEvent {
|
|
17
25
|
type: string;
|
|
@@ -115,17 +123,6 @@ export interface DefaultChildSessionFactoryOptions {
|
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
type ModelRuntimeInstance = Awaited<ReturnType<PiCodingAgentModule["ModelRuntime"]["create"]>>;
|
|
118
|
-
type QueuedProviderRegistration = { name: string; config: Parameters<ModelRuntimeInstance["registerProvider"]>[1]; extensionPath: string };
|
|
119
|
-
type QueuedNativeProviderRegistration = { provider: Parameters<ModelRuntimeInstance["registerNativeProvider"]>[0]; extensionPath: string };
|
|
120
|
-
|
|
121
|
-
interface LoaderWithExtensions {
|
|
122
|
-
getExtensions(): {
|
|
123
|
-
runtime: {
|
|
124
|
-
pendingProviderRegistrations: QueuedProviderRegistration[];
|
|
125
|
-
pendingNativeProviderRegistrations: QueuedNativeProviderRegistration[];
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
126
|
|
|
130
127
|
/** One launch at a time from env application through `session_start`, so parallel launches never observe each other's `processEnv` while their extensions load and start. */
|
|
131
128
|
let loading: Promise<unknown> = Promise.resolve();
|
|
@@ -151,25 +148,29 @@ function applyProcessEnv(values: Record<string, string | undefined> | undefined)
|
|
|
151
148
|
}
|
|
152
149
|
}
|
|
153
150
|
|
|
154
|
-
function flushQueuedProviderRegistrations(loader:
|
|
151
|
+
async function flushQueuedProviderRegistrations(loader: InstanceType<PiCodingAgentModule["DefaultResourceLoader"]>, modelRuntime: ModelRuntimeInstance, onError: ((error: ChildSessionExtensionError) => void) | undefined): Promise<void> {
|
|
155
152
|
if (!("getExtensions" in loader) || typeof loader.getExtensions !== "function") return;
|
|
156
|
-
const { runtime } =
|
|
157
|
-
|
|
153
|
+
const { runtime } = loader.getExtensions();
|
|
154
|
+
let registered = false;
|
|
155
|
+
for (const { name, config, extensionPath } of runtime.pendingProviderRegistrations ?? []) {
|
|
158
156
|
try {
|
|
159
157
|
modelRuntime.registerProvider(name, config);
|
|
158
|
+
registered = true;
|
|
160
159
|
} catch (error) {
|
|
161
160
|
onError?.({ extensionPath, event: "register_provider", error });
|
|
162
161
|
}
|
|
163
162
|
}
|
|
164
|
-
runtime.pendingProviderRegistrations = [];
|
|
165
|
-
for (const { provider, extensionPath } of runtime.pendingNativeProviderRegistrations) {
|
|
163
|
+
if (Array.isArray(runtime.pendingProviderRegistrations)) runtime.pendingProviderRegistrations = [];
|
|
164
|
+
for (const { provider, extensionPath } of runtime.pendingNativeProviderRegistrations ?? []) {
|
|
166
165
|
try {
|
|
167
166
|
modelRuntime.registerNativeProvider(provider);
|
|
167
|
+
registered = true;
|
|
168
168
|
} catch (error) {
|
|
169
169
|
onError?.({ extensionPath, event: "register_provider", error });
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
runtime.pendingNativeProviderRegistrations = [];
|
|
172
|
+
if (Array.isArray(runtime.pendingNativeProviderRegistrations)) runtime.pendingNativeProviderRegistrations = [];
|
|
173
|
+
if (registered) await modelRuntime.refresh({ allowNetwork: false });
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
/**
|
|
@@ -192,10 +193,13 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
192
193
|
};
|
|
193
194
|
return {
|
|
194
195
|
async create(launch) {
|
|
196
|
+
const observeReadonly = prepareReadonlySessionEvidence(launch);
|
|
195
197
|
const pi = await loadPiCodingAgent();
|
|
196
198
|
const modelRuntime = await sharedRuntime(pi);
|
|
197
199
|
const agentDir = getAgentDir();
|
|
198
200
|
const settingsManager = pi.SettingsManager.create(launch.cwd, agentDir);
|
|
201
|
+
// Headless sessions skip Pi's CLI theme setup; extensions still need ctx.ui.theme.
|
|
202
|
+
if (typeof pi.initTheme === "function") pi.initTheme(settingsManager.getTheme());
|
|
199
203
|
const loader = new pi.DefaultResourceLoader({
|
|
200
204
|
cwd: launch.cwd,
|
|
201
205
|
agentDir,
|
|
@@ -213,8 +217,11 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
213
217
|
const open = async () => {
|
|
214
218
|
applyProcessEnv(launch.processEnv);
|
|
215
219
|
if (!resetExtensionCacheOnReload(loader) && (launch.ambientExtensions || launch.extensionPaths.length)) launch.onExtensionError?.({ extensionPath: "<loader>", event: "load", error: new Error("pi's extension cache reset is unavailable; extensions loaded into this child share module state with other sessions in this process.") });
|
|
216
|
-
|
|
217
|
-
|
|
220
|
+
observeReadonly?.loadingHooks(true);
|
|
221
|
+
try { await loader.reload(); } finally { observeReadonly?.loadingHooks(false); }
|
|
222
|
+
await flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError);
|
|
223
|
+
// No await between receipt validation and the SDK's permissive file open.
|
|
224
|
+
observeReadonly?.beforeOpen();
|
|
218
225
|
const sessionManager = launch.storage.kind === "file"
|
|
219
226
|
? pi.SessionManager.open(launch.storage.sessionFile, undefined, launch.cwd)
|
|
220
227
|
: launch.storage.kind === "dir"
|
|
@@ -222,6 +229,7 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
222
229
|
: launch.storage.kind === "memory"
|
|
223
230
|
? pi.SessionManager.inMemory(launch.cwd)
|
|
224
231
|
: pi.SessionManager.create(launch.cwd);
|
|
232
|
+
observeReadonly?.opened(sessionManager);
|
|
225
233
|
const resolvedModel = launch.model
|
|
226
234
|
? pi.resolveCliModel({ cliModel: launch.model, modelRuntime })
|
|
227
235
|
: undefined;
|
|
@@ -253,6 +261,9 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
253
261
|
const opened = loading.catch(() => {}).then(open);
|
|
254
262
|
loading = opened;
|
|
255
263
|
const session = await opened;
|
|
264
|
+
let evidence: ReturnType<NonNullable<typeof observeReadonly>["observe"]>;
|
|
265
|
+
try { evidence = observeReadonly?.observe(pi, modelRuntime, session); }
|
|
266
|
+
catch (error) { session.dispose(); throw error; }
|
|
256
267
|
let pending: Promise<void> | undefined;
|
|
257
268
|
// pi's own hosts emit `session_shutdown` before disposing a session so the
|
|
258
269
|
// extensions loaded into it (ambient extensions included) release their
|
|
@@ -260,19 +271,29 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
260
271
|
const shutdown = async (): Promise<void> => {
|
|
261
272
|
try {
|
|
262
273
|
const runner = session.extensionRunner;
|
|
263
|
-
if (runner.hasHandlers("session_shutdown"))
|
|
274
|
+
if (runner.hasHandlers("session_shutdown")) {
|
|
275
|
+
evidence?.beforeShutdown();
|
|
276
|
+
const settled = await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }).then(() => true), new Promise<false>((resolve) => setTimeout(() => resolve(false), shutdownTimeoutMs).unref?.())]);
|
|
277
|
+
if (!settled) evidence?.invalidate();
|
|
278
|
+
}
|
|
264
279
|
} catch (error) {
|
|
280
|
+
evidence?.invalidate();
|
|
265
281
|
launch.onExtensionError?.({ extensionPath: "<session>", event: "session_shutdown", error });
|
|
266
282
|
} finally {
|
|
267
283
|
session.dispose();
|
|
284
|
+
evidence?.finish(child);
|
|
268
285
|
}
|
|
269
286
|
};
|
|
270
287
|
const child: ChildSession = {
|
|
271
288
|
subscribe: (listener) => session.subscribe((event) => listener(event as unknown as ChildSessionEvent)),
|
|
272
|
-
prompt: (text) =>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
289
|
+
prompt: (text) => {
|
|
290
|
+
if (!evidence) return session.prompt(text);
|
|
291
|
+
try { evidence.start(); } catch (error) { return Promise.reject(error); }
|
|
292
|
+
return session.prompt(text).then(() => evidence?.settled(), (error) => { evidence?.invalidate(); throw error; });
|
|
293
|
+
},
|
|
294
|
+
steer: (text) => { evidence?.invalidate(); return session.steer(text); },
|
|
295
|
+
followUp: (text) => { evidence?.invalidate(); return session.followUp(text); },
|
|
296
|
+
abort: () => { evidence?.invalidate(); return session.abort(); },
|
|
276
297
|
dispose: () => {
|
|
277
298
|
if (!pending) {
|
|
278
299
|
live.delete(child);
|
|
@@ -288,6 +309,16 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
288
309
|
get sessionId() { return session.sessionId; },
|
|
289
310
|
get modelId() { return session.model ? `${session.model.provider}/${session.model.id}` : undefined; },
|
|
290
311
|
};
|
|
312
|
+
if (evidence && session.model) readonlyModels.set(child, {
|
|
313
|
+
current: toModelInfo(session.model),
|
|
314
|
+
requestBytes: Buffer.byteLength(session.systemPrompt) + Buffer.byteLength(JSON.stringify(session.agent.state.tools)),
|
|
315
|
+
resolve(reference) {
|
|
316
|
+
try {
|
|
317
|
+
const resolved = pi.resolveCliModel({ cliModel: reference, modelRuntime });
|
|
318
|
+
return !resolved.error && resolved.model ? toModelInfo(resolved.model) : undefined;
|
|
319
|
+
} catch { return undefined; }
|
|
320
|
+
},
|
|
321
|
+
});
|
|
291
322
|
live.add(child);
|
|
292
323
|
return child;
|
|
293
324
|
},
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
TEMP_ROOT_DIR,
|
|
18
18
|
type JsonSchemaObject,
|
|
19
|
-
type
|
|
19
|
+
type LaunchResolvedChildExtensions,
|
|
20
20
|
} from "../../shared/types.ts";
|
|
21
21
|
import { THINKING_LEVELS } from "../../shared/model-info.ts";
|
|
22
22
|
import { getAgentDir } from "../../shared/utils.ts";
|
|
@@ -214,7 +214,7 @@ export function projectLaunchResolvedChildExtensions(
|
|
|
214
214
|
| "extensionArgs"
|
|
215
215
|
| "disableAmbientExtensions"
|
|
216
216
|
>,
|
|
217
|
-
):
|
|
217
|
+
): LaunchResolvedChildExtensions {
|
|
218
218
|
const runtime = boundedExtensionIdentifiers(toolPlan.runtimeExtensions);
|
|
219
219
|
const configured = boundedExtensionIdentifiers(toolPlan.configuredExtensions);
|
|
220
220
|
const effective = boundedExtensionIdentifiers(toolPlan.extensionArgs);
|
|
@@ -26,7 +26,7 @@ export function planCompletionEvidence(input: {
|
|
|
26
26
|
mutationAttemptObserved: boolean;
|
|
27
27
|
mutationEvidence?: TrackedMutationEvidence;
|
|
28
28
|
arbiterRescued?: boolean;
|
|
29
|
-
|
|
29
|
+
agentContractEnabled: boolean;
|
|
30
30
|
}): CompletionEvidencePlan {
|
|
31
31
|
const guardBlocked = input.guard?.blocked === true;
|
|
32
32
|
const guardTriggered = input.guardTriggered
|
|
@@ -59,7 +59,7 @@ export function planCompletionEvidence(input: {
|
|
|
59
59
|
mutationExpected,
|
|
60
60
|
mutationAttempted,
|
|
61
61
|
fileMutation,
|
|
62
|
-
legacyFailureError: guardTriggered && !input.
|
|
62
|
+
legacyFailureError: guardTriggered && !input.agentContractEnabled
|
|
63
63
|
? MISSING_IMPLEMENTATION_MUTATION_ERROR
|
|
64
64
|
: undefined,
|
|
65
65
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AsyncStatus,
|
|
3
|
-
|
|
3
|
+
HostStepNode,
|
|
4
4
|
HostStepState,
|
|
5
5
|
HostStepVerdict,
|
|
6
6
|
WorkflowGraphNode,
|
|
@@ -57,7 +57,7 @@ function assertTimestamp(value: unknown, field: string, source: string, required
|
|
|
57
57
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`Invalid host step '${source}': ${field} must be a non-negative safe integer.`);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function assertFreshness(value: unknown, source: string): asserts value is
|
|
60
|
+
function assertFreshness(value: unknown, source: string): asserts value is HostStepNode["freshness"] {
|
|
61
61
|
if (!isRecord(value)) throw new Error(`Invalid host step '${source}': freshness must be an object.`);
|
|
62
62
|
const unknownFields = Object.keys(value).filter((field) => !FRESHNESS_FIELDS.has(field));
|
|
63
63
|
if (unknownFields.length > 0) throw new Error(`Invalid host step '${source}': freshness has unsupported fields: ${unknownFields.join(", ")}.`);
|
|
@@ -70,7 +70,7 @@ function assertFreshness(value: unknown, source: string): asserts value is HostS
|
|
|
70
70
|
* Validate the persisted host-step contract. This deliberately rejects unknown
|
|
71
71
|
* fields and unbounded values so status and receipt loaders fail closed.
|
|
72
72
|
*/
|
|
73
|
-
export function assertHostStepNode(value: unknown, source = "status"): asserts value is
|
|
73
|
+
export function assertHostStepNode(value: unknown, source = "status"): asserts value is HostStepNode {
|
|
74
74
|
if (!isRecord(value)) throw new Error(`Invalid host step '${source}': expected an object.`);
|
|
75
75
|
const unknownFields = Object.keys(value).filter((field) => !HOST_STEP_FIELDS.has(field));
|
|
76
76
|
if (unknownFields.length > 0) throw new Error(`Invalid host step '${source}': unsupported fields: ${unknownFields.join(", ")}.`);
|
|
@@ -105,7 +105,7 @@ export function assertHostStepNode(value: unknown, source = "status"): asserts v
|
|
|
105
105
|
assertTimestamp(value.deadlineAt, "deadlineAt", source);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export function parseHostStepNode(value: unknown, source = "status"):
|
|
108
|
+
export function parseHostStepNode(value: unknown, source = "status"): HostStepNode {
|
|
109
109
|
assertHostStepNode(value, source);
|
|
110
110
|
return {
|
|
111
111
|
...value,
|
|
@@ -113,7 +113,7 @@ export function parseHostStepNode(value: unknown, source = "status"): HostStepNo
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export function assertUniqueHostStepIds(hostSteps: readonly
|
|
116
|
+
export function assertUniqueHostStepIds(hostSteps: readonly HostStepNode[], source = "status"): void {
|
|
117
117
|
const ids = new Set<string>();
|
|
118
118
|
for (const hostStep of hostSteps) {
|
|
119
119
|
if (ids.has(hostStep.id)) throw new Error(`Invalid host step '${source}': duplicate host step id '${hostStep.id}'.`);
|
|
@@ -122,11 +122,11 @@ export function assertUniqueHostStepIds(hostSteps: readonly HostStepNodeV1[], so
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/** Return only valid host nodes so an untrusted in-memory projection fails closed. */
|
|
125
|
-
export function validHostStepNodes(graph: WorkflowGraphSnapshot | undefined):
|
|
125
|
+
export function validHostStepNodes(graph: WorkflowGraphSnapshot | undefined): HostStepNode[] {
|
|
126
126
|
const nodes = graph?.nodes ?? [];
|
|
127
127
|
const nodeIdCounts = new Map<string, number>();
|
|
128
128
|
for (const node of nodes) nodeIdCounts.set(node.id, (nodeIdCounts.get(node.id) ?? 0) + 1);
|
|
129
|
-
const hostSteps:
|
|
129
|
+
const hostSteps: HostStepNode[] = [];
|
|
130
130
|
for (const [index, node] of nodes.entries()) {
|
|
131
131
|
if (node.kind !== "host-step") continue;
|
|
132
132
|
try {
|
|
@@ -147,7 +147,7 @@ export function assertWorkflowGraphHostSteps(graph: WorkflowGraphSnapshot | unde
|
|
|
147
147
|
if (!Array.isArray(graph.nodes)) throw new Error(`Invalid host step '${source}.workflowGraph': nodes must be an array.`);
|
|
148
148
|
const hostStepCount = graph.nodes.filter((node) => node.kind === "host-step").length;
|
|
149
149
|
if (hostStepCount > HOST_STEP_MAX_COUNT) throw new Error(`Invalid host step '${source}': workflowGraph contains more than ${HOST_STEP_MAX_COUNT} host steps.`);
|
|
150
|
-
const hostSteps:
|
|
150
|
+
const hostSteps: HostStepNode[] = [];
|
|
151
151
|
for (const [index, node] of graph.nodes.entries()) {
|
|
152
152
|
if (node.kind !== "host-step") continue;
|
|
153
153
|
const hostStep = parseHostStepNode(node.hostStep, `${source}.workflowGraph.nodes[${index}].hostStep`);
|
|
@@ -161,7 +161,7 @@ export function assertWorkflowGraphHostSteps(graph: WorkflowGraphSnapshot | unde
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
export function hostStepWorkflowNode(hostStep:
|
|
164
|
+
export function hostStepWorkflowNode(hostStep: HostStepNode): WorkflowGraphNode {
|
|
165
165
|
assertHostStepNode(hostStep, hostStep.id);
|
|
166
166
|
return {
|
|
167
167
|
id: hostStep.id,
|
|
@@ -172,7 +172,7 @@ export function hostStepWorkflowNode(hostStep: HostStepNodeV1): WorkflowGraphNod
|
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
function workflowNodeStatus(hostStep:
|
|
175
|
+
function workflowNodeStatus(hostStep: HostStepNode): WorkflowNodeStatus {
|
|
176
176
|
if (hostStep.state === "pending") return "pending";
|
|
177
177
|
if (hostStep.state === "running") return "running";
|
|
178
178
|
if (hostStep.state === "cancelled") return "stopped";
|
|
@@ -187,7 +187,7 @@ function workflowNodeStatus(hostStep: HostStepNodeV1): WorkflowNodeStatus {
|
|
|
187
187
|
*/
|
|
188
188
|
export function upsertHostStep(input: {
|
|
189
189
|
status: AsyncStatus;
|
|
190
|
-
hostStep:
|
|
190
|
+
hostStep: HostStepNode;
|
|
191
191
|
persist: (status: AsyncStatus) => void;
|
|
192
192
|
}): AsyncStatus {
|
|
193
193
|
const hostStep = parseHostStepNode(input.hostStep, input.hostStep.id);
|