@unoverse-platform/studio 0.1.90 → 0.1.92

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 (132) hide show
  1. package/local/plugin.ts +21 -65
  2. package/package.json +13 -3
  3. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  4. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  5. package/vendor/base/src/agent-mcp/index.ts +44 -0
  6. package/vendor/base/src/agent-mcp/invoke.ts +153 -0
  7. package/vendor/base/src/agent-mcp/lean.ts +114 -0
  8. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  9. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  10. package/vendor/base/src/agent-mcp/rowComponents.ts +234 -0
  11. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  12. package/vendor/base/src/agent-mcp/types.ts +138 -0
  13. package/vendor/base/src/auth/auth.ts +307 -0
  14. package/vendor/base/src/auth/authConfig.ts +73 -0
  15. package/vendor/base/src/boot.ts +245 -0
  16. package/vendor/base/src/definitions/definitions.ts +18 -0
  17. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  18. package/vendor/base/src/definitions/fsCache.ts +155 -0
  19. package/vendor/base/src/definitions/index.ts +19 -0
  20. package/vendor/base/src/definitions/inputs.ts +30 -0
  21. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  22. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  23. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  24. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  25. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  26. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  27. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  28. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  29. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  30. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  31. package/vendor/base/src/definitions/load/load.ts +487 -0
  32. package/vendor/base/src/definitions/model.ts +238 -0
  33. package/vendor/base/src/definitions/registry/catalog.ts +659 -0
  34. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  35. package/vendor/base/src/definitions/theme.ts +383 -0
  36. package/vendor/base/src/executor.ts +284 -0
  37. package/vendor/base/src/http/uwsFetchAdapter.ts +397 -0
  38. package/vendor/base/src/items/baseVersion.ts +96 -0
  39. package/vendor/base/src/items/collect.ts +246 -0
  40. package/vendor/base/src/items/fingerprint.ts +34 -0
  41. package/vendor/base/src/items/publish.ts +212 -0
  42. package/vendor/base/src/lint/context.mjs +56 -0
  43. package/vendor/base/src/lint/design/defs.mjs +46 -0
  44. package/vendor/base/src/lint/design/file.mjs +737 -0
  45. package/vendor/base/src/lint/design/index.d.mts +30 -0
  46. package/vendor/base/src/lint/design/index.mjs +996 -0
  47. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  48. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  49. package/vendor/base/src/lint/design/walk.mjs +444 -0
  50. package/vendor/base/src/lint/index.mjs +28 -0
  51. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  52. package/vendor/base/src/lint/nodes/index.d.mts +28 -0
  53. package/vendor/base/src/lint/nodes/index.mjs +82 -0
  54. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  55. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  56. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  57. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  58. package/vendor/base/src/logger.ts +33 -0
  59. package/vendor/base/src/manifests/compose.ts +539 -0
  60. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  61. package/vendor/base/src/manifests/executor/context.ts +110 -0
  62. package/vendor/base/src/manifests/executor/index.ts +47 -0
  63. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  64. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  65. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  66. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  67. package/vendor/base/src/manifests/executor/send.ts +65 -0
  68. package/vendor/base/src/manifests/executor/session.ts +46 -0
  69. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  70. package/vendor/base/src/manifests/index.ts +280 -0
  71. package/vendor/base/src/manifests/integrity.ts +93 -0
  72. package/vendor/base/src/manifests/rows.ts +33 -0
  73. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  74. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  75. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  76. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  77. package/vendor/base/src/manifests/runtime/channels/service.ts +138 -0
  78. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  79. package/vendor/base/src/manifests/runtime/context.ts +156 -0
  80. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  81. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  82. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  83. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  84. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  85. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  86. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  87. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  88. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  89. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  90. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  91. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  92. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  93. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  94. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  95. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  96. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  97. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  98. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  99. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  100. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  101. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +550 -0
  102. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  103. package/vendor/base/src/manifests/source.ts +196 -0
  104. package/vendor/base/src/manifests/watch.ts +67 -0
  105. package/vendor/base/src/markdown/document.ts +141 -0
  106. package/vendor/base/src/paths.ts +180 -0
  107. package/vendor/base/src/platform/audioManager.ts +100 -0
  108. package/vendor/base/src/platform/clientTransport.ts +65 -0
  109. package/vendor/base/src/platform/executionContext.ts +157 -0
  110. package/vendor/base/src/platform/index.ts +10 -0
  111. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  112. package/vendor/base/src/platform/redis.ts +54 -0
  113. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  114. package/vendor/base/src/pluginBase.ts +248 -0
  115. package/vendor/base/src/plugins/README.md +57 -0
  116. package/vendor/base/src/plugins/discovery.ts +87 -0
  117. package/vendor/base/src/plugins/index.ts +16 -0
  118. package/vendor/base/src/plugins/loader.ts +179 -0
  119. package/vendor/base/src/plugins/redis.ts +36 -0
  120. package/vendor/base/src/plugins/state.ts +53 -0
  121. package/vendor/base/src/registry.ts +95 -0
  122. package/vendor/base/src/security/permissions.ts +93 -0
  123. package/vendor/base/src/security/rules.ts +73 -0
  124. package/vendor/base/src/template/SafeExpression.ts +556 -0
  125. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  126. package/vendor/base/src/template/index.ts +22 -0
  127. package/vendor/base/src/types.ts +50 -0
  128. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  129. package/vendor/base/src/workflow/PlatformMemoryCore.ts +449 -0
  130. package/vendor/base/src/workflow/routing.ts +122 -0
  131. package/vendor/sdk/lib/core/store.ts +26 -6
  132. package/local/mcp.ts +0 -265
@@ -0,0 +1,138 @@
1
+ /**
2
+ * THE SERVICE CHANNEL: a method this node offers over a service edge, exposed as MCP.
3
+ *
4
+ * The other of the two ways a node is reached, and they never cross (08-mcp-services.md).
5
+ * Called ad-hoc by an agent rather than run by the graph, and it hands back ONE value rather
6
+ * than emitting on output connectors — which is why the manifest says `returns` and why no
7
+ * events row is involved anywhere in here.
8
+ */
9
+ import type { ComposedNode } from "../../compose.js";
10
+ import type { RunContext } from "../context.js";
11
+ import { evaluate } from "../templating.js";
12
+ import { runCalls } from "../http/request.js";
13
+ import type { StateStore } from "../state.js";
14
+ import { rowComponentsFromResults, renderRowComponents, screenLine } from "../../../agent-mcp/rowComponents.js";
15
+ import { withHelpers } from "../helpers.js";
16
+
17
+ /**
18
+ * THE CALLER'S LIVE SESSION, supplied by the EXECUTOR and never by the manifest.
19
+ *
20
+ * `accessToken` is the deliberate omission from RunContext (§9.4): a manifest is data that
21
+ * can arrive by paste or database row, and one that could read the token could forward it to
22
+ * any host its allowedHosts allows. It is threaded here instead, the same way the tool bridge
23
+ * already uses it — the platform calling its own surface on the user's behalf, rather than
24
+ * data deciding where a secret goes.
25
+ */
26
+ export interface CallerSession {
27
+ userId?: string;
28
+ conversationId?: string;
29
+ chatId?: string;
30
+ accessToken?: string;
31
+ }
32
+
33
+ /**
34
+ * The package's named helpers are bound around the WHOLE method, not around `returns` alone:
35
+ * a call's `when`, a url, an error message and the projection are all expressions, and a
36
+ * helper that some of them could call and others could not would be a rule with no reason
37
+ * behind it.
38
+ */
39
+ export async function performService(
40
+ node: ComposedNode,
41
+ method: string,
42
+ params: Record<string, any>,
43
+ ctx: RunContext,
44
+ store?: StateStore,
45
+ session?: CallerSession,
46
+ ): Promise<unknown> {
47
+ return withHelpers(node, () => serviceMethod(node, method, params, ctx, store, session));
48
+ }
49
+
50
+ async function serviceMethod(
51
+ node: ComposedNode,
52
+ method: string,
53
+ params: Record<string, any>,
54
+ ctx: RunContext,
55
+ store?: StateStore,
56
+ session?: CallerSession,
57
+ ): Promise<unknown> {
58
+ const spec = node.api?.service?.[method];
59
+ if (!spec) {
60
+ const known = Object.keys(node.api?.service ?? {});
61
+ throw new Error(
62
+ `${node.type} has no service method "${method}"${known.length ? ` — it has ${known.join(", ")}` : " — it has none"}`,
63
+ );
64
+ }
65
+
66
+ const scoped: RunContext = { ...ctx, params };
67
+
68
+ // Every call settles here, including the last: a method hands back ONE value, so there
69
+ // is no connector for a stream to emit onto.
70
+ const { results, last } = await runCalls(node, spec.calls, scoped, `${node.type}.${method}`, store);
71
+
72
+ /**
73
+ * CONTENT CARDS, rendered from the rows this method surfaced.
74
+ *
75
+ * BEFORE `returns`, and that ordering is the whole reason this lives here rather than in
76
+ * the executor: `returns` is where a node projects its reply down to what a model should
77
+ * read, and a render needs the FULL row (the component uri, the images, the authored copy)
78
+ * that the projection strips. Rendering after it would draw from data that no longer has
79
+ * what the component is made of.
80
+ *
81
+ * DATA-DRIVEN, never a model tool: a row that carries a component renders it the moment it
82
+ * surfaces, so the model cannot forget to show it, describe one that is not there, or
83
+ * decide not to bother. No prompt is involved anywhere.
84
+ *
85
+ * Fire-and-forget and deliberately not awaited: this is a side channel to a screen, and
86
+ * a slow render must not hold up the answer the caller is waiting on. With no live session
87
+ * (builder, tests, headless) `renderRowComponents` no-ops on the missing userId, so a
88
+ * manifest declaring this stays pure everywhere it is not wanted, with no flag to juggle.
89
+ */
90
+ if (spec.renderComponents) {
91
+ const rows = await evaluate(spec.renderComponents, { response: last, calls: results, params, config: ctx.config });
92
+ const found = rowComponentsFromResults(rows);
93
+ // SAY WHAT HAPPENED, always: this lane no-ops on no component rows or no live session,
94
+ // and silence must not read as success. `renderRowComponents` logs its own settles.
95
+ const sessionCtx = {
96
+ userId: session?.userId,
97
+ conversationId: session?.conversationId,
98
+ chatId: session?.chatId,
99
+ accessToken: session?.accessToken,
100
+ };
101
+ const log = (m: string) => console.log(`[manifests] ${node.type}: ${m}`);
102
+ if (found.length) {
103
+ if (!session?.userId) {
104
+ console.log(`[manifests] ${node.type}: ${found.length} row component(s) not rendered — no live session on this run`);
105
+ }
106
+ const outcomes = renderRowComponents(found, sessionCtx, log);
107
+ /**
108
+ * THE SEARCH MIRRORS TOO (MCP_APP_PROTOCOL §0.1, ruled 2026-09-05). When the delivery
109
+ * holds a component that takes input (a shell), the model must answer the SCREEN,
110
+ * not the rows: it wait for the delivery's verdict — instant here, the ladder at the
111
+ * door leaves one candidate and the rules brain answers at once — and the result
112
+ * carries a `screen` line saying what is on screen and what was not shown.
113
+ * Card-only deliveries stay fire-and-forget, as fast as ever.
114
+ */
115
+ if (found.some((i) => i.shell)) {
116
+ const screen = screenLine(await outcomes);
117
+ const result = await evaluate(spec.returns, { response: last, calls: results, params, config: ctx.config, user: ctx.user });
118
+ return screen && result && typeof result === "object" && !Array.isArray(result) ? { ...(result as Record<string, unknown>), screen } : result;
119
+ }
120
+ } else {
121
+ const n = Array.isArray(rows) ? rows.length : 0;
122
+ console.log(`[manifests] ${node.type}: no row components in ${n} row(s)`);
123
+ // The empty delivery is announced (§12e mechanical spent rule): this search
124
+ // ran and surfaced nothing, and the standing set's fate hangs on that fact.
125
+ void renderRowComponents(found, sessionCtx, log);
126
+ }
127
+ }
128
+
129
+ // `params`, `config` and `user` are in scope so a method can shape its result from what
130
+ // it was asked for and who asked, not only from what came back.
131
+ return evaluate(spec.returns, {
132
+ response: last,
133
+ calls: results,
134
+ params,
135
+ config: ctx.config,
136
+ user: ctx.user,
137
+ });
138
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * THE WORKFLOW CHANNEL: what a node does when the GRAPH triggers it.
3
+ *
4
+ * One of the two ways a node is reached, and they never cross (08-mcp-services.md). This one
5
+ * fires the node's output connectors and answers nobody; `service.ts` answers a caller and
6
+ * fires no connectors.
7
+ *
8
+ * What lives here is the SHAPE OF A RUN and nothing else — which calls lead, which one settles
9
+ * the node, what an events row may see. The individual capabilities are elsewhere: `final.ts`
10
+ * dispatches the last call, `../http/request.ts` runs the leading ones, `../narrate.ts` talks
11
+ * while it works.
12
+ */
13
+ import type { ComposedNode } from "../../compose.js";
14
+ import type { RunContext } from "../context.js";
15
+ import { evaluate } from "../templating.js";
16
+ import { runCalls } from "../http/request.js";
17
+ import type { Emission } from "../http/response.js";
18
+ import type { AudioLane } from "../duplex/audioLane.js";
19
+ import type { StateStore } from "../state.js";
20
+ import { makeEmitter } from "../events.js";
21
+ import { makeNarrator } from "../narrate.js";
22
+ import { runToolLoop, type ToolBridge } from "../tools/toolloop.js";
23
+ import { runFinal } from "./final.js";
24
+ import { withHelpers } from "../helpers.js";
25
+ import { clientTransport } from "../../../platform/clientTransport.js";
26
+ import { rowComponentsFromResults, renderRowComponents } from "../../../agent-mcp/rowComponents.js";
27
+ import type { CallerSession } from "./service.js";
28
+
29
+ export interface RunResult {
30
+ outputs: Record<string, unknown>;
31
+ emissions: Emission[];
32
+ status: number;
33
+ }
34
+
35
+ /**
36
+ * `onEmit` receives every streamed emission as it happens, which is what a CallbackNode
37
+ * forwards to its output connectors in real time.
38
+ */
39
+ export async function performApi(
40
+ node: ComposedNode,
41
+ ctx: RunContext,
42
+ onEmit: (e: Emission) => void = () => {},
43
+ tools?: ToolBridge,
44
+ store?: StateStore,
45
+ lane?: AudioLane | null,
46
+ session?: CallerSession,
47
+ ): Promise<RunResult> {
48
+ // The package's named helpers, bound for the whole run — see the same wrapper on the
49
+ // service channel. Every expression this run evaluates, in a call or an events row or a
50
+ // tool exchange, sees the same bag.
51
+ return withHelpers(node, () => runApi(node, ctx, onEmit, tools, store, lane, session));
52
+ }
53
+
54
+ async function runApi(
55
+ node: ComposedNode,
56
+ ctx: RunContext,
57
+ onEmit: (e: Emission) => void = () => {},
58
+ tools?: ToolBridge,
59
+ store?: StateStore,
60
+ lane?: AudioLane | null,
61
+ session?: CallerSession,
62
+ ): Promise<RunResult> {
63
+ const api = node.api;
64
+ if (!api) throw new Error(`Node "${node.type}" has no api block, so there is nothing to perform`);
65
+
66
+ /**
67
+ * A NODE MAY MAKE NO REQUEST AT ALL.
68
+ *
69
+ * Two different things used to fail on the same check, and only one of them should:
70
+ *
71
+ * service-only `service` methods and nothing else. The graph must NOT run it — it
72
+ * answers a caller over a service edge, and has no inputs to reach it by.
73
+ * Still refused, below.
74
+ * events-only an `events` table and no calls. LEGITIMATE. `IfElse` picks which output
75
+ * dot the data lands on; `Relay` passes its input through; `Note` does
76
+ * nothing at all. There is no url, so `run` would be a fiction.
77
+ *
78
+ * NOT AN ESCAPE HATCH, which is the only reason this is allowed (§2, §10). The safety
79
+ * property is that a manifest cannot EXECUTE — and an events-only node executes nothing. It
80
+ * is an events table over already-resolved config and the incoming signal, evaluated by the
81
+ * same sandbox as every other expression. Nothing new is reachable; there is simply no
82
+ * request in front of it.
83
+ *
84
+ * The distinction is `events`, not the absence of `run`, so a service-only node is still
85
+ * refused with the message it always had.
86
+ */
87
+ const eventsOnly = !api.run?.length && !!api.events?.length;
88
+ if (!api.run?.length && !eventsOnly)
89
+ throw new Error(`Node "${node.type}" has only service methods — the graph cannot run it`);
90
+
91
+ // LEAD then FINAL. Every call but the last settles and just fills `calls.<name>`; the
92
+ // last is the one whose reply becomes the node's answer, so it alone may stream. A node
93
+ // that makes one call is the ordinary case and takes this path with an empty lead.
94
+ const lead = eventsOnly ? [] : api.run.slice(0, -1);
95
+ const final = eventsOnly ? undefined : api.run[api.run.length - 1];
96
+
97
+ /**
98
+ * WHAT AN EVENTS ROW MAY SEE. Mutated rather than copied: `base` is spread at each fire, so filling
99
+ * in `calls` after the lead runs makes it visible to every row, including the `complete` ones.
100
+ *
101
+ * `signal` and `scope` were MISSING, and both are documented roots. That is not theoretical:
102
+ * `aws-s3/S3FileContent` reads `signal.key`, `signal.universalId` and `signal.etag` in its events
103
+ * row, and `flow/Context` reads `scope.*` — so both threw "unknown identifier" on every run. The
104
+ * sandbox refuses an undeclared identifier outright, so the row failed rather than reading empty.
105
+ *
106
+ * Kept in step with the roots `context.ts` documents; a row seeing less than a body template can is
107
+ * a trap, because the same expression works in one file and not the other.
108
+ */
109
+ const base: Record<string, unknown> = {
110
+ config: ctx.config,
111
+ user: ctx.user,
112
+ scope: ctx.scope,
113
+ signal: ctx.signal,
114
+ calls: {},
115
+ };
116
+ const emitter = makeEmitter(node, onEmit, base);
117
+
118
+ /**
119
+ * EVENTS-ONLY: there is no reply, so the rows fire over the SIGNAL.
120
+ *
121
+ * `response` is the incoming signal rather than a body, which is the only honest thing it
122
+ * can be: `IfElse` routes what arrived, and `Relay` passes it through. A row's `when` is
123
+ * what makes routing work — two rows, one per output dot, each testing the same condition.
124
+ *
125
+ * Returns before the request machinery, so nothing below it can reach for a `final` that
126
+ * does not exist.
127
+ */
128
+ if (eventsOnly) {
129
+ await emitter.response(ctx.signal ?? {});
130
+ const emissions = await emitter.finish();
131
+ const outputs = emitter.outputs();
132
+ await publishTemplateData(node, api, { ...base, output: outputs }, session);
133
+ return { outputs, emissions, status: 200 };
134
+ }
135
+
136
+ // Narration first, and on BOTH paths: it is a property of the node, not of tools.
137
+ const narrator = makeNarrator(node, ctx, emitter);
138
+ narrator.fire({ kind: "turnStart", userMessage: ctx.config?.prompt ?? "" });
139
+
140
+ try {
141
+ const { results, last } = lead.length
142
+ ? await runCalls(node, lead, ctx, node.type, store)
143
+ : { results: {} as Record<string, any>, last: undefined as any };
144
+ base.calls = results;
145
+ const scoped: RunContext = { ...ctx, calls: results };
146
+
147
+ // THE LAST CALL HONOURS ITS `when` LIKE EVERY OTHER ONE.
148
+ //
149
+ // It did not, and the failure was live rather than theoretical: HubSpot's last call
150
+ // writes queued notes to the CRM and is gated on a config toggle, so with that toggle
151
+ // OFF the node still POSTed to the notes endpoint on every run. Nothing errored,
152
+ // because the vendor accepted an empty write. A conditional call that fires anyway is
153
+ // worse than one that never fires, because the wrongness is invisible.
154
+ //
155
+ // Skipped, the node settles on the last call that DID run, which is what a reader
156
+ // expects of a list whose tail is optional.
157
+ const skipFinal = !!final.when && !(await evaluate(final.when, scoped as unknown as Record<string, unknown>));
158
+
159
+ let status = 200;
160
+ if (skipFinal) {
161
+ await emitter.response(last);
162
+ } else {
163
+ // No bridge means nothing granted this node tools. Normal (an mcp connector nobody
164
+ // wired), so it degrades to one ordinary call rather than failing.
165
+ /**
166
+ * `runToolLoop` is the HTTP tool loop and must NOT take a duplex node.
167
+ *
168
+ * It works by TURNS — send a request, read the tool calls out of the reply, send another —
169
+ * and a socket has no turns: the conversation never ended, so a tool result goes back down
170
+ * the same connection. A ws node declaring a toolExchange therefore stays on the duplex
171
+ * path, which resolves its calls inline. Without this check, declaring tools on the voice
172
+ * node would silently route it through HTTP and never open the socket at all.
173
+ */
174
+ const duplex = final.transport === "ws";
175
+ status = api.toolExchange && tools && !duplex
176
+ ? await runToolLoop(node, scoped, tools, emitter, narrator.fire, final, store)
177
+ : await runFinal(node, final, scoped, emitter, base, store, lane, tools);
178
+ }
179
+
180
+ const emissions = await emitter.finish();
181
+ const outputs = withSaveFlag(emitter.outputs(), api, base);
182
+ // The FULL settled reply, for the card lane: runFinal writes the final call's payload
183
+ // into base.calls under its name; on a skipped final the run settled on the lead's last.
184
+ const settled = (base.calls as Record<string, unknown>)[final.name] ?? last;
185
+ await publishTemplateData(node, api, { ...base, output: outputs }, session);
186
+ await renderRunComponents(node, api, { ...base, output: outputs, response: settled }, session);
187
+ return { outputs, emissions, status };
188
+ } finally {
189
+ narrator.settle();
190
+ }
191
+ }
192
+
193
+ /**
194
+ * `publish` — A GENERIC WRITE INTO THE CALLER'S TEMPLATE STATE, the workflow channel's
195
+ * sibling of the service channel's `renderComponents`: both reach the person watching rather
196
+ * than answer the graph, and both no-op when nobody is (builder, tests, headless, cron).
197
+ *
198
+ * AFTER the emitter settles, over `output`, deliberately: the pushed value derives from what
199
+ * the node actually emitted, so the screen and the graph cannot drift apart — a Suggestions
200
+ * whose connector carried one object and whose push carried another would be exactly the
201
+ * half-working that takes an afternoon to see.
202
+ *
203
+ * The frame is the one the retired Suggestions node sent and the client already handles
204
+ * (`connection.ts` APP_DATA): the client merges `data` opaquely into template state and
205
+ * knows no key names — the PRODUCER names them (UNOVERSE_STATE_MODEL §2/§8).
206
+ *
207
+ * SAY WHAT HAPPENED, always, for the same reason the card lane learned to: this no-ops three
208
+ * different ways (no session, nothing evaluated, nobody listening) and silence must not read
209
+ * as success.
210
+ */
211
+ async function publishTemplateData(
212
+ node: ComposedNode,
213
+ api: NonNullable<ComposedNode["api"]>,
214
+ scope: Record<string, unknown>,
215
+ session?: CallerSession,
216
+ ): Promise<void> {
217
+ const spec = api.publish;
218
+ if (!spec) return;
219
+ if (spec.when && !(await evaluate(spec.when, scope))) return;
220
+ const data = await evaluate(spec.data, scope);
221
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
222
+ console.log(`[manifests] ${node.type}: publish evaluated to ${Array.isArray(data) ? "an array" : typeof data} — template state merges OBJECTS, nothing pushed`);
223
+ return;
224
+ }
225
+ if (!session?.userId || !session?.conversationId) {
226
+ console.log(`[manifests] ${node.type}: template data not pushed — no live session on this run`);
227
+ return;
228
+ }
229
+ const delivered = clientTransport().pushToClient(session.userId, session.conversationId, {
230
+ type: "APP_DATA",
231
+ chatId: session.chatId,
232
+ conversationId: session.conversationId,
233
+ userId: session.userId,
234
+ data,
235
+ timestamp: new Date().toISOString(),
236
+ });
237
+ console.log(
238
+ `[manifests] ${node.type}: template data ${delivered ? "pushed" : "found NO listener"} — keys: ${Object.keys(data as object).join(", ") || "(none)"}`,
239
+ );
240
+ }
241
+
242
+ /**
243
+ * `renderComponents` — THE RUN LANE'S CARD LANE, the workflow channel's sibling of the
244
+ * service channel's per-method `renderComponents` (service.ts, which this mirrors
245
+ * deliberately, same renderer): rows the run surfaced render as content cards
246
+ * on the caller's live screen, data-driven, never a model tool. Declared as
247
+ * api/renderComponents.yaml on the node.
248
+ *
249
+ * AFTER the events table settles, over the FULL reply: an events row is where a node
250
+ * projects its outputs down for the graph, and a card needs the full row (component uri,
251
+ * images, authored copy) that the projection strips. `response` in the scope is the final
252
+ * call's settled payload, never an output.
253
+ *
254
+ * Fire-and-forget and no-op without a live session, exactly like the service lane and
255
+ * `publish` above: a run nobody is watching should carry on, not throw. SAY WHAT HAPPENED,
256
+ * always, for the same reason the service lane learned to — this no-ops three different
257
+ * ways and silence must not read as success.
258
+ */
259
+ async function renderRunComponents(
260
+ node: ComposedNode,
261
+ api: NonNullable<ComposedNode["api"]>,
262
+ scope: Record<string, unknown>,
263
+ session?: CallerSession,
264
+ ): Promise<void> {
265
+ if (!api.renderComponents) return;
266
+ const log = (m: string) => console.log(`[manifests] ${node.type}: ${m}`);
267
+ const rows = await evaluate(api.renderComponents, scope);
268
+ const sessionCtx = {
269
+ userId: session?.userId,
270
+ conversationId: session?.conversationId,
271
+ chatId: session?.chatId,
272
+ accessToken: session?.accessToken,
273
+ };
274
+ const found = rowComponentsFromResults(rows);
275
+ if (found.length) {
276
+ if (!session?.userId) log(`${found.length} row component(s) not rendered — no live session on this run`);
277
+ renderRowComponents(found, sessionCtx, log);
278
+ } else {
279
+ const n = Array.isArray(rows) ? rows.length : 0;
280
+ log(`no row components in ${n} row(s)`);
281
+ // The empty delivery is announced (§12e mechanical spent rule): this search
282
+ // ran and surfaced nothing, and the standing set's fate hangs on that fact.
283
+ renderRowComponents(found, sessionCtx, log);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * `__saveToContext`, which the ENGINE reads off the output envelope.
289
+ *
290
+ * Two halves make one behaviour and both must happen or neither is any use: the `save` state op
291
+ * writes `saved:<executionId>` for a LATER run of the template resolver, and this flag tells
292
+ * `executingState` to put the same value into the in-memory cache so THIS run can see it too. The
293
+ * retired Code node set both by hand. Here the flag is DERIVED rather than declared, so a manifest
294
+ * cannot write the value and forget the flag — the value would be reachable next node but not this
295
+ * one, which is the sort of half-working that takes an afternoon to see.
296
+ *
297
+ * Derived from `base.calls`, not from the declaration: a call records its name there only if it
298
+ * actually RAN, so a save gated `when: config.saveToContext` correctly sets no flag when the toggle
299
+ * is off. That also makes it work the same whether the save is the last call or an earlier one,
300
+ * with nothing threaded through.
301
+ */
302
+ function withSaveFlag(
303
+ outputs: Record<string, unknown>,
304
+ api: NonNullable<ComposedNode["api"]>,
305
+ base: Record<string, unknown>,
306
+ ): Record<string, unknown> {
307
+ const ran = base.calls as Record<string, unknown>;
308
+ const saved = (api.run ?? []).some((c: any) => c.state === "save" && ran[c.name] !== undefined);
309
+ return saved ? { ...outputs, __saveToContext: true } : outputs;
310
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * What a manifest may see at run time, and the resolvers applied before it does.
3
+ *
4
+ * Part of the manifest runtime (DECLARATIVE_NODES.md §2): the manifest DESCRIBES the
5
+ * service, this half COMPUTES it. Split by concern so each piece stays readable.
6
+ */
7
+ import type { ComposedNode } from "../compose.js";
8
+
9
+ /** Everything a manifest's templates and expressions may see. */
10
+ export interface RunContext {
11
+ config: Record<string, any>;
12
+ credentials: Record<string, any>;
13
+ /** Upstream node outputs, keyed signal.<sourceId>.<outputHandle>. */
14
+ signal: Record<string, any>;
15
+ /** Prompt blocks by camelCase name, so {{prompt.markdownGuidelines}} resolves. */
16
+ prompt: Record<string, string>;
17
+ /** Runtime service wiring, e.g. services.mcpService.tools. */
18
+ services: Record<string, any>;
19
+ /** Arguments a CALLER passed to a service method. Empty when the graph runs the node. */
20
+ params: Record<string, any>;
21
+ /**
22
+ * Replies from the calls made EARLIER in this run, keyed by each call's `name`.
23
+ *
24
+ * Empty for a node that makes one call. A call skipped by its `when` has no key at all,
25
+ * which is what lets a later call, an events row, or `returns` ask whether it happened.
26
+ *
27
+ * Named `calls` and not `steps` on purpose: a step is a node on a canvas, and reusing
28
+ * the word here would make "this node's steps" and "the workflow's steps" the same
29
+ * phrase for two different things.
30
+ */
31
+ calls: Record<string, any>;
32
+ /**
33
+ * The CALLER'S DISCONNECT, when the node runs over a stream. Set from the request's
34
+ * own signal; a turn loop checks it between turns and stops. Absent for a node that
35
+ * runs in-process, which cannot be hung up on.
36
+ */
37
+ abort?: AbortSignal;
38
+ /**
39
+ * The SIGNED-IN person, and nothing that authenticates as them.
40
+ *
41
+ * Identity only: email, id, name. The caller's `accessToken` is deliberately absent and
42
+ * must stay absent (see executor.ts contextFor). The distinction is the whole reason
43
+ * this scope can exist at all: a JWT IS the user against our own services, so a pasted
44
+ * manifest could send it to any allowed host, whereas an email authenticates nothing.
45
+ *
46
+ * It earns a first-class scope because "who is asking" is the join key for every CRM,
47
+ * support and account node, and reading it off the wire instead would let a caller
48
+ * fetch someone else's record.
49
+ */
50
+ user: { email?: string; id?: string; name?: string };
51
+ /**
52
+ * WHICH RUN this is, for keying platform state.
53
+ *
54
+ * `userId` is the PUBLISHING context's id, not `user.id`: the platform's own user id and
55
+ * the authenticated identity are different values, and the memory server keys its
56
+ * snapshot by the former. Using the wrong one writes to a key nobody reads, silently.
57
+ */
58
+ /**
59
+ * `conversationId` and `chatId` are the AUDIO LANE's keys, not decoration. A duplex voice node
60
+ * registers its microphone handler and sends the model's audio against the conversation, so
61
+ * without them the lane silently matches nothing: every frame in is dropped by an id comparison
62
+ * that cannot succeed, and every frame out goes to a conversation nobody is listening on. That
63
+ * failed with no error at all, which is why they are first-class here rather than read ad hoc.
64
+ */
65
+ /**
66
+ * `nodeId` is WHICH INSTANCE on the canvas, and it is here because two different things are
67
+ * keyed by it and neither can be derived from anything else in scope.
68
+ *
69
+ * A content-addressed id mixes it in, so two Code nodes computing the same value in the same
70
+ * workflow still mint different universal ids — which is the point, since they are different
71
+ * places in the graph. And the saved-context hash stores one field per node, so a later
72
+ * template reaches `saved.<nodeId>`. Without it both fall back to a constant and collide
73
+ * silently: the ids look stable while describing the wrong node.
74
+ */
75
+ /**
76
+ * `platformUrl` is THE PLATFORM'S OWN API, for the nodes that call US rather than a vendor.
77
+ *
78
+ * Most nodes reach a third party at a host they can name in `allowedHosts`. A growing few reach the
79
+ * engine instead — spatial ingestion hands raw text to the content pipeline, spatial search queries
80
+ * the dictionary — and that host is a DEPLOYMENT fact: localhost in dev, a service name in a cluster,
81
+ * a domain in production. A manifest cannot derive it and must never guess it.
82
+ *
83
+ * DERIVED from `UNOVERSE_RUNTIME_PORT`, which docker-compose already sets and the server already
84
+ * reads — a node runs INSIDE the service, so it is calling itself and needs no configuration to learn
85
+ * its own address. Loopback, because :4106 is the ungated internal listener and is published to
86
+ * 127.0.0.1 only.
87
+ */
88
+ scope: { userId?: string; workflowId?: string; conversationId?: string; chatId?: string; executionId?: string; nodeId?: string; platformUrl?: string; engineUrl?: string };
89
+ /**
90
+ * THE CONVERSATION SO FAR, for a tool loop against a vendor with no chain id.
91
+ *
92
+ * Empty except inside `runToolLoop`, and empty there on the first turn. It grows by the assistant's
93
+ * tool-call turn and one message per result, and a manifest spreads it into its own message array:
94
+ *
95
+ * messages: [{ role: 'system', … }, { role: 'user', … }, ...transcript]
96
+ *
97
+ * It exists because Chat Completions — GLM, Grok, most OpenAI-COMPATIBLE vendors — resends the whole
98
+ * history every turn, where the Responses API hands back a `previous_response_id` and the next turn
99
+ * just names it. `continuity` models the latter; this models the former. The SHAPE of a message stays
100
+ * in the manifest, because it is the vendor's, not ours.
101
+ */
102
+ transcript: unknown[];
103
+ /**
104
+ * The skills in force, rendered for a system prompt. Empty except inside `runToolLoop`,
105
+ * and empty there until a search resolves the conversation into a region that has one.
106
+ * Read by a manifest as `{{ skillsText }}`, the same way `transcript` is spread into a
107
+ * message array: the platform decides WHICH skills apply, the manifest decides where the
108
+ * vendor wants them.
109
+ */
110
+ skillsText: string;
111
+
112
+ /**
113
+ * What an OAuth2 exchange returned, for the call it authenticated.
114
+ *
115
+ * Some vendors tell you WHERE to talk as well as how: Salesforce returns `instance_url`
116
+ * and the org's data lives there, not at the login host you authenticated against. Only
117
+ * populated for `oauth2ClientCredentials`, and never carries the token itself.
118
+ */
119
+ token: { instanceUrl?: string };
120
+ }
121
+
122
+ export function emptyContext(partial: Partial<RunContext> = {}): RunContext {
123
+ return { config: {}, credentials: {}, signal: {}, prompt: {}, services: {}, params: {}, calls: {}, user: {}, scope: {}, token: {}, transcript: [], skillsText: "", ...partial };
124
+ }
125
+
126
+ /** Config values a resolver rewrites just before the request is built. */
127
+ export const RESOLVERS: Record<string, (value: any, node: ComposedNode) => any> = {
128
+ // Migration of shared/models.ts resolveModel(): a saved workflow stores a concrete
129
+ // model id, and if that id is retired this falls back to the current best model in
130
+ // the same tier. Without it, replacing a model generation breaks every saved
131
+ // workflow that named an old id.
132
+ modelTier(value: any, node: ComposedNode) {
133
+ const field = node.definition.configSchema?.properties?.model;
134
+ const options: string[] = field?.enum ?? [];
135
+ if (!value || options.includes(value)) return value || options[0];
136
+ const m = String(value).toLowerCase();
137
+ // Tier words cover both families the packages ship: GPT (luna/terra) and Claude
138
+ // (haiku/sonnet). Anything unrecognised lands on large, the safe over-provision.
139
+ const tier = /luna|mini|small|nano|haiku/.test(m) ? 0 : /terra|balanced|medium|sonnet/.test(m) ? 1 : 2;
140
+ // Options are listed large first, so index from the end for small.
141
+ const picked = [options[options.length - 1], options[1], options[0]][tier] ?? options[0];
142
+ console.warn(`[manifests] model "${value}" is no longer available — using "${picked}". Update this node.`);
143
+ return picked;
144
+ },
145
+ };
146
+
147
+ /** Apply every declared resolver to the config before templates see it. */
148
+ export function applyResolvers(config: Record<string, any>, node: ComposedNode): Record<string, any> {
149
+ const props = node.definition.configSchema?.properties ?? {};
150
+ const out = { ...config };
151
+ for (const [name, spec] of Object.entries<any>(props)) {
152
+ const fn = spec?.resolve ? RESOLVERS[spec.resolve] : null;
153
+ if (fn) out[name] = fn(out[name], node);
154
+ }
155
+ return out;
156
+ }