@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
@@ -0,0 +1,47 @@
1
+ import { ControlPayload } from "../ipc/control-channel.js";
2
+ /**
3
+ * Decode every payload of `type_` from `lines`, in arrival order.
4
+ *
5
+ * Frames that fail envelope or payload validation are skipped, so a test
6
+ * asserting on one payload kind is not fooled by an unrelated frame sharing
7
+ * the stream.
8
+ */
9
+ export declare function readPayloadsOfType<T extends string>(lines: readonly string[], type_: T): Extract<typeof ControlPayload.infer, {
10
+ type: T;
11
+ }>[];
12
+ /** The runIds carried on every `trigger.fire` frame in `lines`, in order. */
13
+ export declare function parseTriggerFireRunIds(lines: readonly string[]): string[];
14
+ /** The part of a stream double this module reads. */
15
+ export type UpstreamFrameSource = {
16
+ flushed(): readonly string[];
17
+ nextWrite(): Promise<void>;
18
+ };
19
+ /**
20
+ * Resolve with the first payload of `type_` that `match` accepts, whether it
21
+ * is already buffered or arrives later.
22
+ *
23
+ * The buffer is re-read only when a line actually arrives, and the wait
24
+ * carries no deadline: a frame that never comes is caught by the lane
25
+ * timeout, per "Synchronizing on State, Not Time" in CONVENTIONS.md.
26
+ */
27
+ export declare function waitForUpstreamPayload<T extends string>(stream: UpstreamFrameSource, type_: T, match?: (payload: Extract<typeof ControlPayload.infer, {
28
+ type: T;
29
+ }>) => boolean): Promise<Extract<typeof ControlPayload.infer, {
30
+ type: T;
31
+ }>>;
32
+ /**
33
+ * Resolve once at least `count` payloads of `type_` have been written, with
34
+ * every matching payload in arrival order.
35
+ *
36
+ * The count form exists because most waits here are for the Nth frame rather
37
+ * than for a particular one; a caller wanting a specific frame should use
38
+ * `waitForUpstreamPayload` with a predicate.
39
+ */
40
+ export declare function waitForUpstreamPayloads<T extends string>(stream: UpstreamFrameSource, type_: T, count?: number): Promise<Extract<typeof ControlPayload.infer, {
41
+ type: T;
42
+ }>[]>;
43
+ /**
44
+ * Resolve once at least `count` `trigger.fire` frames have been written, with
45
+ * every runId seen in order.
46
+ */
47
+ export declare function waitForTriggerFireRunIds(stream: UpstreamFrameSource, count: number): Promise<string[]>;
@@ -0,0 +1,94 @@
1
+ // Reading and awaiting the control frames a supervisor writes upstream.
2
+ //
3
+ // A test that drives a supervisor asserts on the frames it emitted, and has
4
+ // to wait for the one it cares about first. That wait was written as a
5
+ // deadline plus a one-millisecond tick that re-decoded the whole buffer --
6
+ // two wall-clock numbers deciding a run whose subject is what a frame
7
+ // CONTAINS, never how quickly it appears. Waiting on the write removes the
8
+ // window instead of widening it.
9
+ import { type } from "arktype";
10
+ import { ControlPayload } from "../ipc/control-channel.js";
11
+ import { SignedEnvelope } from "../ipc/envelope.js";
12
+ /**
13
+ * Decode every payload of `type_` from `lines`, in arrival order.
14
+ *
15
+ * Frames that fail envelope or payload validation are skipped, so a test
16
+ * asserting on one payload kind is not fooled by an unrelated frame sharing
17
+ * the stream.
18
+ */
19
+ export function readPayloadsOfType(lines, type_) {
20
+ const out = [];
21
+ for (const line of lines) {
22
+ let raw;
23
+ try {
24
+ raw = JSON.parse(line);
25
+ }
26
+ catch {
27
+ continue;
28
+ }
29
+ const signed = SignedEnvelope(raw);
30
+ if (signed instanceof type.errors)
31
+ continue;
32
+ const payload = ControlPayload(signed.envelope.payload);
33
+ if (payload instanceof type.errors)
34
+ continue;
35
+ if (payload.type !== type_)
36
+ continue;
37
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- the arktype narrow above pins the discriminator; the cast walks the union to the matching branch
38
+ out.push(payload);
39
+ }
40
+ return out;
41
+ }
42
+ /** The runIds carried on every `trigger.fire` frame in `lines`, in order. */
43
+ export function parseTriggerFireRunIds(lines) {
44
+ return readPayloadsOfType(lines, "trigger.fire").map((p) => p.data.runId);
45
+ }
46
+ /**
47
+ * Resolve with the first payload of `type_` that `match` accepts, whether it
48
+ * is already buffered or arrives later.
49
+ *
50
+ * The buffer is re-read only when a line actually arrives, and the wait
51
+ * carries no deadline: a frame that never comes is caught by the lane
52
+ * timeout, per "Synchronizing on State, Not Time" in CONVENTIONS.md.
53
+ */
54
+ export async function waitForUpstreamPayload(stream, type_, match = () => true) {
55
+ for (;;) {
56
+ // Re-read on every pass, so a frame already in the buffer resolves this
57
+ // rather than leaving it waiting for the next write.
58
+ const arrived = stream.nextWrite();
59
+ const found = readPayloadsOfType(stream.flushed(), type_).find(match);
60
+ if (found !== undefined)
61
+ return found;
62
+ await arrived;
63
+ }
64
+ }
65
+ /**
66
+ * Resolve once at least `count` payloads of `type_` have been written, with
67
+ * every matching payload in arrival order.
68
+ *
69
+ * The count form exists because most waits here are for the Nth frame rather
70
+ * than for a particular one; a caller wanting a specific frame should use
71
+ * `waitForUpstreamPayload` with a predicate.
72
+ */
73
+ export async function waitForUpstreamPayloads(stream, type_, count = 1) {
74
+ for (;;) {
75
+ const arrived = stream.nextWrite();
76
+ const found = readPayloadsOfType(stream.flushed(), type_);
77
+ if (found.length >= count)
78
+ return found;
79
+ await arrived;
80
+ }
81
+ }
82
+ /**
83
+ * Resolve once at least `count` `trigger.fire` frames have been written, with
84
+ * every runId seen in order.
85
+ */
86
+ export async function waitForTriggerFireRunIds(stream, count) {
87
+ for (;;) {
88
+ const arrived = stream.nextWrite();
89
+ const ids = parseTriggerFireRunIds(stream.flushed());
90
+ if (ids.length >= count)
91
+ return ids;
92
+ await arrived;
93
+ }
94
+ }
@@ -0,0 +1,187 @@
1
+ import { type AnnotatedPluginFactory, type DirectorRegistry, type ToolDeclaration } from "@intx/agent";
2
+ import type { WorkflowDefinition } from "@intx/workflow/definition";
3
+ import type { ActionHandler, LoopFnRegistry } from "@intx/workflow";
4
+ export interface LoadWorkflowDefinitionFromClosureArgs {
5
+ /**
6
+ * Directory of the materialized workflow package within the closure:
7
+ * the directory holding the package's `package.json`, with its
8
+ * `node_modules/` already laid out by the closure-materialization
9
+ * machinery so the entry module's bare-specifier imports resolve.
10
+ */
11
+ readonly packageDir: string;
12
+ /**
13
+ * Optional token mixed into the import URL's query string to bust
14
+ * Node's ESM module cache. Node keys the ESM cache by resolved
15
+ * URL/path, not by content: a process that imports the same package
16
+ * directory twice with different bytes underneath (a rare re-apply in
17
+ * a reused child) would otherwise resolve to the first-imported module
18
+ * instance. Passing a per-materialization token (the closure's
19
+ * integrity SRI is the natural choice) makes each materialization a
20
+ * distinct ESM cache entry. Omit it when the process imports a given
21
+ * package directory at most once.
22
+ */
23
+ readonly importCacheKey?: string;
24
+ /**
25
+ * Test seam for dynamic import. Production omits this and the loader
26
+ * uses the native dynamic-import expression. The argument is the
27
+ * `file://` URL the loader resolves for the `interchange.workflow`
28
+ * entry.
29
+ */
30
+ readonly importModule?: (importUrl: string) => Promise<unknown>;
31
+ }
32
+ /**
33
+ * Import the `interchange.workflow` entry from a materialized workflow
34
+ * package closure, evaluate it, and return the validated
35
+ * `WorkflowDefinition` its `defineWorkflow(...)` call produced.
36
+ *
37
+ * @param args - the materialized package directory plus optional import
38
+ * seams
39
+ * @returns the validated `WorkflowDefinition`
40
+ * @throws if the package.json is missing/malformed, declares no
41
+ * `interchange.workflow` entry, the entry path escapes the package
42
+ * directory, the module cannot be imported, or its evaluation does not
43
+ * produce exactly one value that validates as a `WorkflowDefinition`
44
+ */
45
+ export declare function loadWorkflowDefinitionFromClosure(args: LoadWorkflowDefinitionFromClosureArgs): Promise<WorkflowDefinition>;
46
+ export interface LoadWorkflowDirectorRegistryFromClosureArgs {
47
+ /**
48
+ * Directory of the materialized workflow package within the closure --
49
+ * the same directory `loadWorkflowDefinitionFromClosure` reads. Both the
50
+ * approval-time probe and the run-child call this over the SAME frozen
51
+ * closure, so the director set they compose cannot drift.
52
+ */
53
+ readonly packageDir: string;
54
+ /** See `LoadWorkflowDefinitionFromClosureArgs.importCacheKey`. */
55
+ readonly importCacheKey?: string;
56
+ /** Test seam for dynamic import; see the definition loader's variant. */
57
+ readonly importModule?: (importUrl: string) => Promise<unknown>;
58
+ }
59
+ /**
60
+ * Compose the `DirectorRegistry` for a workflow closure from the closure
61
+ * package's OWN `interchange.directors` module (if any), alongside the
62
+ * built-in default director. A package with no `interchange.directors`
63
+ * field composes to the built-ins-only registry -- absence is valid, a
64
+ * workflow need not ship a director. A present-but-empty directors module
65
+ * is malformed and throws, matching the tool-package loader.
66
+ *
67
+ * Only the workflow's OWN package directors are loaded here. Directors
68
+ * shipped by PINNED dependency packages are deliberately not resolved on
69
+ * the source-ref path yet: the airlocked probe does not materialize pinned
70
+ * packages, so loading them here would let the runtime resolve a director
71
+ * the probe never advertised for approval. A workflow referencing a
72
+ * pinned-package director fails closed (the capability walk reports it as
73
+ * unresolved).
74
+ *
75
+ * @throws if the directors entry path escapes the package, the module
76
+ * cannot be imported, or it exports no `AnnotatedDirectorFactory` value
77
+ */
78
+ export declare function loadWorkflowDirectorRegistryFromClosure(args: LoadWorkflowDirectorRegistryFromClosureArgs): Promise<DirectorRegistry>;
79
+ export interface LoadWorkflowLoopFnsFromClosureArgs {
80
+ /**
81
+ * Directory of the materialized workflow package within the closure --
82
+ * the same directory `loadWorkflowDefinitionFromClosure` reads.
83
+ */
84
+ readonly packageDir: string;
85
+ /** See `LoadWorkflowDefinitionFromClosureArgs.importCacheKey`. */
86
+ readonly importCacheKey?: string;
87
+ /** Test seam for dynamic import; see the definition loader's variant. */
88
+ readonly importModule?: (importUrl: string) => Promise<unknown>;
89
+ }
90
+ /**
91
+ * Compose the `LoopFnRegistry` for a workflow closure from the closure
92
+ * package's OWN `interchange.loops` module. A `loop` primitive's `while` and
93
+ * `carry` refs resolve by EXPORT NAME against that module's exports.
94
+ *
95
+ * Unlike directors there is NO built-in default: a package with no
96
+ * `interchange.loops` field composes to an EMPTY registry that throws on any
97
+ * ref lookup. A workflow that declares a `loop` but ships no loops module thus
98
+ * fails closed when its refs are resolved (eagerly, at establish); a workflow
99
+ * with no `loop` primitive never resolves a ref, so an absent field is valid
100
+ * there. Loading OUTSIDE the definition-hash re-verify is safe: the approved
101
+ * hash pins each ref string, and the closure's SRI pins the module bytes.
102
+ *
103
+ * @throws (from the returned registry) if a requested ref names no export, or
104
+ * names an export that is not a function.
105
+ * @throws if the loops entry path escapes the package or cannot be imported.
106
+ */
107
+ export declare function loadWorkflowLoopFnsFromClosure(args: LoadWorkflowLoopFnsFromClosureArgs): Promise<LoopFnRegistry>;
108
+ export interface LoadWorkflowActionHandlersFromClosureArgs {
109
+ /** Directory of the materialized workflow package within the closure. */
110
+ readonly packageDir: string;
111
+ /** See `LoadWorkflowDefinitionFromClosureArgs.importCacheKey`. */
112
+ readonly importCacheKey?: string;
113
+ /** Test seam for dynamic import; see the definition loader's variant. */
114
+ readonly importModule?: (importUrl: string) => Promise<unknown>;
115
+ }
116
+ /**
117
+ * Compose the action-handler resolver for a workflow closure from the closure
118
+ * package's OWN `interchange.actions` module. An `action` primitive's `handler`
119
+ * ref resolves by EXPORT NAME against that module's exports.
120
+ *
121
+ * Mirrors {@link loadWorkflowLoopFnsFromClosure}: there is NO built-in default,
122
+ * so a package with no `interchange.actions` field composes to a resolver that
123
+ * throws on any lookup. A workflow that declares an `action` but ships no
124
+ * actions module fails closed when its handler is resolved (eagerly, at
125
+ * establish); a workflow with no `action` primitive never resolves a handler.
126
+ * Loading OUTSIDE the definition-hash re-verify is safe: the approved hash pins
127
+ * each handler ref string, and the closure's SRI pins the module bytes.
128
+ *
129
+ * @throws (from the returned resolver) if a requested ref names no export, or an
130
+ * export that is not a function.
131
+ * @throws if the actions entry path escapes the package or cannot be imported.
132
+ */
133
+ export declare function loadWorkflowActionHandlersFromClosure(args: LoadWorkflowActionHandlersFromClosureArgs): Promise<(ref: string) => ActionHandler>;
134
+ export interface LoadWorkflowPluginsFromClosureArgs {
135
+ /**
136
+ * Directory of the materialized workflow package within the closure --
137
+ * the same directory `loadWorkflowDefinitionFromClosure` reads. Each
138
+ * declared plugin package is resolved from this package's laid-out
139
+ * `node_modules/`, exactly as the workflow entry's own bare-specifier
140
+ * imports resolve.
141
+ */
142
+ readonly packageDir: string;
143
+ /**
144
+ * Plugin-package names the workflow's agents declare via
145
+ * `AgentDefinition.plugins` (`["@intx/tools-lsp"]`). Each MUST be a
146
+ * direct dependency of the workflow package so it is laid out under the
147
+ * workflow package's `node_modules/`. Empty is valid (no plugins).
148
+ */
149
+ readonly plugins: readonly string[];
150
+ /** See `LoadWorkflowDefinitionFromClosureArgs.importCacheKey`. */
151
+ readonly importCacheKey?: string;
152
+ /** Test seam for dynamic import; see the definition loader's variant. */
153
+ readonly importModule?: (importUrl: string) => Promise<unknown>;
154
+ }
155
+ /**
156
+ * Import each declared plugin package's `interchange.tools` module from the
157
+ * materialized workflow closure and collect the `AnnotatedPluginFactory`
158
+ * values it exports. This is the run-child counterpart to the tool-package
159
+ * loader's plugin channel: a source-ref workflow contributes no plugin factory
160
+ * through its agent definition (a plugin has no agent slot), so the child
161
+ * materializes the declared plugins straight from the already-laid-out closure
162
+ * -- no re-download, no manifest -- and feeds them into the existing per-step
163
+ * plugin chain. The closure bytes were SRI-verified when the deploy applied the
164
+ * frozen closure, and resolution walks the same `node_modules/` graph the
165
+ * workflow entry's imports use.
166
+ *
167
+ * @throws if a declared plugin package cannot be resolved, declares no
168
+ * `interchange.tools` entry, the entry escapes the package, cannot be
169
+ * imported, or exports no `AnnotatedPluginFactory` value
170
+ */
171
+ export declare function loadWorkflowPluginFactoriesFromClosure(args: LoadWorkflowPluginsFromClosureArgs): Promise<AnnotatedPluginFactory[]>;
172
+ /**
173
+ * Read the static tool `definitions` each declared plugin package
174
+ * contributes, keyed by plugin-package name, WITHOUT retaining the plugin
175
+ * factory (so the caller never instantiates a plugin, which for LSP would
176
+ * start a subprocess). This is the probe/capability-walk counterpart to
177
+ * `loadWorkflowPluginFactoriesFromClosure`: it loads the SAME plugin module
178
+ * from the SAME frozen closure so the tool grant surface the walk approves
179
+ * matches the plugin the run-child materializes.
180
+ *
181
+ * A plugin package that exports plugin factories but declares no tool
182
+ * definitions (a middleware-only plugin) maps to an empty array -- valid,
183
+ * it contributes no tool grant.
184
+ *
185
+ * @throws under the same conditions as `loadWorkflowPluginFactoriesFromClosure`
186
+ */
187
+ export declare function loadWorkflowPluginToolDefinitionsFromClosure(args: LoadWorkflowPluginsFromClosureArgs): Promise<Map<string, readonly ToolDeclaration[]>>;