@codemation/core 0.11.0 → 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/CHANGELOG.md +16 -0
- package/dist/CostCatalogContract-DD7fQ4FF.d.cts +19 -0
- package/dist/{EngineRuntimeRegistration.types-MPYWsEM0.d.cts → EngineRuntimeRegistration.types-DTV5_7Jw.d.cts} +3 -2
- package/dist/{EngineRuntimeRegistration.types-BZ_1XWAJ.d.ts → EngineRuntimeRegistration.types-Dl92Hdoi.d.ts} +2 -2
- package/dist/InMemoryRunDataFactory-qMiYjhCK.d.cts +202 -0
- package/dist/{InMemoryRunEventBusRegistry-sM4z4n_i.js → InMemoryRunEventBusRegistry-Bwunvt1T.js} +1 -1
- package/dist/{InMemoryRunEventBusRegistry-sM4z4n_i.js.map → InMemoryRunEventBusRegistry-Bwunvt1T.js.map} +1 -1
- package/dist/{InMemoryRunEventBusRegistry-VM3OWnHo.cjs → InMemoryRunEventBusRegistry-Sa86VxuV.cjs} +1 -1
- package/dist/{InMemoryRunEventBusRegistry-VM3OWnHo.cjs.map → InMemoryRunEventBusRegistry-Sa86VxuV.cjs.map} +1 -1
- package/dist/ItemsInputNormalizer-BhuxvZh5.js +36 -0
- package/dist/ItemsInputNormalizer-BhuxvZh5.js.map +1 -0
- package/dist/ItemsInputNormalizer-C09a7iFP.d.ts +321 -0
- package/dist/ItemsInputNormalizer-DLaD6rTl.d.cts +407 -0
- package/dist/ItemsInputNormalizer-Div-fb6a.cjs +43 -0
- package/dist/ItemsInputNormalizer-Div-fb6a.cjs.map +1 -0
- package/dist/RunIntentService-BOSGwmqn.d.ts +299 -0
- package/dist/RunIntentService-CWMMrAP4.d.cts +220 -0
- package/dist/{RunIntentService-MUHJ1bhO.d.cts → agentMcpTypes-DUmniLOY.d.cts} +183 -206
- package/dist/bootstrap/index.cjs +4 -2
- package/dist/bootstrap/index.d.cts +63 -5
- package/dist/bootstrap/index.d.ts +5 -4
- package/dist/bootstrap/index.js +4 -2
- package/dist/{bootstrap-Dgzsjoj7.js → bootstrap-CKTMMNmL.js} +174 -3
- package/dist/bootstrap-CKTMMNmL.js.map +1 -0
- package/dist/{bootstrap-dVmpU1ju.cjs → bootstrap-D460dCgS.cjs} +209 -36
- package/dist/bootstrap-D460dCgS.cjs.map +1 -0
- package/dist/browser.cjs +17 -0
- package/dist/browser.d.cts +4 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +4 -0
- package/dist/contracts-CK0x6w_G.cjs +74 -0
- package/dist/contracts-CK0x6w_G.cjs.map +1 -0
- package/dist/contracts-DXdfTdpW.js +50 -0
- package/dist/contracts-DXdfTdpW.js.map +1 -0
- package/dist/contracts.cjs +6 -0
- package/dist/contracts.d.cts +5 -0
- package/dist/contracts.d.ts +2 -0
- package/dist/contracts.js +3 -0
- package/dist/di-DdsgWfVy.js +405 -0
- package/dist/di-DdsgWfVy.js.map +1 -0
- package/dist/di-tO6R7VJV.cjs +524 -0
- package/dist/di-tO6R7VJV.cjs.map +1 -0
- package/dist/executionPersistenceContracts-DenJJK2T.d.cts +275 -0
- package/dist/{index-Bes88mxT.d.ts → index-BZDhEQ6W.d.ts} +278 -415
- package/dist/{RunIntentService-BrEq6Jm6.d.ts → index-CSKKuK60.d.ts} +441 -286
- package/dist/index.cjs +97 -250
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +395 -803
- package/dist/index.d.ts +5 -3
- package/dist/index.js +58 -224
- package/dist/index.js.map +1 -1
- package/dist/params-DqRvku2h.d.cts +44 -0
- package/dist/{runtime-Duf3ClPw.js → runtime-BPZgnZ9G.js} +591 -382
- package/dist/runtime-BPZgnZ9G.js.map +1 -0
- package/dist/{runtime-vH0EeZzH.cjs → runtime-CyW9c9XM.cjs} +651 -500
- package/dist/runtime-CyW9c9XM.cjs.map +1 -0
- package/dist/testing.cjs +23 -21
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +3 -2
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +5 -3
- package/dist/testing.js.map +1 -1
- package/package.json +9 -5
- package/src/ai/AgentConnectionNodeCollector.ts +1 -1
- package/src/authoring/defineHumanApprovalNode.types.ts +379 -0
- package/src/authoring/index.ts +6 -0
- package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +29 -0
- package/src/contracts/CodemationTelemetryAttributeNames.ts +10 -0
- package/src/contracts/credentialTypes.ts +10 -0
- package/src/contracts/hitlSeamTypes.ts +34 -0
- package/src/contracts/humanTaskStoreTypes.ts +48 -0
- package/src/contracts/inboxChannelTypes.ts +58 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/runTypes.ts +61 -3
- package/src/contracts/runtimeTypes.ts +112 -0
- package/src/credentials/CredentialMaterialProvider.types.ts +61 -0
- package/src/credentials/ManagedCredentialMaterialWriteError.ts +14 -0
- package/src/credentials/ManagedMaterialFetchError.ts +16 -0
- package/src/execution/ActivationEnqueueService.ts +16 -0
- package/src/execution/DefaultExecutionContextFactory.ts +11 -0
- package/src/execution/NodeExecutionSnapshotFactory.ts +7 -1
- package/src/execution/NodeExecutor.ts +60 -1
- package/src/execution/NodeExecutorFactory.ts +12 -2
- package/src/execution/NodeSuspensionHandler.ts +220 -0
- package/src/execution/PersistedRunStateTerminalBuilder.ts +5 -2
- package/src/execution/RunStateSemantics.ts +5 -0
- package/src/execution/RunSuspendedError.ts +21 -0
- package/src/index.ts +40 -0
- package/src/orchestration/Engine.ts +12 -2
- package/src/orchestration/EngineWaiters.ts +1 -1
- package/src/orchestration/NodeExecutionRequestHandlerService.ts +25 -2
- package/src/orchestration/RunContinuationService.ts +226 -2
- package/src/orchestration/TestSuiteOrchestrator.ts +5 -4
- package/src/runtime/RunIntentService.ts +3 -0
- package/src/workflow/dsl/ChainCursorResolver.ts +36 -0
- package/tsdown.config.ts +1 -1
- package/dist/InMemoryRunDataFactory-hmkh0lzR.d.cts +0 -138
- package/dist/bootstrap-Dgzsjoj7.js.map +0 -1
- package/dist/bootstrap-dVmpU1ju.cjs.map +0 -1
- package/dist/runtime-Duf3ClPw.js.map +0 -1
- package/dist/runtime-vH0EeZzH.cjs.map +0 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import type { HitlResumeTokenSignerSeam, HitlTimeoutJobSchedulerSeam } from "../contracts/hitlSeamTypes";
|
|
5
|
+
import type { HumanTaskRecord, HumanTaskStore } from "../contracts/humanTaskStoreTypes";
|
|
6
|
+
import type {
|
|
7
|
+
HumanTaskHandle,
|
|
8
|
+
NodeActivationId,
|
|
9
|
+
NodeId,
|
|
10
|
+
PersistedRunState,
|
|
11
|
+
PersistedSuspensionEntry,
|
|
12
|
+
RunId,
|
|
13
|
+
SuspensionRequest,
|
|
14
|
+
WorkflowExecutionRepository,
|
|
15
|
+
} from "../types";
|
|
16
|
+
import type { TelemetryScope } from "../contracts/telemetryTypes";
|
|
17
|
+
import { CodemationTelemetryAttributeNames } from "../contracts/CodemationTelemetryAttributeNames";
|
|
18
|
+
|
|
19
|
+
import { RunSuspendedError } from "./RunSuspendedError";
|
|
20
|
+
export { RunSuspendedError };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handles per-item `SuspensionRequest` catches in the engine's item execution loop.
|
|
24
|
+
*
|
|
25
|
+
* Responsibilities:
|
|
26
|
+
* 1. Generate a `taskId` (UUID v4).
|
|
27
|
+
* 2. Persist a `HumanTask` row via `HumanTaskStore.create`.
|
|
28
|
+
* 3. Sign a resume URL via `HitlResumeTokenSigner.sign`.
|
|
29
|
+
* 4. Enqueue a delayed BullMQ timeout job via `HitlTimeoutJobScheduler.enqueue`.
|
|
30
|
+
* 5. Build a `HumanTaskHandle` and call `deliver`.
|
|
31
|
+
* 6. Append a `PersistedSuspensionEntry` to the run state and flip status to `"suspended"`.
|
|
32
|
+
* 7. Persist via `WorkflowExecutionRepository.save`.
|
|
33
|
+
* 8. Throw `RunSuspendedError` so the caller can exit cleanly.
|
|
34
|
+
*
|
|
35
|
+
* If `deliver` throws, the error propagates up to `NodeExecutionRequestHandlerService`
|
|
36
|
+
* which routes it through `resumeFromNodeError` → run status becomes `"failed"`.
|
|
37
|
+
*
|
|
38
|
+
* `humanTaskStore`, `tokenSigner`, and `timeoutScheduler` are optional —
|
|
39
|
+
* when not registered (e.g. in unit tests), the handler still suspends the run but
|
|
40
|
+
* skips persistence, token signing, and job scheduling.
|
|
41
|
+
*/
|
|
42
|
+
export class NodeSuspensionHandler {
|
|
43
|
+
constructor(
|
|
44
|
+
private readonly workflowExecutionRepository: WorkflowExecutionRepository,
|
|
45
|
+
private readonly humanTaskStore?: HumanTaskStore,
|
|
46
|
+
private readonly tokenSigner?: HitlResumeTokenSignerSeam,
|
|
47
|
+
private readonly timeoutScheduler?: HitlTimeoutJobSchedulerSeam,
|
|
48
|
+
/** Workspace ID to stamp on HumanTaskRecord in managed mode (T7 security fix). Null in non-managed mode. */
|
|
49
|
+
private readonly workspaceId?: string,
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
async handle(args: {
|
|
53
|
+
runId: RunId;
|
|
54
|
+
nodeId: NodeId;
|
|
55
|
+
activationId: NodeActivationId;
|
|
56
|
+
itemIndex: number;
|
|
57
|
+
suspensionRequest: SuspensionRequest;
|
|
58
|
+
state: PersistedRunState;
|
|
59
|
+
/** Telemetry scope of the node's per-item span. Used to emit `hitl.task.*` span events. */
|
|
60
|
+
telemetry?: TelemetryScope;
|
|
61
|
+
}): Promise<never> {
|
|
62
|
+
const taskId = `htask_${globalThis.crypto.randomUUID()}`;
|
|
63
|
+
const { timeout, onTimeout, deliver, decisionSchema, subject, metadata } = args.suspensionRequest.request;
|
|
64
|
+
|
|
65
|
+
const timeoutMs = this.parseDurationMs(timeout);
|
|
66
|
+
const expiresAt = new Date(Date.now() + timeoutMs);
|
|
67
|
+
|
|
68
|
+
const decisionSchemaHash = this.hashSchema(decisionSchema);
|
|
69
|
+
const decisionSchemaJson = this.schemaToJson(decisionSchema);
|
|
70
|
+
|
|
71
|
+
// Build resume token (when signer is available)
|
|
72
|
+
let resumeUrl = "";
|
|
73
|
+
let resumeTokenHash = "";
|
|
74
|
+
if (this.tokenSigner) {
|
|
75
|
+
const token = this.tokenSigner.sign({ taskId, expiresAt, schemaHash: decisionSchemaHash });
|
|
76
|
+
resumeUrl = token; // callers (deliver) receive the raw token; inbox layers wrap into a URL
|
|
77
|
+
resumeTokenHash = this.tokenSigner.hashToken(token);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handle: HumanTaskHandle = {
|
|
81
|
+
taskId,
|
|
82
|
+
runId: args.runId,
|
|
83
|
+
nodeId: args.nodeId,
|
|
84
|
+
expiresAt,
|
|
85
|
+
resumeUrl,
|
|
86
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Emit hitl.task.created before calling deliver.
|
|
90
|
+
const channel = (metadata as Record<string, unknown> | undefined)?.["channel"];
|
|
91
|
+
await args.telemetry?.addSpanEvent?.({
|
|
92
|
+
name: "hitl.task.created",
|
|
93
|
+
attributes: {
|
|
94
|
+
[CodemationTelemetryAttributeNames.hitlTaskId]: taskId,
|
|
95
|
+
[CodemationTelemetryAttributeNames.hitlChannel]: typeof channel === "string" ? channel : "unknown",
|
|
96
|
+
[CodemationTelemetryAttributeNames.runId]: args.runId,
|
|
97
|
+
[CodemationTelemetryAttributeNames.nodeId]: args.nodeId,
|
|
98
|
+
expiresAt: expiresAt.toISOString(),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// D5: deliver throws → emit hitl.task.delivery_failed, then propagate upward;
|
|
103
|
+
// caller routes to resumeFromNodeError → "failed"
|
|
104
|
+
let deliveryRef: Awaited<ReturnType<typeof deliver>>;
|
|
105
|
+
try {
|
|
106
|
+
deliveryRef = await deliver(handle);
|
|
107
|
+
} catch (deliverError) {
|
|
108
|
+
await args.telemetry?.addSpanEvent?.({
|
|
109
|
+
name: "hitl.task.delivery_failed",
|
|
110
|
+
attributes: {
|
|
111
|
+
[CodemationTelemetryAttributeNames.hitlTaskId]: taskId,
|
|
112
|
+
[CodemationTelemetryAttributeNames.hitlChannel]: typeof channel === "string" ? channel : "unknown",
|
|
113
|
+
error: deliverError instanceof Error ? deliverError.message : String(deliverError),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
throw deliverError;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Persist HumanTask row
|
|
120
|
+
if (this.humanTaskStore) {
|
|
121
|
+
const record: HumanTaskRecord = {
|
|
122
|
+
id: taskId,
|
|
123
|
+
runId: args.runId,
|
|
124
|
+
workflowId: args.state.workflowId,
|
|
125
|
+
// T7: stamp workspaceId in managed mode so HitlCallbackHandler can assert workspace identity.
|
|
126
|
+
// Non-managed mode leaves this undefined (null in DB) — the check in HitlCallbackHandler
|
|
127
|
+
// is guarded by `task.workspaceId !== undefined` and is a no-op when null.
|
|
128
|
+
workspaceId: this.workspaceId ?? undefined,
|
|
129
|
+
nodeId: args.nodeId,
|
|
130
|
+
activationId: args.activationId,
|
|
131
|
+
itemIndex: args.itemIndex,
|
|
132
|
+
status: "pending",
|
|
133
|
+
channel: "local",
|
|
134
|
+
subject,
|
|
135
|
+
metadata: (metadata as Record<string, import("../contracts/workflowTypes").JsonValue>) ?? {},
|
|
136
|
+
decisionSchemaJson,
|
|
137
|
+
decisionSchemaHash,
|
|
138
|
+
onTimeout,
|
|
139
|
+
deliveryRef,
|
|
140
|
+
resumeTokenHash: resumeTokenHash || "no-token",
|
|
141
|
+
expiresAt,
|
|
142
|
+
createdAt: new Date(),
|
|
143
|
+
};
|
|
144
|
+
await this.humanTaskStore.create(record);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Enqueue timeout job
|
|
148
|
+
if (this.timeoutScheduler) {
|
|
149
|
+
await this.timeoutScheduler.enqueueTimeoutJob({ taskId, expiresAt });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const entry: PersistedSuspensionEntry = {
|
|
153
|
+
taskId,
|
|
154
|
+
nodeId: args.nodeId,
|
|
155
|
+
activationId: args.activationId,
|
|
156
|
+
itemIndex: args.itemIndex,
|
|
157
|
+
decisionSchemaHash,
|
|
158
|
+
deliveryRef,
|
|
159
|
+
timeoutAt: expiresAt.toISOString(),
|
|
160
|
+
onTimeout,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const existingSuspensions = args.state.suspension ?? [];
|
|
164
|
+
const updatedState: PersistedRunState = {
|
|
165
|
+
...args.state,
|
|
166
|
+
status: "suspended",
|
|
167
|
+
suspension: [...existingSuspensions, entry],
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
await this.workflowExecutionRepository.save(updatedState);
|
|
171
|
+
|
|
172
|
+
throw new RunSuspendedError(args.runId, taskId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Parse a duration string into milliseconds.
|
|
177
|
+
* Accepts ISO 8601 durations ("PT24H", "PT30M") and shorthand ("24h", "30m", "1d").
|
|
178
|
+
* Throws for unrecognised formats.
|
|
179
|
+
*/
|
|
180
|
+
private parseDurationMs(duration: string): number {
|
|
181
|
+
// Shorthand: "24h", "30m", "7d", "3600s"
|
|
182
|
+
const shorthand = /^(\d+(?:\.\d+)?)(s|m|h|d)$/i.exec(duration);
|
|
183
|
+
if (shorthand) {
|
|
184
|
+
const value = parseFloat(shorthand[1]!);
|
|
185
|
+
const unit = shorthand[2]!.toLowerCase();
|
|
186
|
+
const multipliers: Record<string, number> = {
|
|
187
|
+
s: 1_000,
|
|
188
|
+
m: 60_000,
|
|
189
|
+
h: 3_600_000,
|
|
190
|
+
d: 86_400_000,
|
|
191
|
+
};
|
|
192
|
+
return value * multipliers[unit]!;
|
|
193
|
+
}
|
|
194
|
+
// ISO 8601 duration subset: PTnHnMnS (days handled via P1D)
|
|
195
|
+
const iso = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/.exec(duration);
|
|
196
|
+
if (iso) {
|
|
197
|
+
const days = parseFloat(iso[1] ?? "0");
|
|
198
|
+
const hours = parseFloat(iso[2] ?? "0");
|
|
199
|
+
const minutes = parseFloat(iso[3] ?? "0");
|
|
200
|
+
const seconds = parseFloat(iso[4] ?? "0");
|
|
201
|
+
return (days * 86_400 + hours * 3_600 + minutes * 60 + seconds) * 1_000;
|
|
202
|
+
}
|
|
203
|
+
throw new Error(`NodeSuspensionHandler: unrecognised duration format: "${duration}"`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private hashSchema(schema: unknown): string {
|
|
207
|
+
const json = this.schemaToJson(schema);
|
|
208
|
+
return createHash("sha256").update(json).digest("hex");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private schemaToJson(schema: unknown): string {
|
|
212
|
+
if (schema instanceof z.ZodType) {
|
|
213
|
+
return JSON.stringify(z.toJSONSchema(schema));
|
|
214
|
+
}
|
|
215
|
+
if (typeof (schema as { toJSON?: unknown }).toJSON === "function") {
|
|
216
|
+
return JSON.stringify((schema as { toJSON: () => unknown }).toJSON());
|
|
217
|
+
}
|
|
218
|
+
return JSON.stringify(schema);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EngineRunCounters, PersistedRunState, RunQueueEntry } from "../types";
|
|
1
|
+
import type { EngineRunCounters, PersistedRunState, RunHaltReason, RunQueueEntry } from "../types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Merges common terminal-run fields onto a loaded {@link PersistedRunState} without repeating object literals.
|
|
@@ -7,7 +7,9 @@ export class PersistedRunStateTerminalBuilder {
|
|
|
7
7
|
mergeTerminal(args: {
|
|
8
8
|
state: PersistedRunState;
|
|
9
9
|
engineCounters: EngineRunCounters;
|
|
10
|
-
status: "completed" | "failed";
|
|
10
|
+
status: "completed" | "failed" | "halted";
|
|
11
|
+
/** Populated when `status === "halted"`. */
|
|
12
|
+
reason?: RunHaltReason;
|
|
11
13
|
queue: RunQueueEntry[];
|
|
12
14
|
outputsByNode: PersistedRunState["outputsByNode"];
|
|
13
15
|
nodeSnapshotsByNodeId: NonNullable<PersistedRunState["nodeSnapshotsByNodeId"]>;
|
|
@@ -18,6 +20,7 @@ export class PersistedRunStateTerminalBuilder {
|
|
|
18
20
|
...args.state,
|
|
19
21
|
engineCounters: args.engineCounters,
|
|
20
22
|
status: args.status,
|
|
23
|
+
reason: args.reason,
|
|
21
24
|
pending: undefined,
|
|
22
25
|
queue: args.queue,
|
|
23
26
|
outputsByNode: args.outputsByNode,
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
Items,
|
|
3
3
|
NodeActivationId,
|
|
4
4
|
NodeExecutionSnapshot,
|
|
5
|
+
NodeExecutionStatus,
|
|
5
6
|
NodeId,
|
|
6
7
|
NodeInputsByPort,
|
|
7
8
|
NodeOutputs,
|
|
@@ -139,6 +140,10 @@ export class RunStateSemantics {
|
|
|
139
140
|
finishedAt: string;
|
|
140
141
|
inputsByPort: NodeInputsByPort;
|
|
141
142
|
outputs: NodeOutputs;
|
|
143
|
+
hitlStatus?: Extract<
|
|
144
|
+
NodeExecutionStatus,
|
|
145
|
+
"hitl-approved" | "hitl-rejected" | "hitl-timeout" | "hitl-auto-accepted" | "hitl-cancelled"
|
|
146
|
+
>;
|
|
142
147
|
}): NodeExecutionSnapshot {
|
|
143
148
|
const definition = args.workflow.nodes.find((node) => node.id === args.nodeId);
|
|
144
149
|
if (this.missingRuntimeExecutionMarker.isMarked(definition?.config)) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RunId } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internal sentinel thrown by {@link NodeSuspensionHandler} after persisting a suspension
|
|
5
|
+
* entry. `NodeExecutionRequestHandlerService` catches this specifically and returns cleanly —
|
|
6
|
+
* no continuation call, preventing `resumeFromNodeResult` / `resumeFromNodeError` from
|
|
7
|
+
* overwriting the `"suspended"` run status.
|
|
8
|
+
*
|
|
9
|
+
* The `Error` suffix satisfies the ESLint `no-manual-di-new` allowlist. This is NOT a
|
|
10
|
+
* user-facing error — it is an engine-internal control-flow primitive and should NOT be
|
|
11
|
+
* exported from the public barrel.
|
|
12
|
+
*/
|
|
13
|
+
export class RunSuspendedError extends Error {
|
|
14
|
+
constructor(
|
|
15
|
+
readonly runId: RunId,
|
|
16
|
+
readonly taskId: string,
|
|
17
|
+
) {
|
|
18
|
+
super(`RunSuspendedError: run ${runId} suspended on task ${taskId}`);
|
|
19
|
+
this.name = "RunSuspendedError";
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
export { SystemClock, type Clock } from "./contracts/Clock";
|
|
2
|
+
export {
|
|
3
|
+
SuspensionRequest,
|
|
4
|
+
type HumanTaskHandle,
|
|
5
|
+
type HumanTaskSubject,
|
|
6
|
+
type HumanTaskActor,
|
|
7
|
+
type HumanTaskId,
|
|
8
|
+
type Duration,
|
|
9
|
+
type ResumeContext,
|
|
10
|
+
} from "./contracts/runtimeTypes";
|
|
11
|
+
export type { PersistedSuspensionEntry, PendingResumeEntry, RunHaltReason } from "./contracts/runTypes";
|
|
12
|
+
export type { HumanTaskRecord, HumanTaskStatus, HumanTaskStore } from "./contracts/humanTaskStoreTypes";
|
|
13
|
+
export { HumanTaskStoreToken } from "./contracts/humanTaskStoreTypes";
|
|
14
|
+
export type { HitlResumeTokenSignerSeam, HitlTimeoutJobSchedulerSeam } from "./contracts/hitlSeamTypes";
|
|
15
|
+
export {
|
|
16
|
+
HitlResumeTokenSignerToken,
|
|
17
|
+
HitlTimeoutJobSchedulerToken,
|
|
18
|
+
HitlWorkspaceIdToken,
|
|
19
|
+
} from "./contracts/hitlSeamTypes";
|
|
20
|
+
export type {
|
|
21
|
+
InboxChannel,
|
|
22
|
+
InboxChannelResolverSeam,
|
|
23
|
+
InboxDeliverArgs,
|
|
24
|
+
InboxDelivery,
|
|
25
|
+
InboxOnDecisionArgs,
|
|
26
|
+
InboxOnTimeoutArgs,
|
|
27
|
+
} from "./contracts/inboxChannelTypes";
|
|
28
|
+
export {
|
|
29
|
+
InboxChannelResolverToken,
|
|
30
|
+
LocalInboxChannelToken,
|
|
31
|
+
ControlPlaneInboxChannelToken,
|
|
32
|
+
} from "./contracts/inboxChannelTypes";
|
|
2
33
|
export * from "./authoring";
|
|
3
34
|
export * from "./ai/AiHost";
|
|
4
35
|
export { AgentConnectionNodeCollector } from "./ai/AgentConnectionNodeCollector";
|
|
@@ -44,3 +75,12 @@ export type {
|
|
|
44
75
|
OAuthMaterial,
|
|
45
76
|
OAuthFlowExecutor,
|
|
46
77
|
} from "./credentials/OAuthFlowExecutor.types";
|
|
78
|
+
export type {
|
|
79
|
+
CredentialMaterialProvider,
|
|
80
|
+
CredentialMaterialRef,
|
|
81
|
+
MaterialBundle,
|
|
82
|
+
CallerContext,
|
|
83
|
+
} from "./credentials/CredentialMaterialProvider.types";
|
|
84
|
+
export { IllegalMaterialSourceError } from "./credentials/CredentialMaterialProvider.types";
|
|
85
|
+
export { ManagedCredentialMaterialWriteError } from "./credentials/ManagedCredentialMaterialWriteError";
|
|
86
|
+
export { ManagedMaterialFetchError } from "./credentials/ManagedMaterialFetchError";
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
NodeOutputs,
|
|
12
12
|
ParentExecutionRef,
|
|
13
13
|
PersistedWorkflowTokenRegistryLike,
|
|
14
|
+
ResumeContext,
|
|
14
15
|
RunExecutionOptions,
|
|
15
16
|
RunId,
|
|
16
17
|
RunResult,
|
|
@@ -77,8 +78,9 @@ interface EngineRunContinuationService {
|
|
|
77
78
|
nodeId: NodeId;
|
|
78
79
|
error: Error;
|
|
79
80
|
}): Promise<RunResult>;
|
|
80
|
-
waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" }>>;
|
|
81
|
+
waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" | "halted" }>>;
|
|
81
82
|
waitForWebhookResponse(runId: RunId): Promise<WebhookRunResult>;
|
|
83
|
+
resumeRun(args: { runId: RunId; taskId: string; resumeContext: ResumeContext }): Promise<RunResult>;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
interface EngineNodeExecutionRequestHandler {
|
|
@@ -226,7 +228,7 @@ export class Engine implements NodeActivationContinuation, NodeExecutionRequestH
|
|
|
226
228
|
return await this.deps.runContinuationService.resumeFromStepError(args);
|
|
227
229
|
}
|
|
228
230
|
|
|
229
|
-
async waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" }>> {
|
|
231
|
+
async waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" | "halted" }>> {
|
|
230
232
|
return await this.deps.runContinuationService.waitForCompletion(runId);
|
|
231
233
|
}
|
|
232
234
|
|
|
@@ -234,6 +236,14 @@ export class Engine implements NodeActivationContinuation, NodeExecutionRequestH
|
|
|
234
236
|
return await this.deps.runContinuationService.waitForWebhookResponse(runId);
|
|
235
237
|
}
|
|
236
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Re-activate a suspended run item with a human decision (HITL).
|
|
241
|
+
* The HTTP resume endpoint calls this; this method exposes the engine primitive.
|
|
242
|
+
*/
|
|
243
|
+
async resumeRun(args: { runId: RunId; taskId: string; resumeContext: ResumeContext }): Promise<RunResult> {
|
|
244
|
+
return await this.deps.runContinuationService.resumeRun(args);
|
|
245
|
+
}
|
|
246
|
+
|
|
237
247
|
async handleNodeExecutionRequest(request: NodeExecutionRequest): Promise<void> {
|
|
238
248
|
await this.deps.nodeExecutionRequestHandler.handleNodeExecutionRequest(request);
|
|
239
249
|
}
|
|
@@ -21,7 +21,7 @@ export class EngineWaiters {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
resolveRunCompletion(result: RunResult): void {
|
|
24
|
-
if (result.status !== "completed" && result.status !== "failed") return;
|
|
24
|
+
if (result.status !== "completed" && result.status !== "failed" && result.status !== "halted") return;
|
|
25
25
|
const list = this.completionWaiters.get(result.runId);
|
|
26
26
|
if (!list || list.length === 0) return;
|
|
27
27
|
this.completionWaiters.delete(result.runId);
|
|
@@ -4,12 +4,14 @@ import type {
|
|
|
4
4
|
NodeExecutionRequest,
|
|
5
5
|
NodeExecutionRequestHandler,
|
|
6
6
|
PersistedRunState,
|
|
7
|
+
ResumeContext,
|
|
7
8
|
RunDataFactory,
|
|
8
9
|
WorkflowDefinition,
|
|
9
10
|
WorkflowExecutionRepository,
|
|
10
11
|
WorkflowSnapshotResolver,
|
|
11
12
|
} from "../types";
|
|
12
13
|
import type { EngineExecutionLimitsPolicy } from "../policies/executionLimits/EngineExecutionLimitsPolicy";
|
|
14
|
+
import { RunSuspendedError } from "../execution/RunSuspendedError";
|
|
13
15
|
import { NodeActivationRequestComposer } from "../execution/NodeActivationRequestComposer";
|
|
14
16
|
import { NodeRunStateWriterFactory } from "../execution/NodeRunStateWriterFactory";
|
|
15
17
|
import { WorkflowRunExecutionContextFactory } from "../execution/WorkflowRunExecutionContextFactory";
|
|
@@ -84,6 +86,14 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
84
86
|
const portKeys = Object.keys(inputsByPort);
|
|
85
87
|
const kind = portKeys.length === 1 && portKeys[0] === "in" ? ("single" as const) : ("multi" as const);
|
|
86
88
|
const batchId = pendingExecution.batchId ?? "batch_1";
|
|
89
|
+
// Splice resumeContext from pendingResume if this activation is a HITL resume.
|
|
90
|
+
const pendingResume = state.pendingResume;
|
|
91
|
+
const resumeContext: ResumeContext | undefined =
|
|
92
|
+
pendingResume?.activationId === request.activationId && pendingResume?.nodeId === request.nodeId
|
|
93
|
+
? (pendingResume.resumeContext as ResumeContext)
|
|
94
|
+
: undefined;
|
|
95
|
+
const baseWithResume = resumeContext != null ? { ...base, resumeContext } : base;
|
|
96
|
+
|
|
87
97
|
const activationRequest =
|
|
88
98
|
kind === "multi"
|
|
89
99
|
? this.nodeActivationRequestComposer.createMultiFromDefinitionWithActivation({
|
|
@@ -92,7 +102,7 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
92
102
|
workflowId: request.workflowId,
|
|
93
103
|
parent: resolvedParent,
|
|
94
104
|
executionOptions: request.executionOptions ?? state.executionOptions,
|
|
95
|
-
base,
|
|
105
|
+
base: baseWithResume,
|
|
96
106
|
data,
|
|
97
107
|
definition: {
|
|
98
108
|
id: definition.id,
|
|
@@ -107,7 +117,7 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
107
117
|
workflowId: request.workflowId,
|
|
108
118
|
parent: resolvedParent,
|
|
109
119
|
executionOptions: request.executionOptions ?? state.executionOptions,
|
|
110
|
-
base,
|
|
120
|
+
base: baseWithResume,
|
|
111
121
|
data,
|
|
112
122
|
definition: {
|
|
113
123
|
id: definition.id,
|
|
@@ -117,6 +127,14 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
117
127
|
input: inputsByPort.in ?? request.input ?? [],
|
|
118
128
|
});
|
|
119
129
|
|
|
130
|
+
// Clear pendingResume from state now that we have consumed it.
|
|
131
|
+
if (resumeContext != null) {
|
|
132
|
+
const clearedState = await this.workflowExecutionRepository.load(request.runId);
|
|
133
|
+
if (clearedState?.pendingResume?.activationId === request.activationId) {
|
|
134
|
+
await this.workflowExecutionRepository.save({ ...clearedState, pendingResume: undefined });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
120
138
|
await this.continuation.markNodeRunning({
|
|
121
139
|
runId: activationRequest.runId,
|
|
122
140
|
activationId: activationRequest.activationId,
|
|
@@ -128,6 +146,11 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
|
|
|
128
146
|
try {
|
|
129
147
|
outputs = await this.nodeExecutor.execute(activationRequest);
|
|
130
148
|
} catch (error) {
|
|
149
|
+
if (error instanceof RunSuspendedError) {
|
|
150
|
+
// The node threw SuspensionRequest; NodeSuspensionHandler already persisted the
|
|
151
|
+
// suspension entry and flipped status to "suspended". Nothing more to do here.
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
131
154
|
await this.resumeAfterExecutionError(activationRequest, this.asError(error));
|
|
132
155
|
return;
|
|
133
156
|
}
|