@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,53 @@
1
+ # Copyright (c) 2025-2026 Datalayer, Inc.
2
+ # Distributed under the terms of the Modified BSD License.
3
+
4
+ """Importing the `agentspecs` package from the clone the specs are read from.
5
+
6
+ Frames and Cogs are resolved by `agentspecs` itself — inheritance, composition
7
+ and rendering are its rules (agentspecs >= 0.0.12) — so the generators import
8
+ it rather than restate them. From the clone `make specs` checked out, not from
9
+ whatever `agentspecs` happens to be installed: the YAML and the code that
10
+ resolves it are then the same version by construction.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import importlib
16
+ import sys
17
+ from pathlib import Path
18
+ from types import ModuleType
19
+
20
+
21
+ def package_root(specs_dir: Path) -> Path:
22
+ """The directory holding the `agentspecs` package a specs directory is in.
23
+
24
+ `agentspecs/agentspecs/frames` → `agentspecs`.
25
+ """
26
+ return specs_dir.resolve().parent.parent
27
+
28
+
29
+ def import_from_clone(specs_dir: Path, module: str) -> ModuleType:
30
+ """Import `agentspecs.<module>` from the clone `specs_dir` belongs to."""
31
+ root = package_root(specs_dir)
32
+ if not (root / "agentspecs" / module).exists():
33
+ raise SystemExit(
34
+ f"Error: {root / 'agentspecs' / module} not found — Frames and Cogs "
35
+ "need agentspecs >= 0.0.12"
36
+ )
37
+ for name in [
38
+ name
39
+ for name in sys.modules
40
+ if name == "agentspecs" or name.startswith("agentspecs.")
41
+ ]:
42
+ del sys.modules[name]
43
+ sys.path.insert(0, str(root))
44
+ try:
45
+ loaded = importlib.import_module(f"agentspecs.{module}")
46
+ finally:
47
+ sys.path.remove(str(root))
48
+ origin = Path(loaded.__file__ or "").resolve()
49
+ if root not in origin.parents:
50
+ raise SystemExit(
51
+ f"Error: agentspecs.{module} was imported from {origin}, not from {root}"
52
+ )
53
+ return loaded
@@ -187,11 +187,76 @@ def resolve_spec(
187
187
  parent_resolved = resolve_spec(
188
188
  parent, specs, fragments, _seen=(*seen, identity)
189
189
  )
190
+ # The parent is applied over this spec's fragments, so what the parent
191
+ # chain says with `!replace` and `!remove` reaches them too. Resolving
192
+ # the parent consumed those markers; they are read again from the
193
+ # chain as it is written.
194
+ replaced, removed = _list_directives(parent, specs, fragments)
195
+ for field in LIST_FIELDS:
196
+ if field not in resolved:
197
+ continue
198
+ if field in replaced:
199
+ del resolved[field]
200
+ elif removed.get(field):
201
+ resolved[field] = [
202
+ entry
203
+ for entry in resolved[field]
204
+ if _key_of(entry) not in removed[field]
205
+ ]
190
206
  resolved = merge_spec(resolved, parent_resolved)
191
207
 
192
208
  return merge_spec(resolved, spec)
193
209
 
194
210
 
211
+ def _list_directives(
212
+ spec: dict[str, Any],
213
+ specs: dict[str, dict[str, Any]],
214
+ fragments: dict[str, dict[str, Any]],
215
+ ) -> tuple[set[str], dict[str, set[str]]]:
216
+ """The `!replace` and `!remove` markers of a spec and of what it extends.
217
+
218
+ Read from every spec of the chain as it is written, and from the
219
+ fragments each of them includes: a marker is the chain's whichever of its
220
+ parts says it. Returns the list fields the chain replaces, and for each
221
+ list field the keys it removes — what a spec lower in the order has to
222
+ give up.
223
+ """
224
+ replaced: set[str] = set()
225
+ removed: dict[str, set[str]] = {}
226
+
227
+ def read(source: dict[str, Any]) -> None:
228
+ for field in LIST_FIELDS:
229
+ entries = source.get(field)
230
+ if not isinstance(entries, list):
231
+ continue
232
+ if REPLACE_MARKER in entries:
233
+ replaced.add(field)
234
+ for entry in entries:
235
+ if isinstance(entry, str) and entry.startswith(REMOVE_PREFIX):
236
+ removed.setdefault(field, set()).add(
237
+ _key_of(entry[len(REMOVE_PREFIX) :])
238
+ )
239
+
240
+ current: Optional[dict[str, Any]] = spec
241
+ for _ in range(MAX_EXTENDS_DEPTH + 1):
242
+ if current is None:
243
+ break
244
+ read(current)
245
+ for include in current.get("includes") or []:
246
+ fragment = fragments.get(_key_of(str(include))) or fragments.get(
247
+ str(include)
248
+ )
249
+ if fragment is not None:
250
+ read(fragment)
251
+ parent_ref = current.get("extends")
252
+ current = (
253
+ specs.get(_key_of(str(parent_ref))) or specs.get(str(parent_ref))
254
+ if parent_ref
255
+ else None
256
+ )
257
+ return replaced, removed
258
+
259
+
195
260
  def resolve_all(
196
261
  specs: Iterable[dict[str, Any]],
197
262
  fragments: Iterable[dict[str, Any]] = (),
@@ -455,7 +455,7 @@ from agent_runtimes.types import (
455
455
  protocol_val = spec.get("protocol")
456
456
  protocol_str = f'"{protocol_val}"' if protocol_val else "None"
457
457
  delegable_str = _fmt_py_delegable(spec.get("delegable", []))
458
- ui_ext = spec.get("ui_extension")
458
+ ui_ext = spec.get("ui_plugin") or spec.get("ui_extension")
459
459
  ui_ext_str = f'"{ui_ext}"' if ui_ext else "None"
460
460
  trigger_val = spec.get("trigger")
461
461
  model_cfg = spec.get("model_config")
@@ -567,7 +567,7 @@ from agent_runtimes.types import (
567
567
  goal={goal_str},
568
568
  delegable={delegable_str},
569
569
  protocol={protocol_str},
570
- ui_extension={ui_ext_str},
570
+ ui_plugin={ui_ext_str},
571
571
  trigger={_fmt_py_literal(trigger_val)},
572
572
  model_configuration={_fmt_py_literal(model_cfg)},
573
573
  mcp_server_tools={_fmt_py_literal(mcp_srv_tools)},
@@ -1075,7 +1075,7 @@ const FRONTEND_TOOL_MAP: Record<string, any> = {
1075
1075
  protocol_val = spec.get("protocol")
1076
1076
  protocol_ts = f"'{protocol_val}'" if protocol_val else "undefined"
1077
1077
  delegable_ts = _fmt_ts_delegable(spec.get("delegable", []))
1078
- ui_ext = spec.get("ui_extension")
1078
+ ui_ext = spec.get("ui_plugin") or spec.get("ui_extension")
1079
1079
  ui_ext_ts = f"'{ui_ext}'" if ui_ext else "undefined"
1080
1080
  trigger_val = spec.get("trigger")
1081
1081
  model_cfg = spec.get("model_config")
@@ -1135,7 +1135,7 @@ const FRONTEND_TOOL_MAP: Record<string, any> = {
1135
1135
  goal: {goal_ts},
1136
1136
  delegable: {delegable_ts},
1137
1137
  protocol: {protocol_ts},
1138
- uiExtension: {ui_ext_ts},
1138
+ uiPlugin: {ui_ext_ts},
1139
1139
  trigger: {_fmt_ts_literal(trigger_val)},
1140
1140
  modelConfig: {_fmt_ts_literal(model_cfg)},
1141
1141
  mcpServerTools: {_fmt_ts_literal(mcp_srv_tools)},
@@ -1573,11 +1573,21 @@ export * from './loops';
1573
1573
  export * from './mcpServers';
1574
1574
  export * from './memory';
1575
1575
  export * from './models';
1576
+ export * from './modelProviders';
1576
1577
  export * from './notifications';
1577
1578
  export * from './outputs';
1578
1579
  export * from './skills';
1579
1580
  export * from './tools';
1580
1581
  export * from './triggers';
1582
+ export * from './uiPlugins';
1583
+ export * from './frames';
1584
+ export * from './cogs';
1585
+ export * from './guards';
1586
+ export * from './gates';
1587
+ export * from './tracks';
1588
+ export * from './ops';
1589
+ export * from './apps';
1590
+ export * from './actions';
1581
1591
  """
1582
1592
  )
1583
1593
 
@@ -0,0 +1,454 @@
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 application specifications.
7
+
8
+ Applications (agentspecs >= 0.0.15): an agent with an interface, rules, tests
9
+ and a place to run — a chat, a widget, a decision or a worker, in one spec,
10
+ the Appspec. One YAML per application under ``agentspecs/apps``.
11
+
12
+ A rule is written in a person's words and enforced on what a tool does, so
13
+ the classes of action travel with the applications: this generator also
14
+ writes what every tool of the catalogue and every MCP server's tools do —
15
+ read, write, send, buy, delete, publish — read from ``agentspecs.actions``.
16
+
17
+ Each application is validated here, by ``agentspecs`` itself, and arrives
18
+ with its layout said and with what it names that is not enabled (`setup`).
19
+
20
+ Usage:
21
+ python generate_apps.py \\
22
+ --specs-dir agentspecs/agentspecs/apps \\
23
+ --python-output agent_runtimes/specs/apps.py \\
24
+ --typescript-output src/specs/apps.ts \\
25
+ --actions-python-output agent_runtimes/specs/actions.py \\
26
+ --actions-typescript-output src/specs/actions.ts
27
+ """
28
+
29
+ import argparse
30
+ import json
31
+ import sys
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from agentspecs_clone import import_from_clone
36
+ from versioning import ensure_spec_version, version_suffix
37
+
38
+ #: The keys of an Appspec, as TypeScript spells them. Keys inside a surface's
39
+ #: components are the protocol's own and are left alone.
40
+ CAMEL = {
41
+ "applies_to": "appliesTo",
42
+ "ready_at": "readyAt",
43
+ "keep_for": "keepFor",
44
+ "retention_days": "retentionDays",
45
+ "composed_by": "composedBy",
46
+ "composed_at": "composedAt",
47
+ "min_confidence": "minConfidence",
48
+ "judgment_model": "judgmentModel",
49
+ }
50
+
51
+ #: The keys whose value is carried as it is written: a component tree, weights by name.
52
+ VERBATIM = ("components", "weights")
53
+
54
+
55
+ def _flat(text: Any) -> str:
56
+ """A text on one line."""
57
+ return " ".join(str(text or "").split())
58
+
59
+
60
+ def load_specs(specs_dir: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
61
+ """Every application, validated, as plain data — and the action classes."""
62
+ apps_module = import_from_clone(specs_dir, "apps")
63
+ actions_module = sys.modules["agentspecs.actions"]
64
+ apps = apps_module.load_apps(specs_dir)
65
+ specs: list[dict[str, Any]] = []
66
+ behaviours: dict[str, dict[str, str]] = {}
67
+ escalations: dict[str, dict[str, Any]] = {}
68
+ sources: dict[str, Any] = {}
69
+ for identity in sorted(apps):
70
+ app = apps[identity]
71
+ spec = app.model_dump(mode="json", by_alias=True)
72
+ spec["interface"]["layout"] = app.layout.value
73
+ spec["record"]["retention_days"] = app.record.retention_days
74
+ spec["setup"] = apps_module.app_setup(app)
75
+ behaviours[identity] = {
76
+ tool: behaviour.value
77
+ for tool, behaviour in sorted(apps_module.tool_behaviours(app).items())
78
+ }
79
+ escalations[identity] = dict(sorted(apps_module.tool_escalations(app).items()))
80
+ sources[identity] = apps_module.dump_app(app)
81
+ for rule in spec["rules"]:
82
+ rule["applies_to"] = (
83
+ [rule["applies_to"]]
84
+ if isinstance(rule["applies_to"], str)
85
+ else rule["applies_to"]
86
+ )
87
+ for key in ("description", "instructions", "goal"):
88
+ spec[key] = _flat(spec.get(key))
89
+ spec["interface"]["welcome"] = _flat(spec["interface"].get("welcome"))
90
+ ensure_spec_version(spec)
91
+ specs.append(spec)
92
+
93
+ tools = {
94
+ identity: [item.value for item in actions_module.tool_classes(tool)]
95
+ for identity, tool in sorted(actions_module.tool_specs().items())
96
+ }
97
+ servers: dict[str, Any] = {}
98
+ for identity, server in sorted(actions_module.server_specs().items()):
99
+ declared = server.get("actions") or {}
100
+ names = list(declared.get("tools") or {})
101
+ conditions = {
102
+ name: [
103
+ condition.as_data()
104
+ for condition in actions_module.server_tool_conditions(server, name)
105
+ ]
106
+ for name in names
107
+ }
108
+ servers[identity] = {
109
+ "checked": declared.get("checked") or None,
110
+ "default": [
111
+ item.value
112
+ for item in actions_module.classes_from(declared.get("default"))
113
+ ],
114
+ # What each tool does of its own, whatever it is asked.
115
+ "tools": {
116
+ name: [
117
+ item.value
118
+ for item in actions_module.server_tool_classes(server, name, {})
119
+ ]
120
+ for name in names
121
+ },
122
+ # What an argument makes it do besides.
123
+ "conditions": {name: found for name, found in conditions.items() if found},
124
+ }
125
+ actions = {
126
+ "classes": [item.value for item in actions_module.ActionClass],
127
+ "tools": tools,
128
+ "servers": servers,
129
+ "behaviours": behaviours,
130
+ "escalations": escalations,
131
+ "sources": sources,
132
+ }
133
+ return specs, actions
134
+
135
+
136
+ def _const_name(spec: dict[str, Any]) -> str:
137
+ """`web-research` 0.0.1 → `WEB_RESEARCH_APP_0_0_1`."""
138
+ base = spec["id"].upper().replace("-", "_").replace(".", "_")
139
+ return f"{base}_APP" + version_suffix(spec["version"])
140
+
141
+
142
+ def _typescript_value(value: Any, verbatim: bool = False) -> Any:
143
+ """An application as the TypeScript interfaces read it; nulls left out."""
144
+ if isinstance(value, dict):
145
+ return {
146
+ (key if verbatim else CAMEL.get(key, key)): _typescript_value(
147
+ item, verbatim or key in VERBATIM
148
+ )
149
+ for key, item in value.items()
150
+ if item is not None
151
+ }
152
+ if isinstance(value, list):
153
+ return [_typescript_value(item, verbatim) for item in value]
154
+ return value
155
+
156
+
157
+ HEADER = [
158
+ "This file is AUTO-GENERATED from YAML specifications.",
159
+ "DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
160
+ ]
161
+
162
+ ABOUT_APPS = [
163
+ "Application Catalog.",
164
+ "",
165
+ "What a person uses and relies on: an agent with an interface, rules, tests",
166
+ "and a place to run. A chat, a widget, a decision or a worker, in one spec.",
167
+ ]
168
+
169
+ ABOUT_ACTIONS = [
170
+ "Action Classes.",
171
+ "",
172
+ "What every tool does to the world: read, write, send, buy, delete, publish.",
173
+ "A tool with no class is unknown, and unknown is the most restricted.",
174
+ ]
175
+
176
+
177
+ def generate_python_code(specs: list[dict[str, Any]]) -> str:
178
+ """Generate the Python application catalogue."""
179
+ lines = [
180
+ "# Copyright (c) 2025-2026 Datalayer, Inc.",
181
+ "# Distributed under the terms of the Modified BSD License.",
182
+ '"""',
183
+ *ABOUT_APPS,
184
+ "",
185
+ *HEADER,
186
+ '"""',
187
+ "",
188
+ "from typing import Dict",
189
+ "",
190
+ "from agent_runtimes.types import AppSpec",
191
+ "",
192
+ "",
193
+ "# " + "=" * 76,
194
+ "# Application Definitions",
195
+ "# " + "=" * 76,
196
+ "",
197
+ ]
198
+ for spec in specs:
199
+ lines.extend(
200
+ [f"{_const_name(spec)} = AppSpec.model_validate(", f" {spec!r}", ")", ""]
201
+ )
202
+ lines.extend(
203
+ [
204
+ "",
205
+ "# " + "=" * 76,
206
+ "# Application Catalog",
207
+ "# " + "=" * 76,
208
+ "",
209
+ "APP_CATALOGUE: Dict[str, AppSpec] = {",
210
+ ]
211
+ )
212
+ for spec in specs:
213
+ lines.append(f" {spec['id']!r}: {_const_name(spec)},")
214
+ lines.extend(
215
+ [
216
+ "}",
217
+ "",
218
+ "",
219
+ "def get_app(app_id: str) -> AppSpec | None:",
220
+ ' """An application, by `id` or `id:version`, or None."""',
221
+ " found = APP_CATALOGUE.get(app_id)",
222
+ " if found is not None:",
223
+ " return found",
224
+ ' base, _, version = app_id.rpartition(":")',
225
+ ' return APP_CATALOGUE.get(base) if base and "." in version else None',
226
+ "",
227
+ "",
228
+ "def list_apps(kind: str | None = None) -> list[AppSpec]:",
229
+ ' """Every application of the catalogue, or those of a kind."""',
230
+ " return [app for app in APP_CATALOGUE.values() if kind is None or app.kind == kind]",
231
+ "",
232
+ ]
233
+ )
234
+ return "\n".join(lines)
235
+
236
+
237
+ def generate_typescript_code(
238
+ specs: list[dict[str, Any]], sources: dict[str, Any]
239
+ ) -> str:
240
+ """Generate the TypeScript application catalogue."""
241
+ lines = [
242
+ "/*",
243
+ " * Copyright (c) 2025-2026 Datalayer, Inc.",
244
+ " * Distributed under the terms of the Modified BSD License.",
245
+ " */",
246
+ "",
247
+ "/**",
248
+ *[f" * {line}".rstrip() for line in ABOUT_APPS],
249
+ " *",
250
+ *[f" * {line}" for line in HEADER],
251
+ " */",
252
+ "",
253
+ "import type { AppKind, AppSpec } from '../types/agentspecs';",
254
+ "",
255
+ ]
256
+ for spec in specs:
257
+ body = json.dumps(_typescript_value(spec), indent=2, ensure_ascii=False)
258
+ lines.extend([f"export const {_const_name(spec)}: AppSpec = {body};", ""])
259
+ lines.append("export const APP_CATALOGUE: Record<string, AppSpec> = {")
260
+ for spec in specs:
261
+ lines.append(f" {json.dumps(spec['id'])}: {_const_name(spec)},")
262
+ lines.extend(
263
+ [
264
+ "};",
265
+ "",
266
+ "/** An application, by `id` or `id:version`, or undefined. */",
267
+ "export function getApp(ref: string): AppSpec | undefined {",
268
+ " // Own entries only: `constructor` and `toString` are not applications.",
269
+ " const own = (id: string): AppSpec | undefined =>",
270
+ " Object.prototype.hasOwnProperty.call(APP_CATALOGUE, id)",
271
+ " ? APP_CATALOGUE[id]",
272
+ " : undefined;",
273
+ " const at = ref.lastIndexOf(':');",
274
+ " return (",
275
+ " own(ref) ??",
276
+ " (at > 0 && ref.slice(at + 1).includes('.')",
277
+ " ? own(ref.slice(0, at))",
278
+ " : undefined)",
279
+ " );",
280
+ "}",
281
+ "",
282
+ "/**",
283
+ " * Each application as the document its file holds: the spec's own words,",
284
+ " * nothing written that is at its default. What reading and writing an",
285
+ " * Appspec have to give back.",
286
+ " */",
287
+ "export const APP_SOURCES: Record<string, Record<string, unknown>> = "
288
+ + json.dumps(sources, indent=2, ensure_ascii=False)
289
+ + ";",
290
+ "",
291
+ "/** Every application of the catalogue, or those of a kind. */",
292
+ "export function listApps(kind?: AppKind): AppSpec[] {",
293
+ " return Object.values(APP_CATALOGUE).filter(",
294
+ " app => kind === undefined || app.kind === kind,",
295
+ " );",
296
+ "}",
297
+ "",
298
+ ]
299
+ )
300
+ return "\n".join(lines)
301
+
302
+
303
+ def generate_actions_python_code(actions: dict[str, Any]) -> str:
304
+ """Generate the Python action classes."""
305
+ lines = [
306
+ "# Copyright (c) 2025-2026 Datalayer, Inc.",
307
+ "# Distributed under the terms of the Modified BSD License.",
308
+ '"""',
309
+ *ABOUT_ACTIONS,
310
+ "",
311
+ *HEADER,
312
+ '"""',
313
+ "",
314
+ "from typing import Any, Dict, List",
315
+ "",
316
+ "from agent_runtimes.types import ServerActionsSpec",
317
+ "",
318
+ "#: The classes of action, from the one that changes nothing.",
319
+ f"ACTION_CLASSES: List[str] = {actions['classes']!r}",
320
+ "",
321
+ "#: What each tool of the tools catalogue does, by id.",
322
+ f"TOOL_ACTIONS: Dict[str, List[str]] = {actions['tools']!r}",
323
+ "",
324
+ "#: What each MCP server's tools do, by server id. `checked` is the day the",
325
+ "#: names were read off the running server, or None when nobody looked.",
326
+ "SERVER_ACTIONS: Dict[str, ServerActionsSpec] = {",
327
+ ]
328
+ for identity, server in actions["servers"].items():
329
+ lines.append(f" {identity!r}: ServerActionsSpec.model_validate({server!r}),")
330
+ lines.extend(
331
+ [
332
+ "}",
333
+ "",
334
+ "#: What each application of the catalogue does about each classed tool of the",
335
+ "#: servers it connects to, as agentspecs decides it: `do_it`, `if_asked`,",
336
+ "#: `ask_first` or `leave_to_me`. What the rules engine has to reproduce.",
337
+ f"APP_BEHAVIOURS: Dict[str, Dict[str, str]] = {actions['behaviours']!r}",
338
+ "",
339
+ "#: Where what a tool is asked changes what an application does about it: by",
340
+ "#: application and tool, each condition and the behaviour when it holds.",
341
+ f"APP_ESCALATIONS: Dict[str, Dict[str, List[Dict[str, Any]]]] = {actions['escalations']!r}",
342
+ "",
343
+ ]
344
+ )
345
+ return "\n".join(lines)
346
+
347
+
348
+ def generate_actions_typescript_code(actions: dict[str, Any]) -> str:
349
+ """Generate the TypeScript action classes."""
350
+ lines = [
351
+ "/*",
352
+ " * Copyright (c) 2025-2026 Datalayer, Inc.",
353
+ " * Distributed under the terms of the Modified BSD License.",
354
+ " */",
355
+ "",
356
+ "/**",
357
+ *[f" * {line}".rstrip() for line in ABOUT_ACTIONS],
358
+ " *",
359
+ *[f" * {line}" for line in HEADER],
360
+ " */",
361
+ "",
362
+ "import type {",
363
+ " ActionClass,",
364
+ " AppBehaviour,",
365
+ " AppEscalation,",
366
+ " ServerActionsSpec,",
367
+ "} from '../types/agentspecs';",
368
+ "",
369
+ "/** The classes of action, from the one that changes nothing. */",
370
+ f"export const ACTION_CLASSES: ActionClass[] = {json.dumps(actions['classes'])};",
371
+ "",
372
+ "/** What each tool of the tools catalogue does, by id. */",
373
+ "export const TOOL_ACTIONS: Record<string, ActionClass[]> = "
374
+ + json.dumps(actions["tools"], indent=2)
375
+ + ";",
376
+ "",
377
+ "/**",
378
+ " * What each MCP server's tools do, by server id. `checked` is the day the",
379
+ " * names were read off the running server; absent when nobody looked.",
380
+ " */",
381
+ "export const SERVER_ACTIONS: Record<string, ServerActionsSpec> = "
382
+ + json.dumps(_drop_nulls(actions["servers"]), indent=2)
383
+ + ";",
384
+ "",
385
+ "/**",
386
+ " * What each application of the catalogue does about each classed tool of the",
387
+ " * servers it connects to, as agentspecs decides it. What the rules engine has",
388
+ " * to reproduce.",
389
+ " */",
390
+ "export const APP_BEHAVIOURS: Record<string, Record<string, AppBehaviour>> = "
391
+ + json.dumps(actions["behaviours"], indent=2)
392
+ + ";",
393
+ "",
394
+ "/**",
395
+ " * Where what a tool is asked changes what an application does about it: by",
396
+ " * application and tool, each condition and the behaviour when it holds.",
397
+ " */",
398
+ "export const APP_ESCALATIONS: Record<",
399
+ " string,",
400
+ " Record<string, AppEscalation[]>",
401
+ "> = " + json.dumps(actions["escalations"], indent=2) + ";",
402
+ "",
403
+ ]
404
+ return "\n".join(lines)
405
+
406
+
407
+ def _drop_nulls(value: Any) -> Any:
408
+ """A value without the keys that hold nothing."""
409
+ if isinstance(value, dict):
410
+ return {
411
+ key: _drop_nulls(item) for key, item in value.items() if item is not None
412
+ }
413
+ if isinstance(value, list):
414
+ return [_drop_nulls(item) for item in value]
415
+ return value
416
+
417
+
418
+ def main() -> None:
419
+ """Generate the application catalogue and the action classes."""
420
+ parser = argparse.ArgumentParser(
421
+ description="Generate Python and TypeScript code from YAML application specifications"
422
+ )
423
+ parser.add_argument("--specs-dir", type=Path, required=True)
424
+ parser.add_argument("--python-output", type=Path, required=True)
425
+ parser.add_argument("--typescript-output", type=Path, required=True)
426
+ parser.add_argument("--actions-python-output", type=Path, required=True)
427
+ parser.add_argument("--actions-typescript-output", type=Path, required=True)
428
+ args = parser.parse_args()
429
+
430
+ if not args.specs_dir.exists():
431
+ print(f"Error: Specs directory not found: {args.specs_dir}", file=sys.stderr)
432
+ sys.exit(1)
433
+ specs, actions = load_specs(args.specs_dir)
434
+ outputs = [
435
+ (args.python_output, generate_python_code(specs)),
436
+ (
437
+ args.typescript_output,
438
+ generate_typescript_code(specs, actions["sources"]),
439
+ ),
440
+ (args.actions_python_output, generate_actions_python_code(actions)),
441
+ (args.actions_typescript_output, generate_actions_typescript_code(actions)),
442
+ ]
443
+ for path, text in outputs:
444
+ path.parent.mkdir(parents=True, exist_ok=True)
445
+ path.write_text(text)
446
+ print(f"Generated: {path}")
447
+ print(
448
+ f"\n✓ Successfully generated code from {len(specs)} application specs, "
449
+ f"{len(actions['tools'])} tools and {len(actions['servers'])} MCP servers"
450
+ )
451
+
452
+
453
+ if __name__ == "__main__":
454
+ main()