@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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build what a manifest's templates and expressions may see.
|
|
3
|
+
*
|
|
4
|
+
* `config` arrives ALREADY RESOLVED: the engine runs its template resolver before it
|
|
5
|
+
* calls execute, so {{signal.x}} and {{prompt.y}} an author wrote in a config field are
|
|
6
|
+
* plain text by the time they reach here.
|
|
7
|
+
*
|
|
8
|
+
* `executionContext.auth.accessToken` — the CALLER'S JWT (AUTH_TOKEN_FLOW.md) — is
|
|
9
|
+
* DELIBERATELY WITHHELD, and must stay that way. A code node may reach it because a code
|
|
10
|
+
* node is bounded by provenance; a manifest is data that can arrive by paste or database
|
|
11
|
+
* row. The blast radii are not comparable: a third-party API key reaches that vendor
|
|
12
|
+
* account, while a platform JWT IS the user against our own services, so a pasted
|
|
13
|
+
* manifest could forward it to any host its allowedHosts list allows.
|
|
14
|
+
*
|
|
15
|
+
* `auth.user` — the signed-in person's email, id and name — IS exposed, as `user`. That
|
|
16
|
+
* is not a softening of the rule, it is the rule applied to a different thing: an email
|
|
17
|
+
* authenticates nothing, and it is the join key every CRM, support and account node needs.
|
|
18
|
+
* Withholding it would force those nodes to take identity off the WIRE, which is strictly
|
|
19
|
+
* worse: a caller could then ask for someone else's record. Never widen this to the token.
|
|
20
|
+
*
|
|
21
|
+
* The tool bridge DOES use the token, and that is not a contradiction: there the
|
|
22
|
+
* platform is calling its own internal /mcp on the user's behalf, rather than data
|
|
23
|
+
* deciding where a secret goes.
|
|
24
|
+
*
|
|
25
|
+
* EXPORTED ONLY FOR THE SIBLING FILES IN THIS FOLDER (the two executors and the re-fire).
|
|
26
|
+
* Nothing outside `executor/` may import it: every consumer of a RunContext outside this
|
|
27
|
+
* folder receives one already built, and a second builder is how the token rule would
|
|
28
|
+
* eventually be widened by accident. Pinned by manifest-chain.test.ts.
|
|
29
|
+
*/
|
|
30
|
+
import type { ComposedNode } from "../compose.js";
|
|
31
|
+
import { applyResolvers, emptyContext, type RunContext } from "../runtime/index.js";
|
|
32
|
+
|
|
33
|
+
export function contextFor(node: ComposedNode, inputs: any, config: any, executionContext: any): RunContext {
|
|
34
|
+
const user = executionContext?.auth?.user ?? {};
|
|
35
|
+
return emptyContext({
|
|
36
|
+
config: applyResolvers(config ?? {}, node),
|
|
37
|
+
// The bag holds EVERY credential in the workflow keyed by name. Selecting by name is
|
|
38
|
+
// the manifest's job, which is what stops a node authenticating with a neighbour's
|
|
39
|
+
// apiKey (04-credentials.md).
|
|
40
|
+
credentials: executionContext?.credentials ?? {},
|
|
41
|
+
signal: inputs ?? {},
|
|
42
|
+
services: executionContext?.services ?? {},
|
|
43
|
+
// Picked field by field, never spread: a future field on auth.user must be an explicit
|
|
44
|
+
// decision to expose, not something a manifest silently inherits.
|
|
45
|
+
user: { email: user.email, id: user.id ?? user.sub, name: user.name },
|
|
46
|
+
// The PLATFORM's ids, which is what state keys are built from. Same precedence the
|
|
47
|
+
// retired CRM code used, and it is not interchangeable with user.id above.
|
|
48
|
+
//
|
|
49
|
+
// `conversationId` and `chatId` are here because the AUDIO LANE is keyed by conversation.
|
|
50
|
+
// Their absence was a silent, total failure of the voice node: `runDuplexSession` fell back
|
|
51
|
+
// to the node TYPE as its key, so `setAudioDataHandler` compared the client's real session id
|
|
52
|
+
// against the string "OpenAIRealtimeVoice", dropped every microphone frame, and sent the
|
|
53
|
+
// model's audio to a conversation nobody was listening on. No error anywhere — just silence.
|
|
54
|
+
//
|
|
55
|
+
// Same precedence the retired node used: publishingContext first, then workflow variables.
|
|
56
|
+
scope: {
|
|
57
|
+
userId: executionContext?.publishingContext?.userId ?? executionContext?.workflow?.variables?.userId,
|
|
58
|
+
workflowId: executionContext?.workflowId ?? executionContext?.workflow?.id,
|
|
59
|
+
conversationId:
|
|
60
|
+
executionContext?.publishingContext?.conversationId ?? executionContext?.workflow?.variables?.conversationId,
|
|
61
|
+
chatId: executionContext?.publishingContext?.chatId ?? executionContext?.workflow?.variables?.chatId,
|
|
62
|
+
executionId: executionContext?.executionId,
|
|
63
|
+
// Which INSTANCE on the canvas. The retired Code node fell back to the literal "code" when
|
|
64
|
+
// this was missing, which meant two Code nodes minted the same universal id and the saved
|
|
65
|
+
// context stored them under one key. Passing it through rather than defaulting is what makes
|
|
66
|
+
// that failure impossible instead of merely unlikely.
|
|
67
|
+
nodeId: executionContext?.nodeId,
|
|
68
|
+
/**
|
|
69
|
+
* THE PLATFORM'S OWN API, for a node that calls us rather than a vendor.
|
|
70
|
+
*
|
|
71
|
+
* DERIVED, not configured. A node runs INSIDE the unoverse service, so it is calling itself —
|
|
72
|
+
* asking a developer to supply the platform's own address would be configuration for something
|
|
73
|
+
* the process already knows. `UNOVERSE_RUNTIME_PORT` is the internal listener and is already set
|
|
74
|
+
* in docker-compose; the server reads it the same way (`RUNTIME_PORT`), and already builds its own
|
|
75
|
+
* loopback url as `http://127.0.0.1:${port}` elsewhere.
|
|
76
|
+
*
|
|
77
|
+
* LOOPBACK, not a service name: :4106 is the UNGATED internal listener, published to 127.0.0.1
|
|
78
|
+
* only and never widened. Reaching it from anywhere else would be a mistake, so the address says
|
|
79
|
+
* so.
|
|
80
|
+
*
|
|
81
|
+
* The retired SpatialIngest read UNOVERSE_SERVICE_URL with a `|| "http://localhost:4106"`
|
|
82
|
+
* fallback — a variable that is set NOWHERE in this repo, so the fallback was the whole
|
|
83
|
+
* behaviour. Deriving it removes both the dead variable and the hardcoded string.
|
|
84
|
+
*/
|
|
85
|
+
platformUrl: `http://127.0.0.1:${process.env.UNOVERSE_RUNTIME_PORT ?? 4106}`,
|
|
86
|
+
/**
|
|
87
|
+
* THE ENGINE, on :4101, and it is NOT the same service as `platformUrl`.
|
|
88
|
+
*
|
|
89
|
+
* Two internal listeners, two different sets of routes, and picking the wrong one is a
|
|
90
|
+
* 404 rather than anything that reads like an address problem:
|
|
91
|
+
*
|
|
92
|
+
* :4106 the unoverse runtime — /service-call, /execute, /skills
|
|
93
|
+
* :4101 the engine — /spatial/search, /dictionary/entry, /workflows
|
|
94
|
+
*
|
|
95
|
+
* SpatialSearch needs BOTH, which is what makes the distinction worth a second field
|
|
96
|
+
* rather than a note: its searches are engine routes and its skill reads are runtime
|
|
97
|
+
* routes. The retired node had the same split and hid it in two different env vars
|
|
98
|
+
* (WORKFLOW_SERVICE_URL and UNOVERSE_SERVICE_URL), neither of which is set anywhere in
|
|
99
|
+
* this repo, so both fallbacks were the entire behaviour.
|
|
100
|
+
*
|
|
101
|
+
* Same derivation as above, and the same reasoning: loopback because these listeners
|
|
102
|
+
* are internal and never published, and derived so there is no dead variable to set.
|
|
103
|
+
*/
|
|
104
|
+
engineUrl: `http://127.0.0.1:${process.env.ENGINE_PORT ?? process.env.WORKFLOW_SERVICE_PORT ?? 4101}`,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TWO executors every manifest node shares.
|
|
3
|
+
*
|
|
4
|
+
* There are exactly two, not one per node. A node's identity, ports, config and
|
|
5
|
+
* selection text are DATA in the registry; the code that performs it is one of these
|
|
6
|
+
* classes. Which one is decided by `kind`, and the split exists only because the
|
|
7
|
+
* platform genuinely has two execution shapes: settle-once (PromiseNode) and
|
|
8
|
+
* emit-repeatedly (CallbackNode). The registry itself derives executionMode by
|
|
9
|
+
* checking whether the prototype has initializeState, so the shapes cannot be merged.
|
|
10
|
+
*
|
|
11
|
+
* They are STATELESS with respect to which node they are running. The manifest is
|
|
12
|
+
* looked up by nodeType at execution time rather than closed over at registration
|
|
13
|
+
* (lookup.ts), which is what keeps this at two classes instead of one per manifest. It
|
|
14
|
+
* also means a reload swaps behaviour with no re-registration: the class stays, the
|
|
15
|
+
* data moves.
|
|
16
|
+
*
|
|
17
|
+
* ONE FILE PER JOB:
|
|
18
|
+
*
|
|
19
|
+
* lookup.ts how an executor finds its manifest by node type
|
|
20
|
+
* context.ts what a MANIFEST may see — and the token it must never see
|
|
21
|
+
* session.ts what the executor hands to PLATFORM code (session, state, audio)
|
|
22
|
+
* toolBridge.ts the agent harness, bridged into the tool loop
|
|
23
|
+
* refire.ts the hybrid contract: a doc mutation re-fires the workflow channel
|
|
24
|
+
* promise.ts settle-once
|
|
25
|
+
* callback.ts emit-repeatedly
|
|
26
|
+
*
|
|
27
|
+
* `contextFor` is deliberately NOT re-exported here: everything outside this folder
|
|
28
|
+
* receives a RunContext already built, and a second builder is how the token rule would
|
|
29
|
+
* eventually be widened by accident.
|
|
30
|
+
*/
|
|
31
|
+
import type { ComposedNode } from "../compose.js";
|
|
32
|
+
import { ManifestPromiseExecutor } from "./promise.js";
|
|
33
|
+
import { ManifestCallbackExecutor } from "./callback.js";
|
|
34
|
+
|
|
35
|
+
export { setManifestLookup } from "./lookup.js";
|
|
36
|
+
export { ManifestPromiseExecutor } from "./promise.js";
|
|
37
|
+
export { ManifestCallbackExecutor } from "./callback.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The class a node of this kind registers.
|
|
41
|
+
*
|
|
42
|
+
* Returns one of the two shared classes. The registry stores the class and
|
|
43
|
+
* constructs it with the node type, so nothing here is per-manifest.
|
|
44
|
+
*/
|
|
45
|
+
export function executorForKind(kind: ComposedNode["kind"]): any {
|
|
46
|
+
return kind === "CallbackNode" ? ManifestCallbackExecutor : ManifestPromiseExecutor;
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOW AN EXECUTOR FINDS ITS MANIFEST. The two executor classes are stateless with respect
|
|
3
|
+
* to which node they are running: the manifest is looked up by nodeType at execution time
|
|
4
|
+
* rather than closed over at registration, which is what keeps the platform at two classes
|
|
5
|
+
* instead of one per manifest — and what makes a reload swap behaviour with no
|
|
6
|
+
* re-registration: the class stays, the data moves.
|
|
7
|
+
*/
|
|
8
|
+
import type { ComposedNode } from "../compose.js";
|
|
9
|
+
|
|
10
|
+
/** Set by the loader so both executors can resolve their manifest by node type. */
|
|
11
|
+
let lookup: (type: string) => ComposedNode | undefined = () => undefined;
|
|
12
|
+
|
|
13
|
+
export function setManifestLookup(fn: (type: string) => ComposedNode | undefined): void {
|
|
14
|
+
lookup = fn;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function manifestFor(type: string): ComposedNode {
|
|
18
|
+
const node = lookup(type);
|
|
19
|
+
if (!node) throw new Error(`No manifest loaded for node type "${type}" — it may have been unloaded mid-run.`);
|
|
20
|
+
return node;
|
|
21
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE MEMORY LANES — the two harness calls every legacy agent made and the manifest
|
|
3
|
+
* executor stopped making, which is how the memory system sat healthy-but-idle after the
|
|
4
|
+
* migration: a running memory server, an empty ingest stream, and toggles that had
|
|
5
|
+
* vanished from the UI because no manifest declared them.
|
|
6
|
+
*
|
|
7
|
+
* Wired at the EXECUTOR, not in any manifest, mirroring how the engine injects the
|
|
8
|
+
* goal-memory toolset from the same flags (PlatformMemoryCore): a node declares
|
|
9
|
+
* `enableUserMemory` / `enableAgentMemory` in its config and the platform does the rest.
|
|
10
|
+
* Both lanes self-gate on the toggle and on identity, so a node without the fields, a
|
|
11
|
+
* bench run, or a headless caller costs nothing.
|
|
12
|
+
*
|
|
13
|
+
* The implementations live in ../../agent-mcp (memoryContext.ts / memoryIngest.ts) —
|
|
14
|
+
* they moved there in the harness extraction and are TRANSCRIBED conventions:
|
|
15
|
+
* hydration prepends the user's context snapshot to the USER PROMPT (never the system
|
|
16
|
+
* prompt — facts that ride the request refresh every turn and survive prompt caching),
|
|
17
|
+
* and ingest saves original input + final answer, never tool chatter.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Prepend the user's memory context to config.prompt when the toggle is on. */
|
|
21
|
+
export async function hydrateMemory(config: any, executionContext: any): Promise<any> {
|
|
22
|
+
if (config?.enableUserMemory !== true || typeof config?.prompt !== "string") return config;
|
|
23
|
+
try {
|
|
24
|
+
const harness: any = await import("../../agent-mcp/index.js");
|
|
25
|
+
const prompt = await harness.hydratePromptWithUserMemory(config, executionContext, console, config.prompt);
|
|
26
|
+
return prompt === config.prompt ? config : { ...config, prompt };
|
|
27
|
+
} catch (e: any) {
|
|
28
|
+
// Non-blocking, exactly as the legacy hydration was: memory enriches a run, it
|
|
29
|
+
// never gates one.
|
|
30
|
+
console.log(`[manifests] user-memory hydration failed (non-blocking): ${e?.message ?? e}`);
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Save the completed turn (original input + final answer) to the memory server.
|
|
37
|
+
* Fire-and-forget; `ingestConversationTurn` itself gates on the toggle and identity.
|
|
38
|
+
* The answer is read off the node's own outputs — `text` is the settled connector on
|
|
39
|
+
* every agent family (the streamed `stream` connector carries the same final value).
|
|
40
|
+
*/
|
|
41
|
+
export async function ingestMemoryTurn(
|
|
42
|
+
nodeType: string,
|
|
43
|
+
config: any,
|
|
44
|
+
executionContext: any,
|
|
45
|
+
outputs: Record<string, unknown>,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
if (config?.enableUserMemory !== true) return;
|
|
48
|
+
const answer = outputs?.text ?? outputs?.stream ?? outputs?.output;
|
|
49
|
+
if (typeof answer !== "string" || !answer) return;
|
|
50
|
+
try {
|
|
51
|
+
const harness: any = await import("../../agent-mcp/index.js");
|
|
52
|
+
harness.ingestConversationTurn(
|
|
53
|
+
config,
|
|
54
|
+
executionContext,
|
|
55
|
+
{ userMessage: String(config?.prompt ?? ""), agentResponse: answer },
|
|
56
|
+
console,
|
|
57
|
+
nodeType,
|
|
58
|
+
);
|
|
59
|
+
} catch (e: any) {
|
|
60
|
+
console.log(`[manifests] memory ingest failed (non-blocking): ${e?.message ?? e}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Settle-once. One request, one result. */
|
|
2
|
+
import { performApi, performService } from "../runtime/index.js";
|
|
3
|
+
import { deliverSends } from "./send.js";
|
|
4
|
+
import { assertAuthorized } from "../runtime/authorize.js";
|
|
5
|
+
import { manifestFor } from "./lookup.js";
|
|
6
|
+
import { contextFor } from "./context.js";
|
|
7
|
+
import { sessionFor, stateStoreFor } from "./session.js";
|
|
8
|
+
import { refireAfterDocMutation } from "./refire.js";
|
|
9
|
+
import { toolBridgeFor } from "./toolBridge.js";
|
|
10
|
+
|
|
11
|
+
export class ManifestPromiseExecutor {
|
|
12
|
+
logger: any = console;
|
|
13
|
+
constructor(readonly nodeType: string) {}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The SERVICE channel: a method another node called over a service edge.
|
|
17
|
+
*
|
|
18
|
+
* Independent of the workflow channel. A service call returns a value to the caller
|
|
19
|
+
* and does NOT fire this node's output connectors; a graph trigger fires the outputs
|
|
20
|
+
* and does not answer a caller. The two never cross (08-mcp-services.md).
|
|
21
|
+
*/
|
|
22
|
+
async handleServiceCall(method: string, params: any, config: any, executionContext: any): Promise<unknown> {
|
|
23
|
+
const node = manifestFor(this.nodeType);
|
|
24
|
+
// The service channel is gated too, and it is the one that would be missed. A node
|
|
25
|
+
// reached over a service edge never fires its own connectors, so it is easy to think of
|
|
26
|
+
// as an internal detail of the caller. It is not: it runs this node's calls with this
|
|
27
|
+
// node's credentials, and leaving it open would make a service edge the way around a
|
|
28
|
+
// role rather than a way to reuse a capability.
|
|
29
|
+
assertAuthorized(node, executionContext, config);
|
|
30
|
+
const result = await performService(
|
|
31
|
+
node,
|
|
32
|
+
method,
|
|
33
|
+
params ?? {},
|
|
34
|
+
contextFor(node, {}, config, executionContext),
|
|
35
|
+
stateStoreFor(executionContext),
|
|
36
|
+
// The caller's live session, for a method that renders content cards. Assembled HERE
|
|
37
|
+
// because the token is deliberately absent from the manifest's context and must stay
|
|
38
|
+
// that way: the executor holds it and hands it to platform code, never to the manifest.
|
|
39
|
+
sessionFor(executionContext),
|
|
40
|
+
);
|
|
41
|
+
await refireAfterDocMutation(node, method, config, executionContext);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async execute(inputs: any, config: any, executionContext: any): Promise<{ __outputs: Record<string, unknown> }> {
|
|
46
|
+
const node = manifestFor(this.nodeType);
|
|
47
|
+
// BEFORE anything is built or sent, so a refused run costs no vendor request, no token,
|
|
48
|
+
// and no side effect. Authorization that runs after the call has gone out is an audit
|
|
49
|
+
// log, not a gate.
|
|
50
|
+
assertAuthorized(node, executionContext, config);
|
|
51
|
+
const { outputs, emissions } = await performApi(
|
|
52
|
+
node,
|
|
53
|
+
contextFor(node, inputs, config, executionContext),
|
|
54
|
+
() => {},
|
|
55
|
+
node.api?.toolExchange ? await toolBridgeFor(executionContext) : undefined,
|
|
56
|
+
stateStoreFor(executionContext),
|
|
57
|
+
// No audio lane on the promise path (a duplex session is always a CallbackNode).
|
|
58
|
+
undefined,
|
|
59
|
+
// The caller's live session, for a node that publishes template data. Assembled here
|
|
60
|
+
// for the same reason as the service channel's: the executor holds the identity and
|
|
61
|
+
// hands it to platform code, never to the manifest.
|
|
62
|
+
sessionFor(executionContext),
|
|
63
|
+
);
|
|
64
|
+
// Any `send` row addressed a NODE rather than a connector: deliver those before settling,
|
|
65
|
+
// so a loop's next pass is already queued when this node reports done.
|
|
66
|
+
await deliverSends(node, emissions, executionContext);
|
|
67
|
+
// Outputs go back TOP-LEVEL under __outputs; anything else never reaches a
|
|
68
|
+
// downstream node.
|
|
69
|
+
return { __outputs: outputs };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE HYBRID CONTRACT (Pattern C, 08-mcp-services.md): a service method that MUTATED the
|
|
3
|
+
* node's document re-fires the WORKFLOW channel, so a downstream renderer receives the
|
|
4
|
+
* fresh markdown without the agent doing anything. Read methods must not re-fire — the
|
|
5
|
+
* retired SmartDocument learned that live, when every outline() re-emitted the whole
|
|
6
|
+
* document and flooded the renderer.
|
|
7
|
+
*
|
|
8
|
+
* Detection is by the method's own call list naming a mutating docstore op, so the manifest
|
|
9
|
+
* cannot promise the contract and forget it: declaring the op IS declaring the re-fire.
|
|
10
|
+
*
|
|
11
|
+
* `executeNodeWithRouting` is the engine's own bridge (NodeExecutionUtils), reached through
|
|
12
|
+
* the plugin api exactly as the retired executor reached it via getPlatform(). Absent
|
|
13
|
+
* (tests, headless) the mutation still returns to the caller — the render is a side channel
|
|
14
|
+
* and failures here are logged, never thrown, exactly as the retired node treated them.
|
|
15
|
+
*/
|
|
16
|
+
import { performApi, MUTATING_DOC_OPS } from "../runtime/index.js";
|
|
17
|
+
import { contextFor } from "./context.js";
|
|
18
|
+
import { stateStoreFor } from "./session.js";
|
|
19
|
+
|
|
20
|
+
export async function refireAfterDocMutation(node: any, method: string, config: any, executionContext: any): Promise<void> {
|
|
21
|
+
const calls = node.api?.service?.[method]?.calls ?? [];
|
|
22
|
+
if (!calls.some((c: any) => c.docstore && MUTATING_DOC_OPS.has(String(c.docstore)))) return;
|
|
23
|
+
const route = executionContext?.api?.executeNodeWithRouting;
|
|
24
|
+
if (typeof route !== "function") return;
|
|
25
|
+
const run = async (_inputs: any, cfg: any, ec: any) => {
|
|
26
|
+
const { outputs } = await performApi(node, contextFor(node, {}, cfg, ec), () => {}, undefined, stateStoreFor(ec));
|
|
27
|
+
return { __outputs: outputs };
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
await route(run, {}, config, executionContext);
|
|
31
|
+
} catch (e: any) {
|
|
32
|
+
console.log(`[manifests] ${node.type}: post-mutation re-fire failed — ${e?.message ?? e}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `send` rows — one node handing a payload to a NAMED node, with no connector and no edge.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. LoopEnd already names its partner in `loopStartNodeId`. Requiring an edge as
|
|
5
|
+
* well made the same fact true in two places, and the two could disagree: the canonical loop
|
|
6
|
+
* pattern shipped without the edge for months, which builds a loop that runs exactly one pass and
|
|
7
|
+
* looks like it worked. A row that addresses a node removes the second place.
|
|
8
|
+
*
|
|
9
|
+
* It is deliberately NOT loop-specific. "Deliver this to that node" is the general primitive;
|
|
10
|
+
* the loop is its first caller.
|
|
11
|
+
*
|
|
12
|
+
* DELIVERY HAPPENS HERE, at the executor boundary, and not in events.ts. The events table
|
|
13
|
+
* DESCRIBES what leaves a node; performing a side effect needs the execution context, which only
|
|
14
|
+
* an executor holds. Keeping the split means a manifest still cannot reach the engine directly.
|
|
15
|
+
*
|
|
16
|
+
* FAILURE IS LOUD. A send that silently does nothing is a loop that stops after one pass while
|
|
17
|
+
* reporting success, which is the exact failure this replaced. An unresolvable target or an
|
|
18
|
+
* absent host raises, in the same spirit as `loop: advance` raising "loop state not found"
|
|
19
|
+
* rather than returning done.
|
|
20
|
+
*/
|
|
21
|
+
import type { Emission } from "../runtime/http/response.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The host's delivery bridge. Absent in tests and headless runs.
|
|
25
|
+
*
|
|
26
|
+
* SAME RUN ONLY, and that is the point of the leading `executionId`: it is the SENDER's own,
|
|
27
|
+
* taken from its execution context, so a node can only ever reach nodes in the workflow it is
|
|
28
|
+
* running in. There is no addressing scheme that reaches another execution.
|
|
29
|
+
*/
|
|
30
|
+
type DeliverToNode = (
|
|
31
|
+
executionId: string,
|
|
32
|
+
targetNodeId: string,
|
|
33
|
+
handle: string,
|
|
34
|
+
value: unknown,
|
|
35
|
+
) => Promise<void> | void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Deliver every `send` emission this run produced. A no-op for a node that declares none, which
|
|
39
|
+
* is every node but LoopEnd today, so this costs nothing on the common path.
|
|
40
|
+
*/
|
|
41
|
+
export async function deliverSends(node: any, emissions: Emission[], executionContext: any): Promise<void> {
|
|
42
|
+
const sends = emissions.filter((e) => e.to);
|
|
43
|
+
if (sends.length === 0) return;
|
|
44
|
+
|
|
45
|
+
const deliver: DeliverToNode | undefined = executionContext?.api?.deliverToNode;
|
|
46
|
+
if (typeof deliver !== "function") {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${node?.type ?? "node"}: an events row sends to another node, but this runtime provides no ` +
|
|
49
|
+
`deliverToNode bridge, so the message would be dropped. The run is stopped rather than ` +
|
|
50
|
+
`continuing with a message nobody received.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const executionId = executionContext?.executionId ?? executionContext?.scope?.executionId;
|
|
55
|
+
if (!executionId) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`${node?.type ?? "node"}: an events row sends to another node, but this run has no executionId, ` +
|
|
58
|
+
`so the delivery could not be confined to this workflow.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const s of sends) {
|
|
63
|
+
await deliver(String(executionId), String(s.to), s.handle ?? "input", s.value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT THE EXECUTOR ASSEMBLES FROM THE EXECUTION CONTEXT and hands to PLATFORM code —
|
|
3
|
+
* never to the manifest. `context.ts` builds what a manifest may READ; these build what
|
|
4
|
+
* the runtime's own capabilities need, and the difference is exactly the token rule:
|
|
5
|
+
* `sessionFor` carries the caller's JWT because the platform calling its own surface on
|
|
6
|
+
* the user's behalf is not data deciding where a secret goes.
|
|
7
|
+
*/
|
|
8
|
+
import { makeStateStore } from "../runtime/index.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* WHO IS WATCHING, for a service method that renders content cards onto a live screen or
|
|
12
|
+
* a node that publishes template data.
|
|
13
|
+
*
|
|
14
|
+
* Kept out of `contextFor` on purpose (§9.4): a manifest that could read the token could
|
|
15
|
+
* forward it anywhere its allowedHosts allows.
|
|
16
|
+
*
|
|
17
|
+
* `publishingContext` is the trigger-supplied identity of the conversation the run belongs
|
|
18
|
+
* to. Absent for builder, test and headless callers, which is exactly when card rendering
|
|
19
|
+
* should no-op rather than needing a flag.
|
|
20
|
+
*/
|
|
21
|
+
export function sessionFor(executionContext: any) {
|
|
22
|
+
const pub = executionContext?.publishingContext;
|
|
23
|
+
return {
|
|
24
|
+
userId: pub?.userId,
|
|
25
|
+
conversationId: pub?.conversationId,
|
|
26
|
+
chatId: pub?.chatId,
|
|
27
|
+
accessToken: executionContext?.auth?.accessToken,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The platform's audio lane, through the same handle a code node already gets.
|
|
33
|
+
*
|
|
34
|
+
* `null` when the platform has no lane, and that is NOT an error: a voice node with nowhere to
|
|
35
|
+
* send audio still holds a valid conversation with the vendor, and its transcripts still reach
|
|
36
|
+
* the workflow over the events table. Only the audio is lost, which is the correct degradation
|
|
37
|
+
* for a headless run.
|
|
38
|
+
*/
|
|
39
|
+
export function audioLaneFor(executionContext: any) {
|
|
40
|
+
return executionContext?.api?.getAudioWebSocketManager?.() ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Redis, through the handle the plugin library already provides to a code node. */
|
|
44
|
+
export function stateStoreFor(executionContext: any) {
|
|
45
|
+
return makeStateStore(executionContext?.api?.getRedisClient?.() ?? null, process.env.REDIS_NAMESPACE);
|
|
46
|
+
}
|