@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
|
@@ -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
|
}),
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* memory block (identity + current state, non-intrusion framing included)
|
|
7
7
|
* from the memory server's GET /context at LOOP START and inlines it into the
|
|
8
8
|
* agent's system context — memory arrives with the agent, never as a
|
|
9
|
-
* model-visible tool call (
|
|
9
|
+
* model-visible tool call (docs/architecture/signal/memory/MEMORY.md § Serving: the zero-call injected lane).
|
|
10
10
|
*
|
|
11
11
|
* Every agent family calls this once when building its instructions. Soft-fail
|
|
12
12
|
* by design: memory server down / cold / no identity → returns "" and the agent
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || "http://localhost:4114";
|
|
17
|
-
|
|
17
|
+
// SIX SECONDS, NOT THREE (2026-09-18). The memory service now keeps the block ready, so a
|
|
18
|
+
// read is milliseconds; this only matters for the one cold build after a day's silence, and
|
|
19
|
+
// three seconds lost that race against a database in another region. A call opened without
|
|
20
|
+
// the block told a known caller "I don't have your name yet".
|
|
21
|
+
const CONTEXT_FETCH_TIMEOUT_MS = 6_000;
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Fetch the user's standing memory block, ready to append to the agent's
|
|
@@ -31,16 +35,18 @@ export async function fetchUserMemoryContext(
|
|
|
31
35
|
|
|
32
36
|
const pubCtx = executionContext?.publishingContext;
|
|
33
37
|
const userId = pubCtx?.userId;
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
// The org the run works as is the scope of a person's memory (MEMORY.md § 2.10); named by
|
|
39
|
+
// the run, else the canvas's default. Same precedence as memoryIngest.ts.
|
|
40
|
+
const org = pubCtx?.org ?? executionContext?.workflow?.org;
|
|
41
|
+
if (!userId || !org) {
|
|
36
42
|
logger?.debug?.("[MemoryContext] Skipped — incomplete identity", {
|
|
37
43
|
hasUserId: !!userId,
|
|
38
|
-
|
|
44
|
+
hasOrg: !!org,
|
|
39
45
|
});
|
|
40
46
|
return "";
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
return fetchUserMemoryBlock(userId,
|
|
49
|
+
return fetchUserMemoryBlock(userId, org, executionContext?.auth?.accessToken, logger);
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
/**
|
|
@@ -48,13 +54,13 @@ export async function fetchUserMemoryContext(
|
|
|
48
54
|
* director's verdict lane reads it this way — no node config, no execution
|
|
49
55
|
* context). Same endpoint, same soft-fail: any problem returns "".
|
|
50
56
|
*/
|
|
51
|
-
export async function fetchUserMemoryBlock(userId: string,
|
|
52
|
-
if (!userId || !
|
|
57
|
+
export async function fetchUserMemoryBlock(userId: string, org: string, accessToken?: string, logger?: any): Promise<string> {
|
|
58
|
+
if (!userId || !org) return "";
|
|
53
59
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
54
60
|
if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`;
|
|
55
61
|
|
|
56
62
|
try {
|
|
57
|
-
const params = new URLSearchParams({ userId,
|
|
63
|
+
const params = new URLSearchParams({ userId, org });
|
|
58
64
|
const res = await fetch(`${MEMORY_SERVICE_URL}/context?${params}`, {
|
|
59
65
|
headers,
|
|
60
66
|
signal: AbortSignal.timeout(CONTEXT_FETCH_TIMEOUT_MS),
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* (config.enableUserMemory) is ON, each completed turn (original user input +
|
|
6
6
|
* final agent answer — never intermediate iterations or tool chatter) is
|
|
7
7
|
* POSTed to the memory server's /memory/ingest, which feeds the L1→L4
|
|
8
|
-
*
|
|
8
|
+
* consolidation pipeline (see docs/architecture/signal/memory/MEMORY.md). Replaces the retired
|
|
9
9
|
* GravityMemoryStore canvas node.
|
|
10
10
|
*
|
|
11
11
|
* Every agent family calls this at turn completion with its own node type;
|
|
@@ -38,12 +38,16 @@ export function ingestConversationTurn(
|
|
|
38
38
|
|
|
39
39
|
const pubCtx = executionContext?.publishingContext;
|
|
40
40
|
const userId = pubCtx?.userId;
|
|
41
|
+
// THE ORG IS THE SCOPE (MEMORY.md § 2.10): a person's memory belongs to the organisation the
|
|
42
|
+
// run works as, never to one canvas. Named by the run, else the canvas's default — the same
|
|
43
|
+
// precedence every manifest gets (manifests/executor/context.ts). The canvas rides as a stamp.
|
|
44
|
+
const org = pubCtx?.org ?? executionContext?.workflow?.org;
|
|
41
45
|
const workflowId = executionContext?.workflow?.id;
|
|
42
46
|
const conversationId = pubCtx?.conversationId;
|
|
43
|
-
if (!userId || !
|
|
47
|
+
if (!userId || !org || !conversationId) {
|
|
44
48
|
logger?.debug?.("[MemoryIngest] Skipped — incomplete identity", {
|
|
45
49
|
hasUserId: !!userId,
|
|
46
|
-
|
|
50
|
+
hasOrg: !!org,
|
|
47
51
|
hasConversationId: !!conversationId,
|
|
48
52
|
});
|
|
49
53
|
return;
|
|
@@ -51,6 +55,7 @@ export function ingestConversationTurn(
|
|
|
51
55
|
|
|
52
56
|
const payload: Record<string, any> = {
|
|
53
57
|
userId,
|
|
58
|
+
org,
|
|
54
59
|
workflowId,
|
|
55
60
|
conversationId,
|
|
56
61
|
executionId: executionContext?.executionId || "",
|