@codemation/core 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/CHANGELOG.md +6 -0
- package/dist/{CostCatalogContract-DZgcUBE4.d.cts → CostCatalogContract-DD7fQ4FF.d.cts} +2 -2
- package/dist/{EngineRuntimeRegistration.types-Cggm5GVY.d.cts → EngineRuntimeRegistration.types-DTV5_7Jw.d.cts} +3 -3
- package/dist/{EngineRuntimeRegistration.types-BQbS9_gs.d.ts → EngineRuntimeRegistration.types-Dl92Hdoi.d.ts} +2 -2
- package/dist/InMemoryRunDataFactory-qMiYjhCK.d.cts +202 -0
- package/dist/{ItemsInputNormalizer-D-MH8MBs.js → ItemsInputNormalizer-BhuxvZh5.js} +2 -2
- package/dist/{ItemsInputNormalizer-D-MH8MBs.js.map → ItemsInputNormalizer-BhuxvZh5.js.map} +1 -1
- package/dist/{ItemsInputNormalizer-_Mfcd3YU.d.ts → ItemsInputNormalizer-C09a7iFP.d.ts} +2 -2
- package/dist/{ItemsInputNormalizer-C_dpn76M.d.cts → ItemsInputNormalizer-DLaD6rTl.d.cts} +3 -3
- package/dist/{ItemsInputNormalizer-CwdOhSAK.cjs → ItemsInputNormalizer-Div-fb6a.cjs} +2 -2
- package/dist/{ItemsInputNormalizer-CwdOhSAK.cjs.map → ItemsInputNormalizer-Div-fb6a.cjs.map} +1 -1
- package/dist/{RunIntentService-BVur7x9n.d.ts → RunIntentService-BOSGwmqn.d.ts} +18 -4
- package/dist/{RunIntentService-CEF-sFfI.d.cts → RunIntentService-CWMMrAP4.d.cts} +18 -4
- package/dist/{agentMcpTypes-ZiNbNsEi.d.cts → agentMcpTypes-DUmniLOY.d.cts} +183 -4
- package/dist/bootstrap/index.cjs +3 -3
- package/dist/bootstrap/index.d.cts +63 -7
- package/dist/bootstrap/index.d.ts +5 -5
- package/dist/bootstrap/index.js +3 -3
- package/dist/{bootstrap-D_Yyi0wL.js → bootstrap-CKTMMNmL.js} +173 -4
- package/dist/bootstrap-CKTMMNmL.js.map +1 -0
- package/dist/{bootstrap-BxuTFTLB.cjs → bootstrap-D460dCgS.cjs} +175 -4
- package/dist/bootstrap-D460dCgS.cjs.map +1 -0
- package/dist/browser.cjs +3 -2
- package/dist/browser.d.cts +4 -4
- package/dist/browser.d.ts +3 -3
- package/dist/browser.js +3 -3
- package/dist/contracts.d.cts +5 -5
- package/dist/contracts.d.ts +2 -2
- package/dist/{di-0Wop7z1y.js → di-DdsgWfVy.js} +31 -2
- package/dist/di-DdsgWfVy.js.map +1 -0
- package/dist/{di-BlEKdoZS.cjs → di-tO6R7VJV.cjs} +36 -1
- package/dist/di-tO6R7VJV.cjs.map +1 -0
- package/dist/{executionPersistenceContracts-BgZMRsTa.d.cts → executionPersistenceContracts-DenJJK2T.d.cts} +2 -2
- package/dist/{index-62Ba9f7D.d.ts → index-BZDhEQ6W.d.ts} +277 -101
- package/dist/{index-zWGtEhrf.d.ts → index-CSKKuK60.d.ts} +441 -5
- package/dist/index.cjs +71 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +395 -97
- package/dist/index.d.ts +5 -5
- package/dist/index.js +56 -159
- package/dist/index.js.map +1 -1
- package/dist/{params-B5SENSzZ.d.cts → params-DqRvku2h.d.cts} +2 -2
- package/dist/{runtime-cxmUkk0l.js → runtime-BPZgnZ9G.js} +611 -16
- package/dist/runtime-BPZgnZ9G.js.map +1 -0
- package/dist/{runtime-DBzq5YBi.cjs → runtime-CyW9c9XM.cjs} +670 -15
- package/dist/runtime-CyW9c9XM.cjs.map +1 -0
- package/dist/testing.cjs +3 -3
- package/dist/testing.d.cts +3 -3
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +3 -3
- package/package.json +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/dist/InMemoryRunDataFactory-C7YItvHG.d.cts +0 -123
- package/dist/bootstrap-BxuTFTLB.cjs.map +0 -1
- package/dist/bootstrap-D_Yyi0wL.js.map +0 -1
- package/dist/di-0Wop7z1y.js.map +0 -1
- package/dist/di-BlEKdoZS.cjs.map +0 -1
- package/dist/runtime-DBzq5YBi.cjs.map +0 -1
- package/dist/runtime-cxmUkk0l.js.map +0 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { TypeToken } from "../di";
|
|
2
|
+
import type { HumanTaskActor, HumanTaskHandle, HumanTaskSubject } from "./runtimeTypes";
|
|
3
|
+
import type { Item } from "./workflowTypes";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A single inbox delivery channel.
|
|
7
|
+
* Implementations: `LocalInboxChannel`, `ControlPlaneInboxChannel`.
|
|
8
|
+
*/
|
|
9
|
+
export interface InboxChannel {
|
|
10
|
+
readonly kind: "local" | "control-plane-inbox";
|
|
11
|
+
deliver(args: InboxDeliverArgs): Promise<InboxDelivery>;
|
|
12
|
+
updateOnDecision?(args: InboxOnDecisionArgs): Promise<void>;
|
|
13
|
+
updateOnTimeout?(args: InboxOnTimeoutArgs): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type InboxDeliverArgs = Readonly<{
|
|
17
|
+
task: HumanTaskHandle;
|
|
18
|
+
subject: HumanTaskSubject;
|
|
19
|
+
priority: "low" | "normal" | "high";
|
|
20
|
+
item: Item;
|
|
21
|
+
/** Present in managed mode (from `PairingConfig.workspaceId`). */
|
|
22
|
+
workspaceId?: string;
|
|
23
|
+
}>;
|
|
24
|
+
|
|
25
|
+
export type InboxDelivery =
|
|
26
|
+
| { kind: "local"; inboxItemId: string }
|
|
27
|
+
| { kind: "cp"; inboxItemId: string; workspaceId: string };
|
|
28
|
+
|
|
29
|
+
export type InboxOnDecisionArgs = Readonly<{
|
|
30
|
+
delivery: InboxDelivery;
|
|
31
|
+
decision: { approved: boolean; note?: string };
|
|
32
|
+
actor: HumanTaskActor;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
export type InboxOnTimeoutArgs = Readonly<{
|
|
36
|
+
delivery: InboxDelivery;
|
|
37
|
+
policy: "halt" | "auto-accept";
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the correct `InboxChannel` for the current deployment mode
|
|
42
|
+
* (local dev vs. managed/CP). Implemented in `@codemation/host`.
|
|
43
|
+
*/
|
|
44
|
+
export interface InboxChannelResolverSeam {
|
|
45
|
+
resolve(): { channel: InboxChannel; workspaceId?: string };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const InboxChannelResolverToken = Symbol.for("codemation.core.InboxChannelResolver") as TypeToken<
|
|
49
|
+
InboxChannelResolverSeam | undefined
|
|
50
|
+
>;
|
|
51
|
+
|
|
52
|
+
export const LocalInboxChannelToken = Symbol.for("codemation.core.LocalInboxChannel") as TypeToken<
|
|
53
|
+
InboxChannel | undefined
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
export const ControlPlaneInboxChannelToken = Symbol.for("codemation.core.ControlPlaneInboxChannel") as TypeToken<
|
|
57
|
+
InboxChannel | undefined
|
|
58
|
+
>;
|
package/src/contracts/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export * from "./AgentBindError";
|
|
2
2
|
export * from "./agentMcpTypes";
|
|
3
|
+
export * from "./hitlSeamTypes";
|
|
4
|
+
export * from "./humanTaskStoreTypes";
|
|
5
|
+
export * from "./inboxChannelTypes";
|
|
3
6
|
export * from "./NoOpAgentMcpIntegration";
|
|
4
7
|
export * from "./baseTypes";
|
|
5
8
|
export * from "./assertionTypes";
|
|
@@ -128,7 +128,18 @@ export interface RunQueueEntry {
|
|
|
128
128
|
}>;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
export type NodeExecutionStatus =
|
|
131
|
+
export type NodeExecutionStatus =
|
|
132
|
+
| "pending"
|
|
133
|
+
| "queued"
|
|
134
|
+
| "running"
|
|
135
|
+
| "completed"
|
|
136
|
+
| "failed"
|
|
137
|
+
| "skipped"
|
|
138
|
+
| "hitl-approved"
|
|
139
|
+
| "hitl-rejected"
|
|
140
|
+
| "hitl-timeout"
|
|
141
|
+
| "hitl-auto-accepted"
|
|
142
|
+
| "hitl-cancelled";
|
|
132
143
|
|
|
133
144
|
export interface NodeExecutionError {
|
|
134
145
|
message: string;
|
|
@@ -245,7 +256,10 @@ export interface ExecutionFrontierPlan {
|
|
|
245
256
|
preservedPinnedNodeIds: ReadonlyArray<NodeId>;
|
|
246
257
|
}
|
|
247
258
|
|
|
248
|
-
export type RunStatus = "running" | "pending" | "completed" | "failed";
|
|
259
|
+
export type RunStatus = "running" | "pending" | "completed" | "failed" | "suspended" | "halted";
|
|
260
|
+
|
|
261
|
+
/** Reason a run transitioned to {@link RunStatus} `"halted"`. */
|
|
262
|
+
export type RunHaltReason = "hitl-rejected" | "hitl-timeout" | "hitl-cancelled";
|
|
249
263
|
|
|
250
264
|
export interface RunSummary {
|
|
251
265
|
runId: RunId;
|
|
@@ -283,6 +297,37 @@ export interface PersistedRunSchedulingState {
|
|
|
283
297
|
queue: RunQueueEntry[];
|
|
284
298
|
}
|
|
285
299
|
|
|
300
|
+
/** One persisted suspension entry per suspended item. */
|
|
301
|
+
export interface PersistedSuspensionEntry {
|
|
302
|
+
/** Opaque task identifier (UUID v4). */
|
|
303
|
+
readonly taskId: string;
|
|
304
|
+
readonly nodeId: NodeId;
|
|
305
|
+
readonly activationId: NodeActivationId;
|
|
306
|
+
readonly itemIndex: number;
|
|
307
|
+
/** SHA-256 hex digest of the decision schema JSON (for schema-drift detection). */
|
|
308
|
+
readonly decisionSchemaHash: string;
|
|
309
|
+
/** Serialized return value from `SuspensionRequest.deliver` (stored on the HumanTask row). */
|
|
310
|
+
readonly deliveryRef: JsonValue;
|
|
311
|
+
/** ISO timestamp when the task expires. */
|
|
312
|
+
readonly timeoutAt: string;
|
|
313
|
+
readonly onTimeout: "halt" | "auto-accept";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* When a node is re-activated after suspension, the engine writes the resume context here
|
|
318
|
+
* so `NodeExecutionRequestHandlerService` can splice `resumeContext` into ctx.
|
|
319
|
+
* Cleared once the re-activation is consumed.
|
|
320
|
+
*/
|
|
321
|
+
export interface PendingResumeEntry {
|
|
322
|
+
readonly activationId: NodeActivationId;
|
|
323
|
+
readonly nodeId: NodeId;
|
|
324
|
+
/**
|
|
325
|
+
* Typed as `unknown` here to avoid a circular import between runTypes ↔ runtimeTypes.
|
|
326
|
+
* `NodeExecutionRequestHandlerService` casts this to `ResumeContext` from runtimeTypes.
|
|
327
|
+
*/
|
|
328
|
+
readonly resumeContext: unknown;
|
|
329
|
+
}
|
|
330
|
+
|
|
286
331
|
export interface PersistedRunState {
|
|
287
332
|
runId: RunId;
|
|
288
333
|
workflowId: WorkflowId;
|
|
@@ -301,12 +346,24 @@ export interface PersistedRunState {
|
|
|
301
346
|
/** Successful node completions so far (for activation budget). */
|
|
302
347
|
engineCounters?: EngineRunCounters;
|
|
303
348
|
status: RunStatus;
|
|
349
|
+
/** Populated when `status === "halted"` to discriminate why the run was halted. */
|
|
350
|
+
reason?: RunHaltReason;
|
|
304
351
|
pending?: PendingNodeExecution;
|
|
305
352
|
queue: RunQueueEntry[];
|
|
306
353
|
outputsByNode: Record<NodeId, NodeOutputs>;
|
|
307
354
|
nodeSnapshotsByNodeId: Record<NodeId, NodeExecutionSnapshot>;
|
|
308
355
|
/** Append-only history of connection invocations (LLM/tool) nested under owning nodes. */
|
|
309
356
|
connectionInvocations?: ReadonlyArray<ConnectionInvocationRecord>;
|
|
357
|
+
/**
|
|
358
|
+
* One entry per outstanding HITL suspension (per-item).
|
|
359
|
+
* Present and non-empty iff `status === "suspended"`.
|
|
360
|
+
*/
|
|
361
|
+
suspension?: ReadonlyArray<PersistedSuspensionEntry>;
|
|
362
|
+
/**
|
|
363
|
+
* Written by `resumeRun()` so `NodeExecutionRequestHandlerService` can splice `resumeContext`
|
|
364
|
+
* into the ctx when re-executing the suspended node. Cleared once consumed.
|
|
365
|
+
*/
|
|
366
|
+
pendingResume?: PendingResumeEntry;
|
|
310
367
|
}
|
|
311
368
|
|
|
312
369
|
export interface WorkflowExecutionRepository {
|
|
@@ -349,7 +406,8 @@ export interface WorkflowExecutionPruneRepository {
|
|
|
349
406
|
export type RunResult =
|
|
350
407
|
| { runId: RunId; workflowId: WorkflowId; startedAt: string; status: "completed"; outputs: Items }
|
|
351
408
|
| { runId: RunId; workflowId: WorkflowId; startedAt: string; status: "pending"; pending: PendingNodeExecution }
|
|
352
|
-
| { runId: RunId; workflowId: WorkflowId; startedAt: string; status: "failed"; error: { message: string } }
|
|
409
|
+
| { runId: RunId; workflowId: WorkflowId; startedAt: string; status: "failed"; error: { message: string } }
|
|
410
|
+
| { runId: RunId; workflowId: WorkflowId; startedAt: string; status: "halted"; reason: RunHaltReason };
|
|
353
411
|
|
|
354
412
|
export type WebhookRunResult = Readonly<{
|
|
355
413
|
runId: RunId;
|
|
@@ -19,6 +19,105 @@ import type { WorkflowActivationPolicy } from "./workflowActivationPolicy";
|
|
|
19
19
|
import type { TriggerInstanceId, WebhookTriggerMatcher } from "./webhookTypes";
|
|
20
20
|
import type { ZodType } from "zod";
|
|
21
21
|
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// HITL primitives
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Opaque unique identifier for a single HumanTask instance. */
|
|
27
|
+
export type HumanTaskId = string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Duration string — ISO 8601 duration ("PT24H") or shorthand ("24h").
|
|
31
|
+
* Parsed by the timeout job; stored as-is in the suspension record.
|
|
32
|
+
*/
|
|
33
|
+
export type Duration = string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Minimal handle handed to the `deliver` callback so it can route to the correct
|
|
37
|
+
* inbox channel.
|
|
38
|
+
*/
|
|
39
|
+
export interface HumanTaskHandle {
|
|
40
|
+
readonly taskId: HumanTaskId;
|
|
41
|
+
readonly runId: string;
|
|
42
|
+
readonly nodeId: string;
|
|
43
|
+
readonly expiresAt: Date;
|
|
44
|
+
/** TODO: real signed URL; placeholder empty string for now. */
|
|
45
|
+
readonly resumeUrl: string;
|
|
46
|
+
/**
|
|
47
|
+
* Arbitrary JSON metadata copied from `SuspensionRequest.request.metadata` at suspension time.
|
|
48
|
+
* Used by the agent runtime to round-trip the `agentCheckpoint` back to the
|
|
49
|
+
* resumed node via `ctx.resumeContext.task.metadata`.
|
|
50
|
+
*/
|
|
51
|
+
readonly metadata?: Readonly<Record<string, import("./workflowTypes").JsonValue>>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Human-readable description surface shown to the reviewer. */
|
|
55
|
+
export interface HumanTaskSubject {
|
|
56
|
+
readonly title: string;
|
|
57
|
+
readonly summary: string;
|
|
58
|
+
readonly attributes?: import("./workflowTypes").JsonValue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Identity of the person who made a decision on the task. */
|
|
62
|
+
export interface HumanTaskActor {
|
|
63
|
+
readonly actorId: string;
|
|
64
|
+
readonly displayName?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resume context injected into `NodeExecutionContext` when the engine re-activates
|
|
69
|
+
* a previously suspended node. `defineHumanApprovalNode` wraps this with parsed
|
|
70
|
+
* `TDecision`; at the engine layer `decision.value` is `unknown`.
|
|
71
|
+
*/
|
|
72
|
+
export interface ResumeContext {
|
|
73
|
+
readonly decision:
|
|
74
|
+
| Readonly<{ kind: "decided"; value: unknown; actor: HumanTaskActor; decidedAt: Date }>
|
|
75
|
+
| Readonly<{ kind: "timed_out"; at: Date }>
|
|
76
|
+
| Readonly<{ kind: "auto_accepted"; at: Date }>;
|
|
77
|
+
readonly delivery: import("./workflowTypes").JsonValue;
|
|
78
|
+
readonly task: HumanTaskHandle;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Thrown by a node's `execute()` to request durable suspension of the current item.
|
|
83
|
+
* The engine catches this, persists the suspension entry, calls `deliver`, and
|
|
84
|
+
* continues to the next item (per-item semantics).
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* throw new SuspensionRequest({
|
|
89
|
+
* decisionSchema: z.object({ approved: z.boolean() }),
|
|
90
|
+
* timeout: "PT24H",
|
|
91
|
+
* onTimeout: "halt",
|
|
92
|
+
* subject: { title: "Approve invoice", summary: "Invoice #1234 needs approval" },
|
|
93
|
+
* deliver: async (handle) => {
|
|
94
|
+
* await notifySlack(handle);
|
|
95
|
+
* return { channel: "slack", ts: "..." };
|
|
96
|
+
* },
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export class SuspensionRequest<
|
|
101
|
+
TDecision = unknown,
|
|
102
|
+
TDelivery extends import("./workflowTypes").JsonValue = import("./workflowTypes").JsonValue,
|
|
103
|
+
> extends Error {
|
|
104
|
+
constructor(
|
|
105
|
+
readonly request: Readonly<{
|
|
106
|
+
decisionSchema: ZodType<TDecision>;
|
|
107
|
+
timeout: Duration;
|
|
108
|
+
onTimeout: "halt" | "auto-accept";
|
|
109
|
+
subject: HumanTaskSubject;
|
|
110
|
+
metadata?: Readonly<Record<string, import("./workflowTypes").JsonValue>>;
|
|
111
|
+
deliver: (handle: HumanTaskHandle) => Promise<TDelivery>;
|
|
112
|
+
}>,
|
|
113
|
+
) {
|
|
114
|
+
// Extending Error so wrappers like InProcessRetryRunner preserve identity
|
|
115
|
+
// (`instanceof SuspensionRequest`) instead of coercing via String(thrown).
|
|
116
|
+
super(`SuspensionRequest(${request.subject?.title ?? "untitled"})`);
|
|
117
|
+
this.name = "SuspensionRequest";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
22
121
|
import type {
|
|
23
122
|
ActivationIdFactory,
|
|
24
123
|
BinaryAttachment,
|
|
@@ -183,6 +282,14 @@ export interface ExecutionContext {
|
|
|
183
282
|
* Collections registered in the codemation config, keyed by collection name.
|
|
184
283
|
*/
|
|
185
284
|
readonly collections?: CollectionsContext;
|
|
285
|
+
/**
|
|
286
|
+
* Resolve a DI token from the host container.
|
|
287
|
+
* Allows nodes to reach host-side services (e.g. `InboxChannelResolverToken`)
|
|
288
|
+
* without importing host code. Wired by `DefaultExecutionContextFactory`; throws
|
|
289
|
+
* a clear error when no resolver is configured (e.g. in unit tests that don't
|
|
290
|
+
* set up the full container).
|
|
291
|
+
*/
|
|
292
|
+
resolve<T>(token: TypeToken<T>): T;
|
|
186
293
|
}
|
|
187
294
|
|
|
188
295
|
export interface ExecutionContextFactory {
|
|
@@ -208,6 +315,11 @@ export interface NodeExecutionContext<TConfig extends NodeConfigBase = NodeConfi
|
|
|
208
315
|
config: TConfig;
|
|
209
316
|
telemetry: NodeExecutionTelemetry;
|
|
210
317
|
binary: NodeBinaryAttachmentService;
|
|
318
|
+
/**
|
|
319
|
+
* Present when this node activation is a HITL resume.
|
|
320
|
+
* The node checks `ctx.resumeContext !== undefined` and takes the resume branch.
|
|
321
|
+
*/
|
|
322
|
+
resumeContext?: ResumeContext;
|
|
211
323
|
}
|
|
212
324
|
|
|
213
325
|
export interface PollingTriggerHandle {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { OAuthMaterial } from "./OAuthFlowExecutor.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Material provider seam — see `docs/design/credentials-oauth-unification.md`,
|
|
5
|
+
* "Material provider seam" section. Sits beside the workspace's
|
|
6
|
+
* `CredentialStore`; persistence of the row stays at the store, persistence of
|
|
7
|
+
* the bytes goes through this provider so they can live at the control plane
|
|
8
|
+
* in managed mode.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Pointer to material bytes. For local rows `ref` is the workspace instance id
|
|
13
|
+
* and the bytes co-locate with the row (existing `CredentialOAuth2Material` /
|
|
14
|
+
* `CredentialSecretMaterial` tables). For control-plane rows `ref` is the
|
|
15
|
+
* CP-side credential id; the workspace stores only the pointer.
|
|
16
|
+
*/
|
|
17
|
+
export type CredentialMaterialRef = Readonly<{
|
|
18
|
+
source: "local" | "control-plane";
|
|
19
|
+
id: string;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Decrypted material bytes returned by a provider. Shape matches
|
|
24
|
+
* `OAuthMaterial` — every supported credential type today is OAuth-shaped.
|
|
25
|
+
*/
|
|
26
|
+
export type MaterialBundle = OAuthMaterial;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Caller context recorded by the CP material endpoint per fetch (D5 in the
|
|
30
|
+
* `credentials-vault` sprint README). The local provider accepts but ignores
|
|
31
|
+
* it; standalone mode has no audit log.
|
|
32
|
+
*/
|
|
33
|
+
export type CallerContext = Readonly<{
|
|
34
|
+
workspaceId: string;
|
|
35
|
+
caller:
|
|
36
|
+
| Readonly<{ kind: "workflow-node"; workflowId: string; nodeId: string }>
|
|
37
|
+
| Readonly<{ kind: "concierge"; chatId: string }>
|
|
38
|
+
| Readonly<{ kind: "research-agent"; chatId: string }>
|
|
39
|
+
| Readonly<{ kind: "manual"; userId: string }>;
|
|
40
|
+
reason?: string;
|
|
41
|
+
}>;
|
|
42
|
+
|
|
43
|
+
export interface CredentialMaterialProvider {
|
|
44
|
+
getMaterial(ref: CredentialMaterialRef, context: CallerContext): Promise<MaterialBundle>;
|
|
45
|
+
setMaterial(ref: CredentialMaterialRef, material: MaterialBundle): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Thrown by a provider when asked to operate on a `ref.source` it does not
|
|
50
|
+
* handle (e.g. the local provider being asked to read `control-plane` bytes).
|
|
51
|
+
* Exported so `instanceof`-checks work across the workspace boundary.
|
|
52
|
+
*/
|
|
53
|
+
export class IllegalMaterialSourceError extends Error {
|
|
54
|
+
constructor(
|
|
55
|
+
public readonly source: CredentialMaterialRef["source"],
|
|
56
|
+
public readonly providerName: string,
|
|
57
|
+
) {
|
|
58
|
+
super(`Provider "${providerName}" cannot handle material source "${source}".`);
|
|
59
|
+
this.name = "IllegalMaterialSourceError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by managed-mode providers when `setMaterial` is called. Managed
|
|
3
|
+
* credential bytes are owned by the control plane; the workspace must not
|
|
4
|
+
* mutate them. See `docs/design/credentials-oauth-unification.md` and
|
|
5
|
+
* `planning/sprints/credentials-vault/02-controlplane-material-provider.md`.
|
|
6
|
+
*/
|
|
7
|
+
export class ManagedCredentialMaterialWriteError extends Error {
|
|
8
|
+
constructor(
|
|
9
|
+
message: string = "managed credentials are owned by the control plane; use the Connected apps page to create or modify them.",
|
|
10
|
+
) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "ManagedCredentialMaterialWriteError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by `ControlPlaneCredentialMaterialProvider` when the control-plane
|
|
3
|
+
* material endpoint returns a non-2xx response or a malformed body. Exposes
|
|
4
|
+
* the HTTP status and the raw error body so call sites can surface actionable
|
|
5
|
+
* detail without parsing strings.
|
|
6
|
+
*/
|
|
7
|
+
export class ManagedMaterialFetchError extends Error {
|
|
8
|
+
constructor(
|
|
9
|
+
public readonly status: number,
|
|
10
|
+
public readonly providerErrorBody: string,
|
|
11
|
+
message?: string,
|
|
12
|
+
) {
|
|
13
|
+
super(message ?? `Control-plane material fetch failed: HTTP ${status} ${providerErrorBody}`);
|
|
14
|
+
this.name = "ManagedMaterialFetchError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ConnectionInvocationRecord,
|
|
3
3
|
EngineRunCounters,
|
|
4
|
+
PendingResumeEntry,
|
|
5
|
+
PersistedSuspensionEntry,
|
|
4
6
|
PreparedNodeActivationDispatch,
|
|
5
7
|
NodeActivationRequest,
|
|
6
8
|
NodeActivationScheduler,
|
|
@@ -45,6 +47,16 @@ export type ActivationEnqueueRequest = {
|
|
|
45
47
|
planner: RunQueuePlanner;
|
|
46
48
|
engineCounters?: EngineRunCounters;
|
|
47
49
|
connectionInvocations?: ReadonlyArray<ConnectionInvocationRecord>;
|
|
50
|
+
/**
|
|
51
|
+
* Remaining suspension entries after consuming one for a HITL resume.
|
|
52
|
+
* When provided, saved alongside the new pending state so they survive the enqueue.
|
|
53
|
+
*/
|
|
54
|
+
suspension?: ReadonlyArray<PersistedSuspensionEntry>;
|
|
55
|
+
/**
|
|
56
|
+
* Resume context to attach to the re-activated node's execution context.
|
|
57
|
+
* Written here and consumed by `NodeExecutionRequestHandlerService` when building ctx.
|
|
58
|
+
*/
|
|
59
|
+
pendingResume?: PendingResumeEntry;
|
|
48
60
|
};
|
|
49
61
|
|
|
50
62
|
export class ActivationEnqueueService {
|
|
@@ -114,6 +126,10 @@ export class ActivationEnqueueService {
|
|
|
114
126
|
...args.previousNodeSnapshotsByNodeId,
|
|
115
127
|
[args.request.nodeId]: queuedSnapshot,
|
|
116
128
|
},
|
|
129
|
+
// HITL: preserve suspension entries and resume context when re-activating a
|
|
130
|
+
// suspended node. Omit fields when not provided (avoids polluting normal enqueue).
|
|
131
|
+
...(args.suspension !== undefined ? { suspension: args.suspension } : {}),
|
|
132
|
+
...(args.pendingResume !== undefined ? { pendingResume: args.pendingResume } : {}),
|
|
117
133
|
});
|
|
118
134
|
await this.dispatchPreparedActivation(preparedDispatch);
|
|
119
135
|
return {
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ExecutionContextFactory,
|
|
7
7
|
ExecutionTelemetryFactory,
|
|
8
8
|
NodeExecutionStatePublisher,
|
|
9
|
+
NodeResolver,
|
|
9
10
|
ParentExecutionRef,
|
|
10
11
|
RunDataSnapshot,
|
|
11
12
|
RunId,
|
|
@@ -13,6 +14,7 @@ import type {
|
|
|
13
14
|
WorkflowId,
|
|
14
15
|
} from "../types";
|
|
15
16
|
import { NoOpCostTrackingTelemetryFactory, NoOpExecutionTelemetryFactory } from "../types";
|
|
17
|
+
import type { TypeToken } from "../di";
|
|
16
18
|
|
|
17
19
|
import {
|
|
18
20
|
DefaultExecutionBinaryService,
|
|
@@ -29,6 +31,7 @@ export class DefaultExecutionContextFactory implements ExecutionContextFactory {
|
|
|
29
31
|
private readonly costTrackingFactory: CostTrackingTelemetryFactory = new NoOpCostTrackingTelemetryFactory(),
|
|
30
32
|
private readonly currentDate: () => Date = () => new Date(),
|
|
31
33
|
private readonly collections?: CollectionsContext,
|
|
34
|
+
private readonly nodeResolver?: NodeResolver,
|
|
32
35
|
) {}
|
|
33
36
|
|
|
34
37
|
create(args: {
|
|
@@ -72,6 +75,14 @@ export class DefaultExecutionContextFactory implements ExecutionContextFactory {
|
|
|
72
75
|
getCredential: args.getCredential,
|
|
73
76
|
testContext: args.testContext,
|
|
74
77
|
collections: this.collections,
|
|
78
|
+
resolve: <T>(token: TypeToken<T>): T => {
|
|
79
|
+
if (!this.nodeResolver) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"ExecutionContext.resolve() is not available: no NodeResolver was provided to DefaultExecutionContextFactory.",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return this.nodeResolver.resolve(token);
|
|
85
|
+
},
|
|
75
86
|
};
|
|
76
87
|
}
|
|
77
88
|
}
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
JsonValue,
|
|
3
3
|
NodeActivationId,
|
|
4
4
|
NodeExecutionSnapshot,
|
|
5
|
+
NodeExecutionStatus,
|
|
5
6
|
NodeId,
|
|
6
7
|
NodeInputsByPort,
|
|
7
8
|
NodeOutputs,
|
|
@@ -70,6 +71,11 @@ export class NodeExecutionSnapshotFactory {
|
|
|
70
71
|
inputsByPort: NodeInputsByPort;
|
|
71
72
|
outputs: NodeOutputs;
|
|
72
73
|
fromPinnedOutput?: boolean;
|
|
74
|
+
/** Override the terminal status for HITL outcomes (defaults to `"completed"`). */
|
|
75
|
+
hitlStatus?: Extract<
|
|
76
|
+
NodeExecutionStatus,
|
|
77
|
+
"hitl-approved" | "hitl-rejected" | "hitl-timeout" | "hitl-auto-accepted" | "hitl-cancelled"
|
|
78
|
+
>;
|
|
73
79
|
}): NodeExecutionSnapshot {
|
|
74
80
|
const fromPinnedOutput = args.fromPinnedOutput ?? false;
|
|
75
81
|
const startedAt = fromPinnedOutput ? (args.previous?.startedAt ?? args.finishedAt) : args.previous?.startedAt;
|
|
@@ -79,7 +85,7 @@ export class NodeExecutionSnapshotFactory {
|
|
|
79
85
|
nodeId: args.nodeId,
|
|
80
86
|
activationId: args.activationId,
|
|
81
87
|
parent: args.parent,
|
|
82
|
-
status: "completed",
|
|
88
|
+
status: args.hitlStatus ?? "completed",
|
|
83
89
|
queuedAt: args.previous?.queuedAt,
|
|
84
90
|
startedAt,
|
|
85
91
|
finishedAt: args.finishedAt,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { isPortsEmission, isUnbrandedPortsEmissionShape } from "../contracts/emitPorts";
|
|
3
3
|
import { CredentialUnboundError } from "../contracts/credentialTypes";
|
|
4
|
+
import { SuspensionRequest } from "../contracts/runtimeTypes";
|
|
4
5
|
|
|
5
6
|
import type {
|
|
6
7
|
Item,
|
|
@@ -8,9 +9,11 @@ import type {
|
|
|
8
9
|
NodeActivationRequest,
|
|
9
10
|
NodeExecutionContext,
|
|
10
11
|
NodeOutputs,
|
|
12
|
+
PersistedRunState,
|
|
11
13
|
RunnableNode,
|
|
12
14
|
RunnableNodeConfig,
|
|
13
15
|
RunnableNodeExecuteArgs,
|
|
16
|
+
RunId,
|
|
14
17
|
TriggerNode,
|
|
15
18
|
WorkflowNodeInstanceFactory,
|
|
16
19
|
} from "../types";
|
|
@@ -20,6 +23,8 @@ import { FanInMergeByOriginMerger } from "./FanInMergeByOriginMerger";
|
|
|
20
23
|
import { ItemExprResolver } from "./ItemExprResolver";
|
|
21
24
|
import { InProcessRetryRunner } from "./InProcessRetryRunner";
|
|
22
25
|
import { NodeOutputNormalizer } from "./NodeOutputNormalizer";
|
|
26
|
+
import { NodeSuspensionHandler } from "./NodeSuspensionHandler";
|
|
27
|
+
import { RunSuspendedError } from "./RunSuspendedError";
|
|
23
28
|
import { RunnableOutputBehaviorResolver } from "./RunnableOutputBehaviorResolver";
|
|
24
29
|
|
|
25
30
|
export class NodeExecutor {
|
|
@@ -33,6 +38,10 @@ export class NodeExecutor {
|
|
|
33
38
|
private readonly retryRunner: InProcessRetryRunner,
|
|
34
39
|
itemExprResolver?: ItemExprResolver,
|
|
35
40
|
outputBehaviorResolver?: RunnableOutputBehaviorResolver,
|
|
41
|
+
/** Required for HITL suspension support. When omitted, `SuspensionRequest` throws upward. */
|
|
42
|
+
private readonly suspensionHandler?: NodeSuspensionHandler,
|
|
43
|
+
/** Required alongside `suspensionHandler`. */
|
|
44
|
+
private readonly loadRunState?: (runId: RunId) => Promise<PersistedRunState | undefined>,
|
|
36
45
|
) {
|
|
37
46
|
this.itemExprResolver = itemExprResolver ?? new ItemExprResolver();
|
|
38
47
|
this.outputBehaviorResolver = outputBehaviorResolver ?? new RunnableOutputBehaviorResolver();
|
|
@@ -173,6 +182,7 @@ export class NodeExecutor {
|
|
|
173
182
|
}) as NodeOutputs;
|
|
174
183
|
}
|
|
175
184
|
const byPort: Partial<Record<string, Item[]>> = {};
|
|
185
|
+
let hasSuspension = false;
|
|
176
186
|
for (let i = 0; i < inputBatch.length; i++) {
|
|
177
187
|
const item = inputBatch[i] as Item;
|
|
178
188
|
this.assertItemJsonNotTopLevelArray(request.nodeId, item);
|
|
@@ -194,7 +204,51 @@ export class NodeExecutor {
|
|
|
194
204
|
items: inputBatch,
|
|
195
205
|
ctx: iterationCtx,
|
|
196
206
|
};
|
|
197
|
-
|
|
207
|
+
let raw: unknown;
|
|
208
|
+
try {
|
|
209
|
+
raw = await Promise.resolve(node.execute(args));
|
|
210
|
+
} catch (e) {
|
|
211
|
+
// Use both instanceof AND name check: under tsx/dev with mixed source/dist resolution,
|
|
212
|
+
// SuspensionRequest may load as two distinct class objects and instanceof fails. The
|
|
213
|
+
// name brand survives the duality because both copies set name="SuspensionRequest".
|
|
214
|
+
const isSuspension =
|
|
215
|
+
e instanceof SuspensionRequest ||
|
|
216
|
+
(e instanceof Error &&
|
|
217
|
+
e.name === "SuspensionRequest" &&
|
|
218
|
+
typeof (e as { request?: unknown }).request === "object");
|
|
219
|
+
if (isSuspension) {
|
|
220
|
+
if (!this.suspensionHandler || !this.loadRunState) {
|
|
221
|
+
// Suspension not supported in this executor configuration — propagate as a regular error.
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Node ${request.nodeId} threw SuspensionRequest but this NodeExecutor has no suspensionHandler configured.`,
|
|
224
|
+
{ cause: e },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
// Per-item suspension: load current state, persist the suspension entry, and
|
|
228
|
+
// continue processing remaining items. If deliver throws it propagates upward.
|
|
229
|
+
const state = await this.loadRunState(request.runId);
|
|
230
|
+
if (!state) {
|
|
231
|
+
throw new Error(`NodeExecutor: run state not found for runId ${request.runId} during suspension`, {
|
|
232
|
+
cause: e,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// handleSuspension throws RunSuspendedError after persisting — we re-throw it
|
|
236
|
+
// to exit the loop immediately. Partial byPort outputs are intentionally dropped
|
|
237
|
+
// (TODO: consider stashing outputs of non-suspended items alongside the suspension).
|
|
238
|
+
await this.suspensionHandler.handle({
|
|
239
|
+
runId: request.runId,
|
|
240
|
+
nodeId: request.nodeId,
|
|
241
|
+
activationId: request.activationId,
|
|
242
|
+
itemIndex: i,
|
|
243
|
+
suspensionRequest: e as SuspensionRequest,
|
|
244
|
+
state,
|
|
245
|
+
telemetry: iterationCtx.telemetry,
|
|
246
|
+
});
|
|
247
|
+
hasSuspension = true; // unreachable — handler always throws, but satisfies TS control-flow
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
throw e;
|
|
251
|
+
}
|
|
198
252
|
const normalized = this.outputNormalizer.normalizeExecuteResult({
|
|
199
253
|
baseItem: item,
|
|
200
254
|
raw,
|
|
@@ -209,6 +263,11 @@ export class NodeExecutor {
|
|
|
209
263
|
byPort[port] = list;
|
|
210
264
|
}
|
|
211
265
|
}
|
|
266
|
+
if (hasSuspension) {
|
|
267
|
+
// Unreachable in practice (suspensionHandler always throws RunSuspendedError) but
|
|
268
|
+
// guards against future refactors that might change handler behaviour.
|
|
269
|
+
throw new RunSuspendedError(request.runId, "unknown");
|
|
270
|
+
}
|
|
212
271
|
return byPort as NodeOutputs;
|
|
213
272
|
}
|
|
214
273
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { WorkflowNodeInstanceFactory } from "../types";
|
|
1
|
+
import type { PersistedRunState, RunId, WorkflowNodeInstanceFactory } from "../types";
|
|
2
2
|
|
|
3
3
|
import { InProcessRetryRunner } from "./InProcessRetryRunner";
|
|
4
4
|
import { NodeExecutor } from "./NodeExecutor";
|
|
5
|
+
import { NodeSuspensionHandler } from "./NodeSuspensionHandler";
|
|
5
6
|
import { RunnableOutputBehaviorResolver } from "./RunnableOutputBehaviorResolver";
|
|
6
7
|
|
|
7
8
|
export class NodeExecutorFactory {
|
|
@@ -9,7 +10,16 @@ export class NodeExecutorFactory {
|
|
|
9
10
|
workflowNodeInstanceFactory: WorkflowNodeInstanceFactory,
|
|
10
11
|
retryRunner: InProcessRetryRunner,
|
|
11
12
|
outputBehaviorResolver: RunnableOutputBehaviorResolver,
|
|
13
|
+
suspensionHandler?: NodeSuspensionHandler,
|
|
14
|
+
loadRunState?: (runId: RunId) => Promise<PersistedRunState | undefined>,
|
|
12
15
|
): NodeExecutor {
|
|
13
|
-
return new NodeExecutor(
|
|
16
|
+
return new NodeExecutor(
|
|
17
|
+
workflowNodeInstanceFactory,
|
|
18
|
+
retryRunner,
|
|
19
|
+
undefined,
|
|
20
|
+
outputBehaviorResolver,
|
|
21
|
+
suspensionHandler,
|
|
22
|
+
loadRunState,
|
|
23
|
+
);
|
|
14
24
|
}
|
|
15
25
|
}
|