@intx/workflow-host 0.3.0 → 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 (78) hide show
  1. package/README.md +21 -4
  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.js +5 -14
  5. package/dist/adapters/spawn-child.d.ts +42 -6
  6. package/dist/adapters/spawn-child.js +8 -18
  7. package/dist/adapters/step-invoker.d.ts +52 -2
  8. package/dist/adapters/step-invoker.js +230 -60
  9. package/dist/adapters/substrate-mailbox-store.d.ts +80 -0
  10. package/dist/adapters/substrate-mailbox-store.js +404 -0
  11. package/dist/child/child-mailbox-reader.d.ts +10 -0
  12. package/dist/child/child-mailbox-reader.js +23 -0
  13. package/dist/child/credential-cell.d.ts +8 -0
  14. package/dist/child/credential-cell.js +66 -0
  15. package/dist/child/from-process-env.d.ts +12 -0
  16. package/dist/child/from-process-env.js +6 -0
  17. package/dist/child/index.d.ts +4 -1
  18. package/dist/child/index.js +4 -1
  19. package/dist/child/mailbox-mutation-bridge.d.ts +61 -0
  20. package/dist/child/mailbox-mutation-bridge.js +101 -0
  21. package/dist/child/mailbox-watch-registry.d.ts +17 -0
  22. package/dist/child/mailbox-watch-registry.js +61 -0
  23. package/dist/child/outbound-mail-bridge.d.ts +3 -2
  24. package/dist/child/outbound-mail-bridge.js +20 -32
  25. package/dist/child/pending-request.d.ts +89 -0
  26. package/dist/child/pending-request.js +80 -0
  27. package/dist/child/run-child.d.ts +69 -7
  28. package/dist/child/run-child.js +307 -75
  29. package/dist/child/substrate-write-bridge.d.ts +3 -2
  30. package/dist/child/substrate-write-bridge.js +21 -38
  31. package/dist/child/supervisor-backed-transport.d.ts +52 -6
  32. package/dist/child/supervisor-backed-transport.js +205 -62
  33. package/dist/child/warm-agent-cache.d.ts +44 -4
  34. package/dist/child/warm-agent-cache.js +41 -10
  35. package/dist/index.d.ts +4 -3
  36. package/dist/index.js +4 -3
  37. package/dist/ipc/control-channel.d.ts +93 -2
  38. package/dist/ipc/control-channel.js +147 -47
  39. package/dist/ipc/index.d.ts +1 -1
  40. package/dist/ipc/index.js +1 -1
  41. package/dist/run-body-then-cleanup.d.ts +17 -0
  42. package/dist/run-body-then-cleanup.js +38 -0
  43. package/dist/seams/scheduler.d.ts +12 -0
  44. package/dist/seams/scheduler.js +13 -4
  45. package/dist/supervisor/cancel-signing.js +3 -7
  46. package/dist/supervisor/credentials.d.ts +17 -5
  47. package/dist/supervisor/recycle.d.ts +5 -1
  48. package/dist/supervisor/run-event-compaction.d.ts +2 -2
  49. package/dist/supervisor/run-event-compaction.js +11 -16
  50. package/dist/supervisor/run-event-recovery.d.ts +34 -0
  51. package/dist/supervisor/run-event-recovery.js +45 -0
  52. package/dist/supervisor/supervisor.d.ts +27 -4
  53. package/dist/supervisor/supervisor.js +644 -58
  54. package/dist/supervisor/terminal-commit.js +3 -7
  55. package/dist/supervisor/types.d.ts +30 -0
  56. package/dist/testing/change-notifier.d.ts +12 -0
  57. package/dist/testing/change-notifier.js +63 -0
  58. package/dist/testing/index.d.ts +8 -0
  59. package/dist/testing/index.js +16 -0
  60. package/dist/testing/log-capture.d.ts +52 -0
  61. package/dist/testing/log-capture.js +124 -0
  62. package/dist/testing/mail-bus.d.ts +22 -0
  63. package/dist/testing/mail-bus.js +78 -0
  64. package/dist/testing/memory-streams.d.ts +43 -0
  65. package/dist/testing/memory-streams.js +211 -0
  66. package/dist/testing/spawn-observer.d.ts +12 -0
  67. package/dist/testing/spawn-observer.js +36 -0
  68. package/dist/testing/stub-repo-store.d.ts +10 -0
  69. package/dist/testing/stub-repo-store.js +39 -0
  70. package/dist/testing/supervisor-reaper.d.ts +24 -0
  71. package/dist/testing/supervisor-reaper.js +49 -0
  72. package/dist/testing/upstream-frames.d.ts +47 -0
  73. package/dist/testing/upstream-frames.js +94 -0
  74. package/dist/workflow-definition-loader.d.ts +56 -0
  75. package/dist/workflow-definition-loader.js +106 -0
  76. package/package.json +17 -11
  77. package/dist/conversation-text.d.ts +0 -23
  78. package/dist/conversation-text.js +0 -56
@@ -112,6 +112,112 @@ export async function loadWorkflowDirectorRegistryFromClosure(args) {
112
112
  logger.debug `loaded ${String(loaded.length)} custom director(s) from ${args.packageDir}`;
113
113
  return createWorkflowDirectorRegistry(loaded);
114
114
  }
115
+ /**
116
+ * Compose the `LoopFnRegistry` for a workflow closure from the closure
117
+ * package's OWN `interchange.loops` module. A `loop` primitive's `while` and
118
+ * `carry` refs resolve by EXPORT NAME against that module's exports.
119
+ *
120
+ * Unlike directors there is NO built-in default: a package with no
121
+ * `interchange.loops` field composes to an EMPTY registry that throws on any
122
+ * ref lookup. A workflow that declares a `loop` but ships no loops module thus
123
+ * fails closed when its refs are resolved (eagerly, at establish); a workflow
124
+ * with no `loop` primitive never resolves a ref, so an absent field is valid
125
+ * there. Loading OUTSIDE the definition-hash re-verify is safe: the approved
126
+ * hash pins each ref string, and the closure's SRI pins the module bytes.
127
+ *
128
+ * @throws (from the returned registry) if a requested ref names no export, or
129
+ * names an export that is not a function.
130
+ * @throws if the loops entry path escapes the package or cannot be imported.
131
+ */
132
+ export async function loadWorkflowLoopFnsFromClosure(args) {
133
+ const importModule = args.importModule ?? ((url) => import(url));
134
+ const pkgJson = await readPackageJSON(args.packageDir);
135
+ const entryRel = pkgJson.interchange?.loops;
136
+ if (entryRel === undefined) {
137
+ // No loops module. A workflow with no loop primitive never calls this; one
138
+ // that declares a loop fails closed here when its ref is resolved.
139
+ return (ref) => {
140
+ throw new Error(`loop fn ${JSON.stringify(ref)} was requested, but the workflow package at ${args.packageDir} declares no interchange.loops module`);
141
+ };
142
+ }
143
+ const entryAbs = await resolveContainedEntry(args.packageDir, entryRel, "interchange.loops");
144
+ const importUrl = args.importCacheKey === undefined
145
+ ? pathToFileURL(entryAbs).href
146
+ : `${pathToFileURL(entryAbs).href}?importCacheKey=${encodeURIComponent(args.importCacheKey)}`;
147
+ let mod;
148
+ try {
149
+ mod = await importModule(importUrl);
150
+ }
151
+ catch (cause) {
152
+ throw new Error(`failed to import interchange.loops entry ${JSON.stringify(entryRel)} for workflow package at ${args.packageDir}`, { cause });
153
+ }
154
+ if (mod === null || typeof mod !== "object") {
155
+ throw new Error(`interchange.loops entry ${JSON.stringify(entryRel)} for workflow package at ${args.packageDir} did not evaluate to a module object`);
156
+ }
157
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- module namespace object: loop fns resolve by export name
158
+ const loopModule = mod;
159
+ logger.debug `loaded interchange.loops module from ${args.packageDir}`;
160
+ return (ref) => {
161
+ const fn = loopModule[ref];
162
+ if (typeof fn !== "function") {
163
+ throw new Error(`interchange.loops entry ${JSON.stringify(entryRel)} for workflow package at ${args.packageDir} exports no loop fn named ${JSON.stringify(ref)}`);
164
+ }
165
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- resolved by export name; the loop runtime applies it as a pure (childOutput, carryState) fn
166
+ return fn;
167
+ };
168
+ }
169
+ /**
170
+ * Compose the action-handler resolver for a workflow closure from the closure
171
+ * package's OWN `interchange.actions` module. An `action` primitive's `handler`
172
+ * ref resolves by EXPORT NAME against that module's exports.
173
+ *
174
+ * Mirrors {@link loadWorkflowLoopFnsFromClosure}: there is NO built-in default,
175
+ * so a package with no `interchange.actions` field composes to a resolver that
176
+ * throws on any lookup. A workflow that declares an `action` but ships no
177
+ * actions module fails closed when its handler is resolved (eagerly, at
178
+ * establish); a workflow with no `action` primitive never resolves a handler.
179
+ * Loading OUTSIDE the definition-hash re-verify is safe: the approved hash pins
180
+ * each handler ref string, and the closure's SRI pins the module bytes.
181
+ *
182
+ * @throws (from the returned resolver) if a requested ref names no export, or an
183
+ * export that is not a function.
184
+ * @throws if the actions entry path escapes the package or cannot be imported.
185
+ */
186
+ export async function loadWorkflowActionHandlersFromClosure(args) {
187
+ const importModule = args.importModule ?? ((url) => import(url));
188
+ const pkgJson = await readPackageJSON(args.packageDir);
189
+ const entryRel = pkgJson.interchange?.actions;
190
+ if (entryRel === undefined) {
191
+ return (ref) => {
192
+ throw new Error(`action handler ${JSON.stringify(ref)} was requested, but the workflow package at ${args.packageDir} declares no interchange.actions module`);
193
+ };
194
+ }
195
+ const entryAbs = await resolveContainedEntry(args.packageDir, entryRel, "interchange.actions");
196
+ const importUrl = args.importCacheKey === undefined
197
+ ? pathToFileURL(entryAbs).href
198
+ : `${pathToFileURL(entryAbs).href}?importCacheKey=${encodeURIComponent(args.importCacheKey)}`;
199
+ let mod;
200
+ try {
201
+ mod = await importModule(importUrl);
202
+ }
203
+ catch (cause) {
204
+ throw new Error(`failed to import interchange.actions entry ${JSON.stringify(entryRel)} for workflow package at ${args.packageDir}`, { cause });
205
+ }
206
+ if (mod === null || typeof mod !== "object") {
207
+ throw new Error(`interchange.actions entry ${JSON.stringify(entryRel)} for workflow package at ${args.packageDir} did not evaluate to a module object`);
208
+ }
209
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- module namespace object: action handlers resolve by export name
210
+ const actionModule = mod;
211
+ logger.debug `loaded interchange.actions module from ${args.packageDir}`;
212
+ return (ref) => {
213
+ const fn = actionModule[ref];
214
+ if (typeof fn !== "function") {
215
+ throw new Error(`interchange.actions entry ${JSON.stringify(entryRel)} for workflow package at ${args.packageDir} exports no action handler named ${JSON.stringify(ref)}`);
216
+ }
217
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- resolved by export name; invoked as an ActionHandler (input, ctx, signal) by createDefaultActionInvoker
218
+ return fn;
219
+ };
220
+ }
115
221
  /**
116
222
  * Import each declared plugin package's `interchange.tools` module from the
117
223
  * materialized workflow closure and collect the `AnnotatedPluginFactory`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@intx/workflow-host",
3
3
  "description": "Production-host implementations of the abstract WorkflowRuntimeEnv from @intx/workflow",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "license": "LGPL-2.1-only",
6
6
  "type": "module",
7
7
  "exports": {
@@ -9,19 +9,25 @@
9
9
  "intx-src": "./src/index.ts",
10
10
  "types": "./dist/index.d.ts",
11
11
  "default": "./dist/index.js"
12
+ },
13
+ "./testing": {
14
+ "intx-src": "./src/testing/index.ts",
15
+ "types": "./dist/testing/index.d.ts",
16
+ "default": "./dist/testing/index.js"
12
17
  }
13
18
  },
14
19
  "dependencies": {
15
- "@intx/agent": "0.3.0",
16
- "@intx/crypto": "0.3.0",
17
- "@intx/hub-sessions": "0.3.0",
18
- "@intx/inference": "0.3.0",
19
- "@intx/log": "0.3.0",
20
- "@intx/mail-memory": "0.3.0",
21
- "@intx/mime": "0.3.0",
22
- "@intx/storage-isogit": "0.3.0",
23
- "@intx/types": "0.3.0",
24
- "@intx/workflow": "0.3.0",
20
+ "@intx/agent": "0.4.0",
21
+ "@intx/crypto": "0.4.0",
22
+ "@intx/hub-sessions": "0.4.0",
23
+ "@intx/inference": "0.4.0",
24
+ "@intx/log": "0.4.0",
25
+ "@intx/mail-memory": "0.4.0",
26
+ "@intx/mailbox": "0.4.0",
27
+ "@intx/mime": "0.4.0",
28
+ "@intx/storage-isogit": "0.4.0",
29
+ "@intx/types": "0.4.0",
30
+ "@intx/workflow": "0.4.0",
25
31
  "arktype": "^2.1.29"
26
32
  },
27
33
  "files": [
@@ -1,23 +0,0 @@
1
- /**
2
- * Extract the conversation body text from a raw inbound MIME message.
3
- *
4
- * Three on-wire shapes are handled, matching every producer the mail
5
- * bus accepts:
6
- * 1. The Interchange assembler's `multipart/signed` envelope whose
7
- * first part is a `multipart/mixed` body carrying the text at part
8
- * path `1.1`.
9
- * 2. A `multipart/signed` envelope wrapping a bare `text/plain` part
10
- * (a sender that signs without the `multipart/mixed` wrapper); the
11
- * text is at part path `1`.
12
- * 3. A flat top-level `text/plain` message (no multipart structure at
13
- * all); the body is the bytes after the header section.
14
- *
15
- * The top-level `Content-Type` selects the shape: only a `multipart/*`
16
- * root walks into parts; anything else reads the single body directly.
17
- * This mirrors the conversation branch of mail-memory's `fetchFull`
18
- * while also tolerating the flat single-part case the in-process agent
19
- * accepts, so a non-standard inbound mail still delivers its text to
20
- * the agent rather than crashing the run. `messageId` is used only for
21
- * error attribution.
22
- */
23
- export declare function extractConversationText(raw: Uint8Array, messageId: string): string;
@@ -1,56 +0,0 @@
1
- // Shared inbound-mail conversation-text extraction. Two call sites resolve a
2
- // run's mail input to the text the agent actually sees, each at the layer that
3
- // knows its bytes are inbound mail: the child's `resolveTriggerPayload` (turn
4
- // 1, the trigger) reads the mail from the substrate, and the supervisor's
5
- // dispatch loop (turn 2+, a mail delivered to a parked run as a signal) inlines
6
- // it. Both must produce the SAME shape -- extracted body text, not the raw MIME
7
- // envelope -- so the extractor lives here rather than in either caller.
8
- import { extractPartByPath, parseHeaderSection, parseMimePart, } from "@intx/mime";
9
- /**
10
- * Extract the conversation body text from a raw inbound MIME message.
11
- *
12
- * Three on-wire shapes are handled, matching every producer the mail
13
- * bus accepts:
14
- * 1. The Interchange assembler's `multipart/signed` envelope whose
15
- * first part is a `multipart/mixed` body carrying the text at part
16
- * path `1.1`.
17
- * 2. A `multipart/signed` envelope wrapping a bare `text/plain` part
18
- * (a sender that signs without the `multipart/mixed` wrapper); the
19
- * text is at part path `1`.
20
- * 3. A flat top-level `text/plain` message (no multipart structure at
21
- * all); the body is the bytes after the header section.
22
- *
23
- * The top-level `Content-Type` selects the shape: only a `multipart/*`
24
- * root walks into parts; anything else reads the single body directly.
25
- * This mirrors the conversation branch of mail-memory's `fetchFull`
26
- * while also tolerating the flat single-part case the in-process agent
27
- * accepts, so a non-standard inbound mail still delivers its text to
28
- * the agent rather than crashing the run. `messageId` is used only for
29
- * error attribution.
30
- */
31
- export function extractConversationText(raw, messageId) {
32
- const { headers, bodyOffset } = parseHeaderSection(raw);
33
- const rootMime = (headers.get("content-type") ?? "")
34
- .split(";")[0]
35
- ?.trim()
36
- .toLowerCase();
37
- if (rootMime === undefined || !rootMime.startsWith("multipart/")) {
38
- // Flat single-part message: the body is everything after the
39
- // header section.
40
- return new TextDecoder("utf-8", { fatal: false }).decode(raw.subarray(bodyOffset));
41
- }
42
- let part1;
43
- try {
44
- part1 = parseMimePart(extractPartByPath(raw, "1"));
45
- }
46
- catch (cause) {
47
- throw new Error(`conversation-text: cannot parse inbound mail part 1 for messageId ${messageId}`, { cause });
48
- }
49
- const part1Mime = (part1.contentType.split(";")[0] ?? "")
50
- .trim()
51
- .toLowerCase();
52
- const bodyBytes = part1Mime.startsWith("multipart/")
53
- ? parseMimePart(extractPartByPath(raw, "1.1")).body
54
- : part1.body;
55
- return new TextDecoder("utf-8", { fatal: false }).decode(bodyBytes);
56
- }