@unoverse-platform/studio 0.1.115 → 0.1.116

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 (37) hide show
  1. package/local/catalog.ts +3 -0
  2. package/local/plugin.ts +12 -9
  3. package/package.json +1 -1
  4. package/screens/AgentChain.tsx +216 -0
  5. package/screens/AgentsView.tsx +45 -6
  6. package/screens/SkillsView.tsx +21 -3
  7. package/screens/index.ts +3 -0
  8. package/vendor/base/src/agent-mcp/discovery.ts +4 -1
  9. package/vendor/base/src/agent-mcp/invoke.ts +155 -25
  10. package/vendor/base/src/agent-mcp/tools.ts +17 -10
  11. package/vendor/base/src/agent-mcp/types.ts +8 -0
  12. package/vendor/base/src/auth/agentSwitch.ts +52 -0
  13. package/vendor/base/src/auth/agentToken.ts +142 -0
  14. package/vendor/base/src/auth/auth.ts +7 -0
  15. package/vendor/base/src/definitions/load/load.ts +6 -1
  16. package/vendor/base/src/definitions/registry/agentFolders.ts +113 -0
  17. package/vendor/base/src/definitions/registry/objects.ts +16 -0
  18. package/vendor/base/src/definitions/registry/pipelines.ts +13 -5
  19. package/vendor/base/src/definitions/registry/taskStarts.ts +76 -0
  20. package/vendor/base/src/definitions/registry/taskUri.ts +67 -0
  21. package/vendor/base/src/lint/design/index.mjs +10 -2
  22. package/vendor/base/src/lint/objects/index.d.mts +1 -0
  23. package/vendor/base/src/lint/objects/index.mjs +120 -2
  24. package/vendor/base/src/lint/objects/rules.mjs +16 -5
  25. package/vendor/base/src/manifests/executor/toolBridge.ts +10 -3
  26. package/vendor/base/src/manifests/marketplaceMcp.ts +20 -0
  27. package/vendor/base/src/manifests/runtime/http/request.ts +4 -1
  28. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +58 -23
  29. package/vendor/base/src/manifests/runtime/usage.ts +13 -5
  30. package/vendor/base/src/platform/agentBudget.ts +65 -0
  31. package/vendor/base/src/platform/agentCalendar.ts +87 -0
  32. package/vendor/base/src/platform/taskChanged.ts +25 -0
  33. package/vendor/base/src/platform/tokenCost.ts +395 -0
  34. package/vendor/base/src/security/permissions.ts +10 -2
  35. package/vendor/sdk/appEngine.ts +42 -1
  36. package/vendor/sdk/lib/dom/markdown.ts +6 -2
  37. package/vendor/sdk/pageInterfaces.ts +109 -8
@@ -18,6 +18,7 @@
18
18
  * The frame shares the page's origin (EMBED_CLIENT.md §1), so the page's document is reachable
19
19
  * from here, and a part drawn into the page is as live as one drawn in the frame.
20
20
  */
21
+ import { taskUri, taskUriOf } from "../../../../../packages/base/src/definitions/registry/taskUri.js";
21
22
  import { ComponentStore } from "./lib/core/store";
22
23
  import type { UnoverseDefinition } from "./lib/core/types";
23
24
  import type { ActionHandler, ActionMeta } from "./lib/actions";
@@ -25,6 +26,20 @@ import { mountComponent, type ComponentMount, type DefinitionSource } from "./li
25
26
  import type { VoiceService } from "./lib/voiceService";
26
27
 
27
28
  const MARK = "data-unoverse-interface";
29
+ /** WHAT THE PAGE HANDS A TEMPLATE'S TASK (docs/design/lifecycle-hooks.md): a JSON object on the
30
+ * marked element, such as the id in the page's address. Its task reads it as `props.input`;
31
+ * the page hands values, never calls. */
32
+ const INPUT = "data-unoverse-input";
33
+ function inputOf(el: HTMLElement): Record<string, unknown> | undefined {
34
+ const raw = el.getAttribute(INPUT);
35
+ if (!raw) return undefined;
36
+ try {
37
+ const v = JSON.parse(raw);
38
+ return v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : undefined;
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
28
43
  const CHAT = "page";
29
44
  const TEMPLATE_NODE = "~template";
30
45
 
@@ -60,6 +75,17 @@ function keyOwner(doc: Document): HTMLElement | null {
60
75
 
61
76
  /** A page template's address, from the name its element is marked with. */
62
77
  const uriOf = (name: string) => (name.includes("://") ? name : `unoverse://templates/${name}`);
78
+ /**
79
+ * THE TASK A PAGE TEMPLATE IS, by its one URI (docs/mcp/MCP_TASKS.md § 2a): the template's own
80
+ * address (`unoverse://templates/<org>/<name>`, what draws it) names the task
81
+ * `unoverse://tasks/<org>/<name>`, which is what starts it (`metadata.task`) and what its progress
82
+ * is published under (`tasks.<uri>`). The shared parser, bundled, so the page and the platform
83
+ * read a task's name one way.
84
+ */
85
+ const taskOf = (uri: string): string => {
86
+ const short = uri.replace(/^unoverse:\/\/templates\//, "");
87
+ return taskUri(short) ?? taskUriOf(null, short) ?? uri;
88
+ };
63
89
 
64
90
  /** THE AVATARS: page templates whose steps are the call's phases (`listening`, `speaking`).
65
91
  * They are how the page is spoken to, so they are never the task in view; they follow the call. */
@@ -67,7 +93,7 @@ const avatars = new Set<HTMLElement>();
67
93
  const CALL_PHASES = ["listening", "speaking"];
68
94
 
69
95
  /** The workflow a template names in its manifest (UNOVERSE_HOST_PAGE.md rule 4). */
70
- export type Binding = { workflow?: string; trigger?: string; pipeline?: string };
96
+ export type Binding = { workflow?: string; trigger?: string; pipeline?: string; autoTrigger?: boolean };
71
97
 
72
98
  /** THE TASK IN VIEW: the marked template, not an avatar, spanning the middle of the screen. */
73
99
  function taskInView(doc: Document): HTMLElement | null {
@@ -121,6 +147,8 @@ interface PageTemplateOptions {
121
147
  waiting: () => boolean;
122
148
  onBinding: (uri: string, binding: Binding) => void;
123
149
  onCall: ActionHandler;
150
+ /** Run the task this template carries (its lifecycle hook) as the viewer; its fields come back. */
151
+ runTask: (uri: string, phase: "onStart", input?: Record<string, unknown>) => Promise<Record<string, unknown>>;
124
152
  }
125
153
 
126
154
  /** One marked template: read it, give it its parts, draw it. */
@@ -132,6 +160,12 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
132
160
  let def: { stateTree?: StateTree } | null = null;
133
161
  let comp: ComponentMount | null = null;
134
162
  let alive = true;
163
+ // THE TASK IT CARRIES, ASKED AT ONCE (docs/design/lifecycle-hooks.md): in parallel with reading
164
+ // the template, not after it. A template without an `onStart` hook answers `{}` and is ignored.
165
+ const early = o.runTask(uri, "onStart", inputOf(el)).catch((e) => {
166
+ console.warn(`[unoverse] page template "${uri}" task failed:`, e);
167
+ return {} as Record<string, unknown>;
168
+ });
135
169
 
136
170
  // Where the template draws: its element, or the dock while a fullscreen template holds one
137
171
  // and this template is an avatar.
@@ -142,10 +176,14 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
142
176
  };
143
177
 
144
178
  const ready = () => {
145
- comp = mountComponent({ client, store, chatId: CHAT, nodeId: TEMPLATE_NODE, onAction: o.onCall, place: () => [dock.slot], appStore, doc: el.ownerDocument, arrowKeys: false });
146
- place();
147
- stops.push(dock.watch(place), store.subscribe(() => dock.check()));
148
- dock.check();
179
+ // DRAWN WHEN ITS DATA IS IN: a template with a task shows nothing until the task answers, so
180
+ // its sample values (the preview a studio draws) never flash on a live page.
181
+ const show = () => {
182
+ comp = mountComponent({ client, store, chatId: CHAT, nodeId: TEMPLATE_NODE, onAction: o.onCall, place: () => [dock.slot], appStore, doc: el.ownerDocument, arrowKeys: false });
183
+ place();
184
+ stops.push(dock.watch(place), store.subscribe(() => dock.check()));
185
+ dock.check();
186
+ };
149
187
 
150
188
  // A TEMPLATE FOLLOWS ITS TASK. Whatever runs the task publishes where it is as
151
189
  // `tasks.<its URI>` in the app state (SIGNAL.md 6c): this template moves to that step, and its
@@ -161,6 +199,47 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
161
199
  for (const nodeId of parts.filter((id) => id.startsWith(`${part}~`))) store.mergeComponentState(CHAT, nodeId, values as Record<string, unknown>);
162
200
  }
163
201
  };
202
+ // A TASK'S VALUES THAT NAME NO PART are the template's own props (a list it holds, a line
203
+ // above it): they fill the template itself, as a part's fill its part.
204
+ const fillOwn = (byPart: Record<string, unknown>) => {
205
+ const own = Object.fromEntries(Object.entries(byPart).filter(([key]) => key !== "state" && !parts.some((id) => id.startsWith(`${key}~`))));
206
+ if (!Object.keys(own).length) return;
207
+ syncHeld({ ...((store.get(CHAT, TEMPLATE_NODE) ?? {}) as Record<string, unknown>), ...own });
208
+ store.mergeComponentState(CHAT, TEMPLATE_NODE, own);
209
+ };
210
+
211
+ // WHAT THE TEMPLATE HOLDS (docs/design/templates.md "Holding a component"): one instance of
212
+ // the held component per item of the template's own data, keyed as the server keys it in an
213
+ // app (server runtime/screen/held.ts) and as `heldPointer` looks it up, so each card draws in
214
+ // its spot and keeps its own state. Re-sent only when an item changes. MADE BEFORE the data
215
+ // that names them reaches the template: a spot looks for its instance as it draws, once.
216
+ const heldSpecs = Object.values(((def as { held?: Record<string, { component: string; items: string[] }[]> } | null)?.held) ?? {}).flat();
217
+ const lastHeld = new Map<string, string>();
218
+ const syncHeld = (data: Record<string, unknown>) => {
219
+ if (!heldSpecs.length) return;
220
+ for (const spec of heldSpecs) {
221
+ if (!(spec.items[0] in data)) continue;
222
+ let level: unknown[] = [data];
223
+ for (const field of spec.items)
224
+ level = level.flatMap((o) => {
225
+ const list = o && typeof o === "object" ? (o as Record<string, unknown>)[field] : undefined;
226
+ return Array.isArray(list) ? list : [];
227
+ });
228
+ level.forEach((item, index) => {
229
+ if (!item || typeof item !== "object" || Array.isArray(item)) return;
230
+ const row = item as Record<string, unknown>;
231
+ const id = row.universal_id ?? row.ref ?? row.id;
232
+ const key = typeof id === "string" || typeof id === "number" ? String(id) : String(index);
233
+ const nodeId = `${TEMPLATE_NODE}:${spec.component}:${key}`;
234
+ const next = JSON.stringify(row);
235
+ if (lastHeld.get(nodeId) === next) return;
236
+ lastHeld.set(nodeId, next);
237
+ if (!store.getType(CHAT, nodeId)) store.apply({ type: "COMPONENT_INIT", chatId: CHAT, nodeId, component: { type: spec.component, props: row } });
238
+ else store.apply({ type: "COMPONENT_DATA", chatId: CHAT, nodeId, data: row });
239
+ });
240
+ }
241
+ };
242
+ syncHeld((store.get(CHAT, TEMPLATE_NODE) ?? {}) as Record<string, unknown>);
164
243
  const moveTo = (at: unknown) => {
165
244
  if (typeof at !== "string") return;
166
245
  const tree = def?.stateTree?.find((t) => t.on && t.states?.some((x) => x.name === at));
@@ -169,12 +248,13 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
169
248
  if (tpl?.[tree.on] !== at) store.mergeComponentState(CHAT, TEMPLATE_NODE, { [tree.on]: at });
170
249
  };
171
250
  const follow = () => {
172
- const task = ((appStore.getAppState().tasks ?? {}) as Record<string, { step?: string; props?: Record<string, unknown> }>)[uri];
251
+ const task = ((appStore.getAppState().tasks ?? {}) as Record<string, { step?: string; props?: Record<string, unknown> }>)[taskOf(uri)];
173
252
  if (task?.props && typeof task.props === "object") {
174
253
  const next = JSON.stringify(task.props);
175
254
  if (next !== lastProps) {
176
255
  lastProps = next;
177
256
  fillParts(task.props);
257
+ fillOwn(task.props);
178
258
  }
179
259
  }
180
260
  // A NEW STEP MOVES IT, once. A step the person took since (Enter) stands until the task
@@ -214,6 +294,18 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
214
294
  follow();
215
295
  stops.push(appStore.subscribe(follow));
216
296
 
297
+ // THE TASK IT CARRIES (docs/design/lifecycle-hooks.md): a template whose manifest has an
298
+ // `onStart` hook asks the server to run it as it loads, as the viewer, and fills its own
299
+ // fields with the answer, its held cards made first, as a workflow's fill does.
300
+ const hooks = (def as { lifecycle?: Array<{ phase?: string }> } | null)?.lifecycle ?? [];
301
+ if (hooks.some((h) => h.phase === "onStart"))
302
+ void early.then((fields) => {
303
+ if (!alive) return;
304
+ if (fields && Object.keys(fields).length) fillOwn(fields);
305
+ show();
306
+ });
307
+ else show();
308
+
217
309
  const doc = pageDocument();
218
310
  if (doc) {
219
311
  // THE ARROW KEYS STEP IT, on the PAGE (the keys are the page's), when its state says
@@ -336,6 +428,7 @@ export interface PageInterfacesOptions {
336
428
  appStore: ComponentStore;
337
429
  toServer: (t: string, data: Record<string, unknown>, meta?: ActionMeta) => void;
338
430
  startTask: (binding: Binding, task: string, submit?: Record<string, unknown>) => void;
431
+ runTask: (uri: string, phase: "onStart", input?: Record<string, unknown>) => Promise<Record<string, unknown>>;
339
432
  }
340
433
 
341
434
  /**
@@ -374,7 +467,7 @@ export function mountPageInterfaces(o: PageInterfacesOptions): { destroy(): void
374
467
  if (action === "submit" || action === "cancel") {
375
468
  o.toServer(action, data, meta);
376
469
  const state = (meta?.state ?? data) as Record<string, unknown> | undefined;
377
- if (action === "submit" && (bindings[uri]?.workflow || bindings[uri]?.pipeline) && state && typeof state === "object") o.startTask(bindings[uri], uri, state);
470
+ if (action === "submit" && (bindings[uri]?.workflow || bindings[uri]?.pipeline) && state && typeof state === "object") o.startTask(bindings[uri], taskOf(uri), state);
378
471
  } else if (action === "startCall") {
379
472
  const t = target();
380
473
  if (!t || started.has(t)) return;
@@ -382,7 +475,7 @@ export function mountPageInterfaces(o: PageInterfacesOptions): { destroy(): void
382
475
  refreshAll();
383
476
  // The press is the click the browser needs before it plays a voice the workflow may have.
384
477
  o.voice.unlock();
385
- o.startTask(bindings[t], t);
478
+ o.startTask(bindings[t], taskOf(t));
386
479
  } else if (action === "endCall") void o.voice.endCall();
387
480
  else if (action === "toggleMute") o.voice.toggleMute();
388
481
  // Another app on the page, opened or closed by its `<org>/<app>` (its own tag, EMBED_CLIENT.md).
@@ -425,9 +518,17 @@ export function mountPageInterfaces(o: PageInterfacesOptions): { destroy(): void
425
518
  onBinding: (u, b) => {
426
519
  if (bindings[u]) return;
427
520
  bindings[u] = b;
521
+ // A TASK THAT STARTS ITSELF (UNOVERSE_HOST_PAGE.md rule 4b): a binding that says
522
+ // `autoTrigger: true` starts its task once, as the page draws the template, the way
523
+ // an app's does on load. Nothing to press, and never twice on one page.
524
+ if (b.autoTrigger && !started.has(u)) {
525
+ started.add(u);
526
+ o.startTask(b, u);
527
+ }
428
528
  refreshAll();
429
529
  },
430
530
  onCall: onCallFor(uri),
531
+ runTask: o.runTask,
431
532
  }),
432
533
  );
433
534
  }