@unoverse-platform/studio 0.1.96 → 0.1.98
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 +3 -3
- package/local/authoring.ts +1 -1
- package/local/catalog.ts +22 -2
- package/local/keys.ts +1 -1
- package/local/nodes.ts +29 -42
- package/local/plugin.ts +75 -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 +51 -22
- package/local/source.ts +4 -9
- 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/IdentityHeader.tsx +1 -1
- package/screens/IdentityView.tsx +206 -0
- package/screens/PromptBlocksView.tsx +1 -1
- package/screens/SourceView.tsx +1 -1
- package/screens/TemplatesLibraryView.tsx +91 -6
- package/screens/TemplatesView.tsx +24 -9
- package/screens/index.ts +5 -2
- package/src/App.tsx +14 -4
- package/src/NodeKeys.tsx +1 -1
- package/src/host.ts +3 -16
- 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/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 +111 -0
- package/vendor/base/src/definitions/registry/catalog.ts +28 -12
- 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 +107 -0
- package/vendor/base/src/definitions/registry/orgPack.ts +50 -0
- 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 +33 -25
- package/vendor/base/src/items/publish.ts +31 -7
- 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 +6 -3
- 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 +6 -1
- 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 +81 -10
- package/vendor/base/src/manifests/executor/callback.ts +15 -4
- package/vendor/base/src/manifests/executor/context.ts +12 -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/index.ts +1 -1
- 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/manifests/source.ts +1 -1
- 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/README.md +1 -1
- 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 +31 -12
- 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/vite.config.ts +1 -1
|
@@ -647,7 +647,11 @@ export function TemplatesView({
|
|
|
647
647
|
...restState,
|
|
648
648
|
...defaultsOf(c),
|
|
649
649
|
...(data ?? {}),
|
|
650
|
-
|
|
650
|
+
// ALL THREE SPELLINGS, as the activation below writes them: `state` is the ruled
|
|
651
|
+
// public-axis word (2026-08-29) and what a synthesized `Switch on: state` reads;
|
|
652
|
+
// with only the legacy two, the ladder placed a deal in the grid while the deal
|
|
653
|
+
// drew its FIRST face (observed 2026-09-11: list tiles inside a grid).
|
|
654
|
+
...(view ? { state: view, view, defaultState: view } : { defaultState: "inline" }),
|
|
651
655
|
},
|
|
652
656
|
},
|
|
653
657
|
});
|
|
@@ -815,19 +819,30 @@ export function TemplatesView({
|
|
|
815
819
|
// a single-key object naming the component and carrying its own view + data
|
|
816
820
|
// (UNOVERSE_INTERFACE_MODEL §3). Envelope instances arrive already in their view.
|
|
817
821
|
const toSpec = (item: string | Record<string, Record<string, unknown>>): SeedSpec | null => {
|
|
822
|
+
// THE HASH NAMES THE STATE IT OPENS IN (STATE_MODEL rule 3; owner 2026-09-11: a
|
|
823
|
+
// preview is written `deal#grid`, the same address grammar as everywhere else).
|
|
824
|
+
// Split ONCE, for both forms: a mount state wraps each name as `{ "deal#grid": {} }`
|
|
825
|
+
// before it gets here, so the split lives beside the lookup, not on one branch.
|
|
826
|
+
const split = (ref: string): [string, string | undefined] => {
|
|
827
|
+
const [name, hash] = ref.split("#");
|
|
828
|
+
return [name, hash || undefined];
|
|
829
|
+
};
|
|
818
830
|
if (typeof item === "string") {
|
|
819
|
-
const
|
|
831
|
+
const [name, hash] = split(item);
|
|
832
|
+
const def = byName(name);
|
|
820
833
|
// In the FLAT (container-model) list every entry is an envelope: a bare name
|
|
821
834
|
// wakes in its own arrival face. Only the keyed legacy form seeds inline for
|
|
822
|
-
// the state chip to move.
|
|
823
|
-
return def ? { def, ...(flatPreview ? { envelope: true } : {}) } : null;
|
|
835
|
+
// the state chip to move. A hash seeds the instance's own view, as an envelope.
|
|
836
|
+
return def ? { def, ...(hash ? { envelope: true, view: hash } : flatPreview ? { envelope: true } : {}) } : null;
|
|
824
837
|
}
|
|
825
|
-
const [
|
|
826
|
-
if (!
|
|
838
|
+
const [rawName, body] = Object.entries(item ?? {})[0] ?? [];
|
|
839
|
+
if (!rawName) return null;
|
|
840
|
+
const [name, hash] = split(rawName);
|
|
827
841
|
const def = byName(name);
|
|
828
842
|
if (!def) return null;
|
|
829
843
|
const { view, ...data } = (body ?? {}) as Record<string, unknown>;
|
|
830
|
-
|
|
844
|
+
const face = typeof view === "string" && view ? view : hash;
|
|
845
|
+
return { def, envelope: true, ...(face ? { view: face } : {}), ...(Object.keys(data).length ? { data } : {}) };
|
|
831
846
|
};
|
|
832
847
|
const set = entry
|
|
833
848
|
? entry.map(toSpec).filter((s): s is SeedSpec => Boolean(s))
|
|
@@ -1008,9 +1023,9 @@ export function TemplatesView({
|
|
|
1008
1023
|
onClick={() => setMetaOpen((o) => !o)}
|
|
1009
1024
|
className="flex items-center gap-1 rounded border px-2 py-0.5 font-medium"
|
|
1010
1025
|
style={{ borderColor: theme.color["border.subtle"], color: metaOpen ? theme.color["text.primary"] : theme.color["text.secondary"], background: metaOpen ? theme.color["surface.sunken"] : "transparent" }}
|
|
1011
|
-
title="Show the
|
|
1026
|
+
title="Show the app's manifest: its meta, its binding, its shape"
|
|
1012
1027
|
>
|
|
1013
|
-
|
|
1028
|
+
ⓘ Manifest <span className="opacity-60">{metaOpen ? "▴" : "▾"}</span>
|
|
1014
1029
|
</button>
|
|
1015
1030
|
)}
|
|
1016
1031
|
|
package/screens/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* studio: the Studio screens, owned once and imported by BOTH Studios,
|
|
3
3
|
* the Canvas-embedded catalog and the standalone local authoring app.
|
|
4
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
4
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
5
5
|
*
|
|
6
6
|
* These screens contain NO transport, auth or persistence of their own. Everything
|
|
7
7
|
* host-specific is imported from the bare specifier `studio-host`, which each app
|
|
@@ -24,6 +24,9 @@ export { TemplatesView } from "./TemplatesView";
|
|
|
24
24
|
export { TemplatesLibraryView } from "./TemplatesLibraryView";
|
|
25
25
|
export { DesignSystemView } from "./DesignSystemView";
|
|
26
26
|
export { SkillsView } from "./SkillsView";
|
|
27
|
+
// Agents: the one file authored for the map (docs.unoverse.ai/design/tasks), one lane in both Studios.
|
|
28
|
+
export { AgentsView } from "./AgentsView";
|
|
29
|
+
export { IdentityView } from "./IdentityView";
|
|
27
30
|
export { PromptBlocksView } from "./PromptBlocksView";
|
|
28
31
|
export { IdentityHeader } from "./IdentityHeader";
|
|
29
32
|
export { stateActivation } from "./states";
|
|
@@ -32,5 +35,5 @@ export { stateActivation } from "./states";
|
|
|
32
35
|
// so it is shared even though neither shares the screen around it.
|
|
33
36
|
export { ConfigForm, defaultsFromSchema } from "./ConfigForm";
|
|
34
37
|
// The source view renders only where the host grants source access, so it is exported
|
|
35
|
-
// unconditionally and returns null in Canvas (docs/architecture/LOCAL_STUDIO.md).
|
|
38
|
+
// unconditionally and returns null in Canvas (docs/architecture/authoring/LOCAL_STUDIO.md).
|
|
36
39
|
export { SourceView } from "./SourceView";
|
package/src/App.tsx
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Studio renders. This file only does what a host must: pick the project, pick the
|
|
6
6
|
* theme variant, resolve the theme, and choose which screen is showing.
|
|
7
7
|
*
|
|
8
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
8
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
9
9
|
*/
|
|
10
10
|
import { useEffect, useState } from "react";
|
|
11
11
|
import type { ResolvedTheme } from "@unoverse/sdk";
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
TemplatesView,
|
|
16
16
|
TemplatesLibraryView,
|
|
17
17
|
DesignSystemView,
|
|
18
|
-
SkillsView,
|
|
18
|
+
SkillsView, AgentsView,
|
|
19
|
+
IdentityView,
|
|
19
20
|
PromptBlocksView,
|
|
20
21
|
} from "@unoverse/studio";
|
|
21
22
|
import { client, usePersistentState } from "./host";
|
|
@@ -31,20 +32,25 @@ import {
|
|
|
31
32
|
LayoutTemplate,
|
|
32
33
|
Palette,
|
|
33
34
|
Sparkles,
|
|
35
|
+
Bot,
|
|
34
36
|
SquareDashed,
|
|
35
37
|
TextQuote,
|
|
38
|
+
Landmark,
|
|
36
39
|
type LucideIcon,
|
|
37
40
|
} from "lucide-react";
|
|
38
41
|
|
|
39
|
-
type View = "apps" | "templates" | "components" | "atoms" | "tokens" | "skills" | "prompts" | "nodes";
|
|
42
|
+
type View = "apps" | "agents" | "templates" | "components" | "atoms" | "tokens" | "identity" | "skills" | "prompts" | "nodes";
|
|
40
43
|
|
|
41
44
|
const NAV: { key: View; label: string; group: string; Icon: LucideIcon }[] = [
|
|
45
|
+
// Owner's order (2026-09-06): apps, agents, skills, templates, then the parts.
|
|
42
46
|
{ key: "apps", label: "Apps", group: "Build", Icon: LayoutDashboard },
|
|
47
|
+
{ key: "agents", label: "Agents", group: "Build", Icon: Bot },
|
|
48
|
+
{ key: "skills", label: "Skills", group: "Build", Icon: Sparkles },
|
|
43
49
|
{ key: "templates", label: "Templates", group: "Build", Icon: LayoutTemplate },
|
|
44
50
|
{ key: "components", label: "Components", group: "Build", Icon: SquareDashed },
|
|
45
51
|
{ key: "atoms", label: "Atoms", group: "Build", Icon: Atom },
|
|
46
52
|
{ key: "tokens", label: "Styles", group: "Build", Icon: Palette },
|
|
47
|
-
{ key: "
|
|
53
|
+
{ key: "identity", label: "Identity", group: "AI", Icon: Landmark },
|
|
48
54
|
{ key: "prompts", label: "Prompt Blocks", group: "AI", Icon: TextQuote },
|
|
49
55
|
{ key: "nodes", label: "Nodes", group: "Code", Icon: Boxes },
|
|
50
56
|
];
|
|
@@ -193,6 +199,8 @@ function Shell() {
|
|
|
193
199
|
// Standalone Studio authors offline: open apps in MOCK (the defaultLive=true
|
|
194
200
|
// resting mode is Canvas's, where the universe is right there).
|
|
195
201
|
<TemplatesView theme={theme} themeName={variant} org={org} defaultLive={false} />
|
|
202
|
+
) : view === "agents" ? (
|
|
203
|
+
<AgentsView org={org} />
|
|
196
204
|
) : view === "templates" ? (
|
|
197
205
|
<TemplatesLibraryView theme={theme} org={org} />
|
|
198
206
|
) : view === "components" ? (
|
|
@@ -201,6 +209,8 @@ function Shell() {
|
|
|
201
209
|
<AtomsView theme={theme} />
|
|
202
210
|
) : view === "tokens" ? (
|
|
203
211
|
<DesignSystemView theme={theme} foundation={foundation} org={org} />
|
|
212
|
+
) : view === "identity" ? (
|
|
213
|
+
<IdentityView org={org} />
|
|
204
214
|
) : view === "skills" ? (
|
|
205
215
|
<SkillsView org={org} />
|
|
206
216
|
) : view === "nodes" ? (
|
package/src/NodeKeys.tsx
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* So this panel is read-only. It names the exact variable each field wants and says
|
|
10
10
|
* whether it is set. Entering the value happens in the editor they are already in.
|
|
11
11
|
*
|
|
12
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
12
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
13
13
|
*/
|
|
14
14
|
import { useEffect, useState } from "react";
|
|
15
15
|
|
package/src/host.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* There is no auth here on purpose. Local Studio is offline and single-user: the files
|
|
10
10
|
* are already yours. Authentication belongs to a universe, not to a folder.
|
|
11
11
|
*
|
|
12
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
12
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
13
13
|
*/
|
|
14
14
|
import { useCallback, useEffect, useState } from "react";
|
|
15
15
|
import type { ResolvedTheme, UnoverseDefinition, UnoverseClient } from "@unoverse/sdk";
|
|
@@ -44,7 +44,7 @@ export function liveUniverseUrl(): string | null {
|
|
|
44
44
|
* The capability lives on the host for the reason `hasAuth` does: a shared screen must
|
|
45
45
|
* never branch on which app it is running in. Canvas leaves it false, so hosted surfaces
|
|
46
46
|
* render and never edit, by construction rather than by discipline
|
|
47
|
-
* (docs/architecture/LOCAL_STUDIO.md §The capability gate).
|
|
47
|
+
* (docs/architecture/authoring/LOCAL_STUDIO.md §The capability gate).
|
|
48
48
|
*/
|
|
49
49
|
export const hasSourceAccess = true;
|
|
50
50
|
|
|
@@ -129,19 +129,6 @@ class LocalUnoverseClient {
|
|
|
129
129
|
return def;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
/**
|
|
133
|
-
* The SYNCHRONOUS cached-definition peek (see the SDK client). Studio's `readDefinition`
|
|
134
|
-
* fetches every time and keeps no cache — deliberately, so an edit on disk shows up on
|
|
135
|
-
* the next read — so there is nothing to peek and the honest answer is `null`.
|
|
136
|
-
*
|
|
137
|
-
* `null` means "no opinion", never "no": the one caller (the latch derivation on send)
|
|
138
|
-
* treats it as "no latch", which is right here. Studio previews a definition; it has no
|
|
139
|
-
* live conversation to address a component from. Present so the structural contract holds
|
|
140
|
-
* — the SDK takes this client by shape, so a missing method is a crash, not a type error.
|
|
141
|
-
*/
|
|
142
|
-
peekDefinition(_uri: string): UnoverseDefinition | null {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
132
|
|
|
146
133
|
async readTheme(name = "light"): Promise<ResolvedTheme> {
|
|
147
134
|
const r = await fetch(`/local/theme?ref=${encodeURIComponent(name)}`);
|
|
@@ -187,7 +174,7 @@ export function usePersistentState<T>(key: string, initial: T): [T, React.Dispat
|
|
|
187
174
|
return [value, setValue];
|
|
188
175
|
}
|
|
189
176
|
|
|
190
|
-
export type RegistryKind = "node" | "skill" | "app" | "prompt" | "component" | "template";
|
|
177
|
+
export type RegistryKind = "node" | "skill" | "app" | "prompt" | "component" | "template" | "identity" | "agent";
|
|
191
178
|
|
|
192
179
|
/**
|
|
193
180
|
* Registry toggles decide what a RUNNING universe exposes; they persist to
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* kinds into one shape, so a model gets one tool per row regardless of which kind it was.
|
|
6
6
|
* Also the etiquette an interactive app carries, and the anchoring a follow-up search needs.
|
|
7
7
|
*/
|
|
8
|
-
import type { AgentToolExchange, DiscoveredMCP, AppInvocationContext } from "./types.js";
|
|
8
|
+
import type { AgentToolExchange, DiscoveredMCP, AppInvocationContext, OfferedFunction } from "./types.js";
|
|
9
9
|
|
|
10
10
|
/** The discovery tools whose results can carry `object_type: "mcp"` rows. */
|
|
11
11
|
export const DISCOVERY_TOOL_NAMES: readonly string[] = ["findIntent", "discoverRelated"];
|
|
@@ -25,7 +25,7 @@ import { leanToolResultForModel } from "./lean.js";
|
|
|
25
25
|
* calling it fires `workflow@trigger`.
|
|
26
26
|
* - Path B — bindingless component-app rows (§0.1): an `app` URI but NO workflow. The
|
|
27
27
|
* app IS the tool; calling it makes the held native `/mcp` call
|
|
28
|
-
* (`
|
|
28
|
+
* (`invokeTaskNative`) — the SERVER renders the component + elicits, nothing
|
|
29
29
|
* to fire. Carries `component` (the app-tool id) instead of workflow/trigger.
|
|
30
30
|
*
|
|
31
31
|
* Path B also covers CONTENT-TREE rows (object_type "service"/"need") with an app
|
|
@@ -34,20 +34,11 @@ import { leanToolResultForModel } from "./lean.js";
|
|
|
34
34
|
* render (inline card, focus, …). These rows carry no `metadata.name`, so the tool is
|
|
35
35
|
* minted under the component id and deduped across rows pointing at the same app.
|
|
36
36
|
*
|
|
37
|
-
* App descriptions gain
|
|
38
|
-
* instead of answering alongside it (observed live: a full interview streamed next to
|
|
39
|
-
* the wizard asking the same questions).
|
|
37
|
+
* App descriptions gain one sentence of etiquette; the schema is the instruction channel.
|
|
40
38
|
*/
|
|
41
39
|
export const APP_ETIQUETTE =
|
|
42
|
-
"INTERACTIVE APP: calling this
|
|
43
|
-
"
|
|
44
|
-
"never answer the request in text alongside it, never list options, never ask questions the app will " +
|
|
45
|
-
"collect. If the result includes `output`, those are the user's complete answers — continue the task " +
|
|
46
|
-
"with them (search if the request needs a lookup) and only then answer. " +
|
|
47
|
-
"If your instructions include background about this user, every text field you compose here is written " +
|
|
48
|
-
"FOR that person — use their name and what you know about them in headlines, intros, and copy; " +
|
|
49
|
-
"generic audience-neutral brochure copy is wrong when you know who is reading.";
|
|
50
|
-
|
|
40
|
+
"INTERACTIVE APP: calling this puts a UI on screen that the user completes there; reply with one short sentence inviting them to it, and when the result carries `output` those are their answers, so continue with them. " +
|
|
41
|
+
"Text you compose here is written for this person when you know who they are.";
|
|
51
42
|
|
|
52
43
|
export function parseDiscoveredMCPs(calls: AgentToolExchange[]): DiscoveredMCP[] {
|
|
53
44
|
const discovered: DiscoveredMCP[] = [];
|
|
@@ -60,11 +51,52 @@ export function parseDiscoveredMCPs(calls: AgentToolExchange[]): DiscoveredMCP[]
|
|
|
60
51
|
const results = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.results) ? parsed.results : null;
|
|
61
52
|
if (!results) continue;
|
|
62
53
|
for (const item of results) {
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
// Two row types mint tools: the registry family (stored "mcp": components, apps,
|
|
55
|
+
// node-MCPs) and TASKS (an outcome with a door, docs/unoverse/data/objects/elements/task.md).
|
|
56
|
+
if (item?.object_type !== "mcp" && item?.object_type !== "task") continue;
|
|
57
|
+
// WHAT KIND OF STEP a call to it will be on the timeline: a task is an outcome the
|
|
58
|
+
// person can get done, and the viewer draws it as one rather than as a bare tool.
|
|
59
|
+
const isTask = item.object_type === "task";
|
|
60
|
+
if (item.object_type === "task" && item.metadata?.workflow && item.metadata?.trigger && item.metadata?.output) {
|
|
61
|
+
// A WORKFLOW TASK: a task-augmented native tool on the platform's own /mcp
|
|
62
|
+
// (server mcp/server/taskTools.ts). Calling it fires the Agent and HOLDS until the
|
|
63
|
+
// run completes; the terminal node's output is the result. `taskTool` is the tool's
|
|
64
|
+
// identity (`<org>/<name>`), which the wire call spells dotted.
|
|
65
|
+
const methodName = item.metadata.name;
|
|
66
|
+
if (!methodName) continue;
|
|
67
|
+
const input = item.metadata.inputSchema || { type: "object", properties: { message: { type: "string" } } };
|
|
68
|
+
// What to do rides the tool description: the only channel that reaches a model for a
|
|
69
|
+
// tool minted mid-conversation. The skills it names are loaded through readSkill as usual.
|
|
70
|
+
const skills = Array.isArray(item.metadata?.skills) && item.metadata.skills.length ? ` Skills required: ${item.metadata.skills.join(", ")} (read them with readSkill first).` : "";
|
|
71
|
+
// A TASK IS A TOOL, DESCRIBED LIKE ONE (owner ruling 2026-09-08). This used to close
|
|
72
|
+
// with TASK_ETIQUETTE — "calling this runs an Agent and holds until it completes while
|
|
73
|
+
// the person watches its progress on screen; say in one short sentence what you are
|
|
74
|
+
// doing" — and the only thing it told the model to DO was speak. Measured on SAB
|
|
75
|
+
// (2026-09-08): across four runs of one question the model called once and, three
|
|
76
|
+
// times, emitted exactly one sentence of intent and ended the turn, with no
|
|
77
|
+
// function-call events in the stream at all.
|
|
78
|
+
//
|
|
79
|
+
// Nothing replaces it. Tool calling is native to these models, and the etiquette was
|
|
80
|
+
// us describing a heavyweight thing with consequences — holds, progress, watch — where
|
|
81
|
+
// a tool description should say what the tool does and what it returns. What the
|
|
82
|
+
// AUTHOR wrote stands on its own; add words back only if a measurement asks for them.
|
|
83
|
+
const taskDescription = `${item.description || item.title || methodName}${item.metadata?.instructions ? ` ${item.metadata.instructions}` : ""}${skills}`;
|
|
84
|
+
discovered.push({
|
|
85
|
+
...(isTask ? { task: true as const } : {}),
|
|
86
|
+
id: item.id || item.universal_id,
|
|
87
|
+
title: item.title || "",
|
|
88
|
+
description: taskDescription,
|
|
89
|
+
workflowId: item.metadata.workflow || item.workflow_id,
|
|
90
|
+
nodeId: item.metadata.trigger,
|
|
91
|
+
methodName,
|
|
92
|
+
taskTool: String(methodName),
|
|
93
|
+
schema: { methods: { [methodName]: { description: taskDescription, input } } },
|
|
94
|
+
});
|
|
95
|
+
} else if (item.metadata?.schema?.methods) {
|
|
65
96
|
const methodName = Object.keys(item.metadata.schema.methods)[0];
|
|
66
97
|
if (!methodName) continue;
|
|
67
98
|
discovered.push({
|
|
99
|
+
...(isTask ? { task: true as const } : {}),
|
|
68
100
|
id: item.id || item.universal_id,
|
|
69
101
|
title: item.title || "",
|
|
70
102
|
description: item.description || "",
|
|
@@ -80,6 +112,7 @@ export function parseDiscoveredMCPs(calls: AgentToolExchange[]): DiscoveredMCP[]
|
|
|
80
112
|
const input = item.metadata.inputSchema || { type: "object", properties: { message: { type: "string" } } };
|
|
81
113
|
const appDescription = `${item.description || item.title || methodName} ${APP_ETIQUETTE}`;
|
|
82
114
|
discovered.push({
|
|
115
|
+
...(isTask ? { task: true as const } : {}),
|
|
83
116
|
id: item.id || item.universal_id,
|
|
84
117
|
title: item.title || "",
|
|
85
118
|
description: appDescription,
|
|
@@ -90,7 +123,7 @@ export function parseDiscoveredMCPs(calls: AgentToolExchange[]): DiscoveredMCP[]
|
|
|
90
123
|
});
|
|
91
124
|
} else if (item.metadata?.app && !item.metadata?.workflow) {
|
|
92
125
|
// PATH B — bindingless component app: no workflow. Minted like an app tool; the
|
|
93
|
-
// adapter passes `component` to `
|
|
126
|
+
// adapter passes `component` to `invokeTaskNative` (held native `/mcp`
|
|
94
127
|
// call → the SERVER renders + elicits) instead of firing. The row carries NO
|
|
95
128
|
// schema — the input schema is pulled LIVE from `<appUri>/schema` at mint time
|
|
96
129
|
// (`resolveToolDefNative`); this default is the un-briefed fallback only.
|
|
@@ -99,6 +132,7 @@ export function parseDiscoveredMCPs(calls: AgentToolExchange[]): DiscoveredMCP[]
|
|
|
99
132
|
const input = { type: "object", properties: {} };
|
|
100
133
|
const appDescription = `${item.description || item.title || methodName} ${APP_ETIQUETTE}`;
|
|
101
134
|
discovered.push({
|
|
135
|
+
...(isTask ? { task: true as const } : {}),
|
|
102
136
|
id: item.id || item.universal_id,
|
|
103
137
|
title: item.title || "",
|
|
104
138
|
description: appDescription,
|
|
@@ -130,7 +164,7 @@ export function parseDiscoveredMCPs(calls: AgentToolExchange[]): DiscoveredMCP[]
|
|
|
130
164
|
* DISCOVERY NEVER CALLS THE APP. Finding a component in search results is not someone
|
|
131
165
|
* asking for it; only the model's own tool call is. Two fire-and-forget calls lived here
|
|
132
166
|
* (a shell-open on unlock, a draft top-up when a later search carried images) to paint a
|
|
133
|
-
* composed page's frame early. Both were `
|
|
167
|
+
* composed page's frame early. Both were `invokeTaskNative`, so on an app whose
|
|
134
168
|
* component declares `outputs` each one rendered the component AND opened its own
|
|
135
169
|
* elicitation: observed live 2026-08-10, three renders and three concurrent forms in ONE
|
|
136
170
|
* turn, of which the channel can hold only one (connection.tsx keeps a single pending
|
|
@@ -162,6 +196,30 @@ export async function handleDiscoveryResult(
|
|
|
162
196
|
return leanToolResultForModel(resultContent);
|
|
163
197
|
}
|
|
164
198
|
|
|
199
|
+
/**
|
|
200
|
+
* THE FUNCTIONS THE RESULT'S ROWS OFFER, lifted off the search envelope before the lean drops the
|
|
201
|
+
* key. The engine wrote them once per name (`functions: { getDetail: { description, input,
|
|
202
|
+
* objects } }`); each becomes a tool the moment the row that offers it is found, and the model
|
|
203
|
+
* never reads the key as data. Not a discovery tool's reply, or a reply without the key: nothing.
|
|
204
|
+
*/
|
|
205
|
+
export function parseOfferedFunctions(toolName: string, resultContent: string): OfferedFunction[] {
|
|
206
|
+
if (!DISCOVERY_TOOL_NAMES.includes(toolName)) return [];
|
|
207
|
+
const parsed = parseToolResult(resultContent);
|
|
208
|
+
const fns = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as { functions?: unknown }).functions : null;
|
|
209
|
+
if (!fns || typeof fns !== "object" || Array.isArray(fns)) return [];
|
|
210
|
+
const out: OfferedFunction[] = [];
|
|
211
|
+
for (const [name, raw] of Object.entries(fns as Record<string, any>)) {
|
|
212
|
+
if (!name || !raw || typeof raw !== "object") continue;
|
|
213
|
+
out.push({
|
|
214
|
+
name,
|
|
215
|
+
description: typeof raw.description === "string" ? raw.description : "",
|
|
216
|
+
...(raw.input && typeof raw.input === "object" && !Array.isArray(raw.input) ? { input: raw.input as Record<string, unknown> } : {}),
|
|
217
|
+
objects: Array.isArray(raw.objects) ? raw.objects.filter((o: unknown) => typeof o === "string") : [],
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
165
223
|
/**
|
|
166
224
|
* THE SKILLS THE SEARCH SAID ARE IN FORCE where this query landed.
|
|
167
225
|
*
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DOES THIS TOOL CALL END THE TURN?
|
|
3
3
|
*
|
|
4
|
-
* Decided by PROVENANCE and RESULT SHAPE, never by a hardcoded list of tool names.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Decided by PROVENANCE and RESULT SHAPE, never by a hardcoded list of tool names. The
|
|
5
|
+
* bridge knows which minted tools are workflow handoffs; whether one actually ends the
|
|
6
|
+
* turn depends on what came back.
|
|
7
7
|
*/
|
|
8
8
|
import type { AgentToolExchange } from "./types.js";
|
|
9
9
|
|
|
@@ -16,39 +16,19 @@ export function parseToolResult(content: string): any {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
* Whether any tool call is a dynamically-discovered workflow handoff.
|
|
21
|
-
*
|
|
22
|
-
* Provenance, not a hardcoded name list: a tool keeps the conversation going if it
|
|
23
|
-
* was wired in up front (`coreToolNames`, the getSchema set — SpatialSearch, memory,
|
|
24
|
-
* connector MCPs). Only tools spatial surfaced mid-loop are handoff candidates.
|
|
25
|
-
* With no wired-tool info we can't identify a handoff, so we keep going.
|
|
26
|
-
*/
|
|
27
|
-
export function hasDynamicHandoff(toolNames: string[], coreToolNames?: Set<string>): boolean {
|
|
28
|
-
if (!coreToolNames || coreToolNames.size === 0) return false;
|
|
29
|
-
return toolNames.some((name) => !coreToolNames.has(name));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
19
|
/**
|
|
33
20
|
* Whether this batch of tool calls ENDS the agent's turn.
|
|
34
21
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
22
|
+
* Only a tool in `handoffTools` can — a workflow-bound app fired through the service lane,
|
|
23
|
+
* whose result is the fire-and-forget signature ({ status: "started" } / unparseable). A
|
|
24
|
+
* result carrying `output` (or an explicit completed/error status) is a held call that
|
|
25
|
+
* came back with the answers, and the conversation continues with them. A component app, a
|
|
26
|
+
* workflow task or a platform tool is never a handoff, whatever it returns.
|
|
40
27
|
*/
|
|
41
|
-
export function isTurnEndingHandoff(calls: AgentToolExchange[],
|
|
42
|
-
if (
|
|
43
|
-
!hasDynamicHandoff(
|
|
44
|
-
calls.map((c) => c.name),
|
|
45
|
-
coreToolNames,
|
|
46
|
-
)
|
|
47
|
-
) {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
28
|
+
export function isTurnEndingHandoff(calls: AgentToolExchange[], handoffTools?: Set<string>): boolean {
|
|
29
|
+
if (!handoffTools?.size) return false;
|
|
50
30
|
return calls.some((c) => {
|
|
51
|
-
if (
|
|
31
|
+
if (!handoffTools.has(c.name)) return false;
|
|
52
32
|
const res = parseToolResult(c.resultContent);
|
|
53
33
|
if (res && typeof res === "object" && (res.output || res.status === "completed" || res.status === "error")) return false;
|
|
54
34
|
return true;
|
|
@@ -33,12 +33,12 @@ export type {
|
|
|
33
33
|
AppInvocationContext,
|
|
34
34
|
} from "./types.js";
|
|
35
35
|
|
|
36
|
-
export { DISCOVERY_TOOL_NAMES, APP_ETIQUETTE, parseDiscoveredMCPs, handleDiscoveryResult, parseRegionSkills, type LoadedRegionSkill } from "./discovery.js";
|
|
37
|
-
export { parseToolResult,
|
|
38
|
-
export { rowComponentsFromResults, renderRowComponents, rankedRowIds, componentTypeFromAppUri,
|
|
36
|
+
export { DISCOVERY_TOOL_NAMES, APP_ETIQUETTE, parseDiscoveredMCPs, handleDiscoveryResult, parseRegionSkills, parseOfferedFunctions, type LoadedRegionSkill } from "./discovery.js";
|
|
37
|
+
export { parseToolResult, isTurnEndingHandoff } from "./handoff.js";
|
|
38
|
+
export { rowComponentsFromResults, renderRowComponents, rankedRowIds, componentTypeFromAppUri, onScreenRead, onScreenPlaces, placesOn, placesLine, rememberRows, rowsById, cardIdOf, screenOf, openedOn, type ScreenItem, openTasksOn, onScreenLine, ABOUT_THE_PAGE, ABOUT_THE_OPENED } from "./rowComponents.js";
|
|
39
39
|
export { leanToolResultForModel } from "./lean.js";
|
|
40
|
-
export { toolDefFromDiscoveredMCP, resolveToolDefNative, isBindinglessComponentApp, componentAppInvoker, modelToolName } from "./tools.js";
|
|
41
|
-
export {
|
|
40
|
+
export { toolDefFromDiscoveredMCP, resolveToolDefNative, isBindinglessComponentApp, componentAppInvoker, isWorkflowTask, workflowTaskInvoker, workflowAppInvoker, modelToolName, objectFunctionToolDef, objectFunctionInvoker } from "./tools.js";
|
|
41
|
+
export { invokeTaskNative, connectPlatformMcp } from "./invoke.js";
|
|
42
42
|
|
|
43
43
|
export { ingestConversationTurn, type ConversationTurn } from "./memoryIngest.js";
|
|
44
44
|
export { fetchUserMemoryContext, hydratePromptWithUserMemory } from "./memoryContext.js";
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* THE ONE WIRE CALL. Where our code ends and native MCP begins.
|
|
3
3
|
*
|
|
4
4
|
* Everything else in this folder is interpretation and routing that never touches the MCP
|
|
5
|
-
* wire. This is the exception: `
|
|
6
|
-
* `tools/call`. From there the server renders, elicits, and its answers resolve the call.
|
|
5
|
+
* wire. This is the exception: `invokeTaskNative` opens a client and makes a real
|
|
6
|
+
* `tools/call`. From there the server renders, elicits, runs, and its answers resolve the call.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* TWO KINDS OF TASK ride it (2026-09-06, "every task is a native MCP tool"): a component
|
|
9
|
+
* app (path B: the server renders and elicits) and a WORKFLOW TASK (the server fires the
|
|
10
|
+
* Agent and holds the call until the run completes; the terminal output is the result).
|
|
11
|
+
* Node-MCP rows still route in-process via the caller's `api.callService`: they have a
|
|
12
|
+
* node to run. Renamed from `invokeComponentAppNative` when workflow tasks joined it.
|
|
11
13
|
*/
|
|
12
14
|
import type { AppInvocationContext, DiscoveredMCP } from "./types.js";
|
|
13
15
|
|
|
@@ -68,7 +70,7 @@ export async function connectPlatformMcp(
|
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
export async function
|
|
73
|
+
export async function invokeTaskNative(
|
|
72
74
|
toolId: string,
|
|
73
75
|
ctx: AppInvocationContext,
|
|
74
76
|
): Promise<{ status: "completed"; output?: Record<string, unknown>; conversationId?: string; action?: string; message?: string }> {
|
|
@@ -87,24 +89,44 @@ export async function invokeComponentAppNative(
|
|
|
87
89
|
...(ctx.instanceId ? { instanceId: ctx.instanceId } : {}),
|
|
88
90
|
...(ctx.fromRow ? { fromRow: true } : {}),
|
|
89
91
|
...(ctx.shell ? { shell: true } : {}),
|
|
92
|
+
...(typeof ctx.rank === "number" ? { rank: ctx.rank } : {}),
|
|
90
93
|
...(ctx.batch ? { batch: ctx.batch } : {}),
|
|
94
|
+
// A workflow task's own arguments, as the model wrote them against the task's schema.
|
|
95
|
+
...(ctx.args ?? {}),
|
|
91
96
|
},
|
|
92
97
|
},
|
|
93
98
|
undefined,
|
|
94
|
-
// A human is completing the wizard — lift the SDK's 60s default to 24h.
|
|
99
|
+
// A human is completing the wizard, or an Agent is working — lift the SDK's 60s default to 24h.
|
|
95
100
|
{ timeout: 24 * 60 * 60 * 1000 },
|
|
96
101
|
)) as {
|
|
97
102
|
isError?: boolean;
|
|
98
103
|
content?: Array<{ type: string; text?: string }>;
|
|
99
|
-
structuredContent?: { output?: Record<string, unknown>; conversationId?: string; action?: string };
|
|
104
|
+
structuredContent?: { output?: Record<string, unknown>; conversationId?: string; action?: string; result?: string; executionId?: string; status?: string };
|
|
100
105
|
};
|
|
101
106
|
// A handler exception arrives as an isError RESULT, not a rejection — surface it, or a
|
|
102
107
|
// broken run masquerades as "completed with no output".
|
|
103
108
|
if (result.isError) {
|
|
104
109
|
throw new Error(result.content?.find((c) => c.type === "text")?.text || `${toolId} app tool call failed`);
|
|
105
110
|
}
|
|
106
|
-
|
|
111
|
+
// THE ANSWER IS THE RESULT (2026-09-15): a component that asks returns `structuredContent`
|
|
112
|
+
// conforming to its `outputSchema`, with no envelope. Older shapes keep `output` inside one.
|
|
113
|
+
const sc = result.structuredContent as Record<string, unknown> | undefined;
|
|
114
|
+
const bare = !!sc && sc.output === undefined && sc.app === undefined && sc.result === undefined && sc.rendered === undefined;
|
|
115
|
+
const output = bare ? sc : result.structuredContent?.output;
|
|
107
116
|
const resultText = result.content?.find((c) => c.type === "text")?.text;
|
|
117
|
+
// A WORKFLOW TASK resolved: the Agent's run completed and `result` is its terminal
|
|
118
|
+
// output (server mcp/server/taskRun.ts). Handed to the model as `output.result` with
|
|
119
|
+
// the instruction that this IS the answer to use, not a profile to search with.
|
|
120
|
+
if (typeof result.structuredContent?.result === "string") {
|
|
121
|
+
return {
|
|
122
|
+
status: "completed",
|
|
123
|
+
output: { result: result.structuredContent.result },
|
|
124
|
+
conversationId: result.structuredContent?.conversationId,
|
|
125
|
+
message:
|
|
126
|
+
"The Agent completed. `output.result` is what it found, already shown to the person on screen. " +
|
|
127
|
+
"Continue the task with it: answer from it, and do not repeat it verbatim unless asked.",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
108
130
|
// BRIEFED component result (output carries `rendered`): the TEXT is the instruction
|
|
109
131
|
// channel — the referee's "NOT RENDERED — <violations>" correction or the mirror's
|
|
110
132
|
// "Rendered. The guest now sees this page…" reflection. Pass it through VERBATIM so
|
|
@@ -128,18 +150,14 @@ export async function invokeComponentAppNative(
|
|
|
128
150
|
// live: card streamed in, then no follow-up text, twice).
|
|
129
151
|
status: "completed",
|
|
130
152
|
output,
|
|
131
|
-
conversationId: result.structuredContent?.conversationId,
|
|
153
|
+
conversationId: result.structuredContent?.conversationId ?? ctx.conversationId,
|
|
132
154
|
action: result.structuredContent?.action,
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// de-fork is why "the model does nothing with the answers". Restores the behaviour.
|
|
155
|
+
// THE ANSWER IS THE RESULT (2026-09-15, native elicitation): what each field means was
|
|
156
|
+
// declared on the tool's `outputSchema` and read before the call. Nothing is said here.
|
|
157
|
+
// A fixed "search now with this profile" sentence lived in this spot; it turned a
|
|
158
|
+
// chosen card into a search query.
|
|
138
159
|
...(output && Object.keys(output).length
|
|
139
|
-
? {
|
|
140
|
-
message:
|
|
141
|
-
"`output` is the user's complete profile collected by the app — do NOT ask for any of these values again. The user's request is NOT yet fulfilled: any earlier search predates these answers. Search NOW with a query built from this profile and recommend the specific matches you find.",
|
|
142
|
-
}
|
|
160
|
+
? {}
|
|
143
161
|
: {
|
|
144
162
|
message:
|
|
145
163
|
"The component is now displayed on the user's screen. Continue your answer for the user now — the render is a visual aid, not your reply.",
|
|
@@ -36,6 +36,11 @@ const MODEL_METADATA_FIELDS = [
|
|
|
36
36
|
"category",
|
|
37
37
|
"whenToUse",
|
|
38
38
|
"section",
|
|
39
|
+
// A TASK row (object_type task): its identity URI and what its face says while it runs.
|
|
40
|
+
// The door fields (workflow, trigger, output) are read from the RAW row by the harness
|
|
41
|
+
// at mint time, never by the model.
|
|
42
|
+
"task",
|
|
43
|
+
"message",
|
|
39
44
|
] as const;
|
|
40
45
|
|
|
41
46
|
/**
|
|
@@ -54,6 +59,14 @@ export function leanToolResultForModel(resultContent: string): string {
|
|
|
54
59
|
// THE SEARCH'S MIRROR (MCP_APP_PROTOCOL §0.1): what the delivery put on screen. The
|
|
55
60
|
// one key the model must read before the rows; kept verbatim.
|
|
56
61
|
...(typeof parsed.screen === "string" && parsed.screen ? { screen: parsed.screen } : {}),
|
|
62
|
+
// THE TOOLS THESE ROWS BROUGHT (objects-in-spatial.md, "Found objects' functions as tools").
|
|
63
|
+
// The harness minted them a moment ago; a tool minted mid-conversation reaches the model
|
|
64
|
+
// through its description alone, and observed 2026-09-16 that was not enough on a long
|
|
65
|
+
// conversation: the model answered from the rows with the tool on offer. Named here, in
|
|
66
|
+
// the reply it is reading, before the rows.
|
|
67
|
+
...(parsed.functions && typeof parsed.functions === "object" && !Array.isArray(parsed.functions) && Object.keys(parsed.functions).length
|
|
68
|
+
? { tools: Object.entries(parsed.functions as Record<string, any>).map(([tool, f]) => ({ tool, offeredBy: Array.isArray(f?.objects) ? f.objects : [], does: typeof f?.description === "string" ? f.description : "", call: "with a result's universal_id" })) }
|
|
69
|
+
: {}),
|
|
57
70
|
results: parsed.results.map((r: Record<string, any>) => {
|
|
58
71
|
const m = (r.metadata ?? {}) as Record<string, unknown>;
|
|
59
72
|
const meta: Record<string, unknown> = {};
|
|
@@ -74,6 +87,9 @@ export function leanToolResultForModel(resultContent: string): string {
|
|
|
74
87
|
description: r.description,
|
|
75
88
|
...(r.key_need ? { key_need: r.key_need } : {}),
|
|
76
89
|
object_type: r.object_type,
|
|
90
|
+
// WHICH OF THE TWELVE IT IS (objects README 4a): the search route stamps it, the
|
|
91
|
+
// node keeps it, and this projection runs last, so it must keep it too.
|
|
92
|
+
...(r.element ? { element: r.element } : {}),
|
|
77
93
|
// One relevance number per mode: similarity (intent) or distance (discovery).
|
|
78
94
|
...(typeof r.similarity === "number" ? { similarity: r.similarity } : {}),
|
|
79
95
|
...(typeof r.distance === "number" ? { distance: r.distance } : {}),
|
|
@@ -83,6 +99,9 @@ export function leanToolResultForModel(resultContent: string): string {
|
|
|
83
99
|
// 12 chars: cheaper than the URL it complements.
|
|
84
100
|
...(r.source_id ? { source_id: r.source_id } : {}),
|
|
85
101
|
hasImage,
|
|
102
|
+
// ITS LINES, by name (migration 051): a need's `of` is what it was read from, a part's
|
|
103
|
+
// `partOf` its whole. They let rows be joined without a read.
|
|
104
|
+
...(r.links && typeof r.links === "object" && Object.keys(r.links).length ? { links: r.links } : {}),
|
|
86
105
|
...(Object.keys(meta).length ? { metadata: meta } : {}),
|
|
87
106
|
};
|
|
88
107
|
}),
|