@intx/workflow-host 0.2.2

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 (81) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +287 -0
  3. package/dist/adapters/blob-substrate.d.ts +49 -0
  4. package/dist/adapters/blob-substrate.js +140 -0
  5. package/dist/adapters/repo-store.d.ts +39 -0
  6. package/dist/adapters/repo-store.js +344 -0
  7. package/dist/adapters/spawn-child.d.ts +74 -0
  8. package/dist/adapters/spawn-child.js +152 -0
  9. package/dist/adapters/step-invoker.d.ts +114 -0
  10. package/dist/adapters/step-invoker.js +360 -0
  11. package/dist/child/env-bootstrap.d.ts +56 -0
  12. package/dist/child/env-bootstrap.js +120 -0
  13. package/dist/child/from-process-env.d.ts +127 -0
  14. package/dist/child/from-process-env.js +183 -0
  15. package/dist/child/index.d.ts +9 -0
  16. package/dist/child/index.js +9 -0
  17. package/dist/child/outbound-mail-bridge.d.ts +36 -0
  18. package/dist/child/outbound-mail-bridge.js +143 -0
  19. package/dist/child/proxy-repo-store.d.ts +27 -0
  20. package/dist/child/proxy-repo-store.js +200 -0
  21. package/dist/child/run-child.d.ts +320 -0
  22. package/dist/child/run-child.js +900 -0
  23. package/dist/child/self-discovery.d.ts +29 -0
  24. package/dist/child/self-discovery.js +57 -0
  25. package/dist/child/substrate-write-bridge.d.ts +72 -0
  26. package/dist/child/substrate-write-bridge.js +188 -0
  27. package/dist/child/supervisor-backed-transport.d.ts +10 -0
  28. package/dist/child/supervisor-backed-transport.js +113 -0
  29. package/dist/child/warm-agent-cache.d.ts +78 -0
  30. package/dist/child/warm-agent-cache.js +112 -0
  31. package/dist/drain-controller.d.ts +37 -0
  32. package/dist/drain-controller.js +46 -0
  33. package/dist/index.d.ts +10 -0
  34. package/dist/index.js +10 -0
  35. package/dist/ipc/control-channel.d.ts +336 -0
  36. package/dist/ipc/control-channel.js +532 -0
  37. package/dist/ipc/crypto.d.ts +46 -0
  38. package/dist/ipc/crypto.js +126 -0
  39. package/dist/ipc/envelope.d.ts +53 -0
  40. package/dist/ipc/envelope.js +88 -0
  41. package/dist/ipc/event-channel.d.ts +677 -0
  42. package/dist/ipc/event-channel.js +278 -0
  43. package/dist/ipc/index.d.ts +4 -0
  44. package/dist/ipc/index.js +143 -0
  45. package/dist/mail-bus/hub-transport-adapter.d.ts +30 -0
  46. package/dist/mail-bus/hub-transport-adapter.js +76 -0
  47. package/dist/mail-bus/index.d.ts +1 -0
  48. package/dist/mail-bus/index.js +1 -0
  49. package/dist/seams/index.d.ts +3 -0
  50. package/dist/seams/index.js +3 -0
  51. package/dist/seams/scheduler-adapter.d.ts +3 -0
  52. package/dist/seams/scheduler-adapter.js +24 -0
  53. package/dist/seams/scheduler.d.ts +94 -0
  54. package/dist/seams/scheduler.js +397 -0
  55. package/dist/seams/signal-channel.d.ts +74 -0
  56. package/dist/seams/signal-channel.js +304 -0
  57. package/dist/supervisor/cancel-signing.d.ts +68 -0
  58. package/dist/supervisor/cancel-signing.js +144 -0
  59. package/dist/supervisor/child-termination.d.ts +51 -0
  60. package/dist/supervisor/child-termination.js +76 -0
  61. package/dist/supervisor/credentials.d.ts +101 -0
  62. package/dist/supervisor/credentials.js +153 -0
  63. package/dist/supervisor/dispatch-attribution.d.ts +37 -0
  64. package/dist/supervisor/dispatch-attribution.js +114 -0
  65. package/dist/supervisor/drain-timeout.d.ts +127 -0
  66. package/dist/supervisor/drain-timeout.js +231 -0
  67. package/dist/supervisor/index.d.ts +7 -0
  68. package/dist/supervisor/index.js +6 -0
  69. package/dist/supervisor/recycle.d.ts +212 -0
  70. package/dist/supervisor/recycle.js +440 -0
  71. package/dist/supervisor/run-event-compaction.d.ts +34 -0
  72. package/dist/supervisor/run-event-compaction.js +115 -0
  73. package/dist/supervisor/spawn-env.d.ts +39 -0
  74. package/dist/supervisor/spawn-env.js +36 -0
  75. package/dist/supervisor/supervisor.d.ts +202 -0
  76. package/dist/supervisor/supervisor.js +2244 -0
  77. package/dist/supervisor/terminal-broadcaster.d.ts +45 -0
  78. package/dist/supervisor/terminal-broadcaster.js +184 -0
  79. package/dist/supervisor/types.d.ts +542 -0
  80. package/dist/supervisor/types.js +10 -0
  81. package/package.json +35 -0
@@ -0,0 +1,143 @@
1
+ // Child-side outbound-mail bridge (OUTBOUND half of mailbox ownership,
2
+ // §3a).
3
+ //
4
+ // The workflow-process child holds no signing key for the agent's
5
+ // identity. The supervisor is the sole mail owner: it holds the host
6
+ // transport against which the agent's address is registered with its
7
+ // `CryptoProvider`, and it is the only process that can emit signed mail
8
+ // on the agent's behalf. So a step agent never calls `transport.send`
9
+ // directly; its mail tools are backed by a transport whose outbound side
10
+ // routes through this bridge, which forwards the structured
11
+ // `OutboundMessage` plus the sender (agent) address up over the control
12
+ // IPC. The supervisor performs the actual signed send and replies with
13
+ // the `SendReceipt`.
14
+ //
15
+ // Lifecycle of one outbound send:
16
+ //
17
+ // 1. The agent's mail tool (or the step reply path) calls the
18
+ // supervisor-backed transport's `send`. The transport calls
19
+ // `bridge.submit(senderAddress, message)`.
20
+ // 2. `submit` mints a `requestId`, registers a pending awaiter, and
21
+ // emits `outbound.message` upstream carrying the JSON-projected
22
+ // message (attachment bytes base64-encoded).
23
+ // 3. The supervisor receives the request, performs the signed send
24
+ // through the host transport (`MailBusBindings.sendOutbound`), and
25
+ // replies with `outbound.result` carrying the `SendReceipt` (or a
26
+ // structured failure).
27
+ // 4. The bridge resolves / rejects the pending awaiter; the
28
+ // transport's `send` returns the receipt to the mail tool. A
29
+ // supervisor-side failure (unregistered sender, signing failure,
30
+ // transport rejection) surfaces as a rejection so the agent's
31
+ // mail-tool call fails loudly rather than silently dropping the
32
+ // send.
33
+ import { getLogger } from "@intx/log";
34
+ import { base64Encode } from "@intx/types";
35
+ const logger = getLogger(["workflow-host", "child", "outbound-mail-bridge"]);
36
+ /**
37
+ * Construct the child-side outbound-mail bridge. Pending sends live in
38
+ * a map keyed by `requestId`; the bridge resolves the awaiter when the
39
+ * supervisor's matching `outbound.result` lands.
40
+ */
41
+ export function createChildOutboundMailBridge(opts) {
42
+ const pending = new Map();
43
+ const allocate = opts.allocateRequestId ?? defaultRequestIdAllocator();
44
+ return {
45
+ get pendingCount() {
46
+ return pending.size;
47
+ },
48
+ async submit(senderAddress, message) {
49
+ const requestId = allocate();
50
+ const resultPromise = new Promise((resolve, reject) => {
51
+ pending.set(requestId, { resolve, reject });
52
+ });
53
+ try {
54
+ await opts.upstreamSender.send({
55
+ type: "outbound.message",
56
+ data: {
57
+ requestId,
58
+ senderAddress,
59
+ message: projectOutboundMessage(message),
60
+ },
61
+ });
62
+ }
63
+ catch (cause) {
64
+ pending.delete(requestId);
65
+ const reason = cause instanceof Error ? cause.message : String(cause);
66
+ throw new Error(`workflow-child outbound mail: upstream send failed for requestId ${requestId}: ${reason}`, { cause });
67
+ }
68
+ return resultPromise;
69
+ },
70
+ handleResult(data) {
71
+ const entry = pending.get(data.requestId);
72
+ if (entry === undefined) {
73
+ logger.warn `outbound.result landed with no pending entry; requestId=${data.requestId} dropped`;
74
+ return;
75
+ }
76
+ pending.delete(data.requestId);
77
+ if (data.result.ok) {
78
+ entry.resolve({
79
+ messageId: data.result.messageId,
80
+ status: data.result.status,
81
+ });
82
+ return;
83
+ }
84
+ entry.reject(new Error(`workflow-child outbound mail (requestId=${data.requestId}) rejected by supervisor: ${data.result.reason}`));
85
+ },
86
+ cancelAll(reason) {
87
+ for (const [requestId, entry] of pending) {
88
+ entry.reject(new Error(`workflow-child outbound mail (requestId=${requestId}) cancelled: ${reason}`));
89
+ }
90
+ pending.clear();
91
+ },
92
+ };
93
+ }
94
+ /**
95
+ * Project a runtime `OutboundMessage` into the IPC wire shape. Optional
96
+ * fields are omitted when absent (the wire validator spells them
97
+ * optional), and attachment bytes ride base64-encoded so the NDJSON
98
+ * control channel stays text-safe.
99
+ */
100
+ function projectOutboundMessage(message) {
101
+ const payload = {
102
+ to: message.to,
103
+ type: message.type,
104
+ };
105
+ if (message.cc !== undefined)
106
+ payload.cc = message.cc;
107
+ if (message.subject !== undefined)
108
+ payload.subject = message.subject;
109
+ if (message.content !== undefined)
110
+ payload.content = message.content;
111
+ if (message.payload !== undefined)
112
+ payload.payload = message.payload;
113
+ if (message.summary !== undefined)
114
+ payload.summary = message.summary;
115
+ if (message.inReplyTo !== undefined)
116
+ payload.inReplyTo = message.inReplyTo;
117
+ if (message.correlationId !== undefined) {
118
+ payload.correlationId = message.correlationId;
119
+ }
120
+ if (message.sessionId !== undefined)
121
+ payload.sessionId = message.sessionId;
122
+ if (message.tenantId !== undefined)
123
+ payload.tenantId = message.tenantId;
124
+ if (message.attachments !== undefined) {
125
+ payload.attachments = message.attachments.map((a) => ({
126
+ name: a.name,
127
+ contentType: a.contentType,
128
+ dataBase64: bytesToBase64(a.data),
129
+ }));
130
+ }
131
+ return payload;
132
+ }
133
+ function bytesToBase64(bytes) {
134
+ return base64Encode(bytes);
135
+ }
136
+ function defaultRequestIdAllocator() {
137
+ let counter = 0;
138
+ return () => {
139
+ counter += 1;
140
+ const rand = Math.random().toString(36).slice(2, 10);
141
+ return `om-${String(counter)}-${rand}`;
142
+ };
143
+ }
@@ -0,0 +1,27 @@
1
+ import type { RepoId, RepoStore } from "@intx/hub-sessions/substrate";
2
+ import type { ChildSubstrateWriteBridge } from "./substrate-write-bridge.js";
3
+ export interface CreateProxyWorkflowRunRepoStoreOpts {
4
+ /**
5
+ * Bare substrate handle the child opens against the shared on-disk
6
+ * data dir. Used for the read-only methods that consult the
7
+ * substrate's local state -- `getRepoDir` (path computation, no
8
+ * I/O), `resolveRef`, `listRefs`, `resolveHead`, `createPack`. The
9
+ * bare store is never used as a writer here; its
10
+ * `writeTreePreservingPrefix` / `writeTree` / `receivePack` are not
11
+ * reachable through this proxy.
12
+ */
13
+ bareStore: RepoStore;
14
+ /**
15
+ * Substrate-write bridge that forwards proxied writes over the
16
+ * control IPC into the supervisor.
17
+ */
18
+ bridge: ChildSubstrateWriteBridge;
19
+ /**
20
+ * Workflow-run repo id this proxy services. Used as a guard: writes
21
+ * targeting any other repo id surface a structured failure so a
22
+ * stray call against a non-workflow-run repo does not silently land
23
+ * in the wrong substrate.
24
+ */
25
+ workflowRunRepoId: RepoId;
26
+ }
27
+ export declare function createProxyWorkflowRunRepoStore(opts: CreateProxyWorkflowRunRepoStoreOpts): RepoStore;
@@ -0,0 +1,200 @@
1
+ // Child-side proxy `RepoStore` for the workflow-run repo.
2
+ //
3
+ // Wraps a bare read-only substrate handle (the child constructs one
4
+ // against the shared on-disk data dir for `getRepoDir` + other read
5
+ // paths) and intercepts `writeTreePreservingPrefix` so the write is
6
+ // proxied over the control IPC into the supervisor's substrate. The
7
+ // supervisor is the sole writer of the workflow-run repo's ref; the
8
+ // child has no write authority and would race the supervisor's
9
+ // claim-check writes if it opened its own.
10
+ //
11
+ // Subscription fan-out: `RepoStore.subscribe` is a local-process
12
+ // in-memory subscriber pattern; the bare-store's `subscribe` only
13
+ // fires when a write lands on THAT particular substrate instance. The
14
+ // supervisor's writes against its own substrate do not reach the
15
+ // child's bare-store subscribers. The proxy therefore maintains its
16
+ // own per-ref subscriber list and synthesizes a `ref.updated` event to
17
+ // every subscriber after each successful proxied write -- the on-disk
18
+ // repo already carries the new commit (the supervisor's substrate
19
+ // commits before responding to the IPC), so subscribers that follow
20
+ // up with a tree read (via `subscribeKind`'s `getRepoDir` +
21
+ // `readBlobAtCommit` path) see the prospective tree's bytes.
22
+ //
23
+ // Methods that mutate state via paths other than
24
+ // `writeTreePreservingPrefix` (`initRepo`, `writeTree`, `receivePack`)
25
+ // throw on call. The workflow-host runtime body and adapters do not
26
+ // invoke these against the workflow-run repo proxy today; a future
27
+ // caller that tries to surfaces a structured failure rather than a
28
+ // silent disk write that would corrupt the single-writer invariant.
29
+ export function createProxyWorkflowRunRepoStore(opts) {
30
+ const { bareStore, bridge, workflowRunRepoId } = opts;
31
+ // Per-ref subscriber lists. The proxy fans `ref.updated` events to
32
+ // every subscriber on the ref after a successful proxied write so
33
+ // a `subscribeKind` loop watching this ref (the signal channel's
34
+ // `awaitNext` is the canonical consumer) wakes up.
35
+ const subscribers = new Map();
36
+ // Per-ref last-seen sha so the synthesized `ref.updated` carries the
37
+ // matching `oldSha`. Subscriber semantics in `subscribeKind` use the
38
+ // oldSha to enumerate commits added on top of the prior tip.
39
+ const lastSha = new Map();
40
+ function refKey(repoId, ref) {
41
+ return `${repoId.kind}/${repoId.id}/${ref}`;
42
+ }
43
+ function notifyRefUpdate(repoId, ref, oldSha, newSha) {
44
+ const key = refKey(repoId, ref);
45
+ const set = subscribers.get(key);
46
+ if (set === undefined)
47
+ return;
48
+ const event = {
49
+ type: "ref.updated",
50
+ ref,
51
+ oldSha,
52
+ newSha,
53
+ };
54
+ // The substrate's subscribe contract emits one entry per commit
55
+ // with the seq value derived at commit time. The proxy does not
56
+ // know the canonical seq the substrate assigned (the substrate
57
+ // computes it from the ref's history); the field is informational
58
+ // for the subscribe iterator's consumers. `subscribeKind` does
59
+ // not consult the value -- it walks the commit tree from `oldSha`
60
+ // to `newSha` itself -- so any monotonic value preserves the
61
+ // downstream contract. Use a per-ref monotonic counter.
62
+ const seq = (lastRefSeq.get(key) ?? 0) + 1;
63
+ lastRefSeq.set(key, seq);
64
+ for (const sub of set) {
65
+ if (sub.closed)
66
+ continue;
67
+ const entry = { seq, event };
68
+ if (sub.waiter !== null) {
69
+ const w = sub.waiter;
70
+ sub.waiter = null;
71
+ w({ value: entry, done: false });
72
+ continue;
73
+ }
74
+ sub.buffer.push(entry);
75
+ }
76
+ }
77
+ const lastRefSeq = new Map();
78
+ return {
79
+ initRepo: (_repoId, _initOpts) => {
80
+ throw new Error("workflow-child proxy substrate: initRepo is not supported (writes are proxied to the supervisor)");
81
+ },
82
+ writeTree: (_principal, _repoId, _ref, _content) => {
83
+ throw new Error("workflow-child proxy substrate: writeTree is not supported (writes are proxied to the supervisor)");
84
+ },
85
+ receivePack: (_principal, _repoId, _ref, _pack, _commitSha, _expectedOldSha) => {
86
+ throw new Error("workflow-child proxy substrate: receivePack is not supported (writes are proxied to the supervisor)");
87
+ },
88
+ writeTreeDelta: (_principal, _repoId, _ref, _args) => {
89
+ throw new Error("workflow-child proxy substrate: writeTreeDelta is not supported (claim-check writes run supervisor-side)");
90
+ },
91
+ async writeTreePreservingPrefix(_principal, repoId, ref, args) {
92
+ if (repoId.kind !== workflowRunRepoId.kind ||
93
+ repoId.id !== workflowRunRepoId.id) {
94
+ throw new Error(`workflow-child proxy substrate: writeTreePreservingPrefix targeting ${repoId.kind}/${repoId.id} is not supported (proxy services ${workflowRunRepoId.kind}/${workflowRunRepoId.id})`);
95
+ }
96
+ const key = refKey(repoId, ref);
97
+ const priorSha = lastSha.has(key)
98
+ ? (lastSha.get(key) ?? null)
99
+ : await bareStore.resolveRef(_principal, repoId, ref);
100
+ const result = await bridge.submit({
101
+ repoId: { kind: repoId.kind, id: repoId.id },
102
+ ref,
103
+ preservePrefix: args.preservePrefix,
104
+ message: args.message,
105
+ merge: args.merge,
106
+ });
107
+ lastSha.set(key, result.commitSha);
108
+ notifyRefUpdate(repoId, ref, priorSha, result.commitSha);
109
+ // The terminal signal is consumed supervisor-side (where the real
110
+ // substrate write happens); the child-proxied result carries only
111
+ // the commit, so report no terminal runs to the runtime body.
112
+ return { commitSha: result.commitSha, newlyTerminalRuns: [] };
113
+ },
114
+ createPack: bareStore.createPack.bind(bareStore),
115
+ commitPackedTip: bareStore.commitPackedTip.bind(bareStore),
116
+ resolveRef: bareStore.resolveRef.bind(bareStore),
117
+ listRefs: bareStore.listRefs.bind(bareStore),
118
+ resolveHead: bareStore.resolveHead.bind(bareStore),
119
+ getRepoDir: bareStore.getRepoDir.bind(bareStore),
120
+ subscribe(_principal, repoId, ref, subOpts) {
121
+ // Synthesizing the subscribe surface in the proxy: the bare
122
+ // store's `subscribe` would only fire from its own writes, but
123
+ // the writes for this ref happen in the supervisor's address
124
+ // space. Subscribers attached here receive events whenever the
125
+ // proxy's `writeTreePreservingPrefix` returns successfully.
126
+ //
127
+ // `from: { seq }` replay against historical commits is not
128
+ // emitted here today: the runtime body's signal-channel and
129
+ // similar consumers attach with `from: "head"` so only events
130
+ // committed after subscription fire. A `from: { seq }` caller
131
+ // (a resume path that wants to replay) would currently miss
132
+ // historical commits -- the bare store's `subscribe` is the
133
+ // path that supports replay today, and resume code can call it
134
+ // directly if needed.
135
+ const key = refKey(repoId, ref);
136
+ let set = subscribers.get(key);
137
+ if (set === undefined) {
138
+ set = new Set();
139
+ subscribers.set(key, set);
140
+ }
141
+ const sub = {
142
+ buffer: [],
143
+ waiter: null,
144
+ closed: false,
145
+ signal: subOpts.signal,
146
+ };
147
+ set.add(sub);
148
+ const cleanup = () => {
149
+ sub.closed = true;
150
+ const current = subscribers.get(key);
151
+ if (current !== undefined) {
152
+ current.delete(sub);
153
+ if (current.size === 0)
154
+ subscribers.delete(key);
155
+ }
156
+ if (sub.waiter !== null) {
157
+ const w = sub.waiter;
158
+ sub.waiter = null;
159
+ w({ value: undefined, done: true });
160
+ }
161
+ };
162
+ const onAbort = () => {
163
+ cleanup();
164
+ };
165
+ if (subOpts.signal.aborted) {
166
+ cleanup();
167
+ }
168
+ else {
169
+ subOpts.signal.addEventListener("abort", onAbort, { once: true });
170
+ }
171
+ return {
172
+ [Symbol.asyncIterator]() {
173
+ return this;
174
+ },
175
+ next() {
176
+ if (sub.closed) {
177
+ return Promise.resolve({ value: undefined, done: true });
178
+ }
179
+ if (sub.buffer.length > 0) {
180
+ const next = sub.buffer.shift();
181
+ if (next === undefined) {
182
+ return Promise.resolve({ value: undefined, done: true });
183
+ }
184
+ return Promise.resolve({ value: next, done: false });
185
+ }
186
+ return new Promise((resolve) => {
187
+ sub.waiter = resolve;
188
+ });
189
+ },
190
+ return() {
191
+ cleanup();
192
+ return Promise.resolve({
193
+ value: undefined,
194
+ done: true,
195
+ });
196
+ },
197
+ };
198
+ },
199
+ };
200
+ }