@intx/workflow-host 0.2.2 → 0.4.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.
Files changed (101) hide show
  1. package/README.md +77 -14
  2. package/dist/adapters/mail-part-store.d.ts +46 -0
  3. package/dist/adapters/mail-part-store.js +251 -0
  4. package/dist/adapters/repo-store.d.ts +22 -1
  5. package/dist/adapters/repo-store.js +56 -65
  6. package/dist/adapters/spawn-child.d.ts +109 -44
  7. package/dist/adapters/spawn-child.js +77 -81
  8. package/dist/adapters/step-invoker.d.ts +52 -2
  9. package/dist/adapters/step-invoker.js +284 -37
  10. package/dist/adapters/substrate-mailbox-store.d.ts +80 -0
  11. package/dist/adapters/substrate-mailbox-store.js +404 -0
  12. package/dist/child/child-mailbox-reader.d.ts +10 -0
  13. package/dist/child/child-mailbox-reader.js +23 -0
  14. package/dist/child/credential-cell.d.ts +8 -0
  15. package/dist/child/credential-cell.js +66 -0
  16. package/dist/child/env-bootstrap.d.ts +20 -6
  17. package/dist/child/env-bootstrap.js +9 -1
  18. package/dist/child/from-process-env.d.ts +12 -0
  19. package/dist/child/from-process-env.js +6 -0
  20. package/dist/child/index.d.ts +6 -2
  21. package/dist/child/index.js +4 -1
  22. package/dist/child/mailbox-mutation-bridge.d.ts +61 -0
  23. package/dist/child/mailbox-mutation-bridge.js +101 -0
  24. package/dist/child/mailbox-watch-registry.d.ts +17 -0
  25. package/dist/child/mailbox-watch-registry.js +61 -0
  26. package/dist/child/outbound-mail-bridge.d.ts +3 -2
  27. package/dist/child/outbound-mail-bridge.js +20 -32
  28. package/dist/child/parked-correlations.d.ts +42 -0
  29. package/dist/child/parked-correlations.js +80 -0
  30. package/dist/child/pending-request.d.ts +89 -0
  31. package/dist/child/pending-request.js +80 -0
  32. package/dist/child/proxy-repo-store.d.ts +3 -2
  33. package/dist/child/proxy-repo-store.js +2 -0
  34. package/dist/child/run-child.d.ts +170 -14
  35. package/dist/child/run-child.js +569 -155
  36. package/dist/child/self-discovery.d.ts +10 -0
  37. package/dist/child/self-discovery.js +25 -1
  38. package/dist/child/substrate-write-bridge.d.ts +3 -2
  39. package/dist/child/substrate-write-bridge.js +21 -38
  40. package/dist/child/supervisor-backed-transport.d.ts +52 -6
  41. package/dist/child/supervisor-backed-transport.js +205 -62
  42. package/dist/child/verified-definition-loader.d.ts +33 -0
  43. package/dist/child/verified-definition-loader.js +43 -0
  44. package/dist/child/warm-agent-cache.d.ts +44 -4
  45. package/dist/child/warm-agent-cache.js +41 -10
  46. package/dist/index.d.ts +6 -4
  47. package/dist/index.js +6 -4
  48. package/dist/ipc/control-channel.d.ts +151 -2
  49. package/dist/ipc/control-channel.js +222 -29
  50. package/dist/ipc/event-channel.d.ts +32 -1
  51. package/dist/ipc/index.d.ts +1 -1
  52. package/dist/ipc/index.js +1 -1
  53. package/dist/mail-bus/hub-transport-adapter.d.ts +12 -7
  54. package/dist/mail-bus/hub-transport-adapter.js +9 -5
  55. package/dist/run-body-then-cleanup.d.ts +17 -0
  56. package/dist/run-body-then-cleanup.js +38 -0
  57. package/dist/seams/scheduler.d.ts +16 -6
  58. package/dist/seams/scheduler.js +87 -97
  59. package/dist/supervisor/cancel-signing.d.ts +2 -2
  60. package/dist/supervisor/cancel-signing.js +4 -8
  61. package/dist/supervisor/credentials.d.ts +28 -15
  62. package/dist/supervisor/credentials.js +7 -7
  63. package/dist/supervisor/dispatch-attribution.js +1 -1
  64. package/dist/supervisor/drain-timeout.d.ts +2 -2
  65. package/dist/supervisor/drain-timeout.js +1 -1
  66. package/dist/supervisor/index.d.ts +3 -3
  67. package/dist/supervisor/index.js +2 -2
  68. package/dist/supervisor/recycle.d.ts +10 -3
  69. package/dist/supervisor/recycle.js +18 -7
  70. package/dist/supervisor/run-event-compaction.d.ts +5 -5
  71. package/dist/supervisor/run-event-compaction.js +14 -19
  72. package/dist/supervisor/run-event-recovery.d.ts +34 -0
  73. package/dist/supervisor/run-event-recovery.js +45 -0
  74. package/dist/supervisor/spawn-env.d.ts +2 -2
  75. package/dist/supervisor/spawn-env.js +1 -1
  76. package/dist/supervisor/supervisor.d.ts +106 -26
  77. package/dist/supervisor/supervisor.js +1903 -414
  78. package/dist/supervisor/terminal-commit.d.ts +36 -0
  79. package/dist/supervisor/terminal-commit.js +130 -0
  80. package/dist/supervisor/types.d.ts +180 -23
  81. package/dist/testing/change-notifier.d.ts +12 -0
  82. package/dist/testing/change-notifier.js +63 -0
  83. package/dist/testing/index.d.ts +8 -0
  84. package/dist/testing/index.js +16 -0
  85. package/dist/testing/log-capture.d.ts +52 -0
  86. package/dist/testing/log-capture.js +124 -0
  87. package/dist/testing/mail-bus.d.ts +22 -0
  88. package/dist/testing/mail-bus.js +78 -0
  89. package/dist/testing/memory-streams.d.ts +43 -0
  90. package/dist/testing/memory-streams.js +211 -0
  91. package/dist/testing/spawn-observer.d.ts +12 -0
  92. package/dist/testing/spawn-observer.js +36 -0
  93. package/dist/testing/stub-repo-store.d.ts +10 -0
  94. package/dist/testing/stub-repo-store.js +39 -0
  95. package/dist/testing/supervisor-reaper.d.ts +24 -0
  96. package/dist/testing/supervisor-reaper.js +49 -0
  97. package/dist/testing/upstream-frames.d.ts +47 -0
  98. package/dist/testing/upstream-frames.js +94 -0
  99. package/dist/workflow-definition-loader.d.ts +187 -0
  100. package/dist/workflow-definition-loader.js +422 -0
  101. package/package.json +18 -11
@@ -1,5 +1,6 @@
1
- import type { Principal, RepoStore } from "@intx/hub-sessions/substrate";
2
- import type { SpawnChildWorkflow, WorkflowDefinition } from "@intx/workflow";
1
+ import type { InferenceEvent } from "@intx/types/runtime";
2
+ import type { SpawnChildWorkflow, SpawnSuspendableChild, SuspendableChildHandle, WorkflowDefinition, WorkflowEvent } from "@intx/workflow";
3
+ import type { CredentialMaterialRef } from "../child/run-child.js";
3
4
  /**
4
5
  * The terminal-status shape the runtime body expects back from a
5
6
  * spawn. Mirrored from `SpawnChildWorkflow`'s return type so the
@@ -16,7 +17,18 @@ export type ChildTerminalStatus = "completed" | "failed" | "cancelled";
16
17
  *
17
18
  * The callback receives the same `AbortSignal` the parent runtime
18
19
  * passed into the adapter so a parent-initiated cancellation
19
- * propagates to the child without an intermediate wrapper.
20
+ * propagates to the child without an intermediate wrapper. It also
21
+ * receives the parent run's live `onEvent` sink so the child's agent
22
+ * steps emit inference events up the same channel (mirroring
23
+ * {@link RunSuspendableChild}), and the spawn `depth` / ceiling so the
24
+ * child run's own spawns keep counting against the tree-wide bound.
25
+ *
26
+ * The callback also receives the parent run's live credential-material cell
27
+ * (the same reference the top-level step invoker reads live), so the child's
28
+ * inference resolves its source secret by `credentialId` against the run's
29
+ * current delivery -- a rotation the parent applies reaches the child through
30
+ * the shared reference. A non-sidecar executor that carries no credential
31
+ * material omits it.
20
32
  */
21
33
  export type RunChildWorkflow = (input: {
22
34
  definition: WorkflowDefinition;
@@ -26,49 +38,102 @@ export type RunChildWorkflow = (input: {
26
38
  parentRunId: string;
27
39
  parentStepId: string;
28
40
  signal: AbortSignal;
29
- }) => Promise<{
41
+ depth: number;
42
+ maxChildSpawnDepth: number;
43
+ }, onEvent: (event: InferenceEvent) => void, credentialMaterial?: CredentialMaterialRef) => Promise<{
30
44
  terminalStatus: ChildTerminalStatus;
31
45
  }>;
32
- export interface WorkflowSpawnChildOpts {
33
- /**
34
- * Substrate the deploy orchestrator wrote the workflow asset into.
35
- * The adapter reads the workflow envelope through
36
- * `substrate.getRepoDir` -- the deploy-time `writeTree` already
37
- * materialized the file under the returned directory and a flat
38
- * `fs.readFile` does not need to walk the git object database.
39
- */
40
- substrate: RepoStore;
41
- /**
42
- * Principal the adapter presents to the substrate for any future
43
- * authorize-gated read path. The current implementation does not
44
- * gate `getRepoDir` (the substrate documents it as a pure path
45
- * computation), but holding the principal in closure keeps the
46
- * adapter symmetric with the sibling production adapters and ready
47
- * for a future API that surfaces an authorize gate on the same
48
- * read path.
49
- */
50
- principal: Principal;
51
- /**
52
- * Ref under the workflow asset's repo whose tree holds the
53
- * deployed `workflow.json`. Callers typically supply
54
- * `"refs/heads/main"` -- the workflow-kind handler enforces the
55
- * envelope's structural shape at push time so a deploy ref read
56
- * here either yields a valid envelope or surfaces a targeted
57
- * parse/validation error.
58
- */
59
- deployRef: string;
60
- /**
61
- * Runtime-supplied child execution callback. The adapter delegates
62
- * here once the `WorkflowDefinition` is resolved; the supervisor
63
- * owns the child `WorkflowRuntimeEnv` and the `runtimeRun`
64
- * invocation.
65
- */
46
+ /**
47
+ * Construct the terminal `WorkflowRuntimeEnv.SpawnChildWorkflow` adapter for an
48
+ * owned childWorkflow import. The child re-evaluated the whole pinned closure
49
+ * and lifted every inline child to an internal `{ ref }`, so the child
50
+ * definitions are in hand and already covered by the parent's re-verify.
51
+ * Resolve each `definitionRef`
52
+ * from the in-memory `bodies` map and delegate to the runtime-supplied
53
+ * `runChild`, with NO on-disk round-trip and NO separate per-child re-verify:
54
+ * materializing the child back out and re-fingerprinting it would round-trip
55
+ * trusted-in-hand data for no gain, and the closure re-eval on restart
56
+ * re-derives the same bodies durably. Mirrors
57
+ * {@link createInMemorySpawnSuspendableChild} but drives the child terminal-only
58
+ * (await its terminal status) rather than across approval parks.
59
+ */
60
+ /**
61
+ * Host-side widening of the runtime {@link SpawnChildWorkflow} contract: the
62
+ * same input plus the per-run `onEvent` sink the host injects. The runtime env
63
+ * carries the narrow `SpawnChildWorkflow` (no event slot); the caller wraps this
64
+ * with its run's `onEvent`, mirroring {@link HostSpawnSuspendableChild}. The
65
+ * sink is a call argument (not closed over at construction) because the resolver
66
+ * is selected once per deployment while `onEvent` is built per run. The run's
67
+ * live credential-material cell rides the same seam, so the child's inference
68
+ * reads the parent's current credential delivery.
69
+ */
70
+ export type HostSpawnChild = (input: Parameters<SpawnChildWorkflow>[0], onEvent: (event: InferenceEvent) => void, credentialMaterial?: CredentialMaterialRef) => ReturnType<SpawnChildWorkflow>;
71
+ export declare function createInMemorySpawnChild(opts: {
72
+ bodies: ReadonlyMap<string, WorkflowDefinition>;
66
73
  runChild: RunChildWorkflow;
67
- }
74
+ }): HostSpawnChild;
75
+ /**
76
+ * Runtime-supplied suspendable child execution callback. The park-aware
77
+ * analog of {@link RunChildWorkflow}: the supervisor owns the child
78
+ * `WorkflowRuntimeEnv` construction and the `runtimeRun` invocation and
79
+ * returns a live `SuspendableChildHandle` the caller drives across the
80
+ * body's approval parks, rather than awaiting a terminal. The adapter is
81
+ * the single resolution point that hands the supervisor a concrete
82
+ * `WorkflowDefinition` alongside the parent attribution the runtime body
83
+ * produced.
84
+ */
85
+ export type RunSuspendableChild = (input: {
86
+ definition: WorkflowDefinition;
87
+ definitionRef: string;
88
+ childRunId: string;
89
+ input: unknown;
90
+ parentRunId: string;
91
+ parentStepId: string;
92
+ signal: AbortSignal;
93
+ depth: number;
94
+ maxChildSpawnDepth: number;
95
+ resumeFromEvents?: readonly WorkflowEvent[];
96
+ },
97
+ /**
98
+ * Live inference-event sink for the child's agent steps. Threaded from the
99
+ * host's per-run funnel (the parent run's event-channel closure) so the
100
+ * body's inference events reach the hub's live stream instead of being
101
+ * silently dropped. Per-run durable attribution is unaffected -- the child
102
+ * runtime commits its events under `runs/<childRunId>/events/` regardless.
103
+ */
104
+ onEvent: (event: InferenceEvent) => void,
105
+ /**
106
+ * The parent run's live credential-material cell. Threaded so the body's
107
+ * inference resolves its source secret by `credentialId` against the run's
108
+ * current delivery, reached live through the shared reference on a rotation.
109
+ * A non-sidecar executor that carries no credential material omits it.
110
+ */
111
+ credentialMaterial?: CredentialMaterialRef) => Promise<SuspendableChildHandle>;
112
+ /**
113
+ * Host-side widening of the runtime {@link SpawnSuspendableChild} contract: the
114
+ * same input plus the per-run `onEvent` sink the host injects. The runtime
115
+ * calls the narrow `SpawnSuspendableChild` (no event slot); the host binding
116
+ * wired into the runtime env closes over the run's funnel and forwards it here,
117
+ * mirroring how `ChildStepInvoker` widens the runtime `StepInvoker` with
118
+ * `onEvent`. The runtime contract in `@intx/workflow` stays untouched. The
119
+ * run's live credential-material cell rides the same seam, so the body's
120
+ * inference reads the parent's current credential delivery.
121
+ */
122
+ export type HostSpawnSuspendableChild = (input: Parameters<SpawnSuspendableChild>[0], onEvent: (event: InferenceEvent) => void, credentialMaterial?: CredentialMaterialRef) => ReturnType<SpawnSuspendableChild>;
68
123
  /**
69
- * Construct the production `WorkflowRuntimeEnv.SpawnChildWorkflow`
70
- * adapter. The substrate handle, the principal, the deploy ref, and
71
- * the runtime-supplied child callback live in closure; the returned
72
- * callable satisfies the runtime-env interface.
124
+ * Construct the `WorkflowRuntimeEnv.SpawnSuspendableChild` adapter for the
125
+ * source-ref (code-sourced) path -- the only deploy lineage. The parent child
126
+ * re-evaluated the whole pinned closure in one sandbox and re-verified it
127
+ * against the approved hash -- which already covers every inline onTrigger body
128
+ * -- so the body definitions are in hand and already proven. Resolve each
129
+ * `definitionRef` from that in-memory `bodies` map and run it in-process, with
130
+ * NO disk round-trip and NO separate per-body re-verify: materializing the body
131
+ * back out and re-fingerprinting it would round-trip trusted-in-hand data for no
132
+ * gain, and the closure re-eval on restart re-derives the same bodies durably.
133
+ * The body still runs in the parent's sandbox (in-process today; a stricter
134
+ * per-body boundary is the deferred, opt-in SandboxBoundary case).
73
135
  */
74
- export declare function createWorkflowSpawnChild(opts: WorkflowSpawnChildOpts): SpawnChildWorkflow;
136
+ export declare function createInMemorySpawnSuspendableChild(opts: {
137
+ bodies: ReadonlyMap<string, WorkflowDefinition>;
138
+ runSuspendableChild: RunSuspendableChild;
139
+ }): HostSpawnSuspendableChild;
@@ -1,34 +1,34 @@
1
1
  // Production `WorkflowRuntimeEnv.SpawnChildWorkflow` adapter.
2
2
  //
3
- // The runtime body sees the spawn callback shape: given a
4
- // `definitionRef` (a workflow asset's repo id), a parent-allocated
5
- // `childRunId`, the materialized child input, and parent attribution,
6
- // settle once the child run reaches a terminal phase. The adapter
7
- // itself does not execute the child workflow -- it resolves the
8
- // `definitionRef` into a concrete `WorkflowDefinition` from the
9
- // workflow repo's deploy ref, then delegates the spawn to a
10
- // runtime-supplied `runChild` callback. The supervisor wires the
3
+ // The runtime body sees the spawn callback shape: given a `definitionRef`
4
+ // (the internal ref the deploy step assigned when it lifted the authored
5
+ // inline child), a parent-allocated `childRunId`, the materialized child
6
+ // input, and parent attribution, settle once the child run reaches a terminal
7
+ // phase. The adapter itself does not execute the child workflow -- it resolves
8
+ // the `definitionRef` into a concrete `WorkflowDefinition` and delegates the
9
+ // spawn to a runtime-supplied `runChild` callback. The supervisor wires the
11
10
  // callback against a child `WorkflowRuntimeEnv` and `runtimeRun`.
12
11
  //
13
- // Resolution path:
14
- // 1. Build `RepoId { kind: "workflow", id: definitionRef }` against
15
- // the substrate the deploy orchestrator wrote the workflow asset
16
- // into.
17
- // 2. Read `workflow.json` from the deploy ref's working tree at
18
- // `getRepoDir(repoId)`. The deploy-time `writeTree` materializes
19
- // the file on disk under the same path, so a flat `fs.readFile`
20
- // against the substrate's repo dir gives the workflow envelope
21
- // without dragging in a git object-database read for this commit.
22
- // The sibling repo-store and blob-substrate adapters use the same
23
- // working-tree-read pattern.
24
- // 3. Parse as JSON, validate the envelope shape via
25
- // `workflowDefinitionEnvelopeSchema`, and surface the parsed
26
- // object as a `WorkflowDefinition`. The state-machine-narrowed
27
- // primitives are validated by the runtime body downstream; the
28
- // adapter does the structural-shape check the workflow-kind
29
- // handler already enforces at push time so a tampered-on-disk
30
- // tree still surfaces a clear error here rather than crashing
31
- // deep inside the runtime.
12
+ // Two spawn types with DIFFERENT trust structures resolve here, so they
13
+ // take different resolution paths -- not one shared resolver that pretends
14
+ // they are the same:
15
+ //
16
+ // - onTrigger BODY (the suspendable adapter): a body is a section
17
+ // extracted from the PARENT's own approved definition. Source-ref is the
18
+ // only deploy lineage, so the body is resolved in-memory from the parent's
19
+ // re-evaluated closure (`createInMemorySpawnSuspendableChild`), already
20
+ // covered by the parent's re-verify -- no separate on-disk read and no
21
+ // separate per-body re-verify.
22
+ //
23
+ // - childWorkflow (the terminal adapter): an owned import embedded inline in
24
+ // the parent's definition. It is lifted to an internal `{ ref }` at child
25
+ // boot and resolved in-memory from the parent's closure map
26
+ // (`createInMemorySpawnChild`) -- exactly like a source-ref onTrigger
27
+ // body, with NO on-disk asset and NO separate per-child re-verify (the
28
+ // parent's re-verify already covers it, since the inline child rides the
29
+ // parent's hashed projection). The terminal-only drive (await the child's
30
+ // terminal, no park) is the only thing that distinguishes it from the
31
+ // suspendable body adapter.
32
32
  //
33
33
  // Drain coordination is handled by the supervisor's drain primitive
34
34
  // (`packages/workflow-host/src/supervisor`), not by this adapter. The
@@ -59,21 +59,17 @@
59
59
  // `runChild` does -- but the callback's input shape (`{ definition,
60
60
  // childRunId, ... }`) is the seam that makes the scoping unambiguous
61
61
  // at the boundary.
62
- import { type } from "arktype";
63
- import { workflowDefinitionEnvelopeSchema } from "@intx/hub-sessions/substrate";
64
- const WORKFLOW_JSON_PATH = "workflow.json";
65
- /**
66
- * Construct the production `WorkflowRuntimeEnv.SpawnChildWorkflow`
67
- * adapter. The substrate handle, the principal, the deploy ref, and
68
- * the runtime-supplied child callback live in closure; the returned
69
- * callable satisfies the runtime-env interface.
70
- */
71
- export function createWorkflowSpawnChild(opts) {
72
- return async ({ definitionRef, childRunId, input, parentRunId, parentStepId, signal, }) => {
62
+ export function createInMemorySpawnChild(opts) {
63
+ return async ({ definitionRef, childRunId, input, parentRunId, parentStepId, signal, depth, maxChildSpawnDepth, }, onEvent, credentialMaterial) => {
73
64
  if (signal.aborted) {
74
65
  throw abortError(signal);
75
66
  }
76
- const definition = await resolveDefinition(opts, definitionRef);
67
+ const definition = opts.bodies.get(definitionRef);
68
+ if (definition === undefined) {
69
+ throw new Error(`workflow-runtime: spawn-child has no in-memory childWorkflow ` +
70
+ `definition for ${JSON.stringify(definitionRef)}; the parent's ` +
71
+ `closure should have lifted every inline child`);
72
+ }
77
73
  // Re-check the abort signal after the resolution await. The
78
74
  // caller can fire `signal.abort()` between the entry-time check
79
75
  // and here; without this re-check the child callback would be
@@ -91,46 +87,52 @@ export function createWorkflowSpawnChild(opts) {
91
87
  parentRunId,
92
88
  parentStepId,
93
89
  signal,
94
- });
90
+ depth,
91
+ maxChildSpawnDepth,
92
+ }, onEvent, credentialMaterial);
95
93
  return { terminalStatus: result.terminalStatus };
96
94
  };
97
95
  }
98
- async function resolveDefinition(opts, definitionRef) {
99
- const repoId = { kind: "workflow", id: definitionRef };
100
- const fs = await import("node:fs/promises");
101
- const path = await import("node:path");
102
- const dir = opts.substrate.getRepoDir(repoId);
103
- const workflowPath = path.join(dir, WORKFLOW_JSON_PATH);
104
- let raw;
105
- try {
106
- raw = await fs.readFile(workflowPath, "utf8");
107
- }
108
- catch (cause) {
109
- if (isErrnoNotFound(cause)) {
110
- throw new Error(`workflow-runtime: spawn-child cannot resolve definitionRef ${JSON.stringify(definitionRef)}: ${WORKFLOW_JSON_PATH} not present under ${repoId.kind}/${repoId.id} on ${opts.deployRef}`, { cause });
96
+ /**
97
+ * Construct the `WorkflowRuntimeEnv.SpawnSuspendableChild` adapter for the
98
+ * source-ref (code-sourced) path -- the only deploy lineage. The parent child
99
+ * re-evaluated the whole pinned closure in one sandbox and re-verified it
100
+ * against the approved hash -- which already covers every inline onTrigger body
101
+ * -- so the body definitions are in hand and already proven. Resolve each
102
+ * `definitionRef` from that in-memory `bodies` map and run it in-process, with
103
+ * NO disk round-trip and NO separate per-body re-verify: materializing the body
104
+ * back out and re-fingerprinting it would round-trip trusted-in-hand data for no
105
+ * gain, and the closure re-eval on restart re-derives the same bodies durably.
106
+ * The body still runs in the parent's sandbox (in-process today; a stricter
107
+ * per-body boundary is the deferred, opt-in SandboxBoundary case).
108
+ */
109
+ export function createInMemorySpawnSuspendableChild(opts) {
110
+ return async ({ definitionRef, childRunId, input, parentRunId, parentStepId, signal, depth, maxChildSpawnDepth, resumeFromEvents, }, onEvent, credentialMaterial) => {
111
+ if (signal.aborted) {
112
+ throw abortError(signal);
113
+ }
114
+ const definition = opts.bodies.get(definitionRef);
115
+ if (definition === undefined) {
116
+ throw new Error(`workflow-runtime: source-ref spawn-child has no in-memory onTrigger ` +
117
+ `body for ${JSON.stringify(definitionRef)}; the parent's closure ` +
118
+ `re-eval should have extracted every inline body`);
119
+ }
120
+ if (signal.aborted) {
121
+ throw abortError(signal);
111
122
  }
112
- throw cause;
113
- }
114
- let parsed;
115
- try {
116
- parsed = JSON.parse(raw);
117
- }
118
- catch (cause) {
119
- throw new Error(`workflow-runtime: spawn-child read ${WORKFLOW_JSON_PATH} for ${repoId.kind}/${repoId.id} on ${opts.deployRef} is not valid JSON`, { cause });
120
- }
121
- const validated = workflowDefinitionEnvelopeSchema(parsed);
122
- if (validated instanceof type.errors) {
123
- throw new Error(`workflow-runtime: spawn-child ${WORKFLOW_JSON_PATH} for ${repoId.kind}/${repoId.id} on ${opts.deployRef} failed envelope validation: ${validated.summary}`);
124
- }
125
- // The envelope schema enforces the structural shape the workflow
126
- // body and state machine consume; the discriminated narrow over
127
- // every `Primitive` variant lives downstream (the runtime body
128
- // walks the steps and dispatches per-kind). Re-deriving the
129
- // primitive narrow here would duplicate `defineWorkflow`'s
130
- // validation, and the workflow-kind handler already enforced the
131
- // same envelope at push time.
132
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- WorkflowDefinition's primitive union is narrowed downstream by the runtime body; the envelope schema enforces the structural shape this adapter cares about
133
- return validated;
123
+ return opts.runSuspendableChild({
124
+ definition,
125
+ definitionRef,
126
+ childRunId,
127
+ input,
128
+ parentRunId,
129
+ parentStepId,
130
+ signal,
131
+ depth,
132
+ maxChildSpawnDepth,
133
+ ...(resumeFromEvents !== undefined ? { resumeFromEvents } : {}),
134
+ }, onEvent, credentialMaterial);
135
+ };
134
136
  }
135
137
  /**
136
138
  * Construct the rejection used when `signal.aborted` short-circuits.
@@ -144,9 +146,3 @@ function abortError(signal) {
144
146
  return reason;
145
147
  return new DOMException("aborted", "AbortError");
146
148
  }
147
- function isErrnoNotFound(cause) {
148
- if (cause === null || typeof cause !== "object")
149
- return false;
150
- const code = cause.code;
151
- return code === "ENOENT";
152
- }
@@ -1,7 +1,7 @@
1
1
  import { type Agent, type AgentDefinition, type BaseEnv } from "@intx/agent";
2
- import type { InferenceEvent, InferenceSource } from "@intx/types/runtime";
2
+ import type { InboundMessage, InferenceEvent, InferenceSource, MailPartReader } from "@intx/types/runtime";
3
3
  import type { StepInvokeRequest, StepInvoker, WorkflowAuthorizeFn } from "@intx/workflow";
4
- import type { WarmAgentCache } from "../child/warm-agent-cache.js";
4
+ import type { WarmAgentCache, WarmReplyDrive } from "../child/warm-agent-cache.js";
5
5
  /**
6
6
  * Per-step env contributions the caller of the adapter owns.
7
7
  *
@@ -104,6 +104,56 @@ export interface WorkflowStepInvokerOpts {
104
104
  * has no cross-run conversation to mirror.
105
105
  */
106
106
  onRunBoundary?: (key: string) => Promise<void>;
107
+ /**
108
+ * Seed hook for the warm path (design §3c threading). When supplied, the
109
+ * adapter calls it before `agent.send` on every message whose delivered
110
+ * input is a mail-derived `InboundMessage`, passing the step identity
111
+ * (`authzContext.stepId`, the same key `onRunBoundary` and the warm cache
112
+ * use) and that message. The sidecar wires this to the warm agent's
113
+ * durable conversation store, which routes the message onto the connector
114
+ * thread (seeding threadRoot / lastMessageId / replyTo) so the reply path
115
+ * can compose a threaded reply. Awaited before the send so the thread
116
+ * state is committed and durably flushed before the reply is produced; a
117
+ * seed failure surfaces by rejecting the step.
118
+ *
119
+ * Only mail-derived inbound messages seed: an approval-resume inbound
120
+ * carries a synthetic sender and a correlation id, and a synthesized
121
+ * string input is not a message, so neither advances the connector
122
+ * thread. Omitted on the cold path, which has no durable connector state
123
+ * to seed.
124
+ */
125
+ seedInbound?: (key: string, message: InboundMessage) => Promise<void>;
126
+ /**
127
+ * Connector reply-drain hook for the warm path (design §3c). When supplied,
128
+ * the adapter invokes it ONCE -- at the warm agent's first-message build --
129
+ * with the step identity (`authzContext.stepId`, the same key the warm
130
+ * cache, seed, and run-boundary hooks use) and the agent's lifetime event
131
+ * stream. The sidecar wires this to the shared connector reply drain: on
132
+ * every `connector.reply` the agent emits, the drain composes a threaded
133
+ * reply from the durable store's connector thread and sends it through the
134
+ * supervisor-backed outbound bridge, then advances the thread from the send
135
+ * receipt.
136
+ *
137
+ * The returned drive handle exposes the drain's lifetime `done` promise --
138
+ * which settles when the agent's stream ends at eviction, folded into the
139
+ * warm entry's event-forward promise so the cache drains the reply loop
140
+ * alongside the observability forwarder -- plus the per-turn settle barrier
141
+ * (`replySeq` / `waitForReplyAfter`) the warm step gates each reply turn on,
142
+ * so the run parks only after the reply is durably sent.
143
+ *
144
+ * Omitted on the cold path (a torn-down per-step agent has no cross-message
145
+ * connector thread) and whenever the deployment is not warm-kept.
146
+ */
147
+ driveReplies?: (key: string, stream: ReturnType<Agent["stream"]>) => WarmReplyDrive;
148
+ /**
149
+ * Reader for the run's inbound-mail parts. When the step input is a decoded
150
+ * `Mail`, the adapter resolves each part's `ref` to its committed bytes
151
+ * through this reader and delivers a real `InboundMessage` (text and/or
152
+ * attachments) to `agent.send`. Supplied by the run child for the top-level
153
+ * run's steps; absent for body steps, where a part whose bytes must be read
154
+ * is refused loudly rather than silently flattened to text.
155
+ */
156
+ mailPartReader?: MailPartReader;
107
157
  }
108
158
  /**
109
159
  * Construct the production `WorkflowRuntimeEnv.StepInvoker` adapter.