@unoverse-platform/studio 0.1.97 → 0.1.99
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/bin/studio.mjs +2 -2
- package/local/catalog.ts +5 -1
- package/local/nodes.ts +28 -41
- package/local/plugin.ts +71 -13
- package/local/sample-identity/brand.yaml +24 -0
- package/local/sample-identity/organisation.yaml +14 -0
- package/local/sample-identity/purpose.yaml +14 -0
- package/local/sample-identity/story.yaml +18 -0
- package/local/scaffold.mjs +43 -23
- package/local/source.ts +3 -8
- package/package.json +1 -1
- package/screens/AgentsView.tsx +173 -0
- package/screens/ComponentsView.tsx +2 -2
- package/screens/EmptyLibrary.tsx +1 -1
- package/screens/IdentityView.tsx +3 -3
- package/screens/PromptBlocksView.tsx +1 -1
- package/screens/TemplatesLibraryView.tsx +91 -6
- package/screens/TemplatesView.tsx +24 -9
- package/screens/index.ts +2 -0
- package/src/App.tsx +8 -3
- package/src/host.ts +1 -14
- package/vendor/base/src/agent-mcp/discovery.ts +76 -18
- package/vendor/base/src/agent-mcp/handoff.ts +11 -31
- package/vendor/base/src/agent-mcp/index.ts +5 -5
- package/vendor/base/src/agent-mcp/invoke.ts +37 -19
- package/vendor/base/src/agent-mcp/lean.ts +19 -0
- package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
- package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
- package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
- package/vendor/base/src/agent-mcp/tools.ts +136 -7
- package/vendor/base/src/agent-mcp/types.ts +47 -10
- package/vendor/base/src/auth/auth.ts +35 -5
- package/vendor/base/src/definitions/isolated.ts +37 -0
- package/vendor/base/src/definitions/load/compile/component.ts +59 -4
- package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
- package/vendor/base/src/definitions/load/compile/template.ts +19 -15
- package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
- package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
- package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
- package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
- package/vendor/base/src/definitions/load/load.ts +37 -28
- package/vendor/base/src/definitions/model.ts +8 -12
- package/vendor/base/src/definitions/registry/agents.ts +117 -0
- package/vendor/base/src/definitions/registry/catalog.ts +40 -24
- package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
- package/vendor/base/src/definitions/registry/homes.ts +69 -5
- package/vendor/base/src/definitions/registry/objects.ts +413 -0
- package/vendor/base/src/definitions/registry/orgDocuments.ts +69 -29
- package/vendor/base/src/definitions/registry/orgPack.ts +11 -11
- package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
- package/vendor/base/src/definitions/theme.ts +1 -2
- package/vendor/base/src/items/collect.ts +29 -25
- package/vendor/base/src/items/publish.ts +30 -6
- package/vendor/base/src/lint/context.mjs +17 -0
- package/vendor/base/src/lint/design/file.mjs +199 -2
- package/vendor/base/src/lint/design/index.mjs +66 -73
- package/vendor/base/src/lint/design/vocabulary.mjs +5 -2
- package/vendor/base/src/lint/design/walk.mjs +14 -4
- package/vendor/base/src/lint/index.mjs +15 -9
- package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
- package/vendor/base/src/lint/nodes/index.mjs +5 -0
- package/vendor/base/src/lint/nodes/node.mjs +157 -7
- package/vendor/base/src/lint/objects/index.d.mts +32 -0
- package/vendor/base/src/lint/objects/index.mjs +324 -0
- package/vendor/base/src/lint/objects/rules.d.mts +44 -0
- package/vendor/base/src/lint/objects/rules.mjs +446 -0
- package/vendor/base/src/lint/prompts/index.d.mts +34 -0
- package/vendor/base/src/lint/prompts/index.mjs +198 -0
- package/vendor/base/src/manifests/compose.ts +80 -9
- package/vendor/base/src/manifests/executor/callback.ts +15 -4
- package/vendor/base/src/manifests/executor/context.ts +9 -1
- package/vendor/base/src/manifests/executor/memory.ts +83 -3
- package/vendor/base/src/manifests/executor/session.ts +50 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
- package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
- package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
- package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
- package/vendor/base/src/manifests/runtime/context.ts +18 -6
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
- package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
- package/vendor/base/src/manifests/runtime/events.ts +39 -3
- package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
- package/vendor/base/src/manifests/runtime/index.ts +3 -2
- package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
- package/vendor/base/src/manifests/runtime/usage.ts +10 -0
- package/vendor/base/src/paths.ts +16 -8
- package/vendor/base/src/platform/audioManager.ts +32 -0
- package/vendor/base/src/platform/executionContext.ts +14 -0
- package/vendor/base/src/platform/liveInput.ts +52 -0
- package/vendor/base/src/plugins/discovery.ts +1 -1
- package/vendor/base/src/template/SafeExpression.ts +6 -0
- package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
- package/vendor/sdk/appEngine.tsx +30 -11
- package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
- package/vendor/sdk/lib/app.tsx +70 -95
- package/vendor/sdk/lib/connection.tsx +47 -57
- package/vendor/sdk/lib/core/actions.ts +6 -0
- package/vendor/sdk/lib/core/apps.ts +66 -81
- package/vendor/sdk/lib/core/client.ts +5 -14
- package/vendor/sdk/lib/core/connection.ts +30 -0
- package/vendor/sdk/lib/core/index.ts +2 -2
- package/vendor/sdk/lib/core/store.ts +90 -15
- package/vendor/sdk/lib/core/types.ts +9 -8
- package/vendor/sdk/lib/primitives.tsx +113 -4
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
- package/vendor/sdk/lib/render.tsx +3 -3
- package/vendor/sdk/lib/voice.tsx +27 -13
- package/local/sample-identity/brand/brand.yaml +0 -88
- package/local/sample-identity/brand/manifest.yaml +0 -13
- package/local/sample-identity/organisation/manifest.yaml +0 -15
- package/local/sample-identity/organisation/organisation.yaml +0 -53
- package/local/sample-identity/purpose/manifest.yaml +0 -12
- package/local/sample-identity/purpose/purpose.yaml +0 -46
- package/local/sample-identity/story/manifest.yaml +0 -12
- package/local/sample-identity/story/story.yaml +0 -63
- package/vendor/base/src/definitions/registry/identitySeed.ts +0 -17
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* outlived the accuracy (renamed 2026-08-09).
|
|
8
8
|
*/
|
|
9
9
|
import type { RowComponent, AppInvocationContext } from "./types.js";
|
|
10
|
-
import {
|
|
10
|
+
import { invokeTaskNative } from "./invoke.js";
|
|
11
11
|
// Pure section helpers, shared with every renderer so the rules live in one place.
|
|
12
12
|
import { toDocument } from "../markdown/document.js";
|
|
13
13
|
|
|
@@ -32,27 +32,10 @@ export function rowComponentsFromResults(results: unknown): RowComponent[] {
|
|
|
32
32
|
const seen = new Set<string>();
|
|
33
33
|
if (!Array.isArray(results)) return out;
|
|
34
34
|
for (const item of results as any[]) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* workflow — the card finder) used to be only UNLOCKED here as a tool and rendered a
|
|
40
|
-
* turn later when the model called it; meanwhile the display cards from the same
|
|
41
|
-
* search had already landed, so a `page` card held the screen for the second before
|
|
42
|
-
* the `focus` component existed. Now it lands in the SAME delivery as the cards, in
|
|
43
|
-
* its declared state, and the ladder decides among them at once. Delivered, never
|
|
44
|
-
* invoked: no row props, no outputs collected — the model's own call (still minted
|
|
45
|
-
* by discovery) merges its arguments into this slice and collects the answers.
|
|
46
|
-
* Workflow-bound apps (path A) and native MCP tools stay tools.
|
|
47
|
-
*/
|
|
48
|
-
const uri = item?.metadata?.app;
|
|
49
|
-
if (typeof uri !== "string" || !uri.startsWith("unoverse://components/") || item.metadata?.workflow || item.metadata?.schema?.methods) continue;
|
|
50
|
-
const component = componentTypeFromAppUri(uri);
|
|
51
|
-
if (!component || seen.has(component)) continue;
|
|
52
|
-
seen.add(component);
|
|
53
|
-
out.push({ id: component, component, props: {}, shell: true });
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
35
|
+
// TASKS ARE ASKED, CONTENT IS RENDERED (RENDER_PIPELINE §4, locked 2026-09-06). A
|
|
36
|
+
// registry row (a component, a task, an app) is a door the LLM calls; it never reaches
|
|
37
|
+
// the screen through this lane. Only rows with a component attached — content — do.
|
|
38
|
+
if (item?.object_type === "mcp" || item?.object_type === "task") continue;
|
|
56
39
|
const app = item?.metadata?.app;
|
|
57
40
|
if (typeof app !== "string" || !app.startsWith("unoverse://components/") || item.metadata.workflow) continue;
|
|
58
41
|
const component = componentTypeFromAppUri(app);
|
|
@@ -62,12 +45,16 @@ export function rowComponentsFromResults(results: unknown): RowComponent[] {
|
|
|
62
45
|
out.push({
|
|
63
46
|
id,
|
|
64
47
|
component,
|
|
48
|
+
rank: rankOf(item),
|
|
65
49
|
// The row's content IS the component's state. Row-level facts ride along:
|
|
66
50
|
// `link` is the row's page as a LABELLED markdown link — tagline (fallback
|
|
67
51
|
// title) linked to source_url, ready for a Markdown bind.
|
|
68
52
|
props: {
|
|
69
53
|
title: item.title,
|
|
70
54
|
description: item.description,
|
|
55
|
+
// THE SOURCE (owner, 2026-09-11): the row's source_id rides its card, so the screen
|
|
56
|
+
// can name it and the model can scope a search to what is open.
|
|
57
|
+
...(item.source_id ? { source_id: item.source_id } : {}),
|
|
71
58
|
// EVERY row carries `sections`, so a renderer has ONE thing to read. A row promoted
|
|
72
59
|
// before the structuring pass existed has only `bodyCopy`, which is a valid document
|
|
73
60
|
// of one prose section — synthesised here rather than in each renderer, and never
|
|
@@ -86,6 +73,25 @@ export function rowComponentsFromResults(results: unknown): RowComponent[] {
|
|
|
86
73
|
return out;
|
|
87
74
|
}
|
|
88
75
|
|
|
76
|
+
/**
|
|
77
|
+
* THE ROW'S RELEVANCE, read the same way the search's own merge reads it
|
|
78
|
+
* (routes/dictionary/search/ranking.ts `rank`): the score where there is one, else the
|
|
79
|
+
* negated distance, so higher is always better. Rows in one delivery come from one search
|
|
80
|
+
* call, so their numbers compare even though intent scores cosine and discovery scores
|
|
81
|
+
* proximity.
|
|
82
|
+
*/
|
|
83
|
+
function rankOf(item: any): number | undefined {
|
|
84
|
+
const r =
|
|
85
|
+
typeof item?.similarity_score === "number"
|
|
86
|
+
? item.similarity_score
|
|
87
|
+
: typeof item?.similarity === "number"
|
|
88
|
+
? item.similarity
|
|
89
|
+
: typeof item?.distance === "number"
|
|
90
|
+
? -item.distance
|
|
91
|
+
: undefined;
|
|
92
|
+
return typeof r === "number" && Number.isFinite(r) ? r : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
/**
|
|
90
96
|
* Render the components discovered rows carry, on the caller's live session — the ONE
|
|
91
97
|
* place this lane executes (every agent family calls this; adapters never re-implement
|
|
@@ -93,40 +99,149 @@ export function rowComponentsFromResults(results: unknown): RowComponent[] {
|
|
|
93
99
|
* held call resolves immediately server-side) and a failed render never breaks the
|
|
94
100
|
* conversation.
|
|
95
101
|
*/
|
|
102
|
+
/** ONE INSTANCE ON SCREEN, as the screen document says it (docs/unoverse/design/screen/UNOVERSE_SCREEN.md §2.5). */
|
|
103
|
+
export interface ScreenItem {
|
|
104
|
+
/** The instance id; for a row card, the row's own id. */
|
|
105
|
+
id: string;
|
|
106
|
+
type: string;
|
|
107
|
+
/** True when the LLM called it (a task). */
|
|
108
|
+
asked?: boolean;
|
|
109
|
+
/** The state the instance is in now, the guest's own move included. */
|
|
110
|
+
state?: string;
|
|
111
|
+
/** The state it arrived in. Differs from `state` when the guest moved it: they opened it. */
|
|
112
|
+
arrived?: string;
|
|
113
|
+
/** What it shows: its props, as the delivery carried them. */
|
|
114
|
+
props?: Record<string, unknown>;
|
|
115
|
+
/** The place it is shown in, on an app that declares places. */
|
|
116
|
+
place?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** WHAT IS ON SCREEN, asked of the server's screen document, which registers this at boot; the
|
|
120
|
+
* search result reads it so the model is told what the person is looking at. */
|
|
121
|
+
let screenReader: ((conversationId: string) => ScreenItem[]) | null = null;
|
|
122
|
+
export function onScreenRead(fn: (conversationId: string) => ScreenItem[]): void {
|
|
123
|
+
screenReader = fn;
|
|
124
|
+
}
|
|
125
|
+
/** THE PLACES a conversation's app declares (docs/unoverse/design/screen/UNOVERSE_SCREEN.md), asked of
|
|
126
|
+
* the server, which registers this at boot. The conversation is always among them; nothing is
|
|
127
|
+
* shown unless something shows it. */
|
|
128
|
+
let placesReader: ((conversationId: string) => string[] | null) | null = null;
|
|
129
|
+
export function onScreenPlaces(fn: (conversationId: string) => string[] | null): void {
|
|
130
|
+
placesReader = fn;
|
|
131
|
+
}
|
|
132
|
+
export function placesOn(conversationId: string | undefined): string[] {
|
|
133
|
+
return (conversationId && placesReader ? placesReader(conversationId) : null) ?? ["chat"];
|
|
134
|
+
}
|
|
135
|
+
export function screenOf(conversationId: string | undefined): ScreenItem[] {
|
|
136
|
+
return conversationId && screenReader ? screenReader(conversationId) : [];
|
|
137
|
+
}
|
|
138
|
+
/** The open tasks standing, by component id: what the LLM called and holds the screen. */
|
|
139
|
+
export function openTasksOn(conversationId: string | undefined): string[] {
|
|
140
|
+
return screenOf(conversationId).filter((i) => i.asked).map((i) => i.type).filter(Boolean);
|
|
141
|
+
}
|
|
142
|
+
/** The instances the GUEST opened: moved out of the state they arrived in (a card tapped
|
|
143
|
+
* into its page). Derived from the move, never from a state's name: no word is special. */
|
|
144
|
+
export function openedOn(conversationId: string | undefined): ScreenItem[] {
|
|
145
|
+
return screenOf(conversationId).filter((i) => !i.asked && i.state && i.arrived && i.state !== i.arrived);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** WHEN AN OPEN PAGE IS THE ANSWER (RENDER_PIPELINE §3.3, ruled 2026-09-07). "About it" is
|
|
149
|
+
* narrow: a message about the page itself updates it; a NEW QUESTION is answered and shown,
|
|
150
|
+
* and the page closes. Read loosely ("if the message is about it") the model rebuilt a
|
|
151
|
+
* comparison for "what is the best card for travel", a question that never asked to compare
|
|
152
|
+
* (Gavin: "but I did not ask to compare"). ONE sentence, said in the search mirror and in the
|
|
153
|
+
* re-minted page tool, so the two can never drift. */
|
|
154
|
+
export const ABOUT_THE_PAGE =
|
|
155
|
+
"Call that tool ONLY when the user's message is about the page itself: change it, add to it, swap one of its items, explain one of its rows. A new question is NOT about the page, even if the page could hold the answer: answer it and call screen_show, which closes the page.";
|
|
156
|
+
|
|
157
|
+
/** WHAT THE GUEST OPENED, said in the mirror (ruled 2026-09-10): the item and its id, so
|
|
158
|
+
* the model's next search and its getDetail carry the id of what is open. ONE sentence,
|
|
159
|
+
* the same in the mirror and in screen_read. */
|
|
160
|
+
export const ABOUT_THE_OPENED =
|
|
161
|
+
"The user is looking at it: a question is about it unless they say otherwise. Its full record is getDetail(<id>); what it is linked to, one step at a time, is walk(<id>); pass its source_id as sourceId to a search when the question is inside it.";
|
|
162
|
+
|
|
163
|
+
/** The mirror's first line: any open page (a task the LLM called, with its tool), then
|
|
164
|
+
* anything the guest opened (a card moved into its page, with its id), then the rules. */
|
|
165
|
+
export function onScreenLine(open: string[], toolNameOf: (id: string) => string, opened: ScreenItem[] = []): string {
|
|
166
|
+
const page = open.length ? `ON SCREEN NOW: ${open.map((id) => `${id} (open page; its tool is ${toolNameOf(id)})`).join(", ")}. ${ABOUT_THE_PAGE} ` : "";
|
|
167
|
+
const moved = opened.length
|
|
168
|
+
? `THE USER OPENED: ${opened.map((i) => `${i.type} ${i.id} (now in ${i.state}${titleOf(i) ? `, "${titleOf(i)}"` : ""}${sourceOf(i) ? `, source_id ${sourceOf(i)}` : ""})`).join(", ")}. ${ABOUT_THE_OPENED} `
|
|
169
|
+
: "";
|
|
170
|
+
return `${page}${moved}`;
|
|
171
|
+
}
|
|
172
|
+
const titleOf = (i: ScreenItem): string => (typeof i.props?.title === "string" ? (i.props.title as string).slice(0, 80) : "");
|
|
173
|
+
const sourceOf = (i: ScreenItem): string => (typeof i.props?.source_id === "string" ? (i.props.source_id as string) : "");
|
|
174
|
+
|
|
175
|
+
/** WHAT IS ON SCREEN, said in a search result on an app with places
|
|
176
|
+
* (docs/unoverse/design/screen/UNOVERSE_SCREEN.md §3.1): each place and what it holds, then what this
|
|
177
|
+
* search showed. The model reads it right before it answers. */
|
|
96
178
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* boot; the lane announces, the receiver decides. In-process only by design —
|
|
102
|
-
* if the lane ever runs out of process the note goes missing and the rule fails
|
|
103
|
-
* toward KEEP, the safe side.
|
|
179
|
+
* WHAT THIS TURN'S SEARCHES FOUND (docs/unoverse/design/screen/UNOVERSE_SCREEN.md §3.1). A search
|
|
180
|
+
* draws nothing: the rows that carry a card wait here, per conversation, until the model shows
|
|
181
|
+
* the ones its answer is about with `screen_show`. Searches in one turn add up (a run the turn
|
|
182
|
+
* started extends its chat id); the first search of a new turn replaces them.
|
|
104
183
|
*/
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
184
|
+
const foundRows = new Map<string, { turn: string; rows: RowComponent[] }>();
|
|
185
|
+
const MAX_FOUND = 10000;
|
|
186
|
+
|
|
187
|
+
export function rememberRows(conversationId: string, chatId: string, rows: RowComponent[]): void {
|
|
188
|
+
const held = foundRows.get(conversationId);
|
|
189
|
+
const sameTurn = !!held && (chatId === held.turn || chatId.startsWith(`${held.turn}:`));
|
|
190
|
+
const kept = sameTurn ? held!.rows.filter((r) => !rows.some((x) => x.id === r.id)) : [];
|
|
191
|
+
foundRows.delete(conversationId);
|
|
192
|
+
foundRows.set(conversationId, { turn: sameTurn ? held!.turn : chatId, rows: [...kept, ...rows] });
|
|
193
|
+
if (foundRows.size > MAX_FOUND) foundRows.delete(foundRows.keys().next().value!);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The id the model is given for a card: the row's universal id, as the search result shows it. */
|
|
197
|
+
export const cardIdOf = (row: RowComponent): string => String(row.props?.universal_id ?? row.id);
|
|
198
|
+
|
|
199
|
+
/** This turn's cards with these ids, in the order the model gave them. */
|
|
200
|
+
export function rowsById(conversationId: string, ids: string[]): RowComponent[] {
|
|
201
|
+
const rows = foundRows.get(conversationId)?.rows ?? [];
|
|
202
|
+
const out: RowComponent[] = [];
|
|
203
|
+
for (const id of ids) {
|
|
204
|
+
const row = rows.find((r) => r.id === id || cardIdOf(r) === id);
|
|
205
|
+
if (row && !out.includes(row)) out.push(row);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The task the search put first (objects-in-spatial.md, "A task first"). */
|
|
211
|
+
export interface TaskFirstLine {
|
|
212
|
+
universal_id: string;
|
|
213
|
+
title: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function placesLine(conversationId: string | undefined, places: string[], cards: RowComponent[], task?: TaskFirstLine | null): string {
|
|
217
|
+
const items = screenOf(conversationId);
|
|
218
|
+
const named = places.filter((p) => p !== "chat");
|
|
219
|
+
const now = named
|
|
220
|
+
.map((p) => {
|
|
221
|
+
const here = items.filter((i) => i.place === p);
|
|
222
|
+
return here.length ? `${p}: ${here.map((i) => `${i.type} ${i.id}${titleOf(i) ? ` "${titleOf(i)}"` : ""}`).join(", ")}` : `${p}: empty`;
|
|
223
|
+
})
|
|
224
|
+
.join("; ");
|
|
225
|
+
const list = cards
|
|
226
|
+
.slice(0, 12)
|
|
227
|
+
.map((c) => `${cardIdOf(c)}${typeof c.props?.title === "string" ? ` "${c.props.title}"` : ""}`)
|
|
228
|
+
.join(", ");
|
|
229
|
+
const found = cards.length
|
|
230
|
+
? ` This search found ${cards.length} card(s): ${list}. None is shown yet. If your answer is these cards themselves, call screen_show with their ids now, before reading their details: the user sees them at once, and you read while they look. If you will hand them to a task instead, such as a comparison, do not show them: the task is what the user sees.`
|
|
231
|
+
: "";
|
|
232
|
+
// A TASK FIRST (objects-in-spatial.md, 2026-09-18): the search ranked a task first, so
|
|
233
|
+
// the fact is said, and the order is: call it now, read after.
|
|
234
|
+
const opened = task
|
|
235
|
+
? ` A task answers this request: ${task.title} (${task.universal_id}). Call it now, before reading any detail, and give it the cards it needs from this search; it is what the user sees, so do not show the cards.`
|
|
236
|
+
: "";
|
|
237
|
+
return `ON SCREEN: ${now}.${opened}${found}`;
|
|
108
238
|
}
|
|
109
239
|
|
|
110
|
-
/** What the delivery did with one row: rendered,
|
|
111
|
-
* delivered as a shell, or failed. The search's MIRROR is written from these. */
|
|
240
|
+
/** What the delivery did with one row: rendered, or failed. */
|
|
112
241
|
export interface RowOutcome {
|
|
113
242
|
component: string;
|
|
114
243
|
id: string;
|
|
115
|
-
action: "rendered" | "
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* THE SEARCH'S MIRROR (MCP_APP_PROTOCOL §0.1, ruled 2026-09-05): one line saying what the
|
|
120
|
-
* delivery put on screen and what it did not show, so the model answers the screen and
|
|
121
|
-
* not the rows. Empty when nothing is on screen (card-only deliveries do not mirror).
|
|
122
|
-
*/
|
|
123
|
-
export function screenLine(outcomes: RowOutcome[]): string {
|
|
124
|
-
const shown = outcomes.filter((o) => o.action === "shell" || o.action === "rendered");
|
|
125
|
-
const hidden = outcomes.filter((o) => o.action === "held-back");
|
|
126
|
-
if (!shown.some((o) => o.action === "shell")) return "";
|
|
127
|
-
const on = shown.map((o) => (o.action === "shell" ? `${o.component} (on screen, in its opening state)` : o.component)).join(", ");
|
|
128
|
-
const off = hidden.length ? ` Not shown: ${hidden.length} result card(s), ranked below it on the app's ladder — do not describe them as visible.` : "";
|
|
129
|
-
return `On screen: ${on}. Answer by pointing the guest to what is on screen.${off}`;
|
|
244
|
+
action: "rendered" | "failed";
|
|
130
245
|
}
|
|
131
246
|
|
|
132
247
|
export function renderRowComponents(
|
|
@@ -136,15 +251,8 @@ export function renderRowComponents(
|
|
|
136
251
|
): Promise<RowOutcome[]> {
|
|
137
252
|
// No live session (builder/test/headless callers) → nothing to render; the
|
|
138
253
|
// search stays pure automatically — no flag juggling needed by the caller.
|
|
139
|
-
if (!ctx.userId) return Promise.resolve([]);
|
|
140
|
-
|
|
141
|
-
// The search ran on a live session and surfaced nothing — say so (§12e).
|
|
142
|
-
if (ctx.chatId) emptyDeliveryListener?.(ctx.chatId);
|
|
143
|
-
return Promise.resolve([]);
|
|
144
|
-
}
|
|
145
|
-
// ONE DELIVERY, announced as a batch (UNOVERSE_TEMPLATE_MODEL §12e): the server's
|
|
146
|
-
// director waits for all siblings, judges once, and only the held set renders —
|
|
147
|
-
// in verdict order, before anything reaches the screen.
|
|
254
|
+
if (!ctx.userId || !items?.length) return Promise.resolve([]);
|
|
255
|
+
// ONE DELIVERY, carried as a batch: the screen keeps the cards of one search in its order.
|
|
148
256
|
const batchId = `${ctx.chatId ?? ctx.conversationId ?? "b"}:${Math.random().toString(36).slice(2, 8)}`;
|
|
149
257
|
let index = 0;
|
|
150
258
|
const outcomes: Promise<RowOutcome>[] = [];
|
|
@@ -160,24 +268,19 @@ export function renderRowComponents(
|
|
|
160
268
|
// Without it the brief referee judges the row — a component that renders the Document
|
|
161
269
|
// inherits its prose/keyFacts briefs and gets rejected for parts nobody composes.
|
|
162
270
|
// Fire-and-forget, log on settle: a slow render must not hold up the answer.
|
|
163
|
-
// A shell keys the component's own slice (no per-row instance), so the model's later
|
|
164
|
-
// call for the same component merges into it rather than drawing a second one.
|
|
165
271
|
outcomes.push(
|
|
166
|
-
|
|
272
|
+
invokeTaskNative(item.component, {
|
|
167
273
|
...ctx,
|
|
168
274
|
message: "",
|
|
169
275
|
props: item.props,
|
|
170
|
-
|
|
276
|
+
instanceId: item.id,
|
|
277
|
+
...(typeof item.rank === "number" ? { rank: item.rank } : {}),
|
|
171
278
|
fromRow: true,
|
|
172
279
|
batch: { id: batchId, size: items.length, index: index++ },
|
|
173
280
|
})
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
.then((r): RowOutcome => {
|
|
178
|
-
const action: RowOutcome["action"] = r.action === "held-back" ? "held-back" : r.action === "shell" ? "shell" : "rendered";
|
|
179
|
-
log?.(action === "held-back" ? `🧩 HELD BACK ${item.component} (${item.id})` : `🧩 RENDERED ${item.component} (${item.id})`);
|
|
180
|
-
return { component: item.component, id: item.id, action };
|
|
281
|
+
.then((): RowOutcome => {
|
|
282
|
+
log?.(`🧩 RENDERED ${item.component} (${item.id})`);
|
|
283
|
+
return { component: item.component, id: item.id, action: "rendered" };
|
|
181
284
|
})
|
|
182
285
|
.catch((e: any): RowOutcome => {
|
|
183
286
|
log?.(`🧩 FAILED ${item.component} (${item.id}) — ${e?.message ?? e}`);
|
|
@@ -189,7 +292,7 @@ export function renderRowComponents(
|
|
|
189
292
|
}
|
|
190
293
|
|
|
191
294
|
/** `unoverse://components/journey-finder` → `journey-finder`;
|
|
192
|
-
* `unoverse://components
|
|
295
|
+
* `unoverse://components/<org>/finder` → `<org>/finder` (the render ref);
|
|
193
296
|
* `unoverse://templates/<org>/<page>` → `<org>/<page>`.
|
|
194
297
|
* The org survives: names are unique only within a tier, so stripping to the last
|
|
195
298
|
* segment would collapse two orgs' same-named components into one ambiguous type.
|
|
@@ -201,9 +304,18 @@ export function renderRowComponents(
|
|
|
201
304
|
* `unoverse:..templates.<org>.<page>`. Observed live 2026-08-31: an agent
|
|
202
305
|
* composed an entire page, every part filled, and the call bounced with -32602 at the
|
|
203
306
|
* door. Silent but for the agent's own trace, since nothing downstream logs a name it
|
|
204
|
-
* never resolved.
|
|
307
|
+
* never resolved.
|
|
308
|
+
*
|
|
309
|
+
* THE HASH IS NOT PART OF THE NAME (docs/unoverse/data/objects/README.md, the object's `app`):
|
|
310
|
+
* `unoverse://components/<org>/<name>#<state>` names the interface, and the hash the state
|
|
311
|
+
* the canvas opens ONE row in. A delivery of rows names the component alone: where an
|
|
312
|
+
* arrival lands is the receiving template's (UNOVERSE_TEMPLATE_MODEL §5d, the grid place
|
|
313
|
+
* takes arrivals). Observed live 2026-09-11: the deal object named `#page`, the tool asked
|
|
314
|
+
* for was `better-alpha.deal#page`, -32602 at the door, and the voice agent described a
|
|
315
|
+
* deal nobody could see. */
|
|
205
316
|
export function componentTypeFromAppUri(app: string): string {
|
|
206
317
|
return String(app)
|
|
318
|
+
.replace(/#.*$/, "")
|
|
207
319
|
.replace(/^unoverse:\/\/(components|templates)\//, "")
|
|
208
320
|
.replace(/^\/+|\/+$/g, "");
|
|
209
321
|
}
|
|
@@ -215,7 +327,7 @@ export function componentTypeFromAppUri(app: string): string {
|
|
|
215
327
|
* Every row, not only the ones carrying a component: a draft fills parts that need no
|
|
216
328
|
* authoring (an image ref, a gallery of pure refs), and those reference CONTENT, which has
|
|
217
329
|
* nothing to do with whether a row has a card attached. Restricting this to card-carrying
|
|
218
|
-
* rows is what left a page drafting from
|
|
330
|
+
* rows is what left a page drafting from rows of one kind while rows of another sat unusable.
|
|
219
331
|
*/
|
|
220
332
|
export function rankedRowIds(resultContent: string, limit = 12): string[] {
|
|
221
333
|
try {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* One discovered row becomes one tool definition. A path-B component app has no schema on
|
|
5
5
|
* the row at all, so its input schema is read LIVE from the app's own resource at mint time.
|
|
6
6
|
*/
|
|
7
|
-
import type { AgentToolDef, DiscoveredMCP, AppInvocationContext } from "./types.js";
|
|
8
|
-
import { connectPlatformMcp,
|
|
7
|
+
import type { AgentToolDef, DiscoveredMCP, AppInvocationContext, OfferedFunction } from "./types.js";
|
|
8
|
+
import { connectPlatformMcp, invokeTaskNative } from "./invoke.js";
|
|
9
9
|
import { APP_ETIQUETTE } from "./discovery.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -72,8 +72,12 @@ export async function resolveToolDefNative(
|
|
|
72
72
|
const contract = JSON.parse(res.contents?.[0]?.text ?? "{}") as {
|
|
73
73
|
description?: string;
|
|
74
74
|
inputSchema?: Record<string, unknown> | null;
|
|
75
|
+
outputSchema?: Record<string, unknown> | null;
|
|
75
76
|
};
|
|
76
|
-
|
|
77
|
+
// WHAT COMES BACK rides the tool (MCP `outputSchema`), briefed or not: a finder with no
|
|
78
|
+
// fillable field still tells the model, before the call, what it hands back.
|
|
79
|
+
const returns = contract.outputSchema ? { outputSchema: contract.outputSchema } : {};
|
|
80
|
+
if (!contract.inputSchema) return { ...toolDefFromDiscoveredMCP(mcp), ...returns }; // not briefed — generic contract
|
|
77
81
|
// A BRIEFED schema carries the composer directive in its top-level description.
|
|
78
82
|
// Models read TOOL descriptions far more reliably than schema descriptions —
|
|
79
83
|
// surface it there too, so the briefed tool self-instructs at both levels.
|
|
@@ -82,23 +86,60 @@ export async function resolveToolDefNative(
|
|
|
82
86
|
name: modelToolName(mcp.methodName),
|
|
83
87
|
description: `${contract.description || mcp.title || mcp.methodName}${directive} ${APP_ETIQUETTE}`,
|
|
84
88
|
parameters: contract.inputSchema,
|
|
89
|
+
...returns,
|
|
85
90
|
};
|
|
86
91
|
} finally {
|
|
87
92
|
await close();
|
|
88
93
|
}
|
|
89
|
-
} catch {
|
|
94
|
+
} catch (err) {
|
|
95
|
+
// SAID, NOT SWALLOWED (2026-09-16): a contract read that fails hands the model a tool with
|
|
96
|
+
// no schema either way, and until this line nothing reported it. The card finder reached
|
|
97
|
+
// the model with `output_schema: null` while the server's contract carried one, and the
|
|
98
|
+
// only path that produces that shape is this fallback.
|
|
99
|
+
console.warn(`[agent-mcp] contract read failed for ${mcp.appUri ?? mcp.methodName}; minted without its schema: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
100
|
return toolDefFromDiscoveredMCP(mcp);
|
|
91
101
|
}
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
/**
|
|
95
105
|
* A bindingless component app (path B): a `component` app-tool id and NO workflow. The
|
|
96
|
-
* adapter routes it to `
|
|
106
|
+
* adapter routes it to `invokeTaskNative` (held `/mcp` call) instead of firing.
|
|
97
107
|
*/
|
|
98
108
|
export function isBindinglessComponentApp(mcp: DiscoveredMCP): boolean {
|
|
99
109
|
return !!mcp.component && !mcp.workflowId;
|
|
100
110
|
}
|
|
101
111
|
|
|
112
|
+
/** A workflow task (2026-09-06): a task-augmented native tool, held until the Agent's run completes. */
|
|
113
|
+
export function isWorkflowTask(mcp: DiscoveredMCP): boolean {
|
|
114
|
+
return !!mcp.taskTool;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* THE ONE workflow-task adapter every agent family wires for a discovered workflow task.
|
|
119
|
+
* The model's arguments go to the tool as they are (the task's own input schema shaped
|
|
120
|
+
* them); the identity rides beside them so the server pushes the face into THIS
|
|
121
|
+
* conversation and runs the Agent as THIS caller. The held call resolves with the run's
|
|
122
|
+
* result in `output.result`.
|
|
123
|
+
*/
|
|
124
|
+
export function workflowTaskInvoker(
|
|
125
|
+
mcp: DiscoveredMCP,
|
|
126
|
+
identity: Pick<AppInvocationContext, "conversationId" | "userId" | "chatId" | "accessToken" | "baseUrl">,
|
|
127
|
+
): (input: unknown) => ReturnType<typeof invokeTaskNative> {
|
|
128
|
+
const toolId = mcp.taskTool as string;
|
|
129
|
+
return (input: unknown) => {
|
|
130
|
+
const { message, ...rest } = (input ?? {}) as Record<string, unknown>;
|
|
131
|
+
return invokeTaskNative(toolId, {
|
|
132
|
+
conversationId: identity.conversationId,
|
|
133
|
+
userId: identity.userId,
|
|
134
|
+
accessToken: identity.accessToken,
|
|
135
|
+
chatId: identity.chatId,
|
|
136
|
+
...(identity.baseUrl ? { baseUrl: identity.baseUrl } : {}),
|
|
137
|
+
message: typeof message === "string" ? message : "",
|
|
138
|
+
...(Object.keys(rest).length ? { args: rest } : {}),
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
102
143
|
/**
|
|
103
144
|
* The ONE component-app adapter every agent family wires for a discovered bindingless
|
|
104
145
|
* component app (path B). MCP-NATIVE by design: the tool's schema is the instruction
|
|
@@ -116,15 +157,50 @@ export function isBindinglessComponentApp(mcp: DiscoveredMCP): boolean {
|
|
|
116
157
|
export function componentAppInvoker(
|
|
117
158
|
mcp: DiscoveredMCP,
|
|
118
159
|
identity: Pick<AppInvocationContext, "conversationId" | "userId" | "chatId" | "accessToken">,
|
|
119
|
-
): (input: unknown) => ReturnType<typeof
|
|
160
|
+
): (input: unknown) => ReturnType<typeof invokeTaskNative> {
|
|
120
161
|
const toolId = mcp.component as string;
|
|
121
162
|
return (input: unknown) => {
|
|
122
163
|
const { message, ...hydrated } = (input ?? {}) as Record<string, unknown>;
|
|
123
|
-
return
|
|
164
|
+
return invokeTaskNative(toolId, {
|
|
165
|
+
conversationId: identity.conversationId,
|
|
166
|
+
userId: identity.userId,
|
|
167
|
+
accessToken: identity.accessToken,
|
|
168
|
+
chatId: identity.chatId,
|
|
169
|
+
message: typeof message === "string" ? message : "",
|
|
170
|
+
...(Object.keys(hydrated).length ? { props: hydrated } : {}),
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The ONE workflow-bound app adapter (path A), and the reason a called app now opens.
|
|
177
|
+
*
|
|
178
|
+
* An app is ALREADY a native MCP tool on the platform's own server — `registerTools`
|
|
179
|
+
* (server mcp/server/tools.ts) registers every app under its bare id plus an `<org>.<name>`
|
|
180
|
+
* alias — so opening one is the same held `/mcp` call a component app makes. Only the id
|
|
181
|
+
* differs: a path-A row carries no `component`, and its identity is the app's own name.
|
|
182
|
+
*
|
|
183
|
+
* WITHOUT THIS, a workflow-bound app minted a tool that nothing could invoke. The mint set
|
|
184
|
+
* `handoffTools` and nothing else, so `bridge.call` found no entry in `appInvokers` and fell
|
|
185
|
+
* through to `api.callService`, which addresses SERVICE methods: the platform answered
|
|
186
|
+
* "SpatialSearch has no service method \"sab-voice\"" and the call failed in 10ms while the
|
|
187
|
+
* run still reported success. Observed live on SAB (2026-09-07): the app was found, minted
|
|
188
|
+
* and CALLED, and the person watched a blank panel. Every workflow-bound app shared this —
|
|
189
|
+
* the path-B sibling three lines above had an invoker from the day it was written.
|
|
190
|
+
*/
|
|
191
|
+
export function workflowAppInvoker(
|
|
192
|
+
mcp: DiscoveredMCP,
|
|
193
|
+
identity: Pick<AppInvocationContext, "conversationId" | "userId" | "chatId" | "accessToken" | "baseUrl">,
|
|
194
|
+
): (input: unknown) => ReturnType<typeof invokeTaskNative> {
|
|
195
|
+
const toolId = String(mcp.methodName);
|
|
196
|
+
return (input: unknown) => {
|
|
197
|
+
const { message, ...hydrated } = (input ?? {}) as Record<string, unknown>;
|
|
198
|
+
return invokeTaskNative(toolId, {
|
|
124
199
|
conversationId: identity.conversationId,
|
|
125
200
|
userId: identity.userId,
|
|
126
201
|
accessToken: identity.accessToken,
|
|
127
202
|
chatId: identity.chatId,
|
|
203
|
+
...(identity.baseUrl ? { baseUrl: identity.baseUrl } : {}),
|
|
128
204
|
message: typeof message === "string" ? message : "",
|
|
129
205
|
...(Object.keys(hydrated).length ? { props: hydrated } : {}),
|
|
130
206
|
});
|
|
@@ -132,3 +208,56 @@ export function componentAppInvoker(
|
|
|
132
208
|
}
|
|
133
209
|
|
|
134
210
|
/** Context a held app call needs to reach the human on the live session. */
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A FOUND OBJECT'S FUNCTION AS A TOOL (docs/unoverse/data/spatial/objects-in-spatial.md, "Found
|
|
214
|
+
* objects' functions as tools"). Named by the function, never per object: the model sees one
|
|
215
|
+
* `getDetail`, told which objects answer it, and passes the row's universal_id beside the
|
|
216
|
+
* function's own arguments. `input: false` never reaches a tool; the function's `input` is what
|
|
217
|
+
* its author declared for callers, so it is handed over as written, with the id required.
|
|
218
|
+
*/
|
|
219
|
+
export function objectFunctionToolDef(fn: OfferedFunction): AgentToolDef {
|
|
220
|
+
const input = (fn.input ?? { type: "object", properties: {} }) as Record<string, any>;
|
|
221
|
+
const properties = { ...(input.properties && typeof input.properties === "object" ? input.properties : {}) };
|
|
222
|
+
const required = Array.isArray(input.required) ? input.required.filter((r: unknown) => typeof r === "string") : [];
|
|
223
|
+
const who = fn.objects.length ? ` Offered by: ${fn.objects.join(", ")}.` : "";
|
|
224
|
+
return {
|
|
225
|
+
name: modelToolName(fn.name),
|
|
226
|
+
description: `${fn.description || fn.name}${who} Pass the universal_id of a result you already hold, exactly as it appeared; never invented.`,
|
|
227
|
+
parameters: {
|
|
228
|
+
...input,
|
|
229
|
+
type: "object",
|
|
230
|
+
properties: {
|
|
231
|
+
universal_id: { type: "string", description: "The result's universal_id, exactly as it appeared in the search result." },
|
|
232
|
+
...properties,
|
|
233
|
+
},
|
|
234
|
+
required: ["universal_id", ...required.filter((r: string) => r !== "universal_id")],
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The call: the engine's one function door, `POST /dictionary/function/<name>/<universal_id>`,
|
|
241
|
+
* with the model's other arguments as the function's params. The engine runs the function of
|
|
242
|
+
* THAT row's object (a fund's `documents`, a document's `getDetail`), or answers 404 when the
|
|
243
|
+
* row's object offers no such function, which comes back to the model as the error it is.
|
|
244
|
+
*/
|
|
245
|
+
export function objectFunctionInvoker(fn: OfferedFunction, engineUrl: string): (input: unknown) => Promise<unknown> {
|
|
246
|
+
return async (input: unknown) => {
|
|
247
|
+
const { universal_id, ...params } = (input ?? {}) as Record<string, unknown>;
|
|
248
|
+
const id = typeof universal_id === "string" ? universal_id.trim() : "";
|
|
249
|
+
if (!id) return { error: `${fn.name} needs the universal_id of a result you already hold.` };
|
|
250
|
+
const res = await fetch(`${engineUrl}/dictionary/function/${encodeURIComponent(fn.name)}/${encodeURIComponent(id)}`, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
headers: { "content-type": "application/json" },
|
|
253
|
+
body: JSON.stringify(params),
|
|
254
|
+
});
|
|
255
|
+
const text = await res.text();
|
|
256
|
+
if (!res.ok) return { error: text || `${fn.name} answered ${res.status}` };
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(text);
|
|
259
|
+
} catch {
|
|
260
|
+
return text;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -25,11 +25,12 @@
|
|
|
25
25
|
*
|
|
26
26
|
* THE ONE-WIRE-CALL RULE — where our code ends and native MCP begins. Everything here
|
|
27
27
|
* is pure interpretation/routing and never touches the MCP wire, EXCEPT
|
|
28
|
-
* `
|
|
28
|
+
* `invokeTaskNative` — the single function that opens an MCP client and makes a
|
|
29
29
|
* real `tools/call`. From that call onward it is all native MCP (the server renders +
|
|
30
|
-
* elicits; the answers resolve the call).
|
|
31
|
-
* in-process via the caller's `api.callService` because they have a node
|
|
32
|
-
*
|
|
30
|
+
* elicits, or runs an Agent and holds until it completes; the answers resolve the call).
|
|
31
|
+
* Node-MCP rows route in-process via the caller's `api.callService` because they have a node
|
|
32
|
+
* to run; component apps and WORKFLOW TASKS (every task is a native MCP tool, 2026-09-06)
|
|
33
|
+
* ride the wire. Full map:
|
|
33
34
|
* docs/MCP_COMPLETE_GUIDE.md → "where YOUR code ends and native MCP begins".
|
|
34
35
|
*/
|
|
35
36
|
|
|
@@ -54,7 +55,7 @@ export interface DiscoveredMCP {
|
|
|
54
55
|
/**
|
|
55
56
|
* PATH B (UNOVERSE_MCP_APP_PROTOCOL §0.1 / §4b): a self-contained component app —
|
|
56
57
|
* an `app` URI but NO workflow. Holds the app-tool id the adapter hands to
|
|
57
|
-
* `
|
|
58
|
+
* `invokeTaskNative` (the held native `/mcp` call) instead of firing
|
|
58
59
|
* `workflow@trigger`; `workflowId`/`nodeId` are empty. Undefined for node-MCP + path-A apps.
|
|
59
60
|
*/
|
|
60
61
|
component?: string;
|
|
@@ -62,6 +63,33 @@ export interface DiscoveredMCP {
|
|
|
62
63
|
* schema handle. The tool's input schema is read LIVE from `<appUri>/schema` at mint
|
|
63
64
|
* time (`resolveToolDefNative`); spatial rows carry no schema copy. */
|
|
64
65
|
appUri?: string;
|
|
66
|
+
/**
|
|
67
|
+
* A WORKFLOW TASK (2026-09-06): a task-augmented native tool on the platform's /mcp. The
|
|
68
|
+
* value is the tool's identity (`<org>/<name>`), spelled dotted on the wire. Calling it
|
|
69
|
+
* holds until the Agent's run completes and returns the terminal node's output.
|
|
70
|
+
*/
|
|
71
|
+
taskTool?: string;
|
|
72
|
+
/**
|
|
73
|
+
* MINTED FROM A TASK ROW (object_type `task`): an outcome with a door, whichever door it
|
|
74
|
+
* has. The bridge reports a call to it as a `task` step, so the execution timeline draws
|
|
75
|
+
* it as a task and not as one more tool.
|
|
76
|
+
*/
|
|
77
|
+
task?: true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A FUNCTION THE ROWS OF A SEARCH RESULT OFFER (docs/unoverse/data/spatial/objects-in-spatial.md,
|
|
82
|
+
* "Found objects' functions as tools"): the engine names, once per NAME across the result, every
|
|
83
|
+
* public function the found rows' objects declare in api/service.yaml. The harness mints one tool
|
|
84
|
+
* per name, its input the function's own plus the row's universal_id, and a call runs that row's
|
|
85
|
+
* own object's function through the engine's one function door.
|
|
86
|
+
*/
|
|
87
|
+
export interface OfferedFunction {
|
|
88
|
+
name: string;
|
|
89
|
+
description: string;
|
|
90
|
+
input?: Record<string, unknown>;
|
|
91
|
+
/** The objects in the result that offer it, by title. */
|
|
92
|
+
objects: string[];
|
|
65
93
|
}
|
|
66
94
|
|
|
67
95
|
/** A neutral function-tool definition; each family maps this to its wire format. */
|
|
@@ -69,6 +97,8 @@ export interface AgentToolDef {
|
|
|
69
97
|
name: string;
|
|
70
98
|
description: string;
|
|
71
99
|
parameters: Record<string, unknown>;
|
|
100
|
+
/** What the tool returns, from a component's `outputs` block (MCP `outputSchema`). */
|
|
101
|
+
outputSchema?: Record<string, unknown>;
|
|
72
102
|
}
|
|
73
103
|
|
|
74
104
|
/** Mint the model-facing tool definition for a discovered MCP. */
|
|
@@ -91,10 +121,17 @@ export interface AppInvocationContext {
|
|
|
91
121
|
/** Deliver the component's SHELL only: render it in its declared state and return
|
|
92
122
|
* at once — never collect its outputs (§12e "Discovery delivers; it never invokes"). */
|
|
93
123
|
shell?: boolean;
|
|
124
|
+
/** The row's relevance as the search scored it — the director's ordering. */
|
|
125
|
+
rank?: number;
|
|
94
126
|
/** The delivery batch this render belongs to (UNOVERSE_TEMPLATE_MODEL §12e): the
|
|
95
127
|
* server's director judges the WHOLE batch before anything renders. `size` is how
|
|
96
128
|
* many siblings to wait for; `index` is this render's arrival position. */
|
|
97
129
|
batch?: { id: string; size: number; index: number };
|
|
130
|
+
/**
|
|
131
|
+
* A WORKFLOW TASK's own arguments (2026-09-06), as the model wrote them against the task's
|
|
132
|
+
* input schema. Spread into the call beside the identity; the server shapes the run from them.
|
|
133
|
+
*/
|
|
134
|
+
args?: Record<string, unknown>;
|
|
98
135
|
/** Forwarded as a Bearer token so the internal `/mcp` call rides the gated lane too. */
|
|
99
136
|
accessToken?: string;
|
|
100
137
|
/** Internal MCP base; defaults to UNOVERSE_SERVICE_URL or http://localhost:4106. */
|
|
@@ -130,9 +167,9 @@ export interface RowComponent {
|
|
|
130
167
|
component: string;
|
|
131
168
|
/** The row's content AS-IS — the component's state (binds resolve by name). */
|
|
132
169
|
props: Record<string, unknown>;
|
|
133
|
-
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
|
|
170
|
+
/** THE ROW'S RELEVANCE, as the search scored it (`similarity_score`, else `similarity`,
|
|
171
|
+
* else `-distance`, read exactly as `routes/dictionary/search/ranking.ts` ranks). Carried
|
|
172
|
+
* as a NUMBER because arrival position is not rank: renders dispatch in parallel. Rows
|
|
173
|
+
* within one delivery come from one search call, so their numbers compare. */
|
|
174
|
+
rank?: number;
|
|
138
175
|
}
|