@unoverse-platform/studio 0.1.89 → 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/screens/TemplatesLibraryView.tsx +31 -6
- package/screens/TemplatesView.tsx +1 -1
- 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,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,154 @@
|
|
|
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
|
+
logger: console,
|
|
48
|
+
auth: context.auth,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Build API with callService that routes to workflow-service
|
|
52
|
+
executionContext.api = {
|
|
53
|
+
gravityPublish: async () => {},
|
|
54
|
+
createLogger: (name: string) => {
|
|
55
|
+
const log = (level: string, ...args: any[]) => {
|
|
56
|
+
const msg = args.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))).join(" ");
|
|
57
|
+
console.log(JSON.stringify({ level, nodeType: name, msg, service: "node-service" }));
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
info: (...args: any[]) => log("info", ...args),
|
|
61
|
+
error: (...args: any[]) => log("error", ...args),
|
|
62
|
+
debug: (...args: any[]) => log("debug", ...args),
|
|
63
|
+
warn: (...args: any[]) => log("warn", ...args),
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
getNodeCredentials: async (ctx: any, credentialName: string) => {
|
|
67
|
+
return credentials[credentialName] || {};
|
|
68
|
+
},
|
|
69
|
+
saveTokenUsage: saveTokenUsageToWorkflow,
|
|
70
|
+
saveMCPTrace: saveMCPTraceToWorkflow,
|
|
71
|
+
/**
|
|
72
|
+
* THE `send:` BRIDGE, and it belongs on THIS api for the reason the audio-lane note below
|
|
73
|
+
* spells out: a manifest node has only what is on `executionContext.api`. `pluginAPI.ts`
|
|
74
|
+
* exposing it is not enough — that is the api a PLUGIN receives at setup, and LoopEnd runs
|
|
75
|
+
* here, in the node host, reached remotely by the engine.
|
|
76
|
+
*
|
|
77
|
+
* Same crossing as `saveTokenUsage` above: the work happens in the workflow service, so the
|
|
78
|
+
* call goes over the same door. It THROWS on failure rather than logging, because a dropped
|
|
79
|
+
* delivery is a loop that stops after one pass and reports success.
|
|
80
|
+
*/
|
|
81
|
+
deliverToNode: async (executionId: string, nodeId: string, handle: string, value: unknown, signal?: string) =>
|
|
82
|
+
deliverToNodeInWorkflow({ executionId, nodeId, handle, value, signal }),
|
|
83
|
+
getRedisClient,
|
|
84
|
+
/**
|
|
85
|
+
* THE AUDIO LANE, and its absence here was a total, silent failure of every voice node.
|
|
86
|
+
*
|
|
87
|
+
* A live voice node registers its microphone handler and sends the model's speech through this
|
|
88
|
+
* handle. It was missing from THIS api — the one the runtime builds per execution — while
|
|
89
|
+
* `pluginAPI.ts` exposed it on the api a PLUGIN receives at setup. A code node reached the lane
|
|
90
|
+
* through plugin-base's platform singleton and never noticed; a manifest node has only what is
|
|
91
|
+
* on `executionContext.api`, so it got `undefined`, took the no-lane path, and ran a perfectly
|
|
92
|
+
* healthy conversation that nobody could hear or speak to. No error, in either direction.
|
|
93
|
+
*
|
|
94
|
+
* Same accessor `pluginAPI.ts` uses, so both paths reach one manager rather than two.
|
|
95
|
+
*/
|
|
96
|
+
getAudioWebSocketManager: () => getDistributedAudioManager(),
|
|
97
|
+
callService: async (method: string, params: any) => {
|
|
98
|
+
return callServiceViaWorkflow(method, params, executionContext);
|
|
99
|
+
},
|
|
100
|
+
getWebSocketManager: () => {
|
|
101
|
+
if (!publishingContext) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
get: (userId: string, conversationId: string) => ({
|
|
106
|
+
mountedComponents: new Set<string>(),
|
|
107
|
+
currentTemplate: null as string | null,
|
|
108
|
+
send: (data: string) => {
|
|
109
|
+
const message = JSON.parse(data);
|
|
110
|
+
// §5/§5a data plane — components/data + template state go ONLY to the native MCP
|
|
111
|
+
// SSE stream. The legacy WS is AUDIO-ONLY now (the SDK reads components from MCP).
|
|
112
|
+
clientTransport().pushToClient(userId, conversationId, message);
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
publishStreamingUpdate: (streamConfig: { componentType: string; props: Record<string, any> }) => {
|
|
118
|
+
if (!publishingContext) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const { chatId, conversationId, userId } = publishingContext;
|
|
122
|
+
const nodeId = context.nodeId;
|
|
123
|
+
const workflowId = context.workflowId;
|
|
124
|
+
|
|
125
|
+
const componentKey = `${chatId}_${nodeId}`;
|
|
126
|
+
const event = {
|
|
127
|
+
id: `stream_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
type: "COMPONENT_INIT",
|
|
130
|
+
nodeId,
|
|
131
|
+
chatId,
|
|
132
|
+
conversationId,
|
|
133
|
+
userId,
|
|
134
|
+
providerId: "streaming",
|
|
135
|
+
component: {
|
|
136
|
+
type: streamConfig.componentType,
|
|
137
|
+
// §4a: address the component on the Unoverse definition server, not a legacy
|
|
138
|
+
// `/components/<x>.js` bundle. The SDK resolves it by type over MCP.
|
|
139
|
+
componentUrl: `unoverse://components/${streamConfig.componentType}`,
|
|
140
|
+
props: streamConfig.props,
|
|
141
|
+
},
|
|
142
|
+
metadata: {
|
|
143
|
+
workflowId,
|
|
144
|
+
componentKey,
|
|
145
|
+
streaming: true,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
clientTransport().pushToClient(userId, conversationId, event);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return executionContext;
|
|
154
|
+
}
|
|
@@ -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
|
+
}
|