@osolmaz/pi-workflows 0.13.4 → 0.15.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/README.md +146 -160
- package/dist/builtins/autoimplement.workflow.js +3 -1
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/change-verification.workflow.js +7 -2
- package/dist/builtins/change-verification.workflow.js.map +1 -1
- package/dist/builtins/metadata.d.ts +22 -0
- package/dist/builtins/metadata.js +10 -0
- package/dist/builtins/metadata.js.map +1 -0
- package/dist/builtins/monitor.workflow.js +4 -1
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.js +4 -1
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/builtins/workspace-preparation.workflow.js +3 -1
- package/dist/builtins/workspace-preparation.workflow.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -2
- package/dist/controllers/index.js +0 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +85 -31
- package/dist/controllers/sqlite.js +541 -126
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +1 -0
- package/dist/extension/controller-command.d.ts +22 -0
- package/dist/extension/controller-command.js +32 -0
- package/dist/extension/controller-command.js.map +1 -0
- package/dist/extension/index.d.ts +2 -9
- package/dist/extension/index.js +876 -2928
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/message-card.d.ts +13 -0
- package/dist/extension/message-card.js +46 -0
- package/dist/extension/message-card.js.map +1 -0
- package/dist/extension/step-message.d.ts +5 -6
- package/dist/extension/step-message.js +41 -52
- package/dist/extension/step-message.js.map +1 -1
- package/dist/host/child-worker-supervisor.d.ts +50 -0
- package/dist/host/child-worker-supervisor.js +159 -0
- package/dist/host/child-worker-supervisor.js.map +1 -0
- package/dist/host/client.d.ts +48 -0
- package/dist/host/client.js +216 -0
- package/dist/host/client.js.map +1 -0
- package/dist/host/controller-worker-entry.d.ts +2 -0
- package/dist/host/controller-worker-entry.js +244 -0
- package/dist/host/controller-worker-entry.js.map +1 -0
- package/dist/host/controller-worker-protocol.d.ts +36 -0
- package/dist/host/controller-worker-protocol.js +49 -0
- package/dist/host/controller-worker-protocol.js.map +1 -0
- package/dist/host/controller-worker-supervisor.d.ts +21 -0
- package/dist/host/controller-worker-supervisor.js +54 -0
- package/dist/host/controller-worker-supervisor.js.map +1 -0
- package/dist/host/host-entry.d.ts +2 -0
- package/dist/host/host-entry.js +23 -0
- package/dist/host/host-entry.js.map +1 -0
- package/dist/host/processes.d.ts +17 -12
- package/dist/host/processes.js +154 -50
- package/dist/host/processes.js.map +1 -1
- package/dist/host/protocol.d.ts +38 -0
- package/dist/host/protocol.js +156 -0
- package/dist/host/protocol.js.map +1 -0
- package/dist/host/resolver-entry.d.ts +49 -0
- package/dist/host/resolver-entry.js +149 -0
- package/dist/host/resolver-entry.js.map +1 -0
- package/dist/host/rpc-executor.d.ts +14 -3
- package/dist/host/rpc-executor.js +63 -30
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +95 -29
- package/dist/host/runner.js +2311 -348
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +174 -0
- package/dist/host/state.js +689 -0
- package/dist/host/state.js.map +1 -0
- package/dist/host/worker-entry.d.ts +10 -0
- package/dist/host/worker-entry.js +446 -0
- package/dist/host/worker-entry.js.map +1 -0
- package/dist/host/worker-protocol.d.ts +31 -0
- package/dist/host/worker-protocol.js +122 -0
- package/dist/host/worker-protocol.js.map +1 -0
- package/dist/host/worker-store.d.ts +80 -0
- package/dist/host/worker-store.js +91 -0
- package/dist/host/worker-store.js.map +1 -0
- package/dist/host/worker-supervisor.d.ts +22 -0
- package/dist/host/worker-supervisor.js +54 -0
- package/dist/host/worker-supervisor.js.map +1 -0
- package/dist/render/canvas.d.ts +2 -1
- package/dist/render/canvas.js +40 -15
- package/dist/render/canvas.js.map +1 -1
- package/dist/render/graph-render.d.ts +7 -3
- package/dist/render/graph-render.js +125 -74
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.js +3 -1
- package/dist/state/database.js.map +1 -1
- package/dist/state/index.d.ts +1 -0
- package/dist/state/index.js +1 -0
- package/dist/state/index.js.map +1 -1
- package/dist/state/prune.js +36 -10
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.d.ts +1 -1
- package/dist/state/schema.js +171 -3
- package/dist/state/schema.js.map +1 -1
- package/dist/state/viewer.d.ts +46 -0
- package/dist/state/viewer.js +249 -0
- package/dist/state/viewer.js.map +1 -0
- package/dist/viewer/cli.d.ts +1 -1
- package/dist/viewer/cli.js +47 -18
- package/dist/viewer/cli.js.map +1 -1
- package/dist/workflows/composition.js +25 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +3 -1
- package/dist/workflows/definition.js +25 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +1 -0
- package/dist/workflows/engine.js +157 -42
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +3 -1
- package/dist/workflows/errors.js +4 -7
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +3 -0
- package/dist/workflows/human-decision.js +31 -0
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +17 -6
- package/dist/workflows/schema.js.map +1 -1
- package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
- package/dist/{viewer → workflows}/session-reducer.js +4 -0
- package/dist/workflows/session-reducer.js.map +1 -0
- package/dist/workflows/store.d.ts +87 -1
- package/dist/workflows/store.js +892 -93
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +0 -22
- package/dist/workflows/tool-input.js +1 -44
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +36 -4
- package/docs/2026-08-25-workflow-follow-ups.md +8 -6
- package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
- package/docs/CONTROLLERS.md +11 -11
- package/docs/DEFERRED_TURNS.md +61 -27
- package/docs/HUMAN_DECISIONS.md +12 -4
- package/docs/SQLITE_STATE.md +42 -8
- package/docs/WORKFLOW_HOST.md +452 -0
- package/docs/development.md +46 -30
- package/docs/live-replay-protocol.md +129 -100
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +34 -8
- package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +399 -0
- package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
- package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
- package/docs/plans/tui-viewer-implementation-plan.md +5 -0
- package/docs/tui-viewer.md +18 -3
- package/docs/workflows.md +164 -156
- package/examples/workflows/command-batch.workflow.ts +2 -0
- package/examples/workflows/shell.workflow.ts +2 -1
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/skills/autodoc/SKILL.md +1 -1
- package/skills/autoimplement/SKILL.md +1 -1
- package/skills/autoplan/SKILL.md +1 -1
- package/skills/pi-workflows/SKILL.md +2 -0
- package/src/builtins/autoimplement.workflow.ts +3 -0
- package/src/builtins/change-verification.workflow.ts +7 -2
- package/src/builtins/metadata.ts +9 -0
- package/src/builtins/monitor.workflow.ts +4 -0
- package/src/builtins/sanity-check.workflow.ts +4 -0
- package/src/builtins/workspace-preparation.workflow.ts +3 -1
- package/src/controllers/index.ts +3 -5
- package/src/controllers/sqlite.ts +929 -225
- package/src/controllers/store.ts +1 -0
- package/src/extension/controller-command.ts +45 -0
- package/src/extension/index.ts +948 -3506
- package/src/extension/message-card.ts +61 -0
- package/src/extension/step-message.ts +58 -63
- package/src/host/child-worker-supervisor.ts +183 -0
- package/src/host/client.ts +293 -0
- package/src/host/controller-worker-entry.ts +311 -0
- package/src/host/controller-worker-protocol.ts +104 -0
- package/src/host/controller-worker-supervisor.ts +79 -0
- package/src/host/host-entry.ts +23 -0
- package/src/host/processes.ts +171 -54
- package/src/host/protocol.ts +196 -0
- package/src/host/resolver-entry.ts +241 -0
- package/src/host/rpc-executor.ts +76 -34
- package/src/host/runner.ts +2813 -422
- package/src/host/state.ts +1160 -0
- package/src/host/worker-entry.ts +533 -0
- package/src/host/worker-protocol.ts +165 -0
- package/src/host/worker-store.ts +229 -0
- package/src/host/worker-supervisor.ts +74 -0
- package/src/render/canvas.ts +44 -10
- package/src/render/graph-render.ts +145 -90
- package/src/state/database.ts +2 -1
- package/src/state/index.ts +14 -0
- package/src/state/prune.ts +35 -9
- package/src/state/schema.ts +171 -3
- package/src/state/viewer.ts +356 -0
- package/src/viewer/cli.ts +49 -17
- package/src/workflows/composition.ts +36 -2
- package/src/workflows/definition.ts +32 -0
- package/src/workflows/engine.ts +157 -54
- package/src/workflows/errors.ts +11 -2
- package/src/workflows/human-decision.ts +49 -0
- package/src/workflows/index.ts +2 -0
- package/src/workflows/schema.ts +19 -6
- package/src/{viewer → workflows}/session-reducer.ts +11 -1
- package/src/workflows/store.ts +1316 -108
- package/src/workflows/tool-input.ts +2 -60
- package/src/workflows/types.ts +32 -4
- package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
- package/dist/controllers/workflow-engine-scheduler.js +0 -175
- package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
- package/dist/extension/controller-host.d.ts +0 -48
- package/dist/extension/controller-host.js +0 -110
- package/dist/extension/controller-host.js.map +0 -1
- package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
- package/dist/extension/deferred-turn-coordinator.js +0 -143
- package/dist/extension/deferred-turn-coordinator.js.map +0 -1
- package/dist/extension/deferred-turn.d.ts +0 -44
- package/dist/extension/deferred-turn.js +0 -110
- package/dist/extension/deferred-turn.js.map +0 -1
- package/dist/extension/executor.d.ts +0 -86
- package/dist/extension/executor.js +0 -311
- package/dist/extension/executor.js.map +0 -1
- package/dist/extension/follow-up-coordinator.d.ts +0 -27
- package/dist/extension/follow-up-coordinator.js +0 -131
- package/dist/extension/follow-up-coordinator.js.map +0 -1
- package/dist/extension/recorder.d.ts +0 -84
- package/dist/extension/recorder.js +0 -528
- package/dist/extension/recorder.js.map +0 -1
- package/dist/extension/session-events.d.ts +0 -133
- package/dist/extension/session-events.js +0 -61
- package/dist/extension/session-events.js.map +0 -1
- package/dist/viewer/session-reducer.js.map +0 -1
- package/src/controllers/workflow-engine-scheduler.ts +0 -246
- package/src/extension/controller-host.ts +0 -167
- package/src/extension/deferred-turn-coordinator.ts +0 -171
- package/src/extension/deferred-turn.ts +0 -166
- package/src/extension/executor.ts +0 -411
- package/src/extension/follow-up-coordinator.ts +0 -151
- package/src/extension/recorder.ts +0 -655
- package/src/extension/session-events.ts +0 -121
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { builtinWorkflowCatalog } from "../builtins/catalog.js";
|
|
6
|
+
import { discoverControllers, loadControllerFile } from "../controllers/loader.js";
|
|
7
|
+
import { canonicalJson, parseJson, type JsonValue } from "../state/json.js";
|
|
8
|
+
import { compositionMetadata } from "../workflows/composition.js";
|
|
9
|
+
import { errorMessage } from "../workflows/errors.js";
|
|
10
|
+
import { validateWorkflowDefinition } from "../workflows/graph.js";
|
|
11
|
+
import { resolveWorkflowRef } from "../workflows/loader.js";
|
|
12
|
+
import { applyWorkflowSettingsPatch } from "../workflows/settings.js";
|
|
13
|
+
import { createDefinitionSnapshot } from "../workflows/store.js";
|
|
14
|
+
import type { WorkflowDefinition, WorkflowSource } from "../workflows/types.js";
|
|
15
|
+
import { MAX_PROTOCOL_MESSAGE_BYTES } from "./protocol.js";
|
|
16
|
+
|
|
17
|
+
export type ResolvedWorkflowLaunch = {
|
|
18
|
+
schema: "pi-workflows.resolved-launch.v1";
|
|
19
|
+
workflowName: string;
|
|
20
|
+
workflowSourceRef: string;
|
|
21
|
+
workflowSource: JsonValue;
|
|
22
|
+
definitionDigest: string;
|
|
23
|
+
definitionSnapshot: JsonValue;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ResolvedControllerInitialization = {
|
|
27
|
+
schema: "pi-workflows.resolved-controller-initialization.v1";
|
|
28
|
+
controllerName: string;
|
|
29
|
+
controllerPath: string;
|
|
30
|
+
sourceHash: string;
|
|
31
|
+
initialStatus: JsonValue;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ResolvedSettingsChange = {
|
|
35
|
+
schema: "pi-workflows.resolved-settings-change.v1";
|
|
36
|
+
definitionDigest: string;
|
|
37
|
+
patch: JsonValue;
|
|
38
|
+
settings: JsonValue;
|
|
39
|
+
paths: JsonValue;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type ResolverRequest = {
|
|
43
|
+
schema: "pi-workflows.resolve-request.v1";
|
|
44
|
+
cwd: string;
|
|
45
|
+
workflowRef: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type SettingsValidationRequest = {
|
|
49
|
+
schema: "pi-workflows.settings-validation-request.v1";
|
|
50
|
+
cwd: string;
|
|
51
|
+
workflowRef: string;
|
|
52
|
+
definitionDigest: string;
|
|
53
|
+
mountPath: string;
|
|
54
|
+
current: JsonValue;
|
|
55
|
+
patch: JsonValue;
|
|
56
|
+
actorId: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type ControllerInitializationRequest = {
|
|
60
|
+
schema: "pi-workflows.controller-initialization-request.v1";
|
|
61
|
+
cwd: string;
|
|
62
|
+
controllerName: string;
|
|
63
|
+
spec: JsonValue;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type ResolverInput = ResolverRequest | SettingsValidationRequest | ControllerInitializationRequest;
|
|
67
|
+
type ResolverOutput =
|
|
68
|
+
| ResolvedWorkflowLaunch
|
|
69
|
+
| ResolvedSettingsChange
|
|
70
|
+
| ResolvedControllerInitialization;
|
|
71
|
+
|
|
72
|
+
export async function resolveWorkflowLaunch(
|
|
73
|
+
request: ResolverRequest,
|
|
74
|
+
): Promise<ResolvedWorkflowLaunch> {
|
|
75
|
+
if (request.schema !== "pi-workflows.resolve-request.v1") {
|
|
76
|
+
throw new Error("Invalid workflow resolver request schema");
|
|
77
|
+
}
|
|
78
|
+
const resolved = await loadResolvedWorkflow(request.cwd, request.workflowRef);
|
|
79
|
+
return {
|
|
80
|
+
schema: "pi-workflows.resolved-launch.v1",
|
|
81
|
+
workflowName: resolved.definition.name,
|
|
82
|
+
workflowSourceRef: sourceRef(resolved.source),
|
|
83
|
+
workflowSource: {
|
|
84
|
+
root: resolved.source,
|
|
85
|
+
mounted: compositionMetadata(resolved.definition)?.sources ?? [],
|
|
86
|
+
} as JsonValue,
|
|
87
|
+
definitionDigest: resolved.definitionDigest,
|
|
88
|
+
definitionSnapshot: resolved.snapshot,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function resolveControllerInitialization(
|
|
93
|
+
request: ControllerInitializationRequest,
|
|
94
|
+
): Promise<ResolvedControllerInitialization> {
|
|
95
|
+
const discovered = (await discoverControllers({ cwd: request.cwd })).find(
|
|
96
|
+
(candidate) => candidate.name === request.controllerName,
|
|
97
|
+
);
|
|
98
|
+
if (discovered === undefined) {
|
|
99
|
+
throw new Error(`Controller not found: ${request.controllerName}`);
|
|
100
|
+
}
|
|
101
|
+
const definition = await loadControllerFile(discovered.path);
|
|
102
|
+
if (definition.name !== request.controllerName) {
|
|
103
|
+
throw new Error("Controller source name does not match its discovered name");
|
|
104
|
+
}
|
|
105
|
+
const initialStatus = parseJson(canonicalJson(definition.initialStatus(request.spec)));
|
|
106
|
+
const sourceHash = createHash("sha256")
|
|
107
|
+
.update(await fs.readFile(discovered.path))
|
|
108
|
+
.digest("hex");
|
|
109
|
+
return {
|
|
110
|
+
schema: "pi-workflows.resolved-controller-initialization.v1",
|
|
111
|
+
controllerName: definition.name,
|
|
112
|
+
controllerPath: discovered.path,
|
|
113
|
+
sourceHash,
|
|
114
|
+
initialStatus,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function resolveSettingsChange(
|
|
119
|
+
request: SettingsValidationRequest,
|
|
120
|
+
): Promise<ResolvedSettingsChange> {
|
|
121
|
+
if (request.schema !== "pi-workflows.settings-validation-request.v1") {
|
|
122
|
+
throw new Error("Invalid workflow settings validation request schema");
|
|
123
|
+
}
|
|
124
|
+
const resolved = await loadResolvedWorkflow(request.cwd, request.workflowRef);
|
|
125
|
+
if (resolved.definitionDigest !== request.definitionDigest) {
|
|
126
|
+
throw new Error("Workflow source changed before the settings proposal was validated");
|
|
127
|
+
}
|
|
128
|
+
const definition = settingsDefinition(resolved.definition, request.mountPath);
|
|
129
|
+
if (definition === undefined) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Workflow scope ${request.mountPath || "/"} does not declare editable settings`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const applied = await applyWorkflowSettingsPatch(
|
|
135
|
+
definition,
|
|
136
|
+
request.current,
|
|
137
|
+
request.patch,
|
|
138
|
+
{ type: "controller", id: request.actorId },
|
|
139
|
+
"controller-request",
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
schema: "pi-workflows.resolved-settings-change.v1",
|
|
143
|
+
definitionDigest: resolved.definitionDigest,
|
|
144
|
+
patch: applied.patch as JsonValue,
|
|
145
|
+
settings: applied.json,
|
|
146
|
+
paths: definition.paths as unknown as JsonValue,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function loadResolvedWorkflow(
|
|
151
|
+
cwd: string,
|
|
152
|
+
workflowRef: string,
|
|
153
|
+
): Promise<{
|
|
154
|
+
definition: WorkflowDefinition;
|
|
155
|
+
source: WorkflowSource;
|
|
156
|
+
snapshot: JsonValue;
|
|
157
|
+
definitionDigest: string;
|
|
158
|
+
}> {
|
|
159
|
+
const resolved = await resolveWorkflowRef(workflowRef, { cwd }, builtinWorkflowCatalog);
|
|
160
|
+
validateWorkflowDefinition(resolved.definition);
|
|
161
|
+
const snapshot = createDefinitionSnapshot(resolved.definition) as unknown as JsonValue;
|
|
162
|
+
const definitionDigest = `sha256:${createHash("sha256")
|
|
163
|
+
.update(canonicalJson(snapshot))
|
|
164
|
+
.digest("hex")}`;
|
|
165
|
+
return { definition: resolved.definition, source: resolved.source, snapshot, definitionDigest };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function settingsDefinition(definition: WorkflowDefinition, mountPath: string) {
|
|
169
|
+
return mountPath === ""
|
|
170
|
+
? definition.settings
|
|
171
|
+
: compositionMetadata(definition)?.scopes[mountPath]?.settings;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function sourceRef(source: WorkflowSource): string {
|
|
175
|
+
return source.kind === "builtin" ? `builtin:${source.id}` : source.path;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function readRequest(): Promise<ResolverInput> {
|
|
179
|
+
const chunks: Buffer[] = [];
|
|
180
|
+
let length = 0;
|
|
181
|
+
for await (const chunk of process.stdin) {
|
|
182
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
183
|
+
length += buffer.byteLength;
|
|
184
|
+
if (length > MAX_PROTOCOL_MESSAGE_BYTES) {
|
|
185
|
+
throw new Error("Workflow resolver request exceeds 1 MiB");
|
|
186
|
+
}
|
|
187
|
+
chunks.push(buffer);
|
|
188
|
+
}
|
|
189
|
+
const value = parseJson(Buffer.concat(chunks).toString("utf8").trimEnd());
|
|
190
|
+
if (!isRecord(value) || typeof value.cwd !== "string") {
|
|
191
|
+
throw new Error("Invalid source resolver request");
|
|
192
|
+
}
|
|
193
|
+
if (value.schema === "pi-workflows.resolve-request.v1" && typeof value.workflowRef === "string") {
|
|
194
|
+
return value as ResolverRequest;
|
|
195
|
+
}
|
|
196
|
+
if (
|
|
197
|
+
value.schema === "pi-workflows.controller-initialization-request.v1" &&
|
|
198
|
+
typeof value.controllerName === "string" &&
|
|
199
|
+
Object.hasOwn(value, "spec")
|
|
200
|
+
) {
|
|
201
|
+
return value as ControllerInitializationRequest;
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
value.schema === "pi-workflows.settings-validation-request.v1" &&
|
|
205
|
+
typeof value.workflowRef === "string" &&
|
|
206
|
+
typeof value.definitionDigest === "string" &&
|
|
207
|
+
typeof value.mountPath === "string" &&
|
|
208
|
+
typeof value.actorId === "string" &&
|
|
209
|
+
Object.hasOwn(value, "current") &&
|
|
210
|
+
Object.hasOwn(value, "patch")
|
|
211
|
+
) {
|
|
212
|
+
return value as SettingsValidationRequest;
|
|
213
|
+
}
|
|
214
|
+
throw new Error("Invalid workflow resolver request schema");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function main(): Promise<void> {
|
|
218
|
+
try {
|
|
219
|
+
const request = await readRequest();
|
|
220
|
+
const result: ResolverOutput =
|
|
221
|
+
request.schema === "pi-workflows.resolve-request.v1"
|
|
222
|
+
? await resolveWorkflowLaunch(request)
|
|
223
|
+
: request.schema === "pi-workflows.controller-initialization-request.v1"
|
|
224
|
+
? await resolveControllerInitialization(request)
|
|
225
|
+
: await resolveSettingsChange(request);
|
|
226
|
+
const encoded = `${canonicalJson(result)}\n`;
|
|
227
|
+
if (Buffer.byteLength(encoded) > MAX_PROTOCOL_MESSAGE_BYTES) {
|
|
228
|
+
throw new Error("Workflow resolver result exceeds 1 MiB");
|
|
229
|
+
}
|
|
230
|
+
process.stdout.write(encoded);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
process.stderr.write(`${errorMessage(error)}\n`);
|
|
233
|
+
process.exitCode = 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
238
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) void main();
|
package/src/host/rpc-executor.ts
CHANGED
|
@@ -7,7 +7,6 @@ import type {
|
|
|
7
7
|
AgentStepRequest,
|
|
8
8
|
AgentStepSubmission,
|
|
9
9
|
} from "../workflows/types.js";
|
|
10
|
-
import type { HostProcessRegistry } from "./processes.js";
|
|
11
10
|
import { RPC_SUBMISSION_PREFIX } from "./rpc-bridge.js";
|
|
12
11
|
|
|
13
12
|
// Production resolves the compiled .js; tests and dev checkouts load the .ts
|
|
@@ -15,7 +14,7 @@ import { RPC_SUBMISSION_PREFIX } from "./rpc-bridge.js";
|
|
|
15
14
|
const BRIDGE_PATH = ["./rpc-bridge.js", "./rpc-bridge.ts"]
|
|
16
15
|
.map((candidate) => fileURLToPath(new URL(candidate, import.meta.url)))
|
|
17
16
|
.find((candidate) => fs.existsSync(candidate));
|
|
18
|
-
const
|
|
17
|
+
const DEFAULT_ABORT_GRACE_MS = 3_000;
|
|
19
18
|
|
|
20
19
|
type StepSubmission = {
|
|
21
20
|
action: "submit";
|
|
@@ -34,9 +33,18 @@ type StepUpdate = {
|
|
|
34
33
|
|
|
35
34
|
type StepAction = StepSubmission | StepUpdate;
|
|
36
35
|
|
|
36
|
+
export interface SupervisedProcessRegistry {
|
|
37
|
+
register(pid: number): unknown | Promise<unknown>;
|
|
38
|
+
unregister(pid: number): unknown | Promise<unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
export type RpcStepExecutorOptions = {
|
|
38
42
|
cwd: string;
|
|
39
|
-
registry
|
|
43
|
+
registry?: SupervisedProcessRegistry;
|
|
44
|
+
/** Start a separate process group, or inherit the caller's process group. */
|
|
45
|
+
processGroup?: "own" | "inherit";
|
|
46
|
+
/** Grace period before a process group receives SIGKILL. */
|
|
47
|
+
abortGraceMs?: number;
|
|
40
48
|
/** Absolute path to the pi binary; defaults to the installed `pi`. */
|
|
41
49
|
piBin?: string;
|
|
42
50
|
/** Extra environment for the child; the host's environment is inherited. */
|
|
@@ -63,6 +71,8 @@ export class RpcStepExecutor implements AgentStepExecutor {
|
|
|
63
71
|
private actions: StepAction[] = [];
|
|
64
72
|
private submissionWaiters: Array<() => void> = [];
|
|
65
73
|
private stdoutBuffer = "";
|
|
74
|
+
private registeredPid: number | null = null;
|
|
75
|
+
private closePromise: Promise<void> | null = null;
|
|
66
76
|
|
|
67
77
|
constructor(options: RpcStepExecutorOptions) {
|
|
68
78
|
this.options = options;
|
|
@@ -70,7 +80,7 @@ export class RpcStepExecutor implements AgentStepExecutor {
|
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
async runAgentStep(request: AgentStepRequest, signal: AbortSignal): Promise<AgentStepSubmission> {
|
|
73
|
-
this.ensureStarted();
|
|
83
|
+
await this.ensureStarted();
|
|
74
84
|
let prompt = request.prompt;
|
|
75
85
|
for (;;) {
|
|
76
86
|
throwIfAborted(signal);
|
|
@@ -88,38 +98,41 @@ export class RpcStepExecutor implements AgentStepExecutor {
|
|
|
88
98
|
}
|
|
89
99
|
}
|
|
90
100
|
|
|
91
|
-
/** Stop the child
|
|
101
|
+
/** Stop the child process tree, then unregister its exact group leader. */
|
|
92
102
|
async close(): Promise<void> {
|
|
103
|
+
this.closePromise ??= this.closeChild();
|
|
104
|
+
await this.closePromise;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async closeChild(): Promise<void> {
|
|
93
108
|
const child = this.child;
|
|
94
109
|
this.child = null;
|
|
95
|
-
if (child === null)
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
110
|
+
if (child === null) return;
|
|
98
111
|
const pid = child.pid;
|
|
99
|
-
|
|
112
|
+
try {
|
|
100
113
|
try {
|
|
101
114
|
child.stdin?.end();
|
|
102
115
|
} catch {
|
|
103
116
|
// The pipe may already be closed.
|
|
104
117
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (pid !== undefined) {
|
|
118
|
+
const ownGroup = this.options.processGroup !== "inherit";
|
|
119
|
+
if (pid !== undefined && ownGroup) {
|
|
108
120
|
killGroup(pid, "SIGTERM");
|
|
109
|
-
|
|
121
|
+
const exited = await waitForGroupExit(
|
|
122
|
+
pid,
|
|
123
|
+
this.options.abortGraceMs ?? DEFAULT_ABORT_GRACE_MS,
|
|
124
|
+
);
|
|
125
|
+
if (!exited) killGroup(pid, "SIGKILL");
|
|
126
|
+
} else if (this.childExited === null) {
|
|
110
127
|
child.kill("SIGTERM");
|
|
128
|
+
const exited = await waitForExit(
|
|
129
|
+
child,
|
|
130
|
+
this.options.abortGraceMs ?? DEFAULT_ABORT_GRACE_MS,
|
|
131
|
+
);
|
|
132
|
+
if (!exited) child.kill("SIGKILL");
|
|
111
133
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (pid !== undefined) {
|
|
115
|
-
killGroup(pid, "SIGKILL");
|
|
116
|
-
} else {
|
|
117
|
-
child.kill("SIGKILL");
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (pid !== undefined) {
|
|
122
|
-
this.options.registry.unregister(pid);
|
|
134
|
+
} finally {
|
|
135
|
+
await this.unregisterChild();
|
|
123
136
|
}
|
|
124
137
|
}
|
|
125
138
|
|
|
@@ -127,7 +140,8 @@ export class RpcStepExecutor implements AgentStepExecutor {
|
|
|
127
140
|
return this.childExited;
|
|
128
141
|
}
|
|
129
142
|
|
|
130
|
-
private ensureStarted(): void {
|
|
143
|
+
private async ensureStarted(): Promise<void> {
|
|
144
|
+
if (this.closePromise !== null) throw new Error("Headless pi session is closed");
|
|
131
145
|
if (this.child !== null) {
|
|
132
146
|
const exited = this.currentExit();
|
|
133
147
|
if (exited !== null) {
|
|
@@ -159,20 +173,12 @@ export class RpcStepExecutor implements AgentStepExecutor {
|
|
|
159
173
|
cwd: this.options.cwd,
|
|
160
174
|
env: { ...process.env, ...this.options.env },
|
|
161
175
|
stdio: ["pipe", "pipe", "pipe"],
|
|
162
|
-
|
|
163
|
-
// so a killed host cannot leave an orphaned agent working.
|
|
164
|
-
detached: true,
|
|
176
|
+
detached: this.options.processGroup !== "inherit",
|
|
165
177
|
},
|
|
166
178
|
);
|
|
167
179
|
this.child = child;
|
|
168
|
-
if (child.pid !== undefined) {
|
|
169
|
-
this.options.registry.register(child.pid);
|
|
170
|
-
}
|
|
171
180
|
child.on("exit", (code, signal) => {
|
|
172
181
|
this.childExited = { code, signal };
|
|
173
|
-
if (child.pid !== undefined) {
|
|
174
|
-
this.options.registry.unregister(child.pid);
|
|
175
|
-
}
|
|
176
182
|
this.wakeSubmissionWaiters();
|
|
177
183
|
});
|
|
178
184
|
// A failed spawn (missing binary, EACCES) emits error instead of exit;
|
|
@@ -213,6 +219,23 @@ export class RpcStepExecutor implements AgentStepExecutor {
|
|
|
213
219
|
child.stdout?.on("data", (chunk: Buffer) => {
|
|
214
220
|
this.stdoutBuffer = (this.stdoutBuffer + chunk.toString("utf8")).slice(-64 * 1024);
|
|
215
221
|
});
|
|
222
|
+
if (child.pid !== undefined && this.options.registry !== undefined) {
|
|
223
|
+
try {
|
|
224
|
+
await this.options.registry.register(child.pid);
|
|
225
|
+
this.registeredPid = child.pid;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
killGroup(child.pid, "SIGKILL");
|
|
228
|
+
this.child = null;
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private async unregisterChild(): Promise<void> {
|
|
235
|
+
const pid = this.registeredPid;
|
|
236
|
+
if (pid === null) return;
|
|
237
|
+
this.registeredPid = null;
|
|
238
|
+
await this.options.registry?.unregister(pid);
|
|
216
239
|
}
|
|
217
240
|
|
|
218
241
|
private async promptForSubmission(
|
|
@@ -310,6 +333,25 @@ function throwIfAborted(signal: AbortSignal): void {
|
|
|
310
333
|
}
|
|
311
334
|
}
|
|
312
335
|
|
|
336
|
+
async function waitForGroupExit(pid: number, timeoutMs: number): Promise<boolean> {
|
|
337
|
+
if (process.platform === "win32") return true;
|
|
338
|
+
const deadline = Date.now() + timeoutMs;
|
|
339
|
+
while (processGroupExists(pid)) {
|
|
340
|
+
if (Date.now() >= deadline) return false;
|
|
341
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function processGroupExists(pid: number): boolean {
|
|
347
|
+
try {
|
|
348
|
+
process.kill(-pid, 0);
|
|
349
|
+
return true;
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
313
355
|
async function waitForExit(child: ChildProcess, timeoutMs: number): Promise<boolean> {
|
|
314
356
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
315
357
|
return true;
|