@osolmaz/pi-workflows 0.11.2 → 0.12.1
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 +15 -10
- package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
- package/dist/builtins/autoimplement-command-batches.js +258 -0
- package/dist/builtins/autoimplement-command-batches.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +509 -124
- package/dist/builtins/autoimplement.workflow.js +884 -327
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +5 -3
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +4 -1
- package/dist/builtins/index.js +3 -1
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +2 -4
- package/dist/builtins/monitor.workflow.js +26 -128
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/pi-agent-group.d.ts +72 -0
- package/dist/builtins/pi-agent-group.js +1087 -0
- package/dist/builtins/pi-agent-group.js.map +1 -0
- package/dist/builtins/plan-approval.workflow.d.ts +39 -5
- package/dist/builtins/plan-approval.workflow.js +92 -14
- package/dist/builtins/plan-approval.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +301 -0
- package/dist/builtins/plan-change.workflow.js +256 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.js +2 -2
- package/dist/builtins/plan-presentation.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.d.ts +85 -0
- package/dist/builtins/sanity-check.workflow.js +482 -0
- package/dist/builtins/sanity-check.workflow.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +90 -2
- package/dist/controllers/sqlite.js +380 -5
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -2
- package/dist/extension/decision-channels.js +29 -35
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
- package/dist/extension/deferred-turn-coordinator.js +143 -0
- package/dist/extension/deferred-turn-coordinator.js.map +1 -0
- package/dist/extension/deferred-turn.d.ts +44 -0
- package/dist/extension/deferred-turn.js +110 -0
- package/dist/extension/deferred-turn.js.map +1 -0
- package/dist/extension/index.js +348 -80
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -2
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +23 -3
- package/dist/extension/widget.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- package/dist/render/graph-render.js +1 -2
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/render.js +7 -6
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/catalog.js +7 -2
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/command-batch.d.ts +38 -0
- package/dist/workflows/command-batch.js +176 -0
- package/dist/workflows/command-batch.js.map +1 -0
- package/dist/workflows/composition.js +8 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +1 -1
- package/dist/workflows/decision-presentation.js +51 -38
- package/dist/workflows/decision-presentation.js.map +1 -1
- package/dist/workflows/engine.d.ts +2 -2
- package/dist/workflows/engine.js +22 -17
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +13 -0
- package/dist/workflows/errors.js +15 -0
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +16 -4
- package/dist/workflows/human-decision.js +175 -72
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -2
- package/dist/workflows/index.js +2 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/progress.d.ts +1 -0
- package/dist/workflows/progress.js +15 -3
- package/dist/workflows/progress.js.map +1 -1
- package/dist/workflows/schema.js +12 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/shell.d.ts +4 -0
- package/dist/workflows/shell.js +6 -0
- package/dist/workflows/shell.js.map +1 -1
- package/dist/workflows/store.js +8 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +37 -48
- package/docs/2026-08-18-herdr-piw-plan.md +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
- package/docs/CONTROLLERS.md +2 -2
- package/docs/DEFERRED_TURNS.md +298 -0
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/HUMAN_DECISIONS.md +30 -40
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +17 -27
- package/docs/MONITOR.md +7 -13
- package/docs/WORKFLOW_COMPOSITION.md +10 -9
- package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
- package/docs/WORKFLOW_UPDATES.md +28 -5
- package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
- package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
- package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
- package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
- package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +283 -0
- package/docs/run-bundles.md +8 -6
- package/docs/workflows.md +85 -22
- package/examples/workflows/approved-plan.workflow.ts +19 -46
- package/examples/workflows/command-batch.workflow.ts +50 -0
- package/examples/workflows/sanity-check.workflow.ts +1 -0
- package/herdr-plugin.toml +3 -3
- package/package.json +7 -7
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +16 -4
- package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
- package/schemas/human-decision-cancellation-v1.schema.json +1 -1
- package/schemas/human-decision-continuation-v1.schema.json +11 -2
- package/schemas/human-decision-delivery-v1.schema.json +9 -1
- package/schemas/human-decision-receipt-v1.schema.json +9 -1
- package/schemas/human-decision-request-v1.schema.json +25 -5
- package/schemas/human-decision-resolution-v1.schema.json +1 -1
- package/schemas/human-decision-settlement-v1.schema.json +1 -1
- package/skills/autodoc/SKILL.md +33 -2
- package/skills/autoimplement/SKILL.md +93 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +66 -24
- package/skills/pi-workflows/SKILL.md +21 -6
- package/skills/sanity-check/SKILL.md +44 -0
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1075 -370
- package/src/builtins/catalog.ts +5 -3
- package/src/builtins/index.ts +24 -0
- package/src/builtins/monitor.workflow.ts +27 -150
- package/src/builtins/pi-agent-group.ts +1407 -0
- package/src/builtins/plan-approval.workflow.ts +157 -24
- package/src/builtins/plan-change.workflow.ts +321 -0
- package/src/builtins/plan-presentation.ts +2 -2
- package/src/builtins/sanity-check.workflow.ts +769 -0
- package/src/controllers/sqlite.ts +580 -7
- package/src/extension/decision-channels.ts +36 -66
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +438 -90
- package/src/extension/session-events.ts +2 -2
- package/src/extension/step-message.ts +1 -0
- package/src/extension/widget.ts +24 -5
- package/src/herdr/setup.ts +4 -4
- package/src/render/graph-render.ts +1 -2
- package/src/viewer/render.ts +7 -6
- package/src/workflows/catalog.ts +7 -2
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/composition.ts +9 -0
- package/src/workflows/decision-presentation.ts +56 -43
- package/src/workflows/engine.ts +25 -20
- package/src/workflows/errors.ts +24 -0
- package/src/workflows/human-decision.ts +218 -101
- package/src/workflows/index.ts +20 -11
- package/src/workflows/progress.ts +18 -3
- package/src/workflows/schema.ts +19 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +8 -1
- package/src/workflows/types.ts +43 -59
- package/schemas/human-decision-accepted-v2.schema.json +0 -50
- package/schemas/human-decision-delivery-v2.schema.json +0 -36
- package/schemas/human-decision-receipt-v2.schema.json +0 -39
- package/schemas/human-decision-request-v2.schema.json +0 -69
- package/schemas/human-decision-resolution-v2.schema.json +0 -27
|
@@ -9,7 +9,7 @@ export type PublicAssistantMessageEvent =
|
|
|
9
9
|
| { type: "toolcall_start"; contentIndex: number; partial: unknown }
|
|
10
10
|
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: unknown }
|
|
11
11
|
| { type: "toolcall_end"; contentIndex: number; toolCall: unknown; partial: unknown }
|
|
12
|
-
| { type: "done"; reason: "stop" | "length" | "toolUse"; message: unknown }
|
|
12
|
+
| { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred"; message: unknown }
|
|
13
13
|
| { type: "error"; reason: "aborted" | "error"; error: unknown };
|
|
14
14
|
|
|
15
15
|
export type TurnStartEventLike = { turnIndex: number };
|
|
@@ -44,7 +44,7 @@ export type NormalizedAssistantEvent =
|
|
|
44
44
|
| { type: "toolcall_start"; contentIndex: number }
|
|
45
45
|
| { type: "toolcall_delta"; contentIndex: number; delta: string }
|
|
46
46
|
| { type: "toolcall_end"; contentIndex: number; toolCall: unknown }
|
|
47
|
-
| { type: "done"; reason: "stop" | "length" | "toolUse" }
|
|
47
|
+
| { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred" }
|
|
48
48
|
| { type: "error"; reason: "aborted" | "error" };
|
|
49
49
|
|
|
50
50
|
/** Remove cumulative snapshots while preserving every semantic stream event. */
|
package/src/extension/widget.ts
CHANGED
|
@@ -162,9 +162,11 @@ function progressLines(
|
|
|
162
162
|
: progressTracksFromRecords(updateHistory, now).map((track) => track.estimate);
|
|
163
163
|
const projected = mergeProgressEstimates(latestProgressEstimates(state, now), measured ?? []);
|
|
164
164
|
const estimates = mergeProgressEstimates(projected, monitorEstimates(state) ?? []);
|
|
165
|
-
const lines = prioritizeProgressEstimates(estimates).map((estimate) =>
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
const lines = prioritizeWidgetProgress(prioritizeProgressEstimates(estimates)).map((estimate) => {
|
|
166
|
+
const indentation =
|
|
167
|
+
estimate.key.startsWith("agents/") && estimate.key.split("/").length > 2 ? " " : "";
|
|
168
|
+
return `${indentation}${formatProgressLine(estimate, now)}`;
|
|
169
|
+
});
|
|
168
170
|
const schedule = (state.updates ?? []).find(
|
|
169
171
|
(record) => record.type === "monitor.schedule" && record.key === "next-check",
|
|
170
172
|
);
|
|
@@ -180,6 +182,24 @@ function progressLines(
|
|
|
180
182
|
return lines;
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
function prioritizeWidgetProgress(estimates: ProgressEstimate[]): ProgressEstimate[] {
|
|
186
|
+
const weight = (estimate: ProgressEstimate) => {
|
|
187
|
+
if (!estimate.key.startsWith("agents/")) return 0;
|
|
188
|
+
const child = estimate.key.split("/").length > 2;
|
|
189
|
+
if (!child) return -4;
|
|
190
|
+
if (estimate.data.status === "failed" || estimate.data.status === "blocked") return -3;
|
|
191
|
+
if (
|
|
192
|
+
estimate.data.status === "running" ||
|
|
193
|
+
estimate.data.status === "pending" ||
|
|
194
|
+
estimate.data.status === "waiting"
|
|
195
|
+
) {
|
|
196
|
+
return -2;
|
|
197
|
+
}
|
|
198
|
+
return 2;
|
|
199
|
+
};
|
|
200
|
+
return [...estimates].sort((left, right) => weight(left) - weight(right));
|
|
201
|
+
}
|
|
202
|
+
|
|
183
203
|
function monitorEstimates(state: WorkflowRunState): ProgressEstimate[] | undefined {
|
|
184
204
|
const output = state.outputs.estimate;
|
|
185
205
|
if (output === null || typeof output !== "object" || Array.isArray(output)) return undefined;
|
|
@@ -309,8 +329,7 @@ function nodeRuntimeSegments(
|
|
|
309
329
|
})
|
|
310
330
|
: undefined;
|
|
311
331
|
const requestAudience =
|
|
312
|
-
|
|
313
|
-
request?.schema === "pi-workflows.human-decision-request.v2") &&
|
|
332
|
+
request?.schema === "pi-workflows.human-decision-request.v1" &&
|
|
314
333
|
typeof request.audience === "string"
|
|
315
334
|
? request.audience
|
|
316
335
|
: human.audience;
|
package/src/herdr/setup.ts
CHANGED
|
@@ -115,20 +115,20 @@ function preflightPackage(packageRoot: string): PluginPackage {
|
|
|
115
115
|
try {
|
|
116
116
|
root = fs.realpathSync(requestedRoot);
|
|
117
117
|
} catch (error) {
|
|
118
|
-
throw new Error(`
|
|
118
|
+
throw new Error(`pi-workflows package root is missing: ${requestedRoot}`, { cause: error });
|
|
119
119
|
}
|
|
120
120
|
if (!fs.statSync(root).isDirectory()) {
|
|
121
|
-
throw new Error(`
|
|
121
|
+
throw new Error(`pi-workflows package root is not a directory: ${requestedRoot}`);
|
|
122
122
|
}
|
|
123
123
|
const packagePath = checkedRegularFile(root, "package.json");
|
|
124
124
|
const manifestPath = checkedRegularFile(root, MANIFEST_NAME);
|
|
125
125
|
const packageJson = parseJsonObject(fs.readFileSync(packagePath, "utf8"), "package.json");
|
|
126
126
|
if (packageJson["name"] !== PACKAGE_NAME) {
|
|
127
|
-
throw new Error(`Unexpected
|
|
127
|
+
throw new Error(`Unexpected pi-workflows package name in ${packagePath}.`);
|
|
128
128
|
}
|
|
129
129
|
const packageVersion = packageJson["version"];
|
|
130
130
|
if (typeof packageVersion !== "string" || packageVersion.length === 0) {
|
|
131
|
-
throw new Error(`
|
|
131
|
+
throw new Error(`pi-workflows package version is missing in ${packagePath}.`);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
const manifest = fs.readFileSync(manifestPath, "utf8");
|
|
@@ -313,8 +313,7 @@ function renderCellText(
|
|
|
313
313
|
? (state.finalOutput as { schema?: unknown }).schema
|
|
314
314
|
: undefined;
|
|
315
315
|
const waitingRequest =
|
|
316
|
-
waitingSchema === "pi-workflows.human-decision-request.v1"
|
|
317
|
-
waitingSchema === "pi-workflows.human-decision-request.v2"
|
|
316
|
+
waitingSchema === "pi-workflows.human-decision-request.v1"
|
|
318
317
|
? (state.finalOutput as {
|
|
319
318
|
audience?: unknown;
|
|
320
319
|
presentationDigest?: unknown;
|
package/src/viewer/render.ts
CHANGED
|
@@ -147,13 +147,13 @@ function nodeStatusLine(bundle: LoadedRunBundle, nodeId: string, width: number,
|
|
|
147
147
|
suffix = ansi.yellow(
|
|
148
148
|
human === undefined
|
|
149
149
|
? " waiting"
|
|
150
|
-
: ` waiting for human · ${sanitizeText(requestAudience ?? "operator")}${request
|
|
150
|
+
: ` waiting for human · ${sanitizeText(requestAudience ?? "operator")}${request === null ? "" : ` · ${sanitizeText(request.presentation.summary)}`} · ${Object.values(
|
|
151
151
|
human.choices,
|
|
152
152
|
)
|
|
153
153
|
.map((choice) => sanitizeText(choice.label))
|
|
154
154
|
.join(
|
|
155
155
|
" / ",
|
|
156
|
-
)}${request
|
|
156
|
+
)}${request === null ? "" : ` · ${request.presentationDigest.slice(7, 19)}`}`,
|
|
157
157
|
);
|
|
158
158
|
} else if (result) {
|
|
159
159
|
glyph = result.outcome === "ok" ? ansi.green("✓") : ansi.red("✗");
|
|
@@ -210,8 +210,7 @@ function inspectorLines(step: WorkflowStepRecord, width: number): string[] {
|
|
|
210
210
|
function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
|
|
211
211
|
if (value === null || typeof value !== "object") return null;
|
|
212
212
|
const schema = (value as { schema?: unknown }).schema;
|
|
213
|
-
return schema === "pi-workflows.human-decision-request.v1"
|
|
214
|
-
schema === "pi-workflows.human-decision-request.v2"
|
|
213
|
+
return schema === "pi-workflows.human-decision-request.v1"
|
|
215
214
|
? (value as HumanDecisionRequest)
|
|
216
215
|
: null;
|
|
217
216
|
}
|
|
@@ -271,12 +270,14 @@ export function renderRunDetailLines(
|
|
|
271
270
|
lines.push("");
|
|
272
271
|
lines.push(ansi.bold("progress"));
|
|
273
272
|
for (const track of progress) {
|
|
274
|
-
|
|
273
|
+
const depth = track.key.startsWith("agents/") ? track.key.split("/").length - 2 : 0;
|
|
274
|
+
const indentation = " ".repeat(Math.max(0, depth));
|
|
275
|
+
lines.push(fitWidth(`${indentation}${formatProgressLine(track.estimate, now)}`, size.width));
|
|
275
276
|
const latest = track.samples.at(-1);
|
|
276
277
|
lines.push(
|
|
277
278
|
fitWidth(
|
|
278
279
|
ansi.dim(
|
|
279
|
-
|
|
280
|
+
`${indentation} ${track.estimate.sampleCount} samples · ${track.estimate.confidence ?? "no"} confidence · updated ${latest?.at ?? "unknown"}`,
|
|
280
281
|
),
|
|
281
282
|
size.width,
|
|
282
283
|
),
|
package/src/workflows/catalog.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isWorkflowDefinition } from "./definition.js";
|
|
2
|
-
import {
|
|
2
|
+
import { BuiltinWorkflowRevisionChangedError } from "./errors.js";
|
|
3
3
|
import type { WorkflowDefinition, WorkflowSource } from "./types.js";
|
|
4
4
|
|
|
5
5
|
const BUILTIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
@@ -102,7 +102,12 @@ export class BuiltinWorkflowCatalog {
|
|
|
102
102
|
throw new Error(`Unknown built-in workflow: ${source.id}`);
|
|
103
103
|
}
|
|
104
104
|
if (entry.revision !== source.revision) {
|
|
105
|
-
throw new
|
|
105
|
+
throw new BuiltinWorkflowRevisionChangedError({
|
|
106
|
+
runId,
|
|
107
|
+
workflowId: entry.id,
|
|
108
|
+
previousRevision: source.revision,
|
|
109
|
+
currentRevision: entry.revision,
|
|
110
|
+
});
|
|
106
111
|
}
|
|
107
112
|
return entry.definition;
|
|
108
113
|
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { CancelledError, TimeoutError, errorMessage, isAbortLikeError } from "./errors.js";
|
|
3
|
+
import { runShellAction, shellOutputTruncation, shellResultFromError } from "./shell.js";
|
|
4
|
+
import type { MaybePromise, ShellActionResult } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const COMMAND_BATCH_RESULT_SCHEMA = "pi-workflows.command-batch-result.v1";
|
|
7
|
+
export const MAX_COMMAND_BATCH_ITEMS = 64;
|
|
8
|
+
export const MAX_COMMAND_BATCH_CONCURRENCY = 8;
|
|
9
|
+
export const MAX_COMMAND_BATCH_TIMEOUT_MS = 60 * 60_000;
|
|
10
|
+
export const MAX_COMMAND_BATCH_OUTPUT_CHARS = 1_000_000;
|
|
11
|
+
|
|
12
|
+
const MAX_ERROR_CHARS = 2_000;
|
|
13
|
+
const ITEM_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
|
|
14
|
+
|
|
15
|
+
export type CommandBatchItem = {
|
|
16
|
+
id: string;
|
|
17
|
+
command: string;
|
|
18
|
+
args: string[];
|
|
19
|
+
cwd: string;
|
|
20
|
+
timeoutMs: number;
|
|
21
|
+
maxOutputChars: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type CommandBatchRequest = {
|
|
25
|
+
items: CommandBatchItem[];
|
|
26
|
+
maxConcurrency: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type CommandBatchItemOutcome = "succeeded" | "failed" | "timedOut" | "cancelled";
|
|
30
|
+
|
|
31
|
+
export type CommandBatchItemResult = ShellActionResult & {
|
|
32
|
+
id: string;
|
|
33
|
+
outcome: CommandBatchItemOutcome;
|
|
34
|
+
stdoutTruncated: boolean;
|
|
35
|
+
stderrTruncated: boolean;
|
|
36
|
+
error?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type CommandBatchResult = {
|
|
40
|
+
schema: typeof COMMAND_BATCH_RESULT_SCHEMA;
|
|
41
|
+
items: CommandBatchItemResult[];
|
|
42
|
+
completed: number;
|
|
43
|
+
total: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type RunCommandBatchOptions = {
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
onItemSettled?: (
|
|
49
|
+
result: CommandBatchItemResult,
|
|
50
|
+
completed: number,
|
|
51
|
+
total: number,
|
|
52
|
+
) => MaybePromise<void>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function validateCommandBatchRequest(value: unknown): CommandBatchRequest {
|
|
56
|
+
const request = requireRecord(value, "command batch request");
|
|
57
|
+
for (const key of Object.keys(request)) {
|
|
58
|
+
if (key !== "items" && key !== "maxConcurrency") {
|
|
59
|
+
throw new Error(`command batch request.${key} is not supported`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(request.items)) {
|
|
63
|
+
throw new Error("command batch items must be an array");
|
|
64
|
+
}
|
|
65
|
+
if (request.items.length > MAX_COMMAND_BATCH_ITEMS) {
|
|
66
|
+
throw new Error(`command batch items must contain at most ${MAX_COMMAND_BATCH_ITEMS} entries`);
|
|
67
|
+
}
|
|
68
|
+
const ids = new Set<string>();
|
|
69
|
+
const items = request.items.map((item, index) => {
|
|
70
|
+
const normalized = validateItem(item, index);
|
|
71
|
+
if (ids.has(normalized.id)) {
|
|
72
|
+
throw new Error(`command batch item id is duplicated: ${normalized.id}`);
|
|
73
|
+
}
|
|
74
|
+
ids.add(normalized.id);
|
|
75
|
+
return normalized;
|
|
76
|
+
});
|
|
77
|
+
const maxConcurrency = positiveInteger(
|
|
78
|
+
request.maxConcurrency,
|
|
79
|
+
"command batch maxConcurrency",
|
|
80
|
+
MAX_COMMAND_BATCH_CONCURRENCY,
|
|
81
|
+
);
|
|
82
|
+
return { items, maxConcurrency };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function runCommandBatch(
|
|
86
|
+
input: CommandBatchRequest,
|
|
87
|
+
options: RunCommandBatchOptions = {},
|
|
88
|
+
): Promise<CommandBatchResult> {
|
|
89
|
+
const request = validateCommandBatchRequest(input);
|
|
90
|
+
const total = request.items.length;
|
|
91
|
+
if (total === 0) {
|
|
92
|
+
return { schema: COMMAND_BATCH_RESULT_SCHEMA, items: [], completed: 0, total: 0 };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const results = Array.from<CommandBatchItemResult | undefined>({ length: total });
|
|
96
|
+
const signal = options.signal;
|
|
97
|
+
let nextIndex = 0;
|
|
98
|
+
let completed = 0;
|
|
99
|
+
|
|
100
|
+
const worker = async () => {
|
|
101
|
+
while (!signal?.aborted) {
|
|
102
|
+
const index = nextIndex;
|
|
103
|
+
if (index >= total) return;
|
|
104
|
+
nextIndex += 1;
|
|
105
|
+
if (signal?.aborted) return;
|
|
106
|
+
|
|
107
|
+
const item = request.items[index];
|
|
108
|
+
if (item === undefined) return;
|
|
109
|
+
const result = await runItem(item, signal);
|
|
110
|
+
results[index] = result;
|
|
111
|
+
completed += 1;
|
|
112
|
+
try {
|
|
113
|
+
await options.onItemSettled?.(result, completed, total);
|
|
114
|
+
} catch {
|
|
115
|
+
// Completion callbacks are observational and cannot change batch execution.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const workerCount = Math.min(request.maxConcurrency, total);
|
|
121
|
+
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
122
|
+
|
|
123
|
+
for (let index = 0; index < total; index += 1) {
|
|
124
|
+
if (results[index] !== undefined) continue;
|
|
125
|
+
const item = request.items[index];
|
|
126
|
+
if (item !== undefined) results[index] = cancelledResult(item);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
schema: COMMAND_BATCH_RESULT_SCHEMA,
|
|
131
|
+
items: results as CommandBatchItemResult[],
|
|
132
|
+
completed,
|
|
133
|
+
total,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateItem(value: unknown, index: number): CommandBatchItem {
|
|
138
|
+
const item = requireRecord(value, `command batch items[${index}]`);
|
|
139
|
+
const allowed = new Set(["id", "command", "args", "cwd", "timeoutMs", "maxOutputChars"]);
|
|
140
|
+
for (const key of Object.keys(item)) {
|
|
141
|
+
if (!allowed.has(key)) throw new Error(`command batch items[${index}].${key} is not supported`);
|
|
142
|
+
}
|
|
143
|
+
const id = requireString(item.id, `command batch items[${index}].id`);
|
|
144
|
+
if (!ITEM_ID_PATTERN.test(id)) {
|
|
145
|
+
throw new Error(`command batch items[${index}].id is invalid`);
|
|
146
|
+
}
|
|
147
|
+
const command = requireString(item.command, `command batch items[${index}].command`);
|
|
148
|
+
if (!Array.isArray(item.args) || item.args.some((arg) => typeof arg !== "string")) {
|
|
149
|
+
throw new Error(`command batch items[${index}].args must be an array of strings`);
|
|
150
|
+
}
|
|
151
|
+
const cwd = requireString(item.cwd, `command batch items[${index}].cwd`);
|
|
152
|
+
if (!path.isAbsolute(cwd)) {
|
|
153
|
+
throw new Error(`command batch items[${index}].cwd must be absolute`);
|
|
154
|
+
}
|
|
155
|
+
const timeoutMs = positiveInteger(
|
|
156
|
+
item.timeoutMs,
|
|
157
|
+
`command batch items[${index}].timeoutMs`,
|
|
158
|
+
MAX_COMMAND_BATCH_TIMEOUT_MS,
|
|
159
|
+
);
|
|
160
|
+
const maxOutputChars = positiveInteger(
|
|
161
|
+
item.maxOutputChars,
|
|
162
|
+
`command batch items[${index}].maxOutputChars`,
|
|
163
|
+
MAX_COMMAND_BATCH_OUTPUT_CHARS,
|
|
164
|
+
);
|
|
165
|
+
return { id, command, args: [...item.args] as string[], cwd, timeoutMs, maxOutputChars };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function runItem(
|
|
169
|
+
item: CommandBatchItem,
|
|
170
|
+
signal?: AbortSignal,
|
|
171
|
+
): Promise<CommandBatchItemResult> {
|
|
172
|
+
try {
|
|
173
|
+
const result = await runShellAction(
|
|
174
|
+
{
|
|
175
|
+
command: item.command,
|
|
176
|
+
args: item.args,
|
|
177
|
+
cwd: item.cwd,
|
|
178
|
+
timeoutMs: item.timeoutMs,
|
|
179
|
+
maxOutputChars: item.maxOutputChars,
|
|
180
|
+
},
|
|
181
|
+
signal,
|
|
182
|
+
);
|
|
183
|
+
return itemResult(item.id, "succeeded", result);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const result = shellResultFromError(error) ?? emptyShellResult(item);
|
|
186
|
+
const outcome =
|
|
187
|
+
error instanceof TimeoutError
|
|
188
|
+
? "timedOut"
|
|
189
|
+
: error instanceof CancelledError || isAbortLikeError(error)
|
|
190
|
+
? "cancelled"
|
|
191
|
+
: "failed";
|
|
192
|
+
return itemResult(item.id, outcome, result, boundedError(error));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function itemResult(
|
|
197
|
+
id: string,
|
|
198
|
+
outcome: CommandBatchItemOutcome,
|
|
199
|
+
result: ShellActionResult,
|
|
200
|
+
error?: string,
|
|
201
|
+
): CommandBatchItemResult {
|
|
202
|
+
const truncation = shellOutputTruncation(result);
|
|
203
|
+
return {
|
|
204
|
+
id,
|
|
205
|
+
outcome,
|
|
206
|
+
...result,
|
|
207
|
+
stdoutTruncated: truncation.stdout,
|
|
208
|
+
stderrTruncated: truncation.stderr,
|
|
209
|
+
...(error !== undefined ? { error } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cancelledResult(item: CommandBatchItem): CommandBatchItemResult {
|
|
214
|
+
return itemResult(item.id, "cancelled", emptyShellResult(item), "Command was not started");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function emptyShellResult(item: CommandBatchItem): ShellActionResult {
|
|
218
|
+
return {
|
|
219
|
+
command: item.command,
|
|
220
|
+
args: [...item.args],
|
|
221
|
+
cwd: item.cwd,
|
|
222
|
+
stdout: "",
|
|
223
|
+
stderr: "",
|
|
224
|
+
exitCode: null,
|
|
225
|
+
signal: null,
|
|
226
|
+
durationMs: 0,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function boundedError(error: unknown): string {
|
|
231
|
+
const message = errorMessage(error);
|
|
232
|
+
return message.length <= MAX_ERROR_CHARS ? message : `${message.slice(0, MAX_ERROR_CHARS)}…`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function positiveInteger(value: unknown, label: string, maximum: number): number {
|
|
236
|
+
if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > maximum) {
|
|
237
|
+
throw new Error(`${label} must be an integer from 1 through ${maximum}`);
|
|
238
|
+
}
|
|
239
|
+
return value as number;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
243
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
244
|
+
throw new Error(`${label} must be an object`);
|
|
245
|
+
}
|
|
246
|
+
return value as Record<string, unknown>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function requireString(value: unknown, label: string): string {
|
|
250
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
251
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
@@ -480,6 +480,7 @@ function wrapNode(
|
|
|
480
480
|
case "checkpoint": {
|
|
481
481
|
const human = node.humanDecision;
|
|
482
482
|
const audience = human?.audience;
|
|
483
|
+
const onTimeout = human?.onTimeout;
|
|
483
484
|
return {
|
|
484
485
|
...common,
|
|
485
486
|
nodeType: "checkpoint",
|
|
@@ -496,6 +497,14 @@ function wrapNode(
|
|
|
496
497
|
: audience!,
|
|
497
498
|
choices: human.choices,
|
|
498
499
|
request: (context: WorkflowNodeContext) => human.request(project(context)),
|
|
500
|
+
...(onTimeout !== undefined
|
|
501
|
+
? {
|
|
502
|
+
onTimeout:
|
|
503
|
+
typeof onTimeout === "function"
|
|
504
|
+
? (context: WorkflowNodeContext) => onTimeout(project(context))
|
|
505
|
+
: onTimeout,
|
|
506
|
+
}
|
|
507
|
+
: {}),
|
|
499
508
|
},
|
|
500
509
|
}
|
|
501
510
|
: {}),
|