@unoverse-platform/studio 0.1.90 → 0.1.91
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.
- package/local/plugin.ts +20 -12
- package/package.json +13 -3
- package/vendor/base/src/agent-mcp/discovery.ts +203 -0
- package/vendor/base/src/agent-mcp/handoff.ts +56 -0
- package/vendor/base/src/agent-mcp/index.ts +44 -0
- package/vendor/base/src/agent-mcp/invoke.ts +140 -0
- package/vendor/base/src/agent-mcp/lean.ts +111 -0
- package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
- package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
- package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
- package/vendor/base/src/agent-mcp/tools.ts +134 -0
- package/vendor/base/src/agent-mcp/types.ts +130 -0
- package/vendor/base/src/auth/auth.ts +278 -0
- package/vendor/base/src/auth/authConfig.ts +73 -0
- package/vendor/base/src/boot.ts +245 -0
- package/vendor/base/src/definitions/definitions.ts +18 -0
- package/vendor/base/src/definitions/dsPackage.ts +45 -0
- package/vendor/base/src/definitions/fsCache.ts +128 -0
- package/vendor/base/src/definitions/index.ts +19 -0
- package/vendor/base/src/definitions/inputs.ts +30 -0
- package/vendor/base/src/definitions/load/compile/component.ts +80 -0
- package/vendor/base/src/definitions/load/compile/index.ts +12 -0
- package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
- package/vendor/base/src/definitions/load/compile/template.ts +153 -0
- package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
- package/vendor/base/src/definitions/load/expand/index.ts +17 -0
- package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
- package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
- package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
- package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
- package/vendor/base/src/definitions/load/load.ts +487 -0
- package/vendor/base/src/definitions/model.ts +238 -0
- package/vendor/base/src/definitions/registry/catalog.ts +643 -0
- package/vendor/base/src/definitions/registry/homes.ts +252 -0
- package/vendor/base/src/definitions/theme.ts +383 -0
- package/vendor/base/src/executor.ts +284 -0
- package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
- package/vendor/base/src/items/baseVersion.ts +96 -0
- package/vendor/base/src/items/collect.ts +199 -0
- package/vendor/base/src/items/fingerprint.ts +34 -0
- package/vendor/base/src/items/publish.ts +198 -0
- package/vendor/base/src/lint/context.mjs +56 -0
- package/vendor/base/src/lint/design/defs.mjs +46 -0
- package/vendor/base/src/lint/design/file.mjs +737 -0
- package/vendor/base/src/lint/design/index.d.mts +30 -0
- package/vendor/base/src/lint/design/index.mjs +996 -0
- package/vendor/base/src/lint/design/tokens.mjs +128 -0
- package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
- package/vendor/base/src/lint/design/walk.mjs +444 -0
- package/vendor/base/src/lint/index.mjs +28 -0
- package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
- package/vendor/base/src/lint/nodes/index.mjs +64 -0
- package/vendor/base/src/lint/nodes/load.mjs +126 -0
- package/vendor/base/src/lint/nodes/node.mjs +764 -0
- package/vendor/base/src/lint/nodes/package.mjs +112 -0
- package/vendor/base/src/lint/nodes/schema.mjs +75 -0
- package/vendor/base/src/logger.ts +33 -0
- package/vendor/base/src/manifests/compose.ts +539 -0
- package/vendor/base/src/manifests/executor/callback.ts +107 -0
- package/vendor/base/src/manifests/executor/context.ts +107 -0
- package/vendor/base/src/manifests/executor/index.ts +47 -0
- package/vendor/base/src/manifests/executor/lookup.ts +21 -0
- package/vendor/base/src/manifests/executor/memory.ts +62 -0
- package/vendor/base/src/manifests/executor/promise.ts +71 -0
- package/vendor/base/src/manifests/executor/refire.ts +34 -0
- package/vendor/base/src/manifests/executor/send.ts +65 -0
- package/vendor/base/src/manifests/executor/session.ts +46 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
- package/vendor/base/src/manifests/index.ts +280 -0
- package/vendor/base/src/manifests/integrity.ts +93 -0
- package/vendor/base/src/manifests/rows.ts +33 -0
- package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
- package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
- package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
- package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
- package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
- package/vendor/base/src/manifests/runtime/context.ts +150 -0
- package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
- package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
- package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
- package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
- package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
- package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
- package/vendor/base/src/manifests/runtime/events.ts +223 -0
- package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
- package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
- package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
- package/vendor/base/src/manifests/runtime/index.ts +57 -0
- package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
- package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
- package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
- package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
- package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
- package/vendor/base/src/manifests/runtime/state.ts +171 -0
- package/vendor/base/src/manifests/runtime/templating.ts +110 -0
- package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
- package/vendor/base/src/manifests/runtime/usage.ts +87 -0
- package/vendor/base/src/manifests/source.ts +187 -0
- package/vendor/base/src/manifests/watch.ts +67 -0
- package/vendor/base/src/markdown/document.ts +141 -0
- package/vendor/base/src/paths.ts +180 -0
- package/vendor/base/src/platform/audioManager.ts +100 -0
- package/vendor/base/src/platform/clientTransport.ts +65 -0
- package/vendor/base/src/platform/executionContext.ts +154 -0
- package/vendor/base/src/platform/index.ts +10 -0
- package/vendor/base/src/platform/pluginAPI.ts +146 -0
- package/vendor/base/src/platform/redis.ts +54 -0
- package/vendor/base/src/platform/serviceCalls.ts +222 -0
- package/vendor/base/src/pluginBase.ts +248 -0
- package/vendor/base/src/plugins/README.md +57 -0
- package/vendor/base/src/plugins/discovery.ts +87 -0
- package/vendor/base/src/plugins/index.ts +16 -0
- package/vendor/base/src/plugins/loader.ts +179 -0
- package/vendor/base/src/plugins/redis.ts +36 -0
- package/vendor/base/src/plugins/state.ts +53 -0
- package/vendor/base/src/registry.ts +95 -0
- package/vendor/base/src/security/permissions.ts +93 -0
- package/vendor/base/src/security/rules.ts +73 -0
- package/vendor/base/src/template/SafeExpression.ts +556 -0
- package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
- package/vendor/base/src/template/index.ts +22 -0
- package/vendor/base/src/types.ts +50 -0
- package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
- package/vendor/base/src/workflow/routing.ts +122 -0
- package/vendor/sdk/lib/core/store.ts +26 -6
|
@@ -0,0 +1,125 @@
|
|
|
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 } 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
|
+
renderRowComponents(found, sessionCtx, log);
|
|
107
|
+
} else {
|
|
108
|
+
const n = Array.isArray(rows) ? rows.length : 0;
|
|
109
|
+
console.log(`[manifests] ${node.type}: no row components in ${n} row(s)`);
|
|
110
|
+
// The empty delivery is announced (§12e mechanical spent rule): this search
|
|
111
|
+
// ran and surfaced nothing, and the standing set's fate hangs on that fact.
|
|
112
|
+
renderRowComponents(found, sessionCtx, log);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// `params`, `config` and `user` are in scope so a method can shape its result from what
|
|
117
|
+
// it was asked for and who asked, not only from what came back.
|
|
118
|
+
return evaluate(spec.returns, {
|
|
119
|
+
response: last,
|
|
120
|
+
calls: results,
|
|
121
|
+
params,
|
|
122
|
+
config: ctx.config,
|
|
123
|
+
user: ctx.user,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -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,150 @@
|
|
|
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 SIGNED-IN person, and nothing that authenticates as them.
|
|
34
|
+
*
|
|
35
|
+
* Identity only: email, id, name. The caller's `accessToken` is deliberately absent and
|
|
36
|
+
* must stay absent (see executor.ts contextFor). The distinction is the whole reason
|
|
37
|
+
* this scope can exist at all: a JWT IS the user against our own services, so a pasted
|
|
38
|
+
* manifest could send it to any allowed host, whereas an email authenticates nothing.
|
|
39
|
+
*
|
|
40
|
+
* It earns a first-class scope because "who is asking" is the join key for every CRM,
|
|
41
|
+
* support and account node, and reading it off the wire instead would let a caller
|
|
42
|
+
* fetch someone else's record.
|
|
43
|
+
*/
|
|
44
|
+
user: { email?: string; id?: string; name?: string };
|
|
45
|
+
/**
|
|
46
|
+
* WHICH RUN this is, for keying platform state.
|
|
47
|
+
*
|
|
48
|
+
* `userId` is the PUBLISHING context's id, not `user.id`: the platform's own user id and
|
|
49
|
+
* the authenticated identity are different values, and the memory server keys its
|
|
50
|
+
* snapshot by the former. Using the wrong one writes to a key nobody reads, silently.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* `conversationId` and `chatId` are the AUDIO LANE's keys, not decoration. A duplex voice node
|
|
54
|
+
* registers its microphone handler and sends the model's audio against the conversation, so
|
|
55
|
+
* without them the lane silently matches nothing: every frame in is dropped by an id comparison
|
|
56
|
+
* that cannot succeed, and every frame out goes to a conversation nobody is listening on. That
|
|
57
|
+
* failed with no error at all, which is why they are first-class here rather than read ad hoc.
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* `nodeId` is WHICH INSTANCE on the canvas, and it is here because two different things are
|
|
61
|
+
* keyed by it and neither can be derived from anything else in scope.
|
|
62
|
+
*
|
|
63
|
+
* A content-addressed id mixes it in, so two Code nodes computing the same value in the same
|
|
64
|
+
* workflow still mint different universal ids — which is the point, since they are different
|
|
65
|
+
* places in the graph. And the saved-context hash stores one field per node, so a later
|
|
66
|
+
* template reaches `saved.<nodeId>`. Without it both fall back to a constant and collide
|
|
67
|
+
* silently: the ids look stable while describing the wrong node.
|
|
68
|
+
*/
|
|
69
|
+
/**
|
|
70
|
+
* `platformUrl` is THE PLATFORM'S OWN API, for the nodes that call US rather than a vendor.
|
|
71
|
+
*
|
|
72
|
+
* Most nodes reach a third party at a host they can name in `allowedHosts`. A growing few reach the
|
|
73
|
+
* engine instead — spatial ingestion hands raw text to the content pipeline, spatial search queries
|
|
74
|
+
* the dictionary — and that host is a DEPLOYMENT fact: localhost in dev, a service name in a cluster,
|
|
75
|
+
* a domain in production. A manifest cannot derive it and must never guess it.
|
|
76
|
+
*
|
|
77
|
+
* DERIVED from `UNOVERSE_RUNTIME_PORT`, which docker-compose already sets and the server already
|
|
78
|
+
* reads — a node runs INSIDE the service, so it is calling itself and needs no configuration to learn
|
|
79
|
+
* its own address. Loopback, because :4106 is the ungated internal listener and is published to
|
|
80
|
+
* 127.0.0.1 only.
|
|
81
|
+
*/
|
|
82
|
+
scope: { userId?: string; workflowId?: string; conversationId?: string; chatId?: string; executionId?: string; nodeId?: string; platformUrl?: string; engineUrl?: string };
|
|
83
|
+
/**
|
|
84
|
+
* THE CONVERSATION SO FAR, for a tool loop against a vendor with no chain id.
|
|
85
|
+
*
|
|
86
|
+
* Empty except inside `runToolLoop`, and empty there on the first turn. It grows by the assistant's
|
|
87
|
+
* tool-call turn and one message per result, and a manifest spreads it into its own message array:
|
|
88
|
+
*
|
|
89
|
+
* messages: [{ role: 'system', … }, { role: 'user', … }, ...transcript]
|
|
90
|
+
*
|
|
91
|
+
* It exists because Chat Completions — GLM, Grok, most OpenAI-COMPATIBLE vendors — resends the whole
|
|
92
|
+
* history every turn, where the Responses API hands back a `previous_response_id` and the next turn
|
|
93
|
+
* just names it. `continuity` models the latter; this models the former. The SHAPE of a message stays
|
|
94
|
+
* in the manifest, because it is the vendor's, not ours.
|
|
95
|
+
*/
|
|
96
|
+
transcript: unknown[];
|
|
97
|
+
/**
|
|
98
|
+
* The skills in force, rendered for a system prompt. Empty except inside `runToolLoop`,
|
|
99
|
+
* and empty there until a search resolves the conversation into a region that has one.
|
|
100
|
+
* Read by a manifest as `{{ skillsText }}`, the same way `transcript` is spread into a
|
|
101
|
+
* message array: the platform decides WHICH skills apply, the manifest decides where the
|
|
102
|
+
* vendor wants them.
|
|
103
|
+
*/
|
|
104
|
+
skillsText: string;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* What an OAuth2 exchange returned, for the call it authenticated.
|
|
108
|
+
*
|
|
109
|
+
* Some vendors tell you WHERE to talk as well as how: Salesforce returns `instance_url`
|
|
110
|
+
* and the org's data lives there, not at the login host you authenticated against. Only
|
|
111
|
+
* populated for `oauth2ClientCredentials`, and never carries the token itself.
|
|
112
|
+
*/
|
|
113
|
+
token: { instanceUrl?: string };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function emptyContext(partial: Partial<RunContext> = {}): RunContext {
|
|
117
|
+
return { config: {}, credentials: {}, signal: {}, prompt: {}, services: {}, params: {}, calls: {}, user: {}, scope: {}, token: {}, transcript: [], skillsText: "", ...partial };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Config values a resolver rewrites just before the request is built. */
|
|
121
|
+
export const RESOLVERS: Record<string, (value: any, node: ComposedNode) => any> = {
|
|
122
|
+
// Migration of shared/models.ts resolveModel(): a saved workflow stores a concrete
|
|
123
|
+
// model id, and if that id is retired this falls back to the current best model in
|
|
124
|
+
// the same tier. Without it, replacing a model generation breaks every saved
|
|
125
|
+
// workflow that named an old id.
|
|
126
|
+
modelTier(value: any, node: ComposedNode) {
|
|
127
|
+
const field = node.definition.configSchema?.properties?.model;
|
|
128
|
+
const options: string[] = field?.enum ?? [];
|
|
129
|
+
if (!value || options.includes(value)) return value || options[0];
|
|
130
|
+
const m = String(value).toLowerCase();
|
|
131
|
+
// Tier words cover both families the packages ship: GPT (luna/terra) and Claude
|
|
132
|
+
// (haiku/sonnet). Anything unrecognised lands on large, the safe over-provision.
|
|
133
|
+
const tier = /luna|mini|small|nano|haiku/.test(m) ? 0 : /terra|balanced|medium|sonnet/.test(m) ? 1 : 2;
|
|
134
|
+
// Options are listed large first, so index from the end for small.
|
|
135
|
+
const picked = [options[options.length - 1], options[1], options[0]][tier] ?? options[0];
|
|
136
|
+
console.warn(`[manifests] model "${value}" is no longer available — using "${picked}". Update this node.`);
|
|
137
|
+
return picked;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** Apply every declared resolver to the config before templates see it. */
|
|
142
|
+
export function applyResolvers(config: Record<string, any>, node: ComposedNode): Record<string, any> {
|
|
143
|
+
const props = node.definition.configSchema?.properties ?? {};
|
|
144
|
+
const out = { ...config };
|
|
145
|
+
for (const [name, spec] of Object.entries<any>(props)) {
|
|
146
|
+
const fn = spec?.resolve ? RESOLVERS[spec.resolve] : null;
|
|
147
|
+
if (fn) out[name] = fn(out[name], node);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|