@osolmaz/pi-workflows 0.11.1 → 0.12.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 +15 -6
- 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 +29 -3
- package/dist/builtins/autoimplement.workflow.js +876 -231
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +3 -1
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -0
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/sanity-check-session.d.ts +17 -0
- package/dist/builtins/sanity-check-session.js +168 -0
- package/dist/builtins/sanity-check-session.js.map +1 -0
- package/dist/builtins/sanity-check.workflow.d.ts +83 -0
- package/dist/builtins/sanity-check.workflow.js +398 -0
- package/dist/builtins/sanity-check.workflow.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +141 -5
- package/dist/controllers/sqlite.js +568 -46
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.js +7 -7
- 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 +515 -84
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- package/dist/host/runner.js +3 -0
- package/dist/host/runner.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/engine.js +8 -4
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -0
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/migrate-sources.d.ts +1 -1
- package/dist/workflows/migrate-sources.js.map +1 -1
- package/dist/workflows/schema.js +2 -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 +3 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +1 -0
- package/dist/workflows/tool-input.js +2 -2
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +4 -3
- package/docs/2026-08-18-herdr-piw-plan.md +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +445 -0
- 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 +7 -7
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +3 -3
- package/docs/MONITOR.md +2 -2
- package/docs/WORKFLOW_COMPOSITION.md +2 -2
- 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-sanity-check-plan.md +175 -0
- package/docs/run-bundles.md +3 -1
- package/docs/workflows.md +67 -17
- 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 +1 -1
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v2.schema.json +1 -1
- 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 +1 -1
- package/schemas/human-decision-delivery-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v2.schema.json +1 -1
- package/schemas/human-decision-receipt-v1.schema.json +1 -1
- package/schemas/human-decision-receipt-v2.schema.json +1 -1
- package/schemas/human-decision-request-v1.schema.json +1 -1
- package/schemas/human-decision-request-v2.schema.json +1 -1
- package/schemas/human-decision-resolution-v1.schema.json +1 -1
- package/schemas/human-decision-resolution-v2.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 +66 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +35 -21
- package/skills/pi-workflows/SKILL.md +19 -5
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1065 -261
- package/src/builtins/catalog.ts +3 -1
- package/src/builtins/index.ts +13 -0
- package/src/builtins/sanity-check-session.ts +205 -0
- package/src/builtins/sanity-check.workflow.ts +624 -0
- package/src/controllers/sqlite.ts +878 -51
- package/src/extension/decision-channels.ts +7 -7
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +646 -91
- package/src/extension/step-message.ts +1 -0
- package/src/herdr/setup.ts +4 -4
- package/src/host/runner.ts +3 -0
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/engine.ts +8 -5
- package/src/workflows/index.ts +15 -0
- package/src/workflows/migrate-sources.ts +5 -1
- package/src/workflows/schema.ts +2 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +3 -1
- package/src/workflows/tool-input.ts +5 -2
- package/src/workflows/types.ts +4 -3
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");
|
package/src/host/runner.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/workflows/engine.ts
CHANGED
|
@@ -983,9 +983,11 @@ export class WorkflowEngine {
|
|
|
983
983
|
if (abort.signal.aborted) {
|
|
984
984
|
throw abortError(abort.signal);
|
|
985
985
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
986
|
+
if (timeoutMs !== null) {
|
|
987
|
+
timer = setTimeout(() => {
|
|
988
|
+
abort.abort(new TimeoutError(timeoutMs));
|
|
989
|
+
}, timeoutMs);
|
|
990
|
+
}
|
|
989
991
|
this.activeAttempt = { runDir, state, nodeId, attemptId, signal: abort.signal };
|
|
990
992
|
const dispatched = this.dispatchNode(
|
|
991
993
|
workflow,
|
|
@@ -1047,8 +1049,9 @@ export class WorkflowEngine {
|
|
|
1047
1049
|
node: WorkflowNodeDefinition,
|
|
1048
1050
|
context: WorkflowNodeContext,
|
|
1049
1051
|
abort: AbortController,
|
|
1050
|
-
): Promise<number> {
|
|
1052
|
+
): Promise<number | null> {
|
|
1051
1053
|
const configured = node.timeoutMs;
|
|
1054
|
+
if (configured === null) return null;
|
|
1052
1055
|
if (typeof configured !== "function") {
|
|
1053
1056
|
return assertValidTimeout(configured ?? this.defaultNodeTimeoutMs);
|
|
1054
1057
|
}
|
|
@@ -1061,7 +1064,7 @@ export class WorkflowEngine {
|
|
|
1061
1064
|
Promise.resolve(configured(context)),
|
|
1062
1065
|
abortRejection(abort.signal),
|
|
1063
1066
|
]);
|
|
1064
|
-
return assertValidTimeout(resolved);
|
|
1067
|
+
return resolved === null ? null : assertValidTimeout(resolved);
|
|
1065
1068
|
} finally {
|
|
1066
1069
|
clearTimeout(timer);
|
|
1067
1070
|
}
|
package/src/workflows/index.ts
CHANGED
|
@@ -74,6 +74,21 @@ export {
|
|
|
74
74
|
type WorkflowSearchPaths,
|
|
75
75
|
} from "./loader.js";
|
|
76
76
|
export { renderShellCommand, runShellAction } from "./shell.js";
|
|
77
|
+
export {
|
|
78
|
+
COMMAND_BATCH_RESULT_SCHEMA,
|
|
79
|
+
MAX_COMMAND_BATCH_CONCURRENCY,
|
|
80
|
+
MAX_COMMAND_BATCH_ITEMS,
|
|
81
|
+
MAX_COMMAND_BATCH_OUTPUT_CHARS,
|
|
82
|
+
MAX_COMMAND_BATCH_TIMEOUT_MS,
|
|
83
|
+
runCommandBatch,
|
|
84
|
+
validateCommandBatchRequest,
|
|
85
|
+
type CommandBatchItem,
|
|
86
|
+
type CommandBatchItemOutcome,
|
|
87
|
+
type CommandBatchItemResult,
|
|
88
|
+
type CommandBatchRequest,
|
|
89
|
+
type CommandBatchResult,
|
|
90
|
+
type RunCommandBatchOptions,
|
|
91
|
+
} from "./command-batch.js";
|
|
77
92
|
export {
|
|
78
93
|
estimateProgress,
|
|
79
94
|
formatProgressLine,
|
|
@@ -24,7 +24,11 @@ export type LegacySourceMigrationQueue = {
|
|
|
24
24
|
leaseMs: number;
|
|
25
25
|
}): boolean;
|
|
26
26
|
parkWorkflowRun(options: { runId: string; claimToken: string }): boolean;
|
|
27
|
-
getWorkflowRun(runId: string):
|
|
27
|
+
getWorkflowRun(runId: string):
|
|
28
|
+
| {
|
|
29
|
+
status: "queued" | "starting" | "running" | "parked" | "done" | "failed" | "cancelled";
|
|
30
|
+
}
|
|
31
|
+
| undefined;
|
|
28
32
|
setWorkflowRunOriginSession?(runId: string, originSessionId: string): boolean;
|
|
29
33
|
};
|
|
30
34
|
|
package/src/workflows/schema.ts
CHANGED
|
@@ -35,10 +35,11 @@ function assertOptionalFunction(value: unknown, description: string): void {
|
|
|
35
35
|
function assertCommonNodeFields(node: WorkflowNodeDefinition, nodeId: string): void {
|
|
36
36
|
if (
|
|
37
37
|
node.timeoutMs !== undefined &&
|
|
38
|
+
node.timeoutMs !== null &&
|
|
38
39
|
typeof node.timeoutMs !== "function" &&
|
|
39
40
|
(typeof node.timeoutMs !== "number" || !Number.isFinite(node.timeoutMs) || node.timeoutMs <= 0)
|
|
40
41
|
) {
|
|
41
|
-
fail(`node ${nodeId} timeoutMs must be a finite positive number or function`);
|
|
42
|
+
fail(`node ${nodeId} timeoutMs must be null, a finite positive number, or a function`);
|
|
42
43
|
}
|
|
43
44
|
if (node.statusDetail !== undefined && typeof node.statusDetail !== "string") {
|
|
44
45
|
fail(`node ${nodeId} statusDetail must be a string`);
|
package/src/workflows/shell.ts
CHANGED
|
@@ -26,6 +26,16 @@ export function renderShellCommand(command: string, args: string[]): string {
|
|
|
26
26
|
return renderedArgs.length > 0 ? `${command} ${renderedArgs}` : command;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function shellOutputTruncation(result: ShellActionResult): {
|
|
30
|
+
stdout: boolean;
|
|
31
|
+
stderr: boolean;
|
|
32
|
+
} {
|
|
33
|
+
return {
|
|
34
|
+
stdout: result.stdout.endsWith(TRUNCATION_MARKER),
|
|
35
|
+
stderr: result.stderr.endsWith(TRUNCATION_MARKER),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
function shellFailure(
|
|
30
40
|
spec: ShellActionExecution,
|
|
31
41
|
args: string[],
|
package/src/workflows/store.ts
CHANGED
|
@@ -1417,7 +1417,9 @@ function snapshotNode(
|
|
|
1417
1417
|
: exit !== undefined
|
|
1418
1418
|
? { includeTransition: "exit" as const }
|
|
1419
1419
|
: {}),
|
|
1420
|
-
...(typeof node.timeoutMs === "number"
|
|
1420
|
+
...(typeof node.timeoutMs === "number" || node.timeoutMs === null
|
|
1421
|
+
? { timeoutMs: node.timeoutMs }
|
|
1422
|
+
: {}),
|
|
1421
1423
|
...(node.statusDetail !== undefined ? { statusDetail: node.statusDetail } : {}),
|
|
1422
1424
|
};
|
|
1423
1425
|
if (node.nodeType === "agent" && node.expectedOutput !== undefined) {
|
|
@@ -11,7 +11,7 @@ const inputSchema = Type.Unknown({
|
|
|
11
11
|
description: "Checkpoint answer for answer; optional structured workflow input for start",
|
|
12
12
|
});
|
|
13
13
|
const runIdSchema = Type.String({
|
|
14
|
-
description: "Run id; optional for status and answer",
|
|
14
|
+
description: "Run id; optional for status, cancel, and answer",
|
|
15
15
|
});
|
|
16
16
|
const stepSchema = Type.String({
|
|
17
17
|
description: "Workflow step id; required when action is update or submit",
|
|
@@ -50,7 +50,10 @@ export const WorkflowActionSchemas = {
|
|
|
50
50
|
),
|
|
51
51
|
pause: Type.Object({ action: Type.Literal("pause") }, noExtraProperties),
|
|
52
52
|
resume: Type.Object({ action: Type.Literal("resume") }, noExtraProperties),
|
|
53
|
-
cancel: Type.Object(
|
|
53
|
+
cancel: Type.Object(
|
|
54
|
+
{ action: Type.Literal("cancel"), runId: Type.Optional(runIdSchema) },
|
|
55
|
+
noExtraProperties,
|
|
56
|
+
),
|
|
54
57
|
answer: Type.Object(
|
|
55
58
|
{
|
|
56
59
|
action: Type.Literal("answer"),
|
package/src/workflows/types.ts
CHANGED
|
@@ -71,9 +71,10 @@ export type WorkflowProgressData = {
|
|
|
71
71
|
export type WorkflowNodeCommon = {
|
|
72
72
|
/**
|
|
73
73
|
* Per-node timeout or a callback that derives it from the run context.
|
|
74
|
-
*
|
|
74
|
+
* Null disables the wall-clock deadline. Omission falls back to the engine
|
|
75
|
+
* default (15 minutes).
|
|
75
76
|
*/
|
|
76
|
-
timeoutMs?: number | ((context: WorkflowNodeContext) => MaybePromise<number>);
|
|
77
|
+
timeoutMs?: number | null | ((context: WorkflowNodeContext) => MaybePromise<number | null>);
|
|
77
78
|
/** Short human-readable label shown in the viewer while the node runs. */
|
|
78
79
|
statusDetail?: string;
|
|
79
80
|
};
|
|
@@ -715,7 +716,7 @@ export type WorkflowRunState = {
|
|
|
715
716
|
|
|
716
717
|
export type WorkflowNodeSnapshot = {
|
|
717
718
|
nodeType: WorkflowNodeDefinition["nodeType"];
|
|
718
|
-
timeoutMs?: number;
|
|
719
|
+
timeoutMs?: number | null;
|
|
719
720
|
statusDetail?: string;
|
|
720
721
|
summary?: string;
|
|
721
722
|
expectedOutput?: string;
|