@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,65 @@
1
+ /**
2
+ * THE WAY BACK TO A CONNECTED PERSON. Injected, never imported.
3
+ *
4
+ * A running node sometimes has to reach the browser that is waiting on it: a realtime node
5
+ * streams audio down, and a component node pushes a rendered piece of interface. Both are
6
+ * genuinely needed, and neither belongs in this package.
7
+ *
8
+ * The reason is not taste. The audio path is a uWebSockets socket and the component path is
9
+ * an MCP stream with its own session registry: both are the SERVER's connections, owned by
10
+ * the process that accepted them. This package reached across and imported them directly,
11
+ * which meant it could not be installed anywhere else — the imports pointed at a folder that
12
+ * only exists in that one repo.
13
+ *
14
+ * So it is inverted. This file says WHAT the runtime needs, in four functions; the server
15
+ * hands in HOW at boot, exactly as it already does for Redis (`setRedisClient`).
16
+ *
17
+ * DEFAULTS ARE NO-OPS, deliberately. A box that runs nodes with nobody watching (a cron, a
18
+ * queue worker, a test) has no client to push to, and that is a normal state rather than a
19
+ * failure. A node that emits audio into a run nobody is watching should carry on, not throw.
20
+ */
21
+
22
+ export interface ClientTransport {
23
+ /**
24
+ * Push a message to a client over the data plane. Rendered components travel this way.
25
+ * Keyed by userId AND conversationId, because one person may have several open.
26
+ * Returns false when nobody is listening.
27
+ */
28
+ pushToClient(userId: string, conversationId: string, message: Record<string, any>): boolean;
29
+
30
+ /** Stream audio bytes down to a connected client. False when nobody is listening. */
31
+ sendAudioToClient(conversationId: string, audioData: Buffer | ArrayBuffer): boolean;
32
+
33
+ /** Tell a client the audio session changed state (started, ended, interrupted). */
34
+ sendAudioStateToClient(conversationId: string, state: string, metadata?: Record<string, unknown>): boolean;
35
+
36
+ /** Register the handler for audio arriving FROM a client. */
37
+ setAudioDataHandler(handler: (conversationId: string, audioData: ArrayBuffer) => Promise<void>): void;
38
+
39
+ /** Register the handler for audio control messages from a client. */
40
+ setAudioControlHandler(handler: (conversationId: string, message: Record<string, unknown>) => Promise<void>): void;
41
+ }
42
+
43
+ /**
44
+ * `false` from the send functions, not `true`: with no transport there IS no listener, and
45
+ * saying otherwise would have a caller believe audio reached someone.
46
+ */
47
+ const NONE: ClientTransport = {
48
+ pushToClient: () => false,
49
+ sendAudioToClient: () => false,
50
+ sendAudioStateToClient: () => false,
51
+ setAudioDataHandler: () => {},
52
+ setAudioControlHandler: () => {},
53
+ };
54
+
55
+ let transport: ClientTransport = NONE;
56
+
57
+ /** Called once by the host at boot, with its own websocket and data-plane implementations. */
58
+ export function setClientTransport(impl: Partial<ClientTransport>): void {
59
+ transport = { ...NONE, ...impl };
60
+ }
61
+
62
+ /** The transport in force. Never null, so no caller has to guard. */
63
+ export function clientTransport(): ClientTransport {
64
+ return transport;
65
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Execution Context Builder
3
+ * Builds the execution context for node execution in node-service
4
+ */
5
+
6
+ import { clientTransport } from "./clientTransport.js";
7
+ import {
8
+ callServiceViaWorkflow,
9
+ saveTokenUsageToWorkflow,
10
+ saveMCPTraceToWorkflow,
11
+ deliverToNodeInWorkflow,
12
+ } from "./serviceCalls.js";
13
+ import { getRedisClient } from "./redis.js";
14
+ import { getDistributedAudioManager } from "./audioManager.js";
15
+
16
+ /**
17
+ * Build execution context for a node
18
+ */
19
+ export function buildExecutionContext(nodeType: string, inputs: any, config: any, context: any) {
20
+ const credentials = context.credentials || {};
21
+ const publishingContext = context.publishingContext;
22
+ const serviceNodeId = context.serviceNodeId;
23
+
24
+ // Extract workflow variables - they come from context.variables (passed from CallbackNodeActor)
25
+ const workflowVariables = context.variables || {};
26
+
27
+ // Build execution context first so we can reference it in callService
28
+ const executionContext: any = {
29
+ nodeId: context.nodeId || `node-service-${Date.now()}`,
30
+ executionId: context.executionId || `exec-${Date.now()}`,
31
+ workflowId: context.workflowId || "node-service",
32
+ nodeType,
33
+ config,
34
+ inputs,
35
+ credentials,
36
+ serviceNodeId,
37
+ // Preserve workflow definition for service calls (node lookups) AND add variables for MCP discovery
38
+ workflow: context.workflow
39
+ ? { ...context.workflow, variables: workflowVariables }
40
+ : {
41
+ id: context.workflowId || "node-service",
42
+ runId: context.executionId || `exec-${Date.now()}`,
43
+ variables: workflowVariables,
44
+ },
45
+ publishingContext,
46
+ activeLoop: context.activeLoop,
47
+ // The caller's disconnect, when the node runs over a stream (routeExecuteStream). A
48
+ // turn loop checks it between turns so a reaped run stops spending model calls.
49
+ abortSignal: context.abortSignal,
50
+ logger: console,
51
+ auth: context.auth,
52
+ };
53
+
54
+ // Build API with callService that routes to workflow-service
55
+ executionContext.api = {
56
+ gravityPublish: async () => {},
57
+ createLogger: (name: string) => {
58
+ const log = (level: string, ...args: any[]) => {
59
+ const msg = args.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))).join(" ");
60
+ console.log(JSON.stringify({ level, nodeType: name, msg, service: "node-service" }));
61
+ };
62
+ return {
63
+ info: (...args: any[]) => log("info", ...args),
64
+ error: (...args: any[]) => log("error", ...args),
65
+ debug: (...args: any[]) => log("debug", ...args),
66
+ warn: (...args: any[]) => log("warn", ...args),
67
+ };
68
+ },
69
+ getNodeCredentials: async (ctx: any, credentialName: string) => {
70
+ return credentials[credentialName] || {};
71
+ },
72
+ saveTokenUsage: saveTokenUsageToWorkflow,
73
+ saveMCPTrace: saveMCPTraceToWorkflow,
74
+ /**
75
+ * THE `send:` BRIDGE, and it belongs on THIS api for the reason the audio-lane note below
76
+ * spells out: a manifest node has only what is on `executionContext.api`. `pluginAPI.ts`
77
+ * exposing it is not enough — that is the api a PLUGIN receives at setup, and LoopEnd runs
78
+ * here, in the node host, reached remotely by the engine.
79
+ *
80
+ * Same crossing as `saveTokenUsage` above: the work happens in the workflow service, so the
81
+ * call goes over the same door. It THROWS on failure rather than logging, because a dropped
82
+ * delivery is a loop that stops after one pass and reports success.
83
+ */
84
+ deliverToNode: async (executionId: string, nodeId: string, handle: string, value: unknown, signal?: string) =>
85
+ deliverToNodeInWorkflow({ executionId, nodeId, handle, value, signal }),
86
+ getRedisClient,
87
+ /**
88
+ * THE AUDIO LANE, and its absence here was a total, silent failure of every voice node.
89
+ *
90
+ * A live voice node registers its microphone handler and sends the model's speech through this
91
+ * handle. It was missing from THIS api — the one the runtime builds per execution — while
92
+ * `pluginAPI.ts` exposed it on the api a PLUGIN receives at setup. A code node reached the lane
93
+ * through plugin-base's platform singleton and never noticed; a manifest node has only what is
94
+ * on `executionContext.api`, so it got `undefined`, took the no-lane path, and ran a perfectly
95
+ * healthy conversation that nobody could hear or speak to. No error, in either direction.
96
+ *
97
+ * Same accessor `pluginAPI.ts` uses, so both paths reach one manager rather than two.
98
+ */
99
+ getAudioWebSocketManager: () => getDistributedAudioManager(),
100
+ callService: async (method: string, params: any) => {
101
+ return callServiceViaWorkflow(method, params, executionContext);
102
+ },
103
+ getWebSocketManager: () => {
104
+ if (!publishingContext) {
105
+ return null;
106
+ }
107
+ return {
108
+ get: (userId: string, conversationId: string) => ({
109
+ mountedComponents: new Set<string>(),
110
+ currentTemplate: null as string | null,
111
+ send: (data: string) => {
112
+ const message = JSON.parse(data);
113
+ // §5/§5a data plane — components/data + template state go ONLY to the native MCP
114
+ // SSE stream. The legacy WS is AUDIO-ONLY now (the SDK reads components from MCP).
115
+ clientTransport().pushToClient(userId, conversationId, message);
116
+ },
117
+ }),
118
+ };
119
+ },
120
+ publishStreamingUpdate: (streamConfig: { componentType: string; props: Record<string, any> }) => {
121
+ if (!publishingContext) {
122
+ return;
123
+ }
124
+ const { chatId, conversationId, userId } = publishingContext;
125
+ const nodeId = context.nodeId;
126
+ const workflowId = context.workflowId;
127
+
128
+ const componentKey = `${chatId}_${nodeId}`;
129
+ const event = {
130
+ id: `stream_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
131
+ timestamp: new Date().toISOString(),
132
+ type: "COMPONENT_INIT",
133
+ nodeId,
134
+ chatId,
135
+ conversationId,
136
+ userId,
137
+ providerId: "streaming",
138
+ component: {
139
+ type: streamConfig.componentType,
140
+ // §4a: address the component on the Unoverse definition server, not a legacy
141
+ // `/components/<x>.js` bundle. The SDK resolves it by type over MCP.
142
+ componentUrl: `unoverse://components/${streamConfig.componentType}`,
143
+ props: streamConfig.props,
144
+ },
145
+ metadata: {
146
+ workflowId,
147
+ componentKey,
148
+ streaming: true,
149
+ },
150
+ };
151
+
152
+ clientTransport().pushToClient(userId, conversationId, event);
153
+ },
154
+ };
155
+
156
+ return executionContext;
157
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Platform Module
3
+ * Re-exports all platform functionality for node-service
4
+ */
5
+
6
+ export { getDistributedAudioManager } from "./audioManager.js";
7
+ export { callServiceViaWorkflow, saveTokenUsageToWorkflow } from "./serviceCalls.js";
8
+ export { buildExecutionContext } from "./executionContext.js";
9
+ export { createNodeServiceAPI } from "./pluginAPI.js";
10
+ export { setRedisClient, getRedisClient } from "./redis.js";
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Plugin API Factory
3
+ * Creates the minimal Plugin API for node-service plugin registration
4
+ */
5
+
6
+ import { PromiseNode, CallbackNode, NodeInputType } from "../pluginBase.js";
7
+ import { getNodeRegistry, registerCredentialType } from "../registry.js";
8
+ import { getDistributedAudioManager } from "./audioManager.js";
9
+ import {
10
+ saveTokenUsageToWorkflow,
11
+ saveMCPTraceToWorkflow,
12
+ emitNodeOutputToWorkflow,
13
+ deliverToNodeInWorkflow,
14
+ } from "./serviceCalls.js";
15
+ import { getRedisClient } from "./redis.js";
16
+ import { boot } from "../boot.js";
17
+
18
+ /**
19
+ * Create a minimal Plugin API for node-service
20
+ * Collects registered nodes without needing full platform dependencies
21
+ */
22
+ export function createNodeServiceAPI() {
23
+ const nodeRegistry = getNodeRegistry();
24
+
25
+ return {
26
+ registerNode(node: any) {
27
+ const def = node.definition;
28
+ const executor = node.executor;
29
+
30
+ // Determine execution mode by checking if executor has initializeState
31
+ let executionMode: "single" | "generator" = "single";
32
+ try {
33
+ const proto = executor.prototype;
34
+ if (proto && typeof proto.initializeState === "function") {
35
+ executionMode = "generator";
36
+ }
37
+ } catch (e) {}
38
+
39
+ nodeRegistry.set(def.type, {
40
+ type: def.type,
41
+ name: def.name,
42
+ description: def.description || "",
43
+ // whenToUse is the field the catalog ranker (CatalogService) embeds for
44
+ // semantic node search. Omitting it here drops it for EVERY node at
45
+ // registration — Redis stores "", ranking falls back to description, and
46
+ // discovery degrades platform-wide. It must survive into the registry.
47
+ whenToUse: def.whenToUse || "",
48
+ category: def.category || "general",
49
+ version: def.version || "1.0.0",
50
+ configSchema: def.configSchema || {},
51
+ // The authored settings-form order. Must survive into the registry for the same
52
+ // reason whenToUse must: dropped here means dropped for every node platform-wide,
53
+ // and the canvas falls back to schema declaration order.
54
+ "ui:order": def["ui:order"] || null,
55
+ inputs: def.inputs || [],
56
+ outputs: def.outputs || [],
57
+ color: def.color || "#6366f1",
58
+ logoUrl: def.logoUrl || null,
59
+ template: def.template || null,
60
+ nodeSize: def.nodeSize || { width: 200, height: 100 },
61
+ isService: def.isService || false,
62
+ serviceConnectors: def.serviceConnectors || null,
63
+ componentTemplate: def.componentTemplate || null,
64
+ // Per-component render-URL map for the generic `Component` node (org-aware).
65
+ // The Canvas resolves an instance's URL from config.component against this.
66
+ componentUrls: def.componentUrls || null,
67
+ credentials: def.credentials || [],
68
+ testData: def.testData || null,
69
+ // Author-declared capabilities (e.g. cacheable) must survive into the
70
+ // registry so the engine's memoization gate can read them — without this
71
+ // the field is lost at load and isCacheable() is always false.
72
+ capabilities: def.capabilities || null,
73
+ executor,
74
+ executionMode,
75
+ });
76
+
77
+ boot.node(def.type);
78
+ },
79
+ registerService: () => {},
80
+ registerCredential: (credentialType: any) => {
81
+ registerCredentialType(credentialType);
82
+ },
83
+ registerComponentPath: () => {},
84
+ createLogger: () => console,
85
+ getConfig: () => ({}),
86
+ saveTokenUsage: saveTokenUsageToWorkflow,
87
+ saveMCPTrace: saveMCPTraceToWorkflow,
88
+ getNodeCredentials: async (context: any, credentialName: string) => {
89
+ const cred = context?.credentials?.[credentialName];
90
+ if (!cred) {
91
+ console.error(
92
+ `[node-service] Credential ${credentialName} not found. Available: ${Object.keys(
93
+ context?.credentials || {},
94
+ ).join(", ")}`,
95
+ );
96
+ }
97
+ return cred || null;
98
+ },
99
+ callService: async () => ({}),
100
+ getRedisClient,
101
+ gravityPublish: async () => {},
102
+ getAudioWebSocketManager: () => getDistributedAudioManager(),
103
+ getWebSocketManager: () => null,
104
+ executeNodeWithRouting: async (
105
+ executeNode: (inputs: any, config: any, context: any) => Promise<any>,
106
+ params: any,
107
+ config: any,
108
+ context: any,
109
+ ) => {
110
+ // Run the node locally (package hybrid nodes run in node-service).
111
+ const result = await executeNode(params, config, context);
112
+
113
+ // Bridge to workflow-service so the xstate actor emits NODE_OUTPUT
114
+ // and downstream nodes fire. Same contract as the in-process
115
+ // executeNodeWithRouting in workflow-service.
116
+ const executionId = context?.executionId;
117
+ const nodeId = context?.nodeId;
118
+ if (executionId && nodeId) {
119
+ await emitNodeOutputToWorkflow({ executionId, nodeId, output: result });
120
+ } else {
121
+ console.warn(
122
+ "[node-service] executeNodeWithRouting: missing executionId or nodeId — NODE_OUTPUT not emitted",
123
+ );
124
+ }
125
+
126
+ return result;
127
+ },
128
+ /**
129
+ * The `send:` row's bridge for nodes running in THIS host (every manifest node). Mirrors
130
+ * executeNodeWithRouting above: the work happens in the workflow service, reached over the
131
+ * same door. `executionId` is the SENDER's own, so a node can only address nodes in the
132
+ * workflow it is running in.
133
+ */
134
+ deliverToNode: async (executionId: string, nodeId: string, handle: string, value: unknown, signal?: string) => {
135
+ await deliverToNodeInWorkflow({ executionId, nodeId, handle, value, signal });
136
+ },
137
+ classes: {
138
+ PromiseNode,
139
+ CallbackNode,
140
+ },
141
+ types: {
142
+ NodeInputType,
143
+ NodeConcurrency: {},
144
+ },
145
+ };
146
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Redis Client Management
3
+ */
4
+
5
+ import { Redis } from "ioredis";
6
+ import { boot } from "../boot.js";
7
+
8
+ let redisClient: Redis | null = null;
9
+
10
+ export function setRedisClient(client: Redis): void {
11
+ redisClient = client;
12
+ }
13
+
14
+ export function getRedisClient(): Redis | null {
15
+ return redisClient;
16
+ }
17
+
18
+ /**
19
+ * Initialise the runtime's shared Redis command client from env, once, at boot.
20
+ *
21
+ * Node execution reaches Redis through `executionContext.api.getRedisClient()`
22
+ * (see executionContext.ts) — used e.g. by the OpenAI/ChatGPT agents to persist
23
+ * per-conversation state (`openai:conv:*`, `previous_response_id`) so multi-turn
24
+ * history survives across turns. Without this, getRedisClient() returns null,
25
+ * the agents log `hasRedis=false`, and every turn starts a NEW conversation.
26
+ *
27
+ * This is a normal command connection — distinct from the pub/sub subscriber in
28
+ * lifecycleBridge.ts (an ioredis connection in subscriber mode can't run GET/SETEX).
29
+ */
30
+ export function initRuntimeRedis(): void {
31
+ if (redisClient) return; // already initialised
32
+ const host = process.env.REDIS_HOST;
33
+ const port = process.env.REDIS_PORT;
34
+ if (!host || !port) {
35
+ console.warn(
36
+ "[unoverse:runtime] REDIS_HOST/REDIS_PORT not configured — node runtime Redis disabled (agent multi-turn history will not persist)",
37
+ );
38
+ return;
39
+ }
40
+
41
+ const client = new Redis({
42
+ host,
43
+ port: parseInt(port, 10),
44
+ password: process.env.REDIS_PASSWORD || undefined,
45
+ tls: process.env.REDIS_TLS === "true" ? {} : undefined,
46
+ retryStrategy: (times: number) => Math.min(times * 100, 3000),
47
+ });
48
+ client.on("error", (err: Error) => {
49
+ console.warn(`[unoverse:runtime] Redis client error: ${err.message}`);
50
+ });
51
+
52
+ setRedisClient(client);
53
+ boot.redis("connected, agent multi-turn history enabled");
54
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Service Calls
3
+ * HTTP calls to workflow-service for core node services and analytics
4
+ */
5
+
6
+ import { boot } from "../boot.js";
7
+
8
+ const WORKFLOW_SERVICE_URL = process.env.WORKFLOW_SERVICE_URL || "http://localhost:4101";
9
+
10
+ /**
11
+ * Call a service on a core node via workflow service
12
+ * This enables package nodes (node-service) to call core node services (workflow-service)
13
+ * Example: OpenAIStream calls GetActiveMCPs.getSchema
14
+ */
15
+ export async function callServiceViaWorkflow(method: string, params: any, executionContext: any): Promise<any> {
16
+ // No wired provider is NOT a reason to skip: workflow-service serves platform
17
+ // tools (memory toolsets gated by workflow memory_config) and re-routes methods
18
+ // to their real owner by name. The sentinel just satisfies the nodeId field;
19
+ // calls that nothing can serve fail server-side and return {} as before.
20
+ const serviceNodeId = executionContext?.serviceNodeId || "__platform";
21
+
22
+ try {
23
+ const response = await fetch(`${WORKFLOW_SERVICE_URL}/service-call`, {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json" },
26
+ body: JSON.stringify({
27
+ nodeId: serviceNodeId,
28
+ method,
29
+ params,
30
+ context: {
31
+ workflowId: executionContext?.workflowId,
32
+ executionId: executionContext?.executionId,
33
+ nodeId: executionContext?.nodeId,
34
+ credentials: executionContext?.credentials,
35
+ workflow: executionContext?.workflow,
36
+ auth: executionContext?.auth,
37
+ publishingContext: executionContext?.publishingContext,
38
+ },
39
+ }),
40
+ });
41
+
42
+ if (!response.ok) {
43
+ const errorText = await response.text();
44
+ console.error(`[node-service] callService failed: ${response.status} ${errorText}`);
45
+ return {};
46
+ }
47
+
48
+ return await response.json();
49
+ } catch (error: any) {
50
+ console.error("[node-service] callService error:", error.message);
51
+ return {};
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Save token usage by calling workflow service
57
+ */
58
+ export async function saveTokenUsageToWorkflow(usage: any): Promise<void> {
59
+ try {
60
+ const response = await fetch(`${WORKFLOW_SERVICE_URL}/analytics/tokens`, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify(usage),
64
+ });
65
+ if (!response.ok) {
66
+ console.error(`[node-service] Failed to save token usage: ${response.status}`);
67
+ }
68
+ } catch (error) {
69
+ console.error("[node-service] Error saving token usage:", error);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Emit NODE_OUTPUT to workflow service so downstream nodes fire.
75
+ * Used by package hybrid nodes (SmartDocument etc.) whose xstate actor
76
+ * lives in the workflow-service process.
77
+ */
78
+ export async function emitNodeOutputToWorkflow(payload: {
79
+ executionId: string;
80
+ nodeId: string;
81
+ output: any;
82
+ }): Promise<void> {
83
+ try {
84
+ const response = await fetch(`${WORKFLOW_SERVICE_URL}/emit-node-output`, {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify(payload),
88
+ });
89
+ if (!response.ok) {
90
+ console.error(`[node-service] emitNodeOutput failed: ${response.status}`);
91
+ }
92
+ } catch (error: any) {
93
+ console.error("[node-service] emitNodeOutput error:", error.message);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Hand a value to a NAMED node in the caller's own run, with no edge between them.
99
+ *
100
+ * The out-of-process half of the manifest `send:` row: manifest nodes execute in this host, so a
101
+ * send has to cross to the workflow service the same way a node output does.
102
+ *
103
+ * THROWS, unlike `emitNodeOutputToWorkflow` above, and the difference is deliberate. A lost node
104
+ * output costs a render. A lost delivery costs a loop its next pass, and the loop then reports
105
+ * success having processed one item — the exact silent failure this mechanism replaced. Better a
106
+ * stopped run than a wrong answer.
107
+ */
108
+ export async function deliverToNodeInWorkflow(payload: {
109
+ executionId: string;
110
+ nodeId: string;
111
+ handle: string;
112
+ value: any;
113
+ signal?: string;
114
+ }): Promise<void> {
115
+ let response: Response;
116
+ try {
117
+ response = await fetch(`${WORKFLOW_SERVICE_URL}/deliver-to-node`, {
118
+ method: "POST",
119
+ headers: { "Content-Type": "application/json" },
120
+ body: JSON.stringify(payload),
121
+ });
122
+ } catch (error: any) {
123
+ throw new Error(
124
+ `deliverToNode: could not reach the workflow service to deliver "${payload.nodeId}.${payload.handle}" — ${error.message}`,
125
+ );
126
+ }
127
+ if (!response.ok) {
128
+ throw new Error(
129
+ `deliverToNode: the workflow service refused delivery of "${payload.nodeId}.${payload.handle}" (${response.status})`,
130
+ );
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Save MCP trace by calling workflow service
136
+ */
137
+ export async function saveMCPTraceToWorkflow(trace: any): Promise<string> {
138
+ try {
139
+ const response = await fetch(`${WORKFLOW_SERVICE_URL}/analytics/mcp-trace`, {
140
+ method: "POST",
141
+ headers: { "Content-Type": "application/json" },
142
+ body: JSON.stringify(trace),
143
+ });
144
+ if (!response.ok) {
145
+ console.error(`[node-service] Failed to save MCP trace: ${response.status}`);
146
+ return "";
147
+ }
148
+ const result = (await response.json()) as { traceId?: string };
149
+ return result.traceId || "";
150
+ } catch (error) {
151
+ console.error("[node-service] Error saving MCP trace:", error);
152
+ return "";
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Persist plugin state to PostgreSQL via workflow-service
158
+ */
159
+ export async function persistPluginState(plugin: {
160
+ name: string;
161
+ version: string;
162
+ source: "local" | "npm";
163
+ enabled: boolean;
164
+ metadata?: any;
165
+ }): Promise<void> {
166
+ try {
167
+ const response = await fetch(`${WORKFLOW_SERVICE_URL}/plugins/state`, {
168
+ method: "POST",
169
+ headers: { "Content-Type": "application/json" },
170
+ body: JSON.stringify(plugin),
171
+ });
172
+ if (!response.ok) {
173
+ console.error(`[node-service] Failed to persist plugin state: ${response.status}`);
174
+ }
175
+ } catch (error: any) {
176
+ console.error("[node-service] Error persisting plugin state:", error.message);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Remove plugin state from PostgreSQL (on uninstall)
182
+ */
183
+ export async function removePluginState(name: string): Promise<void> {
184
+ try {
185
+ const response = await fetch(
186
+ `${WORKFLOW_SERVICE_URL}/plugins/state/${encodeURIComponent(name)}`,
187
+ { method: "DELETE", headers: { "Content-Type": "application/json" } },
188
+ );
189
+ if (!response.ok) {
190
+ console.error(`[node-service] Failed to remove plugin state: ${response.status}`);
191
+ }
192
+ } catch (error: any) {
193
+ console.error("[node-service] Error removing plugin state:", error.message);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Fetch all plugin state from PostgreSQL (for Redis recovery on startup)
199
+ */
200
+ export async function fetchPluginState(): Promise<Array<{
201
+ name: string;
202
+ version: string;
203
+ source: string;
204
+ enabled: boolean;
205
+ metadata: any;
206
+ }>> {
207
+ try {
208
+ const response = await fetch(`${WORKFLOW_SERVICE_URL}/plugins/state`);
209
+ if (!response.ok) return [];
210
+ const data: any = await response.json();
211
+ return data.plugins || [];
212
+ } catch (error: any) {
213
+ // EXPECTED once in merged mode: the runtime asks :4101 for plugin state before the
214
+ // in-process engine has bound it, and index.ts re-runs the whole load after the
215
+ // engine is up. Shouting about a failure that self-heals seconds later is how a log
216
+ // teaches people to ignore it. Remembered instead, and surfaced by the boot report
217
+ // only if the retry ALSO came back with nothing.
218
+ if (boot.isBooting()) boot.pluginStateFetchFailed(error.message);
219
+ else console.error("[node-service] Error fetching plugin state:", error.message);
220
+ return [];
221
+ }
222
+ }