@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.
- package/README.md +1 -1
- package/lib/chat/base/ChatBase.js +3 -2
- package/lib/config/AgentConfiguration.d.ts +1 -1
- package/lib/loop/apps/AppRenderer.d.ts +24 -0
- package/lib/loop/apps/AppRenderer.js +101 -0
- package/lib/loop/apps/appspec.d.ts +69 -0
- package/lib/loop/apps/appspec.js +566 -0
- package/lib/loop/apps/checks.d.ts +21 -0
- package/lib/loop/apps/checks.js +556 -0
- package/lib/loop/apps/index.d.ts +10 -0
- package/lib/loop/apps/index.js +14 -0
- package/lib/loop/apps/rules.d.ts +64 -0
- package/lib/loop/apps/rules.js +289 -0
- package/lib/loop/apps/yaml.d.ts +17 -0
- package/lib/loop/apps/yaml.js +185 -0
- package/lib/loop/embed/LoopEmbed.d.ts +6 -1
- package/lib/loop/embed/LoopEmbed.js +3 -2
- package/lib/loop/index.d.ts +1 -0
- package/lib/loop/index.js +3 -0
- package/lib/loop/plugins/chat/ChatView.js +9 -4
- package/lib/loop/plugins/chat/index.d.ts +10 -0
- package/lib/loop/plugins/chat/index.js +1 -0
- package/lib/loop/presets.d.ts +6 -0
- package/lib/loop/presets.js +2 -1
- package/lib/loop/shell/LoopWorkspace.d.ts +14 -0
- package/lib/loop/shell/LoopWorkspace.js +20 -6
- package/lib/loop/shell/index.d.ts +1 -1
- package/lib/loop/shell/index.js +1 -1
- package/lib/loop/shell/useWorkspaceFullScreen.d.ts +17 -4
- package/lib/loop/shell/useWorkspaceFullScreen.js +23 -6
- package/lib/specs/actions.d.ts +30 -0
- package/lib/specs/actions.js +616 -0
- package/lib/specs/agents/agents.d.ts +1 -0
- package/lib/specs/agents/agents.js +338 -284
- package/lib/specs/apps.d.ts +25 -0
- package/lib/specs/apps.js +1064 -0
- package/lib/specs/cogs.d.ts +19 -0
- package/lib/specs/cogs.js +736 -0
- package/lib/specs/events.d.ts +6 -1
- package/lib/specs/events.js +144 -1
- package/lib/specs/frames.d.ts +19 -0
- package/lib/specs/frames.js +444 -0
- package/lib/specs/gates.d.ts +22 -0
- package/lib/specs/gates.js +177 -0
- package/lib/specs/guards.d.ts +26 -0
- package/lib/specs/guards.js +930 -0
- package/lib/specs/index.d.ts +10 -0
- package/lib/specs/index.js +10 -0
- package/lib/specs/mcpServers.js +2 -2
- package/lib/specs/memory.js +4 -0
- package/lib/specs/modelProviders.d.ts +22 -0
- package/lib/specs/modelProviders.js +102 -0
- package/lib/specs/models.d.ts +31 -0
- package/lib/specs/models.js +215 -0
- package/lib/specs/ops.d.ts +15 -0
- package/lib/specs/ops.js +1272 -0
- package/lib/specs/outputs.js +2 -2
- package/lib/specs/skills.js +1 -1
- package/lib/specs/teams/teams.js +18 -18
- package/lib/specs/tracks.d.ts +15 -0
- package/lib/specs/tracks.js +87 -0
- package/lib/specs/uiPlugins.d.ts +16 -0
- package/lib/specs/uiPlugins.js +40 -0
- package/lib/types/agentspecs.d.ts +453 -2
- package/lib/types/memory.d.ts +2 -0
- package/lib/types/models.d.ts +52 -0
- package/package.json +2 -1
- package/scripts/check-cloudflare-models.py +122 -0
- package/scripts/codegen/agentspecs_clone.py +53 -0
- package/scripts/codegen/compose.py +65 -0
- package/scripts/codegen/generate_agents.py +14 -4
- package/scripts/codegen/generate_apps.py +454 -0
- package/scripts/codegen/generate_cogs.py +308 -0
- package/scripts/codegen/generate_frames.py +278 -0
- package/scripts/codegen/generate_memory.py +2 -0
- package/scripts/codegen/generate_model_providers.py +246 -0
- package/scripts/codegen/generate_models.py +135 -4
- package/scripts/codegen/generate_ops.py +388 -0
- package/scripts/codegen/generate_sandbox_agents.py +1 -1
- package/scripts/codegen/generate_ui_plugins.py +226 -0
- 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
|
-
|
|
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
|
-
|
|
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()
|