@datalayer/agent-runtimes 1.3.29 → 1.3.30

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 (52) hide show
  1. package/lib/AgentNode.js +14 -33
  2. package/lib/chat/base/ChatBase.js +41 -32
  3. package/lib/chat/base/modelChoice.d.ts +13 -0
  4. package/lib/chat/base/modelChoice.js +13 -0
  5. package/lib/loop/apps/AppRenderer.d.ts +33 -1
  6. package/lib/loop/apps/AppRenderer.js +96 -29
  7. package/lib/loop/apps/agent.d.ts +15 -0
  8. package/lib/loop/apps/agent.js +42 -0
  9. package/lib/loop/apps/index.d.ts +1 -0
  10. package/lib/loop/apps/index.js +1 -0
  11. package/lib/loop/core/index.d.ts +8 -0
  12. package/lib/loop/embed/AppEmbed.d.ts +42 -0
  13. package/lib/loop/embed/AppEmbed.js +205 -0
  14. package/lib/loop/embed/datalayer-app.d.ts +1 -0
  15. package/lib/loop/embed/datalayer-app.js +16 -0
  16. package/lib/loop/embed/element.d.ts +57 -0
  17. package/lib/loop/embed/element.js +405 -0
  18. package/lib/loop/embed/embedConfig.d.ts +120 -0
  19. package/lib/loop/embed/embedConfig.js +148 -0
  20. package/lib/loop/embed/embedTheme.d.ts +24 -0
  21. package/lib/loop/embed/embedTheme.js +83 -0
  22. package/lib/loop/index.d.ts +6 -2
  23. package/lib/loop/index.js +8 -2
  24. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +21 -3
  25. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +34 -6
  26. package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
  27. package/lib/loop/plugins/agent-capacity/index.js +3 -1
  28. package/lib/loop/plugins/agents/switchable.d.ts +7 -0
  29. package/lib/loop/plugins/agents/switchable.js +91 -88
  30. package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
  31. package/lib/loop/plugins/app-page/AppPage.js +53 -0
  32. package/lib/loop/plugins/app-page/appPage.d.ts +180 -0
  33. package/lib/loop/plugins/app-page/appPage.js +443 -0
  34. package/lib/loop/plugins/app-page/index.d.ts +8 -0
  35. package/lib/loop/plugins/app-page/index.js +34 -0
  36. package/lib/loop/plugins/chat/ChatView.js +47 -25
  37. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
  38. package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
  39. package/lib/loop/plugins/page-layout/index.d.ts +13 -0
  40. package/lib/loop/plugins/page-layout/index.js +10 -1
  41. package/lib/loop/presets.d.ts +12 -0
  42. package/lib/loop/presets.js +5 -3
  43. package/lib/specs/actions.js +8 -0
  44. package/lib/specs/agents/agents.js +4 -0
  45. package/lib/specs/apps.d.ts +10 -1
  46. package/lib/specs/apps.js +1792 -178
  47. package/lib/specs/appspecSchema.js +1 -1
  48. package/lib/types/agentspecs.d.ts +11 -0
  49. package/lib/types/config.d.ts +12 -0
  50. package/package.json +2 -1
  51. package/scripts/codegen/generate_agents.py +50 -2
  52. package/scripts/codegen/generate_apps.py +36 -6
@@ -565,7 +565,7 @@ export const APPSPEC_SCHEMA = {
565
565
  },
566
566
  composed_by: {
567
567
  default: '',
568
- description: "Who composed it: a model's id, `developer`, `template`",
568
+ description: "Who composed it: a model's id, `canvas` (a person on the Canvas), `developer`, `template`",
569
569
  title: 'Composed By',
570
570
  type: 'string',
571
571
  },
@@ -372,6 +372,12 @@ export interface ServerActionsSpec {
372
372
  }
373
373
  /** What kind of application it is: what its user meets. */
374
374
  export type AppKind = 'chat' | 'widget' | 'decision' | 'worker';
375
+ /**
376
+ * How an application was built: in Python (its `app.py`), on the Canvas (its
377
+ * page composed there) or written out as its spec. Carried beside the
378
+ * catalogue (`APP_BUILT`), since an Appspec does not hold it.
379
+ */
380
+ export type AppBuilt = 'python' | 'canvas' | 'written';
375
381
  /** What an application does when it meets an action. */
376
382
  export type AppBehaviour = 'do_it' | 'if_asked' | 'ask_first' | 'leave_to_me';
377
383
  export type AppLayout = 'chat' | 'page' | 'split';
@@ -595,6 +601,11 @@ export interface Agentspec {
595
601
  enabled: boolean;
596
602
  /** AI model identifier to use for this agent */
597
603
  model?: string;
604
+ /**
605
+ * Other models of the catalogue this agent may be switched to, beside its
606
+ * `model`. The runtime offers those of them its inference serves.
607
+ */
608
+ modelAdditionals?: string[];
598
609
  /** Inference provider routing strategy */
599
610
  inferenceProvider?: 'local' | 'datalayer';
600
611
  /** MCP servers used by this agent */
@@ -45,7 +45,19 @@ export interface AgentConfig {
45
45
  * Remote configuration from server
46
46
  */
47
47
  export interface RemoteConfig {
48
+ /**
49
+ * The models on offer — for an agent, its `model` and `model_additionals` —
50
+ * each saying whether it can be used.
51
+ */
48
52
  models: ModelConfig[];
53
+ /**
54
+ * Who decided which models can be used: ai-inference's own list, or the
55
+ * runtime's configuration when it did not route through ai-inference or
56
+ * ai-inference did not answer.
57
+ */
58
+ modelsSource?: 'ai-inference' | 'local';
59
+ /** That decision in a sentence. */
60
+ modelsNote?: string;
49
61
  defaultModel?: string;
50
62
  builtinTools: BuiltinTool[];
51
63
  mcpServers?: MCPServerConfig[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.3.29",
3
+ "version": "1.3.30",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",
@@ -56,6 +56,7 @@
56
56
  "build": "npm run clean && gulp resources-to-lib && tsc -b && cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build",
57
57
  "build-storybook": "storybook build",
58
58
  "build:all": "npm run build && npm run build:examples",
59
+ "build:embed": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --config vite.embed.config.ts",
59
60
  "build:examples": "npm run build:nextjs",
60
61
  "build:lib": "npm run clean:lib && gulp resources-to-lib && tsc -b",
61
62
  "build:nextjs": "npm run build --workspace=nextjs-notebook-example",
@@ -276,9 +276,44 @@ def load_yaml_specs(specs_dir: Path) -> List[tuple[str, Dict[str, Any]]]:
276
276
  spec = resolve_spec(spec, by_id, fragments_by_id)
277
277
  resolved.append((subfolder, spec))
278
278
 
279
+ check_model_additionals(resolved, load_model_ids(specs_dir))
279
280
  return resolved
280
281
 
281
282
 
283
+ def load_model_ids(specs_dir: Path) -> set[str]:
284
+ """Every id the models catalogue answers to: its ids and their aliases."""
285
+ ids: set[str] = set()
286
+ for yaml_file in sorted((specs_dir.parent / "models").glob("*.yaml")):
287
+ with open(yaml_file, "r") as f:
288
+ model = yaml.safe_load(f) or {}
289
+ if model.get("id"):
290
+ ids.add(str(model["id"]))
291
+ ids.update(str(alias) for alias in model.get("aliases") or [])
292
+ return ids
293
+
294
+
295
+ def check_model_additionals(
296
+ specs: List[tuple[str, Dict[str, Any]]], model_ids: set[str]
297
+ ) -> None:
298
+ """Refuse an agent whose `model_additionals` names a model the catalogue does not know."""
299
+ problems = []
300
+ for _, spec in specs:
301
+ additionals = spec.get("model_additionals")
302
+ if additionals is None:
303
+ continue
304
+ if not isinstance(additionals, list):
305
+ problems.append(f"{spec.get('id')}: model_additionals is not a list")
306
+ continue
307
+ unknown = [str(m) for m in additionals if str(m) not in model_ids]
308
+ if unknown:
309
+ problems.append(
310
+ f"{spec.get('id')}: model_additionals names models the catalogue "
311
+ f"does not know: {', '.join(unknown)}"
312
+ )
313
+ if problems:
314
+ raise SystemExit("\n".join(problems))
315
+
316
+
282
317
  def generate_python_code(specs: List[tuple[str, Dict[str, Any]]]) -> str:
283
318
  """Generate Python code from agent specifications."""
284
319
  # Header
@@ -423,6 +458,12 @@ from agent_runtimes.types import (
423
458
  # Model field
424
459
  model_id = spec.get("model")
425
460
  model_str = f'"{model_id}"' if model_id else "None"
461
+ model_additionals = spec.get("model_additionals") or []
462
+ model_additionals_py_line = (
463
+ f" model_additionals={_fmt_list(model_additionals)},\n"
464
+ if model_additionals
465
+ else ""
466
+ )
426
467
  inference_provider = spec.get("inference_provider")
427
468
  inference_provider_str = (
428
469
  f'"{inference_provider}"' if inference_provider else "None"
@@ -547,7 +588,7 @@ from agent_runtimes.types import (
547
588
  domain={f'"{domain_value}"' if domain_value else "None"},
548
589
  enabled={spec.get("enabled", True)},
549
590
  model={model_str},
550
- inference_provider={inference_provider_str},
591
+ {model_additionals_py_line} inference_provider={inference_provider_str},
551
592
  mcp_servers=[{mcp_servers_str}],
552
593
  skills={_fmt_list(skill_refs)},
553
594
  tools={_fmt_list(tool_refs)},
@@ -1045,6 +1086,12 @@ const FRONTEND_TOOL_MAP: Record<string, any> = {
1045
1086
  # Model field
1046
1087
  model_id = spec.get("model")
1047
1088
  model_ts = f"'{model_id}'" if model_id else "undefined"
1089
+ model_additionals = spec.get("model_additionals") or []
1090
+ model_additionals_ts_line = (
1091
+ f" modelAdditionals: {_fmt_ts_literal(model_additionals)},\n"
1092
+ if model_additionals
1093
+ else ""
1094
+ )
1048
1095
  inference_provider = spec.get("inference_provider")
1049
1096
  inference_provider_ts = (
1050
1097
  f"'{inference_provider}'" if inference_provider else "undefined"
@@ -1116,7 +1163,7 @@ const FRONTEND_TOOL_MAP: Record<string, any> = {
1116
1163
  domain: {domain_ts},
1117
1164
  enabled: {str(spec.get("enabled", True)).lower()},
1118
1165
  model: {model_ts},
1119
- {inference_provider_line} mcpServers: [{mcp_servers_str}],
1166
+ {model_additionals_ts_line}{inference_provider_line} mcpServers: [{mcp_servers_str}],
1120
1167
  skills: [{skills_str}].filter(Boolean) as SkillSpec[],
1121
1168
  tools: [{tools_str}],
1122
1169
  {disable_tool_approvals_line} frontendTools: [{frontend_tools_str}],
@@ -1588,6 +1635,7 @@ export * from './tracks';
1588
1635
  export * from './ops';
1589
1636
  export * from './apps';
1590
1637
  export * from './actions';
1638
+ export * from './appspecSchema';
1591
1639
  """
1592
1640
  )
1593
1641
 
@@ -17,6 +17,11 @@ read, write, send, buy, delete, publish — read from ``agentspecs.actions``.
17
17
  Each application is validated here, by ``agentspecs`` itself, and arrives
18
18
  with its layout said and with what it names that is not enabled (`setup`).
19
19
 
20
+ How each was built travels beside it (``APP_BUILT``), since an Appspec does
21
+ not hold it: ``python`` when ``apps/<id>/app.py`` sits beside its spec
22
+ (agentspecs >= 0.0.15), ``canvas`` when its page says it was composed on the
23
+ Canvas (``composed_by: canvas``, agentspecs >= 0.0.23), else ``written``.
24
+
20
25
  Usage:
21
26
  python generate_apps.py \\
22
27
  --specs-dir agentspecs/agentspecs/apps \\
@@ -57,6 +62,16 @@ def _flat(text: Any) -> str:
57
62
  return " ".join(str(text or "").split())
58
63
 
59
64
 
65
+ def built_of(specs_dir: Path, identity: str, app: Any) -> str:
66
+ """How an application was built: `python`, `canvas` or `written`."""
67
+ if (specs_dir / identity / "app.py").is_file():
68
+ return "python"
69
+ surface = app.interface.surface
70
+ if surface is not None and surface.composed_by == "canvas":
71
+ return "canvas"
72
+ return "written"
73
+
74
+
60
75
  def load_specs(specs_dir: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
61
76
  """Every application, validated, as plain data — and the action classes."""
62
77
  apps_module = import_from_clone(specs_dir, "apps")
@@ -66,6 +81,7 @@ def load_specs(specs_dir: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
66
81
  behaviours: dict[str, dict[str, str]] = {}
67
82
  escalations: dict[str, dict[str, Any]] = {}
68
83
  sources: dict[str, Any] = {}
84
+ built: dict[str, str] = {}
69
85
  for identity in sorted(apps):
70
86
  app = apps[identity]
71
87
  spec = app.model_dump(mode="json", by_alias=True)
@@ -78,6 +94,7 @@ def load_specs(specs_dir: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
78
94
  }
79
95
  escalations[identity] = dict(sorted(apps_module.tool_escalations(app).items()))
80
96
  sources[identity] = apps_module.dump_app(app)
97
+ built[identity] = built_of(specs_dir, identity, app)
81
98
  for rule in spec["rules"]:
82
99
  rule["applies_to"] = (
83
100
  [rule["applies_to"]]
@@ -129,6 +146,7 @@ def load_specs(specs_dir: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
129
146
  "behaviours": behaviours,
130
147
  "escalations": escalations,
131
148
  "sources": sources,
149
+ "built": built,
132
150
  }
133
151
  return specs, actions
134
152
 
@@ -174,7 +192,7 @@ ABOUT_ACTIONS = [
174
192
  ]
175
193
 
176
194
 
177
- def generate_python_code(specs: list[dict[str, Any]]) -> str:
195
+ def generate_python_code(specs: list[dict[str, Any]], built: dict[str, str]) -> str:
178
196
  """Generate the Python application catalogue."""
179
197
  lines = [
180
198
  "# Copyright (c) 2025-2026 Datalayer, Inc.",
@@ -185,7 +203,7 @@ def generate_python_code(specs: list[dict[str, Any]]) -> str:
185
203
  *HEADER,
186
204
  '"""',
187
205
  "",
188
- "from typing import Dict",
206
+ "from typing import Dict, Literal",
189
207
  "",
190
208
  "from agent_runtimes.types import AppSpec",
191
209
  "",
@@ -215,6 +233,10 @@ def generate_python_code(specs: list[dict[str, Any]]) -> str:
215
233
  [
216
234
  "}",
217
235
  "",
236
+ "#: How each application was built: `python` (its `app.py`), `canvas` (its",
237
+ "#: page composed on the Canvas) or `written` (its spec written out).",
238
+ f"APP_BUILT: Dict[str, Literal['python', 'canvas', 'written']] = {built!r}",
239
+ "",
218
240
  "",
219
241
  "def get_app(app_id: str) -> AppSpec | None:",
220
242
  ' """An application, by `id` or `id:version`, or None."""',
@@ -235,7 +257,7 @@ def generate_python_code(specs: list[dict[str, Any]]) -> str:
235
257
 
236
258
 
237
259
  def generate_typescript_code(
238
- specs: list[dict[str, Any]], sources: dict[str, Any]
260
+ specs: list[dict[str, Any]], sources: dict[str, Any], built: dict[str, str]
239
261
  ) -> str:
240
262
  """Generate the TypeScript application catalogue."""
241
263
  lines = [
@@ -250,7 +272,7 @@ def generate_typescript_code(
250
272
  *[f" * {line}" for line in HEADER],
251
273
  " */",
252
274
  "",
253
- "import type { AppKind, AppSpec } from '../types/agentspecs';",
275
+ "import type { AppBuilt, AppKind, AppSpec } from '../types/agentspecs';",
254
276
  "",
255
277
  ]
256
278
  for spec in specs:
@@ -288,6 +310,14 @@ def generate_typescript_code(
288
310
  + json.dumps(sources, indent=2, ensure_ascii=False)
289
311
  + ";",
290
312
  "",
313
+ "/**",
314
+ " * How each application was built: `python` (its `app.py`), `canvas` (its",
315
+ " * page composed on the Canvas) or `written` (its spec written out).",
316
+ " */",
317
+ "export const APP_BUILT: Record<string, AppBuilt> = "
318
+ + json.dumps(built, indent=2)
319
+ + ";",
320
+ "",
291
321
  "/** Every application of the catalogue, or those of a kind. */",
292
322
  "export function listApps(kind?: AppKind): AppSpec[] {",
293
323
  " return Object.values(APP_CATALOGUE).filter(",
@@ -475,10 +505,10 @@ def main() -> None:
475
505
  sys.exit(1)
476
506
  specs, actions = load_specs(args.specs_dir)
477
507
  outputs = [
478
- (args.python_output, generate_python_code(specs)),
508
+ (args.python_output, generate_python_code(specs, actions["built"])),
479
509
  (
480
510
  args.typescript_output,
481
- generate_typescript_code(specs, actions["sources"]),
511
+ generate_typescript_code(specs, actions["sources"], actions["built"]),
482
512
  ),
483
513
  (args.actions_python_output, generate_actions_python_code(actions)),
484
514
  (args.actions_typescript_output, generate_actions_typescript_code(actions)),