diff --git a/Makefile b/Makefile index 5499b82..6dfface 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build build-linux-slim build-all package-all install install-win uninstall clean help test install-bootstrap-docs sync-embed-workspace cleanup-embed-workspace test-only clean-test-artifacts +.PHONY: all build build-linux-slim build-all package-all install install-win uninstall clean help test install-bootstrap-docs sync-embed-workspace cleanup-embed-workspace test-only clean-test-artifacts dev # Build variables BINARY_NAME=clawgo @@ -47,6 +47,11 @@ WORKSPACE_SKILLS_DIR=$(WORKSPACE_DIR)/skills BUILTIN_SKILLS_DIR=$(CURDIR)/skills WORKSPACE_SOURCE_DIR=$(CURDIR)/workspace EMBED_WORKSPACE_DIR=$(CURDIR)/cmd/$(BINARY_NAME)/workspace +DEV_CONFIG?=$(if $(wildcard $(CURDIR)/config.json),$(CURDIR)/config.json,$(CLAWGO_HOME)/config.json) +DEV_ARGS?=--debug gateway run +DEV_WORKSPACE?=$(WORKSPACE_DIR) +DEV_WEBUI_DIR?=$(CURDIR)/webui +DEV_WEBUI_BUILD?=1 # OS detection UNAME_S:=$(shell uname -s) @@ -298,6 +303,31 @@ deps: run: build @$(BUILD_DIR)/$(BINARY_NAME) $(ARGS) +## dev: Run the local gateway in foreground for debugging +dev: sync-embed-workspace + @if [ ! -f "$(DEV_CONFIG)" ]; then \ + echo "✗ Missing config file: $(DEV_CONFIG)"; \ + echo " Override with: make dev DEV_CONFIG=/path/to/config.json"; \ + exit 1; \ + fi + @if [ ! -d "$(DEV_WEBUI_DIR)" ]; then \ + echo "✗ Missing WebUI directory: $(DEV_WEBUI_DIR)"; \ + exit 1; \ + fi + @set -e; trap '$(MAKE) -C $(CURDIR) cleanup-embed-workspace' EXIT; \ + if [ "$(DEV_WEBUI_BUILD)" = "1" ]; then \ + echo "Building WebUI..."; \ + (cd "$(DEV_WEBUI_DIR)" && npm run build); \ + fi; \ + echo "Syncing WebUI dist to $(DEV_WORKSPACE)/webui ..."; \ + mkdir -p "$(DEV_WORKSPACE)/webui"; \ + rsync -a --delete "$(DEV_WEBUI_DIR)/dist/" "$(DEV_WORKSPACE)/webui/"; \ + echo "Starting local gateway debug session..."; \ + echo " Config: $(DEV_CONFIG)"; \ + echo " WebUI: $(DEV_WORKSPACE)/webui"; \ + echo " Args: $(DEV_ARGS)"; \ + CLAWGO_CONFIG="$(DEV_CONFIG)" $(GO) run $(GOFLAGS) ./$(CMD_DIR) $(DEV_ARGS) + ## test: Build and compile-check in Docker (Dockerfile.test) test: sync-embed-workspace @echo "Running Docker compile test..." @@ -317,6 +347,7 @@ help: @echo "" @echo "Examples:" @echo " make build # Build for current platform" + @echo " make dev # Run local gateway in foreground with debug logs" @echo " make install # Install to /usr/local/bin" @echo " make install-user # Install to ~/.local/bin" @echo " make uninstall # Remove from /usr/local/bin" @@ -325,6 +356,11 @@ help: @echo "Environment Variables:" @echo " INSTALL_PREFIX # Installation prefix (default: /usr/local)" @echo " WORKSPACE_DIR # Workspace directory (default: ~/.clawgo/workspace)" + @echo " DEV_CONFIG # Config path for make dev" + @echo " DEV_ARGS # CLI args for make dev (default: --debug gateway run)" + @echo " DEV_WORKSPACE # Workspace path for WebUI sync in make dev" + @echo " DEV_WEBUI_DIR # WebUI source dir for make dev (default: ./webui)" + @echo " DEV_WEBUI_BUILD # 1=build WebUI before make dev (default: 1)" @echo " VERSION # Version string (default: git describe)" @echo " STRIP_SYMBOLS # 1=strip debug/symbol info (default: 1)" @echo "" diff --git a/README.md b/README.md index 489b78b..c66e19c 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,63 @@ -# ClawGo +# ClawGo 🦞 -一个用 Go 编写的长期运行 AI Agent 系统,核心特点是: +ClawGo 是一个用 Go 构建的长期运行 Agent 系统,面向本地优先、多 Agent 协作、可审计运维。 -- 主 agent + subagent 的声明式配置 -- 可持久化的 run / thread / mailbox 运行态 -- node 注册后的远端 agent 分支 -- 多通道接入与统一 WebUI 运维面板 -- 轻量、可审计、适合长期运行 +- 🎯 `main agent` 负责对话入口、路由、调度、汇总 +- 🤖 `subagents` 负责具体执行,如编码、测试、文档 +- 🌐 `node branches` 允许把远端节点挂成受控 agent 分支 +- 🧠 记忆、线程、邮箱、任务运行态可持久化 +- 🖥️ 内置统一 WebUI,覆盖配置、拓扑、日志、技能、记忆与运维 [English](./README_EN.md) ---- +## 架构概览 -## 当前架构 - -ClawGo 现在不是“单 agent + 一堆工具”的结构,而是一个可编排的 agent tree: - -- `main agent` - - 用户入口 - - 负责路由、调度、汇总结果 -- `local subagents` - - 例如 `coder`、`tester` - - 由 `config.json` 中的 `agents.subagents` 声明 -- `node-backed branches` - - 注册进来的 node 会被视为 `main` 下的一条远端 agent 分支 - - 形如 `node..main` - - 可以作为主 agent 的受控执行目标 - -默认协作模式是: +ClawGo 的默认协作模式是: ```text user -> main -> worker -> main -> user ``` ---- +当前系统由四层组成: + +- `main agent` + - 用户入口 + - 负责路由、拆解、派发、汇总 +- `local subagents` + - 在 `config.json -> agents.subagents` 中声明 + - 使用独立 session 和 memory namespace +- `node-backed branches` + - 注册 node 后,会在主拓扑里表现为远端 agent 分支 + - `transport=node` 的任务通过 `agent_task` 发往远端 +- `runtime store` + - 保存 run、event、thread、message 等运行态 ## 主要能力 -### 1. Agent 配置与路由 +- 🚦 路由与调度 + - 支持 `rules_first` + - 支持显式 `@agent_id` + - 支持关键词自动路由 +- 📦 持久化运行态 + - `subagent_runs.jsonl` + - `subagent_events.jsonl` + - `threads.jsonl` + - `agent_messages.jsonl` +- 📨 mailbox / thread 协作 + - dispatch + - wait + - reply + - ack +- 🧠 记忆双写 + - 子 agent 写自己的详细记忆 + - 主记忆保留简洁协作摘要 +- 🪪 声明式 agent 配置 + - role + - tool allowlist + - runtime policy + - `system_prompt_file` -- `agents.router` - - 主 agent 路由配置 - - 支持 `rules_first`、显式 `@agent_id` 路由、关键词路由 -- `agents.subagents` - - 声明 subagent 身份、角色、运行参数、工具白名单 -- `system_prompt_file` - - subagent 优先读取 `agents//AGENT.md` - - 不再依赖一句短 inline prompt - -### 2. Subagent 运行态持久化 - -运行态会落到: - -- `workspace/agents/runtime/subagent_runs.jsonl` -- `workspace/agents/runtime/subagent_events.jsonl` -- `workspace/agents/runtime/threads.jsonl` -- `workspace/agents/runtime/agent_messages.jsonl` - -因此可以支持: - -- run 恢复 -- 线程追踪 -- mailbox / inbox 查询 -- dispatch / wait / merge - -### 3. Node 分支 - -注册进来的 node 不只是设备状态,而是一个远端主 agent 分支: - -- node 会出现在统一 agent topology 里 -- 远端 registry 会以只读镜像方式展示 -- `transport=node` 的 subagent 会通过 `agent_task` 发往远端 node - -### 4. WebUI - -当前 WebUI 已经整合为统一的 agent 操作台,包含: - -- Dashboard -- Chat -- Config -- Logs -- Cron -- Skills -- Memory -- Task Audit -- EKG -- Agents - - 统一的 agent topology - - 本地/远端 agent 树 - - subagent runtime - - registry - - prompt file editor - - dispatch / thread / inbox - -### 5. 记忆策略 - -记忆不是简单共用,也不是完全隔离,而是双写: - -- `main` - - 维护主记忆与协作摘要 -- `subagent` - - 写入自己的 namespaced memory - - 路径类似 `workspace/agents//...` -- 对于 subagent 的自动摘要: - - 子 agent 自己记详细日志 - - 主记忆里同步保留一条简洁协作摘要 - -当前摘要格式大致是: - -```md -## 15:04 Code Agent | 修复登录接口并补测试 - -- Did: 完成了登录接口修复、增加回归测试,并验证通过。 -``` - ---- - -## 3 分钟上手 +## 快速开始 ### 1. 安装 @@ -130,21 +71,15 @@ curl -fsSL https://raw.githubusercontent.com/YspCoder/clawgo/main/install.sh | b clawgo onboard ``` -### 3. 配置 provider +### 3. 配置模型 ```bash clawgo provider ``` -### 4. 查看状态 +### 4. 启动 -```bash -clawgo status -``` - -### 5. 启动 - -本地交互模式: +交互模式: ```bash clawgo agent @@ -153,49 +88,59 @@ clawgo agent -m "Hello" 网关模式: -```bash -clawgo gateway -clawgo gateway start -clawgo gateway status -``` - -前台运行: - ```bash clawgo gateway run ``` ---- +开发模式: + +```bash +make dev +``` ## WebUI -访问地址: +访问: ```text http://:/webui?token= ``` -建议重点看这几个页面: +核心页面: - `Agents` - - 看整个 agent tree - - 看 node 分支 - - 看当前运行任务 - - 配置本地 subagent + - 统一 agent 拓扑 + - 本地 subagent 与远端 branch 展示 + - 运行状态悬浮查看 - `Config` - - 调整配置 + - 配置编辑 + - 热更新字段查看 - `Logs` - 实时日志 +- `Skills` + - 技能安装、查看、编辑 +- `Memory` + - 记忆文件与摘要 - `Task Audit` - - 看任务拆解、调度与执行痕迹 -- `EKG` - - 看错误签名、来源统计、负载分布 + - 任务链路与执行审计 ---- +### 亮点截图 + +**Dashboard** + +![ClawGo Dashboard](docs/assets/readme-dashboard.png) + +**Agents 拓扑** + +![ClawGo Agents Topology](docs/assets/readme-agents.png) + +**Config 工作台** + +![ClawGo Config](docs/assets/readme-config.png) ## 配置结构 -当前推荐围绕这些字段配置: +当前推荐结构: ```json { @@ -219,41 +164,36 @@ http://:/webui?token= "subagents": { "main": {}, "coder": {}, - "tester": {}, - "node.edge-dev.main": {} + "tester": {} } } } ``` -关键点: +说明: -- `runtime_control` 已移除 +- `runtime_control` 已删除 - 现在使用: - `agents.defaults.execution` - `agents.defaults.summary_policy` - `agents.router.policy` - 启用中的本地 subagent 必须配置 `system_prompt_file` -- node-backed agent 使用: +- 远端分支需要: - `transport: "node"` - `node_id` - `parent_agent_id` -可参考完整示例: +完整示例见 [config.example.json](/Users/lpf/Desktop/project/clawgo/config.example.json)。 -- [config.example.json](/Users/lpf/Desktop/project/clawgo/config.example.json) +## Prompt 文件约定 ---- - -## Subagent Prompt 约定 - -推荐为每个 agent 提供独立 prompt 文件: +推荐把 agent prompt 放到独立文件中,例如: - `agents/main/AGENT.md` - `agents/coder/AGENT.md` - `agents/tester/AGENT.md` -对应配置示例: +配置示例: ```json { @@ -261,73 +201,36 @@ http://:/webui?token= } ``` -规则: +约定: - 路径必须是 workspace 内相对路径 -- 创建 subagent 时应同时更新 config 和对应 `AGENT.md` -- 如果 subagent 职责发生实质变化,应同步更新它的 `AGENT.md` +- 这些路径只是示例,仓库不会内置对应文件 +- 用户或 agent workflow 需要自行创建这些 `AGENT.md` ---- +## 记忆与运行态 -## 常用命令 +ClawGo 默认不是“所有 agent 共用一份上下文”。 -```text -clawgo onboard -clawgo provider -clawgo status -clawgo agent [-m "..."] -clawgo gateway [run|start|stop|restart|status] -clawgo config set|get|check|reload -clawgo cron ... -clawgo skills ... -clawgo uninstall [--purge] [--remove-bin] -``` +- `main` + - 维护主记忆与协作摘要 +- `subagent` + - 使用独立 session key + - 写入自己的 memory namespace +- runtime store + - 持久化任务、事件、线程、消息 ---- +这样可以同时得到: -## 构建 +- 可恢复 +- 可追踪 +- 边界清晰 -本地构建: +## 项目定位 -```bash -make build -``` +ClawGo 适合这些场景: -全平台构建: +- 本地长期运行的个人 AI agent +- 需要多 agent 协作但不想上重型编排平台 +- 需要清晰配置、清晰审计、清晰可观测性的自动化系统 -```bash -make build-all -``` - -打包: - -```bash -make package-all -``` - ---- - -## 当前设计取向 - -ClawGo 当前刻意偏向这些原则: - -- 主 agent 仲裁优先 -- 运行态落盘优先 -- 配置声明优先 -- WebUI 先做统一运维台,再做复杂自动化 -- node 先作为受控远端 agent 分支,而不是完全独立自治体 - -也就是说,这个项目更像一个: - -- 可长期运行的个人 AI agent runtime -- 带多 agent 编排能力 -- 带 node 扩展能力 -- 带运维与审计面的系统 - -而不是一个“只会聊天”的单体机器人。 - ---- - -## License - -见仓库中的 `LICENSE` 文件。 +如果你希望先看一个完整配置,直接从 [config.example.json](/Users/lpf/Desktop/project/clawgo/config.example.json) 开始。 diff --git a/README_EN.md b/README_EN.md index 054d371..f43c648 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,120 +1,61 @@ -# ClawGo +# ClawGo 🦞 -ClawGo is a long-running AI agent system written in Go, designed around: +ClawGo is a long-running AI agent system built in Go for local-first operation, multi-agent coordination, and auditable runtime control. -- declarative main-agent and subagent configuration -- persistent run / thread / mailbox state -- node-registered remote agent branches -- multi-channel access plus a unified WebUI -- lightweight runtime behavior with strong auditability +- 🎯 `main agent` owns the user-facing loop, routing, dispatch, and merge +- 🤖 `subagents` handle concrete execution such as coding, testing, and docs +- 🌐 `node branches` let remote nodes appear as controlled agent branches +- 🧠 memory, threads, mailbox state, and task runtime are persisted +- 🖥️ a unified WebUI covers config, topology, logs, skills, memory, and ops [中文](./README.md) ---- +## Architecture -## Current Architecture - -ClawGo is no longer a "single agent plus some tools" runtime. It is now an orchestrated agent tree: - -- `main agent` - - user-facing entrypoint - - owns routing, dispatch, merge, and coordination -- `local subagents` - - for example `coder`, `tester` - - declared in `config.json` under `agents.subagents` -- `node-backed branches` - - a registered node is treated as a remote branch under `main` - - branch ids look like `node..main` - - they can be targeted by the main agent as controlled remote execution branches - -The default collaboration pattern is: +The default collaboration flow is: ```text user -> main -> worker -> main -> user ``` ---- +ClawGo currently has four layers: + +- `main agent` + - user-facing entrypoint + - handles routing, decomposition, dispatch, and merge +- `local subagents` + - declared in `config.json -> agents.subagents` + - use isolated sessions and memory namespaces +- `node-backed branches` + - registered nodes appear as remote agent branches in the topology + - `transport=node` tasks are sent via `agent_task` +- `runtime store` + - persists runs, events, threads, and messages ## Core Capabilities -### 1. Agent Config and Routing - -- `agents.router` - - main-agent routing configuration - - supports `rules_first`, explicit `@agent_id`, and keyword routing -- `agents.subagents` - - declares agent identity, role, runtime policy, and tool allowlists -- `system_prompt_file` - - subagents now prefer `agents//AGENT.md` - - they are no longer expected to rely on a one-line inline prompt - -### 2. Persistent Subagent Runtime - -Runtime state is persisted under: - -- `workspace/agents/runtime/subagent_runs.jsonl` -- `workspace/agents/runtime/subagent_events.jsonl` -- `workspace/agents/runtime/threads.jsonl` -- `workspace/agents/runtime/agent_messages.jsonl` - -This enables: - -- run recovery -- thread tracing -- mailbox / inbox inspection -- dispatch / wait / merge workflows - -### 3. Node Branches - -A registered node is not treated as just a device entry anymore. It is modeled as a remote main-agent branch: - -- nodes appear inside the unified agent topology -- remote registries are shown as read-only mirrors -- a subagent with `transport=node` is dispatched through `agent_task` to the target node - -### 4. WebUI - -The WebUI is now organized as a unified agent operations console, including: - -- Dashboard -- Chat -- Config -- Logs -- Cron -- Skills -- Memory -- Task Audit -- EKG -- Agents - - unified agent topology - - local and remote agent trees - - subagent runtime - - registry - - prompt file editor - - dispatch / thread / inbox controls - -### 5. Memory Strategy - -Memory is not fully shared and not fully isolated either. It uses a dual-write model: - -- `main` - - keeps workspace-level memory and collaboration summaries -- `subagent` - - writes to its own namespaced memory - - paths look like `workspace/agents//...` -- for automatic subagent summaries: - - the subagent keeps its own detailed entry - - the main memory also receives a short collaboration summary - -Example summary format: - -```md -## 15:04 Code Agent | Fix login API and add tests - -- Did: Fixed the login API, added regression coverage, and verified the result. -``` - ---- +- 🚦 Routing and dispatch + - `rules_first` + - explicit `@agent_id` + - keyword-based auto routing +- 📦 Persistent runtime state + - `subagent_runs.jsonl` + - `subagent_events.jsonl` + - `threads.jsonl` + - `agent_messages.jsonl` +- 📨 mailbox and thread coordination + - dispatch + - wait + - reply + - ack +- 🧠 dual-write memory model + - subagents keep detailed local memory + - main memory keeps compact collaboration summaries +- 🪪 declarative agent config + - role + - tool allowlist + - runtime policy + - `system_prompt_file` ## Quick Start @@ -136,15 +77,9 @@ clawgo onboard clawgo provider ``` -### 4. Check status +### 4. Start -```bash -clawgo status -``` - -### 5. Start - -Local interactive mode: +Interactive mode: ```bash clawgo agent @@ -153,19 +88,15 @@ clawgo agent -m "Hello" Gateway mode: -```bash -clawgo gateway -clawgo gateway start -clawgo gateway status -``` - -Foreground mode: - ```bash clawgo gateway run ``` ---- +Development mode: + +```bash +make dev +``` ## WebUI @@ -175,27 +106,41 @@ Open: http://:/webui?token= ``` -Recommended pages: +Key pages: - `Agents` - - view the entire agent tree - - inspect node branches - - inspect active runtime tasks - - edit local subagent configuration + - unified agent topology + - local subagents and remote branches + - runtime status via hover - `Config` - - edit runtime configuration + - configuration editing + - hot-reload field reference - `Logs` - - inspect real-time logs + - real-time logs +- `Skills` + - install, inspect, and edit skills +- `Memory` + - memory files and summaries - `Task Audit` - - inspect task decomposition, scheduling, and execution flow -- `EKG` - - inspect error signatures, source distribution, and workload hotspots + - execution and scheduling trace ---- +### Highlights + +**Dashboard** + +![ClawGo Dashboard](docs/assets/readme-dashboard.png) + +**Agent Topology** + +![ClawGo Agent Topology](docs/assets/readme-agents.png) + +**Config Workspace** + +![ClawGo Config](docs/assets/readme-config.png) ## Config Layout -The current recommended structure is centered around: +Recommended structure: ```json { @@ -219,35 +164,30 @@ The current recommended structure is centered around: "subagents": { "main": {}, "coder": {}, - "tester": {}, - "node.edge-dev.main": {} + "tester": {} } } } ``` -Important notes: +Notes: - `runtime_control` has been removed -- the current structure uses: +- the current config uses: - `agents.defaults.execution` - `agents.defaults.summary_policy` - `agents.router.policy` - enabled local subagents must define `system_prompt_file` -- node-backed agents use: +- remote branches require: - `transport: "node"` - `node_id` - `parent_agent_id` -See the full example: +See the full example in [config.example.json](/Users/lpf/Desktop/project/clawgo/config.example.json). -- [config.example.json](/Users/lpf/Desktop/project/clawgo/config.example.json) +## Prompt File Convention ---- - -## Subagent Prompt Convention - -Each agent should ideally have its own prompt file: +Keep agent prompts in dedicated files, for example: - `agents/main/AGENT.md` - `agents/coder/AGENT.md` @@ -263,72 +203,35 @@ Example: Rules: -- the path must stay inside the workspace -- the path should be relative -- when creating a subagent, update both config and the matching `AGENT.md` -- when a subagent's responsibility changes materially, update its `AGENT.md` too +- the path must be relative to the workspace +- the file must live inside the workspace +- these paths are examples only; the repo does not ship the files +- users or agent workflows should create the actual `AGENT.md` files ---- +## Memory and Runtime -## Common Commands +ClawGo does not treat all agents as one shared context. -```text -clawgo onboard -clawgo provider -clawgo status -clawgo agent [-m "..."] -clawgo gateway [run|start|stop|restart|status] -clawgo config set|get|check|reload -clawgo cron ... -clawgo skills ... -clawgo uninstall [--purge] [--remove-bin] -``` +- `main` + - keeps workspace-level memory and collaboration summaries +- `subagent` + - uses its own session key + - writes to its own memory namespace +- runtime store + - persists runs, events, threads, and messages ---- +This gives you: -## Build +- recoverability +- traceability +- clear execution boundaries -Local build: +## Positioning -```bash -make build -``` +ClawGo is a good fit for: -Build all targets: +- local long-running personal agents +- multi-agent workflows without heavyweight orchestration platforms +- systems that need explicit config, explicit audit trails, and strong observability -```bash -make build-all -``` - -Package outputs: - -```bash -make package-all -``` - ---- - -## Design Direction - -ClawGo currently leans toward these design choices: - -- main-agent mediation first -- persisted runtime state first -- declarative config first -- unified operations UI before more aggressive automation -- nodes modeled as controlled remote agent branches, not fully autonomous peers - -In practice, that means this project is closer to: - -- a long-running personal AI agent runtime -- with multi-agent orchestration -- with node expansion -- with operational visibility and auditability - -rather than a simple chat bot wrapper. - ---- - -## License - -See `LICENSE` in this repository. +If you want a working starting point, begin with [config.example.json](/Users/lpf/Desktop/project/clawgo/config.example.json). diff --git a/agents/coder/AGENT.md b/agents/coder/AGENT.md deleted file mode 100644 index 6fe2a22..0000000 --- a/agents/coder/AGENT.md +++ /dev/null @@ -1,19 +0,0 @@ -# Coder Agent - -## Role -You are the implementation-focused subagent. Make concrete code changes, keep them minimal and correct, and verify what you changed. - -## Priorities -- Read the relevant code before editing. -- Prefer direct fixes over speculative refactors. -- Preserve established project patterns unless the task requires a broader change. - -## Execution -- Explain assumptions briefly when they matter. -- Run targeted verification after changes when possible. -- Report changed areas, verification status, and residual risks. - -## Output Format -- Summary: what changed. -- Verification: tests, builds, or checks run. -- Risks: what remains uncertain. diff --git a/agents/main/AGENT.md b/agents/main/AGENT.md deleted file mode 100644 index 01e9a69..0000000 --- a/agents/main/AGENT.md +++ /dev/null @@ -1,19 +0,0 @@ -# Main Agent - -## Role -You are the main agent and router for this workspace. Coordinate work, choose whether to handle it directly or dispatch to a subagent, and keep the overall execution coherent. - -## Responsibilities -- Interpret the user's goal and decide the next concrete step. -- Route implementation, testing, or research tasks to the right subagent when delegation is useful. -- Keep control flow main-mediated by default. -- Review subagent results before replying to the user. - -## Subagent Management -- When creating a new subagent, update config and create the matching `agents//AGENT.md` in the same task. -- Treat `system_prompt_file` as the primary prompt source for configured subagents. -- Do not leave newly created agents with only a one-line inline prompt. - -## Output Style -- Be concise. -- Report decisions, outcomes, and remaining risks clearly. diff --git a/agents/tester/AGENT.md b/agents/tester/AGENT.md deleted file mode 100644 index 0185c28..0000000 --- a/agents/tester/AGENT.md +++ /dev/null @@ -1,19 +0,0 @@ -# Tester Agent - -## Role -You are the verification-focused subagent. Validate behavior, look for regressions, and report evidence clearly. - -## Priorities -- Prefer reproducible checks over opinion. -- Focus on behavioral regressions, missing coverage, and unclear assumptions. -- Escalate the most important failures first. - -## Execution -- Use the smallest set of checks that can prove or disprove the target behavior. -- Distinguish confirmed failures from unverified risk. -- If you cannot run a check, say so explicitly. - -## Output Format -- Findings: concrete issues or confirmation that none were found. -- Verification: commands or scenarios checked. -- Gaps: what was not covered. diff --git a/cmd/clawgo/cmd_gateway.go b/cmd/clawgo/cmd_gateway.go index d54ab4f..647f082 100644 --- a/cmd/clawgo/cmd_gateway.go +++ b/cmd/clawgo/cmd_gateway.go @@ -160,9 +160,6 @@ func gatewayCmd() { registryServer.SetSubagentHandler(func(cctx context.Context, action string, args map[string]interface{}) (interface{}, error) { return agentLoop.HandleSubagentRuntime(cctx, action, args) }) - registryServer.SetPipelineHandler(func(cctx context.Context, action string, args map[string]interface{}) (interface{}, error) { - return agentLoop.HandlePipelineRuntime(cctx, action, args) - }) registryServer.SetCronHandler(func(action string, args map[string]interface{}) (interface{}, error) { getStr := func(k string) string { v, _ := args[k].(string) diff --git a/docs/assets/readme-agents.png b/docs/assets/readme-agents.png new file mode 100644 index 0000000..ecdb505 Binary files /dev/null and b/docs/assets/readme-agents.png differ diff --git a/docs/assets/readme-config.png b/docs/assets/readme-config.png new file mode 100644 index 0000000..d4013dc Binary files /dev/null and b/docs/assets/readme-config.png differ diff --git a/docs/assets/readme-dashboard.png b/docs/assets/readme-dashboard.png new file mode 100644 index 0000000..0b40de7 Binary files /dev/null and b/docs/assets/readme-dashboard.png differ diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 2d2eb02..0f653c7 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -61,7 +61,6 @@ type AgentLoop struct { streamMu sync.Mutex sessionStreamed map[string]bool subagentManager *tools.SubagentManager - orchestrator *tools.Orchestrator subagentRouter *tools.SubagentRouter subagentConfigTool *tools.SubagentConfigTool nodeRouter *nodes.Router @@ -184,8 +183,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers toolsRegistry.Register(messageTool) // Register spawn tool - orchestrator := tools.NewOrchestrator() - subagentManager := tools.NewSubagentManager(provider, workspace, msgBus, orchestrator) + subagentManager := tools.NewSubagentManager(provider, workspace, msgBus) subagentRouter := tools.NewSubagentRouter(subagentManager) subagentConfigTool := tools.NewSubagentConfigTool("") spawnTool := tools.NewSpawnTool(subagentManager) @@ -195,10 +193,6 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers if store := subagentManager.ProfileStore(); store != nil { toolsRegistry.Register(tools.NewSubagentProfileTool(store)) } - toolsRegistry.Register(tools.NewPipelineCreateTool(orchestrator)) - toolsRegistry.Register(tools.NewPipelineStatusTool(orchestrator)) - toolsRegistry.Register(tools.NewPipelineStateSetTool(orchestrator)) - toolsRegistry.Register(tools.NewPipelineDispatchTool(orchestrator, subagentManager)) toolsRegistry.Register(tools.NewSessionsTool( func(limit int) []tools.SessionInfo { sessions := alSessionListForTool(sessionsManager, limit) @@ -257,7 +251,6 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers providerResponses: map[string]config.ProviderResponsesConfig{}, telegramStreaming: cfg.Channels.Telegram.Streaming, subagentManager: subagentManager, - orchestrator: orchestrator, subagentRouter: subagentRouter, subagentConfigTool: subagentConfigTool, nodeRouter: nodesRouter, @@ -1748,7 +1741,7 @@ func withToolContextArgs(toolName string, args map[string]interface{}, channel, return args } switch toolName { - case "message", "spawn", "remind", "pipeline_create", "pipeline_dispatch": + case "message", "spawn", "remind": default: return args } diff --git a/pkg/agent/loop_allowlist_test.go b/pkg/agent/loop_allowlist_test.go index ee5e174..318f59f 100644 --- a/pkg/agent/loop_allowlist_test.go +++ b/pkg/agent/loop_allowlist_test.go @@ -52,13 +52,3 @@ func TestEnsureToolAllowedByContext_GroupAllowlist(t *testing.T) { t.Fatalf("expected files_read group to block write_file") } } - -func TestEnsureToolAllowedByContext_GroupAliasToken(t *testing.T) { - ctx := withToolAllowlistContext(context.Background(), []string{"@pipeline"}) - if err := ensureToolAllowedByContext(ctx, "pipeline_status", map[string]interface{}{}); err != nil { - t.Fatalf("expected @pipeline to allow pipeline_status, got: %v", err) - } - if err := ensureToolAllowedByContext(ctx, "memory_search", map[string]interface{}{}); err == nil { - t.Fatalf("expected @pipeline to block memory_search") - } -} diff --git a/pkg/agent/router_dispatch_test.go b/pkg/agent/router_dispatch_test.go index 12c7539..a14ae83 100644 --- a/pkg/agent/router_dispatch_test.go +++ b/pkg/agent/router_dispatch_test.go @@ -44,7 +44,7 @@ func TestMaybeAutoRouteDispatchesExplicitAgentMention(t *testing.T) { t.Cleanup(func() { runtimecfg.Set(config.DefaultConfig()) }) workspace := t.TempDir() - manager := tools.NewSubagentManager(nil, workspace, nil, nil) + manager := tools.NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *tools.SubagentTask) (string, error) { return "auto-routed", nil }) @@ -101,7 +101,7 @@ func TestMaybeAutoRouteDispatchesRulesFirstMatch(t *testing.T) { t.Cleanup(func() { runtimecfg.Set(config.DefaultConfig()) }) workspace := t.TempDir() - manager := tools.NewSubagentManager(nil, workspace, nil, nil) + manager := tools.NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *tools.SubagentTask) (string, error) { return "tested", nil }) diff --git a/pkg/agent/runtime_admin.go b/pkg/agent/runtime_admin.go index 234188a..5c7eb33 100644 --- a/pkg/agent/runtime_admin.go +++ b/pkg/agent/runtime_admin.go @@ -65,8 +65,6 @@ func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, a MaxResultChars: runtimeIntArg(args, "max_result_chars", 0), OriginChannel: fallbackString(runtimeStringArg(args, "channel"), "webui"), OriginChatID: fallbackString(runtimeStringArg(args, "chat_id"), "webui"), - PipelineID: runtimeStringArg(args, "pipeline_id"), - PipelineTask: runtimeStringArg(args, "task_id"), }) if err != nil { return nil, err @@ -388,89 +386,6 @@ func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, a } } -func (al *AgentLoop) HandlePipelineRuntime(ctx context.Context, action string, args map[string]interface{}) (interface{}, error) { - if al == nil || al.orchestrator == nil { - return nil, fmt.Errorf("pipeline runtime is not configured") - } - action = strings.ToLower(strings.TrimSpace(action)) - if action == "" { - action = "list" - } - - switch action { - case "list": - return map[string]interface{}{"items": al.orchestrator.ListPipelines()}, nil - case "get", "status": - pipelineID := fallbackString(runtimeStringArg(args, "pipeline_id"), runtimeStringArg(args, "id")) - if strings.TrimSpace(pipelineID) == "" { - return nil, fmt.Errorf("pipeline_id is required") - } - p, ok := al.orchestrator.GetPipeline(strings.TrimSpace(pipelineID)) - if !ok { - return map[string]interface{}{"found": false}, nil - } - return map[string]interface{}{"found": true, "pipeline": p}, nil - case "ready": - pipelineID := fallbackString(runtimeStringArg(args, "pipeline_id"), runtimeStringArg(args, "id")) - if strings.TrimSpace(pipelineID) == "" { - return nil, fmt.Errorf("pipeline_id is required") - } - items, err := al.orchestrator.ReadyTasks(strings.TrimSpace(pipelineID)) - if err != nil { - return nil, err - } - return map[string]interface{}{"items": items}, nil - case "create": - objective := runtimeStringArg(args, "objective") - if objective == "" { - return nil, fmt.Errorf("objective is required") - } - specs, err := parsePipelineSpecsForRuntime(args["tasks"]) - if err != nil { - return nil, err - } - label := runtimeStringArg(args, "label") - p, err := al.orchestrator.CreatePipeline(label, objective, "webui", "webui", specs) - if err != nil { - return nil, err - } - return map[string]interface{}{"pipeline": p}, nil - case "state_set": - pipelineID := fallbackString(runtimeStringArg(args, "pipeline_id"), runtimeStringArg(args, "id")) - key := runtimeStringArg(args, "key") - if strings.TrimSpace(pipelineID) == "" || strings.TrimSpace(key) == "" { - return nil, fmt.Errorf("pipeline_id and key are required") - } - value, ok := args["value"] - if !ok { - return nil, fmt.Errorf("value is required") - } - if err := al.orchestrator.SetSharedState(strings.TrimSpace(pipelineID), strings.TrimSpace(key), value); err != nil { - return nil, err - } - p, _ := al.orchestrator.GetPipeline(strings.TrimSpace(pipelineID)) - return map[string]interface{}{"ok": true, "pipeline": p}, nil - case "dispatch": - pipelineID := fallbackString(runtimeStringArg(args, "pipeline_id"), runtimeStringArg(args, "id")) - if strings.TrimSpace(pipelineID) == "" { - return nil, fmt.Errorf("pipeline_id is required") - } - maxDispatch := runtimeIntArg(args, "max_dispatch", 3) - dispatchTool := tools.NewPipelineDispatchTool(al.orchestrator, al.subagentManager) - result, err := dispatchTool.Execute(ctx, map[string]interface{}{ - "pipeline_id": strings.TrimSpace(pipelineID), - "max_dispatch": float64(maxDispatch), - }) - if err != nil { - return nil, err - } - p, _ := al.orchestrator.GetPipeline(strings.TrimSpace(pipelineID)) - return map[string]interface{}{"message": result, "pipeline": p}, nil - default: - return nil, fmt.Errorf("unsupported action: %s", action) - } -} - func cloneSubagentTask(in *tools.SubagentTask) *tools.SubagentTask { if in == nil { return nil @@ -514,46 +429,6 @@ func resolveSubagentTaskIDForRuntime(sm *tools.SubagentManager, raw string) (str return tasks[idx-1].ID, nil } -func parsePipelineSpecsForRuntime(raw interface{}) ([]tools.PipelineSpec, error) { - items, ok := raw.([]interface{}) - if !ok || len(items) == 0 { - return nil, fmt.Errorf("tasks is required") - } - specs := make([]tools.PipelineSpec, 0, len(items)) - for i, item := range items { - m, ok := item.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("tasks[%d] must be object", i) - } - id := runtimeStringArg(m, "id") - if id == "" { - return nil, fmt.Errorf("tasks[%d].id is required", i) - } - goal := runtimeStringArg(m, "goal") - if goal == "" { - return nil, fmt.Errorf("tasks[%d].goal is required", i) - } - spec := tools.PipelineSpec{ - ID: id, - Role: runtimeStringArg(m, "role"), - Goal: goal, - } - if deps, ok := m["depends_on"].([]interface{}); ok { - spec.DependsOn = make([]string, 0, len(deps)) - for _, dep := range deps { - d, _ := dep.(string) - d = strings.TrimSpace(d) - if d == "" { - continue - } - spec.DependsOn = append(spec.DependsOn, d) - } - } - specs = append(specs, spec) - } - return specs, nil -} - func runtimeStringArg(args map[string]interface{}, key string) string { if args == nil { return "" diff --git a/pkg/agent/runtime_admin_test.go b/pkg/agent/runtime_admin_test.go index 840f54e..c59893b 100644 --- a/pkg/agent/runtime_admin_test.go +++ b/pkg/agent/runtime_admin_test.go @@ -13,7 +13,7 @@ import ( func TestHandleSubagentRuntimeDispatchAndWait(t *testing.T) { workspace := t.TempDir() - manager := tools.NewSubagentManager(nil, workspace, nil, nil) + manager := tools.NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *tools.SubagentTask) (string, error) { return "runtime-admin-result", nil }) @@ -66,7 +66,7 @@ func TestHandleSubagentRuntimeUpsertConfigSubagent(t *testing.T) { runtimecfg.Set(cfg) t.Cleanup(func() { runtimecfg.Set(config.DefaultConfig()) }) - manager := tools.NewSubagentManager(nil, workspace, nil, nil) + manager := tools.NewSubagentManager(nil, workspace, nil) loop := &AgentLoop{ configPath: configPath, subagentManager: manager, @@ -138,7 +138,7 @@ func TestHandleSubagentRuntimeRegistryAndToggleEnabled(t *testing.T) { runtimecfg.Set(cfg) t.Cleanup(func() { runtimecfg.Set(config.DefaultConfig()) }) - manager := tools.NewSubagentManager(nil, workspace, nil, nil) + manager := tools.NewSubagentManager(nil, workspace, nil) loop := &AgentLoop{ configPath: configPath, subagentManager: manager, @@ -197,7 +197,7 @@ func TestHandleSubagentRuntimeDeleteConfigSubagent(t *testing.T) { runtimecfg.Set(cfg) t.Cleanup(func() { runtimecfg.Set(config.DefaultConfig()) }) - manager := tools.NewSubagentManager(nil, workspace, nil, nil) + manager := tools.NewSubagentManager(nil, workspace, nil) loop := &AgentLoop{ configPath: configPath, subagentManager: manager, @@ -225,7 +225,7 @@ func TestHandleSubagentRuntimeDeleteConfigSubagent(t *testing.T) { func TestHandleSubagentRuntimePromptFileGetSetBootstrap(t *testing.T) { workspace := t.TempDir() - manager := tools.NewSubagentManager(nil, workspace, nil, nil) + manager := tools.NewSubagentManager(nil, workspace, nil) loop := &AgentLoop{ workspace: workspace, subagentManager: manager, @@ -297,7 +297,7 @@ func TestHandleSubagentRuntimeProtectsMainAgent(t *testing.T) { runtimecfg.Set(cfg) t.Cleanup(func() { runtimecfg.Set(config.DefaultConfig()) }) - manager := tools.NewSubagentManager(nil, workspace, nil, nil) + manager := tools.NewSubagentManager(nil, workspace, nil) loop := &AgentLoop{ configPath: configPath, workspace: workspace, diff --git a/pkg/api/server.go b/pkg/api/server.go index 5bf48e5..2d2b8eb 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -44,7 +44,6 @@ type Server struct { onConfigAfter func() onCron func(action string, args map[string]interface{}) (interface{}, error) onSubagents func(ctx context.Context, action string, args map[string]interface{}) (interface{}, error) - onPipelines func(ctx context.Context, action string, args map[string]interface{}) (interface{}, error) webUIDir string ekgCacheMu sync.Mutex ekgCachePath string @@ -80,9 +79,6 @@ func (s *Server) SetCronHandler(fn func(action string, args map[string]interface func (s *Server) SetSubagentHandler(fn func(ctx context.Context, action string, args map[string]interface{}) (interface{}, error)) { s.onSubagents = fn } -func (s *Server) SetPipelineHandler(fn func(ctx context.Context, action string, args map[string]interface{}) (interface{}, error)) { - s.onPipelines = fn -} func (s *Server) SetWebUIDir(dir string) { s.webUIDir = strings.TrimSpace(dir) } func (s *Server) Start(ctx context.Context) error { @@ -111,7 +107,6 @@ func (s *Server) Start(ctx context.Context) error { mux.HandleFunc("/webui/api/memory", s.handleWebUIMemory) mux.HandleFunc("/webui/api/subagent_profiles", s.handleWebUISubagentProfiles) mux.HandleFunc("/webui/api/subagents_runtime", s.handleWebUISubagentsRuntime) - mux.HandleFunc("/webui/api/pipelines", s.handleWebUIPipelines) mux.HandleFunc("/webui/api/tool_allowlist_groups", s.handleWebUIToolAllowlistGroups) mux.HandleFunc("/webui/api/task_audit", s.handleWebUITaskAudit) mux.HandleFunc("/webui/api/task_queue", s.handleWebUITaskQueue) @@ -2333,58 +2328,6 @@ func (s *Server) handleWebUISubagentsRuntime(w http.ResponseWriter, r *http.Requ _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "result": result}) } -func (s *Server) handleWebUIPipelines(w http.ResponseWriter, r *http.Request) { - if !s.checkAuth(r) { - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - if s.onPipelines == nil { - http.Error(w, "pipeline runtime handler not configured", http.StatusServiceUnavailable) - return - } - - action := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("action"))) - args := map[string]interface{}{} - switch r.Method { - case http.MethodGet: - if action == "" { - action = "list" - } - for key, values := range r.URL.Query() { - if key == "action" || key == "token" || len(values) == 0 { - continue - } - args[key] = strings.TrimSpace(values[0]) - } - case http.MethodPost: - var body map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - http.Error(w, "invalid json", http.StatusBadRequest) - return - } - if body == nil { - body = map[string]interface{}{} - } - if action == "" { - if raw, _ := body["action"].(string); raw != "" { - action = strings.ToLower(strings.TrimSpace(raw)) - } - } - delete(body, "action") - args = body - default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - result, err := s.onPipelines(r.Context(), action, args) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "result": result}) -} - func (s *Server) handleWebUIMemory(w http.ResponseWriter, r *http.Request) { if !s.checkAuth(r) { http.Error(w, "unauthorized", http.StatusUnauthorized) diff --git a/pkg/tools/spawn.go b/pkg/tools/spawn.go index 363dd5c..5303167 100644 --- a/pkg/tools/spawn.go +++ b/pkg/tools/spawn.go @@ -69,14 +69,6 @@ func (t *SpawnTool) Parameters() map[string]interface{} { "type": "integer", "description": "Optional result size quota in characters.", }, - "pipeline_id": map[string]interface{}{ - "type": "string", - "description": "Optional pipeline ID for orchestrated multi-agent workflow", - }, - "task_id": map[string]interface{}{ - "type": "string", - "description": "Optional task ID under the pipeline", - }, "channel": map[string]interface{}{ "type": "string", "description": "Optional origin channel override", @@ -111,8 +103,6 @@ func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) (s timeoutSec := intArg(args, "timeout_sec") maxTaskChars := intArg(args, "max_task_chars") maxResultChars := intArg(args, "max_result_chars") - pipelineID, _ := args["pipeline_id"].(string) - taskID, _ := args["task_id"].(string) if label == "" && role != "" { label = role } else if label == "" && agentID != "" { @@ -150,8 +140,6 @@ func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) (s MaxResultChars: maxResultChars, OriginChannel: originChannel, OriginChatID: originChatID, - PipelineID: pipelineID, - PipelineTask: taskID, }) if err != nil { return "", fmt.Errorf("failed to spawn subagent: %w", err) diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index e07c5c3..9347e4f 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -33,8 +33,6 @@ type SubagentTask struct { MaxTaskChars int `json:"max_task_chars,omitempty"` MaxResultChars int `json:"max_result_chars,omitempty"` RetryCount int `json:"retry_count,omitempty"` - PipelineID string `json:"pipeline_id,omitempty"` - PipelineTask string `json:"pipeline_task,omitempty"` ThreadID string `json:"thread_id,omitempty"` CorrelationID string `json:"correlation_id,omitempty"` ParentRunID string `json:"parent_run_id,omitempty"` @@ -57,7 +55,6 @@ type SubagentManager struct { mu sync.RWMutex provider providers.LLMProvider bus *bus.MessageBus - orc *Orchestrator workspace string nextID int runFunc SubagentRunFunc @@ -78,14 +75,12 @@ type SubagentSpawnOptions struct { MaxResultChars int OriginChannel string OriginChatID string - PipelineID string - PipelineTask string ThreadID string CorrelationID string ParentRunID string } -func NewSubagentManager(provider providers.LLMProvider, workspace string, bus *bus.MessageBus, orc *Orchestrator) *SubagentManager { +func NewSubagentManager(provider providers.LLMProvider, workspace string, bus *bus.MessageBus) *SubagentManager { store := NewSubagentProfileStore(workspace) runStore := NewSubagentRunStore(workspace) mailboxStore := NewAgentMailboxStore(workspace) @@ -95,7 +90,6 @@ func NewSubagentManager(provider providers.LLMProvider, workspace string, bus *b archiveAfterMinute: 60, provider: provider, bus: bus, - orc: orc, workspace: workspace, nextID: 1, profileStore: store, @@ -123,9 +117,6 @@ func (sm *SubagentManager) Spawn(ctx context.Context, opts SubagentSpawnOptions) if task.Role != "" { desc += fmt.Sprintf(" role=%s", task.Role) } - if task.PipelineID != "" && task.PipelineTask != "" { - desc += fmt.Sprintf(" (pipeline=%s task=%s)", task.PipelineID, task.PipelineTask) - } return desc, nil } @@ -240,8 +231,6 @@ func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOpti } originChannel := strings.TrimSpace(opts.OriginChannel) originChatID := strings.TrimSpace(opts.OriginChatID) - pipelineID := strings.TrimSpace(opts.PipelineID) - pipelineTask := strings.TrimSpace(opts.PipelineTask) threadID := strings.TrimSpace(opts.ThreadID) correlationID := strings.TrimSpace(opts.CorrelationID) parentRunID := strings.TrimSpace(opts.ParentRunID) @@ -291,8 +280,6 @@ func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOpti MaxTaskChars: maxTaskChars, MaxResultChars: maxResultChars, RetryCount: 0, - PipelineID: pipelineID, - PipelineTask: pipelineTask, ThreadID: threadID, CorrelationID: correlationID, ParentRunID: parentRunID, @@ -336,9 +323,6 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) { sm.persistTaskLocked(task, "started", "") sm.mu.Unlock() - if sm.orc != nil && task.PipelineID != "" && task.PipelineTask != "" { - _ = sm.orc.MarkTaskRunning(task.PipelineID, task.PipelineTask) - } result, runErr := sm.runWithRetry(ctx, task) sm.mu.Lock() if runErr != nil { @@ -359,9 +343,6 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) { CreatedAt: task.Updated, }) sm.persistTaskLocked(task, "completed", task.Result) - if sm.orc != nil && task.PipelineID != "" && task.PipelineTask != "" { - _ = sm.orc.MarkTaskDone(task.PipelineID, task.PipelineTask, task.Result, runErr) - } } else { task.Status = "completed" task.Result = applySubagentResultQuota(result, task.MaxResultChars) @@ -379,9 +360,6 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) { CreatedAt: task.Updated, }) sm.persistTaskLocked(task, "completed", task.Result) - if sm.orc != nil && task.PipelineID != "" && task.PipelineTask != "" { - _ = sm.orc.MarkTaskDone(task.PipelineID, task.PipelineTask, task.Result, nil) - } } sm.mu.Unlock() @@ -399,9 +377,6 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) { } } announceContent := fmt.Sprintf("%s.\n\nResult:\n%s", prefix, task.Result) - if task.PipelineID != "" && task.PipelineTask != "" { - announceContent += fmt.Sprintf("\n\nPipeline: %s\nPipeline Task: %s", task.PipelineID, task.PipelineTask) - } sm.bus.PublishInbound(bus.InboundMessage{ Channel: "system", SenderID: fmt.Sprintf("subagent:%s", task.ID), @@ -409,17 +384,15 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) { SessionKey: task.SessionKey, Content: announceContent, Metadata: map[string]string{ - "trigger": "subagent", - "subagent_id": task.ID, - "agent_id": task.AgentID, - "role": task.Role, - "session_key": task.SessionKey, - "memory_ns": task.MemoryNS, - "retry_count": fmt.Sprintf("%d", task.RetryCount), - "timeout_sec": fmt.Sprintf("%d", task.TimeoutSec), - "pipeline_id": task.PipelineID, - "pipeline_task": task.PipelineTask, - "status": task.Status, + "trigger": "subagent", + "subagent_id": task.ID, + "agent_id": task.AgentID, + "role": task.Role, + "session_key": task.SessionKey, + "memory_ns": task.MemoryNS, + "retry_count": fmt.Sprintf("%d", task.RetryCount), + "timeout_sec": fmt.Sprintf("%d", task.TimeoutSec), + "status": task.Status, }, }) } @@ -692,8 +665,6 @@ func (sm *SubagentManager) ResumeTask(ctx context.Context, taskID string) (strin MaxResultChars: t.MaxResultChars, OriginChannel: t.OriginChannel, OriginChatID: t.OriginChatID, - PipelineID: t.PipelineID, - PipelineTask: t.PipelineTask, ThreadID: t.ThreadID, CorrelationID: t.CorrelationID, ParentRunID: t.ID, diff --git a/pkg/tools/subagent_profile.go b/pkg/tools/subagent_profile.go index 97ad42a..17fd4fd 100644 --- a/pkg/tools/subagent_profile.go +++ b/pkg/tools/subagent_profile.go @@ -382,6 +382,9 @@ func (s *SubagentProfileStore) nodeProfileLocked(agentID string) (SubagentProfil } } for _, node := range nodes.DefaultManager().List() { + if isLocalNode(node.ID) { + continue + } profile := profileFromNode(node, parentAgentID) if profile.AgentID == id { return profile, true @@ -430,6 +433,9 @@ func (s *SubagentProfileStore) nodeProfilesLocked() []SubagentProfile { } out := make([]SubagentProfile, 0, len(nodeItems)) for _, node := range nodeItems { + if isLocalNode(node.ID) { + continue + } profile := profileFromNode(node, parentAgentID) if profile.AgentID == "" { continue @@ -473,6 +479,10 @@ func nodeBranchAgentID(nodeID string) string { return "node." + id + ".main" } +func isLocalNode(nodeID string) bool { + return normalizeSubagentIdentifier(nodeID) == "local" +} + type SubagentProfileTool struct { store *SubagentProfileStore } @@ -504,7 +514,7 @@ func (t *SubagentProfileTool) Parameters() map[string]interface{} { "status": map[string]interface{}{"type": "string", "description": "active|disabled"}, "tool_allowlist": map[string]interface{}{ "type": "array", - "description": "Tool allowlist entries. Supports tool names, '*'/'all', and grouped tokens like 'group:files_read' or '@pipeline'.", + "description": "Tool allowlist entries. Supports tool names, '*'/'all', and grouped tokens like 'group:files_read'.", "items": map[string]interface{}{"type": "string"}, }, "max_retries": map[string]interface{}{"type": "integer", "description": "Retry limit for subagent task execution."}, diff --git a/pkg/tools/subagent_profile_test.go b/pkg/tools/subagent_profile_test.go index 489bc9b..e45cfa1 100644 --- a/pkg/tools/subagent_profile_test.go +++ b/pkg/tools/subagent_profile_test.go @@ -46,7 +46,7 @@ func TestSubagentProfileStoreNormalization(t *testing.T) { func TestSubagentManagerSpawnRejectsDisabledProfile(t *testing.T) { workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { return "ok", nil }) @@ -74,7 +74,7 @@ func TestSubagentManagerSpawnRejectsDisabledProfile(t *testing.T) { func TestSubagentManagerSpawnResolvesProfileByRole(t *testing.T) { workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) store := manager.ProfileStore() if store == nil { t.Fatalf("expected profile store to be available") @@ -234,3 +234,44 @@ func TestSubagentProfileStoreIncludesNodeMainBranchProfiles(t *testing.T) { t.Fatalf("expected node-managed delete to fail") } } + +func TestSubagentProfileStoreExcludesLocalNodeMainBranchProfile(t *testing.T) { + runtimecfg.Set(config.DefaultConfig()) + t.Cleanup(func() { + runtimecfg.Set(config.DefaultConfig()) + nodes.DefaultManager().Remove("local") + }) + + cfg := config.DefaultConfig() + cfg.Agents.Router.Enabled = true + cfg.Agents.Router.MainAgentID = "main" + cfg.Agents.Subagents["main"] = config.SubagentConfig{ + Enabled: true, + Type: "router", + SystemPromptFile: "agents/main/AGENT.md", + } + runtimecfg.Set(cfg) + + nodes.DefaultManager().Upsert(nodes.NodeInfo{ + ID: "local", + Name: "Local", + Online: true, + }) + + store := NewSubagentProfileStore(t.TempDir()) + if profile, ok, err := store.Get(nodeBranchAgentID("local")); err != nil { + t.Fatalf("get failed: %v", err) + } else if ok { + t.Fatalf("expected local node branch profile to be excluded, got %+v", profile) + } + + items, err := store.List() + if err != nil { + t.Fatalf("list failed: %v", err) + } + for _, item := range items { + if item.AgentID == nodeBranchAgentID("local") { + t.Fatalf("local node branch profile should not appear in list") + } + } +} diff --git a/pkg/tools/subagent_router_test.go b/pkg/tools/subagent_router_test.go index 47c3a23..5f7f757 100644 --- a/pkg/tools/subagent_router_test.go +++ b/pkg/tools/subagent_router_test.go @@ -9,7 +9,7 @@ import ( func TestSubagentRouterDispatchAndWaitReply(t *testing.T) { workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { return "router-result", nil }) diff --git a/pkg/tools/subagent_runtime_control_test.go b/pkg/tools/subagent_runtime_control_test.go index efa4fbb..6a0e47f 100644 --- a/pkg/tools/subagent_runtime_control_test.go +++ b/pkg/tools/subagent_runtime_control_test.go @@ -17,7 +17,7 @@ func TestSubagentSpawnEnforcesTaskQuota(t *testing.T) { t.Parallel() workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { return "ok", nil }) @@ -45,7 +45,7 @@ func TestSubagentSpawnEnforcesTaskQuota(t *testing.T) { func TestSubagentRunWithRetryEventuallySucceeds(t *testing.T) { workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) attempts := 0 manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { attempts++ @@ -81,7 +81,7 @@ func TestSubagentRunWithRetryEventuallySucceeds(t *testing.T) { func TestSubagentRunWithTimeoutFails(t *testing.T) { workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { select { case <-ctx.Done(): @@ -116,7 +116,7 @@ func TestSubagentBroadcastIncludesFailureStatus(t *testing.T) { msgBus := bus.NewMessageBus() defer msgBus.Close() - manager := NewSubagentManager(nil, workspace, msgBus, nil) + manager := NewSubagentManager(nil, workspace, msgBus) manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { return "", errors.New("boom") }) @@ -152,7 +152,7 @@ func TestSubagentBroadcastIncludesFailureStatus(t *testing.T) { func TestSubagentManagerRestoresPersistedRuns(t *testing.T) { workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { return "persisted", nil }) @@ -172,7 +172,7 @@ func TestSubagentManagerRestoresPersistedRuns(t *testing.T) { t.Fatalf("expected completed task, got %s", task.Status) } - reloaded := NewSubagentManager(nil, workspace, nil, nil) + reloaded := NewSubagentManager(nil, workspace, nil) got, ok := reloaded.GetTask(task.ID) if !ok { t.Fatalf("expected persisted task to reload") @@ -207,7 +207,7 @@ func TestSubagentManagerRestoresPersistedRuns(t *testing.T) { func TestSubagentManagerPersistsEvents(t *testing.T) { workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { time.Sleep(100 * time.Millisecond) return "ok", nil @@ -249,7 +249,7 @@ func TestSubagentManagerPersistsEvents(t *testing.T) { func TestSubagentMailboxStoresThreadAndReplies(t *testing.T) { workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { return "done", nil }) @@ -294,7 +294,7 @@ func TestSubagentMailboxStoresThreadAndReplies(t *testing.T) { func TestSubagentMailboxInboxIncludesControlMessages(t *testing.T) { workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { time.Sleep(150 * time.Millisecond) return "ok", nil @@ -336,7 +336,7 @@ func TestSubagentMailboxInboxIncludesControlMessages(t *testing.T) { func TestSubagentMailboxReplyAndAckFlow(t *testing.T) { workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { time.Sleep(150 * time.Millisecond) return "ok", nil @@ -405,7 +405,7 @@ func TestSubagentMailboxReplyAndAckFlow(t *testing.T) { func TestSubagentResumeConsumesQueuedThreadInbox(t *testing.T) { workspace := t.TempDir() - manager := NewSubagentManager(nil, workspace, nil, nil) + manager := NewSubagentManager(nil, workspace, nil) observedQueued := make(chan int, 4) manager.SetRunFunc(func(ctx context.Context, task *SubagentTask) (string, error) { inbox, err := manager.TaskInbox(task.ID, 10) @@ -507,7 +507,7 @@ func TestSubagentUsesConfiguredSystemPromptFile(t *testing.T) { t.Fatalf("write coder AGENT failed: %v", err) } provider := &captureProvider{} - manager := NewSubagentManager(provider, workspace, nil, nil) + manager := NewSubagentManager(provider, workspace, nil) if _, err := manager.ProfileStore().Upsert(SubagentProfile{ AgentID: "coder", Status: "active", diff --git a/pkg/tools/tool_allowlist_groups.go b/pkg/tools/tool_allowlist_groups.go index 16850b5..dddb425 100644 --- a/pkg/tools/tool_allowlist_groups.go +++ b/pkg/tools/tool_allowlist_groups.go @@ -43,12 +43,6 @@ var defaultToolAllowlistGroups = []ToolAllowlistGroup{ Aliases: []string{"memory"}, Tools: []string{"memory_search", "memory_get", "memory_write"}, }, - { - Name: "pipeline", - Description: "Pipeline orchestration tools", - Aliases: []string{"pipelines"}, - Tools: []string{"pipeline_create", "pipeline_status", "pipeline_state_set", "pipeline_dispatch"}, - }, { Name: "subagents", Description: "Subagent management tools", diff --git a/pkg/tools/tool_allowlist_groups_test.go b/pkg/tools/tool_allowlist_groups_test.go index f66a686..f2b7212 100644 --- a/pkg/tools/tool_allowlist_groups_test.go +++ b/pkg/tools/tool_allowlist_groups_test.go @@ -17,7 +17,7 @@ func TestExpandToolAllowlistEntries_GroupPrefix(t *testing.T) { } func TestExpandToolAllowlistEntries_BareGroupAndAlias(t *testing.T) { - got := ExpandToolAllowlistEntries([]string{"memory_all", "@pipeline"}) + got := ExpandToolAllowlistEntries([]string{"memory_all", "@subagents"}) contains := map[string]bool{} for _, item := range got { contains[item] = true @@ -25,7 +25,7 @@ func TestExpandToolAllowlistEntries_BareGroupAndAlias(t *testing.T) { if !contains["memory_search"] || !contains["memory_write"] { t.Fatalf("memory_all expansion missing memory tools: %v", got) } - if !contains["pipeline_dispatch"] || !contains["pipeline_status"] { - t.Fatalf("pipeline alias expansion missing pipeline tools: %v", got) + if !contains["spawn"] || !contains["subagents"] || !contains["subagent_profile"] { + t.Fatalf("subagents alias expansion missing subagent tools: %v", got) } } diff --git a/webui/README.md b/webui/README.md index a0b60bd..66a0f7f 100644 --- a/webui/README.md +++ b/webui/README.md @@ -1,54 +1,85 @@ -# ClawGo WebUI +# ClawGo WebUI ✨ -ClawGo 的前端控制台,基于 **React + Vite**。 +ClawGo WebUI 是项目的前端控制台,基于 React 19 + Vite 6,服务于网关模式下的统一运维与操作。 -## 功能简介 +## 功能范围 -- Dashboard(状态看板) -- Chat(对话与流式回复) -- Logs(实时日志 / Raw) -- Skills(技能列表、安装、管理) -- Config(配置编辑与热更新) -- Cron(定时任务管理) -- Nodes(节点状态与管理) -- Memory(记忆文件管理) +- 🗺️ `Agents` + - 统一 agent 拓扑 + - 本地 subagent 与远端 branch 展示 + - 悬浮查看状态与运行信息 +- ⚙️ `Config` + - 配置编辑 + - 热更新字段参考 +- 📜 `Logs` + - 实时日志查看 +- 🧠 `Skills` + - 技能安装、浏览、编辑 +- 🗂️ `Memory` + - 记忆文件查看与编辑 +- 🧾 `Task Audit` + - 执行链路和审计记录 -## 本地使用 +## 开发命令 -### 1) 安装依赖 +安装依赖: ```bash npm install ``` -### 2) 开发模式 +开发模式: ```bash npm run dev ``` -### 3) 构建 +构建: ```bash npm run build ``` -### 4) 预览构建产物 +预览构建产物: ```bash npm run preview ``` -## 线上访问 +类型检查: -部署后通过 gateway 访问: +```bash +npm run lint +``` + +## 运行方式 + +WebUI 通常通过 gateway 提供: ```text http://:/webui?token= ``` -例如: +本地开发时,前端开发服务由: -```text -http://134.195.210.114:18790/webui?token=xxxxx +```bash +npm run dev ``` + +启动。 + +## 技术栈 + +- React 19 +- React Router 7 +- Vite 6 +- TypeScript +- Tailwind CSS 4 +- i18next + +## 约定 + +- UI 以 gateway API 为主,不单独维护复杂业务状态源 +- 页面命名与后端能力保持一致,避免重复概念 +- `Agents` 页面展示的是统一 agent 拓扑,不再拆分独立 `Nodes` 页 +- `system_prompt_file` 编辑能力由 agent/profile 页面承载 diff --git a/webui/src/App.tsx b/webui/src/App.tsx index c3946dd..25a974d 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -15,7 +15,6 @@ import EKG from './pages/EKG'; import LogCodes from './pages/LogCodes'; import SubagentProfiles from './pages/SubagentProfiles'; import Subagents from './pages/Subagents'; -import Pipelines from './pages/Pipelines'; export default function App() { return ( @@ -36,7 +35,6 @@ export default function App() { } /> } /> } /> - } /> diff --git a/webui/src/components/RecursiveConfig.tsx b/webui/src/components/RecursiveConfig.tsx index e08ef34..493eac7 100644 --- a/webui/src/components/RecursiveConfig.tsx +++ b/webui/src/components/RecursiveConfig.tsx @@ -113,7 +113,7 @@ const RecursiveConfig: React.FC = ({ data, labels, path = if (typeof data !== 'object' || data === null) return null; return ( -
+
{Object.entries(data).map(([key, value]) => { const currentPath = path ? `${path}.${key}` : key; const label = labels[key] || key.replace(/_/g, ' '); diff --git a/webui/src/components/Sidebar.tsx b/webui/src/components/Sidebar.tsx index c5347fd..fcdb5c1 100644 --- a/webui/src/components/Sidebar.tsx +++ b/webui/src/components/Sidebar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { LayoutDashboard, MessageSquare, Settings, Clock, Terminal, Zap, FolderOpen, ClipboardList, BrainCircuit, Hash, Bot, Workflow, Boxes } from 'lucide-react'; +import { LayoutDashboard, MessageSquare, Settings, Clock, Terminal, Zap, FolderOpen, ClipboardList, BrainCircuit, Hash, Bot, Boxes } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import NavItem from './NavItem'; @@ -27,7 +27,6 @@ const Sidebar: React.FC = () => { { icon: , label: t('cronJobs'), to: '/cron' }, { icon: , label: t('memory'), to: '/memory' }, { icon: , label: t('subagentProfiles'), to: '/subagent-profiles' }, - { icon: , label: t('pipelines'), to: '/pipelines' }, ], }, { diff --git a/webui/src/components/SpaceParticles.tsx b/webui/src/components/SpaceParticles.tsx new file mode 100644 index 0000000..76fcf84 --- /dev/null +++ b/webui/src/components/SpaceParticles.tsx @@ -0,0 +1,104 @@ +import React, { useEffect, useRef } from 'react'; + +export const SpaceParticles: React.FC = () => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + let animationFrameId: number; + const particles: Array<{ + x: number; + y: number; + size: number; + speedX: number; + speedY: number; + opacity: number; + }> = []; + + const resize = () => { + const parent = canvas.parentElement; + if (parent) { + canvas.width = parent.clientWidth; + canvas.height = parent.clientHeight; + } + }; + + window.addEventListener('resize', resize); + resize(); + + // Initialize particles + const particleCount = Math.floor((canvas.width * canvas.height) / 12000); // Responsive count + for (let i = 0; i < particleCount; i++) { + particles.push({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + size: Math.random() * 1.5 + 0.5, + speedX: (Math.random() - 0.5) * 0.4, + speedY: (Math.random() - 0.5) * 0.4, + opacity: Math.random() * 0.6 + 0.1, + }); + } + + const draw = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw subtle background gradient + const gradient = ctx.createRadialGradient( + canvas.width / 2, canvas.height / 2, 0, + canvas.width / 2, canvas.height / 2, Math.max(canvas.width, canvas.height) + ); + gradient.addColorStop(0, 'rgba(30, 27, 75, 0.15)'); // Deep indigo tint + gradient.addColorStop(1, 'rgba(9, 9, 11, 0.9)'); // Zinc-950 + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + particles.forEach((p) => { + p.x += p.speedX; + p.y += p.speedY; + + if (p.x < 0) p.x = canvas.width; + if (p.x > canvas.width) p.x = 0; + if (p.y < 0) p.y = canvas.height; + if (p.y > canvas.height) p.y = 0; + + ctx.beginPath(); + ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); + ctx.fillStyle = `rgba(167, 139, 250, ${p.opacity})`; // Violet-400 tint + ctx.fill(); + }); + + // Draw connecting lines if close + ctx.lineWidth = 0.5; + for (let i = 0; i < particles.length; i++) { + for (let j = i + 1; j < particles.length; j++) { + const dx = particles[i].x - particles[j].x; + const dy = particles[i].y - particles[j].y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 120) { + ctx.beginPath(); + ctx.strokeStyle = `rgba(139, 92, 246, ${0.15 * (1 - dist / 120)})`; // Violet-500 + ctx.moveTo(particles[i].x, particles[i].y); + ctx.lineTo(particles[j].x, particles[j].y); + ctx.stroke(); + } + } + } + + animationFrameId = requestAnimationFrame(draw); + }; + + draw(); + + return () => { + window.removeEventListener('resize', resize); + cancelAnimationFrame(animationFrameId); + }; + }, []); + + return ; +}; diff --git a/webui/src/i18n/index.ts b/webui/src/i18n/index.ts index 04dfbe0..b0b65f3 100644 --- a/webui/src/i18n/index.ts +++ b/webui/src/i18n/index.ts @@ -26,6 +26,9 @@ const resources = { agentTopologyHint: 'Unified graph for local agents, registered nodes, and mirrored remote agent branches.', runningTasks: 'running', clearFocus: 'Clear Focus', + zoomIn: 'Zoom In', + zoomOut: 'Zoom Out', + fitView: 'Fit View', childrenCount: 'children', 'topologyFilter.all': 'All', 'topologyFilter.running': 'Running', @@ -63,9 +66,6 @@ const resources = { reply: 'Reply', ack: 'Ack', steerMessage: 'Steering message', - pipelines: 'Pipelines', - pipelineDetail: 'Pipeline Detail', - createPipeline: 'Create Pipeline', newProfile: 'New Profile', spawn: 'Spawn', kill: 'Kill', @@ -481,6 +481,9 @@ const resources = { agentTopologyHint: '统一展示本地 agent、注册 node 以及远端镜像 agent 分支的关系图。', runningTasks: '运行中', clearFocus: '清除聚焦', + zoomIn: '放大', + zoomOut: '缩小', + fitView: '适应视图', childrenCount: '子节点', 'topologyFilter.all': '全部', 'topologyFilter.running': '运行中', @@ -518,9 +521,6 @@ const resources = { reply: '回复', ack: '确认', steerMessage: '引导消息', - pipelines: '流水线', - pipelineDetail: '流水线详情', - createPipeline: '创建流水线', newProfile: '新建档案', spawn: '创建', kill: '终止', diff --git a/webui/src/pages/Config.tsx b/webui/src/pages/Config.tsx index b378403..734bcaf 100644 --- a/webui/src/pages/Config.tsx +++ b/webui/src/pages/Config.tsx @@ -34,6 +34,7 @@ const Config: React.FC = () => { ); const hotPrefixes = useMemo(() => hotReloadFieldDetails.map((x) => String(x.path || '').replace(/\.\*$/, '')).filter(Boolean), [hotReloadFieldDetails]); + const hotReloadTabKey = '__hot_reload__'; const allTopKeys = useMemo(() => Object.keys(cfg || {}).filter(k => typeof (cfg as any)?.[k] === 'object' && (cfg as any)?.[k] !== null), [cfg]); const basicTopKeys = useMemo(() => { @@ -50,7 +51,7 @@ const Config: React.FC = () => { const s = search.trim().toLowerCase(); keys = keys.filter((k) => k.toLowerCase().includes(s)); } - return keys; + return [hotReloadTabKey, ...keys]; }, [allTopKeys, basicTopKeys, basicMode, hotOnly, search, hotPrefixes]); const [selectedTop, setSelectedTop] = useState(''); @@ -193,7 +194,7 @@ const Config: React.FC = () => { } return ( -
+

{t('configuration')}

@@ -222,18 +223,6 @@ const Config: React.FC = () => {
-
-
{t('configHotFieldsFull')}
-
- {hotReloadFieldDetails.map((it) => ( -
-
{it.path}
-
{it.name || ''}{it.description ? ` · ${it.description}` : ''}
-
- ))} -
-
-
{!showRaw ? (
@@ -246,13 +235,26 @@ const Config: React.FC = () => { onClick={() => setSelectedTop(k)} className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${activeTop === k ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'text-zinc-300 hover:bg-zinc-800/60'}`} > - {configLabels[k] || k} + {k === hotReloadTabKey ? t('configHotFieldsFull') : (configLabels[k] || k)} ))}
+ {activeTop === hotReloadTabKey && ( +
+
{t('configHotFieldsFull')}
+
+ {hotReloadFieldDetails.map((it) => ( +
+
{it.path}
+
{it.name || ''}{it.description ? ` · ${it.description}` : ''}
+
+ ))} +
+
+ )} {activeTop === 'providers' && !showRaw && (
@@ -278,7 +280,7 @@ const Config: React.FC = () => {
)} - {activeTop ? ( + {activeTop && activeTop !== hotReloadTabKey ? ( { const [draft, setDraft] = useState(emptyDraft); const [saving, setSaving] = useState(false); const [groups, setGroups] = useState([]); + const [promptFileContent, setPromptFileContent] = useState(''); + const [promptFileFound, setPromptFileFound] = useState(false); const selected = useMemo( () => items.find((p) => p.agent_id === selectedId) || null, @@ -77,6 +81,7 @@ const SubagentProfiles: React.FC = () => { name: next.name || '', role: next.role || '', system_prompt: next.system_prompt || '', + system_prompt_file: next.system_prompt_file || '', memory_namespace: next.memory_namespace || '', status: (next.status as string) || 'active', tool_allowlist: Array.isArray(next.tool_allowlist) ? next.tool_allowlist : [], @@ -103,6 +108,33 @@ const SubagentProfiles: React.FC = () => { loadGroups().catch(() => {}); }, [q]); + useEffect(() => { + const path = String(draft.system_prompt_file || '').trim(); + if (!path) { + setPromptFileContent(''); + setPromptFileFound(false); + return; + } + fetch(`/webui/api/subagents_runtime${q}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'prompt_file_get', path }), + }) + .then(async (r) => { + if (!r.ok) throw new Error(await r.text()); + return r.json(); + }) + .then((data) => { + const found = data?.result?.found === true; + setPromptFileFound(found); + setPromptFileContent(found ? String(data?.result?.content || '') : ''); + }) + .catch(() => { + setPromptFileFound(false); + setPromptFileContent(''); + }); + }, [draft.system_prompt_file, q]); + const onSelect = (p: SubagentProfile) => { setSelectedId(p.agent_id || ''); setDraft({ @@ -110,6 +142,7 @@ const SubagentProfiles: React.FC = () => { name: p.name || '', role: p.role || '', system_prompt: p.system_prompt || '', + system_prompt_file: p.system_prompt_file || '', memory_namespace: p.memory_namespace || '', status: (p.status as string) || 'active', tool_allowlist: Array.isArray(p.tool_allowlist) ? p.tool_allowlist : [], @@ -162,6 +195,7 @@ const SubagentProfiles: React.FC = () => { name: draft.name || '', role: draft.role || '', system_prompt: draft.system_prompt || '', + system_prompt_file: draft.system_prompt_file || '', memory_namespace: draft.memory_namespace || '', status: draft.status || 'active', tool_allowlist: draft.tool_allowlist || [], @@ -218,6 +252,25 @@ const SubagentProfiles: React.FC = () => { await load(); }; + const savePromptFile = async () => { + const path = String(draft.system_prompt_file || '').trim(); + if (!path) { + await ui.notify({ title: t('requestFailed'), message: 'system_prompt_file is required' }); + return; + } + const r = await fetch(`/webui/api/subagents_runtime${q}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'prompt_file_set', path, content: promptFileContent }), + }); + if (!r.ok) { + await ui.notify({ title: t('requestFailed'), message: await r.text() }); + return; + } + setPromptFileFound(true); + await ui.notify({ title: t('saved'), message: t('promptFileSaved') }); + }; + return (
@@ -297,6 +350,15 @@ const SubagentProfiles: React.FC = () => {
+
+
system_prompt_file
+ setDraft({ ...draft, system_prompt_file: e.target.value })} + className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" + placeholder="agents/coder/AGENT.md" + /> +
{t('memoryNamespace')}
{ placeholder="You are a coding specialist..." />
+
+
+
system_prompt_file content
+
{promptFileFound ? t('promptFileReady') : t('promptFileMissing')}
+
+