@datalayer/agent-runtimes 1.5.16 → 1.5.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/dist-embed/chunks/ChatView-Dr-1g_cg.js +32 -0
- package/dist-embed/datalayer-app-main.js +59 -59
- package/lib/apps/apps/appspec.d.ts +5 -3
- package/lib/apps/apps/appspec.js +52 -10
- package/lib/apps/apps/threads.d.ts +20 -0
- package/lib/apps/apps/threads.js +31 -0
- package/lib/apps/plugins/canvas-blocks/index.d.ts +7 -9
- package/lib/apps/plugins/canvas-blocks/index.js +8 -10
- package/lib/apps/plugins/canvas-parts/index.d.ts +4 -1
- package/lib/apps/plugins/canvas-parts/index.js +7 -3
- package/lib/apps/plugins/chat/ChatView.js +11 -5
- package/lib/config/AgentConfiguration.d.ts +5 -1
- package/lib/specs/agents/agents.js +169 -169
- package/lib/specs/apps.js +4 -4
- package/lib/specs/appspecSchema.js +30 -2
- package/lib/specs/cogs.js +3 -3
- package/lib/specs/index.d.ts +1 -0
- package/lib/specs/index.js +1 -0
- package/lib/specs/sceneSchema.js +2 -0
- package/lib/specs/ui.d.ts +32 -6
- package/lib/specs/ui.js +760 -26
- package/lib/specs/uiRenderers.d.ts +17 -0
- package/lib/specs/uiRenderers.js +40 -0
- package/lib/types/agentspecs.d.ts +58 -12
- package/package.json +2 -2
- package/scripts/codegen/generate_agents.py +26 -9
- package/scripts/codegen/generate_ui.py +68 -22
- package/scripts/codegen/generate_ui_renderers.py +176 -0
- package/dist-embed/chunks/ChatView-nnI1-MOR.js +0 -32
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Renderer Catalog.
|
|
3
|
+
*
|
|
4
|
+
* How a UI plugin's interface reaches a host that is not Datalayer's own
|
|
5
|
+
* page (AGENTIC_UI R8): A2UI, MCP Apps, MCP UI.
|
|
6
|
+
*
|
|
7
|
+
* This file is AUTO-GENERATED from YAML specifications.
|
|
8
|
+
* DO NOT EDIT MANUALLY - run 'make specs' to regenerate.
|
|
9
|
+
*/
|
|
10
|
+
import type { UiRendererSpec } from '../types/agentspecs';
|
|
11
|
+
export declare const A2UI_UI_RENDERER_0_0_1: UiRendererSpec;
|
|
12
|
+
export declare const MCP_APPS_UI_RENDERER_0_0_1: UiRendererSpec;
|
|
13
|
+
export declare const MCP_UI_UI_RENDERER_0_0_1: UiRendererSpec;
|
|
14
|
+
export declare const UI_RENDERER_CATALOGUE: Record<string, UiRendererSpec>;
|
|
15
|
+
/** The UI renderer a plugin's `renderers` or a `ui` choice names, or undefined. */
|
|
16
|
+
export declare function getUiRenderer(rendererId: string): UiRendererSpec | undefined;
|
|
17
|
+
export declare function listUiRenderers(): UiRendererSpec[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
export const A2UI_UI_RENDERER_0_0_1 = {
|
|
6
|
+
id: 'a2ui',
|
|
7
|
+
version: '0.0.1',
|
|
8
|
+
name: 'A2UI',
|
|
9
|
+
description: 'An agent describes an interface — a form, a table, a card — as a tree of components from a catalogue the host allows, and the host renders it; what the user does in it comes back to the agent as an action.',
|
|
10
|
+
docsUrl: 'https://a2ui.org/',
|
|
11
|
+
enabled: true,
|
|
12
|
+
};
|
|
13
|
+
export const MCP_APPS_UI_RENDERER_0_0_1 = {
|
|
14
|
+
id: 'mcp-apps',
|
|
15
|
+
version: '0.0.1',
|
|
16
|
+
name: 'MCP Apps',
|
|
17
|
+
description: "An MCP server ships an interactive app with a tool: the host renders it in a sandboxed frame beside the conversation, and the app calls the server's tools through the host.",
|
|
18
|
+
docsUrl: 'https://modelcontextprotocol.io/docs/extensions/apps',
|
|
19
|
+
enabled: false,
|
|
20
|
+
};
|
|
21
|
+
export const MCP_UI_UI_RENDERER_0_0_1 = {
|
|
22
|
+
id: 'mcp-ui',
|
|
23
|
+
version: '0.0.1',
|
|
24
|
+
name: 'MCP UI',
|
|
25
|
+
description: 'An MCP tool answers with a UI resource — HTML, a remote page or a component — that the host renders in place of plain text.',
|
|
26
|
+
docsUrl: 'https://mcpui.dev/',
|
|
27
|
+
enabled: false,
|
|
28
|
+
};
|
|
29
|
+
export const UI_RENDERER_CATALOGUE = {
|
|
30
|
+
a2ui: A2UI_UI_RENDERER_0_0_1,
|
|
31
|
+
'mcp-apps': MCP_APPS_UI_RENDERER_0_0_1,
|
|
32
|
+
'mcp-ui': MCP_UI_UI_RENDERER_0_0_1,
|
|
33
|
+
};
|
|
34
|
+
/** The UI renderer a plugin's `renderers` or a `ui` choice names, or undefined. */
|
|
35
|
+
export function getUiRenderer(rendererId) {
|
|
36
|
+
return UI_RENDERER_CATALOGUE[rendererId];
|
|
37
|
+
}
|
|
38
|
+
export function listUiRenderers() {
|
|
39
|
+
return Object.values(UI_RENDERER_CATALOGUE);
|
|
40
|
+
}
|
|
@@ -106,25 +106,71 @@ export interface ComponentSpec {
|
|
|
106
106
|
example?: Record<string, unknown>;
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
109
|
+
* A UI plugin (`agentspecs/ui`, AGENTIC_UI R8): one Reactor plugin of this
|
|
110
|
+
* package a Maker composes an interface with, read from its registration, and
|
|
111
|
+
* the UI renderers it may be exposed through. An agent's or an application's
|
|
112
|
+
* `ui` names plugins, each with its renderer.
|
|
111
113
|
*/
|
|
112
114
|
export interface UISpec {
|
|
113
|
-
/** What
|
|
115
|
+
/** What a `ui` choice's `plugin` names (e.g. 'chat'). */
|
|
114
116
|
id: string;
|
|
115
117
|
version: string;
|
|
118
|
+
/** What a Maker reads. */
|
|
116
119
|
name: string;
|
|
117
|
-
/** What
|
|
120
|
+
/** What it is, in a Maker's words. */
|
|
118
121
|
description: string;
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
|
|
122
|
+
/** Its face. */
|
|
123
|
+
emoji: string;
|
|
124
|
+
/** Its Octicon. */
|
|
125
|
+
octicon?: string;
|
|
126
|
+
/** The name the Reactor plugin registers under. */
|
|
127
|
+
plugin: string;
|
|
128
|
+
/** Its folder under `src/apps/plugins`. */
|
|
129
|
+
folder: string;
|
|
130
|
+
/** Registered once per application, source or UI spec, as `<plugin>-<its id>`. */
|
|
131
|
+
per?: 'application' | 'source' | 'ui';
|
|
132
|
+
/** Its Contributions, as the reactor registers them. */
|
|
133
|
+
contributions: {
|
|
134
|
+
/** The Slots it fills. */
|
|
135
|
+
slots: string[];
|
|
136
|
+
/** The Contribution Points it contributes to. */
|
|
137
|
+
points: string[];
|
|
138
|
+
/** The Commands it adds: Reactor Commands by id, the composer's `/` commands by name. */
|
|
139
|
+
commands: string[];
|
|
140
|
+
};
|
|
141
|
+
/** The Contribution Points it offers. */
|
|
142
|
+
contributionPoints: string[];
|
|
143
|
+
/** Its settings, as the JSON Schema of an object; each default the plugin's. */
|
|
144
|
+
settings: Record<string, unknown>;
|
|
145
|
+
/** The UI renderers it may be exposed through, ids of `agentspecs/ui-renderers`. */
|
|
146
|
+
renderers: string[];
|
|
123
147
|
/** The catalog its components are written in. */
|
|
124
148
|
catalog: string;
|
|
125
149
|
/** The visual components it renders (LOOP C-13). */
|
|
126
150
|
components: ComponentSpec[];
|
|
127
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* A UI renderer (`agentspecs/ui-renderers`, AGENTIC_UI R8): how a UI plugin's
|
|
154
|
+
* interface reaches a host that is not Datalayer's own page.
|
|
155
|
+
*/
|
|
156
|
+
export interface UiRendererSpec {
|
|
157
|
+
/** What a plugin's `renderers` lists (e.g. 'a2ui'). */
|
|
158
|
+
id: string;
|
|
159
|
+
version: string;
|
|
160
|
+
name: string;
|
|
161
|
+
description: string;
|
|
162
|
+
/** Its own documentation. */
|
|
163
|
+
docsUrl: string;
|
|
164
|
+
/** Whether an application may be exposed through it today. */
|
|
165
|
+
enabled: boolean;
|
|
166
|
+
}
|
|
167
|
+
/** A UI plugin an agent or an application names, and the renderer it is exposed through, when it is. */
|
|
168
|
+
export interface UiChoice {
|
|
169
|
+
/** A UI plugin of `agentspecs/ui`, by id. */
|
|
170
|
+
plugin: string;
|
|
171
|
+
/** The UI renderer of `agentspecs/ui-renderers` it is exposed through, by id. */
|
|
172
|
+
renderer?: string;
|
|
173
|
+
}
|
|
128
174
|
/** The layer of an agent's visit a protocol answers (AGENT_PROTOCOLS §1). */
|
|
129
175
|
export type ProtocolLayer = 'transport' | 'consent' | 'identity' | 'commerce' | 'ui';
|
|
130
176
|
/**
|
|
@@ -1257,8 +1303,8 @@ export interface AppSpec {
|
|
|
1257
1303
|
* is listed, and a planned one only while it is not offered.
|
|
1258
1304
|
*/
|
|
1259
1305
|
protocols: string[];
|
|
1260
|
-
/**
|
|
1261
|
-
ui:
|
|
1306
|
+
/** Its UI plugins, each with the renderer it is exposed through, when it is (AGENTIC_UI R8). */
|
|
1307
|
+
ui: UiChoice[];
|
|
1262
1308
|
/** Personal Agent Protocol; absent, the application has none. */
|
|
1263
1309
|
pap?: AppPapSpec;
|
|
1264
1310
|
/** PACT: the businesses it asks for each person (AGENT_PROTOCOLS PI-01). */
|
|
@@ -1396,8 +1442,8 @@ export interface Agentspec {
|
|
|
1396
1442
|
* PS-02). `protocol` says how a page reaches it; this, what it speaks to the world.
|
|
1397
1443
|
*/
|
|
1398
1444
|
protocols?: string[];
|
|
1399
|
-
/**
|
|
1400
|
-
ui?:
|
|
1445
|
+
/** Its UI plugins, each with the renderer it is exposed through, when it is (AGENTIC_UI R8). */
|
|
1446
|
+
ui?: UiChoice[];
|
|
1401
1447
|
/** Trigger configuration (type, cron, event source, prompt) */
|
|
1402
1448
|
trigger?: AgentTriggerConfig;
|
|
1403
1449
|
/** Model configuration (temperature, max_tokens) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalayer/agent-runtimes",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
".",
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"@datalayer/lexical-loro": "^1.0.6",
|
|
161
161
|
"@datalayer/primer-addons": "^1.0.39",
|
|
162
162
|
"@datalayer/primer-rjsf": "^1.0.3",
|
|
163
|
-
"@datalayer/personal-agent-protocol": "^0.
|
|
163
|
+
"@datalayer/personal-agent-protocol": "^0.23.0",
|
|
164
164
|
"@datalayer/reactor": "^1.3.0",
|
|
165
165
|
"@datalayer/reactor-commands": "^1.0.4",
|
|
166
166
|
"@datalayer/reactor-manager": "^1.0.4",
|
|
@@ -236,9 +236,21 @@ def load_fragments(specs_dir: Path) -> List[Dict[str, Any]]:
|
|
|
236
236
|
|
|
237
237
|
def refuse_tools_field(spec: Dict[str, Any], path: Path) -> None:
|
|
238
238
|
"""Refuse an agent spec that still says `tools` (it is `backend_tools` now) or
|
|
239
|
-
`ui_plugin` (it is `ui` now, AGENT_PROTOCOLS PS-03).
|
|
239
|
+
`ui_plugin` (it is `ui` now, AGENT_PROTOCOLS PS-03).
|
|
240
|
+
"""
|
|
240
241
|
if "ui_plugin" in spec:
|
|
241
|
-
raise SystemExit(
|
|
242
|
+
raise SystemExit(
|
|
243
|
+
f"Error: {path} says `ui_plugin`: the field is `ui`, one of agentspecs/ui"
|
|
244
|
+
)
|
|
245
|
+
ui = spec.get("ui")
|
|
246
|
+
if ui is not None and (
|
|
247
|
+
not isinstance(ui, list)
|
|
248
|
+
or not all(isinstance(choice, dict) and "plugin" in choice for choice in ui)
|
|
249
|
+
):
|
|
250
|
+
raise SystemExit(
|
|
251
|
+
f"Error: {path} says `ui: {ui!r}`: `ui` lists UI plugins, each "
|
|
252
|
+
"`{plugin, renderer}` (AGENTIC_UI R8); a renderer is said as a plugin's `renderer`"
|
|
253
|
+
)
|
|
242
254
|
if "tools" in spec:
|
|
243
255
|
raise SystemExit(
|
|
244
256
|
f"Error: {path} says `tools`: the field is `backend_tools` — the tools "
|
|
@@ -305,7 +317,9 @@ def load_protocol_ids(specs_dir: Path) -> set[str]:
|
|
|
305
317
|
return ids
|
|
306
318
|
|
|
307
319
|
|
|
308
|
-
def check_protocols(
|
|
320
|
+
def check_protocols(
|
|
321
|
+
specs: List[tuple[str, Dict[str, Any]]], protocol_ids: set[str]
|
|
322
|
+
) -> None:
|
|
309
323
|
"""Refuse an agent whose `protocols` is not a list of the catalogue's ids, each once (PS-02)."""
|
|
310
324
|
problems = []
|
|
311
325
|
for _, spec in specs:
|
|
@@ -313,11 +327,15 @@ def check_protocols(specs: List[tuple[str, Dict[str, Any]]], protocol_ids: set[s
|
|
|
313
327
|
if said is None:
|
|
314
328
|
continue
|
|
315
329
|
if not isinstance(said, list) or len(set(map(str, said))) != len(said):
|
|
316
|
-
problems.append(
|
|
330
|
+
problems.append(
|
|
331
|
+
f"{spec.get('id')}: protocols is a list of protocol ids, each once"
|
|
332
|
+
)
|
|
317
333
|
continue
|
|
318
334
|
unknown = [str(p) for p in said if str(p) not in protocol_ids]
|
|
319
335
|
if unknown:
|
|
320
|
-
problems.append(
|
|
336
|
+
problems.append(
|
|
337
|
+
f"{spec.get('id')}: protocols names what the catalogue does not know: {', '.join(unknown)}"
|
|
338
|
+
)
|
|
321
339
|
if problems:
|
|
322
340
|
raise SystemExit("\n".join(problems))
|
|
323
341
|
|
|
@@ -539,8 +557,7 @@ from agent_runtimes.types import (
|
|
|
539
557
|
protocol_val = spec.get("protocol")
|
|
540
558
|
protocol_str = f'"{protocol_val}"' if protocol_val else "None"
|
|
541
559
|
delegable_str = _fmt_py_delegable(spec.get("delegable", []))
|
|
542
|
-
|
|
543
|
-
ui_str = f'"{ui_id}"' if ui_id else "None"
|
|
560
|
+
ui_str = _fmt_py_literal(list(spec.get("ui") or []))
|
|
544
561
|
trigger_val = spec.get("trigger")
|
|
545
562
|
model_cfg = spec.get("model_config")
|
|
546
563
|
mcp_srv_tools = spec.get("mcp_server_tools")
|
|
@@ -1167,8 +1184,7 @@ const FRONTEND_TOOL_MAP: Record<string, any> = {
|
|
|
1167
1184
|
protocol_val = spec.get("protocol")
|
|
1168
1185
|
protocol_ts = f"'{protocol_val}'" if protocol_val else "undefined"
|
|
1169
1186
|
delegable_ts = _fmt_ts_delegable(spec.get("delegable", []))
|
|
1170
|
-
|
|
1171
|
-
ui_ts = f"'{ui_id}'" if ui_id else "undefined"
|
|
1187
|
+
ui_ts = _fmt_ts_literal(list(spec.get("ui") or []))
|
|
1172
1188
|
trigger_val = spec.get("trigger")
|
|
1173
1189
|
model_cfg = spec.get("model_config")
|
|
1174
1190
|
mcp_srv_tools = spec.get("mcp_server_tools")
|
|
@@ -1674,6 +1690,7 @@ export * from './skills';
|
|
|
1674
1690
|
export * from './backendTools';
|
|
1675
1691
|
export * from './triggers';
|
|
1676
1692
|
export * from './ui';
|
|
1693
|
+
export * from './uiRenderers';
|
|
1677
1694
|
export * from './protocols';
|
|
1678
1695
|
export * from './frames';
|
|
1679
1696
|
export * from './cogs';
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
"""
|
|
6
6
|
Generate Python and TypeScript code from YAML UI specifications.
|
|
7
7
|
|
|
8
|
-
A UI spec is
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
A UI spec is one Reactor plugin of this package (``src/apps/plugins/``) a
|
|
9
|
+
Maker composes an interface with (AGENTIC_UI R8): its registered name, its
|
|
10
|
+
Contributions, the Contribution Points it offers, its settings, the visual
|
|
11
|
+
components it renders (LOOP C-13) and the UI renderers it may be exposed
|
|
12
|
+
through (``agentspecs/ui-renderers``, ``generate_ui_renderers.py``). One YAML
|
|
13
|
+
per plugin under ``agentspecs/ui``; an agent's or an application's ``ui``
|
|
14
|
+
names plugins, each with its renderer.
|
|
12
15
|
|
|
13
16
|
Usage:
|
|
14
17
|
python generate_ui.py \\
|
|
@@ -29,7 +32,13 @@ from versioning import ensure_spec_version, version_suffix
|
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
def load_plugin_specs(specs_dir: Path) -> list[dict[str, Any]]:
|
|
32
|
-
"""Load all UI YAML specifications from a directory.
|
|
35
|
+
"""Load all UI YAML specifications from a directory.
|
|
36
|
+
|
|
37
|
+
A spec of the old shape — a renderer listed as a UI, with no `plugin` —
|
|
38
|
+
is refused, and so is a renderer the `ui-renderers` beside it does not have.
|
|
39
|
+
"""
|
|
40
|
+
renderers_dir = specs_dir.parent / "ui-renderers"
|
|
41
|
+
renderers = {path.stem for path in renderers_dir.glob("*.yaml")}
|
|
33
42
|
specs = []
|
|
34
43
|
for yaml_file in sorted(specs_dir.glob("*.yaml")):
|
|
35
44
|
with open(yaml_file) as f:
|
|
@@ -39,6 +48,16 @@ def load_plugin_specs(specs_dir: Path) -> list[dict[str, Any]]:
|
|
|
39
48
|
raise ValueError(
|
|
40
49
|
f"{yaml_file.name}: the file is named for id {yaml_file.stem!r}, the spec says {spec['id']!r}"
|
|
41
50
|
)
|
|
51
|
+
if "plugin" not in spec:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"{yaml_file.name}: a UI spec is a Reactor plugin (`plugin`); "
|
|
54
|
+
"a renderer is a spec of agentspecs/ui-renderers"
|
|
55
|
+
)
|
|
56
|
+
for renderer in spec.get("renderers") or []:
|
|
57
|
+
if renderer not in renderers:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"{yaml_file.name}: there is no UI renderer {renderer!r} in {renderers_dir}"
|
|
60
|
+
)
|
|
42
61
|
specs.append(spec)
|
|
43
62
|
return specs
|
|
44
63
|
|
|
@@ -122,8 +141,6 @@ def _py_calls(specs: list[dict[str, Any]]) -> list[str]:
|
|
|
122
141
|
" self._place = place",
|
|
123
142
|
]
|
|
124
143
|
for spec in specs:
|
|
125
|
-
if not spec.get("enabled", True):
|
|
126
|
-
continue
|
|
127
144
|
for component in spec.get("components") or []:
|
|
128
145
|
schema = component.get("properties") or {}
|
|
129
146
|
required = set(schema.get("required") or [])
|
|
@@ -203,7 +220,7 @@ def generate_python_code(specs: list[dict[str, Any]]) -> str:
|
|
|
203
220
|
'"""',
|
|
204
221
|
"UI Catalog.",
|
|
205
222
|
"",
|
|
206
|
-
"
|
|
223
|
+
"The UI plugins: the Reactor plugins an interface is made of (AGENTIC_UI R8).",
|
|
207
224
|
"",
|
|
208
225
|
"This file is AUTO-GENERATED from YAML specifications.",
|
|
209
226
|
"DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
|
|
@@ -211,7 +228,12 @@ def generate_python_code(specs: list[dict[str, Any]]) -> str:
|
|
|
211
228
|
"",
|
|
212
229
|
"from typing import Any, Callable, Dict, List, Literal, Mapping, Optional, Union",
|
|
213
230
|
"",
|
|
214
|
-
"from agent_runtimes.types import
|
|
231
|
+
"from agent_runtimes.types import (",
|
|
232
|
+
" ComponentBindingsSpec,",
|
|
233
|
+
" ComponentSpec,",
|
|
234
|
+
" UiContributionsSpec,",
|
|
235
|
+
" UISpec,",
|
|
236
|
+
")",
|
|
215
237
|
"",
|
|
216
238
|
"# " + "=" * 76,
|
|
217
239
|
"# UI Definitions",
|
|
@@ -227,9 +249,20 @@ def generate_python_code(specs: list[dict[str, Any]]) -> str:
|
|
|
227
249
|
f" version={_py(str(spec['version']))},",
|
|
228
250
|
f" name={_py(spec['name'])},",
|
|
229
251
|
f" description={_py(str(spec.get('description', '')).strip())},",
|
|
230
|
-
f"
|
|
231
|
-
f"
|
|
232
|
-
f"
|
|
252
|
+
f" emoji={json.dumps(spec['emoji'], ensure_ascii=False)},",
|
|
253
|
+
f" octicon={_py(spec['octicon']) if spec.get('octicon') else 'None'},",
|
|
254
|
+
f" plugin={_py(spec['plugin'])},",
|
|
255
|
+
f" folder={_py(spec['folder'])},",
|
|
256
|
+
f" per={_py(spec['per']) if spec.get('per') else 'None'},",
|
|
257
|
+
" contributions=UiContributionsSpec(",
|
|
258
|
+
f" slots={list((spec.get('contributions') or {}).get('slots') or [])!r},",
|
|
259
|
+
f" points={list((spec.get('contributions') or {}).get('points') or [])!r},",
|
|
260
|
+
f" commands={list((spec.get('contributions') or {}).get('commands') or [])!r},",
|
|
261
|
+
" ),",
|
|
262
|
+
f" contribution_points={list(spec.get('contribution_points') or [])!r},",
|
|
263
|
+
f" settings={spec.get('settings') or {}!r},",
|
|
264
|
+
f" renderers={list(spec.get('renderers') or [])!r},",
|
|
265
|
+
f" catalog={_py(str(spec.get('catalog') or ''))},",
|
|
233
266
|
" components=[",
|
|
234
267
|
*[f" {_py_component(c)}," for c in spec.get("components") or []],
|
|
235
268
|
" ],",
|
|
@@ -257,7 +290,7 @@ def generate_python_code(specs: list[dict[str, Any]]) -> str:
|
|
|
257
290
|
"",
|
|
258
291
|
"",
|
|
259
292
|
"def get_ui(plugin_id: str) -> UISpec | None:",
|
|
260
|
-
' """The UI an agent
|
|
293
|
+
' """The UI plugin an agent\'s or an application\'s `ui` names, or None."""',
|
|
261
294
|
" return UI_CATALOGUE.get(plugin_id)",
|
|
262
295
|
"",
|
|
263
296
|
"",
|
|
@@ -265,12 +298,11 @@ def generate_python_code(specs: list[dict[str, Any]]) -> str:
|
|
|
265
298
|
" return list(UI_CATALOGUE.values())",
|
|
266
299
|
"",
|
|
267
300
|
"",
|
|
268
|
-
"#: The visual components the
|
|
301
|
+
"#: The visual components the UI plugins render, by the name a",
|
|
269
302
|
"#: surface gives them (LOOP C-13).",
|
|
270
303
|
"COMPONENT_CATALOGUE: Dict[str, ComponentSpec] = {",
|
|
271
304
|
" component.id: component",
|
|
272
305
|
" for plugin in UI_CATALOGUE.values()",
|
|
273
|
-
" if plugin.enabled",
|
|
274
306
|
" for component in plugin.components",
|
|
275
307
|
"}",
|
|
276
308
|
"",
|
|
@@ -300,7 +332,7 @@ def generate_typescript_code(specs: list[dict[str, Any]]) -> str:
|
|
|
300
332
|
"/**",
|
|
301
333
|
" * UI Catalog.",
|
|
302
334
|
" *",
|
|
303
|
-
" *
|
|
335
|
+
" * The UI plugins: the Reactor plugins an interface is made of (AGENTIC_UI R8).",
|
|
304
336
|
" *",
|
|
305
337
|
" * This file is AUTO-GENERATED from YAML specifications.",
|
|
306
338
|
" * DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
|
|
@@ -318,9 +350,24 @@ def generate_typescript_code(specs: list[dict[str, Any]]) -> str:
|
|
|
318
350
|
f" version: {_ts(str(spec['version']))},",
|
|
319
351
|
f" name: {_ts(spec['name'])},",
|
|
320
352
|
f" description: {_ts(str(spec.get('description', '')).strip())},",
|
|
321
|
-
f"
|
|
322
|
-
|
|
323
|
-
|
|
353
|
+
f" emoji: {json.dumps(spec['emoji'], ensure_ascii=False)},",
|
|
354
|
+
*(
|
|
355
|
+
[f" octicon: {_ts(spec['octicon'])},"]
|
|
356
|
+
if spec.get("octicon")
|
|
357
|
+
else []
|
|
358
|
+
),
|
|
359
|
+
f" plugin: {_ts(spec['plugin'])},",
|
|
360
|
+
f" folder: {_ts(spec['folder'])},",
|
|
361
|
+
*([f" per: {_ts(spec['per'])},"] if spec.get("per") else []),
|
|
362
|
+
" contributions: {",
|
|
363
|
+
f" slots: {json.dumps(list((spec.get('contributions') or {}).get('slots') or []))},",
|
|
364
|
+
f" points: {json.dumps(list((spec.get('contributions') or {}).get('points') or []))},",
|
|
365
|
+
f" commands: {json.dumps(list((spec.get('contributions') or {}).get('commands') or []))},",
|
|
366
|
+
" },",
|
|
367
|
+
f" contributionPoints: {json.dumps(list(spec.get('contribution_points') or []))},",
|
|
368
|
+
f" settings: {json.dumps(spec.get('settings') or {}, ensure_ascii=False)},",
|
|
369
|
+
f" renderers: {json.dumps(list(spec.get('renderers') or []))},",
|
|
370
|
+
f" catalog: {_ts(str(spec.get('catalog') or ''))},",
|
|
324
371
|
f" components: {json.dumps([{**c, 'events': c.get('events', [])} for c in spec.get('components') or []], indent=2, ensure_ascii=False)},",
|
|
325
372
|
"};",
|
|
326
373
|
"",
|
|
@@ -335,7 +382,7 @@ def generate_typescript_code(specs: list[dict[str, Any]]) -> str:
|
|
|
335
382
|
[
|
|
336
383
|
"};",
|
|
337
384
|
"",
|
|
338
|
-
"/** The UI an agent
|
|
385
|
+
"/** The UI plugin an agent's or an application's `ui` names, or undefined. */",
|
|
339
386
|
"export function getUI(pluginId: string): UISpec | undefined {",
|
|
340
387
|
" return UI_CATALOGUE[pluginId];",
|
|
341
388
|
"}",
|
|
@@ -344,10 +391,9 @@ def generate_typescript_code(specs: list[dict[str, Any]]) -> str:
|
|
|
344
391
|
" return Object.values(UI_CATALOGUE);",
|
|
345
392
|
"}",
|
|
346
393
|
"",
|
|
347
|
-
"/** The visual components the
|
|
394
|
+
"/** The visual components the UI plugins render, by the name a surface gives them (LOOP C-13). */",
|
|
348
395
|
"export const COMPONENT_CATALOGUE: Record<string, ComponentSpec> = Object.fromEntries(",
|
|
349
396
|
" Object.values(UI_CATALOGUE)",
|
|
350
|
-
" .filter(plugin => plugin.enabled)",
|
|
351
397
|
" .flatMap(plugin => plugin.components.map(component => [component.id, component])),",
|
|
352
398
|
");",
|
|
353
399
|
"",
|
|
@@ -0,0 +1,176 @@
|
|
|
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 the YAML UI renderer specifications.
|
|
7
|
+
|
|
8
|
+
One YAML per UI renderer under ``agentspecs/ui-renderers`` (AGENTIC_UI R8,
|
|
9
|
+
F-01): how a UI plugin's interface reaches a host that is not Datalayer's own
|
|
10
|
+
page — A2UI, MCP Apps, MCP UI. A UI plugin (``agentspecs/ui``) says the
|
|
11
|
+
renderers it may be exposed through; an agent's or an application's ``ui``
|
|
12
|
+
names, per plugin, the one it uses.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python generate_ui_renderers.py \\
|
|
16
|
+
--specs-dir agentspecs/agentspecs/ui-renderers \\
|
|
17
|
+
--python-output agent_runtimes/specs/ui_renderers.py \\
|
|
18
|
+
--typescript-output src/specs/uiRenderers.ts
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import yaml
|
|
27
|
+
from versioning import ensure_spec_version, version_suffix
|
|
28
|
+
|
|
29
|
+
FIELDS = {"id", "version", "name", "description", "docs_url", "enabled"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_ui_renderer_specs(specs_dir: Path) -> list[dict[str, Any]]:
|
|
33
|
+
"""Every UI renderer spec of a directory, each named for its id, with its fields and no other."""
|
|
34
|
+
specs = []
|
|
35
|
+
for yaml_file in sorted(specs_dir.glob("*.yaml")):
|
|
36
|
+
spec = yaml.safe_load(yaml_file.read_text()) or {}
|
|
37
|
+
ensure_spec_version(spec)
|
|
38
|
+
if spec["id"] != yaml_file.stem:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"{yaml_file.name}: the file is named for {yaml_file.stem!r}, the spec says {spec['id']!r}"
|
|
41
|
+
)
|
|
42
|
+
if set(spec) != FIELDS:
|
|
43
|
+
raise ValueError(f"{yaml_file.name}: a UI renderer says {sorted(FIELDS)}")
|
|
44
|
+
specs.append(spec)
|
|
45
|
+
return specs
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _const(spec: dict[str, Any]) -> str:
|
|
49
|
+
return (
|
|
50
|
+
spec["id"].replace("-", "_").upper()
|
|
51
|
+
+ "_UI_RENDERER"
|
|
52
|
+
+ version_suffix(spec["version"])
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _s(value: Any) -> str:
|
|
57
|
+
return json.dumps(" ".join(str(value).split()), ensure_ascii=False)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def generate_python_code(specs: list[dict[str, Any]]) -> str:
|
|
61
|
+
"""The Python catalogue of the UI renderers."""
|
|
62
|
+
lines = [
|
|
63
|
+
"# Copyright (c) 2025-2026 Datalayer, Inc.",
|
|
64
|
+
"# Distributed under the terms of the Modified BSD License.",
|
|
65
|
+
'"""',
|
|
66
|
+
"UI Renderer Catalog.",
|
|
67
|
+
"",
|
|
68
|
+
"How a UI plugin's interface reaches a host that is not Datalayer's own page",
|
|
69
|
+
"(AGENTIC_UI R8): A2UI, MCP Apps, MCP UI.",
|
|
70
|
+
"",
|
|
71
|
+
"This file is AUTO-GENERATED from YAML specifications.",
|
|
72
|
+
"DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
|
|
73
|
+
'"""',
|
|
74
|
+
"",
|
|
75
|
+
"from typing import Dict",
|
|
76
|
+
"",
|
|
77
|
+
"from agent_runtimes.types import UiRendererSpec",
|
|
78
|
+
"",
|
|
79
|
+
]
|
|
80
|
+
for spec in specs:
|
|
81
|
+
lines += [
|
|
82
|
+
f"{_const(spec)} = UiRendererSpec(",
|
|
83
|
+
f" id={_s(spec['id'])},",
|
|
84
|
+
f" version={_s(spec['version'])},",
|
|
85
|
+
f" name={_s(spec['name'])},",
|
|
86
|
+
f" description={_s(spec['description'])},",
|
|
87
|
+
f" docs_url={_s(spec['docs_url'])},",
|
|
88
|
+
f" enabled={bool(spec['enabled'])},",
|
|
89
|
+
")",
|
|
90
|
+
"",
|
|
91
|
+
]
|
|
92
|
+
lines += ["UI_RENDERER_CATALOGUE: Dict[str, UiRendererSpec] = {"]
|
|
93
|
+
lines += [f" {_s(spec['id'])}: {_const(spec)}," for spec in specs]
|
|
94
|
+
lines += [
|
|
95
|
+
"}",
|
|
96
|
+
"",
|
|
97
|
+
"",
|
|
98
|
+
"def get_ui_renderer(renderer_id: str) -> UiRendererSpec | None:",
|
|
99
|
+
' """The UI renderer a plugin\'s `renderers` or a `ui` choice names, or None."""',
|
|
100
|
+
" return UI_RENDERER_CATALOGUE.get(renderer_id)",
|
|
101
|
+
"",
|
|
102
|
+
"",
|
|
103
|
+
"def list_ui_renderers() -> list[UiRendererSpec]:",
|
|
104
|
+
" return list(UI_RENDERER_CATALOGUE.values())",
|
|
105
|
+
"",
|
|
106
|
+
]
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def generate_typescript_code(specs: list[dict[str, Any]]) -> str:
|
|
111
|
+
"""The TypeScript catalogue of the UI renderers."""
|
|
112
|
+
lines = [
|
|
113
|
+
"/*",
|
|
114
|
+
" * Copyright (c) 2025-2026 Datalayer, Inc.",
|
|
115
|
+
" * Distributed under the terms of the Modified BSD License.",
|
|
116
|
+
" */",
|
|
117
|
+
"",
|
|
118
|
+
"/**",
|
|
119
|
+
" * UI Renderer Catalog.",
|
|
120
|
+
" *",
|
|
121
|
+
" * How a UI plugin's interface reaches a host that is not Datalayer's own",
|
|
122
|
+
" * page (AGENTIC_UI R8): A2UI, MCP Apps, MCP UI.",
|
|
123
|
+
" *",
|
|
124
|
+
" * This file is AUTO-GENERATED from YAML specifications.",
|
|
125
|
+
" * DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
|
|
126
|
+
" */",
|
|
127
|
+
"",
|
|
128
|
+
"import type { UiRendererSpec } from '../types/agentspecs';",
|
|
129
|
+
"",
|
|
130
|
+
]
|
|
131
|
+
for spec in specs:
|
|
132
|
+
lines += [
|
|
133
|
+
f"export const {_const(spec)}: UiRendererSpec = {{",
|
|
134
|
+
f" id: {_s(spec['id'])},",
|
|
135
|
+
f" version: {_s(spec['version'])},",
|
|
136
|
+
f" name: {_s(spec['name'])},",
|
|
137
|
+
f" description: {_s(spec['description'])},",
|
|
138
|
+
f" docsUrl: {_s(spec['docs_url'])},",
|
|
139
|
+
f" enabled: {'true' if spec['enabled'] else 'false'},",
|
|
140
|
+
"};",
|
|
141
|
+
"",
|
|
142
|
+
]
|
|
143
|
+
lines += ["export const UI_RENDERER_CATALOGUE: Record<string, UiRendererSpec> = {"]
|
|
144
|
+
lines += [f" {_s(spec['id'])}: {_const(spec)}," for spec in specs]
|
|
145
|
+
lines += [
|
|
146
|
+
"};",
|
|
147
|
+
"",
|
|
148
|
+
"/** The UI renderer a plugin's `renderers` or a `ui` choice names, or undefined. */",
|
|
149
|
+
"export function getUiRenderer(rendererId: string): UiRendererSpec | undefined {",
|
|
150
|
+
" return UI_RENDERER_CATALOGUE[rendererId];",
|
|
151
|
+
"}",
|
|
152
|
+
"",
|
|
153
|
+
"export function listUiRenderers(): UiRendererSpec[] {",
|
|
154
|
+
" return Object.values(UI_RENDERER_CATALOGUE);",
|
|
155
|
+
"}",
|
|
156
|
+
"",
|
|
157
|
+
]
|
|
158
|
+
return "\n".join(lines)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def main() -> None:
|
|
162
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
163
|
+
parser.add_argument("--specs-dir", type=Path, required=True)
|
|
164
|
+
parser.add_argument("--python-output", type=Path, required=True)
|
|
165
|
+
parser.add_argument("--typescript-output", type=Path, required=True)
|
|
166
|
+
args = parser.parse_args()
|
|
167
|
+
specs = load_ui_renderer_specs(args.specs_dir)
|
|
168
|
+
args.python_output.write_text(generate_python_code(specs))
|
|
169
|
+
args.typescript_output.write_text(generate_typescript_code(specs))
|
|
170
|
+
print(
|
|
171
|
+
f"Generated {len(specs)} UI renderers: {args.python_output}, {args.typescript_output}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
main()
|