@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.
Files changed (36) hide show
  1. package/lib/components/AgentConfiguration.d.ts +30 -0
  2. package/lib/components/AgentConfiguration.js +71 -16
  3. package/lib/components/chat/components/AgentDetails.js +159 -1
  4. package/lib/components/chat/components/ContextDistribution.js +2 -2
  5. package/lib/components/chat/components/ContextInspector.js +4 -2
  6. package/lib/components/chat/components/ContextPanel.js +1 -6
  7. package/lib/components/index.d.ts +2 -2
  8. package/lib/components/index.js +1 -1
  9. package/lib/config/agents/code-ai/agents.d.ts +25 -0
  10. package/lib/config/agents/code-ai/agents.js +70 -0
  11. package/lib/config/agents/code-ai/index.d.ts +1 -0
  12. package/lib/config/agents/code-ai/index.js +5 -0
  13. package/lib/config/{agents.d.ts → agents/codemode-paper/agents.d.ts} +1 -5
  14. package/lib/config/{agents.js → agents/codemode-paper/agents.js} +29 -165
  15. package/lib/config/agents/codemode-paper/index.d.ts +1 -0
  16. package/lib/config/agents/codemode-paper/index.js +5 -0
  17. package/lib/config/agents/datalayer-ai/agents.d.ts +29 -0
  18. package/lib/config/agents/datalayer-ai/agents.js +267 -0
  19. package/lib/config/agents/datalayer-ai/index.d.ts +1 -0
  20. package/lib/config/agents/datalayer-ai/index.js +5 -0
  21. package/lib/config/agents/index.d.ts +19 -0
  22. package/lib/config/agents/index.js +38 -0
  23. package/lib/config/envvars.d.ts +28 -0
  24. package/lib/config/envvars.js +115 -0
  25. package/lib/config/index.d.ts +1 -0
  26. package/lib/config/index.js +1 -0
  27. package/lib/config/mcpServers.js +26 -2
  28. package/lib/config/skills.d.ts +2 -0
  29. package/lib/config/skills.js +6 -0
  30. package/lib/examples/AgentSpaceFormExample.js +51 -9
  31. package/lib/types.d.ts +10 -2
  32. package/package.json +2 -2
  33. package/scripts/codegen/generate_agents.py +565 -154
  34. package/scripts/codegen/generate_envvars.py +302 -0
  35. package/scripts/codegen/generate_mcp_servers.py +28 -16
  36. 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
- """Load all YAML agent specifications from directory."""
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
- # Generate agent spec constants
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
- # Get MCP servers
75
- mcp_server_ids = spec.get("mcp_servers", [])
76
- mcp_servers_str = ", ".join(
77
- f'MCP_SERVER_CATALOG["{sid}"]' for sid in mcp_server_ids
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
- # Format optional fields
81
- icon = f'"{spec.get("icon")}"' if spec.get("icon") else "None"
82
- color = f'"{spec.get("color")}"' if spec.get("color") else "None"
83
- suggestions = spec.get("suggestions", [])
84
- suggestions_str = (
85
- "[\n "
86
- + ",\n ".join(f'"{s}"' for s in suggestions)
87
- + ",\n ]"
88
- if suggestions
89
- else "[]"
90
- )
91
- # Escape multi-line strings properly
92
- welcome = spec.get("welcome_message", "").replace('"', '\\"').replace("\n", " ")
93
- welcome_notebook = spec.get("welcome_notebook")
94
- welcome_document = spec.get("welcome_document")
95
- system_prompt = spec.get("system_prompt", "")
96
- system_prompt_codemode = spec.get("system_prompt_codemode", "")
97
-
98
- # Escape triple quotes in system prompts for Python triple-quoted strings
99
- if system_prompt:
100
- system_prompt = system_prompt.replace('"""', r"\"\"\"")
101
- if system_prompt_codemode:
102
- system_prompt_codemode = system_prompt_codemode.replace('"""', r"\"\"\"")
103
-
104
- # Clean description for Python (single line)
105
- description = spec["description"].replace("\n", " ").replace(" ", " ").strip()
106
-
107
- # Use triple quotes for multiline system prompts
108
- system_prompt_str = f'"""{system_prompt}"""' if system_prompt else "None"
109
- system_prompt_codemode_str = (
110
- f'"""{system_prompt_codemode}"""' if system_prompt_codemode else "None"
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
- code += f'''{const_name} = AgentSpec(
114
- id="{spec["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
- system_prompt_codemode={system_prompt_codemode_str},
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
- for agent_id, const_name in agent_ids:
143
- code += f' "{agent_id}": {const_name},\n'
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
- # Generate import names and map entries dynamically
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
- const_name = server_id.upper().replace("-", "_") + "_MCP_SERVER"
198
- mcp_imports.append(const_name)
199
- mcp_map_entries.append(f" '{server_id}': {const_name},")
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
- const_name = sid.upper().replace("-", "_") + "_SKILL_SPEC"
206
- skill_imports.append(const_name)
207
- skill_map_entries.append(f" '{sid}': {const_name},")
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 '../types';
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
- code += "\n".join(mcp_map_entries) + "\n"
241
- code += """};
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
- code += "\n".join(skill_map_entries) + "\n"
249
- code += """};
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
- # Generate agent spec constants
270
- agent_ids = []
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
- # Get MCP servers
282
- mcp_server_ids = spec.get("mcp_servers", [])
283
- mcp_servers_str = ", ".join(
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
- # Get skills - resolve to AgentSkillSpec via toAgentSkillSpec
288
- skill_ids_list = spec.get("skills", [])
289
- if skill_ids_list:
290
- skills_str = ", ".join(
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
- # Format optional fields
310
- icon = f"'{spec.get('icon')}'" if spec.get("icon") else "undefined"
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
- # Escape backticks for TypeScript template literals
316
- if system_prompt:
317
- system_prompt = system_prompt.replace("`", "\\`")
318
- if system_prompt_codemode:
319
- system_prompt_codemode = system_prompt_codemode.replace("`", "\\`")
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
- # Clean description for TypeScript (multi-line template literal)
322
- description = spec["description"].replace("\n", " ").replace(" ", " ").strip()
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
- code += f"""export const {const_name}: AgentSpec = {{
325
- id: '{spec["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
- systemPromptCodemode: {f"`{system_prompt_codemode}`" if system_prompt_codemode else "undefined"},
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
- for agent_id, const_name in agent_ids:
350
- code += f" '{agent_id}': {const_name},\n"
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
- # Generate Python code
430
- print(f"Generating Python code to {args.python_output}...")
431
- python_code = generate_python_code(specs)
432
- args.python_output.parent.mkdir(parents=True, exist_ok=True)
433
- with open(args.python_output, "w") as f:
434
- f.write(python_code)
435
-
436
- # Generate TypeScript code
437
- print(f"Generating TypeScript code to {args.typescript_output}...")
438
- # Get MCP and skills specs directories (siblings to agents directory)
439
- mcp_specs_dir = args.specs_dir.parent / "mcp-servers"
440
- skills_specs_dir = args.specs_dir.parent / "skills"
441
- typescript_code = generate_typescript_code(
442
- specs, str(mcp_specs_dir), str(skills_specs_dir)
443
- )
444
- args.typescript_output.parent.mkdir(parents=True, exist_ok=True)
445
- with open(args.typescript_output, "w") as f:
446
- f.write(typescript_code)
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