@loomwork/ui-hydration 0.0.0-stage → 0.1.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 (179) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +398 -2
  4. package/dist/cache.d.ts +71 -0
  5. package/dist/cache.d.ts.map +1 -0
  6. package/dist/cache.js +157 -0
  7. package/dist/cache.js.map +1 -0
  8. package/dist/chatSource.d.ts +105 -0
  9. package/dist/chatSource.d.ts.map +1 -0
  10. package/dist/chatSource.js +259 -0
  11. package/dist/chatSource.js.map +1 -0
  12. package/dist/client.d.ts +17 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +36 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/contentRefs.d.ts +78 -0
  17. package/dist/contentRefs.d.ts.map +1 -0
  18. package/dist/contentRefs.js +129 -0
  19. package/dist/contentRefs.js.map +1 -0
  20. package/dist/controller.d.ts +228 -0
  21. package/dist/controller.d.ts.map +1 -0
  22. package/dist/controller.js +454 -0
  23. package/dist/controller.js.map +1 -0
  24. package/dist/dataProvider.d.ts +53 -0
  25. package/dist/dataProvider.d.ts.map +1 -0
  26. package/dist/dataProvider.js +77 -0
  27. package/dist/dataProvider.js.map +1 -0
  28. package/dist/esm/cache.js +151 -0
  29. package/dist/esm/cache.js.map +1 -0
  30. package/dist/esm/chatSource.js +254 -0
  31. package/dist/esm/chatSource.js.map +1 -0
  32. package/dist/esm/client.js +15 -0
  33. package/dist/esm/client.js.map +1 -0
  34. package/dist/esm/contentRefs.js +119 -0
  35. package/dist/esm/contentRefs.js.map +1 -0
  36. package/dist/esm/controller.js +449 -0
  37. package/dist/esm/controller.js.map +1 -0
  38. package/dist/esm/dataProvider.js +72 -0
  39. package/dist/esm/dataProvider.js.map +1 -0
  40. package/dist/esm/index.js +15 -0
  41. package/dist/esm/index.js.map +1 -0
  42. package/dist/esm/invoker.js +2 -0
  43. package/dist/esm/invoker.js.map +1 -0
  44. package/dist/esm/invokers.js +310 -0
  45. package/dist/esm/invokers.js.map +1 -0
  46. package/dist/esm/loomEffectProvider.js +96 -0
  47. package/dist/esm/loomEffectProvider.js.map +1 -0
  48. package/dist/esm/params.js +207 -0
  49. package/dist/esm/params.js.map +1 -0
  50. package/dist/esm/path.js +24 -0
  51. package/dist/esm/path.js.map +1 -0
  52. package/dist/esm/plan.js +190 -0
  53. package/dist/esm/plan.js.map +1 -0
  54. package/dist/esm/projection.js +230 -0
  55. package/dist/esm/projection.js.map +1 -0
  56. package/dist/esm/provider.js +416 -0
  57. package/dist/esm/provider.js.map +1 -0
  58. package/dist/esm/repair.js +53 -0
  59. package/dist/esm/repair.js.map +1 -0
  60. package/dist/esm/reshape.js +156 -0
  61. package/dist/esm/reshape.js.map +1 -0
  62. package/dist/esm/restricted.js +54 -0
  63. package/dist/esm/restricted.js.map +1 -0
  64. package/dist/esm/scheduler.js +247 -0
  65. package/dist/esm/scheduler.js.map +1 -0
  66. package/dist/esm/select.js +18 -0
  67. package/dist/esm/select.js.map +1 -0
  68. package/dist/esm/shape.js +34 -0
  69. package/dist/esm/shape.js.map +1 -0
  70. package/dist/esm/split/client.js +309 -0
  71. package/dist/esm/split/client.js.map +1 -0
  72. package/dist/esm/split/index.js +3 -0
  73. package/dist/esm/split/index.js.map +1 -0
  74. package/dist/esm/split/server.js +488 -0
  75. package/dist/esm/split/server.js.map +1 -0
  76. package/dist/esm/split/shared.js +13 -0
  77. package/dist/esm/split/shared.js.map +1 -0
  78. package/dist/index.d.ts +29 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +63 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/invoker.d.ts +10 -0
  83. package/dist/invoker.d.ts.map +1 -0
  84. package/dist/invoker.js +3 -0
  85. package/dist/invoker.js.map +1 -0
  86. package/dist/invokers.d.ts +36 -0
  87. package/dist/invokers.d.ts.map +1 -0
  88. package/dist/invokers.js +315 -0
  89. package/dist/invokers.js.map +1 -0
  90. package/dist/loomEffectProvider.d.ts +67 -0
  91. package/dist/loomEffectProvider.d.ts.map +1 -0
  92. package/dist/loomEffectProvider.js +101 -0
  93. package/dist/loomEffectProvider.js.map +1 -0
  94. package/dist/params.d.ts +103 -0
  95. package/dist/params.d.ts.map +1 -0
  96. package/dist/params.js +221 -0
  97. package/dist/params.js.map +1 -0
  98. package/dist/path.d.ts +12 -0
  99. package/dist/path.d.ts.map +1 -0
  100. package/dist/path.js +28 -0
  101. package/dist/path.js.map +1 -0
  102. package/dist/plan.d.ts +56 -0
  103. package/dist/plan.d.ts.map +1 -0
  104. package/dist/plan.js +199 -0
  105. package/dist/plan.js.map +1 -0
  106. package/dist/projection.d.ts +122 -0
  107. package/dist/projection.d.ts.map +1 -0
  108. package/dist/projection.js +238 -0
  109. package/dist/projection.js.map +1 -0
  110. package/dist/provider.d.ts +152 -0
  111. package/dist/provider.d.ts.map +1 -0
  112. package/dist/provider.js +421 -0
  113. package/dist/provider.js.map +1 -0
  114. package/dist/repair.d.ts +18 -0
  115. package/dist/repair.d.ts.map +1 -0
  116. package/dist/repair.js +57 -0
  117. package/dist/repair.js.map +1 -0
  118. package/dist/reshape.d.ts +32 -0
  119. package/dist/reshape.d.ts.map +1 -0
  120. package/dist/reshape.js +164 -0
  121. package/dist/reshape.js.map +1 -0
  122. package/dist/restricted.d.ts +21 -0
  123. package/dist/restricted.d.ts.map +1 -0
  124. package/dist/restricted.js +61 -0
  125. package/dist/restricted.js.map +1 -0
  126. package/dist/scheduler.d.ts +82 -0
  127. package/dist/scheduler.d.ts.map +1 -0
  128. package/dist/scheduler.js +254 -0
  129. package/dist/scheduler.js.map +1 -0
  130. package/dist/select.d.ts +34 -0
  131. package/dist/select.d.ts.map +1 -0
  132. package/dist/select.js +21 -0
  133. package/dist/select.js.map +1 -0
  134. package/dist/shape.d.ts +3 -0
  135. package/dist/shape.d.ts.map +1 -0
  136. package/dist/shape.js +37 -0
  137. package/dist/shape.js.map +1 -0
  138. package/dist/split/client.d.ts +132 -0
  139. package/dist/split/client.d.ts.map +1 -0
  140. package/dist/split/client.js +313 -0
  141. package/dist/split/client.js.map +1 -0
  142. package/dist/split/index.d.ts +5 -0
  143. package/dist/split/index.d.ts.map +1 -0
  144. package/dist/split/index.js +14 -0
  145. package/dist/split/index.js.map +1 -0
  146. package/dist/split/server.d.ts +160 -0
  147. package/dist/split/server.d.ts.map +1 -0
  148. package/dist/split/server.js +496 -0
  149. package/dist/split/server.js.map +1 -0
  150. package/dist/split/shared.d.ts +6 -0
  151. package/dist/split/shared.d.ts.map +1 -0
  152. package/dist/split/shared.js +19 -0
  153. package/dist/split/shared.js.map +1 -0
  154. package/package.json +77 -3
  155. package/src/cache.ts +211 -0
  156. package/src/chatSource.ts +374 -0
  157. package/src/client.ts +46 -0
  158. package/src/contentRefs.ts +195 -0
  159. package/src/controller.ts +708 -0
  160. package/src/dataProvider.ts +141 -0
  161. package/src/index.ts +86 -0
  162. package/src/invoker.ts +14 -0
  163. package/src/invokers.ts +348 -0
  164. package/src/loomEffectProvider.ts +136 -0
  165. package/src/params.ts +314 -0
  166. package/src/path.ts +26 -0
  167. package/src/plan.ts +197 -0
  168. package/src/projection.ts +297 -0
  169. package/src/provider.ts +657 -0
  170. package/src/repair.ts +70 -0
  171. package/src/reshape.ts +193 -0
  172. package/src/restricted.ts +69 -0
  173. package/src/scheduler.ts +284 -0
  174. package/src/select.ts +59 -0
  175. package/src/shape.ts +39 -0
  176. package/src/split/client.ts +449 -0
  177. package/src/split/index.ts +18 -0
  178. package/src/split/server.ts +746 -0
  179. package/src/split/shared.ts +18 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,2CASsB;AARpB,qHAAA,uBAAuB,OAAA;AACvB,0HAAA,4BAA4B,OAAA;AAC5B,iHAAA,mBAAmB,OAAA;AACnB,4HAAA,8BAA8B,OAAA;AAC9B,iHAAA,mBAAmB,OAAA;AACnB,4GAAA,cAAc,OAAA;AACd,iHAAA,mBAAmB,OAAA;AACnB,8GAAA,gBAAgB,OAAA;AAUlB,uCAAsE;AAA7D,wGAAA,YAAY,OAAA;AAAE,uGAAA,WAAW,OAAA;AAAE,yGAAA,aAAa,OAAA;AACjD,qCAA+E;AAAtE,0GAAA,eAAe,OAAA;AAAE,yGAAA,cAAc,OAAA;AAAE,4GAAA,iBAAiB,OAAA;AAE3D,mCAA2D;AAAlD,4GAAA,kBAAkB,OAAA;AAAE,qGAAA,WAAW,OAAA;AACxC,6CAAsD;AAA7C,oHAAA,qBAAqB,OAAA;AAO9B,+CAKwB;AAJtB,sHAAA,sBAAsB,OAAA;AACtB,4HAAA,4BAA4B,OAAA;AAC5B,mHAAA,mBAAmB,OAAA;AACnB,2GAAA,WAAW,OAAA;AAGb,2DAM8B;AAL5B,sHAAA,gBAAgB,OAAA;AAChB,mHAAA,aAAa,OAAA;AACb,yHAAA,mBAAmB,OAAA;AACnB,+GAAA,SAAS,OAAA;AACT,wHAAA,kBAAkB,OAAA;AAOpB,uCAA6C;AAApC,2GAAA,eAAe,OAAA;AACxB,+BAAkE;AAAzD,sGAAA,cAAc,OAAA;AAAE,qGAAA,aAAa,OAAA;AAAE,iGAAA,SAAS,OAAA;AAEjD,iCAAoE;AAA3D,kHAAA,yBAAyB,OAAA;AAAE,uGAAA,cAAc,OAAA;AAGlD,2CAAmD;AAA1C,iHAAA,mBAAmB,OAAA;AAE5B,uCASuB;AARrB,0GAAA,iBAAiB,OAAA;AACjB,qHAAA,4BAA4B,OAAA;AAC5B,iHAAA,wBAAwB,OAAA;AACxB,2GAAA,kBAAkB,OAAA;AAClB,wGAAA,eAAe,OAAA;AACf,sGAAA,aAAa,OAAA;AACb,oGAAA,WAAW,OAAA;AACX,qGAAA,YAAY,OAAA;AAad,mCAA2D;AAAlD,yGAAA,eAAe,OAAA;AAAE,wGAAA,cAAc,OAAA;AAExC,2CAAgE;AAAvD,iHAAA,mBAAmB,OAAA;AAAE,yGAAA,WAAW,OAAA"}
@@ -0,0 +1,10 @@
1
+ import type { TransientInvokeOptions } from "@loomwork/effect-runtime";
2
+ /**
3
+ * The one thing hydration needs from loom's runtime: a one-shot, non-durable effect call.
4
+ * `EffectRuntime` satisfies this structurally (ADR 0066 `invokeTransient`); tests use a fake.
5
+ * Keeping it an interface is what makes the read path provably log-free — there is no `ctx`.
6
+ */
7
+ export interface TransientInvoker {
8
+ invokeTransient<TIn = unknown, TOut = unknown>(toolId: string, input: TIn, opts?: TransientInvokeOptions): Promise<TOut>;
9
+ }
10
+ //# sourceMappingURL=invoker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoker.d.ts","sourceRoot":"","sources":["../src/invoker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAEvE;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,eAAe,CAAC,GAAG,GAAG,OAAO,EAAE,IAAI,GAAG,OAAO,EAC3C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,GAAG,EACV,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=invoker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoker.js","sourceRoot":"","sources":["../src/invoker.ts"],"names":[],"mappings":""}
@@ -0,0 +1,36 @@
1
+ import type { TransientInvoker } from "./invoker";
2
+ /**
3
+ * At most `max` calls in flight through this invoker, across every caller. A queued call whose
4
+ * signal aborts leaves the queue and rejects `"aborted"` without ever reaching `inner`. FIFO: a
5
+ * finished call hands its slot straight to the oldest waiter.
6
+ */
7
+ export declare function limitInvoker(inner: TransientInvoker, max: number): TransientInvoker;
8
+ /**
9
+ * Every call through this invoker is ALSO cancelled, synchronously, when `signal` aborts — the
10
+ * owner's lifetime (a controller's `dispose`), independent of any caller's own signal, which still
11
+ * reaches `inner` too. Once `signal` has aborted, new calls reject `"aborted"` without reaching
12
+ * `inner`. Leak-free on a long-lived `signal`: a call holds its listener only until it settles.
13
+ */
14
+ export declare function signalInvoker(inner: TransientInvoker, signal: AbortSignal): TransientInvoker;
15
+ /**
16
+ * One call per identical MCP step for this invoker's lifetime (a PASS): same tool, same resolved
17
+ * arguments, same schema pin, same call options — the canonical JSON of `[principal, every other
18
+ * option except signal, mcp.invoke's input]`, so key order does not matter. A joiner is
19
+ * served straight from the shared promise — it never calls `inner` itself, so it never passes
20
+ * through whatever per-call authorizer `inner` enforces. That is safe ONLY because every
21
+ * authorization input is part of the key: the principal, and also the stamped execution identity
22
+ * (`as` — an authorizer may grant by workflow), the `label` (which names the transient
23
+ * execution when `as` is absent) and anything else in the options. Every joiner
24
+ * sharing one entry was, by construction, asking with the exact inputs the leader's own (sole)
25
+ * call to `inner` was authorized for; an option that is not plain JSON passes through, uncached. Settled results stay (a step in a later wave is still
26
+ * served) and failures are shared. Other effects (a sandbox reshape) always run, uncached.
27
+ *
28
+ * Cancellation is ref-counted, not leader-owned: `inner` is called once, through an internal
29
+ * `AbortController` this function owns — never a caller's own signal directly — so any ONE
30
+ * caller's abort only ever settles that caller's OWN returned promise (a race against the shared
31
+ * promise). The upstream call itself is cancelled, and the entry forgotten, only once every live
32
+ * caller has aborted AND the call had not already settled; a caller that arrives after a real
33
+ * cancellation finds no entry and starts a fresh call.
34
+ */
35
+ export declare function memoInvoker(inner: TransientInvoker): TransientInvoker;
36
+ //# sourceMappingURL=invokers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invokers.d.ts","sourceRoot":"","sources":["../src/invokers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAwBlD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,GAAG,gBAAgB,CA0DnF;AAWD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,GAAG,gBAAgB,CAwB5F;AAyDD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,CA2IrE"}
@@ -0,0 +1,315 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.limitInvoker = limitInvoker;
4
+ exports.signalInvoker = signalInvoker;
5
+ exports.memoInvoker = memoInvoker;
6
+ const core_1 = require("@loomwork/core");
7
+ const plugin_mcp_1 = require("@loomwork/plugin-mcp");
8
+ // ── Invoker wrappers: a call cap, an owner's lifetime signal, an in-pass step memo ─
9
+ // All are TransientInvokers wrapping a TransientInvoker, so they compose with each other and
10
+ // with loom's EffectRuntime without either knowing the other exists. The in-process controller
11
+ // builds ONE `limitInvoker` (the cap is controller-wide: it bounds every call it makes), ONE
12
+ // `signalInvoker` over it bound to its own lifetime (so `dispose` cancels every upstream call at
13
+ // once), and one `memoInvoker` PER PASS in front of both (a memo hit takes no slot). The split
14
+ // server (`serveHandles`, split.ts) composes a memo over a cap the same way, once per batch.
15
+ // None ever returns a promise it did not get from the inner invoker except to wait for a slot,
16
+ // so they add no settle latency on a free slot.
17
+ /**
18
+ * The rejection for a call abandoned because its signal aborted. Named `AbortError` (the
19
+ * platform's own name for a cancelled operation) so a consumer classifies it by `name` — a
20
+ * planner's decider must propagate a cancellation, never retry it or record it as a failed
21
+ * plan. A plain `Error("aborted")` is indistinguishable from a provider failure. Message kept.
22
+ */
23
+ function abortError() {
24
+ const e = new Error("aborted");
25
+ e.name = "AbortError";
26
+ return e;
27
+ }
28
+ /**
29
+ * At most `max` calls in flight through this invoker, across every caller. A queued call whose
30
+ * signal aborts leaves the queue and rejects `"aborted"` without ever reaching `inner`. FIFO: a
31
+ * finished call hands its slot straight to the oldest waiter.
32
+ */
33
+ function limitInvoker(inner, max) {
34
+ if (!Number.isInteger(max) || max < 1)
35
+ throw new RangeError(`limitInvoker: the cap must be a positive integer, got ${String(max)}`);
36
+ let active = 0;
37
+ // Insertion-ordered: the first entry is the oldest waiter; add/delete are O(1).
38
+ const waiting = new Set();
39
+ const release = () => {
40
+ const next = waiting.values().next();
41
+ if (next.done === true) {
42
+ active -= 1;
43
+ return;
44
+ }
45
+ waiting.delete(next.value);
46
+ next.value(); // the slot passes to the waiter: `active` is unchanged
47
+ };
48
+ const acquire = (signal) => {
49
+ // Checked before capacity: an already-aborted caller must never take a slot (or reach
50
+ // `inner`) just because one happened to be free — that slot belongs to the next live caller.
51
+ if (signal?.aborted === true)
52
+ return Promise.reject(abortError());
53
+ if (active < max) {
54
+ active += 1;
55
+ return undefined;
56
+ }
57
+ return new Promise((resolve, reject) => {
58
+ const onAbort = () => {
59
+ waiting.delete(grant);
60
+ reject(abortError());
61
+ };
62
+ const grant = () => {
63
+ signal?.removeEventListener("abort", onAbort);
64
+ resolve();
65
+ };
66
+ waiting.add(grant);
67
+ signal?.addEventListener("abort", onAbort, { once: true });
68
+ });
69
+ };
70
+ const run = (call) => {
71
+ let p;
72
+ try {
73
+ p = call();
74
+ }
75
+ catch (e) {
76
+ release();
77
+ return Promise.reject(e);
78
+ }
79
+ p.then(release, release);
80
+ return p;
81
+ };
82
+ return {
83
+ invokeTransient(toolId, input, opts) {
84
+ const call = () => inner.invokeTransient(toolId, input, opts);
85
+ const slot = acquire(opts?.signal);
86
+ return slot === undefined ? run(call) : slot.then(() => run(call));
87
+ },
88
+ };
89
+ }
90
+ /** `call()`, with a synchronous throw turned into the rejected promise the contract promises. */
91
+ function callSafely(call) {
92
+ try {
93
+ return call();
94
+ }
95
+ catch (e) {
96
+ return Promise.reject(e);
97
+ }
98
+ }
99
+ /**
100
+ * Every call through this invoker is ALSO cancelled, synchronously, when `signal` aborts — the
101
+ * owner's lifetime (a controller's `dispose`), independent of any caller's own signal, which still
102
+ * reaches `inner` too. Once `signal` has aborted, new calls reject `"aborted"` without reaching
103
+ * `inner`. Leak-free on a long-lived `signal`: a call holds its listener only until it settles.
104
+ */
105
+ function signalInvoker(inner, signal) {
106
+ return {
107
+ invokeTransient(toolId, input, opts) {
108
+ const own = opts?.signal;
109
+ if (signal.aborted || own?.aborted === true)
110
+ return Promise.reject(abortError());
111
+ const ac = new AbortController();
112
+ const abort = () => ac.abort();
113
+ signal.addEventListener("abort", abort, { once: true });
114
+ own?.addEventListener("abort", abort, { once: true });
115
+ const release = () => {
116
+ signal.removeEventListener("abort", abort);
117
+ own?.removeEventListener("abort", abort);
118
+ };
119
+ const p = callSafely(() => inner.invokeTransient(toolId, input, { ...opts, signal: ac.signal }));
120
+ p.then(release, release);
121
+ return p;
122
+ },
123
+ };
124
+ }
125
+ /**
126
+ * The shape `canonicalJson` actually understands — null/string/finite-number/boolean, and arrays
127
+ * or plain objects (`Object.prototype` or `null` as the prototype) built from those, recursively.
128
+ * `canonicalJson` does not THROW on a `Date`, `Map`, `Set` or a class instance — it silently reads
129
+ * `Object.keys()` on it, which for most of those is `[]`, so two calls with DIFFERENT such
130
+ * arguments would canonicalize to the same `"{}"` and collide on one memo key. Reject those before
131
+ * a key is ever computed, not after: `memoInvoker` must never be the reason a value collapses.
132
+ *
133
+ * `seen` makes this cycle-safe: a hostile or accidental circular reference is walked at most once
134
+ * per object (the second visit reports "not plain" rather than recursing forever), so a cyclic
135
+ * argument bypasses the memo instead of throwing `RangeError: Maximum call stack size exceeded`
136
+ * out of `invokeTransient`. Sharing one `seen` set across a whole call's plainness check is
137
+ * deliberately conservative: a repeated (non-cyclic) reference is also flagged "not plain" and
138
+ * bypasses sharing rather than being memoized — safe, since bypass just means no caching, never a
139
+ * wrong answer.
140
+ */
141
+ function isPlainJsonValue(value, seen = new WeakSet()) {
142
+ if (value === null || value === undefined)
143
+ return true;
144
+ const t = typeof value;
145
+ if (t === "string" || t === "boolean")
146
+ return true;
147
+ if (t === "number")
148
+ return Number.isFinite(value);
149
+ if (t !== "object")
150
+ return false; // function, bigint, symbol
151
+ const obj = value;
152
+ if (seen.has(obj))
153
+ return false;
154
+ seen.add(obj);
155
+ if (Array.isArray(value))
156
+ return value.every((v) => isPlainJsonValue(v, seen));
157
+ const proto = Object.getPrototypeOf(obj);
158
+ if (proto !== Object.prototype && proto !== null)
159
+ return false;
160
+ return Object.values(value).every((v) => isPlainJsonValue(v, seen));
161
+ }
162
+ /**
163
+ * One call per identical MCP step for this invoker's lifetime (a PASS): same tool, same resolved
164
+ * arguments, same schema pin, same call options — the canonical JSON of `[principal, every other
165
+ * option except signal, mcp.invoke's input]`, so key order does not matter. A joiner is
166
+ * served straight from the shared promise — it never calls `inner` itself, so it never passes
167
+ * through whatever per-call authorizer `inner` enforces. That is safe ONLY because every
168
+ * authorization input is part of the key: the principal, and also the stamped execution identity
169
+ * (`as` — an authorizer may grant by workflow), the `label` (which names the transient
170
+ * execution when `as` is absent) and anything else in the options. Every joiner
171
+ * sharing one entry was, by construction, asking with the exact inputs the leader's own (sole)
172
+ * call to `inner` was authorized for; an option that is not plain JSON passes through, uncached. Settled results stay (a step in a later wave is still
173
+ * served) and failures are shared. Other effects (a sandbox reshape) always run, uncached.
174
+ *
175
+ * Cancellation is ref-counted, not leader-owned: `inner` is called once, through an internal
176
+ * `AbortController` this function owns — never a caller's own signal directly — so any ONE
177
+ * caller's abort only ever settles that caller's OWN returned promise (a race against the shared
178
+ * promise). The upstream call itself is cancelled, and the entry forgotten, only once every live
179
+ * caller has aborted AND the call had not already settled; a caller that arrives after a real
180
+ * cancellation finds no entry and starts a fresh call.
181
+ */
182
+ function memoInvoker(inner) {
183
+ const memo = new Map();
184
+ const onCallerAbort = (key, hit) => {
185
+ hit.live -= 1;
186
+ if (hit.live > 0)
187
+ return;
188
+ // Deferred one microtask: `hit.settled` is set by a handler attached at entry-creation time,
189
+ // which — because it was attached first — runs before this check for any settlement the memo
190
+ // can observe, even when the abort lands in the very same synchronous turn as `hit.promise`
191
+ // settling. Checking `hit.settled` synchronously right here would miss exactly that race.
192
+ // "The memo can observe" is `hit.promise` — `inner`'s promise — not the raw upstream: behind
193
+ // a queued `limitInvoker` (or any wrapper that settles a few hops after its own inner call),
194
+ // an all-abort landing after the RAW upstream settled but before `hit.promise` does still
195
+ // cancels and evicts, so a later caller re-fetches. A wasted call, never a wrong answer.
196
+ //
197
+ // Both conditions below are RE-READ at that later point, not captured now, because either can
198
+ // change in the gap between "live just dropped to 0" and "the microtask actually runs": a
199
+ // fresh caller with no signal of its own can join in the very same synchronous turn (bumping
200
+ // `live` back above 0 before this ever executes), and the upstream can settle in that same
201
+ // window (flipping `settled` true). A snapshot taken at decrement time would race both ways —
202
+ // this is why the check happens here, once, right before the one place that acts on it.
203
+ //
204
+ // Invariant: the controller is aborted, and the entry deleted, AT MOST ONCE per entry — and
205
+ // only when, at the moment this specific check runs, `settled` is still false AND `live` is
206
+ // still <= 0 (no one re-joined). Either condition failing means someone still needs this call
207
+ // to keep going (or already has its answer), so it is left alone.
208
+ queueMicrotask(() => {
209
+ if (hit.settled || hit.live > 0)
210
+ return;
211
+ // Forget the entry BEFORE aborting: `abort()` runs `inner`'s listeners synchronously, and a
212
+ // caller re-entering with the same key from one of them must start a fresh call, not join
213
+ // this dying one.
214
+ if (memo.get(key) === hit)
215
+ memo.delete(key);
216
+ hit.controller.abort();
217
+ });
218
+ };
219
+ const join = (key, hit, signal) => {
220
+ if (signal?.aborted === true) {
221
+ onCallerAbort(key, hit);
222
+ return Promise.reject(abortError());
223
+ }
224
+ return new Promise((resolve, reject) => {
225
+ let settled = false;
226
+ const onAbort = () => {
227
+ if (settled)
228
+ return;
229
+ settled = true;
230
+ onCallerAbort(key, hit);
231
+ reject(abortError());
232
+ };
233
+ signal?.addEventListener("abort", onAbort, { once: true });
234
+ hit.promise.then((v) => {
235
+ if (settled)
236
+ return;
237
+ settled = true;
238
+ signal?.removeEventListener("abort", onAbort);
239
+ resolve(v);
240
+ }, (e) => {
241
+ if (settled)
242
+ return;
243
+ settled = true;
244
+ signal?.removeEventListener("abort", onAbort);
245
+ reject(e);
246
+ });
247
+ });
248
+ };
249
+ return {
250
+ invokeTransient(toolId, input, opts) {
251
+ // Every pass-through is guarded like the leader's call: a synchronous throw from `inner`
252
+ // must come back as a rejected promise, never escape `invokeTransient`.
253
+ const passThrough = () => callSafely(() => inner.invokeTransient(toolId, input, opts));
254
+ if (toolId !== plugin_mcp_1.MCP_INVOKE_TOOL_ID)
255
+ return passThrough();
256
+ // Everything in `opts` except the one field that cannot change the answer (`signal`) is
257
+ // part of the key: the principal, the stamped identity (`as`, which an authorizer may key
258
+ // grants on), the `label` (without `as`, the transient executionId is `transient:<label>`,
259
+ // and an authorizer may grant per execution), the budget snapshot, and any option added
260
+ // later. Keying on the remainder rather than a list of known fields is what keeps a joiner
261
+ // from being served a result authorized for a different input this function has never
262
+ // heard of.
263
+ const { signal: _signal, principal = null, ...rest } = opts ?? {};
264
+ let key;
265
+ try {
266
+ const seen = new WeakSet();
267
+ if (!isPlainJsonValue(principal, seen) ||
268
+ !isPlainJsonValue(rest, seen) ||
269
+ !isPlainJsonValue(input, seen))
270
+ return passThrough();
271
+ key = (0, core_1.canonicalJson)([principal, rest, input]);
272
+ }
273
+ catch {
274
+ return passThrough();
275
+ }
276
+ const hit = memo.get(key);
277
+ if (hit !== undefined) {
278
+ hit.live += 1;
279
+ return join(key, hit, opts?.signal);
280
+ }
281
+ // Mirrors limitInvoker's fast path: an already-aborted caller must never become the leader
282
+ // — that would call `inner`, create an entry nobody asked to keep, and (since nothing here
283
+ // would yet be subscribed to the resulting promise) risk an unhandled rejection.
284
+ if (opts?.signal?.aborted === true)
285
+ return Promise.reject(abortError());
286
+ const controller = new AbortController();
287
+ let promise;
288
+ try {
289
+ // `inner.invokeTransient` is typed to return a Promise, but a misbehaving implementation
290
+ // can still throw synchronously; the interface contract (and every caller of THIS
291
+ // function) requires a rejected promise instead, never a thrown exception. No entry is
292
+ // created for a call that never actually started.
293
+ promise = inner.invokeTransient(toolId, input, {
294
+ ...opts,
295
+ signal: controller.signal,
296
+ });
297
+ }
298
+ catch (e) {
299
+ return Promise.reject(e);
300
+ }
301
+ const entry = { promise, controller, live: 1, settled: false };
302
+ const onSettle = () => {
303
+ entry.settled = true;
304
+ };
305
+ // Attached at entry-creation time — before any joiner's own `.then` — so `entry.settled`
306
+ // is set before `onCallerAbort`'s deferred check can run (see there). Also the
307
+ // backstop: attaching a rejection handler here means NO path can ever leave `promise`
308
+ // unhandled, even if a future caller subscribes late or never at all.
309
+ promise.then(onSettle, onSettle);
310
+ memo.set(key, entry);
311
+ return join(key, entry, opts?.signal);
312
+ },
313
+ };
314
+ }
315
+ //# sourceMappingURL=invokers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invokers.js","sourceRoot":"","sources":["../src/invokers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import { toolInputJsonSchema } from "@loomwork/agent";
2
+ import type { EffectRuntime } from "@loomwork/effect-runtime";
3
+ import type { DataProvider } from "./dataProvider";
4
+ /** The kind id of a step whose target is a registered loom effect. */
5
+ export declare const LOOM_EFFECT_KIND = "loom-effect";
6
+ /**
7
+ * The failure channel for a target the host did not declare. Served to a client as the fixed
8
+ * `tool-targetUnregistered` code — never the target id or any text of the effect's own.
9
+ */
10
+ export declare const TARGET_UNREGISTERED = "targetUnregistered";
11
+ /**
12
+ * The failure channel for an effect that itself raised `schemaDrift` (e.g. a target wrapping
13
+ * `mcp.invoke`). Only the provider's OWN pin check may emit `schemaDrift`: a target's drift
14
+ * would otherwise be read as this kind's pin drift, and a repair would re-pin to the inner
15
+ * hash and flip forever. Served as the `tool-targetFailed` code (`tool-failed` to a client).
16
+ */
17
+ export declare const TARGET_FAILED = "targetFailed";
18
+ /** The schema of an effect's input, as `tool$({ inputSchema })` takes it. */
19
+ export type EffectInputSchema = Parameters<typeof toolInputJsonSchema>[0];
20
+ /**
21
+ * The pin of an effect's input contract: `jsonSchemaHash` (the one canonical hash MCP pins use)
22
+ * of the schema's JSON-Schema form. Use it BOTH when the host builds a planner `CapabilityRef`
23
+ * (`toolSchemaHash`) and when it lists the effect for {@link loomEffectProvider} (via
24
+ * `{ inputSchema }`) so the two come from one source: editing the effect's zod schema moves
25
+ * the pin, and every stored step drifts.
26
+ */
27
+ export declare function effectPin(schema: EffectInputSchema): string;
28
+ /** How a listed effect's pin is obtained: computed from its schema, or asserted by the host. */
29
+ export type EffectEntry = {
30
+ readonly inputSchema: NonNullable<EffectInputSchema>;
31
+ } | {
32
+ readonly pin: string;
33
+ };
34
+ export interface LoomEffectProviderOptions {
35
+ /**
36
+ * The runtime the effects are registered with. Read at CONSTRUCTION only (`has` / `modeOf` /
37
+ * `kindOf`): hydration is a read path that runs per viewer load, unmemoised and retried, so
38
+ * every listed effect must be registered, explicitly `mode: "ambient"` (ADR 0073 — loom
39
+ * refuses only an explicit `journaled` one on the transient path, so an UNDECLARED mode is
40
+ * not a read declaration and is refused here), and not a `human` / `llm` kind.
41
+ */
42
+ readonly runtime: Pick<EffectRuntime, "has" | "modeOf" | "kindOf">;
43
+ /**
44
+ * The effects a stored step may name. `{ inputSchema }` computes the pin with
45
+ * {@link effectPin} (a schema change drifts every stored step); `{ pin }` is HOST-ASSERTED —
46
+ * nothing checks it against the effect, so the host owns changing it when callers must
47
+ * re-author. A step naming an effect not listed is refused (`tool-targetUnregistered`) and
48
+ * nothing runs. Pins must be non-empty.
49
+ */
50
+ readonly effects: Readonly<Record<string, EffectEntry>>;
51
+ }
52
+ /**
53
+ * The `loom-effect` kind: a step whose `toolName` is the id of an effect registered with the
54
+ * loom effect runtime (a `tool$`, a read-model query effect, an A2A call…), invoked through the
55
+ * SAME transient invoker and therefore the same authoriser, budget and principal as the MCP
56
+ * kind. Construction refuses an effect that is unregistered, not explicitly `ambient`, or a
57
+ * `human` / `llm` kind.
58
+ *
59
+ * An effect's call is not memoised across a pass (the in-pass memo coalesces `mcp.invoke` only:
60
+ * an arbitrary effect is not known to be a pure read), so two steps naming one effect with equal
61
+ * arguments call it twice. (Memoising `ambient` effects is a possible follow-up.)
62
+ *
63
+ * A persisted experience crosses a version boundary only through the log: an older in-memory
64
+ * `executeProvider` / `seal` that predates kinds would run a kinded step as an MCP call.
65
+ */
66
+ export declare function loomEffectProvider(options: LoomEffectProviderOptions): DataProvider;
67
+ //# sourceMappingURL=loomEffectProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loomEffectProvider.d.ts","sourceRoot":"","sources":["../src/loomEffectProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAG9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,gBAAgB,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,mBAAmB,uBAAuB,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,iBAAiB,CAAC;AAE5C,6EAA6E;AAC7E,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;AAE1E;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAE3D;AAYD,gGAAgG;AAChG,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,iBAAiB,CAAC,CAAA;CAAE,GACxD;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7B,MAAM,WAAW,yBAAyB;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC,CAAC;IACnE;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;CACzD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,YAAY,CAiDnF"}
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TARGET_FAILED = exports.TARGET_UNREGISTERED = exports.LOOM_EFFECT_KIND = void 0;
4
+ exports.effectPin = effectPin;
5
+ exports.loomEffectProvider = loomEffectProvider;
6
+ const agent_1 = require("@loomwork/agent");
7
+ const core_1 = require("@loomwork/core");
8
+ const plugin_mcp_1 = require("@loomwork/plugin-mcp");
9
+ const plugin_sandbox_1 = require("@loomwork/plugin-sandbox");
10
+ /** The kind id of a step whose target is a registered loom effect. */
11
+ exports.LOOM_EFFECT_KIND = "loom-effect";
12
+ /**
13
+ * The failure channel for a target the host did not declare. Served to a client as the fixed
14
+ * `tool-targetUnregistered` code — never the target id or any text of the effect's own.
15
+ */
16
+ exports.TARGET_UNREGISTERED = "targetUnregistered";
17
+ /**
18
+ * The failure channel for an effect that itself raised `schemaDrift` (e.g. a target wrapping
19
+ * `mcp.invoke`). Only the provider's OWN pin check may emit `schemaDrift`: a target's drift
20
+ * would otherwise be read as this kind's pin drift, and a repair would re-pin to the inner
21
+ * hash and flip forever. Served as the `tool-targetFailed` code (`tool-failed` to a client).
22
+ */
23
+ exports.TARGET_FAILED = "targetFailed";
24
+ /**
25
+ * The pin of an effect's input contract: `jsonSchemaHash` (the one canonical hash MCP pins use)
26
+ * of the schema's JSON-Schema form. Use it BOTH when the host builds a planner `CapabilityRef`
27
+ * (`toolSchemaHash`) and when it lists the effect for {@link loomEffectProvider} (via
28
+ * `{ inputSchema }`) so the two come from one source: editing the effect's zod schema moves
29
+ * the pin, and every stored step drifts.
30
+ */
31
+ function effectPin(schema) {
32
+ return (0, core_1.jsonSchemaHash)((0, agent_1.toolInputJsonSchema)(schema));
33
+ }
34
+ /** The effect kinds a data read can never reach: a person's answer, a model call. */
35
+ const REFUSED_KINDS = new Set(["human", "llm"]);
36
+ /**
37
+ * Effects hydration already reaches through a dedicated plane (the MCP kind, the sandbox
38
+ * reshaper). Exposing them as a data TARGET would let a stored step bypass the pin and the
39
+ * reshaping boundary, so a host cannot list them.
40
+ */
41
+ const RESERVED_TARGETS = new Set([plugin_mcp_1.MCP_INVOKE_TOOL_ID, plugin_sandbox_1.SANDBOX_TOOL_ID]);
42
+ /**
43
+ * The `loom-effect` kind: a step whose `toolName` is the id of an effect registered with the
44
+ * loom effect runtime (a `tool$`, a read-model query effect, an A2A call…), invoked through the
45
+ * SAME transient invoker and therefore the same authoriser, budget and principal as the MCP
46
+ * kind. Construction refuses an effect that is unregistered, not explicitly `ambient`, or a
47
+ * `human` / `llm` kind.
48
+ *
49
+ * An effect's call is not memoised across a pass (the in-pass memo coalesces `mcp.invoke` only:
50
+ * an arbitrary effect is not known to be a pure read), so two steps naming one effect with equal
51
+ * arguments call it twice. (Memoising `ambient` effects is a possible follow-up.)
52
+ *
53
+ * A persisted experience crosses a version boundary only through the log: an older in-memory
54
+ * `executeProvider` / `seal` that predates kinds would run a kinded step as an MCP call.
55
+ */
56
+ function loomEffectProvider(options) {
57
+ const { runtime } = options;
58
+ const pins = new Map();
59
+ for (const [id, entry] of Object.entries(options.effects)) {
60
+ if (id === "")
61
+ throw new TypeError("loomEffectProvider: an effect id is empty");
62
+ if (RESERVED_TARGETS.has(id))
63
+ throw new TypeError(`loomEffectProvider: "${id}" is reached through its own plane (the MCP kind / the sandbox) and cannot be a data target`);
64
+ if (!runtime.has(id))
65
+ throw new TypeError(`loomEffectProvider: effect "${id}" is not registered with the runtime`);
66
+ if (runtime.modeOf(id) !== "ambient")
67
+ throw new TypeError(`loomEffectProvider: effect "${id}" is not declared mode "ambient" (ADR 0073); a data read may not run an undeclared or journaled effect`);
68
+ const kind = runtime.kindOf(id);
69
+ if (kind !== undefined && REFUSED_KINDS.has(kind))
70
+ throw new TypeError(`loomEffectProvider: effect "${id}" is a ${kind} effect, not a data read`);
71
+ const pin = entry !== null && typeof entry === "object" && "inputSchema" in entry
72
+ ? effectPin(entry.inputSchema)
73
+ : entry?.pin;
74
+ if (typeof pin !== "string" || pin === "")
75
+ throw new TypeError(`loomEffectProvider: effect "${id}" needs a non-empty pin`);
76
+ pins.set(id, pin);
77
+ }
78
+ return Object.freeze({
79
+ kind: exports.LOOM_EFFECT_KIND,
80
+ async invoke(call, invoker, opts) {
81
+ const current = pins.get(call.target);
82
+ if (current === undefined)
83
+ throw new core_1.TypedFailure(exports.TARGET_UNREGISTERED, { target: call.target });
84
+ if (current !== call.expectedPin)
85
+ throw new core_1.TypedFailure("schemaDrift", {
86
+ toolName: call.target,
87
+ expectedSchemaHash: call.expectedPin,
88
+ observedHash: current,
89
+ });
90
+ try {
91
+ return await invoker.invokeTransient(call.target, call.args, opts);
92
+ }
93
+ catch (e) {
94
+ if (e instanceof core_1.TypedFailure && e.channel === "schemaDrift")
95
+ throw new core_1.TypedFailure(exports.TARGET_FAILED, { target: call.target });
96
+ throw e;
97
+ }
98
+ },
99
+ });
100
+ }
101
+ //# sourceMappingURL=loomEffectProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loomEffectProvider.js","sourceRoot":"","sources":["../src/loomEffectProvider.ts"],"names":[],"mappings":"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"}