@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.
Files changed (120) hide show
  1. package/bin/studio.mjs +2 -2
  2. package/local/catalog.ts +5 -1
  3. package/local/nodes.ts +28 -41
  4. package/local/plugin.ts +71 -13
  5. package/local/sample-identity/brand.yaml +24 -0
  6. package/local/sample-identity/organisation.yaml +14 -0
  7. package/local/sample-identity/purpose.yaml +14 -0
  8. package/local/sample-identity/story.yaml +18 -0
  9. package/local/scaffold.mjs +43 -23
  10. package/local/source.ts +3 -8
  11. package/package.json +1 -1
  12. package/screens/AgentsView.tsx +173 -0
  13. package/screens/ComponentsView.tsx +2 -2
  14. package/screens/EmptyLibrary.tsx +1 -1
  15. package/screens/IdentityView.tsx +3 -3
  16. package/screens/PromptBlocksView.tsx +1 -1
  17. package/screens/TemplatesLibraryView.tsx +91 -6
  18. package/screens/TemplatesView.tsx +24 -9
  19. package/screens/index.ts +2 -0
  20. package/src/App.tsx +8 -3
  21. package/src/host.ts +1 -14
  22. package/vendor/base/src/agent-mcp/discovery.ts +76 -18
  23. package/vendor/base/src/agent-mcp/handoff.ts +11 -31
  24. package/vendor/base/src/agent-mcp/index.ts +5 -5
  25. package/vendor/base/src/agent-mcp/invoke.ts +37 -19
  26. package/vendor/base/src/agent-mcp/lean.ts +19 -0
  27. package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
  28. package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
  29. package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
  30. package/vendor/base/src/agent-mcp/tools.ts +136 -7
  31. package/vendor/base/src/agent-mcp/types.ts +47 -10
  32. package/vendor/base/src/auth/auth.ts +35 -5
  33. package/vendor/base/src/definitions/isolated.ts +37 -0
  34. package/vendor/base/src/definitions/load/compile/component.ts +59 -4
  35. package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
  36. package/vendor/base/src/definitions/load/compile/template.ts +19 -15
  37. package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
  38. package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
  39. package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
  40. package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
  41. package/vendor/base/src/definitions/load/load.ts +37 -28
  42. package/vendor/base/src/definitions/model.ts +8 -12
  43. package/vendor/base/src/definitions/registry/agents.ts +117 -0
  44. package/vendor/base/src/definitions/registry/catalog.ts +40 -24
  45. package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
  46. package/vendor/base/src/definitions/registry/homes.ts +69 -5
  47. package/vendor/base/src/definitions/registry/objects.ts +413 -0
  48. package/vendor/base/src/definitions/registry/orgDocuments.ts +69 -29
  49. package/vendor/base/src/definitions/registry/orgPack.ts +11 -11
  50. package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
  51. package/vendor/base/src/definitions/theme.ts +1 -2
  52. package/vendor/base/src/items/collect.ts +29 -25
  53. package/vendor/base/src/items/publish.ts +30 -6
  54. package/vendor/base/src/lint/context.mjs +17 -0
  55. package/vendor/base/src/lint/design/file.mjs +199 -2
  56. package/vendor/base/src/lint/design/index.mjs +66 -73
  57. package/vendor/base/src/lint/design/vocabulary.mjs +5 -2
  58. package/vendor/base/src/lint/design/walk.mjs +14 -4
  59. package/vendor/base/src/lint/index.mjs +15 -9
  60. package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
  61. package/vendor/base/src/lint/nodes/index.mjs +5 -0
  62. package/vendor/base/src/lint/nodes/node.mjs +157 -7
  63. package/vendor/base/src/lint/objects/index.d.mts +32 -0
  64. package/vendor/base/src/lint/objects/index.mjs +324 -0
  65. package/vendor/base/src/lint/objects/rules.d.mts +44 -0
  66. package/vendor/base/src/lint/objects/rules.mjs +446 -0
  67. package/vendor/base/src/lint/prompts/index.d.mts +34 -0
  68. package/vendor/base/src/lint/prompts/index.mjs +198 -0
  69. package/vendor/base/src/manifests/compose.ts +80 -9
  70. package/vendor/base/src/manifests/executor/callback.ts +15 -4
  71. package/vendor/base/src/manifests/executor/context.ts +9 -1
  72. package/vendor/base/src/manifests/executor/memory.ts +83 -3
  73. package/vendor/base/src/manifests/executor/session.ts +50 -0
  74. package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
  75. package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
  76. package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
  77. package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
  78. package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
  79. package/vendor/base/src/manifests/runtime/context.ts +18 -6
  80. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
  81. package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
  82. package/vendor/base/src/manifests/runtime/events.ts +39 -3
  83. package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
  84. package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
  85. package/vendor/base/src/manifests/runtime/index.ts +3 -2
  86. package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
  87. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
  88. package/vendor/base/src/manifests/runtime/usage.ts +10 -0
  89. package/vendor/base/src/paths.ts +16 -8
  90. package/vendor/base/src/platform/audioManager.ts +32 -0
  91. package/vendor/base/src/platform/executionContext.ts +14 -0
  92. package/vendor/base/src/platform/liveInput.ts +52 -0
  93. package/vendor/base/src/plugins/discovery.ts +1 -1
  94. package/vendor/base/src/template/SafeExpression.ts +6 -0
  95. package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
  96. package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
  97. package/vendor/sdk/appEngine.tsx +30 -11
  98. package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
  99. package/vendor/sdk/lib/app.tsx +70 -95
  100. package/vendor/sdk/lib/connection.tsx +47 -57
  101. package/vendor/sdk/lib/core/actions.ts +6 -0
  102. package/vendor/sdk/lib/core/apps.ts +66 -81
  103. package/vendor/sdk/lib/core/client.ts +5 -14
  104. package/vendor/sdk/lib/core/connection.ts +30 -0
  105. package/vendor/sdk/lib/core/index.ts +2 -2
  106. package/vendor/sdk/lib/core/store.ts +90 -15
  107. package/vendor/sdk/lib/core/types.ts +9 -8
  108. package/vendor/sdk/lib/primitives.tsx +113 -4
  109. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
  110. package/vendor/sdk/lib/render.tsx +3 -3
  111. package/vendor/sdk/lib/voice.tsx +27 -13
  112. package/local/sample-identity/brand/brand.yaml +0 -88
  113. package/local/sample-identity/brand/manifest.yaml +0 -13
  114. package/local/sample-identity/organisation/manifest.yaml +0 -15
  115. package/local/sample-identity/organisation/organisation.yaml +0 -53
  116. package/local/sample-identity/purpose/manifest.yaml +0 -12
  117. package/local/sample-identity/purpose/purpose.yaml +0 -46
  118. package/local/sample-identity/story/manifest.yaml +0 -12
  119. package/local/sample-identity/story/story.yaml +0 -63
  120. 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
- * (`invokeComponentAppNative`) — the SERVER renders the component + elicits, nothing
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 the INTERACTIVE-APP etiquette so the model promotes the UI
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 renders a UI the user " +
43
- "completes on screen. When you call it, reply with at most ONE short sentence inviting the user to it — " +
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
- if (item?.object_type !== "mcp") continue;
64
- if (item.metadata?.schema?.methods) {
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 `invokeComponentAppNative` (held native `/mcp`
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 `invokeComponentAppNative`, so on an app whose
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. A tool
5
- * wired in up front keeps the conversation going; one spatial surfaced mid-loop is a handoff
6
- * candidate, and whether it actually ends the turn depends on what came back.
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
- * Refined by RESULT shape: a dynamically-discovered tool whose result carries
36
- * `output` (or an explicit completed/error status) is an interactive app's SLOW
37
- * CALL — the user completed it and the answers ARE the result; the conversation
38
- * CONTINUES with them. Only the fire-and-forget handoff signature
39
- * ({ status: "started" } / unparseable) ends the turn.
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[], coreToolNames?: Set<string>): boolean {
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 (coreToolNames?.has(c.name)) return false;
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, hasDynamicHandoff, isTurnEndingHandoff } from "./handoff.js";
38
- export { rowComponentsFromResults, renderRowComponents, rankedRowIds, componentTypeFromAppUri, onEmptyRowDelivery } from "./rowComponents.js";
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 { invokeComponentAppNative, connectPlatformMcp } from "./invoke.js";
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: `invokeComponentAppNative` opens a client and makes a real
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
- * Node-MCP rows and path-A workflow apps do NOT come through here — they route in-process
9
- * via the caller's `api.callService`, because they have a node or a workflow to run. Only a
10
- * path-B component app, with nothing local to call, rides the wire.
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 invokeComponentAppNative(
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
- const output = result.structuredContent?.output;
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
- // Tell the calling model what the output MEANS and that the task is NOT done yet.
134
- // Without this the model summarises the answers and stops instead of using them —
135
- // observed live: it returned a generic reply and never re-searched (iteration with
136
- // toolCalls:0). The legacy MCPInvoker returned this same guidance; dropping it in the
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 (MEMORY_SPEC.md v3 § Serving: zero-call standing lane).
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
- const CONTEXT_FETCH_TIMEOUT_MS = 3_000;
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
- const workflowId = executionContext?.workflow?.id;
35
- if (!userId || !workflowId) {
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
- hasWorkflowId: !!workflowId,
44
+ hasOrg: !!org,
39
45
  });
40
46
  return "";
41
47
  }
42
48
 
43
- return fetchUserMemoryBlock(userId, workflowId, executionContext?.auth?.accessToken, logger);
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, workflowId: string, accessToken?: string, logger?: any): Promise<string> {
52
- if (!userId || !workflowId) return "";
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, workflowId });
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
- * cascade (see docs/architecture/memory/MEMORY_SPEC.md). Replaces the retired
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 || !workflowId || !conversationId) {
47
+ if (!userId || !org || !conversationId) {
44
48
  logger?.debug?.("[MemoryIngest] Skipped — incomplete identity", {
45
49
  hasUserId: !!userId,
46
- hasWorkflowId: !!workflowId,
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 || "",