@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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROJECTING A RESULT DOWN TO WHAT THE MODEL NEEDS.
|
|
3
|
+
*
|
|
4
|
+
* A search result carries far more than the model can use, and everything it does not use
|
|
5
|
+
* is context it pays for. The full rows are still used for minting tools; this is only what
|
|
6
|
+
* goes back into the conversation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Model-usable metadata fields — everything the calling LLM has ever actually used from
|
|
10
|
+
// a search row. The RAW rows (full bodyCopy markdown, openGraph, jsonLd, questions,
|
|
11
|
+
// crawl bookkeeping) are for the SERVER side — the card lane and app mint consume them
|
|
12
|
+
// BEFORE this projection is applied — and were costing ~100k tokens per search in the
|
|
13
|
+
// model's thread (observed live: context-window overflow on the first refinement turn).
|
|
14
|
+
const MODEL_METADATA_FIELDS = [
|
|
15
|
+
"title",
|
|
16
|
+
"tagline",
|
|
17
|
+
"shortDescription",
|
|
18
|
+
"introParagraph",
|
|
19
|
+
"callToAction",
|
|
20
|
+
// NO raw image URLs (primaryImage/images): with ref-hydration the model references a
|
|
21
|
+
// row by universal_id and the SERVER fills the bytes — `hasImage` is all it needs.
|
|
22
|
+
"app",
|
|
23
|
+
/**
|
|
24
|
+
* A NEED's one editorial field: the short verb its page leads with ("Advance Career",
|
|
25
|
+
* "Explore Jobs", "Check Requirements"). Needs are extracted rather than authored, so they
|
|
26
|
+
* carry none of the five fields above and reached the model with no metadata at all.
|
|
27
|
+
*
|
|
28
|
+
* Here AND in the node's own keep-list, because this projection runs second and drops
|
|
29
|
+
* whatever it does not name: either half alone is a no-op. `questions` was considered and
|
|
30
|
+
* left out — four sentences a row is the cost this projection exists to avoid.
|
|
31
|
+
*/
|
|
32
|
+
"action",
|
|
33
|
+
"name",
|
|
34
|
+
"kind",
|
|
35
|
+
"org",
|
|
36
|
+
"category",
|
|
37
|
+
"whenToUse",
|
|
38
|
+
"section",
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* LEAN RESULT FOR THE MODEL: project a search-shaped tool result down to the fields the
|
|
43
|
+
* calling LLM actually uses (title/description/needs/URL/real image URLs/similarity —
|
|
44
|
+
* and for app rows the discovery meta; the app's input schema already rides the MINTED
|
|
45
|
+
* TOOL, never re-sent here). Non-search results pass through untouched. Applied AFTER
|
|
46
|
+
* card rendering and app discovery, which need the full rows.
|
|
47
|
+
*/
|
|
48
|
+
export function leanToolResultForModel(resultContent: string): string {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(resultContent);
|
|
51
|
+
if (!parsed || !Array.isArray(parsed.results)) return resultContent;
|
|
52
|
+
const lean = {
|
|
53
|
+
count: parsed.count,
|
|
54
|
+
results: parsed.results.map((r: Record<string, any>) => {
|
|
55
|
+
const m = (r.metadata ?? {}) as Record<string, unknown>;
|
|
56
|
+
const meta: Record<string, unknown> = {};
|
|
57
|
+
for (const k of MODEL_METADATA_FIELDS) if (m[k] !== undefined) meta[k] = m[k];
|
|
58
|
+
// hasImage: precomputed by the node's projection, else derived — the model's
|
|
59
|
+
// signal that this row can serve as an image REF (it never sees the URL).
|
|
60
|
+
const hasImage =
|
|
61
|
+
r.hasImage ??
|
|
62
|
+
Boolean(
|
|
63
|
+
(typeof m.primaryImage === "string" && m.primaryImage) ||
|
|
64
|
+
(Array.isArray(m.images) && m.images.length) ||
|
|
65
|
+
r.object_type === "image",
|
|
66
|
+
);
|
|
67
|
+
return {
|
|
68
|
+
// The row's HANDLE — hydration refs and follow-up lookups address by this.
|
|
69
|
+
universal_id: r.universal_id,
|
|
70
|
+
title: r.title,
|
|
71
|
+
description: r.description,
|
|
72
|
+
...(r.key_need ? { key_need: r.key_need } : {}),
|
|
73
|
+
object_type: r.object_type,
|
|
74
|
+
// One relevance number per mode: similarity (intent) or distance (discovery).
|
|
75
|
+
...(typeof r.similarity === "number" ? { similarity: r.similarity } : {}),
|
|
76
|
+
...(typeof r.distance === "number" ? { distance: r.distance } : {}),
|
|
77
|
+
source_url: r.source_url,
|
|
78
|
+
// The standard source identity (migration 021) — lets the model (and anyone
|
|
79
|
+
// reading the trace) scope follow-ups to ONE source ("only this doc's needs").
|
|
80
|
+
// 12 chars: cheaper than the URL it complements.
|
|
81
|
+
...(r.source_id ? { source_id: r.source_id } : {}),
|
|
82
|
+
hasImage,
|
|
83
|
+
...(Object.keys(meta).length ? { metadata: meta } : {}),
|
|
84
|
+
};
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
return JSON.stringify(lean);
|
|
88
|
+
} catch {
|
|
89
|
+
return resultContent;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* THE ONE DISCOVERY ABSORBER — what EVERY agent family does with a discovery tool's
|
|
95
|
+
* result, in one place (never re-implemented in an adapter):
|
|
96
|
+
* 1. UNLOCK: each discovered component app's tool joins `discoveredApps` — the set a
|
|
97
|
+
* native MCP attachment's `toolFilter` reads, so the app appears in the model's
|
|
98
|
+
* next `tools/list` (spatial selects; MCP serves).
|
|
99
|
+
* 2. SHELL-OPEN (fire-and-forget): one empty native call per newly discovered app —
|
|
100
|
+
* the server renders the page SKELETON immediately, so the guest sees the page
|
|
101
|
+
* exist seconds in and watches it hydrate, never a blank panel while the model
|
|
102
|
+
* composes.
|
|
103
|
+
* 3. LEAN: returns the model-facing projection of the rows (full cargo was already
|
|
104
|
+
* consumed by the card lane and this unlock pass; raw rows cost ~100k tokens).
|
|
105
|
+
* Non-discovery results pass through untouched.
|
|
106
|
+
*
|
|
107
|
+
* `onAppUnlocked` — fired ONCE per newly-unlocked path-B app, and AWAITED before the lean
|
|
108
|
+
* result returns. A family that mints its own SDK tool per app (rather than exposing them
|
|
109
|
+
* through a native MCP attachment's `toolFilter`) wires the mint here: parsing + dedup stay
|
|
110
|
+
* in this one absorber, and the tool is registered before the model's next turn reads it.
|
|
111
|
+
*/
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-memory context fetch — the READ twin of memoryIngest.ts.
|
|
3
|
+
*
|
|
4
|
+
* Harness-level (model-agnostic): when an agent node's User Memory toggle
|
|
5
|
+
* (config.enableUserMemory) is ON, the harness fetches the user's standing
|
|
6
|
+
* memory block (identity + current state, non-intrusion framing included)
|
|
7
|
+
* from the memory server's GET /context at LOOP START and inlines it into the
|
|
8
|
+
* agent's system context — memory arrives with the agent, never as a
|
|
9
|
+
* model-visible tool call (MEMORY_SPEC.md v3 § Serving: zero-call standing lane).
|
|
10
|
+
*
|
|
11
|
+
* Every agent family calls this once when building its instructions. Soft-fail
|
|
12
|
+
* by design: memory server down / cold / no identity → returns "" and the agent
|
|
13
|
+
* simply starts ungrounded — memory must never block or fail the conversation.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || "http://localhost:4114";
|
|
17
|
+
const CONTEXT_FETCH_TIMEOUT_MS = 3_000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fetch the user's standing memory block, ready to append to the agent's
|
|
21
|
+
* system context. Resolves identity from the node's execution context; returns
|
|
22
|
+
* "" when the toggle is off, identity is incomplete, or the fetch fails.
|
|
23
|
+
* Never throws.
|
|
24
|
+
*/
|
|
25
|
+
export async function fetchUserMemoryContext(
|
|
26
|
+
config: { enableUserMemory?: boolean },
|
|
27
|
+
executionContext: any,
|
|
28
|
+
logger: any,
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
if (config?.enableUserMemory !== true) return "";
|
|
31
|
+
|
|
32
|
+
const pubCtx = executionContext?.publishingContext;
|
|
33
|
+
const userId = pubCtx?.userId;
|
|
34
|
+
const workflowId = executionContext?.workflow?.id;
|
|
35
|
+
if (!userId || !workflowId) {
|
|
36
|
+
logger?.debug?.("[MemoryContext] Skipped — incomplete identity", {
|
|
37
|
+
hasUserId: !!userId,
|
|
38
|
+
hasWorkflowId: !!workflowId,
|
|
39
|
+
});
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return fetchUserMemoryBlock(userId, workflowId, executionContext?.auth?.accessToken, logger);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The bare standing-context read, for callers that hold identity directly (the
|
|
48
|
+
* director's verdict lane reads it this way — no node config, no execution
|
|
49
|
+
* context). Same endpoint, same soft-fail: any problem returns "".
|
|
50
|
+
*/
|
|
51
|
+
export async function fetchUserMemoryBlock(userId: string, workflowId: string, accessToken?: string, logger?: any): Promise<string> {
|
|
52
|
+
if (!userId || !workflowId) return "";
|
|
53
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
54
|
+
if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const params = new URLSearchParams({ userId, workflowId });
|
|
58
|
+
const res = await fetch(`${MEMORY_SERVICE_URL}/context?${params}`, {
|
|
59
|
+
headers,
|
|
60
|
+
signal: AbortSignal.timeout(CONTEXT_FETCH_TIMEOUT_MS),
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
logger?.warn?.(`[MemoryContext] Fetch rejected: ${res.status}`);
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
const data = (await res.json()) as { block?: string; count?: number; identity?: string | null };
|
|
67
|
+
const block = typeof data.block === "string" ? data.block : "";
|
|
68
|
+
if (block) {
|
|
69
|
+
logger?.info?.("[MemoryContext] User context injected", {
|
|
70
|
+
hasIdentity: !!data.identity,
|
|
71
|
+
stateLines: data.count ?? 0,
|
|
72
|
+
chars: block.length,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return block;
|
|
76
|
+
} catch (err: any) {
|
|
77
|
+
logger?.warn?.(`[MemoryContext] Fetch failed (non-blocking): ${err?.message ?? err}`);
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* THE standard user-memory hydration — the ONE call every agent family makes.
|
|
84
|
+
*
|
|
85
|
+
* When the User Memory toggle is on, the user's context snapshot (identity,
|
|
86
|
+
* verified key facts, open tasks, learned state) is prepended to the USER
|
|
87
|
+
* PROMPT of EVERY request — the input side, not the system prompt: facts that
|
|
88
|
+
* ride with the request get factored into the answer, refresh each turn, and
|
|
89
|
+
* survive previousResponseId resume + prompt caching.
|
|
90
|
+
*
|
|
91
|
+
* Families call `hydratePromptWithUserMemory(config, executionContext, logger,
|
|
92
|
+
* prompt)` and send the result as the user turn. Toggle off / fetch failure →
|
|
93
|
+
* the prompt passes through untouched. Do NOT hand-roll the concatenation in
|
|
94
|
+
* node code — the convention lives here so it cannot drift per family.
|
|
95
|
+
*/
|
|
96
|
+
export async function hydratePromptWithUserMemory(
|
|
97
|
+
config: { enableUserMemory?: boolean },
|
|
98
|
+
executionContext: any,
|
|
99
|
+
logger: any,
|
|
100
|
+
prompt: string,
|
|
101
|
+
): Promise<string> {
|
|
102
|
+
const block = await fetchUserMemoryContext(config, executionContext, logger);
|
|
103
|
+
return block ? `${block}\n\n---\n\n${prompt}` : prompt;
|
|
104
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory ingest — the agent saves its own conversation turns.
|
|
3
|
+
*
|
|
4
|
+
* Harness-level (model-agnostic): when an agent node's User Memory toggle
|
|
5
|
+
* (config.enableUserMemory) is ON, each completed turn (original user input +
|
|
6
|
+
* final agent answer — never intermediate iterations or tool chatter) is
|
|
7
|
+
* POSTed to the memory server's /memory/ingest, which feeds the L1→L4
|
|
8
|
+
* cascade (see docs/architecture/MEMORY_SPEC.md). Replaces the retired
|
|
9
|
+
* GravityMemoryStore canvas node.
|
|
10
|
+
*
|
|
11
|
+
* Every agent family calls this at turn completion with its own node type;
|
|
12
|
+
* fire-and-forget — memory must never block or fail the conversation.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || "http://localhost:4114";
|
|
16
|
+
|
|
17
|
+
export interface ConversationTurn {
|
|
18
|
+
userMessage: string;
|
|
19
|
+
agentResponse: string;
|
|
20
|
+
reasoning?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Save one completed conversation turn to the memory server.
|
|
25
|
+
* Resolves identity from the node's execution context; silently no-ops when
|
|
26
|
+
* the toggle is off or identity is incomplete (e.g. canvas test runs with no
|
|
27
|
+
* conversation). Never throws.
|
|
28
|
+
*/
|
|
29
|
+
export function ingestConversationTurn(
|
|
30
|
+
config: { enableUserMemory?: boolean },
|
|
31
|
+
executionContext: any,
|
|
32
|
+
turn: ConversationTurn,
|
|
33
|
+
logger: any,
|
|
34
|
+
sourceNodeType: string,
|
|
35
|
+
): void {
|
|
36
|
+
if (config?.enableUserMemory !== true) return;
|
|
37
|
+
if (!turn.userMessage && !turn.agentResponse) return;
|
|
38
|
+
|
|
39
|
+
const pubCtx = executionContext?.publishingContext;
|
|
40
|
+
const userId = pubCtx?.userId;
|
|
41
|
+
const workflowId = executionContext?.workflow?.id;
|
|
42
|
+
const conversationId = pubCtx?.conversationId;
|
|
43
|
+
if (!userId || !workflowId || !conversationId) {
|
|
44
|
+
logger?.debug?.("[MemoryIngest] Skipped — incomplete identity", {
|
|
45
|
+
hasUserId: !!userId,
|
|
46
|
+
hasWorkflowId: !!workflowId,
|
|
47
|
+
hasConversationId: !!conversationId,
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const payload: Record<string, any> = {
|
|
53
|
+
userId,
|
|
54
|
+
workflowId,
|
|
55
|
+
conversationId,
|
|
56
|
+
executionId: executionContext?.executionId || "",
|
|
57
|
+
sourceNodeId: executionContext?.nodeId,
|
|
58
|
+
sourceNodeType,
|
|
59
|
+
timestamp: new Date().toISOString(),
|
|
60
|
+
};
|
|
61
|
+
// Only include fields with content — /memory/ingest rejects all-empty events
|
|
62
|
+
if (turn.userMessage) payload.userMessage = turn.userMessage;
|
|
63
|
+
if (turn.agentResponse) payload.agentResponse = turn.agentResponse;
|
|
64
|
+
if (turn.reasoning) payload.reasoning = turn.reasoning;
|
|
65
|
+
|
|
66
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
67
|
+
const accessToken = executionContext?.auth?.accessToken;
|
|
68
|
+
if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
|
|
69
|
+
|
|
70
|
+
fetch(`${MEMORY_SERVICE_URL}/memory/ingest`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers,
|
|
73
|
+
body: JSON.stringify(payload),
|
|
74
|
+
})
|
|
75
|
+
.then((res) => {
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
logger?.warn?.(`[MemoryIngest] Ingest rejected: ${res.status}`);
|
|
78
|
+
} else {
|
|
79
|
+
logger?.info?.("[MemoryIngest] Turn saved to memory", {
|
|
80
|
+
userMsg: turn.userMessage.length,
|
|
81
|
+
agentResp: turn.agentResponse.length,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
.catch((err) => {
|
|
86
|
+
logger?.warn?.(`[MemoryIngest] Ingest failed (non-blocking): ${err.message}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ROW COMPONENTS: a search result row may NAME a component, and surfacing the row renders
|
|
3
|
+
* that component rather than describing it.
|
|
4
|
+
*
|
|
5
|
+
* It is whatever component the row attached — a page, a chart, a document, a product panel.
|
|
6
|
+
* Nothing here is card-shaped; the lane was called "cards" for its first use and the name
|
|
7
|
+
* outlived the accuracy (renamed 2026-08-09).
|
|
8
|
+
*/
|
|
9
|
+
import type { RowComponent, AppInvocationContext } from "./types.js";
|
|
10
|
+
import { invokeComponentAppNative } from "./invoke.js";
|
|
11
|
+
// Pure section helpers, shared with every renderer so the rules live in one place.
|
|
12
|
+
import { toDocument } from "../markdown/document.js";
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract renderable COMPONENTS from search RESULT ROWS — content-tree rows
|
|
17
|
+
* (service/need) with a component ATTACHED (`metadata.app`, no workflow).
|
|
18
|
+
*
|
|
19
|
+
* The attachment is a component URI, so it is ANY component the row's author chose,
|
|
20
|
+
* not a card. (`unoverse://components/…` only: a row naming a template or carrying a
|
|
21
|
+
* `workflow` is skipped — the latter is a TOOL the model calls.)
|
|
22
|
+
*
|
|
23
|
+
* These NEVER reach a model's toolset: the component renders DATA-DRIVEN the moment
|
|
24
|
+
* discovery surfaces its row — the conversation flows normally and it simply appears
|
|
25
|
+
* (however many rows carry one). The model is neither asked nor able to decide; there
|
|
26
|
+
* is no prompt. The lane executes in the SPATIAL SEARCH NODE (the search that produced
|
|
27
|
+
* the rows renders them — every agent family gets the behavior by wiring the node), via
|
|
28
|
+
* `renderRowComponents` below.
|
|
29
|
+
*/
|
|
30
|
+
export function rowComponentsFromResults(results: unknown): RowComponent[] {
|
|
31
|
+
const out: RowComponent[] = [];
|
|
32
|
+
const seen = new Set<string>();
|
|
33
|
+
if (!Array.isArray(results)) return out;
|
|
34
|
+
for (const item of results as any[]) {
|
|
35
|
+
if (item?.object_type === "mcp") continue; // registry apps are TOOLS (path A/B above)
|
|
36
|
+
const app = item?.metadata?.app;
|
|
37
|
+
if (typeof app !== "string" || !app.startsWith("unoverse://components/") || item.metadata.workflow) continue;
|
|
38
|
+
const component = componentTypeFromAppUri(app);
|
|
39
|
+
const id = String(item.id || item.universal_id || `${component}:${item.title ?? ""}`);
|
|
40
|
+
if (!component || seen.has(id)) continue;
|
|
41
|
+
seen.add(id);
|
|
42
|
+
out.push({
|
|
43
|
+
id,
|
|
44
|
+
component,
|
|
45
|
+
// The row's content IS the component's state. Row-level facts ride along:
|
|
46
|
+
// `link` is the row's page as a LABELLED markdown link — tagline (fallback
|
|
47
|
+
// title) linked to source_url, ready for a Markdown bind.
|
|
48
|
+
props: {
|
|
49
|
+
title: item.title,
|
|
50
|
+
description: item.description,
|
|
51
|
+
// EVERY row carries `sections`, so a renderer has ONE thing to read. A row promoted
|
|
52
|
+
// before the structuring pass existed has only `bodyCopy`, which is a valid document
|
|
53
|
+
// of one prose section — synthesised here rather than in each renderer, and never
|
|
54
|
+
// overwriting the sections the row already carries.
|
|
55
|
+
...(!item.metadata.sections && item.metadata.bodyCopy
|
|
56
|
+
? { sections: [{ kind: "prose", heading: "", body: item.metadata.bodyCopy }] }
|
|
57
|
+
: {}),
|
|
58
|
+
...(item.metadata.sections ? toDocument(item.metadata.sections, item.metadata.layout as string) : {}),
|
|
59
|
+
...(item.source_url
|
|
60
|
+
? { link: `[${item.metadata.tagline || item.title || item.source_url}](${item.source_url})` }
|
|
61
|
+
: {}),
|
|
62
|
+
...item.metadata,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Render the components discovered rows carry, on the caller's live session — the ONE
|
|
71
|
+
* place this lane executes (every agent family calls this; adapters never re-implement
|
|
72
|
+
* it). Renders are fire-and-forget (a display component declares no `outputs`, so the
|
|
73
|
+
* held call resolves immediately server-side) and a failed render never breaks the
|
|
74
|
+
* conversation.
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* THE EMPTY DELIVERY IS SAID, NOT INFERRED (UNOVERSE_TEMPLATE_MODEL §12e, the
|
|
78
|
+
* mechanical spent rule): a component-capable search that ran and surfaced
|
|
79
|
+
* nothing has REPLACED the previous answer with nothing, and the receiver needs
|
|
80
|
+
* to know. One listener (the server's director bookkeeping) registers here at
|
|
81
|
+
* boot; the lane announces, the receiver decides. In-process only by design —
|
|
82
|
+
* if the lane ever runs out of process the note goes missing and the rule fails
|
|
83
|
+
* toward KEEP, the safe side.
|
|
84
|
+
*/
|
|
85
|
+
let emptyDeliveryListener: ((chatId: string) => void) | null = null;
|
|
86
|
+
export function onEmptyRowDelivery(fn: (chatId: string) => void): void {
|
|
87
|
+
emptyDeliveryListener = fn;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function renderRowComponents(
|
|
91
|
+
items: RowComponent[] | undefined,
|
|
92
|
+
ctx: AppInvocationContext,
|
|
93
|
+
log?: (msg: string) => void,
|
|
94
|
+
): void {
|
|
95
|
+
// No live session (builder/test/headless callers) → nothing to render; the
|
|
96
|
+
// search stays pure automatically — no flag juggling needed by the caller.
|
|
97
|
+
if (!ctx.userId) return;
|
|
98
|
+
if (!items?.length) {
|
|
99
|
+
// The search ran on a live session and surfaced nothing — say so (§12e).
|
|
100
|
+
if (ctx.chatId) emptyDeliveryListener?.(ctx.chatId);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// ONE DELIVERY, announced as a batch (UNOVERSE_TEMPLATE_MODEL §12e): the server's
|
|
104
|
+
// director waits for all siblings, judges once, and only the held set renders —
|
|
105
|
+
// in verdict order, before anything reaches the screen.
|
|
106
|
+
const batchId = `${ctx.chatId ?? ctx.conversationId ?? "b"}:${Math.random().toString(36).slice(2, 8)}`;
|
|
107
|
+
let index = 0;
|
|
108
|
+
for (const item of items) {
|
|
109
|
+
// Per-row instance id: keys the render `<component>:<rowId>` server-side, so
|
|
110
|
+
// multiple rows sharing one component each get their OWN slice (and a repeat
|
|
111
|
+
// render of the same row re-merges the same slice — idempotent, no dupes).
|
|
112
|
+
log?.(
|
|
113
|
+
`🧩 dispatching ${item.component} (${item.id}) → conv=${ctx.conversationId ?? "MISSING"} ` +
|
|
114
|
+
`chat=${ctx.chatId ?? "MISSING"} user=${ctx.userId ?? "MISSING"} token=${ctx.accessToken ? "yes" : "NO"}`,
|
|
115
|
+
);
|
|
116
|
+
// fromRow: this content came from the DATABASE, not from a model composing parts.
|
|
117
|
+
// Without it the brief referee judges the row — a component that renders the Document
|
|
118
|
+
// inherits its prose/keyFacts briefs and gets rejected for parts nobody composes.
|
|
119
|
+
// Fire-and-forget, log on settle: a slow render must not hold up the answer.
|
|
120
|
+
invokeComponentAppNative(item.component, { ...ctx, message: "", props: item.props, instanceId: item.id, fromRow: true, batch: { id: batchId, size: items.length, index: index++ } })
|
|
121
|
+
// "RENDERED" must mean rendered: a director-dropped card resolves too
|
|
122
|
+
// (action: "held-back") and saying RENDERED for it is the exact
|
|
123
|
+
// proof-of-nothing this log line was once rewritten to stop telling.
|
|
124
|
+
.then((r) => log?.(r.action === "held-back" ? `🧩 HELD BACK ${item.component} (${item.id})` : `🧩 RENDERED ${item.component} (${item.id})`))
|
|
125
|
+
.catch((e: any) => log?.(`🧩 FAILED ${item.component} (${item.id}) — ${e?.message ?? e}`));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** `unoverse://components/journey-finder` → `journey-finder`;
|
|
130
|
+
* `unoverse://components/bpp/journey-finder` → `bpp/journey-finder` (the render ref);
|
|
131
|
+
* `unoverse://templates/<org>/<page>` → `<org>/<page>`.
|
|
132
|
+
* The org survives: names are unique only within a tier, so stripping to the last
|
|
133
|
+
* segment would collapse two orgs' same-named components into one ambiguous type.
|
|
134
|
+
*
|
|
135
|
+
* BOTH HOMES, because an app URI names the tier it lives in (`componentUri`, meta.ts:
|
|
136
|
+
* "the address says WHAT it is"). This stripped `components/` alone, so a discovered
|
|
137
|
+
* TEMPLATE fell through with its whole URI intact — and the caller, which swaps `/` for
|
|
138
|
+
* `.` because MCP tool names carry no slash, asked the door for
|
|
139
|
+
* `unoverse:..templates.<org>.<page>`. Observed live 2026-08-31: an agent
|
|
140
|
+
* composed an entire page, every part filled, and the call bounced with -32602 at the
|
|
141
|
+
* door. Silent but for the agent's own trace, since nothing downstream logs a name it
|
|
142
|
+
* never resolved. */
|
|
143
|
+
export function componentTypeFromAppUri(app: string): string {
|
|
144
|
+
return String(app)
|
|
145
|
+
.replace(/^unoverse:\/\/(components|templates)\//, "")
|
|
146
|
+
.replace(/^\/+|\/+$/g, "");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* THE UNLOCKING SEARCH'S ROWS, in rank order — the material a page can be DRAFTED from
|
|
151
|
+
* before the model has written a word (2026-09-01).
|
|
152
|
+
*
|
|
153
|
+
* Every row, not only the ones carrying a component: a draft fills parts that need no
|
|
154
|
+
* authoring (an image ref, a gallery of pure refs), and those reference CONTENT, which has
|
|
155
|
+
* nothing to do with whether a row has a card attached. Restricting this to card-carrying
|
|
156
|
+
* rows is what left a page drafting from three hotels while the attractions sat unusable.
|
|
157
|
+
*/
|
|
158
|
+
export function rankedRowIds(resultContent: string, limit = 12): string[] {
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(resultContent) as { results?: unknown };
|
|
161
|
+
if (!Array.isArray(parsed?.results)) return [];
|
|
162
|
+
const out: string[] = [];
|
|
163
|
+
for (const r of parsed.results as Record<string, unknown>[]) {
|
|
164
|
+
const id = r?.universal_id ?? r?.id;
|
|
165
|
+
if (typeof id === "string" && id.trim() && !out.includes(id)) out.push(id);
|
|
166
|
+
if (out.length >= limit) break;
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
} catch {
|
|
170
|
+
return []; // a search result we cannot read drafts nothing; the page still opens
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MINTING A MODEL TOOL FROM A DISCOVERED APP.
|
|
3
|
+
*
|
|
4
|
+
* One discovered row becomes one tool definition. A path-B component app has no schema on
|
|
5
|
+
* the row at all, so its input schema is read LIVE from the app's own resource at mint time.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentToolDef, DiscoveredMCP, AppInvocationContext } from "./types.js";
|
|
8
|
+
import { connectPlatformMcp, invokeComponentAppNative } from "./invoke.js";
|
|
9
|
+
import { APP_ETIQUETTE } from "./discovery.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* AN IDENTITY IS NOT A TOOL NAME, and this is where the two meet.
|
|
13
|
+
*
|
|
14
|
+
* A discovered app's method name is its platform IDENTITY — for an org component that is the
|
|
15
|
+
* qualified ref `<org>/<name>` (UNOVERSE_COMPONENT_ORGS.md), because two orgs may ship the
|
|
16
|
+
* same component name. A slash is legal in an identity and legal in the `unoverse://` address
|
|
17
|
+
* built from it. It is NOT legal in a model tool name: every vendor with tool calling
|
|
18
|
+
* constrains the name, and OpenAI rejects the whole request with
|
|
19
|
+
* `Invalid 'tools[N].name': string does not match pattern '^[a-zA-Z0-9_-]+$'`.
|
|
20
|
+
*
|
|
21
|
+
* Observed live 2026-08-24, on two universes and two orgs: an agent discovered an org
|
|
22
|
+
* component app, minted it as its sixth tool, and every turn after the discovery 400'd. Turn
|
|
23
|
+
* one had passed, because the static service methods are manifest-authored and never carry
|
|
24
|
+
* an org — only a DISCOVERED app arrives org-qualified.
|
|
25
|
+
*
|
|
26
|
+
* REPLACE THE SEPARATOR, NEVER DROP THE ORG. Stripping the prefix would produce a legal name
|
|
27
|
+
* and reintroduce exactly the collision the qualified ref exists to prevent: two orgs shipping
|
|
28
|
+
* `journey-finder` into one conversation would mint one tool name twice, and `live`'s dedupe
|
|
29
|
+
* (toolloop) would silently keep whichever landed first. Substituting the illegal character
|
|
30
|
+
* keeps the name unique and keeps the org readable to the model.
|
|
31
|
+
*
|
|
32
|
+
* THE NAME IS ALSO THE ROUTE BACK. `toolBridge` keys `appInvokers` by the minted name and the
|
|
33
|
+
* invoker closes over the original `mcp`, so the sanitised name round-trips to the right app
|
|
34
|
+
* without a reverse lookup. Anything unminted falls through to `api.callService`, which is
|
|
35
|
+
* addressed by service method names that never carry an org.
|
|
36
|
+
*/
|
|
37
|
+
export function modelToolName(identity: string): string {
|
|
38
|
+
return String(identity).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Mint the model-facing tool definition for a discovered MCP. */
|
|
42
|
+
export function toolDefFromDiscoveredMCP(mcp: DiscoveredMCP): AgentToolDef {
|
|
43
|
+
const methodDef = mcp.schema?.methods?.[mcp.methodName];
|
|
44
|
+
return {
|
|
45
|
+
name: modelToolName(mcp.methodName),
|
|
46
|
+
description: methodDef?.description || mcp.description || mcp.title,
|
|
47
|
+
parameters: methodDef?.input || { type: "object", properties: { message: { type: "string" } } },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Mint the model-facing tool definition, pulling a component app's schema LIVE.
|
|
53
|
+
*
|
|
54
|
+
* NATIVE MCP, no copies: a path-B app row carries only its `appUri` — the schema is a
|
|
55
|
+
* `resources/read` of `<appUri>/schema` on the platform server, compiled fresh from the
|
|
56
|
+
* definition at read time. Always current (no reconcile), always in document order (a
|
|
57
|
+
* DB-stored copy can't hold key order), and the same source the server's referee
|
|
58
|
+
* validates against — schema drift is structurally impossible. Non-app tools (and any
|
|
59
|
+
* pull failure) fall back to the synchronous mint.
|
|
60
|
+
*/
|
|
61
|
+
export async function resolveToolDefNative(
|
|
62
|
+
mcp: DiscoveredMCP,
|
|
63
|
+
ctx: Pick<AppInvocationContext, "accessToken" | "baseUrl">,
|
|
64
|
+
): Promise<AgentToolDef> {
|
|
65
|
+
if (!isBindinglessComponentApp(mcp) || !mcp.appUri) return toolDefFromDiscoveredMCP(mcp);
|
|
66
|
+
try {
|
|
67
|
+
const { client, close } = await connectPlatformMcp(ctx);
|
|
68
|
+
try {
|
|
69
|
+
const res = (await client.readResource({ uri: `${mcp.appUri}/schema` })) as {
|
|
70
|
+
contents?: Array<{ text?: string }>;
|
|
71
|
+
};
|
|
72
|
+
const contract = JSON.parse(res.contents?.[0]?.text ?? "{}") as {
|
|
73
|
+
description?: string;
|
|
74
|
+
inputSchema?: Record<string, unknown> | null;
|
|
75
|
+
};
|
|
76
|
+
if (!contract.inputSchema) return toolDefFromDiscoveredMCP(mcp); // not briefed — generic contract
|
|
77
|
+
// A BRIEFED schema carries the composer directive in its top-level description.
|
|
78
|
+
// Models read TOOL descriptions far more reliably than schema descriptions —
|
|
79
|
+
// surface it there too, so the briefed tool self-instructs at both levels.
|
|
80
|
+
const directive = typeof contract.inputSchema.description === "string" ? ` ${contract.inputSchema.description}` : "";
|
|
81
|
+
return {
|
|
82
|
+
name: modelToolName(mcp.methodName),
|
|
83
|
+
description: `${contract.description || mcp.title || mcp.methodName}${directive} ${APP_ETIQUETTE}`,
|
|
84
|
+
parameters: contract.inputSchema,
|
|
85
|
+
};
|
|
86
|
+
} finally {
|
|
87
|
+
await close();
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
return toolDefFromDiscoveredMCP(mcp);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A bindingless component app (path B): a `component` app-tool id and NO workflow. The
|
|
96
|
+
* adapter routes it to `invokeComponentAppNative` (held `/mcp` call) instead of firing.
|
|
97
|
+
*/
|
|
98
|
+
export function isBindinglessComponentApp(mcp: DiscoveredMCP): boolean {
|
|
99
|
+
return !!mcp.component && !mcp.workflowId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The ONE component-app adapter every agent family wires for a discovered bindingless
|
|
104
|
+
* component app (path B). MCP-NATIVE by design: the tool's schema is the instruction
|
|
105
|
+
* channel — for a BRIEFED component it was compiled from the definition's `brief` tags
|
|
106
|
+
* (server briefSchema → registry metadata.inputSchema → the minted tool), so the model
|
|
107
|
+
* hydrates real fields, not a prose protocol. This closure is the composer's APPLY half:
|
|
108
|
+
* every arg beyond `message` IS that hydrated content and forwards as `props`, which the
|
|
109
|
+
* platform MCP server seeds into the rendered component's state. Non-briefed apps carry
|
|
110
|
+
* only `message` — behavior unchanged.
|
|
111
|
+
*
|
|
112
|
+
* Agent families should wire exactly:
|
|
113
|
+
* `service[methodName] = componentAppInvoker(mcp, identity)`
|
|
114
|
+
* and never hand-roll the arg mapping again.
|
|
115
|
+
*/
|
|
116
|
+
export function componentAppInvoker(
|
|
117
|
+
mcp: DiscoveredMCP,
|
|
118
|
+
identity: Pick<AppInvocationContext, "conversationId" | "userId" | "chatId" | "accessToken">,
|
|
119
|
+
): (input: unknown) => ReturnType<typeof invokeComponentAppNative> {
|
|
120
|
+
const toolId = mcp.component as string;
|
|
121
|
+
return (input: unknown) => {
|
|
122
|
+
const { message, ...hydrated } = (input ?? {}) as Record<string, unknown>;
|
|
123
|
+
return invokeComponentAppNative(toolId, {
|
|
124
|
+
conversationId: identity.conversationId,
|
|
125
|
+
userId: identity.userId,
|
|
126
|
+
accessToken: identity.accessToken,
|
|
127
|
+
chatId: identity.chatId,
|
|
128
|
+
message: typeof message === "string" ? message : "",
|
|
129
|
+
...(Object.keys(hydrated).length ? { props: hydrated } : {}),
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Context a held app call needs to reach the human on the live session. */
|