R-Kentaren commited on
Commit
fc74cc0
·
verified ·
1 Parent(s): 81aa0b5

feat(agent): add Claude Code-style agent, skills, slash-commands, hooks, todos, sandboxed workspace, and full-stack scaffolding

Browse files

Adds Claude Code-inspired agent capabilities:
- Agent loop with tools (read/write/edit/glob/grep/bash/todos)
- Skills system (markdown skill files, runtime loading)
- Slash commands (/commit /review /feature /design /explain /test /refactor /skill /help)
- Hooks system (block-dangerous-rm, warn-debug-code, warn-secrets-in-code, warn-eval-exec)
- Sandboxed workspace with path-escape protection
- Todo lists (todo_write / todo_read / todo_update)
- Full-stack scaffolding skill
- No external SDK dependencies; no Next.js conversion

CLAUDE.md CHANGED
@@ -22,7 +22,7 @@ code/
22
  ├── model/
23
  │ ├── loader.py ← Dual model loading (text + VLM)
24
  │ └── inference.py ← Streaming inference (text + VLM)
25
- ├── agent/__init__.py ← Agent loop (model ↔ tools)
26
  ├── tools/
27
  │ ├── fs.py ← read_file, write_file, edit_file, glob, grep, list_dir
28
  │ ├── bash.py ← Sandboxed shell execution
@@ -30,9 +30,12 @@ code/
30
  ├── skills/
31
  │ ├── __init__.py ← Skill discovery + loading
32
  │ └── builtins/ ← Built-in skills (markdown)
 
 
 
33
  ├── commands/
34
  │ ├── __init__.py ← Slash command parser + expander
35
- │ └── builtins/ ← Built-in commands (markdown)
36
  ├── hooks/
37
  │ ├── __init__.py ← Hook rule engine
38
  │ └── builtins/ ← Built-in hook rules (markdown)
@@ -106,8 +109,74 @@ content: |
106
  | `/test <target>` | Generate tests |
107
  | `/refactor <target>` | Refactor code for clarity |
108
  | `/skill <name>` | Load and apply a skill |
 
 
 
 
 
 
109
  | `/help` | Show available commands and skills |
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  ## Skills
112
 
113
  | Skill | Description |
 
22
  ├── model/
23
  │ ├── loader.py ← Dual model loading (text + VLM)
24
  │ └── inference.py ← Streaming inference (text + VLM)
25
+ ├── agent/__init__.py ← Agent loop (model ↔ tools, supports custom agents)
26
  ├── tools/
27
  │ ├── fs.py ← read_file, write_file, edit_file, glob, grep, list_dir
28
  │ ├── bash.py ← Sandboxed shell execution
 
30
  ├── skills/
31
  │ ├── __init__.py ← Skill discovery + loading
32
  │ └── builtins/ ← Built-in skills (markdown)
33
+ ├── agents/ ← Custom Agent system (AI-generated personas)
34
+ │ ├── __init__.py ← Agent CRUD + system-prompt builder
35
+ │ └── builtins/ ← Built-in agents (code-reviewer, test-writer)
36
  ├── commands/
37
  │ ├── __init__.py ← Slash command parser + expander
38
+ │ └── builtins/ ← Built-in commands (markdown, includes /agent)
39
  ├── hooks/
40
  │ ├── __init__.py ← Hook rule engine
41
  │ └── builtins/ ← Built-in hook rules (markdown)
 
109
  | `/test <target>` | Generate tests |
110
  | `/refactor <target>` | Refactor code for clarity |
111
  | `/skill <name>` | Load and apply a skill |
112
+ | `/agent create <desc>` | AI generates a custom agent from natural-language description |
113
+ | `/agent use <name>` | Activate a saved agent |
114
+ | `/agent list` | List all saved agents |
115
+ | `/agent show <name>` | Show an agent's full definition |
116
+ | `/agent delete <name>` | Delete a user-defined agent |
117
+ | `/agent reset` | Reset to default SoniCoder persona |
118
  | `/help` | Show available commands and skills |
119
 
120
+ ## Custom Agents
121
+
122
+ Custom agents are AI-generated personas layered on top of the base SoniCoder
123
+ system prompt. They can restrict the tool whitelist, auto-load skills, and
124
+ override temperature / max-iterations.
125
+
126
+ ### Agent file format
127
+
128
+ Agents live in `workspace/.sonicoder/agents/<name>/AGENT.md` (built-ins in
129
+ `code/agents/builtins/<name>/AGENT.md`). Format:
130
+
131
+ ```markdown
132
+ ---
133
+ name: my-agent
134
+ description: One-line description
135
+ tools: read_file, list_dir, grep, bash
136
+ skills: code-review
137
+ temperature: 0.2
138
+ max_iterations: 12
139
+ tags: review, quality
140
+ author: AI-generated
141
+ created: 2026-06-20
142
+ ---
143
+
144
+ # My Agent
145
+
146
+ Full system-prompt extension here. Define the persona, workflow, output format,
147
+ and any hard rules.
148
+ ```
149
+
150
+ ### How `/agent create` works
151
+
152
+ 1. User runs `/agent create a security reviewer that flags hardcoded secrets`.
153
+ 2. The slash-command expansion substitutes `AGENT_GENERATION_PROMPT` (defined
154
+ in `code/agents/__init__.py`) into the prompt.
155
+ 3. The base SoniCoder model (NOT a custom agent) authors an `AGENT.md` file
156
+ via `write_file` and saves it under `.sonicoder/agents/<name>/`.
157
+ 4. The user can then `/agent use <name>` or click the agent in the UI.
158
+
159
+ ### Built-in agents
160
+
161
+ | Agent | Description |
162
+ |-------|-------------|
163
+ | `code-reviewer` | Read-only reviewer, structured issues table output |
164
+ | `test-writer` | Generates pytest/jest tests, runs them, iterates until green |
165
+
166
+ ### API endpoints
167
+
168
+ | Endpoint | Description |
169
+ |----------|-------------|
170
+ | `list_agents` | List all agents (builtins + user) and the active one |
171
+ | `get_agent(name)` | Get a single agent's full definition |
172
+ | `save_agent(...)` | Create or overwrite a user agent (manual save) |
173
+ | `delete_agent(name)` | Delete a user agent (built-ins protected) |
174
+ | `set_active_agent(name)` | Set/clear the active agent for subsequent prompts |
175
+
176
+ The `agent_run` endpoint also intercepts `/agent use|reset|delete|list` and
177
+ dispatches them directly to the agents module, bypassing the model entirely
178
+ for instant session-state updates.
179
+
180
  ## Skills
181
 
182
  | Skill | Description |
README.md CHANGED
@@ -23,7 +23,8 @@ Inspired by [Claude Code](https://github.com/anthropics/claude-code), SoniCoder
23
 
24
  - 🤖 **Agent Loop** — model calls tools (read/write/edit/glob/grep/bash/todos) in a feedback loop
25
  - 🎯 **Skills System** — load markdown skill files at runtime (frontend-design, feature-dev, code-review, debugging, fullstack-scaffold, commit-workflow)
26
- - ⚡ **Slash Commands** — `/commit`, `/review`, `/feature`, `/design`, `/explain`, `/test`, `/refactor`, `/skill`, `/help`
 
27
  - 🪝 **Hooks System** — pre/post tool execution rules (block dangerous commands, warn on debug code/secrets)
28
  - 📁 **Sandboxed Workspace** — agent manipulates files in `./workspace/` (path-escape protected)
29
  - ✅ **Todo Lists** — track multi-step tasks Claude Code-style
@@ -86,8 +87,35 @@ The agent can call these tools (Claude Code-style):
86
  | `/test [target]` | Generate tests |
87
  | `/refactor <target>` | Refactor code for clarity |
88
  | `/skill <name>` | Load and apply a skill |
 
 
 
 
 
 
89
  | `/help` | Show available commands and skills |
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  ### Built-in Skills
92
 
93
  - **frontend-design** — distinctive visual design guidance (palette, typography, signature)
 
23
 
24
  - 🤖 **Agent Loop** — model calls tools (read/write/edit/glob/grep/bash/todos) in a feedback loop
25
  - 🎯 **Skills System** — load markdown skill files at runtime (frontend-design, feature-dev, code-review, debugging, fullstack-scaffold, commit-workflow)
26
+ - ⚡ **Slash Commands** — `/commit`, `/review`, `/feature`, `/design`, `/explain`, `/test`, `/refactor`, `/skill`, `/agent`, `/help`
27
+ - 🧠 **Custom Agents** — describe a specialized agent in natural language and the AI generates a full persona (system prompt + tool whitelist + auto-loaded skills + temperature + max iterations). Activate via `/agent use <name>` or the Agents panel. Built-ins: `code-reviewer`, `test-writer`.
28
  - 🪝 **Hooks System** — pre/post tool execution rules (block dangerous commands, warn on debug code/secrets)
29
  - 📁 **Sandboxed Workspace** — agent manipulates files in `./workspace/` (path-escape protected)
30
  - ✅ **Todo Lists** — track multi-step tasks Claude Code-style
 
87
  | `/test [target]` | Generate tests |
88
  | `/refactor <target>` | Refactor code for clarity |
89
  | `/skill <name>` | Load and apply a skill |
90
+ | `/agent create <desc>` | **AI generates a custom agent from a natural-language description** |
91
+ | `/agent use <name>` | Activate a saved agent for subsequent prompts |
92
+ | `/agent list` | List all saved agents |
93
+ | `/agent show <name>` | Display an agent's full definition |
94
+ | `/agent delete <name>` | Delete a user-defined agent |
95
+ | `/agent reset` | Reset to default SoniCoder persona |
96
  | `/help` | Show available commands and skills |
97
 
98
+ ### Custom Agents
99
+
100
+ Custom agents are AI-generated personas that layer on top of the base SoniCoder system prompt. Each agent defines:
101
+
102
+ - A **system-prompt extension** (persona, workflow, output format)
103
+ - A **tool whitelist** (e.g. a read-only reviewer gets `read_file`, `grep`, `bash` but not `write_file`)
104
+ - **Auto-loaded skills** (e.g. `code-review` for a reviewer agent)
105
+ - A **temperature** and **max_iterations** override
106
+
107
+ **Two ways to create one:**
108
+
109
+ 1. **AI-generated** (recommended): Type `/agent create <natural-language description>` in chat, or use the "AI-Generate a Custom Agent" box in the Agent tab. The model authors an `AGENT.md` file for you.
110
+ 2. **Manual**: Click "Write manually" in the Agents panel and fill in the fields directly.
111
+
112
+ **Built-in agents:**
113
+
114
+ - `code-reviewer` — read-only reviewer that produces a structured issues table
115
+ - `test-writer` — generates pytest/jest tests, runs them, and iterates until green
116
+
117
+ Agent files are saved to `workspace/.sonicoder/agents/<name>/AGENT.md`. Built-ins live in `code/agents/builtins/` and cannot be deleted.
118
+
119
  ### Built-in Skills
120
 
121
  - **frontend-design** — distinctive visual design guidance (palette, typography, signature)
code/agent/__init__.py CHANGED
@@ -388,8 +388,14 @@ def build_agent_system_prompt(
388
  target_language: str = "",
389
  target_framework: str = "",
390
  skills: list[str] | None = None,
 
391
  ) -> str:
392
- """Build the system prompt with tool descriptions and skill context."""
 
 
 
 
 
393
  parts = [
394
  SYSTEM_PROMPT,
395
  "",
@@ -409,7 +415,27 @@ def build_agent_system_prompt(
409
  parts.append("")
410
  parts.append(f"Target: {target_language}" + (f" / {target_framework}" if target_framework else ""))
411
 
412
- skills_ctx = build_skills_context(skills)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  if skills_ctx:
414
  parts.append("")
415
  parts.append("## Skills Loaded")
@@ -427,6 +453,7 @@ def run_agent(
427
  skills: list[str] | None = None,
428
  search_context: str = "",
429
  image_url: str | None = None,
 
430
  ) -> Iterator[dict[str, Any]]:
431
  """Run the agent loop. Yields events as dict.
432
 
@@ -481,8 +508,57 @@ def run_agent(
481
  }
482
  return
483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  # Build system prompt
485
- system_prompt = build_agent_system_prompt(target_language, target_framework, skills)
486
 
487
  # Add search context if present
488
  if search_context:
@@ -498,7 +574,7 @@ def run_agent(
498
  messages.append({"role": "user", "content": user_input})
499
 
500
  # Agent loop
501
- for iteration in range(MAX_ITERATIONS):
502
  yield {
503
  "type": "status",
504
  "status_text": f"Thinking... (step {iteration + 1})",
@@ -537,6 +613,34 @@ def run_agent(
537
  tool_name = tc.get("tool")
538
  args = tc.get("args", {})
539
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  if "error" in tc:
541
  # Unknown tool
542
  tool_result = {"success": False, "error": tc["error"]}
@@ -568,5 +672,5 @@ def run_agent(
568
  yield {
569
  "type": "complete",
570
  "content": full_response + "\n\n_(Max iterations reached)_",
571
- "iterations": MAX_ITERATIONS,
572
  }
 
388
  target_language: str = "",
389
  target_framework: str = "",
390
  skills: list[str] | None = None,
391
+ agent_name: str | None = None,
392
  ) -> str:
393
+ """Build the system prompt with tool descriptions and skill context.
394
+
395
+ If `agent_name` is provided and matches a saved custom agent, that
396
+ agent's persona/system-prompt extension is appended, and any skills
397
+ declared on the agent are auto-loaded.
398
+ """
399
  parts = [
400
  SYSTEM_PROMPT,
401
  "",
 
415
  parts.append("")
416
  parts.append(f"Target: {target_language}" + (f" / {target_framework}" if target_framework else ""))
417
 
418
+ # ── Custom agent persona ───────────────────────────────────────────
419
+ merged_skills = list(skills or [])
420
+ if agent_name:
421
+ try:
422
+ from code.agents import get_agent, build_agent_system_prompt_extension, ALL_TOOLS
423
+ agent_cfg = get_agent(agent_name)
424
+ if agent_cfg:
425
+ ext = build_agent_system_prompt_extension(agent_name)
426
+ if ext:
427
+ parts.append("")
428
+ parts.append(ext)
429
+ # Auto-merge agent-declared skills (after user-selected ones)
430
+ for s in agent_cfg.get("skills", []):
431
+ if s not in merged_skills:
432
+ merged_skills.append(s)
433
+ else:
434
+ logger.warning("Agent '%s' not found; running default", agent_name)
435
+ except Exception as exc:
436
+ logger.warning("Failed to load custom agent '%s': %s", agent_name, exc)
437
+
438
+ skills_ctx = build_skills_context(merged_skills or None)
439
  if skills_ctx:
440
  parts.append("")
441
  parts.append("## Skills Loaded")
 
453
  skills: list[str] | None = None,
454
  search_context: str = "",
455
  image_url: str | None = None,
456
+ agent_name: str | None = None,
457
  ) -> Iterator[dict[str, Any]]:
458
  """Run the agent loop. Yields events as dict.
459
 
 
508
  }
509
  return
510
 
511
+ # ── Resolve active agent (explicit > session-active > none) ──────
512
+ if not agent_name:
513
+ try:
514
+ from code.agents import get_active_agent
515
+ agent_name = get_active_agent()
516
+ except Exception:
517
+ agent_name = None
518
+
519
+ # ── Special-case: /agent create → AI generates an agent definition ─
520
+ # The slash-command expansion already substituted the AGENT_GENERATION_PROMPT
521
+ # into `user_input`, so we just need to make sure agent_name is NOT applied
522
+ # (we want the default SoniCoder persona to author the new agent).
523
+ creating_agent = False
524
+ if user_input.lstrip().startswith("You are creating a custom agent definition"):
525
+ creating_agent = True
526
+ agent_name = None # don't layer persona on top of meta-prompt
527
+
528
+ # ── Load agent config (for tool whitelist + max_iterations) ────────
529
+ agent_cfg = None
530
+ if agent_name and not creating_agent:
531
+ try:
532
+ from code.agents import get_agent
533
+ agent_cfg = get_agent(agent_name)
534
+ if not agent_cfg:
535
+ yield {
536
+ "type": "status",
537
+ "status_text": f"Agent '{agent_name}' not found; using default.",
538
+ "status_state": "warning",
539
+ }
540
+ agent_name = None
541
+ else:
542
+ yield {
543
+ "type": "status",
544
+ "status_text": f"Running as agent: {agent_cfg['name']}",
545
+ "status_state": "working",
546
+ "agent": agent_cfg["name"],
547
+ }
548
+ except Exception as exc:
549
+ logger.warning("Failed to load agent '%s': %s", agent_name, exc)
550
+ agent_name = None
551
+
552
+ # Determine iteration cap
553
+ iter_cap = MAX_ITERATIONS
554
+ if agent_cfg and agent_cfg.get("max_iterations"):
555
+ try:
556
+ iter_cap = max(1, min(40, int(agent_cfg["max_iterations"])))
557
+ except (ValueError, TypeError):
558
+ pass
559
+
560
  # Build system prompt
561
+ system_prompt = build_agent_system_prompt(target_language, target_framework, skills, agent_name=agent_name)
562
 
563
  # Add search context if present
564
  if search_context:
 
574
  messages.append({"role": "user", "content": user_input})
575
 
576
  # Agent loop
577
+ for iteration in range(iter_cap):
578
  yield {
579
  "type": "status",
580
  "status_text": f"Thinking... (step {iteration + 1})",
 
613
  tool_name = tc.get("tool")
614
  args = tc.get("args", {})
615
 
616
+ # ── Enforce agent tool whitelist ────────────────────────────
617
+ if agent_cfg and agent_cfg.get("tools"):
618
+ allowed = set(agent_cfg["tools"])
619
+ if tool_name not in allowed:
620
+ tool_result = {
621
+ "success": False,
622
+ "error": (
623
+ f"Tool '{tool_name}' is not in the active agent's "
624
+ f"whitelist: {sorted(allowed)}. The agent '{agent_cfg['name']}' "
625
+ "is configured to use only those tools."
626
+ ),
627
+ "blocked_by_agent_whitelist": True,
628
+ }
629
+ yield {
630
+ "type": "tool_result",
631
+ "tool": tool_name,
632
+ "result": tool_result,
633
+ "iteration": iteration + 1,
634
+ }
635
+ # Feed back to model so it can pick a different tool
636
+ result_str = json.dumps(tool_result, indent=2, default=str)
637
+ messages.append({"role": "assistant", "content": full_response})
638
+ messages.append({
639
+ "role": "user",
640
+ "content": f"Tool `{tool_name}` result:\n```json\n{result_str}\n```\n\nChoose a different tool from the agent's whitelist, or finish if done.",
641
+ })
642
+ continue
643
+
644
  if "error" in tc:
645
  # Unknown tool
646
  tool_result = {"success": False, "error": tc["error"]}
 
672
  yield {
673
  "type": "complete",
674
  "content": full_response + "\n\n_(Max iterations reached)_",
675
+ "iterations": iter_cap,
676
  }
code/agents/__init__.py ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Custom Agent system — AI-generated, user-saved agent personas.
2
+
3
+ Inspired by Claude Code's sub-agent / agent-customization patterns. A custom
4
+ agent is a markdown file (`AGENT.md`) with YAML frontmatter that defines:
5
+
6
+ - name : unique kebab-case identifier
7
+ - description : one-line summary
8
+ - tools : comma-separated subset of the agent tool registry
9
+ (default: all tools)
10
+ - skills : comma-separated skill names to auto-load
11
+ - temperature : float 0.0–1.0 (optional)
12
+ - max_iterations: int (optional, overrides default)
13
+ - tags : comma-separated tags
14
+ - author : "user" | "AI-generated"
15
+ - created : ISO date
16
+
17
+ The body of the file is a system-prompt extension that is appended to the
18
+ base SoniCoder system prompt when the agent is active.
19
+
20
+ Storage layout:
21
+ workspace/.sonicoder/agents/<agent-name>/AGENT.md
22
+
23
+ Built-in agents live in `code/agents/builtins/` (read-only).
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ import os
30
+ import re
31
+ from datetime import date
32
+ from typing import Any
33
+
34
+ from code.skills import _parse_frontmatter # reuse the same simple YAML parser
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # ─── Discovery roots ────────────────────────────────────────────────────
39
+
40
+ _BUILTIN_AGENTS_DIR = os.path.join(os.path.dirname(__file__), "builtins")
41
+ _USER_AGENTS_DIRNAME = ".sonicoder/agents" # relative to workspace root
42
+
43
+ # All tools available to the agent — kept in sync with code/agent.TOOL_REGISTRY
44
+ ALL_TOOLS: tuple[str, ...] = (
45
+ "read_file",
46
+ "write_file",
47
+ "edit_file",
48
+ "multi_edit",
49
+ "list_dir",
50
+ "glob",
51
+ "grep",
52
+ "bash",
53
+ "todo_read",
54
+ "todo_write",
55
+ "todo_update",
56
+ )
57
+
58
+ # Session state: the currently active agent (None = default SoniCoder)
59
+ _active_agent: str | None = None
60
+
61
+
62
+ # ─── Helpers ────────────────────────────────────────────────────────────
63
+
64
+ def _agent_dirs() -> list[str]:
65
+ """Return all directories to search for agents (builtins first)."""
66
+ dirs = [_BUILTIN_AGENTS_DIR]
67
+ try:
68
+ from code.tools.fs import get_workspace_root
69
+ user_dir = os.path.join(get_workspace_root(), _USER_AGENTS_DIRNAME)
70
+ if os.path.isdir(user_dir):
71
+ dirs.append(user_dir)
72
+ except Exception:
73
+ pass
74
+ return dirs
75
+
76
+
77
+ def _user_agents_dir() -> str:
78
+ """Return the user agents directory (creating it if missing)."""
79
+ from code.tools.fs import get_workspace_root
80
+ user_dir = os.path.join(get_workspace_root(), _USER_AGENTS_DIRNAME)
81
+ os.makedirs(user_dir, exist_ok=True)
82
+ return user_dir
83
+
84
+
85
+ def _safe_agent_name(name: str) -> str:
86
+ """Sanitize an agent name to kebab-case."""
87
+ name = name.strip().lower()
88
+ name = re.sub(r"[^a-z0-9-]+", "-", name)
89
+ name = re.sub(r"-+", "-", name).strip("-")
90
+ if not name:
91
+ raise ValueError("Agent name must contain at least one alphanumeric character")
92
+ if len(name) > 64:
93
+ name = name[:64].rstrip("-")
94
+ return name
95
+
96
+
97
+ def _load_agent(agent_dir: str) -> dict[str, Any] | None:
98
+ """Load a single agent definition from a directory containing AGENT.md."""
99
+ agent_md = os.path.join(agent_dir, "AGENT.md")
100
+ if not os.path.isfile(agent_md):
101
+ return None
102
+
103
+ try:
104
+ with open(agent_md, "r", encoding="utf-8") as f:
105
+ content = f.read()
106
+ except Exception as exc:
107
+ logger.warning("Failed to read %s: %s", agent_md, exc)
108
+ return None
109
+
110
+ meta, body = _parse_frontmatter(content)
111
+
112
+ # Parse comma-separated list fields
113
+ def _split_csv(v: str) -> list[str]:
114
+ return [s.strip() for s in (v or "").split(",") if s.strip()]
115
+
116
+ tools = _split_csv(meta.get("tools", ""))
117
+ if not tools:
118
+ tools = list(ALL_TOOLS)
119
+ # Filter to known tools (defensive)
120
+ tools = [t for t in tools if t in ALL_TOOLS]
121
+
122
+ skills = _split_csv(meta.get("skills", ""))
123
+ tags = _split_csv(meta.get("tags", ""))
124
+
125
+ try:
126
+ temperature = float(meta["temperature"]) if meta.get("temperature") else None
127
+ except (ValueError, TypeError):
128
+ temperature = None
129
+
130
+ try:
131
+ max_iterations = int(meta["max_iterations"]) if meta.get("max_iterations") else None
132
+ except (ValueError, TypeError):
133
+ max_iterations = None
134
+
135
+ return {
136
+ "name": meta.get("name") or os.path.basename(agent_dir),
137
+ "description": meta.get("description", ""),
138
+ "tools": tools,
139
+ "skills": skills,
140
+ "temperature": temperature,
141
+ "max_iterations": max_iterations,
142
+ "tags": tags,
143
+ "author": meta.get("author", "user"),
144
+ "created": meta.get("created", ""),
145
+ "body": body.strip(),
146
+ "path": agent_dir,
147
+ }
148
+
149
+
150
+ # ─── Public API ──────��──────────────────────────────────────────────────
151
+
152
+ def list_agents() -> list[dict[str, Any]]:
153
+ """List all available agents (builtins + user). Returns metadata only."""
154
+ agents: list[dict[str, Any]] = []
155
+ seen: set[str] = set()
156
+
157
+ for agents_dir in _agent_dirs():
158
+ if not os.path.isdir(agents_dir):
159
+ continue
160
+ for entry in sorted(os.listdir(agents_dir)):
161
+ entry_path = os.path.join(agents_dir, entry)
162
+ if not os.path.isdir(entry_path):
163
+ continue
164
+ agent = _load_agent(entry_path)
165
+ if agent and agent["name"] not in seen:
166
+ seen.add(agent["name"])
167
+ agents.append({
168
+ "name": agent["name"],
169
+ "description": agent["description"],
170
+ "tools": agent["tools"],
171
+ "skills": agent["skills"],
172
+ "tags": agent["tags"],
173
+ "author": agent["author"],
174
+ "active": agent["name"] == _active_agent,
175
+ })
176
+ return agents
177
+
178
+
179
+ def get_agent(name: str) -> dict[str, Any] | None:
180
+ """Get full agent definition by name (or None if not found)."""
181
+ name = _safe_agent_name(name)
182
+ for agents_dir in _agent_dirs():
183
+ if not os.path.isdir(agents_dir):
184
+ continue
185
+ for entry in os.listdir(agents_dir):
186
+ entry_path = os.path.join(agents_dir, entry)
187
+ if not os.path.isdir(entry_path):
188
+ continue
189
+ agent = _load_agent(entry_path)
190
+ if agent and agent["name"] == name:
191
+ return agent
192
+ return None
193
+
194
+
195
+ def agent_exists(name: str) -> bool:
196
+ return get_agent(name) is not None
197
+
198
+
199
+ def save_agent(
200
+ name: str,
201
+ description: str,
202
+ body: str,
203
+ tools: list[str] | None = None,
204
+ skills: list[str] | None = None,
205
+ temperature: float | None = None,
206
+ max_iterations: int | None = None,
207
+ tags: list[str] | None = None,
208
+ author: str = "user",
209
+ ) -> dict[str, Any]:
210
+ """Save (create or overwrite) an agent definition."""
211
+ name = _safe_agent_name(name)
212
+
213
+ # Validate tools
214
+ if tools:
215
+ invalid = [t for t in tools if t not in ALL_TOOLS]
216
+ if invalid:
217
+ return {"success": False, "error": f"Unknown tools: {invalid}", "valid_tools": list(ALL_TOOLS)}
218
+ tools_str = ", ".join(tools)
219
+ else:
220
+ tools_str = ", ".join(ALL_TOOLS)
221
+
222
+ skills_str = ", ".join(skills) if skills else ""
223
+ tags_str = ", ".join(tags) if tags else ""
224
+ temp_str = f"{temperature:.2f}" if temperature is not None else ""
225
+ iter_str = str(max_iterations) if max_iterations is not None else ""
226
+
227
+ # Build the markdown file
228
+ frontmatter_lines = [
229
+ "---",
230
+ f"name: {name}",
231
+ f"description: {description.strip()[:200]}",
232
+ f"tools: {tools_str}",
233
+ f"skills: {skills_str}",
234
+ ]
235
+ if temp_str:
236
+ frontmatter_lines.append(f"temperature: {temp_str}")
237
+ if iter_str:
238
+ frontmatter_lines.append(f"max_iterations: {iter_str}")
239
+ if tags_str:
240
+ frontmatter_lines.append(f"tags: {tags_str}")
241
+ frontmatter_lines.append(f"author: {author}")
242
+ frontmatter_lines.append(f"created: {date.today().isoformat()}")
243
+ frontmatter_lines.append("---")
244
+ frontmatter_lines.append("")
245
+ frontmatter_lines.append(body.strip())
246
+ frontmatter_lines.append("")
247
+
248
+ content = "\n".join(frontmatter_lines)
249
+
250
+ # Write to workspace/.sonicoder/agents/<name>/AGENT.md
251
+ agents_dir = _user_agents_dir()
252
+ agent_dir = os.path.join(agents_dir, name)
253
+ os.makedirs(agent_dir, exist_ok=True)
254
+ agent_md = os.path.join(agent_dir, "AGENT.md")
255
+ with open(agent_md, "w", encoding="utf-8") as f:
256
+ f.write(content)
257
+
258
+ logger.info("Saved agent '%s' to %s", name, agent_md)
259
+ return {"success": True, "name": name, "path": agent_md}
260
+
261
+
262
+ def delete_agent(name: str) -> dict[str, Any]:
263
+ """Delete a user-defined agent. Built-ins cannot be deleted."""
264
+ name = _safe_agent_name(name)
265
+ agent = get_agent(name)
266
+ if not agent:
267
+ return {"success": False, "error": f"Agent not found: {name}"}
268
+
269
+ # Only allow deleting files under the user agents dir
270
+ from code.tools.fs import get_workspace_root
271
+ user_dir = os.path.join(get_workspace_root(), _USER_AGENTS_DIRNAME)
272
+ if not agent["path"].startswith(user_dir + os.sep):
273
+ return {"success": False, "error": f"Cannot delete built-in agent: {name}"}
274
+
275
+ import shutil
276
+ try:
277
+ shutil.rmtree(os.path.dirname(agent["path"]), ignore_errors=True)
278
+ except Exception as exc:
279
+ return {"success": False, "error": str(exc)}
280
+
281
+ if _active_agent == name:
282
+ set_active_agent(None)
283
+
284
+ logger.info("Deleted agent '%s'", name)
285
+ return {"success": True, "name": name}
286
+
287
+
288
+ def set_active_agent(name: str | None) -> dict[str, Any]:
289
+ """Set the active agent for subsequent prompts. None resets to default."""
290
+ global _active_agent
291
+ if name is None or name.strip() == "":
292
+ _active_agent = None
293
+ return {"success": True, "active_agent": None, "message": "Reset to default SoniCoder agent"}
294
+ name = _safe_agent_name(name)
295
+ if not agent_exists(name):
296
+ return {"success": False, "error": f"Agent not found: {name}"}
297
+ _active_agent = name
298
+ return {"success": True, "active_agent": name}
299
+
300
+
301
+ def get_active_agent() -> str | None:
302
+ """Return the name of the currently active agent, or None."""
303
+ return _active_agent
304
+
305
+
306
+ def get_active_agent_config() -> dict[str, Any] | None:
307
+ """Return the full config of the active agent, or None if default."""
308
+ if _active_agent is None:
309
+ return None
310
+ return get_agent(_active_agent)
311
+
312
+
313
+ def build_agent_system_prompt_extension(agent_name: str) -> str:
314
+ """Build the system-prompt extension for a custom agent.
315
+
316
+ Returns empty string if the agent doesn't exist or has no body.
317
+ """
318
+ agent = get_agent(agent_name)
319
+ if not agent:
320
+ return ""
321
+ parts: list[str] = []
322
+ parts.append(f"# Active Custom Agent: {agent['name']}")
323
+ if agent["description"]:
324
+ parts.append(f"_{agent['description']}_")
325
+ parts.append("")
326
+ parts.append("You are operating under this custom agent persona. Follow its instructions and workflow.")
327
+ parts.append("")
328
+ parts.append("## Agent Persona & Instructions")
329
+ parts.append("")
330
+ parts.append(agent["body"])
331
+
332
+ if agent["tools"] and len(agent["tools"]) < len(ALL_TOOLS):
333
+ parts.append("")
334
+ parts.append("## Tool Whitelist for this Agent")
335
+ parts.append(
336
+ f"You may ONLY use these tools: {', '.join(agent['tools'])}. "
337
+ "Do not call any other tool."
338
+ )
339
+
340
+ if agent["skills"]:
341
+ parts.append("")
342
+ parts.append("## Auto-loaded Skills")
343
+ parts.append(f"The following skills are pre-loaded for this agent: {', '.join(agent['skills'])}")
344
+
345
+ return "\n".join(parts)
346
+
347
+
348
+ # ─── AI generation helper ───────────────────────────────────────────────
349
+
350
+ # The meta-prompt sent to the model when a user runs `/agent create <desc>`.
351
+ AGENT_GENERATION_PROMPT = """You are creating a custom agent definition for SoniCoder.
352
+
353
+ Based on the user's description, generate a complete AGENT.md file that defines a specialized agent persona.
354
+
355
+ ## Available Tools (pick a subset, or all)
356
+ read_file, write_file, edit_file, multi_edit, list_dir, glob, grep, bash, todo_read, todo_write, todo_update
357
+
358
+ ## Available Built-in Skills (pick zero or more)
359
+ frontend-design, feature-dev, code-review, debugging, fullstack-scaffold, commit-workflow
360
+
361
+ ## Output Format
362
+ Use the @@FILE: multi-file format to write exactly ONE file:
363
+
364
+ @@FILE: .sonicoder/agents/<kebab-case-name>/AGENT.md
365
+ ---
366
+ name: <kebab-case-name>
367
+ description: <one-line description, max 200 chars>
368
+ tools: <comma-separated subset of available tools>
369
+ skills: <comma-separated skill names, or empty>
370
+ temperature: <float 0.0-1.0 — lower for precise tasks, higher for creative>
371
+ max_iterations: <int 4-20>
372
+ tags: <comma-separated tags>
373
+ author: AI-generated
374
+ created: <today's date YYYY-MM-DD>
375
+ ---
376
+
377
+ # <Agent Name in Title Case>
378
+
379
+ <Full system prompt extension. Include:>
380
+ <- Persona description (who the agent is)>
381
+ <- Core responsibilities>
382
+ <- Workflow / step-by-step approach>
383
+ <- Output format expectations>
384
+ <- Critical rules and constraints>
385
+
386
+ @@END@@
387
+
388
+ ## Rules
389
+ - The agent name MUST be kebab-case (lowercase, hyphens only).
390
+ - Pick tools that match the agent's purpose — don't give a read-only reviewer `write_file`.
391
+ - The body should be 150-400 words, specific and actionable.
392
+ - Do NOT include any other files. Just the one AGENT.md.
393
+ - After writing the file, briefly tell the user they can activate the agent with `/agent use <name>`.
394
+
395
+ ## User's Description
396
+ $ARGUMENTS
397
+ """
code/agents/builtins/code-reviewer/AGENT.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: code-reviewer
3
+ description: Read-only agent that reviews code for bugs, style, and best practices. Never modifies files.
4
+ tools: read_file, list_dir, glob, grep, bash, todo_read, todo_write
5
+ skills: code-review
6
+ temperature: 0.2
7
+ max_iterations: 12
8
+ tags: review, quality, readonly
9
+ author: builtin
10
+ created: 2026-06-20
11
+ ---
12
+
13
+ # Code Reviewer
14
+
15
+ You are a meticulous code reviewer. Your job is to read the user's code and produce a structured review — you do **not** modify files.
16
+
17
+ ## Workflow
18
+
19
+ 1. Use `list_dir` to map the project structure.
20
+ 2. Use `todo_write` to plan the review (e.g., "Review entry point", "Review tests", "Review configs").
21
+ 3. For each file in scope, `read_file` and analyze.
22
+ 4. Use `grep` to find patterns (TODOs, FIXMEs, dangerous calls, missing error handling).
23
+ 5. Optionally run `bash` for static checks: `npm test`, `pytest --collect-only`, `ruff check .`, `eslint .`.
24
+ 6. Produce a final structured review.
25
+
26
+ ## Output Format
27
+
28
+ End your review with this exact structure:
29
+
30
+ ### Summary
31
+ One paragraph (3-5 sentences) describing the overall code quality.
32
+
33
+ ### Issues Found
34
+
35
+ | Severity | File:Line | Issue | Recommendation |
36
+ |----------|-----------|-------|----------------|
37
+ | High | src/app.py:42 | SQL injection via string concat | Use parameterized queries |
38
+ | Medium | src/utils.py:18 | Missing input validation | Add `isinstance(x, int)` guard |
39
+ | Low | src/main.py:5 | Unused import `os` | Remove |
40
+
41
+ ### Strengths
42
+ - Bullet list of what the code does well.
43
+
44
+ ### Next Steps
45
+ - 2-3 concrete actions the author should take.
46
+
47
+ ## Rules
48
+
49
+ - NEVER call `write_file`, `edit_file`, or `multi_edit` — you are read-only.
50
+ - Always cite file:line in issues.
51
+ - Be specific — "improve readability" is useless; "rename `x` to `user_count`" is useful.
52
+ - If a test file is missing, flag it as a High severity issue.
code/agents/builtins/test-writer/AGENT.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: test-writer
3
+ description: Generates test suites for existing code. Reads source, writes pytest/jest tests, runs them, and iterates until green.
4
+ tools: read_file, write_file, edit_file, list_dir, glob, grep, bash, todo_read, todo_write, todo_update
5
+ skills: debugging
6
+ temperature: 0.3
7
+ max_iterations: 15
8
+ tags: testing, quality, tdd
9
+ author: builtin
10
+ created: 2026-06-20
11
+ ---
12
+
13
+ # Test Writer
14
+
15
+ You are a test engineering specialist. Given a codebase, you write comprehensive tests, run them, and iterate until they pass.
16
+
17
+ ## Workflow
18
+
19
+ 1. `list_dir` and `read_file` the main source files to understand the public API.
20
+ 2. `todo_write` a test plan: one todo per module/function to test.
21
+ 3. Detect the test framework (`grep` for `pytest`, `jest`, `mocha`, `unittest`).
22
+ 4. Write test files using `write_file` (Python: `tests/test_<module>.py`; JS: `__tests__/<module>.test.js`).
23
+ 5. Run the tests with `bash` (`pytest -v` or `npm test`).
24
+ 6. If tests fail, `read_file` the failure, `edit_file` the test (or report a real bug in source).
25
+ 7. Repeat until green or max iterations.
26
+ 8. Report final coverage summary.
27
+
28
+ ## Test Writing Rules
29
+
30
+ - Test **behavior**, not implementation details.
31
+ - Use descriptive test names: `test_user_cannot_withdraw_more_than_balance` not `test_1`.
32
+ - Cover: happy path, edge cases (empty input, None, negative numbers), error paths.
33
+ - Use fixtures/setup; avoid duplicate boilerplate.
34
+ - For Python, prefer `pytest` with `@pytest.mark.parametrize`.
35
+ - For JS, prefer `jest` with `describe`/`it`/`expect`.
36
+ - Do NOT mock what you don't own without good reason.
37
+ - Aim for at least 3 tests per public function.
38
+
39
+ ## Output Format
40
+
41
+ End with:
42
+
43
+ ### Test Run Summary
44
+ - Tests passed: X / Y
45
+ - Coverage: N% (if measurable)
46
+ - Files created/modified:
47
+ - `tests/test_foo.py` (new, 8 tests)
48
+ - `tests/conftest.py` (new, shared fixtures)
49
+
50
+ ### Notes
51
+ - Any source-code bugs discovered while writing tests.
52
+ - Suggestions for improving testability.
code/commands/builtins/agent.md ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: agent
3
+ description: Create, use, list, show, delete, or reset custom AI agents
4
+ argument-hint: create <description> | use <name> | list | show <name> | delete <name> | reset
5
+ ---
6
+ # Custom Agent Command
7
+
8
+ The user invoked `/agent` with arguments: `$ARGUMENTS`
9
+
10
+ ## Parse the arguments and follow the matching branch
11
+
12
+ ### If arguments start with `create ` (i.e. `/agent create <description>`)
13
+ You are creating a custom agent definition based on the user's natural-language description. Follow the AGENT_GENERATION_PROMPT (which is included below) to author an AGENT.md file, then use `write_file` to save it under `.sonicoder/agents/<name>/AGENT.md` in the workspace.
14
+
15
+ After saving, briefly tell the user the agent's name and that they can activate it with `/agent use <name>` or by clicking it in the Agents panel of the Agent tab.
16
+
17
+ The description provided by the user is:
18
+ **$ARGUMENTS** (everything after `/agent create ` — strip the leading word "create")
19
+
20
+ Use the AGENT_GENERATION_PROMPT exactly. Write only the AGENT.md file — no other files.
21
+
22
+ ### If arguments start with `use ` (i.e. `/agent use <name>`)
23
+ Tell the user that the agent has been set as active for subsequent prompts. Mention the agent's name and description (if known). Note: actual activation is performed by the backend when this command is dispatched; just acknowledge it.
24
+
25
+ ### If arguments are `list`
26
+ List all available agents by reading the `.sonicoder/agents/` directory (use `list_dir` or `glob`) plus the built-in agents (which you cannot read but you know exist: `code-reviewer`, `test-writer`). Present a table:
27
+
28
+ | Name | Description | Author | Tools |
29
+ |------|-------------|--------|-------|
30
+
31
+ ### If arguments start with `show ` (i.e. `/agent show <name>`)
32
+ Read the file `.sonicoder/agents/<name>/AGENT.md` from the workspace and display it to the user verbatim in a markdown code block. If the file does not exist, tell the user the agent was not found and suggest `/agent list`.
33
+
34
+ ### If arguments start with `delete ` (i.e. `/agent delete <name>`)
35
+ Tell the user that the agent has been queued for deletion. Actual deletion is performed by the backend. Just acknowledge.
36
+
37
+ ### If arguments are `reset` (or empty)
38
+ Tell the user that the active agent has been reset to the default SoniCoder agent. Subsequent prompts will use the base persona.
39
+
40
+ ### Otherwise (unknown subcommand or no arguments)
41
+ Print a short help message:
42
+ ```
43
+ Usage:
44
+ /agent create <natural-language description> # AI generates a new agent
45
+ /agent use <name> # Activate a saved agent
46
+ /agent list # List all agents
47
+ /agent show <name> # Show an agent's full definition
48
+ /agent delete <name> # Delete a user agent
49
+ /agent reset # Reset to default SoniCoder
50
+ ```
51
+
52
+ ## AGENT_GENERATION_PROMPT (use this when subcommand is `create`)
53
+
54
+ You are creating a custom agent definition for SoniCoder.
55
+
56
+ Based on the user's description, generate a complete AGENT.md file that defines a specialized agent persona.
57
+
58
+ ### Available Tools (pick a subset, or all)
59
+ read_file, write_file, edit_file, multi_edit, list_dir, glob, grep, bash, todo_read, todo_write, todo_update
60
+
61
+ ### Available Built-in Skills (pick zero or more)
62
+ frontend-design, feature-dev, code-review, debugging, fullstack-scaffold, commit-workflow
63
+
64
+ ### Output Format
65
+ Use the `write_file` tool to save exactly ONE file with this content:
66
+
67
+ Path: `.sonicoder/agents/<kebab-case-name>/AGENT.md`
68
+
69
+ ```
70
+ ---
71
+ name: <kebab-case-name>
72
+ description: <one-line description, max 200 chars>
73
+ tools: <comma-separated subset of available tools>
74
+ skills: <comma-separated skill names, or empty>
75
+ temperature: <float 0.0-1.0 — lower for precise tasks, higher for creative>
76
+ max_iterations: <int 4-20>
77
+ tags: <comma-separated tags>
78
+ author: AI-generated
79
+ created: <today's date YYYY-MM-DD>
80
+ ---
81
+
82
+ # <Agent Name in Title Case>
83
+
84
+ <Full system prompt extension. Include:>
85
+ <- Persona description (who the agent is)>
86
+ <- Core responsibilities>
87
+ <- Workflow / step-by-step approach>
88
+ <- Output format expectations>
89
+ <- Critical rules and constraints>
90
+ ```
91
+
92
+ ### Rules
93
+ - The agent name MUST be kebab-case (lowercase, hyphens only).
94
+ - Pick tools that match the agent's purpose — don't give a read-only reviewer `write_file`.
95
+ - The body should be 150-400 words, specific and actionable.
96
+ - Do NOT include any other files. Just the one AGENT.md.
97
+ - After writing the file, briefly tell the user they can activate the agent with `/agent use <name>`.
98
+
99
+ ### User's Description
100
+ $ARGUMENTS
code/server/routes.py CHANGED
@@ -132,6 +132,14 @@ async def homepage():
132
  except Exception:
133
  hooks_list = []
134
 
 
 
 
 
 
 
 
 
135
  config = json.dumps({
136
  "app_title": APP_TITLE,
137
  "model_id": MODEL_CONFIGS[DEFAULT_MODEL_KEY]["id"],
@@ -146,6 +154,8 @@ async def homepage():
146
  "skills": skills_list,
147
  "commands": commands_list,
148
  "hooks": hooks_list,
 
 
149
  })
150
  content = content.replace("__RUNTIME_CONFIG__", config)
151
  return content
@@ -697,10 +707,15 @@ def handle_agent_run(
697
  skills_json: str = "[]",
698
  search_enabled: str = "false",
699
  image_url: str = "",
 
700
  ) -> str:
701
  """Run the Claude Code-style agent loop with tools.
702
 
703
  Yields JSON events: status, tool_call, tool_result, streaming, complete, error.
 
 
 
 
704
  """
705
  from code.agent import run_agent
706
 
@@ -715,6 +730,78 @@ def handle_agent_run(
715
  })
716
  return
717
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
  # Optional web search
719
  search_context = ""
720
  if search_enabled.lower() == "true":
@@ -739,6 +826,7 @@ def handle_agent_run(
739
  skills=skills,
740
  search_context=search_context,
741
  image_url=image_url.strip() or None,
 
742
  ):
743
  yield json.dumps(event, default=str)
744
  except Exception as exc:
@@ -915,3 +1003,112 @@ def handle_delete_hook(name: str) -> str:
915
  from code.hooks import delete_hook
916
  result = delete_hook(name)
917
  yield json.dumps(result, default=str)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  except Exception:
133
  hooks_list = []
134
 
135
+ try:
136
+ from code.agents import list_agents, get_active_agent
137
+ agents_list = list_agents()
138
+ active_agent = get_active_agent()
139
+ except Exception:
140
+ agents_list = []
141
+ active_agent = None
142
+
143
  config = json.dumps({
144
  "app_title": APP_TITLE,
145
  "model_id": MODEL_CONFIGS[DEFAULT_MODEL_KEY]["id"],
 
154
  "skills": skills_list,
155
  "commands": commands_list,
156
  "hooks": hooks_list,
157
+ "agents": agents_list,
158
+ "active_agent": active_agent,
159
  })
160
  content = content.replace("__RUNTIME_CONFIG__", config)
161
  return content
 
707
  skills_json: str = "[]",
708
  search_enabled: str = "false",
709
  image_url: str = "",
710
+ agent_name: str = "",
711
  ) -> str:
712
  """Run the Claude Code-style agent loop with tools.
713
 
714
  Yields JSON events: status, tool_call, tool_result, streaming, complete, error.
715
+
716
+ `agent_name` (optional) overrides the session-active agent for this run.
717
+ The `/agent use`, `/agent reset`, and `/agent delete` slash commands are
718
+ intercepted here and dispatched to the agents module before the model runs.
719
  """
720
  from code.agent import run_agent
721
 
 
730
  })
731
  return
732
 
733
+ # ── Intercept /agent use|reset|delete (session-state mutations) ────
734
+ # These need to happen server-side BEFORE the model runs so the very
735
+ # next prompt reflects the change.
736
+ stripped = prompt.lstrip()
737
+ if stripped.startswith("/agent ") or stripped == "/agent":
738
+ from code.agents import (
739
+ set_active_agent,
740
+ delete_agent as _delete_agent,
741
+ list_agents as _list_agents,
742
+ get_active_agent,
743
+ )
744
+ parts = stripped.split(None, 2) # ["/agent", <sub>, <rest>]
745
+ sub = parts[1] if len(parts) > 1 else ""
746
+ arg = parts[2].strip() if len(parts) > 2 else ""
747
+
748
+ if sub == "use" and arg:
749
+ result = set_active_agent(arg)
750
+ yield json.dumps({
751
+ "type": "complete",
752
+ "content": (
753
+ f"**Agent activated: `{result.get('active_agent')}`**\n\n"
754
+ + (result.get("message", "") if not result.get("success") else "Subsequent prompts will use this agent's persona and tool whitelist.")
755
+ ),
756
+ "agent": result.get("active_agent"),
757
+ "agent_op": "use",
758
+ })
759
+ return
760
+ if sub == "reset" or (sub == "" and arg == ""):
761
+ result = set_active_agent(None)
762
+ yield json.dumps({
763
+ "type": "complete",
764
+ "content": "**Active agent reset.** Subsequent prompts will use the default SoniCoder persona.",
765
+ "agent": None,
766
+ "agent_op": "reset",
767
+ })
768
+ return
769
+ if sub == "delete" and arg:
770
+ result = _delete_agent(arg)
771
+ if result.get("success"):
772
+ yield json.dumps({
773
+ "type": "complete",
774
+ "content": f"**Agent `{arg}` deleted.**",
775
+ "agent": None,
776
+ "agent_op": "delete",
777
+ })
778
+ else:
779
+ yield json.dumps({
780
+ "type": "error",
781
+ "message": result.get("error", f"Failed to delete agent '{arg}'"),
782
+ "agent_op": "delete",
783
+ })
784
+ return
785
+ if sub == "list":
786
+ agents_list = _list_agents()
787
+ if not agents_list:
788
+ content = "_No agents available._ Create one with `/agent create <description>`."
789
+ else:
790
+ lines = ["| Name | Description | Author | Tools |", "|------|-------------|--------|-------|"]
791
+ for a in agents_list:
792
+ tools = ", ".join(a.get("tools", [])) or "(all)"
793
+ active_marker = " **(active)**" if a.get("active") else ""
794
+ lines.append(f"| `{a['name']}`{active_marker} | {a.get('description', '')[:80]} | {a.get('author', '')} | {tools} |")
795
+ content = "\n".join(lines)
796
+ yield json.dumps({
797
+ "type": "complete",
798
+ "content": content,
799
+ "agents": agents_list,
800
+ "agent_op": "list",
801
+ })
802
+ return
803
+ # /agent create|show → fall through to the model (handled by slash command expansion)
804
+
805
  # Optional web search
806
  search_context = ""
807
  if search_enabled.lower() == "true":
 
826
  skills=skills,
827
  search_context=search_context,
828
  image_url=image_url.strip() or None,
829
+ agent_name=agent_name.strip() or None,
830
  ):
831
  yield json.dumps(event, default=str)
832
  except Exception as exc:
 
1003
  from code.hooks import delete_hook
1004
  result = delete_hook(name)
1005
  yield json.dumps(result, default=str)
1006
+
1007
+
1008
+ # ─── Custom Agent Endpoints ────────────────────────────────────────────
1009
+
1010
+
1011
+ @app.api(name="list_agents", concurrency_limit=4)
1012
+ def handle_list_agents() -> str:
1013
+ """List all available custom agents (builtins + user)."""
1014
+ from code.agents import list_agents, get_active_agent
1015
+ agents = list_agents()
1016
+ active = get_active_agent()
1017
+ yield json.dumps({
1018
+ "success": True,
1019
+ "agents": agents,
1020
+ "active_agent": active,
1021
+ }, default=str)
1022
+
1023
+
1024
+ @app.api(name="get_agent", concurrency_limit=4)
1025
+ def handle_get_agent(name: str) -> str:
1026
+ """Get the full definition of a single agent."""
1027
+ from code.agents import get_agent
1028
+ agent = get_agent(name)
1029
+ if not agent:
1030
+ yield json.dumps({"success": False, "error": f"Agent not found: {name}"})
1031
+ return
1032
+ # Strip non-serializable path
1033
+ agent_serializable = {k: v for k, v in agent.items() if k != "path"}
1034
+ yield json.dumps({"success": True, "agent": agent_serializable}, default=str)
1035
+
1036
+
1037
+ @app.api(name="save_agent", concurrency_limit=1)
1038
+ def handle_save_agent(
1039
+ name: str,
1040
+ description: str,
1041
+ body: str,
1042
+ tools: str = "",
1043
+ skills: str = "",
1044
+ temperature: str = "",
1045
+ max_iterations: str = "",
1046
+ tags: str = "",
1047
+ author: str = "user",
1048
+ ) -> str:
1049
+ """Create or overwrite a custom agent definition (manual save, no AI).
1050
+
1051
+ `tools`, `skills`, `tags` are comma-separated strings. `temperature` and
1052
+ `max_iterations` are strings that will be parsed if non-empty.
1053
+ """
1054
+ from code.agents import save_agent, ALL_TOOLS
1055
+
1056
+ def _split(s: str) -> list[str]:
1057
+ return [x.strip() for x in (s or "").split(",") if x.strip()]
1058
+
1059
+ tools_list = _split(tools) or list(ALL_TOOLS)
1060
+ skills_list = _split(skills)
1061
+ tags_list = _split(tags)
1062
+
1063
+ temp_val = None
1064
+ if temperature.strip():
1065
+ try:
1066
+ temp_val = float(temperature)
1067
+ except ValueError:
1068
+ yield json.dumps({"success": False, "error": f"Invalid temperature: {temperature}"})
1069
+ return
1070
+
1071
+ iter_val = None
1072
+ if max_iterations.strip():
1073
+ try:
1074
+ iter_val = int(max_iterations)
1075
+ except ValueError:
1076
+ yield json.dumps({"success": False, "error": f"Invalid max_iterations: {max_iterations}"})
1077
+ return
1078
+
1079
+ result = save_agent(
1080
+ name=name,
1081
+ description=description,
1082
+ body=body,
1083
+ tools=tools_list,
1084
+ skills=skills_list,
1085
+ temperature=temp_val,
1086
+ max_iterations=iter_val,
1087
+ tags=tags_list,
1088
+ author=author,
1089
+ )
1090
+ yield json.dumps(result, default=str)
1091
+
1092
+
1093
+ @app.api(name="delete_agent", concurrency_limit=1)
1094
+ def handle_delete_agent(name: str) -> str:
1095
+ """Delete a user-defined agent by name."""
1096
+ from code.agents import delete_agent
1097
+ result = delete_agent(name)
1098
+ yield json.dumps(result, default=str)
1099
+
1100
+
1101
+ @app.api(name="set_active_agent", concurrency_limit=1)
1102
+ def handle_set_active_agent(name: str = "") -> str:
1103
+ """Set the active agent for subsequent prompts. Empty string resets."""
1104
+ from code.agents import set_active_agent, list_agents, get_active_agent
1105
+ result = set_active_agent(name.strip() or None)
1106
+ if not result.get("success"):
1107
+ yield json.dumps(result, default=str)
1108
+ return
1109
+ # Return fresh list + active agent so frontend can re-render
1110
+ yield json.dumps({
1111
+ **result,
1112
+ "agents": list_agents(),
1113
+ "active_agent": get_active_agent(),
1114
+ }, default=str)
index.html CHANGED
@@ -1254,6 +1254,42 @@ body.hide-thinking .think-block { display: none; }
1254
  <button onclick="clearActiveSkills()" style="margin-top:6px;font-size:10px;color:var(--red);background:none;border:1px solid var(--red);padding:2px 8px;cursor:pointer;border-radius:3px;">Clear all</button>
1255
  </div>
1256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1257
  <!-- Hooks -->
1258
  <div class="deploy-field">
1259
  <label>Hooks (active rules)</label>
@@ -1402,6 +1438,9 @@ const state = {
1402
  agentMode: false,
1403
  activeSkills: [],
1404
  todos: [],
 
 
 
1405
  };
1406
 
1407
  // ═══════════════════════════════════════════════════════
@@ -2288,10 +2327,11 @@ async function callAgentApi(name, data) {
2288
  function toggleAgentMode() {
2289
  state.agentMode = document.getElementById('agent-mode-toggle').checked;
2290
  if (state.agentMode) {
2291
- addSystemMessage('🤖 Agent mode enabled. The model can now call tools (read_file, write_file, edit_file, bash, etc.) and manipulate the workspace. Slash commands like /commit, /review, /feature are also active.');
2292
- // Refresh workspace + todos + skills display
2293
  refreshWorkspace();
2294
  refreshTodos();
 
2295
  } else {
2296
  addSystemMessage('Agent mode disabled. Back to standard chat mode.');
2297
  }
@@ -2385,6 +2425,201 @@ function renderHooksList() {
2385
  }).join('');
2386
  }
2387
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2388
  async function refreshTodos() {
2389
  try {
2390
  const es = await callAgentApi('todo_read', ['default']);
@@ -2705,12 +2940,16 @@ sendMessage = async function(prompt) {
2705
 
2706
  // Initialize agent UI on page load
2707
  document.addEventListener('DOMContentLoaded', () => {
2708
- // Render skills/commands/hooks after CONFIG is loaded
2709
  setTimeout(() => {
2710
  renderSkillsList();
2711
  renderCommandsList();
2712
  renderHooksList();
2713
  renderActiveSkills();
 
 
 
 
2714
  }, 100);
2715
  });
2716
 
 
1254
  <button onclick="clearActiveSkills()" style="margin-top:6px;font-size:10px;color:var(--red);background:none;border:1px solid var(--red);padding:2px 8px;cursor:pointer;border-radius:3px;">Clear all</button>
1255
  </div>
1256
 
1257
+ <!-- Custom Agents -->
1258
+ <div class="deploy-field">
1259
+ <label>&#129302; Custom Agents <span id="active-agent-badge" style="display:none;background:var(--purple-dim, #2d1b4e);color:var(--purple, #a855f7);padding:1px 6px;border-radius:3px;font-size:9px;margin-left:6px;border:1px solid var(--purple, #a855f7);">active: <span id="active-agent-name">-</span></span></label>
1260
+ <div id="agents-list" style="display:grid;grid-template-columns:1fr;gap:4px;font-size:11px;"></div>
1261
+ <div class="deploy-hint">
1262
+ Click an agent to activate it. Built-in agents: <code>code-reviewer</code>, <code>test-writer</code>.
1263
+ Or describe a new one in natural language and let the AI generate it:
1264
+ </div>
1265
+ <!-- AI agent creation box -->
1266
+ <div style="margin-top:8px;border:1px dashed var(--border);padding:8px;border-radius:4px;background:var(--bg-code);">
1267
+ <div style="font-size:10px;color:var(--gray-light);margin-bottom:4px;font-weight:600;">&#10024; AI-Generate a Custom Agent</div>
1268
+ <textarea id="agent-create-input" rows="2" placeholder="e.g. 'a security reviewer that audits dependencies and flags hardcoded secrets'" style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:6px;font-family:var(--font-mono);font-size:11px;resize:vertical;"></textarea>
1269
+ <div style="display:flex;gap:6px;margin-top:6px;">
1270
+ <button onclick="createAgentViaAI()" id="btn-create-agent-ai" style="font-size:10px;background:var(--green);color:#000;border:none;padding:4px 10px;cursor:pointer;border-radius:3px;font-weight:600;">&#10024; Generate via AI</button>
1271
+ <button onclick="showManualAgentEditor()" style="font-size:10px;background:none;border:1px solid var(--border);padding:3px 10px;cursor:pointer;border-radius:3px;color:var(--gray-light);">Write manually</button>
1272
+ </div>
1273
+ </div>
1274
+ <!-- Manual editor (hidden by default) -->
1275
+ <div id="manual-agent-editor" style="display:none;margin-top:8px;border:1px solid var(--border);padding:8px;border-radius:4px;background:var(--bg-code);">
1276
+ <div style="font-size:10px;color:var(--gray-light);margin-bottom:4px;font-weight:600;">Manual Agent Editor</div>
1277
+ <input type="text" id="agent-name-input" placeholder="name (kebab-case)" style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-family:var(--font-mono);font-size:11px;margin-bottom:4px;">
1278
+ <input type="text" id="agent-desc-input" placeholder="one-line description" style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-size:11px;margin-bottom:4px;">
1279
+ <input type="text" id="agent-tools-input" placeholder="tools (comma-sep, e.g. read_file,grep,bash)" style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-family:var(--font-mono);font-size:11px;margin-bottom:4px;">
1280
+ <input type="text" id="agent-skills-input" placeholder="skills (comma-sep, e.g. code-review)" style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-family:var(--font-mono);font-size:11px;margin-bottom:4px;">
1281
+ <div style="display:flex;gap:4px;margin-bottom:4px;">
1282
+ <input type="text" id="agent-temp-input" placeholder="temp (0.0-1.0)" style="flex:1;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-size:11px;">
1283
+ <input type="text" id="agent-iter-input" placeholder="max_iter" style="flex:1;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-size:11px;">
1284
+ </div>
1285
+ <textarea id="agent-body-input" rows="6" placeholder="# My Agent&#10;&#10;Full system-prompt extension here..." style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:6px;font-family:var(--font-mono);font-size:11px;resize:vertical;margin-bottom:4px;"></textarea>
1286
+ <div style="display:flex;gap:6px;">
1287
+ <button onclick="saveManualAgent()" style="font-size:10px;background:var(--green);color:#000;border:none;padding:4px 10px;cursor:pointer;border-radius:3px;font-weight:600;">Save agent</button>
1288
+ <button onclick="hideManualAgentEditor()" style="font-size:10px;background:none;border:1px solid var(--border);padding:3px 10px;cursor:pointer;border-radius:3px;color:var(--gray-light);">Cancel</button>
1289
+ </div>
1290
+ </div>
1291
+ </div>
1292
+
1293
  <!-- Hooks -->
1294
  <div class="deploy-field">
1295
  <label>Hooks (active rules)</label>
 
1438
  agentMode: false,
1439
  activeSkills: [],
1440
  todos: [],
1441
+ // Custom agents
1442
+ activeAgent: null,
1443
+ agentsList: [],
1444
  };
1445
 
1446
  // ═══════════════════════════════════════════════════════
 
2327
  function toggleAgentMode() {
2328
  state.agentMode = document.getElementById('agent-mode-toggle').checked;
2329
  if (state.agentMode) {
2330
+ addSystemMessage('🤖 Agent mode enabled. The model can now call tools (read_file, write_file, edit_file, bash, etc.) and manipulate the workspace. Slash commands like /commit, /review, /feature, /agent are also active.');
2331
+ // Refresh workspace + todos + skills + agents display
2332
  refreshWorkspace();
2333
  refreshTodos();
2334
+ refreshAgents();
2335
  } else {
2336
  addSystemMessage('Agent mode disabled. Back to standard chat mode.');
2337
  }
 
2425
  }).join('');
2426
  }
2427
 
2428
+ // ═══════════════════════════════════════════════════════
2429
+ // CUSTOM AGENTS (AI-generated personas)
2430
+ // ═══════════════════════════════════════════════════════
2431
+
2432
+ function renderAgentsList() {
2433
+ const container = document.getElementById('agents-list');
2434
+ if (!container) return;
2435
+ const agents = (state.agentsList || CONFIG.agents || []);
2436
+ if (agents.length === 0) {
2437
+ container.innerHTML = '<div style="color:var(--gray-dim);">No agents yet. Describe one above to have the AI generate it, or use <code>/agent create</code> in chat.</div>';
2438
+ return;
2439
+ }
2440
+ container.innerHTML = agents.map(a => {
2441
+ const isActive = state.activeAgent === a.name;
2442
+ const bg = isActive ? 'var(--purple-dim, #2d1b4e)' : 'var(--bg-code)';
2443
+ const color = isActive ? 'var(--purple, #a855f7)' : 'var(--gray-light)';
2444
+ const border = isActive ? 'var(--purple, #a855f7)' : 'var(--border)';
2445
+ const tools = (a.tools || []).slice(0, 4).join(', ') + ((a.tools || []).length > 4 ? '...' : '');
2446
+ const isBuiltin = (a.author === 'builtin');
2447
+ const delBtn = isBuiltin ? '' :
2448
+ `<span style="cursor:pointer;color:var(--red);margin-left:6px;font-size:10px;" onclick="event.stopPropagation();deleteAgent('${a.name}')" title="Delete">x</span>`;
2449
+ return `<div style="padding:6px 8px;background:${bg};color:${color};border:1px solid ${border};border-radius:3px;cursor:pointer;" onclick="activateAgent('${a.name}')">
2450
+ <div style="display:flex;justify-content:space-between;align-items:center;">
2451
+ <span style="font-weight:600;">${isActive ? '★ ' : ''}${a.name} <span style="font-size:9px;color:var(--gray-mid);font-weight:400;">[${a.author || 'user'}]</span></span>
2452
+ ${delBtn}
2453
+ </div>
2454
+ <div style="font-size:10px;color:var(--gray-mid);margin-top:2px;">${(a.description || '').slice(0, 100)}</div>
2455
+ ${tools ? `<div style="font-size:9px;color:var(--gray-dim);margin-top:2px;font-family:var(--font-mono);">tools: ${tools}</div>` : ''}
2456
+ </div>`;
2457
+ }).join('');
2458
+ }
2459
+
2460
+ function renderActiveAgentBadge() {
2461
+ const badge = document.getElementById('active-agent-badge');
2462
+ const name = document.getElementById('active-agent-name');
2463
+ if (!badge || !name) return;
2464
+ if (state.activeAgent) {
2465
+ badge.style.display = 'inline';
2466
+ name.textContent = state.activeAgent;
2467
+ } else {
2468
+ badge.style.display = 'none';
2469
+ name.textContent = '-';
2470
+ }
2471
+ }
2472
+
2473
+ async function refreshAgents() {
2474
+ try {
2475
+ const es = await callAgentApi('list_agents', []);
2476
+ es.addEventListener('complete', (e) => {
2477
+ try {
2478
+ const data = JSON.parse(e.data);
2479
+ const result = JSON.parse(data[0]);
2480
+ if (result.success) {
2481
+ state.agentsList = result.agents || [];
2482
+ state.activeAgent = result.active_agent || null;
2483
+ renderAgentsList();
2484
+ renderActiveAgentBadge();
2485
+ }
2486
+ } catch (err) { console.error('Agent refresh error:', err); }
2487
+ es.close();
2488
+ });
2489
+ es.addEventListener('error', () => es.close());
2490
+ } catch (err) { console.error('refreshAgents failed:', err); }
2491
+ }
2492
+
2493
+ async function activateAgent(name) {
2494
+ try {
2495
+ const es = await callAgentApi('set_active_agent', [name]);
2496
+ es.addEventListener('complete', (e) => {
2497
+ try {
2498
+ const data = JSON.parse(e.data);
2499
+ const result = JSON.parse(data[0]);
2500
+ if (result.success) {
2501
+ state.activeAgent = result.active_agent || name;
2502
+ state.agentsList = result.agents || state.agentsList;
2503
+ renderAgentsList();
2504
+ renderActiveAgentBadge();
2505
+ addSystemMessage(`★ Active agent: ${state.activeAgent}. Subsequent prompts will use this persona and tool whitelist. Use /agent reset to revert.`);
2506
+ } else {
2507
+ addSystemMessage(`Failed to activate agent: ${result.error || 'unknown'}`);
2508
+ }
2509
+ } catch (err) { console.error('Agent activate error:', err); }
2510
+ es.close();
2511
+ });
2512
+ es.addEventListener('error', () => es.close());
2513
+ } catch (err) { console.error('activateAgent failed:', err); }
2514
+ }
2515
+
2516
+ async function resetAgent() {
2517
+ try {
2518
+ const es = await callAgentApi('set_active_agent', ['']);
2519
+ es.addEventListener('complete', (e) => {
2520
+ try {
2521
+ const data = JSON.parse(e.data);
2522
+ const result = JSON.parse(data[0]);
2523
+ if (result.success) {
2524
+ state.activeAgent = null;
2525
+ renderAgentsList();
2526
+ renderActiveAgentBadge();
2527
+ addSystemMessage('Active agent reset to default SoniCoder.');
2528
+ }
2529
+ } catch (err) { console.error('Agent reset error:', err); }
2530
+ es.close();
2531
+ });
2532
+ es.addEventListener('error', () => es.close());
2533
+ } catch (err) { console.error('resetAgent failed:', err); }
2534
+ }
2535
+
2536
+ async function deleteAgent(name) {
2537
+ if (!confirm(`Delete agent '${name}'? This cannot be undone.`)) return;
2538
+ try {
2539
+ const es = await callAgentApi('delete_agent', [name]);
2540
+ es.addEventListener('complete', (e) => {
2541
+ try {
2542
+ const data = JSON.parse(e.data);
2543
+ const result = JSON.parse(data[0]);
2544
+ if (result.success) {
2545
+ addSystemMessage(`Agent '${name}' deleted.`);
2546
+ refreshAgents();
2547
+ } else {
2548
+ addSystemMessage(`Failed to delete agent: ${result.error || 'unknown'}`);
2549
+ }
2550
+ } catch (err) { console.error('Agent delete error:', err); }
2551
+ es.close();
2552
+ });
2553
+ es.addEventListener('error', () => es.close());
2554
+ } catch (err) { console.error('deleteAgent failed:', err); }
2555
+ }
2556
+
2557
+ function showManualAgentEditor() {
2558
+ document.getElementById('manual-agent-editor').style.display = 'block';
2559
+ }
2560
+ function hideManualAgentEditor() {
2561
+ document.getElementById('manual-agent-editor').style.display = 'none';
2562
+ }
2563
+
2564
+ async function saveManualAgent() {
2565
+ const name = document.getElementById('agent-name-input').value.trim();
2566
+ const desc = document.getElementById('agent-desc-input').value.trim();
2567
+ const body = document.getElementById('agent-body-input').value.trim();
2568
+ const tools = document.getElementById('agent-tools-input').value.trim();
2569
+ const skills = document.getElementById('agent-skills-input').value.trim();
2570
+ const temp = document.getElementById('agent-temp-input').value.trim();
2571
+ const iter = document.getElementById('agent-iter-input').value.trim();
2572
+ if (!name || !body) {
2573
+ addSystemMessage('Name and body are required to save an agent.');
2574
+ return;
2575
+ }
2576
+ try {
2577
+ const es = await callAgentApi('save_agent', [name, desc, body, tools, skills, temp, iter, '', 'user']);
2578
+ es.addEventListener('complete', (e) => {
2579
+ try {
2580
+ const data = JSON.parse(e.data);
2581
+ const result = JSON.parse(data[0]);
2582
+ if (result.success) {
2583
+ addSystemMessage(`Agent '${name}' saved. Click it in the list (or /agent use ${name}) to activate.`);
2584
+ hideManualAgentEditor();
2585
+ ['agent-name-input','agent-desc-input','agent-body-input','agent-tools-input','agent-skills-input','agent-temp-input','agent-iter-input']
2586
+ .forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });
2587
+ refreshAgents();
2588
+ } else {
2589
+ addSystemMessage(`Failed to save agent: ${result.error || 'unknown'}`);
2590
+ }
2591
+ } catch (err) { console.error('Agent save error:', err); }
2592
+ es.close();
2593
+ });
2594
+ es.addEventListener('error', () => es.close());
2595
+ } catch (err) { console.error('saveManualAgent failed:', err); }
2596
+ }
2597
+
2598
+ function createAgentViaAI() {
2599
+ const desc = document.getElementById('agent-create-input').value.trim();
2600
+ if (!desc) {
2601
+ addSystemMessage('Describe the agent you want first.');
2602
+ return;
2603
+ }
2604
+ // Send `/agent create <description>` as a chat prompt — the agent_run
2605
+ // handler routes it through the slash-command expansion + AI generation.
2606
+ const input = document.getElementById('chat-input');
2607
+ if (input) {
2608
+ input.value = `/agent create ${desc}`;
2609
+ // Trigger send via the global handleSend() (defined elsewhere in this file)
2610
+ if (typeof handleSend === 'function') {
2611
+ handleSend();
2612
+ } else {
2613
+ const sendBtn = document.getElementById('btn-send');
2614
+ if (sendBtn) sendBtn.click();
2615
+ }
2616
+ } else {
2617
+ // Fallback: tell user
2618
+ addSystemMessage(`Type this in chat: /agent create ${desc}`);
2619
+ }
2620
+ document.getElementById('agent-create-input').value = '';
2621
+ }
2622
+
2623
  async function refreshTodos() {
2624
  try {
2625
  const es = await callAgentApi('todo_read', ['default']);
 
2940
 
2941
  // Initialize agent UI on page load
2942
  document.addEventListener('DOMContentLoaded', () => {
2943
+ // Render skills/commands/hooks/agents after CONFIG is loaded
2944
  setTimeout(() => {
2945
  renderSkillsList();
2946
  renderCommandsList();
2947
  renderHooksList();
2948
  renderActiveSkills();
2949
+ // Pull the live agent list + active agent from the backend
2950
+ refreshAgents();
2951
+ renderAgentsList();
2952
+ renderActiveAgentBadge();
2953
  }, 100);
2954
  });
2955