@codemation/core 0.11.1 → 0.13.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 +28 -0
- package/dist/{CostCatalogContract-DZgcUBE4.d.cts → CostCatalogContract-Dxq1BTyi.d.cts} +2 -2
- package/dist/{EngineRuntimeRegistration.types-Cggm5GVY.d.cts → EngineRuntimeRegistration.types-CqcTWexS.d.cts} +3 -3
- package/dist/{EngineRuntimeRegistration.types-BQbS9_gs.d.ts → EngineRuntimeRegistration.types-Cr75cSfL.d.ts} +2 -2
- package/dist/InMemoryRunDataFactory-Csy2evr_.d.cts +205 -0
- package/dist/{ItemsInputNormalizer-CwdOhSAK.cjs → ItemsInputNormalizer-57EdA1ad.cjs} +2 -2
- package/dist/{ItemsInputNormalizer-CwdOhSAK.cjs.map → ItemsInputNormalizer-57EdA1ad.cjs.map} +1 -1
- package/dist/{ItemsInputNormalizer-_Mfcd3YU.d.ts → ItemsInputNormalizer-BWtlwdVI.d.ts} +2 -2
- package/dist/{ItemsInputNormalizer-D-MH8MBs.js → ItemsInputNormalizer-BkSvmfAW.js} +2 -2
- package/dist/{ItemsInputNormalizer-D-MH8MBs.js.map → ItemsInputNormalizer-BkSvmfAW.js.map} +1 -1
- package/dist/{ItemsInputNormalizer-C_dpn76M.d.cts → ItemsInputNormalizer-pLrWwUAP.d.cts} +3 -3
- package/dist/{RunIntentService-CEF-sFfI.d.cts → RunIntentService-BitgkKaT.d.cts} +18 -4
- package/dist/{RunIntentService-BVur7x9n.d.ts → RunIntentService-DYpqfu6D.d.ts} +18 -4
- package/dist/{agentMcpTypes-ZiNbNsEi.d.cts → agentMcpTypes-DGIwk6Ue.d.cts} +201 -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-BxuTFTLB.cjs → bootstrap-BEu1fJBM.cjs} +175 -4
- package/dist/bootstrap-BEu1fJBM.cjs.map +1 -0
- package/dist/{bootstrap-D_Yyi0wL.js → bootstrap-CSeInbj1.js} +173 -4
- package/dist/bootstrap-CSeInbj1.js.map +1 -0
- package/dist/browser.cjs +4 -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-BlEKdoZS.cjs → di-C-2ep8NZ.cjs} +44 -1
- package/dist/di-C-2ep8NZ.cjs.map +1 -0
- package/dist/{di-0Wop7z1y.js → di-D9Mv3kF3.js} +33 -2
- package/dist/di-D9Mv3kF3.js.map +1 -0
- package/dist/{executionPersistenceContracts-BgZMRsTa.d.cts → executionPersistenceContracts-CN9d7AnL.d.cts} +2 -2
- package/dist/{index-62Ba9f7D.d.ts → index-CqZeNGAp.d.ts} +343 -101
- package/dist/{index-zWGtEhrf.d.ts → index-rllWL4r-.d.ts} +459 -5
- package/dist/index.cjs +91 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +458 -97
- package/dist/index.d.ts +5 -5
- package/dist/index.js +74 -159
- package/dist/index.js.map +1 -1
- package/dist/{params-B5SENSzZ.d.cts → params-DRUr0F5v.d.cts} +2 -2
- package/dist/{runtime-cxmUkk0l.js → runtime-6-U2Cou5.js} +690 -18
- package/dist/runtime-6-U2Cou5.js.map +1 -0
- package/dist/{runtime-DBzq5YBi.cjs → runtime-DjYXgOo0.cjs} +749 -17
- package/dist/runtime-DjYXgOo0.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/binaries/DefaultExecutionBinaryServiceFactory.ts +27 -2
- package/src/binaries/DefaultNodeBinaryAttachmentServiceFactory.ts +14 -0
- package/src/binaries/boundedReadBinary.types.ts +90 -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 +131 -0
- package/src/contracts/workspaceFileTypes.ts +73 -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 +42 -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
|
@@ -19,4 +19,14 @@ export class CodemationTelemetryAttributeNames {
|
|
|
19
19
|
static readonly mcpServerId = "mcp.server_id";
|
|
20
20
|
/** MCP tool name on spans created for callTool invocations. */
|
|
21
21
|
static readonly mcpToolName = "mcp.tool_name";
|
|
22
|
+
/** Terminal node-execution status (e.g. `"hitl-approved"`, `"hitl-rejected"`) on HITL outcome spans. */
|
|
23
|
+
static readonly nodeExecutionStatus = "codemation.node.execution_status";
|
|
24
|
+
/** Populated on run-halted spans; discriminates the halt reason (e.g. `"hitl-rejected"`). */
|
|
25
|
+
static readonly runHaltReason = "codemation.run.halt_reason";
|
|
26
|
+
/** Human task ID on `hitl.task.*` span events. */
|
|
27
|
+
static readonly hitlTaskId = "codemation.hitl.task_id";
|
|
28
|
+
/** HITL channel name (e.g. `"inbox"`, `"control-plane-inbox"`) on `hitl.task.*` span events. */
|
|
29
|
+
static readonly hitlChannel = "codemation.hitl.channel";
|
|
30
|
+
/** Decision outcome (e.g. `"approved"`, `"rejected"`) on `hitl.task.decided` span events. */
|
|
31
|
+
static readonly hitlDecisionStatus = "codemation.hitl.decision_status";
|
|
22
32
|
}
|
|
@@ -162,6 +162,16 @@ export type CredentialInstanceRecord<TPublicConfig extends CredentialJsonRecord
|
|
|
162
162
|
setupStatus: CredentialSetupStatus;
|
|
163
163
|
createdAt: string;
|
|
164
164
|
updatedAt: string;
|
|
165
|
+
/**
|
|
166
|
+
* Pointer to where the credential material bytes live. For OSS / standalone
|
|
167
|
+
* rows this is `{source: "local", ref: instanceId}` and the bytes co-locate
|
|
168
|
+
* with the row in the workspace DB. For managed-mode rows this is
|
|
169
|
+
* `{source: "control-plane", ref: <cp_id>}` and the bytes live at CP.
|
|
170
|
+
*
|
|
171
|
+
* The seam is read through `CredentialMaterialProvider`. See
|
|
172
|
+
* `docs/design/credentials-oauth-unification.md` ("Material provider seam").
|
|
173
|
+
*/
|
|
174
|
+
material: Readonly<{ source: "local" | "control-plane"; ref: string }>;
|
|
165
175
|
}>;
|
|
166
176
|
|
|
167
177
|
/**
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TypeToken } from "../di";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Seam interfaces for HITL collaborators that are implemented in `@codemation/host`
|
|
5
|
+
* and injected into `NodeSuspensionHandler` at runtime. Core defines the interface only —
|
|
6
|
+
* no HTTP, vendor SDK, or Prisma dependencies here.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Signs and hashes a HITL resume token. Core only needs the sign and hash operations. */
|
|
10
|
+
export interface HitlResumeTokenSignerSeam {
|
|
11
|
+
sign(args: { taskId: string; expiresAt: Date; schemaHash: string }): string;
|
|
12
|
+
hashToken(token: string): string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Schedules a delayed BullMQ job that drives the timeout path. */
|
|
16
|
+
export interface HitlTimeoutJobSchedulerSeam {
|
|
17
|
+
enqueueTimeoutJob(args: { taskId: string; expiresAt: Date }): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const HitlResumeTokenSignerToken = Symbol.for("codemation.core.HitlResumeTokenSigner") as TypeToken<
|
|
21
|
+
HitlResumeTokenSignerSeam | undefined
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export const HitlTimeoutJobSchedulerToken = Symbol.for("codemation.core.HitlTimeoutJobScheduler") as TypeToken<
|
|
25
|
+
HitlTimeoutJobSchedulerSeam | undefined
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Optional workspace ID injected into NodeSuspensionHandler in managed mode (T7 security fix).
|
|
30
|
+
* Allows the handler to stamp the workspaceId on each HumanTaskRecord so HitlCallbackHandler
|
|
31
|
+
* can assert workspace identity independently of the HMAC middleware.
|
|
32
|
+
* Not registered in non-managed mode; NodeSuspensionHandler defaults to null.
|
|
33
|
+
*/
|
|
34
|
+
export const HitlWorkspaceIdToken = Symbol.for("codemation.core.HitlWorkspaceId") as TypeToken<string | undefined>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { TypeToken } from "../di";
|
|
2
|
+
import type { HumanTaskActor, HumanTaskSubject } from "./runtimeTypes";
|
|
3
|
+
import type { JsonValue } from "./workflowTypes";
|
|
4
|
+
|
|
5
|
+
export type HumanTaskStatus = "pending" | "decided" | "timed_out" | "auto_accepted" | "cancelled";
|
|
6
|
+
|
|
7
|
+
/** Persisted record for a single HITL task instance. */
|
|
8
|
+
export interface HumanTaskRecord {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly runId: string;
|
|
11
|
+
readonly workflowId: string;
|
|
12
|
+
readonly workspaceId?: string;
|
|
13
|
+
readonly nodeId: string;
|
|
14
|
+
readonly activationId: string;
|
|
15
|
+
readonly itemIndex: number;
|
|
16
|
+
readonly status: HumanTaskStatus;
|
|
17
|
+
readonly channel: string;
|
|
18
|
+
readonly subject: HumanTaskSubject;
|
|
19
|
+
readonly metadata: Record<string, JsonValue>;
|
|
20
|
+
readonly decisionSchemaJson: string;
|
|
21
|
+
readonly decisionSchemaHash: string;
|
|
22
|
+
readonly onTimeout: "halt" | "auto-accept";
|
|
23
|
+
readonly deliveryRef?: JsonValue;
|
|
24
|
+
readonly decision?: JsonValue;
|
|
25
|
+
readonly decidedAt?: Date;
|
|
26
|
+
readonly decidedBy?: HumanTaskActor;
|
|
27
|
+
readonly resumeTokenHash: string;
|
|
28
|
+
readonly expiresAt: Date;
|
|
29
|
+
readonly createdAt: Date;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface HumanTaskStore {
|
|
33
|
+
create(record: HumanTaskRecord): Promise<void>;
|
|
34
|
+
findById(taskId: string): Promise<HumanTaskRecord | undefined>;
|
|
35
|
+
findByResumeTokenHash(tokenHash: string): Promise<HumanTaskRecord | undefined>;
|
|
36
|
+
findPendingForWorkspace(workspaceId: string): Promise<ReadonlyArray<HumanTaskRecord>>;
|
|
37
|
+
/** Returns all pending tasks regardless of workspace. Used by the local dev inbox (non-managed mode). */
|
|
38
|
+
findAllPending(): Promise<ReadonlyArray<HumanTaskRecord>>;
|
|
39
|
+
markDecided(args: { taskId: string; decision: JsonValue; decidedBy: HumanTaskActor; decidedAt: Date }): Promise<void>;
|
|
40
|
+
markTimedOut(taskId: string): Promise<void>;
|
|
41
|
+
markAutoAccepted(taskId: string): Promise<void>;
|
|
42
|
+
markCancelled(taskId: string): Promise<void>;
|
|
43
|
+
cancelPendingForRun(runId: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const HumanTaskStoreToken = Symbol.for("codemation.core.HumanTaskStore") as TypeToken<
|
|
47
|
+
HumanTaskStore | undefined
|
|
48
|
+
>;
|
|
@@ -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,
|
|
@@ -147,9 +246,28 @@ export interface NodeBinaryAttachmentService extends ExecutionBinaryService {
|
|
|
147
246
|
withAttachment<TJson>(item: Item<TJson>, name: string, attachment: BinaryAttachment): Item<TJson>;
|
|
148
247
|
}
|
|
149
248
|
|
|
249
|
+
/** Default maximum bytes read into memory by the bounded helpers (50 MiB). */
|
|
250
|
+
export const BINARY_DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
251
|
+
|
|
150
252
|
export interface ExecutionBinaryService {
|
|
151
253
|
forNode(args: { nodeId: NodeId; activationId: NodeActivationId }): NodeBinaryAttachmentService;
|
|
152
254
|
openReadStream(attachment: BinaryAttachment): Promise<BinaryStorageReadResult | undefined>;
|
|
255
|
+
/**
|
|
256
|
+
* Reads all bytes from the attachment into a contiguous `Uint8Array`.
|
|
257
|
+
* Checks `attachment.size` against `maxBytes` *before* any allocation; throws a bounded-read
|
|
258
|
+
* error when exceeded (no OOM). Throws if the stream is unavailable or the byte count mismatches.
|
|
259
|
+
*/
|
|
260
|
+
getBytes(attachment: BinaryAttachment, maxBytes?: number): Promise<Uint8Array>;
|
|
261
|
+
/**
|
|
262
|
+
* Reads the attachment and decodes the bytes as UTF-8 text.
|
|
263
|
+
* Subject to the same bounded-read safety as `getBytes`.
|
|
264
|
+
*/
|
|
265
|
+
getText(attachment: BinaryAttachment, maxBytes?: number): Promise<string>;
|
|
266
|
+
/**
|
|
267
|
+
* Reads the attachment, decodes as UTF-8 text, and parses as JSON.
|
|
268
|
+
* Throws a clear error on invalid JSON. Subject to the same bounded-read safety.
|
|
269
|
+
*/
|
|
270
|
+
getJson<T = unknown>(attachment: BinaryAttachment, maxBytes?: number): Promise<T>;
|
|
153
271
|
}
|
|
154
272
|
|
|
155
273
|
export interface ExecutionContext {
|
|
@@ -183,6 +301,14 @@ export interface ExecutionContext {
|
|
|
183
301
|
* Collections registered in the codemation config, keyed by collection name.
|
|
184
302
|
*/
|
|
185
303
|
readonly collections?: CollectionsContext;
|
|
304
|
+
/**
|
|
305
|
+
* Resolve a DI token from the host container.
|
|
306
|
+
* Allows nodes to reach host-side services (e.g. `InboxChannelResolverToken`)
|
|
307
|
+
* without importing host code. Wired by `DefaultExecutionContextFactory`; throws
|
|
308
|
+
* a clear error when no resolver is configured (e.g. in unit tests that don't
|
|
309
|
+
* set up the full container).
|
|
310
|
+
*/
|
|
311
|
+
resolve<T>(token: TypeToken<T>): T;
|
|
186
312
|
}
|
|
187
313
|
|
|
188
314
|
export interface ExecutionContextFactory {
|
|
@@ -208,6 +334,11 @@ export interface NodeExecutionContext<TConfig extends NodeConfigBase = NodeConfi
|
|
|
208
334
|
config: TConfig;
|
|
209
335
|
telemetry: NodeExecutionTelemetry;
|
|
210
336
|
binary: NodeBinaryAttachmentService;
|
|
337
|
+
/**
|
|
338
|
+
* Present when this node activation is a HITL resume.
|
|
339
|
+
* The node checks `ctx.resumeContext !== undefined` and takes the resume branch.
|
|
340
|
+
*/
|
|
341
|
+
resumeContext?: ResumeContext;
|
|
211
342
|
}
|
|
212
343
|
|
|
213
344
|
export interface PollingTriggerHandle {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { TypeToken } from "../di";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Metadata returned for a workspace file object.
|
|
5
|
+
* Filename and contentType come from the S3 object's custom metadata
|
|
6
|
+
* (stamped by the control plane at upload time — story 03).
|
|
7
|
+
* The local-fs driver reads them from a companion .meta.json sidecar.
|
|
8
|
+
*/
|
|
9
|
+
export interface WorkspaceFileMetadata {
|
|
10
|
+
/** Storage key: `<workspaceId>/files/<fileId>` */
|
|
11
|
+
readonly key: string;
|
|
12
|
+
/** Last path segment of the storage key. */
|
|
13
|
+
readonly fileId: string;
|
|
14
|
+
/** Original filename as stamped by the CP at upload time. Empty string if not yet stamped (pre-story-03). */
|
|
15
|
+
readonly filename: string;
|
|
16
|
+
readonly contentType: string;
|
|
17
|
+
readonly size: number;
|
|
18
|
+
readonly lastModified: Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read-only, workspace-scoped port for accessing the shared workspace file pool.
|
|
23
|
+
* Implemented in `@codemation/host`; nodes reach it via `ctx.resolve(WorkspaceFileStorageToken)`.
|
|
24
|
+
*
|
|
25
|
+
* Key scheme: `<workspaceId>/files/<fileId>` — but nodes never construct raw keys.
|
|
26
|
+
* Use the workspace-level helpers (`listFiles`, `getFileByName`, `getFileById`) instead.
|
|
27
|
+
*
|
|
28
|
+
* This adapter is SEPARATE from the run-scoped BinaryStorage — do not confuse the two.
|
|
29
|
+
*/
|
|
30
|
+
export interface IWorkspaceFileStorage {
|
|
31
|
+
/**
|
|
32
|
+
* Lists all files in this workspace, sorted newest-first by lastModified.
|
|
33
|
+
* Optional case-insensitive substring filter on filename.
|
|
34
|
+
*/
|
|
35
|
+
listFiles(filenameFilter?: string): Promise<ReadonlyArray<WorkspaceFileMetadata>>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns metadata for the newest file with the given filename in this workspace.
|
|
39
|
+
* @throws WorkspaceFileNotFoundError when no file with that name exists.
|
|
40
|
+
*/
|
|
41
|
+
getFileByName(filename: string): Promise<WorkspaceFileMetadata>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns metadata for the file with the given fileId in this workspace.
|
|
45
|
+
* @throws WorkspaceFileNotFoundError when no file with that id exists.
|
|
46
|
+
*/
|
|
47
|
+
getFileById(fileId: string): Promise<WorkspaceFileMetadata>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns a non-buffered stream of the stored object's bytes.
|
|
51
|
+
* Accepts a full storage key or a fileId (prefixes as needed).
|
|
52
|
+
* @throws WorkspaceFileNotFoundError when the key does not exist.
|
|
53
|
+
*/
|
|
54
|
+
getStream(key: string): Promise<ReadableStream<Uint8Array>>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Error thrown when a requested workspace file key does not exist.
|
|
59
|
+
*/
|
|
60
|
+
export class WorkspaceFileNotFoundError extends Error {
|
|
61
|
+
constructor(readonly key: string) {
|
|
62
|
+
super(`Workspace file not found: ${key}`);
|
|
63
|
+
this.name = "WorkspaceFileNotFoundError";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* DI token for the workspace-scoped file storage adapter.
|
|
69
|
+
* Registered by `@codemation/host`; resolved by workspace-file nodes via `ctx.resolve(...)`.
|
|
70
|
+
*/
|
|
71
|
+
export const WorkspaceFileStorageToken = Symbol.for("codemation.core.WorkspaceFileStorage") as TypeToken<
|
|
72
|
+
IWorkspaceFileStorage | undefined
|
|
73
|
+
>;
|
|
@@ -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 {
|