@datalayer/agent-runtimes 0.0.10 → 0.0.11
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/lib/components/AgentConfiguration.d.ts +30 -0
- package/lib/components/AgentConfiguration.js +71 -16
- package/lib/components/chat/components/AgentDetails.js +159 -1
- package/lib/components/chat/components/ContextDistribution.js +2 -2
- package/lib/components/chat/components/ContextInspector.js +4 -2
- package/lib/components/chat/components/ContextPanel.js +1 -6
- package/lib/components/index.d.ts +2 -2
- package/lib/components/index.js +1 -1
- package/lib/config/agents/code-ai/agents.d.ts +25 -0
- package/lib/config/agents/code-ai/agents.js +70 -0
- package/lib/config/agents/code-ai/index.d.ts +1 -0
- package/lib/config/agents/code-ai/index.js +5 -0
- package/lib/config/{agents.d.ts → agents/codemode-paper/agents.d.ts} +1 -5
- package/lib/config/{agents.js → agents/codemode-paper/agents.js} +29 -165
- package/lib/config/agents/codemode-paper/index.d.ts +1 -0
- package/lib/config/agents/codemode-paper/index.js +5 -0
- package/lib/config/agents/datalayer-ai/agents.d.ts +29 -0
- package/lib/config/agents/datalayer-ai/agents.js +267 -0
- package/lib/config/agents/datalayer-ai/index.d.ts +1 -0
- package/lib/config/agents/datalayer-ai/index.js +5 -0
- package/lib/config/agents/index.d.ts +19 -0
- package/lib/config/agents/index.js +38 -0
- package/lib/config/envvars.d.ts +28 -0
- package/lib/config/envvars.js +115 -0
- package/lib/config/index.d.ts +1 -0
- package/lib/config/index.js +1 -0
- package/lib/config/mcpServers.js +26 -2
- package/lib/config/skills.d.ts +2 -0
- package/lib/config/skills.js +6 -0
- package/lib/examples/AgentSpaceFormExample.js +51 -9
- package/lib/types.d.ts +10 -2
- package/package.json +2 -2
- package/scripts/codegen/generate_agents.py +565 -154
- package/scripts/codegen/generate_envvars.py +302 -0
- package/scripts/codegen/generate_mcp_servers.py +28 -16
- package/scripts/codegen/generate_skills.py +19 -6
|
@@ -9,6 +9,7 @@ Generates Python and TypeScript code from YAML agent specifications.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import argparse
|
|
12
|
+
import subprocess
|
|
12
13
|
import sys
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any, Dict, List
|
|
@@ -23,18 +24,35 @@ def _fmt_list(items: list[str]) -> str:
|
|
|
23
24
|
return "[" + ", ".join(f'"{item}"' for item in items) + "]"
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
def load_yaml_specs(specs_dir: Path) -> List[Dict[str, Any]]:
|
|
27
|
-
"""
|
|
27
|
+
def load_yaml_specs(specs_dir: Path) -> List[tuple[str, Dict[str, Any]]]:
|
|
28
|
+
"""
|
|
29
|
+
Load all YAML agent specifications from directory and subdirectories.
|
|
30
|
+
|
|
31
|
+
Returns list of tuples: (subfolder_name, spec_dict)
|
|
32
|
+
where subfolder_name is the immediate parent folder name, or "" for root level.
|
|
33
|
+
"""
|
|
28
34
|
specs = []
|
|
35
|
+
|
|
36
|
+
# First, load specs from root level
|
|
29
37
|
for yaml_file in sorted(specs_dir.glob("*.yaml")):
|
|
30
38
|
with open(yaml_file, "r") as f:
|
|
31
39
|
spec = yaml.safe_load(f)
|
|
32
40
|
if spec: # Skip empty files
|
|
33
|
-
specs.append(spec)
|
|
41
|
+
specs.append(("", spec))
|
|
42
|
+
|
|
43
|
+
# Then, load specs from subdirectories (one level deep)
|
|
44
|
+
for subdir in sorted(specs_dir.iterdir()):
|
|
45
|
+
if subdir.is_dir() and not subdir.name.startswith("."):
|
|
46
|
+
for yaml_file in sorted(subdir.glob("*.yaml")):
|
|
47
|
+
with open(yaml_file, "r") as f:
|
|
48
|
+
spec = yaml.safe_load(f)
|
|
49
|
+
if spec: # Skip empty files
|
|
50
|
+
specs.append((subdir.name, spec))
|
|
51
|
+
|
|
34
52
|
return specs
|
|
35
53
|
|
|
36
54
|
|
|
37
|
-
def generate_python_code(specs: List[Dict[str, Any]]) -> str:
|
|
55
|
+
def generate_python_code(specs: List[tuple[str, Dict[str, Any]]]) -> str:
|
|
38
56
|
"""Generate Python code from agent specifications."""
|
|
39
57
|
# Header
|
|
40
58
|
code = '''# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
@@ -59,59 +77,96 @@ from agent_runtimes.types import AgentSpec
|
|
|
59
77
|
|
|
60
78
|
'''
|
|
61
79
|
|
|
62
|
-
#
|
|
80
|
+
# Organize specs by subfolder
|
|
81
|
+
from collections import defaultdict
|
|
82
|
+
|
|
83
|
+
specs_by_folder: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
|
84
|
+
for folder, spec in specs:
|
|
85
|
+
specs_by_folder[folder].append(spec)
|
|
86
|
+
|
|
87
|
+
# Generate agent spec constants organized by folder
|
|
63
88
|
agent_ids = []
|
|
64
|
-
for spec in specs:
|
|
65
|
-
agent_id = spec["id"]
|
|
66
|
-
# Create constant name: e.g., "data-acquisition" -> "DATA_ACQUISITION_AGENT_SPEC"
|
|
67
|
-
# But if id already ends with "-agent", don't duplicate: "github-agent" -> "GITHUB_AGENT_SPEC"
|
|
68
|
-
if agent_id.endswith("-agent"):
|
|
69
|
-
const_name = agent_id.upper().replace("-", "_") + "_SPEC"
|
|
70
|
-
else:
|
|
71
|
-
const_name = agent_id.upper().replace("-", "_") + "_AGENT_SPEC"
|
|
72
|
-
agent_ids.append((agent_id, const_name))
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
90
|
+
# Sort folders: empty string (root) first, then alphabetically
|
|
91
|
+
sorted_folders = sorted(
|
|
92
|
+
specs_by_folder.keys(), key=lambda x: "" if x == "" else f"z{x}"
|
|
93
|
+
)
|
|
79
94
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
f'"""
|
|
111
|
-
|
|
95
|
+
for folder in sorted_folders:
|
|
96
|
+
folder_specs = specs_by_folder[folder]
|
|
97
|
+
|
|
98
|
+
# Add folder header if not root
|
|
99
|
+
if folder:
|
|
100
|
+
code += f"\n# {folder.replace('-', ' ').title()} Agents\n"
|
|
101
|
+
code += f"# {'=' * 76}\n\n"
|
|
102
|
+
|
|
103
|
+
for spec in folder_specs:
|
|
104
|
+
agent_id = spec["id"]
|
|
105
|
+
# Prefix agent ID with folder name for uniqueness
|
|
106
|
+
full_agent_id = f"{folder}/{agent_id}" if folder else agent_id
|
|
107
|
+
# Create constant name: e.g., "data-acquisition" -> "DATA_ACQUISITION_AGENT_SPEC"
|
|
108
|
+
# But if id already ends with "-agent", don't duplicate: "github-agent" -> "GITHUB_AGENT_SPEC"
|
|
109
|
+
# NO folder prefix for Python constants
|
|
110
|
+
base_name = agent_id.upper().replace("-", "_")
|
|
111
|
+
|
|
112
|
+
if agent_id.endswith("-agent"):
|
|
113
|
+
const_name = base_name + "_SPEC"
|
|
114
|
+
else:
|
|
115
|
+
const_name = base_name + "_AGENT_SPEC"
|
|
116
|
+
agent_ids.append((full_agent_id, const_name, folder))
|
|
117
|
+
|
|
118
|
+
# Get MCP servers
|
|
119
|
+
mcp_server_ids = spec.get("mcp_servers", [])
|
|
120
|
+
mcp_servers_str = ", ".join(
|
|
121
|
+
f'MCP_SERVER_CATALOG["{sid}"]' for sid in mcp_server_ids
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Format optional fields
|
|
125
|
+
icon = f'"{spec.get("icon")}"' if spec.get("icon") else "None"
|
|
126
|
+
emoji = f'"{spec.get("emoji")}"' if spec.get("emoji") else "None"
|
|
127
|
+
color = f'"{spec.get("color")}"' if spec.get("color") else "None"
|
|
128
|
+
suggestions = spec.get("suggestions", [])
|
|
129
|
+
suggestions_str = (
|
|
130
|
+
"[\n "
|
|
131
|
+
+ ",\n ".join(f'"{s}"' for s in suggestions)
|
|
132
|
+
+ ",\n ]"
|
|
133
|
+
if suggestions
|
|
134
|
+
else "[]"
|
|
135
|
+
)
|
|
136
|
+
# Escape multi-line strings properly
|
|
137
|
+
welcome = (
|
|
138
|
+
spec.get("welcome_message", "").replace('"', '\\"').replace("\n", " ")
|
|
139
|
+
)
|
|
140
|
+
welcome_notebook = spec.get("welcome_notebook")
|
|
141
|
+
welcome_document = spec.get("welcome_document")
|
|
142
|
+
system_prompt = spec.get("system_prompt", "")
|
|
143
|
+
system_prompt_codemode_addons = spec.get(
|
|
144
|
+
"system_prompt_codemode_addons", ""
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Escape triple quotes in system prompts for Python triple-quoted strings
|
|
148
|
+
if system_prompt:
|
|
149
|
+
system_prompt = system_prompt.replace('"""', r"\"\"\"")
|
|
150
|
+
if system_prompt_codemode_addons:
|
|
151
|
+
system_prompt_codemode_addons = system_prompt_codemode_addons.replace(
|
|
152
|
+
'"""', r"\"\"\""
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Clean description for Python (single line)
|
|
156
|
+
description = (
|
|
157
|
+
spec["description"].replace("\n", " ").replace(" ", " ").strip()
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Use triple quotes for multiline system prompts
|
|
161
|
+
system_prompt_str = f'"""{system_prompt}"""' if system_prompt else "None"
|
|
162
|
+
system_prompt_codemode_addons_str = (
|
|
163
|
+
f'"""{system_prompt_codemode_addons}"""'
|
|
164
|
+
if system_prompt_codemode_addons
|
|
165
|
+
else "None"
|
|
166
|
+
)
|
|
112
167
|
|
|
113
|
-
|
|
114
|
-
id="{
|
|
168
|
+
code += f'''{const_name} = AgentSpec(
|
|
169
|
+
id="{full_agent_id}",
|
|
115
170
|
name="{spec["name"]}",
|
|
116
171
|
description="{description}",
|
|
117
172
|
tags={_fmt_list(spec.get("tags", []))},
|
|
@@ -120,18 +175,19 @@ from agent_runtimes.types import AgentSpec
|
|
|
120
175
|
skills={_fmt_list(spec.get("skills", []))},
|
|
121
176
|
environment_name="{spec.get("environment_name", "ai-agents-env")}",
|
|
122
177
|
icon={icon},
|
|
178
|
+
emoji={emoji},
|
|
123
179
|
color={color},
|
|
124
180
|
suggestions={suggestions_str},
|
|
125
181
|
welcome_message="{welcome}",
|
|
126
182
|
welcome_notebook={f'"{welcome_notebook}"' if welcome_notebook else "None"},
|
|
127
183
|
welcome_document={f'"{welcome_document}"' if welcome_document else "None"},
|
|
128
184
|
system_prompt={system_prompt_str},
|
|
129
|
-
|
|
185
|
+
system_prompt_codemode_addons={system_prompt_codemode_addons_str},
|
|
130
186
|
)
|
|
131
187
|
|
|
132
188
|
'''
|
|
133
189
|
|
|
134
|
-
# Generate registry
|
|
190
|
+
# Generate registry organized by folder
|
|
135
191
|
code += """
|
|
136
192
|
# ============================================================================
|
|
137
193
|
# Agent Specs Registry
|
|
@@ -139,8 +195,16 @@ from agent_runtimes.types import AgentSpec
|
|
|
139
195
|
|
|
140
196
|
AGENT_SPECS: Dict[str, AgentSpec] = {
|
|
141
197
|
"""
|
|
142
|
-
|
|
143
|
-
|
|
198
|
+
|
|
199
|
+
# Sort by folder for organized registry
|
|
200
|
+
for folder in sorted_folders:
|
|
201
|
+
folder_agents = [(aid, cname) for aid, cname, f in agent_ids if f == folder]
|
|
202
|
+
if folder_agents and folder:
|
|
203
|
+
code += f" # {folder.replace('-', ' ').title()}\n"
|
|
204
|
+
for full_agent_id, const_name in folder_agents:
|
|
205
|
+
code += f' "{full_agent_id}": {const_name},\n'
|
|
206
|
+
if folder_agents and folder:
|
|
207
|
+
code += "\n"
|
|
144
208
|
|
|
145
209
|
code += """}
|
|
146
210
|
|
|
@@ -172,7 +236,7 @@ def list_agent_specs() -> list[AgentSpec]:
|
|
|
172
236
|
|
|
173
237
|
|
|
174
238
|
def generate_typescript_code(
|
|
175
|
-
specs: List[Dict[str, Any]], mcp_specs_dir: str, skills_specs_dir: str
|
|
239
|
+
specs: List[tuple[str, Dict[str, Any]]], mcp_specs_dir: str, skills_specs_dir: str
|
|
176
240
|
) -> str:
|
|
177
241
|
"""Generate TypeScript code from agent specifications."""
|
|
178
242
|
# Load available MCP servers from specs
|
|
@@ -190,21 +254,36 @@ def generate_typescript_code(
|
|
|
190
254
|
skill_ids = [os.path.basename(f).replace(".yaml", "") for f in skill_files]
|
|
191
255
|
skill_ids.sort()
|
|
192
256
|
|
|
193
|
-
#
|
|
257
|
+
# Determine which MCP servers and skills are actually used in these specs
|
|
258
|
+
used_mcp_servers = set()
|
|
259
|
+
used_skills = set()
|
|
260
|
+
for _, spec in specs:
|
|
261
|
+
for server in spec.get("mcp_servers", []):
|
|
262
|
+
used_mcp_servers.add(server)
|
|
263
|
+
for skill in spec.get("skills", []):
|
|
264
|
+
used_skills.add(skill)
|
|
265
|
+
|
|
266
|
+
# Only import what's actually used
|
|
194
267
|
mcp_imports = []
|
|
195
268
|
mcp_map_entries = []
|
|
196
269
|
for server_id in mcp_server_ids:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
270
|
+
if server_id in used_mcp_servers:
|
|
271
|
+
const_name = server_id.upper().replace("-", "_") + "_MCP_SERVER"
|
|
272
|
+
mcp_imports.append(const_name)
|
|
273
|
+
mcp_map_entries.append(f" '{server_id}': {const_name},")
|
|
200
274
|
|
|
201
275
|
# Generate skill import names and map entries
|
|
202
276
|
skill_imports = []
|
|
203
277
|
skill_map_entries = []
|
|
204
278
|
for sid in skill_ids:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
279
|
+
if sid in used_skills:
|
|
280
|
+
const_name = sid.upper().replace("-", "_") + "_SKILL_SPEC"
|
|
281
|
+
skill_imports.append(const_name)
|
|
282
|
+
skill_map_entries.append(f" '{sid}': {const_name},")
|
|
283
|
+
|
|
284
|
+
# Determine if we need any helper code
|
|
285
|
+
has_mcp = len(mcp_imports) > 0
|
|
286
|
+
has_skills = len(skill_imports) > 0
|
|
208
287
|
|
|
209
288
|
# Header
|
|
210
289
|
code = """/*
|
|
@@ -220,34 +299,45 @@ def generate_typescript_code(
|
|
|
220
299
|
* Generated from YAML specifications in specs/agents/
|
|
221
300
|
*/
|
|
222
301
|
|
|
223
|
-
import type { AgentSpec } from '
|
|
224
|
-
import {
|
|
302
|
+
import type { AgentSpec } from '../../../types';
|
|
225
303
|
"""
|
|
226
|
-
code += " " + ",\n ".join(mcp_imports) + ",\n"
|
|
227
|
-
code += """} from './mcpServers';
|
|
228
|
-
import {
|
|
229
|
-
"""
|
|
230
|
-
code += " " + ",\n ".join(skill_imports) + ",\n"
|
|
231
|
-
code += """} from './skills';
|
|
232
|
-
import type { SkillSpec } from './skills';
|
|
233
304
|
|
|
305
|
+
# Only add MCP server imports if needed
|
|
306
|
+
if has_mcp:
|
|
307
|
+
code += "import {\n"
|
|
308
|
+
code += " " + ",\n ".join(mcp_imports) + ",\n"
|
|
309
|
+
code += "} from '../../mcpServers';\n"
|
|
310
|
+
|
|
311
|
+
# Only add skill imports if needed
|
|
312
|
+
if has_skills:
|
|
313
|
+
code += "import {\n"
|
|
314
|
+
code += " " + ",\n ".join(skill_imports) + ",\n"
|
|
315
|
+
code += "} from '../../skills';\n"
|
|
316
|
+
code += "import type { SkillSpec } from '../../skills';\n"
|
|
317
|
+
|
|
318
|
+
# Only add MCP server lookup if used
|
|
319
|
+
if has_mcp:
|
|
320
|
+
code += """
|
|
234
321
|
// ============================================================================
|
|
235
322
|
// MCP Server Lookup
|
|
236
323
|
// ============================================================================
|
|
237
324
|
|
|
238
325
|
const MCP_SERVER_MAP: Record<string, any> = {
|
|
239
326
|
"""
|
|
240
|
-
|
|
241
|
-
|
|
327
|
+
code += "\n".join(mcp_map_entries) + "\n"
|
|
328
|
+
code += "};\n"
|
|
242
329
|
|
|
330
|
+
# Only add skill lookup if used
|
|
331
|
+
if has_skills:
|
|
332
|
+
code += """
|
|
243
333
|
/**
|
|
244
334
|
* Map skill IDs to SkillSpec objects, converting to AgentSkillSpec shape.
|
|
245
335
|
*/
|
|
246
336
|
const SKILL_MAP: Record<string, any> = {
|
|
247
337
|
"""
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
338
|
+
code += "\n".join(skill_map_entries) + "\n"
|
|
339
|
+
code += "};\n"
|
|
340
|
+
code += """
|
|
251
341
|
function toAgentSkillSpec(skill: SkillSpec) {
|
|
252
342
|
return {
|
|
253
343
|
id: skill.id,
|
|
@@ -259,70 +349,108 @@ function toAgentSkillSpec(skill: SkillSpec) {
|
|
|
259
349
|
requiredEnvVars: skill.requiredEnvVars,
|
|
260
350
|
};
|
|
261
351
|
}
|
|
352
|
+
"""
|
|
262
353
|
|
|
354
|
+
code += """
|
|
263
355
|
// ============================================================================
|
|
264
356
|
// Agent Specs
|
|
265
357
|
// ============================================================================
|
|
266
358
|
|
|
267
359
|
"""
|
|
268
360
|
|
|
269
|
-
#
|
|
270
|
-
|
|
271
|
-
for spec in specs:
|
|
272
|
-
agent_id = spec["id"]
|
|
273
|
-
# Create constant name: e.g., "data-acquisition" -> "DATA_ACQUISITION_AGENT_SPEC"
|
|
274
|
-
# But if id already ends with "-agent", don't duplicate: "github-agent" -> "GITHUB_AGENT_SPEC"
|
|
275
|
-
if agent_id.endswith("-agent"):
|
|
276
|
-
const_name = agent_id.upper().replace("-", "_") + "_SPEC"
|
|
277
|
-
else:
|
|
278
|
-
const_name = agent_id.upper().replace("-", "_") + "_AGENT_SPEC"
|
|
279
|
-
agent_ids.append((agent_id, const_name))
|
|
361
|
+
# Organize specs by subfolder for TypeScript
|
|
362
|
+
from collections import defaultdict
|
|
280
363
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
f"MCP_SERVER_MAP['{sid}']" for sid in mcp_server_ids
|
|
285
|
-
)
|
|
364
|
+
specs_by_folder: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
|
365
|
+
for folder, spec in specs:
|
|
366
|
+
specs_by_folder[folder].append(spec)
|
|
286
367
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if
|
|
290
|
-
|
|
291
|
-
f"toAgentSkillSpec(SKILL_MAP['{sid}'])" for sid in skill_ids_list
|
|
292
|
-
)
|
|
293
|
-
else:
|
|
294
|
-
skills_str = ""
|
|
295
|
-
|
|
296
|
-
# Format tags and suggestions as arrays
|
|
297
|
-
tags = spec.get("tags", [])
|
|
298
|
-
tags_str = "[" + ", ".join(f"'{t}'" for t in tags) + "]"
|
|
299
|
-
|
|
300
|
-
suggestions = spec.get("suggestions", [])
|
|
301
|
-
# Escape single quotes in suggestions for TypeScript
|
|
302
|
-
escaped_suggestions = [s.replace("'", "\\'") for s in suggestions]
|
|
303
|
-
suggestions_str = (
|
|
304
|
-
"[\n " + ",\n ".join(f"'{s}'" for s in escaped_suggestions) + ",\n ]"
|
|
305
|
-
if suggestions
|
|
306
|
-
else "[]"
|
|
307
|
-
)
|
|
368
|
+
# Sort folders: empty string (root) first, then alphabetically
|
|
369
|
+
sorted_folders = sorted(
|
|
370
|
+
specs_by_folder.keys(), key=lambda x: "" if x == "" else f"z{x}"
|
|
371
|
+
)
|
|
308
372
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
color = f"'{spec.get('color')}'" if spec.get("color") else "undefined"
|
|
312
|
-
system_prompt = spec.get("system_prompt")
|
|
313
|
-
system_prompt_codemode = spec.get("system_prompt_codemode")
|
|
373
|
+
# Generate agent spec constants organized by folder
|
|
374
|
+
agent_ids = []
|
|
314
375
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if
|
|
319
|
-
|
|
376
|
+
for folder in sorted_folders:
|
|
377
|
+
folder_specs = specs_by_folder[folder]
|
|
378
|
+
|
|
379
|
+
# Add folder header if not root
|
|
380
|
+
if folder:
|
|
381
|
+
code += f"// {folder.replace('-', ' ').title()} Agents\n"
|
|
382
|
+
code += f"// {'=' * 76}\n\n"
|
|
383
|
+
|
|
384
|
+
for spec in folder_specs:
|
|
385
|
+
agent_id = spec["id"]
|
|
386
|
+
# Prefix agent ID with folder name for uniqueness
|
|
387
|
+
full_agent_id = f"{folder}/{agent_id}" if folder else agent_id
|
|
388
|
+
# Create constant name: e.g., "data-acquisition" -> "DATA_ACQUISITION_AGENT_SPEC"
|
|
389
|
+
# But if id already ends with "-agent", don't duplicate: "github-agent" -> "GITHUB_AGENT_SPEC"
|
|
390
|
+
# NO folder prefix for TypeScript constants
|
|
391
|
+
base_name = agent_id.upper().replace("-", "_")
|
|
392
|
+
|
|
393
|
+
if agent_id.endswith("-agent"):
|
|
394
|
+
const_name = base_name + "_SPEC"
|
|
395
|
+
else:
|
|
396
|
+
const_name = base_name + "_AGENT_SPEC"
|
|
397
|
+
agent_ids.append((full_agent_id, const_name, folder))
|
|
398
|
+
|
|
399
|
+
# Get MCP servers
|
|
400
|
+
mcp_server_ids = spec.get("mcp_servers", [])
|
|
401
|
+
if has_mcp and mcp_server_ids:
|
|
402
|
+
mcp_servers_str = ", ".join(
|
|
403
|
+
f"MCP_SERVER_MAP['{sid}']" for sid in mcp_server_ids
|
|
404
|
+
)
|
|
405
|
+
else:
|
|
406
|
+
mcp_servers_str = ""
|
|
407
|
+
|
|
408
|
+
# Get skills - resolve to AgentSkillSpec via toAgentSkillSpec
|
|
409
|
+
skill_ids_list = spec.get("skills", [])
|
|
410
|
+
if has_skills and skill_ids_list:
|
|
411
|
+
skills_str = ", ".join(
|
|
412
|
+
f"toAgentSkillSpec(SKILL_MAP['{sid}'])" for sid in skill_ids_list
|
|
413
|
+
)
|
|
414
|
+
else:
|
|
415
|
+
skills_str = ""
|
|
416
|
+
|
|
417
|
+
# Format tags and suggestions as arrays
|
|
418
|
+
tags = spec.get("tags", [])
|
|
419
|
+
tags_str = "[" + ", ".join(f"'{t}'" for t in tags) + "]"
|
|
420
|
+
|
|
421
|
+
suggestions = spec.get("suggestions", [])
|
|
422
|
+
# Escape single quotes in suggestions for TypeScript
|
|
423
|
+
escaped_suggestions = [s.replace("'", "\\'") for s in suggestions]
|
|
424
|
+
suggestions_str = (
|
|
425
|
+
"[\n "
|
|
426
|
+
+ ",\n ".join(f"'{s}'" for s in escaped_suggestions)
|
|
427
|
+
+ ",\n ]"
|
|
428
|
+
if suggestions
|
|
429
|
+
else "[]"
|
|
430
|
+
)
|
|
320
431
|
|
|
321
|
-
|
|
322
|
-
|
|
432
|
+
# Format optional fields
|
|
433
|
+
icon = f"'{spec.get('icon')}'" if spec.get("icon") else "undefined"
|
|
434
|
+
emoji = f"'{spec.get('emoji')}'" if spec.get("emoji") else "undefined"
|
|
435
|
+
color = f"'{spec.get('color')}'" if spec.get("color") else "undefined"
|
|
436
|
+
system_prompt = spec.get("system_prompt")
|
|
437
|
+
system_prompt_codemode_addons = spec.get("system_prompt_codemode_addons")
|
|
438
|
+
|
|
439
|
+
# Escape backticks for TypeScript template literals
|
|
440
|
+
if system_prompt:
|
|
441
|
+
system_prompt = system_prompt.replace("`", "\\`")
|
|
442
|
+
if system_prompt_codemode_addons:
|
|
443
|
+
system_prompt_codemode_addons = system_prompt_codemode_addons.replace(
|
|
444
|
+
"`", "\\`"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Clean description for TypeScript (multi-line template literal)
|
|
448
|
+
description = (
|
|
449
|
+
spec["description"].replace("\n", " ").replace(" ", " ").strip()
|
|
450
|
+
)
|
|
323
451
|
|
|
324
|
-
|
|
325
|
-
id: '{
|
|
452
|
+
code += f"""export const {const_name}: AgentSpec = {{
|
|
453
|
+
id: '{full_agent_id}',
|
|
326
454
|
name: '{spec["name"]}',
|
|
327
455
|
description: `{description}`,
|
|
328
456
|
tags: {tags_str},
|
|
@@ -331,23 +459,32 @@ function toAgentSkillSpec(skill: SkillSpec) {
|
|
|
331
459
|
skills: [{skills_str}],
|
|
332
460
|
environmentName: '{spec.get("environment_name", "ai-agents-env")}',
|
|
333
461
|
icon: {icon},
|
|
462
|
+
emoji: {emoji},
|
|
334
463
|
color: {color},
|
|
335
464
|
suggestions: {suggestions_str},
|
|
336
465
|
systemPrompt: {f"`{system_prompt}`" if system_prompt else "undefined"},
|
|
337
|
-
|
|
466
|
+
systemPromptCodemodeAddons: {f"`{system_prompt_codemode_addons}`" if system_prompt_codemode_addons else "undefined"},
|
|
338
467
|
}};
|
|
339
468
|
|
|
340
469
|
"""
|
|
341
470
|
|
|
342
|
-
# Generate registry
|
|
471
|
+
# Generate registry organized by folder
|
|
343
472
|
code += """// ============================================================================
|
|
344
473
|
// Agent Specs Registry
|
|
345
474
|
// ============================================================================
|
|
346
475
|
|
|
347
476
|
export const AGENT_SPECS: Record<string, AgentSpec> = {
|
|
348
477
|
"""
|
|
349
|
-
|
|
350
|
-
|
|
478
|
+
|
|
479
|
+
# Sort by folder for organized registry
|
|
480
|
+
for folder in sorted_folders:
|
|
481
|
+
folder_agents = [(aid, cname) for aid, cname, f in agent_ids if f == folder]
|
|
482
|
+
if folder_agents and folder:
|
|
483
|
+
code += f" // {folder.replace('-', ' ').title()}\n"
|
|
484
|
+
for full_agent_id, const_name in folder_agents:
|
|
485
|
+
code += f" '{full_agent_id}': {const_name},\n"
|
|
486
|
+
if folder_agents and folder:
|
|
487
|
+
code += "\n"
|
|
351
488
|
|
|
352
489
|
code += """};
|
|
353
490
|
|
|
@@ -390,6 +527,265 @@ export function getAgentSpecRequiredEnvVars(spec: AgentSpec): string[] {
|
|
|
390
527
|
return code
|
|
391
528
|
|
|
392
529
|
|
|
530
|
+
def update_init_file(
|
|
531
|
+
specs: List[tuple[str, Dict[str, Any]]], init_file_path: Path
|
|
532
|
+
) -> None:
|
|
533
|
+
"""Update __init__.py with the new agent spec constants."""
|
|
534
|
+
# Collect all constant names
|
|
535
|
+
const_names = []
|
|
536
|
+
for folder, spec in specs:
|
|
537
|
+
agent_id = spec["id"]
|
|
538
|
+
if folder:
|
|
539
|
+
base_name = (
|
|
540
|
+
f"{folder}_{agent_id}".upper().replace("-", "_").replace("/", "_")
|
|
541
|
+
)
|
|
542
|
+
else:
|
|
543
|
+
base_name = agent_id.upper().replace("-", "_")
|
|
544
|
+
|
|
545
|
+
if agent_id.endswith("-agent"):
|
|
546
|
+
const_name = base_name + "_SPEC"
|
|
547
|
+
else:
|
|
548
|
+
const_name = base_name + "_AGENT_SPEC"
|
|
549
|
+
const_names.append(const_name)
|
|
550
|
+
|
|
551
|
+
# Sort for consistent ordering
|
|
552
|
+
const_names.sort()
|
|
553
|
+
|
|
554
|
+
# Read the current __init__.py
|
|
555
|
+
with open(init_file_path, "r") as f:
|
|
556
|
+
content = f.read()
|
|
557
|
+
|
|
558
|
+
# Find the agents import block and replace it
|
|
559
|
+
import re
|
|
560
|
+
|
|
561
|
+
# Pattern to match the entire from .agents import block
|
|
562
|
+
pattern = r"(from \.agents import \(\n)(.*?)(\n\))"
|
|
563
|
+
|
|
564
|
+
# Build new imports
|
|
565
|
+
new_imports = " AGENT_SPECS,\n"
|
|
566
|
+
for const_name in const_names:
|
|
567
|
+
new_imports += f" {const_name},\n"
|
|
568
|
+
new_imports += " get_agent_spec,\n"
|
|
569
|
+
new_imports += " list_agent_specs,"
|
|
570
|
+
|
|
571
|
+
# Replace the imports
|
|
572
|
+
new_content = re.sub(pattern, r"\1" + new_imports + r"\3", content, flags=re.DOTALL)
|
|
573
|
+
|
|
574
|
+
# Write back
|
|
575
|
+
with open(init_file_path, "w") as f:
|
|
576
|
+
f.write(new_content)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def generate_subfolder_structure(specs: List[tuple[str, Dict[str, Any]]], args):
|
|
580
|
+
"""Generate separate agent files per subfolder."""
|
|
581
|
+
from collections import defaultdict
|
|
582
|
+
|
|
583
|
+
# Organize specs by folder
|
|
584
|
+
specs_by_folder: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
|
585
|
+
for folder, spec in specs:
|
|
586
|
+
specs_by_folder[folder].append(spec)
|
|
587
|
+
|
|
588
|
+
# Get MCP and skills specs directories
|
|
589
|
+
mcp_specs_dir = args.specs_dir.parent / "mcp-servers"
|
|
590
|
+
skills_specs_dir = args.specs_dir.parent / "skills"
|
|
591
|
+
|
|
592
|
+
# Determine base directories
|
|
593
|
+
python_base = args.python_output.parent / "agents"
|
|
594
|
+
typescript_base = args.typescript_output.parent / "agents"
|
|
595
|
+
|
|
596
|
+
print(f"Generating subfolder structure in {python_base} and {typescript_base}...")
|
|
597
|
+
|
|
598
|
+
# Generate files for each folder
|
|
599
|
+
all_python_imports = []
|
|
600
|
+
all_typescript_imports = []
|
|
601
|
+
|
|
602
|
+
for folder, folder_specs in sorted(specs_by_folder.items()):
|
|
603
|
+
if not folder: # Skip root level for now
|
|
604
|
+
continue
|
|
605
|
+
|
|
606
|
+
print(f" Generating agents for subfolder: {folder}")
|
|
607
|
+
|
|
608
|
+
# Convert folder name to valid Python module name (replace hyphens with underscores)
|
|
609
|
+
folder_python_name = folder.replace("-", "_")
|
|
610
|
+
|
|
611
|
+
# Create Python subfolder file
|
|
612
|
+
python_folder_dir = python_base / folder_python_name
|
|
613
|
+
python_folder_dir.mkdir(parents=True, exist_ok=True)
|
|
614
|
+
python_file = python_folder_dir / "agents.py"
|
|
615
|
+
|
|
616
|
+
# Generate Python code for this folder
|
|
617
|
+
python_code = generate_python_code([(folder, spec) for spec in folder_specs])
|
|
618
|
+
with open(python_file, "w") as f:
|
|
619
|
+
f.write(python_code)
|
|
620
|
+
|
|
621
|
+
# Create __init__.py for Python subfolder
|
|
622
|
+
python_init = python_folder_dir / "__init__.py"
|
|
623
|
+
with open(python_init, "w") as f:
|
|
624
|
+
f.write(f"""# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
625
|
+
# Distributed under the terms of the Modified BSD License.
|
|
626
|
+
|
|
627
|
+
from .agents import *
|
|
628
|
+
|
|
629
|
+
__all__ = ["AGENT_SPECS", "get_agent_spec", "list_agent_specs"]
|
|
630
|
+
""")
|
|
631
|
+
|
|
632
|
+
# Collect imports for main index
|
|
633
|
+
all_python_imports.append(
|
|
634
|
+
f"from .{folder_python_name} import AGENT_SPECS as {folder_python_name.upper()}_AGENTS"
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
# Create TypeScript subfolder file
|
|
638
|
+
typescript_folder_dir = typescript_base / folder
|
|
639
|
+
typescript_folder_dir.mkdir(parents=True, exist_ok=True)
|
|
640
|
+
typescript_file = typescript_folder_dir / "agents.ts"
|
|
641
|
+
|
|
642
|
+
# Generate TypeScript code for this folder
|
|
643
|
+
typescript_code = generate_typescript_code(
|
|
644
|
+
[(folder, spec) for spec in folder_specs],
|
|
645
|
+
str(mcp_specs_dir),
|
|
646
|
+
str(skills_specs_dir),
|
|
647
|
+
)
|
|
648
|
+
with open(typescript_file, "w") as f:
|
|
649
|
+
f.write(typescript_code)
|
|
650
|
+
|
|
651
|
+
# Create index.ts for TypeScript subfolder
|
|
652
|
+
typescript_index = typescript_folder_dir / "index.ts"
|
|
653
|
+
with open(typescript_index, "w") as f:
|
|
654
|
+
f.write(f"""/*
|
|
655
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
656
|
+
* Distributed under the terms of the Modified BSD License.
|
|
657
|
+
*/
|
|
658
|
+
|
|
659
|
+
export * from './agents';
|
|
660
|
+
""")
|
|
661
|
+
|
|
662
|
+
# Collect imports for main index
|
|
663
|
+
all_typescript_imports.append(f"export * from './{folder}';")
|
|
664
|
+
|
|
665
|
+
# Create main Python index file
|
|
666
|
+
python_index = python_base / "__init__.py"
|
|
667
|
+
python_index_content = """# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
668
|
+
# Distributed under the terms of the Modified BSD License.
|
|
669
|
+
|
|
670
|
+
\"\"\"
|
|
671
|
+
Agent Library - Subfolder Organization.
|
|
672
|
+
|
|
673
|
+
THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
|
|
674
|
+
\"\"\"
|
|
675
|
+
|
|
676
|
+
from typing import Dict
|
|
677
|
+
from agent_runtimes.types import AgentSpec
|
|
678
|
+
|
|
679
|
+
"""
|
|
680
|
+
|
|
681
|
+
# Add imports
|
|
682
|
+
for imp in all_python_imports:
|
|
683
|
+
python_index_content += f"{imp}\n"
|
|
684
|
+
|
|
685
|
+
# Merge all agent specs
|
|
686
|
+
python_index_content += """
|
|
687
|
+
# Merge all agent specs from subfolders
|
|
688
|
+
AGENT_SPECS: Dict[str, AgentSpec] = {}
|
|
689
|
+
"""
|
|
690
|
+
|
|
691
|
+
for folder in sorted(specs_by_folder.keys()):
|
|
692
|
+
if folder:
|
|
693
|
+
folder_python_name = folder.replace("-", "_")
|
|
694
|
+
python_index_content += (
|
|
695
|
+
f"AGENT_SPECS.update({folder_python_name.upper()}_AGENTS)\n"
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
python_index_content += """
|
|
699
|
+
|
|
700
|
+
def get_agent_spec(agent_id: str) -> AgentSpec | None:
|
|
701
|
+
\"\"\"Get an agent specification by ID.\"\"\"
|
|
702
|
+
return AGENT_SPECS.get(agent_id)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def list_agent_specs() -> list[AgentSpec]:
|
|
706
|
+
\"\"\"List all available agent specifications.\"\"\"
|
|
707
|
+
return list(AGENT_SPECS.values())
|
|
708
|
+
|
|
709
|
+
__all__ = ["AGENT_SPECS", "get_agent_spec", "list_agent_specs"]
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
with open(python_index, "w") as f:
|
|
713
|
+
f.write(python_index_content)
|
|
714
|
+
|
|
715
|
+
# Create main TypeScript index file
|
|
716
|
+
typescript_index = typescript_base / "index.ts"
|
|
717
|
+
typescript_index_content = """/*
|
|
718
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
719
|
+
* Distributed under the terms of the Modified BSD License.
|
|
720
|
+
*/
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Agent Library - Subfolder Organization.
|
|
724
|
+
*
|
|
725
|
+
* THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
|
|
726
|
+
*/
|
|
727
|
+
|
|
728
|
+
import type { AgentSpec } from '../../types';
|
|
729
|
+
|
|
730
|
+
"""
|
|
731
|
+
|
|
732
|
+
# Import AGENT_SPECS from each subfolder
|
|
733
|
+
for folder in sorted(specs_by_folder.keys()):
|
|
734
|
+
if folder:
|
|
735
|
+
folder_const = folder.replace("-", "_").upper()
|
|
736
|
+
typescript_index_content += f"import {{ AGENT_SPECS as {folder_const}_AGENTS }} from './{folder}';\n"
|
|
737
|
+
|
|
738
|
+
typescript_index_content += """
|
|
739
|
+
// Merge all agent specs from subfolders
|
|
740
|
+
export const AGENT_SPECS: Record<string, AgentSpec> = {
|
|
741
|
+
"""
|
|
742
|
+
|
|
743
|
+
for folder in sorted(specs_by_folder.keys()):
|
|
744
|
+
if folder:
|
|
745
|
+
folder_const = folder.replace("-", "_").upper()
|
|
746
|
+
typescript_index_content += f" ...{folder_const}_AGENTS,\n"
|
|
747
|
+
|
|
748
|
+
typescript_index_content += """};
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Get an agent specification by ID.
|
|
752
|
+
*/
|
|
753
|
+
export function getAgentSpecs(agentId: string): AgentSpec | undefined {
|
|
754
|
+
return AGENT_SPECS[agentId];
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* List all available agent specifications.
|
|
759
|
+
*/
|
|
760
|
+
export function listAgentSpecs(): AgentSpec[] {
|
|
761
|
+
return Object.values(AGENT_SPECS);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Collect all required environment variables for an agent spec.
|
|
766
|
+
*/
|
|
767
|
+
export function getAgentSpecRequiredEnvVars(spec: AgentSpec): string[] {
|
|
768
|
+
const vars = new Set<string>();
|
|
769
|
+
for (const server of spec.mcpServers) {
|
|
770
|
+
for (const v of server.requiredEnvVars ?? []) {
|
|
771
|
+
vars.add(v);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
for (const skill of spec.skills) {
|
|
775
|
+
for (const v of skill.requiredEnvVars ?? []) {
|
|
776
|
+
vars.add(v);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return Array.from(vars);
|
|
780
|
+
}
|
|
781
|
+
"""
|
|
782
|
+
|
|
783
|
+
with open(typescript_index, "w") as f:
|
|
784
|
+
f.write(typescript_index_content)
|
|
785
|
+
|
|
786
|
+
print(f"✓ Generated {len(specs_by_folder)} subfolder(s)")
|
|
787
|
+
|
|
788
|
+
|
|
393
789
|
def main():
|
|
394
790
|
"""Main entry point."""
|
|
395
791
|
parser = argparse.ArgumentParser(
|
|
@@ -405,13 +801,18 @@ def main():
|
|
|
405
801
|
"--python-output",
|
|
406
802
|
type=Path,
|
|
407
803
|
default=Path("agent_runtimes/config/agents.py"),
|
|
408
|
-
help="Output path for generated Python code",
|
|
804
|
+
help="Output path for generated Python code (if using --subfolder-structure, this will be the parent directory)",
|
|
409
805
|
)
|
|
410
806
|
parser.add_argument(
|
|
411
807
|
"--typescript-output",
|
|
412
808
|
type=Path,
|
|
413
809
|
default=Path("src/config/agents.ts"),
|
|
414
|
-
help="Output path for generated TypeScript code",
|
|
810
|
+
help="Output path for generated TypeScript code (if using --subfolder-structure, this will be the parent directory)",
|
|
811
|
+
)
|
|
812
|
+
parser.add_argument(
|
|
813
|
+
"--subfolder-structure",
|
|
814
|
+
action="store_true",
|
|
815
|
+
help="Generate separate files per subfolder instead of one combined file",
|
|
415
816
|
)
|
|
416
817
|
|
|
417
818
|
args = parser.parse_args()
|
|
@@ -426,24 +827,34 @@ def main():
|
|
|
426
827
|
specs = load_yaml_specs(args.specs_dir)
|
|
427
828
|
print(f"Loaded {len(specs)} agent specification(s)")
|
|
428
829
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
f.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
specs
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
830
|
+
if args.subfolder_structure:
|
|
831
|
+
# Generate separate files per subfolder
|
|
832
|
+
generate_subfolder_structure(specs, args)
|
|
833
|
+
else:
|
|
834
|
+
# Generate Python code (single file)
|
|
835
|
+
print(f"Generating Python code to {args.python_output}...")
|
|
836
|
+
python_code = generate_python_code(specs)
|
|
837
|
+
args.python_output.parent.mkdir(parents=True, exist_ok=True)
|
|
838
|
+
with open(args.python_output, "w") as f:
|
|
839
|
+
f.write(python_code)
|
|
840
|
+
|
|
841
|
+
# Generate TypeScript code (single file)
|
|
842
|
+
print(f"Generating TypeScript code to {args.typescript_output}...")
|
|
843
|
+
# Get MCP and skills specs directories (siblings to agents directory)
|
|
844
|
+
mcp_specs_dir = args.specs_dir.parent / "mcp-servers"
|
|
845
|
+
skills_specs_dir = args.specs_dir.parent / "skills"
|
|
846
|
+
typescript_code = generate_typescript_code(
|
|
847
|
+
specs, str(mcp_specs_dir), str(skills_specs_dir)
|
|
848
|
+
)
|
|
849
|
+
args.typescript_output.parent.mkdir(parents=True, exist_ok=True)
|
|
850
|
+
with open(args.typescript_output, "w") as f:
|
|
851
|
+
f.write(typescript_code)
|
|
852
|
+
|
|
853
|
+
# Update __init__.py with new agent spec constants
|
|
854
|
+
init_file_path = args.python_output.parent / "__init__.py"
|
|
855
|
+
if init_file_path.exists():
|
|
856
|
+
print(f"Updating {init_file_path}...")
|
|
857
|
+
update_init_file(specs, init_file_path)
|
|
447
858
|
|
|
448
859
|
print("✅ Code generation complete!")
|
|
449
860
|
|