@datalayer/agent-runtimes 1.3.15 → 1.3.17

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 (81) hide show
  1. package/README.md +1 -1
  2. package/lib/chat/base/ChatBase.js +3 -2
  3. package/lib/config/AgentConfiguration.d.ts +1 -1
  4. package/lib/loop/apps/AppRenderer.d.ts +24 -0
  5. package/lib/loop/apps/AppRenderer.js +101 -0
  6. package/lib/loop/apps/appspec.d.ts +69 -0
  7. package/lib/loop/apps/appspec.js +566 -0
  8. package/lib/loop/apps/checks.d.ts +21 -0
  9. package/lib/loop/apps/checks.js +556 -0
  10. package/lib/loop/apps/index.d.ts +10 -0
  11. package/lib/loop/apps/index.js +14 -0
  12. package/lib/loop/apps/rules.d.ts +64 -0
  13. package/lib/loop/apps/rules.js +289 -0
  14. package/lib/loop/apps/yaml.d.ts +17 -0
  15. package/lib/loop/apps/yaml.js +185 -0
  16. package/lib/loop/embed/LoopEmbed.d.ts +6 -1
  17. package/lib/loop/embed/LoopEmbed.js +3 -2
  18. package/lib/loop/index.d.ts +1 -0
  19. package/lib/loop/index.js +3 -0
  20. package/lib/loop/plugins/chat/ChatView.js +9 -4
  21. package/lib/loop/plugins/chat/index.d.ts +10 -0
  22. package/lib/loop/plugins/chat/index.js +1 -0
  23. package/lib/loop/presets.d.ts +6 -0
  24. package/lib/loop/presets.js +2 -1
  25. package/lib/loop/shell/LoopWorkspace.d.ts +14 -0
  26. package/lib/loop/shell/LoopWorkspace.js +20 -6
  27. package/lib/loop/shell/index.d.ts +1 -1
  28. package/lib/loop/shell/index.js +1 -1
  29. package/lib/loop/shell/useWorkspaceFullScreen.d.ts +17 -4
  30. package/lib/loop/shell/useWorkspaceFullScreen.js +23 -6
  31. package/lib/specs/actions.d.ts +30 -0
  32. package/lib/specs/actions.js +616 -0
  33. package/lib/specs/agents/agents.d.ts +1 -0
  34. package/lib/specs/agents/agents.js +338 -284
  35. package/lib/specs/apps.d.ts +25 -0
  36. package/lib/specs/apps.js +1064 -0
  37. package/lib/specs/cogs.d.ts +19 -0
  38. package/lib/specs/cogs.js +736 -0
  39. package/lib/specs/events.d.ts +6 -1
  40. package/lib/specs/events.js +144 -1
  41. package/lib/specs/frames.d.ts +19 -0
  42. package/lib/specs/frames.js +444 -0
  43. package/lib/specs/gates.d.ts +22 -0
  44. package/lib/specs/gates.js +177 -0
  45. package/lib/specs/guards.d.ts +26 -0
  46. package/lib/specs/guards.js +930 -0
  47. package/lib/specs/index.d.ts +10 -0
  48. package/lib/specs/index.js +10 -0
  49. package/lib/specs/mcpServers.js +2 -2
  50. package/lib/specs/memory.js +4 -0
  51. package/lib/specs/modelProviders.d.ts +22 -0
  52. package/lib/specs/modelProviders.js +102 -0
  53. package/lib/specs/models.d.ts +31 -0
  54. package/lib/specs/models.js +215 -0
  55. package/lib/specs/ops.d.ts +15 -0
  56. package/lib/specs/ops.js +1272 -0
  57. package/lib/specs/outputs.js +2 -2
  58. package/lib/specs/skills.js +1 -1
  59. package/lib/specs/teams/teams.js +18 -18
  60. package/lib/specs/tracks.d.ts +15 -0
  61. package/lib/specs/tracks.js +87 -0
  62. package/lib/specs/uiPlugins.d.ts +16 -0
  63. package/lib/specs/uiPlugins.js +40 -0
  64. package/lib/types/agentspecs.d.ts +453 -2
  65. package/lib/types/memory.d.ts +2 -0
  66. package/lib/types/models.d.ts +52 -0
  67. package/package.json +2 -1
  68. package/scripts/check-cloudflare-models.py +122 -0
  69. package/scripts/codegen/agentspecs_clone.py +53 -0
  70. package/scripts/codegen/compose.py +65 -0
  71. package/scripts/codegen/generate_agents.py +14 -4
  72. package/scripts/codegen/generate_apps.py +454 -0
  73. package/scripts/codegen/generate_cogs.py +308 -0
  74. package/scripts/codegen/generate_frames.py +278 -0
  75. package/scripts/codegen/generate_memory.py +2 -0
  76. package/scripts/codegen/generate_model_providers.py +246 -0
  77. package/scripts/codegen/generate_models.py +135 -4
  78. package/scripts/codegen/generate_ops.py +388 -0
  79. package/scripts/codegen/generate_sandbox_agents.py +1 -1
  80. package/scripts/codegen/generate_ui_plugins.py +226 -0
  81. package/scripts/gen_workers.py +2 -2
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2025-2026 Datalayer, Inc.
3
+ # Distributed under the terms of the Modified BSD License.
4
+
5
+ """
6
+ Generate Python and TypeScript code from YAML Op, Guard, Gate and Track specifications.
7
+
8
+ The accountability plane of agentspecs (>= 0.0.14): Guards check, Gates
9
+ decide, Tracks record, and an Op orchestrates Cogs under a validation
10
+ strategy made of the three. One YAML per spec under ``agentspecs/guards``,
11
+ ``gates``, ``tracks`` and ``ops``.
12
+
13
+ Each is resolved here, by ``agentspecs`` itself, so the generated catalogues
14
+ are flat: a Guard carries the guardrail it extends, a Gate the signals it
15
+ reads, a Track its retention in days, and an Op everything it names.
16
+
17
+ Usage:
18
+ python generate_ops.py --kind guards \\
19
+ --specs-dir agentspecs/agentspecs/guards \\
20
+ --python-output agent_runtimes/specs/guards.py \\
21
+ --typescript-output src/specs/guards.ts
22
+ """
23
+
24
+ import argparse
25
+ import json
26
+ import sys
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ from agentspecs_clone import import_from_clone
31
+ from versioning import ensure_spec_version, version_suffix
32
+
33
+ #: What each kind is called, and what its catalogue says of itself.
34
+ KINDS: dict[str, dict[str, str]] = {
35
+ "guards": {
36
+ "model": "GuardSpec",
37
+ "suffix": "GUARD",
38
+ "catalogue": "GUARD_CATALOGUE",
39
+ "one": "guard",
40
+ "One": "Guard",
41
+ "title": "Guard Catalog.",
42
+ "about": "Reusable checks: a Guard extends a guardrail — the policy it verifies.\nEvery Guard is resolved: it carries that guardrail's policy.",
43
+ },
44
+ "gates": {
45
+ "model": "GateSpec",
46
+ "suffix": "GATE",
47
+ "catalogue": "GATE_CATALOGUE",
48
+ "one": "gate",
49
+ "One": "Gate",
50
+ "title": "Gate Catalog.",
51
+ "about": "Decision points: what happens on what the Guards found.\nGuards check; Gates decide.",
52
+ },
53
+ "tracks": {
54
+ "model": "TrackSpec",
55
+ "suffix": "TRACK",
56
+ "catalogue": "TRACK_CATALOGUE",
57
+ "one": "track",
58
+ "One": "Track",
59
+ "title": "Track Catalog.",
60
+ "about": "What evidence a run keeps, for how long, and who may read it.",
61
+ },
62
+ "ops": {
63
+ "model": "OpSpec",
64
+ "suffix": "OP",
65
+ "catalogue": "OP_CATALOGUE",
66
+ "one": "op",
67
+ "One": "Op",
68
+ "title": "Op Catalog.",
69
+ "about": "Orchestrated, supervised workflows: Cogs, and how their work is verified.\nEvery Op is resolved: its Guards, its Gates and its Track are in it.",
70
+ },
71
+ }
72
+
73
+ #: The keys this generator owns, as TypeScript spells them. A guardrail's own
74
+ #: keys (`token_limits`, `per_run`, …) stay as `GuardrailSpec` declares them.
75
+ CAMEL = {
76
+ "max_retries": "maxRetries",
77
+ "retain_for": "retainFor",
78
+ "retention_days": "retentionDays",
79
+ "feeds_memory": "feedsMemory",
80
+ "frame_guards": "frameGuards",
81
+ "in_flight": "inFlight",
82
+ "post_run": "postRun",
83
+ }
84
+
85
+ #: A guardrail's permissions are written `read:data` and typed `read_data`.
86
+ PERMISSIONS = (
87
+ "read:data",
88
+ "write:data",
89
+ "execute:code",
90
+ "access:internet",
91
+ "send:email",
92
+ "deploy:production",
93
+ )
94
+
95
+
96
+ def _flat(text: Any) -> str:
97
+ return " ".join(str(text or "").split())
98
+
99
+
100
+ #: What a Guard that does not say gets: agentspecs validates these defaults
101
+ #: and leaves them out of what `resolve_guard` returns, and the generated
102
+ #: types require them.
103
+ GUARD_DEFAULTS: dict[str, Any] = {
104
+ "description": "",
105
+ "tags": [],
106
+ "signals": [],
107
+ "icon": "shield-check",
108
+ "emoji": "\U0001f6e1\ufe0f",
109
+ }
110
+
111
+
112
+ def _tidy_guard(guard: dict[str, Any]) -> dict[str, Any]:
113
+ """A resolved Guard with every field the generated types require."""
114
+ guard = dict(guard)
115
+ for key, default in GUARD_DEFAULTS.items():
116
+ if guard.get(key) is None:
117
+ guard[key] = list(default) if isinstance(default, list) else default
118
+ for key in ("description", "check"):
119
+ guard[key] = _flat(guard.get(key))
120
+ return guard
121
+
122
+
123
+ def load_specs(kind: str, specs_dir: Path) -> list[dict[str, Any]]:
124
+ """Every spec of a kind, resolved and validated, as plain data."""
125
+ module = import_from_clone(specs_dir, kind)
126
+ specs: list[dict[str, Any]] = []
127
+ if kind == "guards":
128
+ raw = module.load_raw_guards(specs_dir)
129
+ specs = [
130
+ _tidy_guard(module.resolve_guard(raw[identity])) for identity in sorted(raw)
131
+ ]
132
+ elif kind == "gates":
133
+ gates = module.load_gates(specs_dir)
134
+ specs = [
135
+ {
136
+ **gates[identity].model_dump(mode="json"),
137
+ "signals": gates[identity].signals,
138
+ }
139
+ for identity in sorted(gates)
140
+ ]
141
+ elif kind == "tracks":
142
+ tracks = module.load_tracks(specs_dir)
143
+ specs = [
144
+ {
145
+ **tracks[identity].model_dump(mode="json"),
146
+ "retention_days": tracks[identity].retention_days,
147
+ }
148
+ for identity in sorted(tracks)
149
+ ]
150
+ elif kind == "ops":
151
+ raw = module.load_raw_ops(specs_dir)
152
+ for identity in sorted(raw):
153
+ op = module.resolve_op(raw[identity])
154
+ op["guards"] = {
155
+ stage: [_tidy_guard(guard) for guard in guards]
156
+ for stage, guards in op["guards"].items()
157
+ }
158
+ op["supervisor"] = {
159
+ "model": op["supervisor"]["model"],
160
+ "instructions": _flat(op["supervisor"].get("instructions")),
161
+ }
162
+ op["goal"] = _flat(op.get("goal"))
163
+ op["color"] = op.get("color") or None
164
+ specs.append(op)
165
+ for spec in specs:
166
+ ensure_spec_version(spec)
167
+ spec["description"] = _flat(spec.get("description"))
168
+ return specs
169
+
170
+
171
+ def _const_name(kind: str, spec: dict[str, Any]) -> str:
172
+ """`schema-guard` 0.0.1 → `SCHEMA_GUARD_0_0_1`; `standard` → `STANDARD_TRACK_0_0_1`."""
173
+ suffix = KINDS[kind]["suffix"]
174
+ base = spec["id"].upper().replace("-", "_").replace(".", "_")
175
+ if not (base.endswith(f"_{suffix}") or base.startswith(f"{suffix}_")):
176
+ base += f"_{suffix}"
177
+ return base + version_suffix(spec["version"])
178
+
179
+
180
+ def _python_value(value: Any) -> Any:
181
+ """A resolved spec as the Python models read it."""
182
+ if isinstance(value, dict):
183
+ converted = {key: _python_value(item) for key, item in value.items()}
184
+ permissions = converted.get("permissions")
185
+ if isinstance(permissions, dict):
186
+ converted["permissions"] = {
187
+ (key.replace(":", "_") if key in PERMISSIONS else key): item
188
+ for key, item in permissions.items()
189
+ }
190
+ return converted
191
+ if isinstance(value, list):
192
+ return [_python_value(item) for item in value]
193
+ return value
194
+
195
+
196
+ def _typescript_value(value: Any) -> Any:
197
+ """A resolved spec as the TypeScript interfaces read it; nulls left out."""
198
+ if isinstance(value, dict):
199
+ return {
200
+ CAMEL.get(key, key): _typescript_value(item)
201
+ for key, item in value.items()
202
+ if item is not None
203
+ }
204
+ if isinstance(value, list):
205
+ return [_typescript_value(item) for item in value]
206
+ return value
207
+
208
+
209
+ def generate_python_code(kind: str, specs: list[dict[str, Any]]) -> str:
210
+ """Generate Python code for one kind."""
211
+ names = KINDS[kind]
212
+ lines = [
213
+ "# Copyright (c) 2025-2026 Datalayer, Inc.",
214
+ "# Distributed under the terms of the Modified BSD License.",
215
+ '"""',
216
+ names["title"],
217
+ "",
218
+ *names["about"].split("\n"),
219
+ "",
220
+ "This file is AUTO-GENERATED from YAML specifications.",
221
+ "DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
222
+ '"""',
223
+ "",
224
+ "from typing import Dict",
225
+ "",
226
+ f"from agent_runtimes.types import {names['model']}",
227
+ "",
228
+ "",
229
+ "# " + "=" * 76,
230
+ f"# {names['One']} Definitions",
231
+ "# " + "=" * 76,
232
+ "",
233
+ ]
234
+ for spec in specs:
235
+ lines.extend(
236
+ [
237
+ f"{_const_name(kind, spec)} = {names['model']}.model_validate(",
238
+ f" {_python_value(spec)!r}",
239
+ ")",
240
+ "",
241
+ ]
242
+ )
243
+ article = "An" if kind == "ops" else "A"
244
+ lines.extend(
245
+ [
246
+ "",
247
+ "# " + "=" * 76,
248
+ f"# {names['One']} Catalog",
249
+ "# " + "=" * 76,
250
+ "",
251
+ f"{names['catalogue']}: Dict[str, {names['model']}] = {{",
252
+ ]
253
+ )
254
+ for spec in specs:
255
+ lines.append(f" {spec['id']!r}: {_const_name(kind, spec)},")
256
+ lines.extend(
257
+ [
258
+ "}",
259
+ "",
260
+ "",
261
+ f"def get_{names['one']}({names['one']}_id: str) -> {names['model']} | None:",
262
+ f' """{article} {names["One"]}, by `id` or `id:version`, or None."""',
263
+ f" found = {names['catalogue']}.get({names['one']}_id)",
264
+ " if found is not None:",
265
+ " return found",
266
+ f' base, _, version = {names["one"]}_id.rpartition(":")',
267
+ f' return {names["catalogue"]}.get(base) if base and "." in version else None',
268
+ "",
269
+ "",
270
+ f"def list_{kind}() -> list[{names['model']}]:",
271
+ f' """Every {names["One"]} of the catalogue, resolved."""',
272
+ f" return list({names['catalogue']}.values())",
273
+ "",
274
+ ]
275
+ )
276
+ return "\n".join(lines)
277
+
278
+
279
+ def generate_typescript_code(kind: str, specs: list[dict[str, Any]]) -> str:
280
+ """Generate TypeScript code for one kind."""
281
+ names = KINDS[kind]
282
+ lines = [
283
+ "/*",
284
+ " * Copyright (c) 2025-2026 Datalayer, Inc.",
285
+ " * Distributed under the terms of the Modified BSD License.",
286
+ " */",
287
+ "",
288
+ "/**",
289
+ f" * {names['title']}",
290
+ " *",
291
+ *[f" * {line}" for line in names["about"].split("\n")],
292
+ " *",
293
+ " * This file is AUTO-GENERATED from YAML specifications.",
294
+ " * DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
295
+ " */",
296
+ "",
297
+ f"import type {{ {names['model']} }} from '../types/agentspecs';",
298
+ "",
299
+ ]
300
+ for spec in specs:
301
+ body = json.dumps(_typescript_value(spec), indent=2, ensure_ascii=False)
302
+ lines.extend(
303
+ [f"export const {_const_name(kind, spec)}: {names['model']} = {body};", ""]
304
+ )
305
+ lines.append(
306
+ f"export const {names['catalogue']}: Record<string, {names['model']}> = {{"
307
+ )
308
+ for spec in specs:
309
+ lines.append(f" {json.dumps(spec['id'])}: {_const_name(kind, spec)},")
310
+ article = "An" if kind == "ops" else "A"
311
+ lines.extend(
312
+ [
313
+ "};",
314
+ "",
315
+ f"/** {article} {names['One']}, by `id` or `id:version`, or undefined. */",
316
+ f"export function get{names['One']}(ref: string): {names['model']} | undefined {{",
317
+ f" // Own entries only: `constructor` and `toString` are not {names['One']}s.",
318
+ f" const own = (id: string): {names['model']} | undefined =>",
319
+ f" Object.prototype.hasOwnProperty.call({names['catalogue']}, id)",
320
+ f" ? {names['catalogue']}[id]",
321
+ " : undefined;",
322
+ " const at = ref.lastIndexOf(':');",
323
+ " return (",
324
+ " own(ref) ??",
325
+ " (at > 0 && ref.slice(at + 1).includes('.')",
326
+ " ? own(ref.slice(0, at))",
327
+ " : undefined)",
328
+ " );",
329
+ "}",
330
+ "",
331
+ f"export function list{names['One']}s(): {names['model']}[] {{",
332
+ f" return Object.values({names['catalogue']});",
333
+ "}",
334
+ "",
335
+ ]
336
+ )
337
+ return "\n".join(lines)
338
+
339
+
340
+ def main() -> None:
341
+ """Generate one kind's catalogues."""
342
+ parser = argparse.ArgumentParser(
343
+ description="Generate Python and TypeScript code from YAML Op, Guard, Gate and Track specifications"
344
+ )
345
+ parser.add_argument(
346
+ "--kind",
347
+ choices=sorted(KINDS),
348
+ required=True,
349
+ help="Which catalogue to generate",
350
+ )
351
+ parser.add_argument(
352
+ "--specs-dir",
353
+ type=Path,
354
+ required=True,
355
+ help="Directory containing the YAML files",
356
+ )
357
+ parser.add_argument(
358
+ "--python-output", type=Path, required=True, help="Output path for Python file"
359
+ )
360
+ parser.add_argument(
361
+ "--typescript-output",
362
+ type=Path,
363
+ required=True,
364
+ help="Output path for TypeScript file",
365
+ )
366
+ args = parser.parse_args()
367
+
368
+ if not args.specs_dir.exists():
369
+ print(f"Error: Specs directory not found: {args.specs_dir}", file=sys.stderr)
370
+ sys.exit(1)
371
+ specs = load_specs(args.kind, args.specs_dir)
372
+ if not specs:
373
+ print(
374
+ f"Warning: No {args.kind} specifications found in {args.specs_dir}",
375
+ file=sys.stderr,
376
+ )
377
+ return
378
+ args.python_output.parent.mkdir(parents=True, exist_ok=True)
379
+ args.python_output.write_text(generate_python_code(args.kind, specs))
380
+ print(f"Generated Python code: {args.python_output}")
381
+ args.typescript_output.parent.mkdir(parents=True, exist_ok=True)
382
+ args.typescript_output.write_text(generate_typescript_code(args.kind, specs))
383
+ print(f"Generated TypeScript code: {args.typescript_output}")
384
+ print(f"\n✓ Successfully generated code from {len(specs)} {args.kind} specs")
385
+
386
+
387
+ if __name__ == "__main__":
388
+ main()
@@ -73,7 +73,7 @@ tags:
73
73
  - code-execution
74
74
  - {key}
75
75
 
76
- enabled: true
76
+ enabled: false
77
77
  model: "bedrock:us.anthropic.claude-sonnet-4-6"
78
78
  harness: pydantic-ai
79
79
 
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2025-2026 Datalayer, Inc.
3
+ # Distributed under the terms of the Modified BSD License.
4
+
5
+ """
6
+ Generate Python and TypeScript code from YAML UI-plugin specifications.
7
+
8
+ A UI plugin is how an agent's answer becomes an interface rather than text —
9
+ a protocol the host knows how to render (A2UI, MCP Apps, MCP UI). One YAML per
10
+ plugin under ``agentspecs/ui-plugins``; an agent spec's ``ui_plugin`` names
11
+ one. They were called UI extensions before agentspecs 0.0.11.
12
+
13
+ Usage:
14
+ python generate_ui_plugins.py \\
15
+ --specs-dir agentspecs/agentspecs/ui-plugins \\
16
+ --python-output agent_runtimes/specs/ui_plugins.py \\
17
+ --typescript-output src/specs/uiPlugins.ts
18
+ """
19
+
20
+ import argparse
21
+ import sys
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ import yaml
26
+ from versioning import ensure_spec_version, version_suffix
27
+
28
+
29
+ def load_plugin_specs(specs_dir: Path) -> list[dict[str, Any]]:
30
+ """Load all UI-plugin YAML specifications from a directory."""
31
+ specs = []
32
+ for yaml_file in sorted(specs_dir.glob("*.yaml")):
33
+ with open(yaml_file) as f:
34
+ spec = yaml.safe_load(f)
35
+ ensure_spec_version(spec)
36
+ if spec["id"] != yaml_file.stem:
37
+ raise ValueError(
38
+ f"{yaml_file.name}: the file is named for id {yaml_file.stem!r}, the spec says {spec['id']!r}"
39
+ )
40
+ specs.append(spec)
41
+ return specs
42
+
43
+
44
+ def _const_name(plugin_id: str) -> str:
45
+ return plugin_id.replace("-", "_").replace(".", "_").upper() + "_UI_PLUGIN"
46
+
47
+
48
+ def _py(value: str) -> str:
49
+ return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
50
+
51
+
52
+ def _ts(value: str) -> str:
53
+ return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"
54
+
55
+
56
+ def generate_python_code(specs: list[dict[str, Any]]) -> str:
57
+ """Generate Python code from UI-plugin specifications."""
58
+ lines = [
59
+ "# Copyright (c) 2025-2026 Datalayer, Inc.",
60
+ "# Distributed under the terms of the Modified BSD License.",
61
+ '"""',
62
+ "UI Plugin Catalog.",
63
+ "",
64
+ "How an agent's answer becomes an interface: the protocols a host renders.",
65
+ "",
66
+ "This file is AUTO-GENERATED from YAML specifications.",
67
+ "DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
68
+ '"""',
69
+ "",
70
+ "from typing import Dict",
71
+ "",
72
+ "from agent_runtimes.types import UIPluginSpec",
73
+ "",
74
+ "",
75
+ "# " + "=" * 76,
76
+ "# UI Plugin Definitions",
77
+ "# " + "=" * 76,
78
+ "",
79
+ ]
80
+ for spec in specs:
81
+ const = _const_name(spec["id"]) + version_suffix(spec["version"])
82
+ lines.extend(
83
+ [
84
+ f"{const} = UIPluginSpec(",
85
+ f" id={_py(spec['id'])},",
86
+ f" version={_py(str(spec['version']))},",
87
+ f" name={_py(spec['name'])},",
88
+ f" description={_py(str(spec.get('description', '')).strip())},",
89
+ f" docs_url={_py(str(spec.get('docs_url', '')))},",
90
+ f" enabled={bool(spec.get('enabled', True))},",
91
+ ")",
92
+ "",
93
+ ]
94
+ )
95
+ lines.extend(
96
+ [
97
+ "",
98
+ "# " + "=" * 76,
99
+ "# UI Plugin Catalog",
100
+ "# " + "=" * 76,
101
+ "",
102
+ "UI_PLUGIN_CATALOGUE: Dict[str, UIPluginSpec] = {",
103
+ ]
104
+ )
105
+ for spec in specs:
106
+ lines.append(
107
+ f" {_py(spec['id'])}: {_const_name(spec['id']) + version_suffix(spec['version'])},"
108
+ )
109
+ lines.extend(
110
+ [
111
+ "}",
112
+ "",
113
+ "",
114
+ "def get_ui_plugin(plugin_id: str) -> UIPluginSpec | None:",
115
+ ' """The plugin an agent spec\'s `ui_plugin` names, or None."""',
116
+ " return UI_PLUGIN_CATALOGUE.get(plugin_id)",
117
+ "",
118
+ "",
119
+ "def list_ui_plugins() -> list[UIPluginSpec]:",
120
+ " return list(UI_PLUGIN_CATALOGUE.values())",
121
+ "",
122
+ ]
123
+ )
124
+ return "\n".join(lines)
125
+
126
+
127
+ def generate_typescript_code(specs: list[dict[str, Any]]) -> str:
128
+ """Generate TypeScript code from UI-plugin specifications."""
129
+ lines = [
130
+ "/*",
131
+ " * Copyright (c) 2025-2026 Datalayer, Inc.",
132
+ " * Distributed under the terms of the Modified BSD License.",
133
+ " */",
134
+ "",
135
+ "/**",
136
+ " * UI Plugin Catalog.",
137
+ " *",
138
+ " * How an agent's answer becomes an interface: the protocols a host renders.",
139
+ " *",
140
+ " * This file is AUTO-GENERATED from YAML specifications.",
141
+ " * DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
142
+ " */",
143
+ "",
144
+ "import type { UIPluginSpec } from '../types/agentspecs';",
145
+ "",
146
+ ]
147
+ for spec in specs:
148
+ const = _const_name(spec["id"]) + version_suffix(spec["version"])
149
+ lines.extend(
150
+ [
151
+ f"export const {const}: UIPluginSpec = {{",
152
+ f" id: {_ts(spec['id'])},",
153
+ f" version: {_ts(str(spec['version']))},",
154
+ f" name: {_ts(spec['name'])},",
155
+ f" description: {_ts(str(spec.get('description', '')).strip())},",
156
+ f" docsUrl: {_ts(str(spec.get('docs_url', '')))},",
157
+ f" enabled: {'true' if spec.get('enabled', True) else 'false'},",
158
+ "};",
159
+ "",
160
+ ]
161
+ )
162
+ lines.append("export const UI_PLUGIN_CATALOGUE: Record<string, UIPluginSpec> = {")
163
+ for spec in specs:
164
+ lines.append(
165
+ f" {_ts(spec['id'])}: {_const_name(spec['id']) + version_suffix(spec['version'])},"
166
+ )
167
+ lines.extend(
168
+ [
169
+ "};",
170
+ "",
171
+ "/** The plugin an agent spec's `uiPlugin` names, or undefined. */",
172
+ "export function getUIPlugin(pluginId: string): UIPluginSpec | undefined {",
173
+ " return UI_PLUGIN_CATALOGUE[pluginId];",
174
+ "}",
175
+ "",
176
+ "export function listUIPlugins(): UIPluginSpec[] {",
177
+ " return Object.values(UI_PLUGIN_CATALOGUE);",
178
+ "}",
179
+ "",
180
+ ]
181
+ )
182
+ return "\n".join(lines)
183
+
184
+
185
+ def main():
186
+ parser = argparse.ArgumentParser(
187
+ description="Generate Python and TypeScript code from YAML UI-plugin specifications"
188
+ )
189
+ parser.add_argument(
190
+ "--specs-dir",
191
+ type=Path,
192
+ required=True,
193
+ help="Directory containing UI-plugin YAML files",
194
+ )
195
+ parser.add_argument(
196
+ "--python-output", type=Path, required=True, help="Output path for Python file"
197
+ )
198
+ parser.add_argument(
199
+ "--typescript-output",
200
+ type=Path,
201
+ required=True,
202
+ help="Output path for TypeScript file",
203
+ )
204
+ args = parser.parse_args()
205
+
206
+ if not args.specs_dir.exists():
207
+ print(f"Error: Specs directory not found: {args.specs_dir}", file=sys.stderr)
208
+ sys.exit(1)
209
+ specs = load_plugin_specs(args.specs_dir)
210
+ if not specs:
211
+ print(
212
+ f"Warning: No UI-plugin specifications found in {args.specs_dir}",
213
+ file=sys.stderr,
214
+ )
215
+ return
216
+ args.python_output.parent.mkdir(parents=True, exist_ok=True)
217
+ args.python_output.write_text(generate_python_code(specs))
218
+ print(f"Generated Python code: {args.python_output}")
219
+ args.typescript_output.parent.mkdir(parents=True, exist_ok=True)
220
+ args.typescript_output.write_text(generate_typescript_code(specs))
221
+ print(f"Generated TypeScript code: {args.typescript_output}")
222
+ print(f"\n✓ Successfully generated code from {len(specs)} UI-plugin specs")
223
+
224
+
225
+ if __name__ == "__main__":
226
+ main()
@@ -441,9 +441,9 @@ color: "{color}"
441
441
  goal: >-
442
442
  {goal}
443
443
 
444
- # Protocols & UI Extensions
444
+ # Protocols & UI Plugins
445
445
  protocol: vercel-ai
446
- ui_extension: a2ui
446
+ ui_plugin: a2ui
447
447
 
448
448
  # Model configuration
449
449
  model_config: