@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,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. */
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes the harness passes around. No behaviour, so every other file here can import
|
|
3
|
+
* it without importing anything else.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AGENT MCP HARNESS — the model-agnostic half of a conversational agent's tool loop.
|
|
8
|
+
*
|
|
9
|
+
* Every agent family (OpenAI, GLM, Grok, …) shares the SAME platform behavior:
|
|
10
|
+
* - which spatial-search results become callable tools (node-MCP rows + Unoverse
|
|
11
|
+
* app rows carrying the manifest call contract),
|
|
12
|
+
* - how an interactive APP tool presents itself to the model (the etiquette lives
|
|
13
|
+
* in the tool DESCRIPTION — the only channel that reaches a model for a tool
|
|
14
|
+
* minted MID-conversation; instructions are built once at loop start),
|
|
15
|
+
* - when a tool call ENDS the agent's turn (provenance + result shape, never a
|
|
16
|
+
* hardcoded name list).
|
|
17
|
+
*
|
|
18
|
+
* This module is that behavior, once — `@unoverse-platform/base/agent-mcp`.
|
|
19
|
+
* Agent families keep only their streaming adapters (wire shapes, chunk handling,
|
|
20
|
+
* token accounting) and map their tool-call objects into `AgentToolExchange` to
|
|
21
|
+
* consume it. Fixes land here and reach every family — the previous state
|
|
22
|
+
* (openai-only, GLM unable to discover apps at all, Grok forked) is exactly what
|
|
23
|
+
* this exists to prevent. A conformance guard fails the build if an agent family
|
|
24
|
+
* re-implements this logic instead of importing it.
|
|
25
|
+
*
|
|
26
|
+
* THE ONE-WIRE-CALL RULE — where our code ends and native MCP begins. Everything here
|
|
27
|
+
* is pure interpretation/routing and never touches the MCP wire, EXCEPT
|
|
28
|
+
* `invokeComponentAppNative` — the single function that opens an MCP client and makes a
|
|
29
|
+
* real `tools/call`. From that call onward it is all native MCP (the server renders +
|
|
30
|
+
* elicits; the answers resolve the call). Node-MCP rows and path-A workflow apps route
|
|
31
|
+
* in-process via the caller's `api.callService` because they have a node/workflow to run;
|
|
32
|
+
* only path-B component apps (nothing local to call) ride the wire. Full map:
|
|
33
|
+
* docs/MCP_COMPLETE_GUIDE.md → "where YOUR code ends and native MCP begins".
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** One executed tool call, reduced to what the harness needs: its name and the
|
|
37
|
+
* raw result content string the model will see. */
|
|
38
|
+
export interface AgentToolExchange {
|
|
39
|
+
name: string;
|
|
40
|
+
resultContent: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A spatial-discovered MCP, normalized: node-MCP rows and Unoverse app rows both
|
|
44
|
+
* come out in this shape, ready to mint as a model tool via `toolDefFromDiscoveredMCP`. */
|
|
45
|
+
export interface DiscoveredMCP {
|
|
46
|
+
id: string;
|
|
47
|
+
title: string;
|
|
48
|
+
description: string;
|
|
49
|
+
workflowId: string;
|
|
50
|
+
nodeId: string;
|
|
51
|
+
methodName: string;
|
|
52
|
+
/** Full method schema ({ methods: { [name]: { description, input } } }). */
|
|
53
|
+
schema?: any;
|
|
54
|
+
/**
|
|
55
|
+
* PATH B (UNOVERSE_MCP_APP_PROTOCOL §0.1 / §4b): a self-contained component app —
|
|
56
|
+
* an `app` URI but NO workflow. Holds the app-tool id the adapter hands to
|
|
57
|
+
* `invokeComponentAppNative` (the held native `/mcp` call) instead of firing
|
|
58
|
+
* `workflow@trigger`; `workflowId`/`nodeId` are empty. Undefined for node-MCP + path-A apps.
|
|
59
|
+
*/
|
|
60
|
+
component?: string;
|
|
61
|
+
/** PATH B: the app's resource URI (`unoverse://components/[<org>/]<name>`) — the ONLY
|
|
62
|
+
* schema handle. The tool's input schema is read LIVE from `<appUri>/schema` at mint
|
|
63
|
+
* time (`resolveToolDefNative`); spatial rows carry no schema copy. */
|
|
64
|
+
appUri?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A neutral function-tool definition; each family maps this to its wire format. */
|
|
68
|
+
export interface AgentToolDef {
|
|
69
|
+
name: string;
|
|
70
|
+
description: string;
|
|
71
|
+
parameters: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Mint the model-facing tool definition for a discovered MCP. */
|
|
75
|
+
|
|
76
|
+
/** Context a held app call needs to reach the human on the live session. */
|
|
77
|
+
export interface AppInvocationContext {
|
|
78
|
+
/** The caller's stream session — where the wizard renders and elicitation is answered. */
|
|
79
|
+
conversationId?: string;
|
|
80
|
+
userId?: string;
|
|
81
|
+
/** The current TURN id — the component renders in this turn's bubble. */
|
|
82
|
+
chatId?: string;
|
|
83
|
+
/** Seeds the elicitation prompt; the app tool schema requires the field (may be ""). */
|
|
84
|
+
message?: string;
|
|
85
|
+
/** Content-tree app rows: the row's content, seeded as-is into the component's state. */
|
|
86
|
+
props?: Record<string, unknown>;
|
|
87
|
+
/** Content-tree app rows: the ROW id — keys the render `<component>:<instanceId>` so multiple rows sharing a component don't collide. */
|
|
88
|
+
instanceId?: string;
|
|
89
|
+
/** These props are ROW DATA, not a model's composition — skip the brief referee. */
|
|
90
|
+
fromRow?: boolean;
|
|
91
|
+
/** The delivery batch this render belongs to (UNOVERSE_TEMPLATE_MODEL §12e): the
|
|
92
|
+
* server's director judges the WHOLE batch before anything renders. `size` is how
|
|
93
|
+
* many siblings to wait for; `index` is this render's arrival position. */
|
|
94
|
+
batch?: { id: string; size: number; index: number };
|
|
95
|
+
/** Forwarded as a Bearer token so the internal `/mcp` call rides the gated lane too. */
|
|
96
|
+
accessToken?: string;
|
|
97
|
+
/** Internal MCP base; defaults to UNOVERSE_SERVICE_URL or http://localhost:4106. */
|
|
98
|
+
baseUrl?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* NATIVE MCP APP INVOCATION (UNOVERSE_MCP_APP_PROTOCOL §3.3 / §4b) — the held
|
|
103
|
+
* slow-call that renders a component app and returns the user's submitted answers to
|
|
104
|
+
* the CALLING model.
|
|
105
|
+
*
|
|
106
|
+
* A bindingless component app has no workflow to fire. Calling it is ONE held
|
|
107
|
+
* `tools/call` to the platform's internal MCP server (`/mcp`): the server registered the
|
|
108
|
+
* app as a tool (mcpServer `listApps()`), renders the component on the caller's live
|
|
109
|
+
* session and ELICITS its declared `outputs` block, then resolves the call with
|
|
110
|
+
* `{ output }`. No polling and no timeout by design — an abandoned form is a tool call
|
|
111
|
+
* that never returns (the SDK's 60s default is lifted to 24h). The answers come back as
|
|
112
|
+
* the tool result so the model continues in-turn.
|
|
113
|
+
*
|
|
114
|
+
* This is the modern replacement for the legacy engine's
|
|
115
|
+
* `MCPInvoker.awaitAppOutputNative`: same native slow-call, but driven from this shared
|
|
116
|
+
* harness (every agent family) using the app id it parsed at mint time — so it needs NO
|
|
117
|
+
* server-side (XState) schema cache.
|
|
118
|
+
*/
|
|
119
|
+
/** Open an MCP client on the platform's internal `/mcp` — the ONE wire-connect shared by
|
|
120
|
+
* every native call this harness makes (app invocation, live schema read). */
|
|
121
|
+
|
|
122
|
+
/** A content-tree row's attached COMPONENT, ready to render — see rowComponentsFromResults. */
|
|
123
|
+
export interface RowComponent {
|
|
124
|
+
/** The row's identity — dedupe key (one render per row, ever). */
|
|
125
|
+
id: string;
|
|
126
|
+
/** The component app-tool id (`unoverse://components/<id>` → `<id>`). */
|
|
127
|
+
component: string;
|
|
128
|
+
/** The row's content AS-IS — the component's state (binds resolve by name). */
|
|
129
|
+
props: Record<string, unknown>;
|
|
130
|
+
}
|