@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.
Files changed (130) hide show
  1. package/local/plugin.ts +20 -12
  2. package/package.json +13 -3
  3. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  4. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  5. package/vendor/base/src/agent-mcp/index.ts +44 -0
  6. package/vendor/base/src/agent-mcp/invoke.ts +140 -0
  7. package/vendor/base/src/agent-mcp/lean.ts +111 -0
  8. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  9. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  10. package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
  11. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  12. package/vendor/base/src/agent-mcp/types.ts +130 -0
  13. package/vendor/base/src/auth/auth.ts +278 -0
  14. package/vendor/base/src/auth/authConfig.ts +73 -0
  15. package/vendor/base/src/boot.ts +245 -0
  16. package/vendor/base/src/definitions/definitions.ts +18 -0
  17. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  18. package/vendor/base/src/definitions/fsCache.ts +128 -0
  19. package/vendor/base/src/definitions/index.ts +19 -0
  20. package/vendor/base/src/definitions/inputs.ts +30 -0
  21. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  22. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  23. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  24. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  25. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  26. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  27. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  28. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  29. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  30. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  31. package/vendor/base/src/definitions/load/load.ts +487 -0
  32. package/vendor/base/src/definitions/model.ts +238 -0
  33. package/vendor/base/src/definitions/registry/catalog.ts +643 -0
  34. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  35. package/vendor/base/src/definitions/theme.ts +383 -0
  36. package/vendor/base/src/executor.ts +284 -0
  37. package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
  38. package/vendor/base/src/items/baseVersion.ts +96 -0
  39. package/vendor/base/src/items/collect.ts +199 -0
  40. package/vendor/base/src/items/fingerprint.ts +34 -0
  41. package/vendor/base/src/items/publish.ts +198 -0
  42. package/vendor/base/src/lint/context.mjs +56 -0
  43. package/vendor/base/src/lint/design/defs.mjs +46 -0
  44. package/vendor/base/src/lint/design/file.mjs +737 -0
  45. package/vendor/base/src/lint/design/index.d.mts +30 -0
  46. package/vendor/base/src/lint/design/index.mjs +996 -0
  47. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  48. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  49. package/vendor/base/src/lint/design/walk.mjs +444 -0
  50. package/vendor/base/src/lint/index.mjs +28 -0
  51. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  52. package/vendor/base/src/lint/nodes/index.mjs +64 -0
  53. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  54. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  55. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  56. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  57. package/vendor/base/src/logger.ts +33 -0
  58. package/vendor/base/src/manifests/compose.ts +539 -0
  59. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  60. package/vendor/base/src/manifests/executor/context.ts +107 -0
  61. package/vendor/base/src/manifests/executor/index.ts +47 -0
  62. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  63. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  64. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  65. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  66. package/vendor/base/src/manifests/executor/send.ts +65 -0
  67. package/vendor/base/src/manifests/executor/session.ts +46 -0
  68. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  69. package/vendor/base/src/manifests/index.ts +280 -0
  70. package/vendor/base/src/manifests/integrity.ts +93 -0
  71. package/vendor/base/src/manifests/rows.ts +33 -0
  72. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  73. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  74. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  75. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  76. package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
  77. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  78. package/vendor/base/src/manifests/runtime/context.ts +150 -0
  79. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  80. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  81. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  82. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  83. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  84. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  85. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  86. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  87. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  88. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  89. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  90. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  91. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  92. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  93. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  94. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  95. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  96. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  97. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  98. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  99. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  100. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
  101. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  102. package/vendor/base/src/manifests/source.ts +187 -0
  103. package/vendor/base/src/manifests/watch.ts +67 -0
  104. package/vendor/base/src/markdown/document.ts +141 -0
  105. package/vendor/base/src/paths.ts +180 -0
  106. package/vendor/base/src/platform/audioManager.ts +100 -0
  107. package/vendor/base/src/platform/clientTransport.ts +65 -0
  108. package/vendor/base/src/platform/executionContext.ts +154 -0
  109. package/vendor/base/src/platform/index.ts +10 -0
  110. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  111. package/vendor/base/src/platform/redis.ts +54 -0
  112. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  113. package/vendor/base/src/pluginBase.ts +248 -0
  114. package/vendor/base/src/plugins/README.md +57 -0
  115. package/vendor/base/src/plugins/discovery.ts +87 -0
  116. package/vendor/base/src/plugins/index.ts +16 -0
  117. package/vendor/base/src/plugins/loader.ts +179 -0
  118. package/vendor/base/src/plugins/redis.ts +36 -0
  119. package/vendor/base/src/plugins/state.ts +53 -0
  120. package/vendor/base/src/registry.ts +95 -0
  121. package/vendor/base/src/security/permissions.ts +93 -0
  122. package/vendor/base/src/security/rules.ts +73 -0
  123. package/vendor/base/src/template/SafeExpression.ts +556 -0
  124. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  125. package/vendor/base/src/template/index.ts +22 -0
  126. package/vendor/base/src/types.ts +50 -0
  127. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  128. package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
  129. package/vendor/base/src/workflow/routing.ts +122 -0
  130. 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
+ }