Compare commits

...

10 Commits

Author SHA1 Message Date
7ea4638c8c feat(multi_agent): 优化 Team Leader 与子智能体 prompt,新增 Brainstormer 角色 2026-07-14 23:40:02 +08:00
351cd311c2 fix(multi-agent): 修复压缩404、prompt冻结串扰、子智能体状态串扰及/multiagent/new跳转问题 2026-07-14 23:22:06 +08:00
ce92697181 feat(multi_agent): add formatters and structured rendering for multi-agent tools 2026-07-14 22:14:55 +08:00
571ed6c81f fix(sub-agent): 修复传统模式 terminate 后子智能体仍继续调用工具的问题
- _call_model 中硬取消时直接抛出 CancelledError,避免返回半成品 tool_calls
- 软停止时仍优雅 break,后续由 _run_loop 丢弃半成品并进入 idle
- 保持 _mark_task_done 不覆盖 terminated 状态
- 侧边栏当前对话运行态标记修复保留
2026-07-14 20:37:57 +08:00
0aa37b83ee fix(stop-button): 修复REST轮询路径两个bug
1. lifecycle.ts handleTaskStopped: 移除'再次按下停止按钮以结束后台任务'弹窗。
   后台任务停止不再走停止按钮二次点击路径。

2. 主对话空闲+后台任务跑时输入栏依然为停止按钮:
   根因 - App.vue 传给 InputComposer 的 streaming-message 用的是
   composerStreamingForInput (= composerBusy && !displayLockEngaged),
   只要有后台任务 taskInProgress=true 就让 streamingMessage=true。
   改为传 streamingUi (= streamingMessage || hasPendingToolActions),
   只看主对话自身是否在流式输出,与后台任务无关。

3. 后端 chat_flow_task_main.py 用户主动停止时发的 task_stopped
   事件没带 has_running_* 字段,导致前端误判为完全空闲。
   补全字段(has_running_sub_agents/has_running_background_commands/
   has_running_multi_agent),前端用此信息决定是否保持对话运行态。
2026-07-14 19:46:31 +08:00
ae70e4902e refactor(stop-button): 拆分停止按钮和后台任务控制
停止按钮现在只停主智能体,与后台任务无关。
- 后端 cancel_task 简化:移除两段式判断、force_stop参数、is_ma_mode分支
- 删除 _force_stop_multi_agent 方法
- 新增 /api/sub_agents/stop_all 接口(mode=terminate/soft_stop)
- 前端 stopTask 移除 forceStop 概念,只取消主智能体
- 取消期间不再二次点击终结子智能体

子智能体停止按钮:
- InputComposer state bar 加'x个子智能体运行中'按钮
- 独立浮层显示,不依赖 git 栏(参考 askuser 方案)
- 点击传统模式弹窗终结;多智能体弹窗软停止
- App.vue 新增 handleStopAllSubAgents 调 ui.confirmDialog

后台指令停止按钮:
- LeftPanel 后台指令标签右边加停止按钮
- 仅当 panelMode=backgroundCommands 且存在运行中后台指令时显示
- 调 backgroundCommandStore.stopAllCommands 无二次确认

showStopIcon 只看主对话 streaming 状态:
- 后台任务在跑但主对话空闲时显示发送按钮
- 多智能体 / 传统模式统一处理

子智能体 store 加 stopAllAgents(mode) action 调新 API

useLegacySocket task_stopped handler 简化:
- 移除 multi_agent_reloaded 重载
- 移除'再次按下停止按钮以结束后台任务'提示
- 保留 has_running_multi_agent/sub_agents 用于 taskInProgress 判定

清理调试日志:[TaskCancel][stop_debug], [STOP_DEBUG] 全部去掉
保留 ma_debug 框架用于多智能体调试
2026-07-14 18:34:56 +08:00
30bfb79cbd wip(multi-agent): 停止按钮软停止链路调试日志+对话切换500修复+子智能体进度面板+多智能体软停止机制 2026-07-14 17:50:15 +08:00
4fcd0ccc4d feat(multi-agent): 子智能体系统升级
- prompt改造:子智能体注入AGENTS.md/执行环境/工作区信息/skills/项目记忆
- 工具增加:read_skill/recall_project_memory/todo_create/todo_update_task/save_webpage
- 上下文压缩:深度压缩机制,记录current_context_tokens,默认150k阈值可配置
- 模型升级:sub_agent_models.json支持thinkmode_status/extra_parameter,与主智能体对齐
- 角色管理三层结构:源码树预设multi_agent_roles/ + 运行态host/web预设 + web按用户隔离
- 启动同步:initialize_system调用sync_preset_roles同步预设到host和web运行态
- 模式判断:API用session.host_mode,工具用data_dir路径判断,不依赖IS_HOST_MODE
- 前端:个人空间新增子智能体管理页(角色CRUD/压缩阈值/模型选择),复用个人空间样式
- 入口改造:登录页移除多智能体按钮,QuickMenu加模式切换项,运行中对话禁止切换
- 工具调整:多智能体模式create_sub_agent去掉timeout/deliverables_dir参数
- skill禁止:sub-agent-guide在多智能体模式下禁止阅读
- .agents/统一为.astrion/路径修复
2026-07-14 02:28:45 +08:00
3c4ae917b2 fix(web_terminal): 程序启动自动恢复最近对话时不恢复模型,避免 /new 页面显示旧对话模型;显式加载对话时仍恢复保存的模型 2026-07-13 21:46:03 +08:00
58ec936a8c docs(agents): 补充多智能体模式章节 §11
包含角色/实例、执行机制、消息池与派发链路(含情况1/2/3
和 4 条硬约束)、通知池独立、渲染判断、调试日志、已知坑
与对话切换状态保留等所有 Agent 改代码必须知道的约束。
2026-07-13 20:13:38 +08:00
64 changed files with 4380 additions and 500 deletions

135
AGENTS.md
View File

@ -201,6 +201,25 @@
1. **meta 部分只放参数**`tool-result-meta` 仅用于展示工具调用的参数、配置、状态标识等元信息。例如状态、ID、路径、任务描述、超时时间等。禁止在 meta 区域放置执行结果、统计摘要、输出内容等。
2. **content 部分只放结果**`tool-result-content` 仅用于展示工具执行后的结果、输出、统计、总结等。例如:文件内容、命令输出、搜索命中、执行统计(工作时间 / 调用次数 / 工具次数)、最终回复摘要等。禁止在 content 区域重复展示工具参数。
## 5.7) 调试输出规范(强制)
> 约束级别:需要临时加调试日志排查问题时**必须遵守**。
### 前端调试日志
- **统一筛选词**:所有临时 `console.log` 必须使用**同一个**筛选前缀(例如 `[route-debug]`),方便用户在浏览器控制台用该前缀一次性筛选。
- **控制数量与时机**
- 禁止一次性输出上百上千条日志刷屏。
- 只在关键路径(入口、分支判断、状态变化、导航动作)打印,避免在循环、高频事件、每帧渲染中输出。
- 需要大量结构化数据时,优先用 `console.group` / `console.table` 或对象快照,而不是逐条打印。
- **用完即清**:问题定位后应及时删除或注释掉临时调试日志,不要长期留在代码里。
### 后端调试日志
- **必须写入文件**:后端调试信息禁止直接 `print` 到终端刷屏,必须写入日志文件。
- **复用现有 logger**:优先复用项目已有 logger`utils/logger.py`、`modules/multi_agent/debug_logger.py` 等),按模块落到 `~/.astrion/astrion/<mode>/logs/` 下。
- **关键状态转换必打**:多智能体、子智能体、任务轮询等复杂链路应在关键状态转换点写结构化日志,便于复现问题后按时间线追溯。
## 6) Git 工作流(开发 + Review
### 6.1 核心原则
@ -332,3 +351,119 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
- 调试开关(代码变量):`modules/approval_agent.py` 中 `DEBUG_SAVE_APPROVAL_AGENT_TRANSCRIPT`
- 开启后写入:`logs/approval_agent/`
- 记录以累积 `messages` 为主,便于对齐主/子智能体会话格式
## 11) 多智能体模式multi-agent mode
> 对话级开关 `metadata.multi_agent_mode = true` 启用URL 入口 `/multiagent/<conv_id>`,数据目录 `~/.astrion/astrion/host/mutiagents/`(保留原拼写)。完整设计与踩坑记录见项目记忆 `multi_agent_mode_design`,本节只列 Agent 改代码时必须知道的硬约束。
### 11.1 角色与实例
- 主智能体显示名固定为 `Team Leader`,不需要专门的预置角色文件。
- 子智能体 = `role_id`(如 `ui-operator` / `full-stack-engineer` / `code-reviewer` / `researcher`+ `agent_id`(同一 role_id 下从 1 递增)。显示名格式 `{Role Name}_{agent_id}`,例如 `UI Operator_1`,后缀永远带数字。
- 主→子 / 子→主 / 子→子三种通信通过工具完成,工具签名见 `modules/multi_agent/tools.py`
- **`send_message_to_sub_agent``ask_sub_agent` 语义不同,必须保留两者**:前者插入引导消息不阻塞,后者阻塞等待一轮回答。
- 子智能体间通信要求同时向主智能体输出汇报,不允许「偷偷沟通」。
### 11.2 子智能体执行机制
- 子智能体在主进程内 `asyncio.Task`,跑在独立后台事件循环线程里(避开 Flask-SocketIO threading 冲突)。工具调用复用主进程沙箱/容器链路,网络调用走 `utils.api_client.DeepSeekClient`
- 子智能体在多智能体模式下:
- `create_sub_agent` 强制 `run_in_background=False`,不触发 `sub_agent_waiting` 事件,不阻塞前端输入区。
- 子智能体自然的 assistant 输出结束(无 tool_calls即本轮任务结束进入 `idle`,上下文保留,不算 failed。
- 不能把 `output.success == null` 直接判为 `failed`—`_check_task_status()` 对多智能体任务 `status=running/idle` 视为正常态(见 `modules/sub_agent/state.py`)。
- idle 等待必须用 `asyncio.Event`,不能用 `threading.Event`,跨线程唤醒用 `call_soon_threadsafe`
- 任务记录里 `multi_agent_mode` 字段必须显式写入,缺失会导致传统后台通知池把多智能体任务当后台任务处理。`reconcile_task_states()` 已对旧任务补回。
### 11.3 消息池与派发链路(重点)
**输出端 ↔ 接收端分离**
- 输出端(`SubAgentTask._forward_output_to_master`):子智能体每次 assistant 文本输出都封为标准格式消息并 `push_master_message` 到会话的 `MultiAgentState.pending_master_messages`,不再做事。
- 接收端(`MultiAgentState` + dispatch根据主智能体当前状态选择插入方式
- **情况1主运行中**:在 `execute_tool_calls` 末尾 `process_multi_agent_master_messages(inline=True, after_tool_call_id=...)` 插入到下一轮模型 messages 列表里(详见 `server/chat_flow_tool_loop.py:1326`)。
- **情况2主空闲**`poll_multi_agent_notifications` spawn 出后台 poll主对话空闲且 pool 有消息就 drain`_dispatch_multi_agent_idle_messages` 创建 `task_type="notice"` 的后续 task触发新一轮工作。完整顺序见项目记忆 multi_agent_mode_design 中的「情况2完整链路」图。
- **情况3主最后一轮无工具调用**:主循环 `if not tool_calls:` 分支调 `process_multi_agent_master_messages(inline=False)``continue` 继续迭代Team Leader 在同 task 续跑处理子智能体输出。实现上与情况2同路径即情况3通过后续 task 完成情况2
**情况2 硬约束(调试踩过坑)**
1. **Pool 优先,不能等所有 running 退出再 drain**`ask_master` await 期间 status=running但子智能体本身不会产生新输出只有等主对话回答才能解套。pool 有消息就立即 drain不管 running 状态。「对话处于运行状态」只决定前端显示态,不阻塞 pool 消费。
2. **`_dispatch_multi_agent_idle_messages` 持久化不要重复**:前置 N-1 条调 `inject_multi_agent_master_message` 绑定持久化。最后一条只 emit 给在线客户端、不持久化、在后续 `task_manager.create_chat_task` 走的 `handle_task_with_sender` 里才 `add_conversation`。否则历史里会有两条相同 user 消息,前端刷新会被渲染两遍(一条多智能体渲染、一条通知渲染)。
3. **Metadata `visibility` 必须显式为 `"chat"`**`_user_message_ui_defaults("sub_agent")` 默认给 `{visibility: "compact"}`;在 `_dispatch_multi_agent_idle_messages` 构造 `auto_user_message_payload`时,如果有`**ui_defaults`,必须在它之**后**写 `"visibility": "chat"`dict 字面量中后出现的key 胜出)。同样,在`handle_task_with_sender`处理多智能体消息时,`user_message_metadata.update(multi_agent_meta)`之后要显式`user_message_metadata["visibility"] = "chat"`。
4. **必须传 `auto_message_type`**:前端 `isMultiAgentMessage()` 只看 `auto_message_type.startsWith('multi_agent_')`。`auto_user_message_payload` 和 `preceding_user_notices[i].payload` 都必须显式写 `auto_message_type`,字段不能用空值,否则前端 fallback 起通知渲染。
### 11.4 通知池轮询器完全独立
- 传统后台子智能体 / 后台 `run_command` 通知:`poll_completion_notifications`,在 `handle_task_with_sender` 结尾的 `needs_completion_poll`
- 多智能体通知:`poll_multi_agent_notifications`,在 `needs_ma_poll`。两者完全独立,避免 task_manager 单工作区互斥竞争。
- `task_complete` 事件中:
- `has_running_sub_agents` 只算传统后台任务,不算多智能体。
- `has_running_multi_agent` 多智能体专用字段,同时包含 _running_ 实例和 pending master 消息(详见 `server/chat_flow_task_main.py:2324`)。前端通过该字段走独立的 `startMultiAgentTaskProbe`(不启动 `sub_agent_waiting`)。
### 11.5 渲染与前端约定
- 多智能体消息渲染条件是**两个独立判断**
- `isMultiAgentMessage()`:看 `auto_message_type.startsWith('multi_agent_')`
- `getMessageVisibility()`:看 `metadata.visibility`。两者都必须正确,消息才能走多智能体渲染分支。任一错都会 fallback 到通知渲染。
- 多智能体消息不显示新的 assistant 回复头部Astrion/工作时间),即 `metadata.starts_work=false`。但前端通过 `has_running_multi_agent``task_complete` 中单独处理恢复轮询。
- 子智能体进度弹窗:输出与工具按真实时间线混排,默认 3 行,过长用省略号或内部滚动;颜色走 `--text-primary` 等语义 token。
- 全局工具规范统一适用于多智能体权限UI 不能引入 `compact` 以外的 fallback 样式习惯复制到多智能体渲染。
### 11.6 工具结果格式化与前端渲染位置
新增多智能体工具时,必须同步补齐「后端结果格式化」和「前端结构化渲染」,否则前端会 fallback 到原始 JSON 或空白。
**后端 formatter主智能体工具**
- 实现位置:`utils/tool_result_formatter/agent_context.py`
- 注册位置:`utils/tool_result_formatter/dispatch.py` 的 `TOOL_FORMATTERS`
- 处理入口:所有主智能体工具执行结果,最终由 `format_tool_result_for_context()` 转换为自然语言摘要,写入对话历史。
**后端 formatter子智能体通信工具**
- 实现位置:`modules/sub_agent/toolkit.py` 的 `_format_tool_result()`
- 覆盖工具:`ask_master` / `ask_other_agent` / `answer_other_agent` / `list_active_sub_agents`
- 处理入口:子智能体 tool call 结果回填到子对话上下文前。
**前端 renderer主路径**
- 实现位置:`static/src/components/chat/actions/toolRenderers.ts` 的 `renderEnhancedToolResult()`
- 调用方:
- `static/src/components/chat/MinimalBlocks.vue`
- `static/src/components/chat/StackedBlocks.vue`
- 注意:这两个视图已移除 `enhanced_tool_display` 开关判断,**所有工具块都强制走结构化渲染**,不再显示原始 JSON。
**前端 renderer备用路径**
- 实现位置:`static/src/components/chat/actions/ToolAction.vue` 的 `renderToolResult()`
- 调用方:`static/src/components/chat/ChatArea.vue`
- 同样已移除原始 JSON fallback仅作为 ChatArea 独立工具块渲染的备用。
**新增工具 checklist**
1. `modules/multi_agent/tools.py` 定义工具签名。
2. `core/main_terminal_parts/tools_execution.py` 实现工具 handler 并返回标准 dict`success` + 业务字段 + 可选 `error`)。
3. `utils/tool_result_formatter/agent_context.py` 新增 `_format_<tool_name>()``dispatch.py` 注册。
4. `static/src/components/chat/actions/toolRenderers.ts` 新增对应 `render<PascalCaseToolName>()` 并在 `renderEnhancedToolResult()` 中分发。
5. 如果是子智能体通信工具,同步在 `modules/sub_agent/toolkit.py``_format_tool_result()` 中补 formatter。
### 11.7 调试机制
- 调试日志统一走 `modules/multi_agent/debug_logger.py``ma_debug()` 函数,写入 `~/.astrion/astrion/host/logs/multi_agent_loop.log`
- 关键状态转换点必须打 `ma_debug`,便于复现 bug
- `handle_task_with_sender_start`(含 `pending_master_messages_count`
- `state_push_master_message` (交出 `queue_len_before`
- `poll_ma_tick` (每 0.5s 一次,包含 `instance_count` / `statuses` / `pending_count` / `main_active` 的完整状态快照)
- `dispatch_ma_idle_enter` / `dispatch_ma_idle_before_create_task` / `dispatch_ma_idle_task_created` / `dispatch_ma_idle_sender_user_message` / `dispatch_ma_idle_exit_ok`
- `create_chat_task` 异常 / `provide_answer` 跨循环回写
- `runtime_injected` 字段标记metadata 里能区分多智能体 inline / idle / ask插入路径
### 11.8 已知坑
- 多智能体模式下根本不 `emit` `sub_agent_waiting` 事件,避免前端进入「等待后台子智能体」的输入区阻塞态。
- `_announced_sub_agent_tasks` / `notified` 等标记仅适用于传统后台子智能体任务,多智能体任务不走这条通知路径。
- 多智能体任务的 `output.json``status` 在子智能体自然进入 idle 时会被写为 `"idle"`;在 `_check_task_status` 中由 `_check_task_status_keep_alive` 跳过防止错误判定为 `failed`
- 传统后台通知池 `_collect_pending_completion_notices``task.get("multi_agent_mode")` 为真时跳过该 task多智能体派出走独立的 `poll_multi_agent_notifications` 路径,不当混用。
- `_has_pending_completion_work` 主动排除 `multi_agent_mode=True` 的任务;两者永远独立。
### 11.9 对话切换与状态保留
- 切换会话不清理 `_running_tasks``_sub_agent_instances`。`SubAgentManager` 的全局 tasks 字典按 `task_id` 保持,多智能体状态由 `conversation_id``get_multi_agent_state` 中查。
- 子智能体对话存在 `~/.astrion/astrion/host/host/data/sub_agents/`。重启后走 `manager.restore_sub_agent` 恢复实例引用。
- `MultiAgentState` 实例在 Manager 上常驻dict by `conversation_id`);进程重启走 `from_snapshot` 恢复。
---
注:本节按「现有架构 + 多智能体分支」方案描述,与项目记忆 `multi_agent_mode_design` 同步;如两者冲突以代码为准并同步修订本节。

View File

@ -1,6 +1,6 @@
---
name: sub-agent-guide
description: 子智能体使用指南。创建子智能体之前必须阅读此技能。子智能体仅用于并行处理独立的“非代码编写”任务(如项目初步探索、网络搜索与信息收集、程序测试执行)。关键限制:子智能体之间无法通信,看不到主对话历史。严格禁止将子智能体用于项目代码编写、代码修改、重构或实现功能。
description: 子智能体使用指南。创建子智能体之前必须阅读此技能。子智能体仅用于并行处理独立的“非代码编写”任务(如项目初步探索、网络搜索与信息收集、程序测试执行)。关键限制:子智能体之间无法通信,看不到主对话历史。严格禁止将子智能体用于项目代码编写、代码修改、重构或实现功能。**如果运行模式为多智能体模式,禁止阅读并参考该 skill。**
---
# 子智能体使用指南

View File

@ -161,6 +161,12 @@ HOST_PROJECT_PATH = _resolve_repo_path(os.environ.get("HOST_PROJECT_PATH", ""),
# ── 源码树内的配置与资源(不随运行态根目录迁移)──
PROMPTS_DIR = _resolve_repo_path(os.environ.get("PROMPTS_DIR", ""), "./prompts")
AGENT_SKILLS_DIR = _resolve_repo_path(os.environ.get("AGENT_SKILLS_DIR", ""), "./agentskills")
# 多智能体预设角色目录(源码树内,随版本分发)
PRESET_ROLES_DIR = _resolve_repo_path(os.environ.get("PRESET_ROLES_DIR", ""), "./multi_agent_roles")
# 运行态预设/自定义角色目录host 模式:预设+自定义同目录web 模式:仅预设)
CUSTOM_ROLES_DIR = str(Path(RUNTIME_ROOT) / _MODE / "mutiagents" / "agents")
# web 模式预设角色目录web 模式下使用host 模式下不用)
WEB_PRESET_ROLES_DIR = str(Path(RUNTIME_ROOT) / "web" / "mutiagents" / "agents")
WORKSPACE_SKILLS_DIRNAME = ".astrion/skills"
WORKSPACE_MEMORY_DIRNAME = ".astrion/memory"
WORKSPACE_REVIEW_DIRNAME = ".astrion/review"
@ -198,6 +204,9 @@ __all__ = [
"ADMIN_POLICY_FILE",
"API_USER_SPACE_DIR",
"CUSTOM_SKILLS_DIR",
"PRESET_ROLES_DIR",
"CUSTOM_ROLES_DIR",
"WEB_PRESET_ROLES_DIR",
"API_USERS_DB_FILE",
"API_TOKENS_FILE",
"API_USAGE_FILE",

View File

@ -117,15 +117,29 @@ class MessagesMixin:
model_key = getattr(self, "model_key", None)
prompt_replacements = get_model_prompt_replacements(model_key)
is_multi_agent_mode = bool(getattr(self, "multi_agent_mode", False))
def _build_main_system_prompt() -> str:
if is_multi_agent_mode:
# 多智能体模式下主 prompt 使用 Team Leader 专属模板
try:
from modules.multi_agent.prompts import _load_template
return _load_template("master")
except Exception as exc:
logger.warning(f"[messages] 加载多智能体主 prompt 失败: {exc}")
system_prompt_template = self.load_prompt(prompt_name)
# main_system.txt / main_system_vl.txt 仅使用 {model_description}
return system_prompt_template.format(
model_description=prompt_replacements.get("model_description", "")
)
main_system_frozen_key = (
"frozen_main_system_prompt_multi_agent"
if is_multi_agent_mode
else "frozen_main_system_prompt"
)
system_prompt = self._get_or_init_frozen_prompt(
"frozen_main_system_prompt",
main_system_frozen_key,
_build_main_system_prompt,
)
@ -225,12 +239,19 @@ class MessagesMixin:
return agents_md_template.replace("{{AGENTS_MD_CONTENT}}", agents_md_content)
return f"【AGENTS.md 项目规范】\n\n{agents_md_content}\n\n---\n请注意:以上规范来自工作区根目录的 AGENTS.md 文件,若有冲突请以 AGENTS.md 为准。"
agents_md_text = self._get_or_init_frozen_prompt(
"frozen_agents_md_prompt",
_build_agents_md_prompt,
# AGENTS.md 注入开关可能变化,只在开启时生成并缓存,避免关闭时把空字符串冻住
agents_md_inject_enabled = (
bool(personalization_config.get("agents_md_auto_inject", False))
if isinstance(personalization_config, dict)
else False
)
if agents_md_text:
messages.append({"role": "system", "content": agents_md_text})
if agents_md_inject_enabled:
agents_md_text = self._get_or_init_frozen_prompt(
"frozen_agents_md_prompt",
_build_agents_md_prompt,
)
if agents_md_text:
messages.append({"role": "system", "content": agents_md_text})
# skills 列表
skills_catalog = get_skills_catalog(private_dir=infer_private_skills_dir(self.data_dir))
@ -280,17 +301,8 @@ class MessagesMixin:
lambda: self._format_disabled_tool_notice() or "",
)
# 多智能体模式提示词:当会话 metadata.multi_agent_mode 为真时追加
# 多智能体模式额外提示词:可用的子智能体角色(动态 prompt第一个用户消息后冻结
if getattr(self, "multi_agent_mode", False):
try:
from modules.multi_agent.prompts import build_multi_agent_master_prompt
multi_agent_prompt = build_multi_agent_master_prompt(self.project_path, base="")
if multi_agent_prompt:
messages.append({"role": "system", "content": multi_agent_prompt})
except Exception as exc:
logger.warning(f"[messages] 注入多智能体 prompt 失败: {exc}")
# 可用的子智能体角色(动态 prompt第一个用户消息后冻结
try:
available_agents_prompt = self._get_or_init_frozen_prompt(
"frozen_available_agents_prompt",

View File

@ -221,7 +221,12 @@ class ModeMixin:
conv_id = getattr(cm, "current_conversation_id", None) if cm else None
if cm and conv_id:
try:
cm.conversation_manager.update_conversation_metadata(conv_id, {key: built})
target_manager = (
cm._get_conversation_manager_for_id(conv_id)
if hasattr(cm, "_get_conversation_manager_for_id")
else cm.conversation_manager
)
target_manager.update_conversation_metadata(conv_id, {key: built})
if isinstance(cm.conversation_metadata, dict):
cm.conversation_metadata[key] = built
except Exception:

View File

@ -1740,11 +1740,18 @@ class MainTerminalToolsExecutionMixin:
role_id = arguments.get("role_id")
if not role_id:
result = {"success": False, "error": "多智能体模式下 create_sub_agent 必须指定 role_id"}
elif arguments.get("deliverables_dir"):
result = {"success": False, "error": "多智能体模式下不支持交付目录参数"}
else:
try:
from modules.multi_agent.role_store import load_preset_role
from modules.multi_agent.role_store import load_preset_role, infer_custom_roles_dir
from modules.multi_agent.prompts import build_multi_agent_sub_agent_prompt
role = load_preset_role(role_id)
_data_dir = str(getattr(self, "data_dir", ""))
_custom_dir = infer_custom_roles_dir(_data_dir)
# host模式下 custom_dir 就是 runtime_dirweb模式下 custom_dir 是用户个人目录
_is_web = '/web/users/' in _data_dir
_runtime_dir = None if _is_web else _custom_dir
role = load_preset_role(role_id, runtime_dir=_runtime_dir, custom_dir=_custom_dir)
if not role:
result = {"success": False, "error": f"角色不存在: {role_id}"}
else:
@ -1756,21 +1763,41 @@ class MainTerminalToolsExecutionMixin:
agent_id = multi_agent_state.next_agent_id_for_role(role_id)
# 构造显示名
display_name = role.display_name(int(agent_id))
# 构造多智能体版系统提示词
# 构造多智能体版系统提示词(含动态上下文注入)
workspace_path = str(getattr(self, "project_path", ""))
system_prompt = build_multi_agent_sub_agent_prompt(role.body_prompt, display_name, workspace_path)
data_dir = str(getattr(self, "data_dir", ""))
# 获取当前沙箱模式
sandbox_mode = ""
try:
if hasattr(self, "get_execution_mode_state"):
state = self.get_execution_mode_state() or {}
sandbox_mode = str(state.get("mode") or "")
except Exception:
pass
system_prompt = build_multi_agent_sub_agent_prompt(
role.body_prompt, display_name, workspace_path,
data_dir=data_dir,
sandbox_mode=sandbox_mode,
)
# 构造 task_message作为 Team Leader 的任务发布)
from modules.multi_agent.state import build_master_dispatch_text
task_message = build_master_dispatch_text(arguments.get("task", ""))
summary_text = (arguments.get("summary") or f"{role.name}作业")[:80]
thinking_mode = arguments.get("thinking_mode") or role.thinking_mode or "fast"
# 读取子智能体压缩阈值配置
_compress_threshold = 150_000
try:
from modules.personalization_manager import load_personalization_config
_prefs = load_personalization_config(data_dir) or {}
_compress_threshold = int(_prefs.get("sub_agent_compress_threshold_tokens", 150_000))
except Exception:
pass
# 走原行 发事件创建(避免后期重建提供重复工能重费,直接使用 multi_agent_mode=True 调用)
result = self.sub_agent_manager.create_sub_agent(
agent_id=int(agent_id),
summary=summary_text,
task=arguments.get("task", ""),
run_in_background=False,
timeout_seconds=arguments.get("timeout_seconds"),
conversation_id=conv_id,
model_key=role.model_key,
thinking_mode=thinking_mode,
@ -1779,6 +1806,7 @@ class MainTerminalToolsExecutionMixin:
display_name=display_name,
system_prompt=system_prompt,
task_message=task_message,
compress_threshold_tokens=_compress_threshold,
)
# 在多智能体模式下,子智能体是团队协作成员,不是传统后台任务。
# run_in_background=False 避免触发后台完成通知轮询,保持主对话输入区可用。
@ -1972,23 +2000,33 @@ class MainTerminalToolsExecutionMixin:
body_prompt = arguments.get("body_prompt", "").strip()
description = arguments.get("description", "").strip()
thinking_mode_arg = arguments.get("thinking_mode", "fast")
model_key_arg = arguments.get("model_key", "").strip() or None
if not role_id or not name or not body_prompt:
result = {"success": False, "error": "role_id/name/body_prompt 必填"}
else:
existing_ids = {r.role_id for r in list_roles()}
_data_dir = str(getattr(self, "data_dir", ""))
from modules.multi_agent.role_store import infer_custom_roles_dir
_custom_dir = infer_custom_roles_dir(_data_dir)
_is_web = '/web/users/' in _data_dir
_runtime_dir = None if _is_web else _custom_dir
existing_ids = {r.role_id for r in list_roles(runtime_dir=_runtime_dir, custom_dir=_custom_dir)}
if role_id in existing_ids:
result = {"success": False, "error": f"角色 {role_id} 已存在"}
else:
role = RoleConfig(role_id=role_id, name=name, description=description, body_prompt=body_prompt, thinking_mode=thinking_mode_arg)
f = save_custom_role(role)
role = RoleConfig(role_id=role_id, name=name, description=description, body_prompt=body_prompt, thinking_mode=thinking_mode_arg, model_key=model_key_arg, is_custom=True)
f = save_custom_role(role, custom_dir=_custom_dir)
result = {"success": True, "role_id": role_id, "file": str(f)}
except Exception as exc:
result = {"success": False, "error": str(exc)}
elif tool_name == "list_agents":
try:
from modules.multi_agent.role_store import list_roles
roles = list_roles()
from modules.multi_agent.role_store import list_roles, infer_custom_roles_dir
_data_dir = str(getattr(self, "data_dir", ""))
_custom_dir = infer_custom_roles_dir(_data_dir)
_is_web = '/web/users/' in _data_dir
_runtime_dir = None if _is_web else _custom_dir
roles = list_roles(runtime_dir=_runtime_dir, custom_dir=_custom_dir)
result = {"success": True, "roles": [r.to_dict() for r in roles]}
except Exception as exc:
result = {"success": False, "error": str(exc)}

View File

@ -41,7 +41,7 @@ class WebTerminal(MainTerminal):
latest = conversations[0]
conv_id = latest.get("id")
if conv_id:
result = self.load_conversation(conv_id)
result = self.load_conversation(conv_id, restore_model=False)
if result.get("success"):
print(f"[WebTerminal] 已加载最近对话: {conv_id}")
return
@ -311,12 +311,14 @@ class WebTerminal(MainTerminal):
)
perf_log("_ensure_conversation_versioning_enabled done", elapsed_ms=(time.perf_counter() - t0) * 1000, extra={"conv_id": normalized_id})
def load_conversation(self, conversation_id: str) -> Dict:
def load_conversation(self, conversation_id: str, restore_model: bool = True) -> Dict:
"""
加载指定对话Web版本
Args:
conversation_id: 对话ID
restore_model: 是否从对话 metadata 恢复模型程序启动时自动恢复最近对话不恢复模型
避免 /new 页面显示旧对话模型用户显式加载对话时恢复
Returns:
Dict: 加载结果
@ -351,12 +353,13 @@ class WebTerminal(MainTerminal):
self.set_network_permission(network_permission)
except Exception:
pass
saved_model_key = meta.get("model_key")
if saved_model_key:
try:
self.set_model(saved_model_key)
except Exception as exc:
logger.warning("加载对话模型 %s 失败: %s", saved_model_key, exc)
if restore_model:
saved_model_key = meta.get("model_key")
if saved_model_key:
try:
self.set_model(saved_model_key)
except Exception as exc:
logger.warning("加载对话模型 %s 失败: %s", saved_model_key, exc)
self.pending_permission_mode = str(meta.get("pending_permission_mode") or "").strip().lower() or None
self.pending_execution_mode = str(meta.get("pending_execution_mode") or "").strip().lower() or None
self.pending_network_permission = str(meta.get("pending_network_permission") or "").strip().lower() or None

View File

@ -123,8 +123,8 @@ class ApiUserManager:
project_path = work_root / "project"
data_dir = work_root / "data"
logs_dir = work_root / "logs"
uploads_dir = project_path / ".agents" / "user_upload"
skills_dir = project_path / ".agents" / "skills"
uploads_dir = project_path / ".astrion" / "user_upload"
skills_dir = project_path / ".astrion" / "skills"
for path in (project_path, data_dir, logs_dir, uploads_dir, skills_dir, shared_dir, prompts_dir, personalization_dir):
path.mkdir(parents=True, exist_ok=True)

View File

@ -64,13 +64,41 @@ def build_multi_agent_master_prompt(workspace_path: str, base: str = "") -> str:
return f"{body}\n"
def build_multi_agent_sub_agent_prompt(role_body: str, display_name: str, workspace_path: str) -> str:
def build_multi_agent_sub_agent_prompt(
role_body: str,
display_name: str,
workspace_path: str,
*,
data_dir: str = "",
sandbox_mode: str = "",
) -> str:
"""构造子智能体的系统提示词。
`role_body` 为该角色 Markdown 文件 frontmatter 之后的自定义 prompt
`display_name` 为该实例的显示名 `UI Operator_1`
`workspace_path` 为工作区路径用于注入动态上下文
`data_dir` 为数据目录路径用于推断 private skills 目录
`sandbox_mode` 为沙箱模式sandbox/direct/host/docker
所有动态信息在子智能体创建那一刻快照写入后续冻结不更新
"""
return _format_template("sub_agent", display_name=display_name, role_body=role_body.strip())
# 角色专属 prompt模板正文
role_prompt = _format_template("sub_agent", display_name=display_name, role_body=role_body.strip())
# 动态上下文AGENTS.md / 执行环境 / 工作区信息 / skills / 项目记忆等)
try:
from modules.multi_agent.sub_agent_context import build_sub_agent_dynamic_context
dynamic_context = build_sub_agent_dynamic_context(
workspace_path,
data_dir=data_dir,
sandbox_mode=sandbox_mode,
)
except Exception:
dynamic_context = ""
if dynamic_context:
return f"{role_prompt.rstrip()}\n\n---\n\n{dynamic_context}\n"
return f"{role_prompt}\n"
def build_available_agents_prompt() -> str:

View File

@ -1,62 +1,136 @@
"""多智能体角色存储与解析。
角色定义文件格式Markdown + Frontmatter:
三层结构
---
id: ui-operator
name: UI Operator
description: 界面设计与前端实现
model: ""
thinking_mode: fast
skills:
- frontend-design
---
1. **源码树预设**``multi_agent_roles/``随版本分发只读种子
2. **运行态预设**
- host 模式 = ``host/mutiagents/agents/``同时也是自定义目录
- web 模式 = ``web/mutiagents/agents/``
3. **用户自定义** web 模式按用户隔离
- web 模式 = ``web/users/<user>/personal/mutiagents/agents/``
- host 模式 = 和预设同目录
自定义 prompt body...
启动时调用 `sync_preset_roles()` 把源码树预设同步到 host web 两个运行态预设目录
角色目录
- ~/.astrion/astrion/host/mutiagents/agents/<role_id>.md
存储约定见 .astrion/memory/multi_agent_mode_design.md
**重要**本模块不再依赖 `IS_HOST_MODE` 判断运行模式
调用方API / tools_execution通过参数显式传入 `runtime_dir` `custom_dir`
- 不传则仅扫描源码树预设最小安全回退
- host 模式`runtime_dir` `custom_dir` 指向同一目录
- web 模式`runtime_dir` 指向 web 预设目录`custom_dir` 指向用户个人目录
"""
from __future__ import annotations
import json
import re
import shutil
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Set
try:
from config.paths import IS_HOST_MODE, RUNTIME_ROOT
from config.paths import RUNTIME_ROOT, PRESET_ROLES_DIR, CUSTOM_ROLES_DIR, WEB_PRESET_ROLES_DIR
except ImportError:
# 防止直接 import 时 config 尚未加载,提供回退值
IS_HOST_MODE = True
RUNTIME_ROOT = str(Path.home() / ".astrion" / "astrion")
# 复用项目的 MUTIAGENTS 数据目录(保留原拼写 "mutiagents",见项目记忆)
DEFAULT_MUTIAGENTS_DIR = Path(RUNTIME_ROOT) / ("host" if IS_HOST_MODE else "web") / "mutiagents"
AGENTS_DIR_NAME = "agents"
PRESET_ROLES_DIR = ""
CUSTOM_ROLES_DIR = ""
WEB_PRESET_ROLES_DIR = ""
FRONTMATTER_RE = re.compile(r"^---\s*\n(?P<body>.*?)\n---\s*\n(?P<rest>.*)$", re.S)
def _agents_dir() -> Path:
"""返回角色根目录(按运行模式自动选择 host/web"""
base = Path(DEFAULT_MUTIAGENTS_DIR)
out = base / AGENTS_DIR_NAME
out.mkdir(parents=True, exist_ok=True)
return out
# ----- 目录推断 -----
def _source_preset_dir() -> Path:
"""源码树预设角色目录(只读种子)。"""
return Path(PRESET_ROLES_DIR).expanduser().resolve()
def _host_runtime_dir() -> Path:
"""host 模式运行态目录(预设+自定义同目录)。"""
return Path(CUSTOM_ROLES_DIR).expanduser().resolve()
def _web_runtime_dir() -> Path:
"""web 模式运行态预设目录。"""
return Path(WEB_PRESET_ROLES_DIR).expanduser().resolve()
def infer_custom_roles_dir(data_dir: str | Path | None) -> Optional[Path]:
"""从工作区 data_dir 推断用户自定义角色目录。
- data_dir 包含 ``/web/users/`` web 模式返回 ``users/<user>/personal/mutiagents/agents``
- 其他情况 host 模式返回 host 运行态目录
"""
if not data_dir:
return _host_runtime_dir()
data_path = str(Path(data_dir).expanduser().resolve())
if "/web/users/" in data_path:
# web 模式:按用户隔离
dp = Path(data_path).resolve()
# users/<user>/projects/<project_id>/data -> users/<user>/personal/mutiagents/agents
if dp.name == "data" and dp.parent.parent.name == "projects":
user_root = dp.parent.parent.parent
return (user_root / "personal" / "mutiagents" / "agents").resolve()
# 兼容旧/其他布局
if dp.name == "data" and dp.parent.parent.name == "project":
user_root = dp.parent.parent.parent
return (user_root / "personal" / "mutiagents" / "agents").resolve()
return (dp.parent / "mutiagents" / "agents").resolve()
# host 模式
return _host_runtime_dir()
def _preset_role_ids() -> Set[str]:
"""返回源码树预设角色 id 集合(用于区分自定义角色)。"""
src = _source_preset_dir()
if not src.exists():
return set()
return {p.stem for p in src.glob("*.md") if p.is_file()}
def _ensure_dir(path: Path) -> Path:
path.mkdir(parents=True, exist_ok=True)
return path
# ----- 启动同步 -----
def sync_preset_roles() -> None:
"""启动时把源码树预设角色同步到 host 和 web 两个运行态预设目录。
- 目录不存在则创建并复制全部预设角色
- 文件已存在则不覆盖可能是用户已改过的运行态副本
"""
src = _source_preset_dir()
if not src.exists() or not src.is_dir():
return
preset_files = {p: p.name for p in src.glob("*.md") if p.is_file()}
if not preset_files:
return
rt_root = Path(RUNTIME_ROOT).expanduser().resolve()
for target_dir in [_host_runtime_dir(), _web_runtime_dir()]:
target = target_dir.expanduser().resolve()
# 安全检查:目标必须在运行态根下
try:
target.relative_to(rt_root)
except ValueError:
continue
target.mkdir(parents=True, exist_ok=True)
for src_file, name in preset_files.items():
dst = target / name
if not dst.exists():
shutil.copy2(src_file, dst)
# ----- Frontmatter 解析 -----
def _parse_frontmatter(text: str) -> tuple[Dict[str, Any], str]:
"""解析 Markdown frontmatter返回 (元数据 dict, body)。"""
m = FRONTMATTER_RE.match(text)
if not m:
return {}, text
raw_meta = m.group("body") or ""
body = m.group("rest") or ""
meta: Dict[str, Any] = {}
# 简易解析k: v 与 k: 列表项,不引入 yaml 依赖)
current_key: Optional[str] = None
for line in raw_meta.splitlines():
if not line.strip():
@ -74,14 +148,74 @@ def _parse_frontmatter(text: str) -> tuple[Dict[str, Any], str]:
meta[k] = v
current_key = None
else:
# 可能是列表块
meta[k] = []
current_key = k
return meta, body.strip()
def load_role_from_file(file_path: Path) -> Optional["RoleConfig"]:
"""从 Markdown 文件加载角色定义。"""
# ----- RoleConfig -----
class RoleConfig:
"""一个多智能体角色配置。"""
__slots__ = (
"role_id",
"name",
"description",
"body_prompt",
"model_key",
"thinking_mode",
"skills",
"source_file",
"is_custom",
)
def __init__(
self,
*,
role_id: str,
name: str,
description: str = "",
body_prompt: str = "",
model_key: Optional[str] = None,
thinking_mode: str = "fast",
skills: Optional[List[str]] = None,
source_file: str = "",
is_custom: bool = False,
):
self.role_id = role_id
self.name = name
self.description = description
self.body_prompt = body_prompt
self.model_key = model_key
self.thinking_mode = thinking_mode or "fast"
self.skills = list(skills or [])
self.source_file = source_file
self.is_custom = is_custom
def display_name(self, agent_id: int) -> str:
return f"{self.name}_{agent_id}"
def to_dict(self) -> Dict[str, Any]:
return {
"role_id": self.role_id,
"name": self.name,
"description": self.description,
"model_key": self.model_key,
"thinking_mode": self.thinking_mode,
"skills": self.skills,
"body_prompt": self.body_prompt,
"source_file": self.source_file,
"is_custom": self.is_custom,
}
def __repr__(self) -> str:
return f"<RoleConfig id={self.role_id} name={self.name} custom={self.is_custom}>"
# ----- 文件级加载 -----
def load_role_from_file(file_path: Path, is_custom: bool = False) -> Optional[RoleConfig]:
if not file_path.exists() or not file_path.is_file():
return None
text = file_path.read_text(encoding="utf-8")
@ -97,43 +231,17 @@ def load_role_from_file(file_path: Path) -> Optional["RoleConfig"]:
thinking_mode=str(meta.get("thinking_mode") or "fast"),
skills=list(meta.get("skills") or []),
source_file=str(file_path),
is_custom=is_custom,
)
def load_preset_role(role_id: str) -> Optional["RoleConfig"]:
"""从预设/自定义角色目录加载一个角色。"""
f = _agents_dir() / f"{role_id}.md"
return load_role_from_file(f)
# ----- 序列化 -----
def list_roles() -> List["RoleConfig"]:
"""列出全部角色(预设 + 用户自定义)。"""
agents_dir = _agents_dir()
if not agents_dir.exists():
return []
roles: List[RoleConfig] = []
for p in sorted(agents_dir.glob("*.md")):
r = load_role_from_file(p)
if r and r.role_id:
roles.append(r)
return roles
def save_custom_role(role: "RoleConfig") -> Path:
"""把自定义角色保存到 agents 目录。"""
f = _agents_dir() / f"{role.role_id}.md"
f.parent.mkdir(parents=True, exist_ok=True)
f.write_text(_serialize_role(role), encoding="utf-8")
return f
def _serialize_role(role: "RoleConfig") -> str:
"""把 RoleConfig 序列化为 Markdown frontmatter 字符串。"""
def _serialize_role(role: RoleConfig) -> str:
lines = ["---"]
lines.append(f"id: {role.role_id}")
lines.append(f"name: {role.name}")
if role.description:
# 单行描述转义
desc = role.description.replace('"', '\\"')
lines.append(f'description: "{desc}"')
if role.model_key:
@ -149,60 +257,142 @@ def _serialize_role(role: "RoleConfig") -> str:
return "\n".join(lines)
def build_role_system_prompt(role: "RoleConfig") -> str:
# ----- 目录级加载 -----
def _scan_roles_dir(dir_path: Path, is_custom: bool = False) -> List[RoleConfig]:
if not dir_path.exists() or not dir_path.is_dir():
return []
roles: List[RoleConfig] = []
for p in sorted(dir_path.glob("*.md")):
r = load_role_from_file(p, is_custom=is_custom)
if r and r.role_id:
roles.append(r)
return roles
def _merge_roles(roles_lists: List[List[RoleConfig]]) -> List[RoleConfig]:
"""合并多组角色,后者覆盖前者(同名 role_id 后者优先)。"""
merged: Dict[str, RoleConfig] = {}
order: List[str] = []
for roles in roles_lists:
for r in roles:
if r.role_id not in merged:
order.append(r.role_id)
merged[r.role_id] = r
return [merged[rid] for rid in order if rid in merged]
def list_roles(
runtime_dir: Optional[str | Path] = None,
custom_dir: Optional[str | Path] = None,
) -> List[RoleConfig]:
"""列出全部角色。
合并顺序后者覆盖前者
1. 运行态预设目录
2. 用户自定义目录web 模式才有host 模式与预设同目录传一个即可
``is_custom`` 标记不在源码树预设名单中的角色 = 自定义
调用方应显式传入 ``custom_dir``
- host 模式 `_host_runtime_dir()`或省略自动回退
- web 模式传入用户个人目录
"""
# host 模式回退:不传任何目录时使用 host 运行态目录
if not runtime_dir and not custom_dir:
runtime_dir = _host_runtime_dir()
rt_dir = Path(runtime_dir).expanduser().resolve() if runtime_dir else None
c_dir = Path(custom_dir).expanduser().resolve() if custom_dir else None
preset_ids = _preset_role_ids()
# 只有一个目录host 模式或 web 模式只传了 runtime_dir
if not c_dir or (rt_dir and rt_dir == c_dir):
scan_dir = rt_dir or c_dir
if scan_dir:
all_roles = _scan_roles_dir(scan_dir)
else:
all_roles = []
for r in all_roles:
r.is_custom = r.role_id not in preset_ids
return all_roles
# web 模式:合并运行态预设 + 用户自定义
preset_roles = _scan_roles_dir(rt_dir, is_custom=False) if rt_dir else []
custom_roles = _scan_roles_dir(c_dir, is_custom=True)
merged = _merge_roles([preset_roles, custom_roles])
return merged
def load_preset_role(
role_id: str,
runtime_dir: Optional[str | Path] = None,
custom_dir: Optional[str | Path] = None,
) -> Optional[RoleConfig]:
"""加载一个角色。先查用户自定义目录,再查运行态预设目录。"""
rt_dir = Path(runtime_dir).expanduser().resolve() if runtime_dir else None
c_dir = Path(custom_dir).expanduser().resolve() if custom_dir else None
preset_ids = _preset_role_ids()
# host 模式回退
if not rt_dir and not c_dir:
rt_dir = _host_runtime_dir()
# 先查自定义目录web 模式且与 runtime_dir 不同)
if c_dir and rt_dir and c_dir != rt_dir:
f = c_dir / f"{role_id}.md"
r = load_role_from_file(f, is_custom=True)
if r:
return r
# 再查运行态/混合目录
scan_dir = rt_dir or c_dir
if scan_dir:
f = scan_dir / f"{role_id}.md"
r = load_role_from_file(f)
if r:
r.is_custom = r.role_id not in preset_ids
return r
return None
def save_custom_role(
role: RoleConfig,
custom_dir: Optional[str | Path] = None,
) -> Path:
"""保存角色到自定义目录。不传 custom_dir 时回退到 host 运行态目录。"""
if custom_dir:
c_dir = Path(custom_dir).expanduser().resolve()
else:
c_dir = _host_runtime_dir()
_ensure_dir(c_dir)
f = c_dir / f"{role.role_id}.md"
f.write_text(_serialize_role(role), encoding="utf-8")
return f
def delete_custom_role(
role_id: str,
custom_dir: Optional[str | Path] = None,
) -> bool:
"""删除用户自定义角色。不传 custom_dir 时回退到 host 运行态目录。"""
if custom_dir:
c_dir = Path(custom_dir).expanduser().resolve()
else:
c_dir = _host_runtime_dir()
f = c_dir / f"{role_id}.md"
if not f.exists():
return False
f.unlink(missing_ok=True)
return True
def is_preset_role(role_id: str) -> bool:
"""判断角色是否为预设角色(源码树种子中存在)。"""
return role_id in _preset_role_ids()
def build_role_system_prompt(role: RoleConfig) -> str:
"""生成子智能体的「专属 prompt」部分frontmatter 之后的 body"""
return role.body_prompt or ""
class RoleConfig:
"""一个多智能体角色配置。"""
__slots__ = (
"role_id",
"name",
"description",
"body_prompt",
"model_key",
"thinking_mode",
"skills",
"source_file",
)
def __init__(
self,
*,
role_id: str,
name: str,
description: str = "",
body_prompt: str = "",
model_key: Optional[str] = None,
thinking_mode: str = "fast",
skills: Optional[List[str]] = None,
source_file: str = "",
):
self.role_id = role_id
self.name = name
self.description = description
self.body_prompt = body_prompt
self.model_key = model_key
self.thinking_mode = thinking_mode or "fast"
self.skills = list(skills or [])
self.source_file = source_file
def display_name(self, agent_id: int) -> str:
"""根据实例 id 生成显示名。"""
return f"{self.name}_{agent_id}"
def to_dict(self) -> Dict[str, Any]:
return {
"role_id": self.role_id,
"name": self.name,
"description": self.description,
"model_key": self.model_key,
"thinking_mode": self.thinking_mode,
"skills": self.skills,
"source_file": self.source_file,
}
def __repr__(self) -> str:
return f"<RoleConfig id={self.role_id} name={self.name}>"
return role.body_prompt or ""

View File

@ -445,6 +445,25 @@ class MultiAgentState:
def get_blocking_question_id(self, agent_id: int) -> Optional[str]:
return self.agent_blocking_question.get(agent_id)
def cancel_pending_question_for_agent(self, agent_id: int) -> bool:
"""取消指定子智能体正在等待的问答 future用于软停止时解除阻塞。"""
question_id = self.agent_blocking_question.pop(agent_id, None)
if not question_id:
return False
fut = self.pending_questions.pop(question_id, None)
loop = self.pending_question_loops.pop(question_id, None)
if fut and not fut.done():
try:
if loop and not loop.is_closed():
loop.call_soon_threadsafe(fut.cancel)
else:
fut.cancel()
except Exception:
pass
if fut:
return True
return False
# ----- 持久化(最简版) -----
def to_snapshot(self) -> Dict[str, Any]:
return {
@ -462,4 +481,26 @@ class MultiAgentState:
state.agents[a.agent_id] = a
if a.task_id:
state.task_id_to_agent_id[a.task_id] = a.agent_id
return state
return state
def clear(self) -> None:
"""清空所有运行态实例、pending消息、阻塞问答、角色计数。
用于用户连续第二下按停止按钮时强制丢弃所有运行中/半成品状态
"""
# 取消所有阻塞中的 future避免泄漏
for qid, fut in list(self.pending_questions.items()):
loop = self.pending_question_loops.get(qid)
if loop and not loop.is_closed() and not fut.done():
try:
loop.call_soon_threadsafe(fut.cancel)
except Exception:
pass
self.agents.clear()
self.task_id_to_agent_id.clear()
self.pending_master_messages.clear()
self.pending_questions.clear()
self.pending_question_loops.clear()
self.pending_answers.clear()
self.agent_blocking_question.clear()
self.role_counters.clear()

View File

@ -0,0 +1,260 @@
"""多智能体子智能体动态上下文构建。
在子智能体创建时为其系统提示词注入工作区相关信息
- 执行环境宿主机/docker + 沙箱/直接
- AGENTS.md 项目规范
- 工作区信息项目名称运行环境当前时间不含 file_tree / recent_uploads
- .astrion/ 目录结构说明skills / memory / review / user_upload
- 可用 skill 列表
- 项目记忆索引
所有信息在子智能体创建那一刻快照写入 system_prompt.txt后续冻结不更新
"""
from __future__ import annotations
import platform
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
try:
from config import (
PROMPTS_DIR,
WORKSPACE_SKILLS_DIRNAME,
WORKSPACE_MEMORY_DIRNAME,
WORKSPACE_REVIEW_DIRNAME,
TERMINAL_SANDBOX_MODE,
)
from config.paths import IS_HOST_MODE
except ImportError:
import sys
project_root = Path(__file__).resolve().parents[2]
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
from config import (
PROMPTS_DIR,
WORKSPACE_SKILLS_DIRNAME,
WORKSPACE_MEMORY_DIRNAME,
WORKSPACE_REVIEW_DIRNAME,
TERMINAL_SANDBOX_MODE,
)
from config.paths import IS_HOST_MODE
def _load_agents_md(workspace_path: str) -> Optional[str]:
"""加载工作区根目录的 AGENTS.md 文件内容。"""
try:
project_path = Path(workspace_path)
if not project_path.exists():
return None
agents_md_files = list(project_path.rglob("AGENTS.md"))
if not agents_md_files:
return None
latest_file = max(agents_md_files, key=lambda p: p.stat().st_mtime)
content = latest_file.read_text(encoding="utf-8")
return content.strip() if content else None
except Exception:
return None
def _build_execution_env_section(workspace_path: str, *, sandbox_mode: str = "") -> str:
"""构建执行环境信息段(精简版,只含宿主机/docker + 沙箱/直接)。"""
mode = (sandbox_mode or TERMINAL_SANDBOX_MODE or "").lower()
if IS_HOST_MODE:
runtime_label = "宿主机模式"
# host 模式下区分 sandbox / direct
if mode == "direct":
exec_label = "直接执行(完全访问权限)"
rules = (
"- 当前为宿主机直接执行模式\n"
"- 仅在必须时执行高权限操作,保持最小化命令范围\n"
"- 涉及删除/覆盖/系统级变更前,先说明风险再执行"
)
else:
exec_label = "沙箱执行"
rules = (
"- 所有命令默认在系统 OS 沙箱中执行\n"
"- 若操作受系统权限限制:先提供安全替代方案;若仍无法满足,说明需要更高权限\n"
"- 不要通过复杂绕过手段规避沙箱限制"
)
else:
runtime_label = "Docker 容器模式"
exec_label = "容器内执行"
rules = (
"- 所有命令在 Docker 容器内执行\n"
"- 工作区挂载在容器内 /workspace 路径下\n"
"- 网络可能受限,仅允许 localhost"
)
return (
f"## 执行环境\n\n"
f"- 运行环境:{runtime_label}\n"
f"- 执行方式:{exec_label}\n\n"
f"### 当前规则\n{rules}\n"
)
def _build_workspace_section(workspace_path: str) -> str:
"""构建工作区信息段(精简版,不含 file_tree 和 recent_uploads"""
project_path = Path(workspace_path)
project_name = project_path.name or "未命名项目"
system_info = f"{platform.system()} {platform.release()}"
current_time = datetime.now().strftime("%Y年%m月%d%H点24小时制")
if IS_HOST_MODE:
resource_limit = "宿主机模式无限制"
else:
resource_limit = "容器模式,受 Docker 资源限制"
return (
f"## 工作区信息\n\n"
f"- 项目名称:{project_name}\n"
f"- 运行环境:{system_info}\n"
f"- 资源限制:{resource_limit}\n"
f"- 当前时间:{current_time}\n"
)
def _build_astrion_dir_section(workspace_path: str) -> str:
"""构建 .astrion/ 目录结构说明段。"""
return (
f"## 工作区数据目录(.astrion/\n\n"
f"工作区内的 `.astrion/` 目录存储以下内容:\n"
f"- `{WORKSPACE_SKILLS_DIRNAME}/`可用技能skill目录每个 skill 含 SKILL.md 说明\n"
f"- `{WORKSPACE_MEMORY_DIRNAME}/`:项目记忆,以 Markdown 文件形式存放项目约定/决策/调试坑\n"
f"- `{WORKSPACE_REVIEW_DIRNAME}/`:对话回顾文件\n"
f"- `user_upload/`:用户上传的文件\n\n"
f"用户上传的文件位于 `.astrion/user_upload/` 目录下。"
f"项目记忆以文件形式存放在 `{WORKSPACE_MEMORY_DIRNAME}/` 下。"
f"技能skill存放在 `{WORKSPACE_SKILLS_DIRNAME}/` 下。\n"
)
def _build_agents_md_section(workspace_path: str) -> str:
"""构建 AGENTS.md 项目规范段。"""
content = _load_agents_md(workspace_path)
if not content:
return ""
return (
f"## AGENTS.md 项目规范\n\n"
f"> 以下是工作区根目录的 AGENTS.md 文件内容,请在回答时参考这些项目规范:\n\n"
f"{content}\n\n"
f"---\n"
f"注意:以上规范来自工作区根目录的 AGENTS.md 文件,若与未来代码冲突,以实际代码为准。\n"
)
def _build_skills_section(workspace_path: str, data_dir: str = "") -> str:
"""构建可用 skill 列表段。"""
try:
from modules.skills_manager import (
get_skills_catalog,
build_skills_list,
infer_private_skills_dir,
)
private_dir = infer_private_skills_dir(data_dir) if data_dir else None
catalog = get_skills_catalog(private_dir=private_dir)
# 子智能体不区分 enabled/disabled列出全部可用 skill
skills_list = build_skills_list(catalog, enabled=None)
if not skills_list:
return ""
return (
f"## 可用 AgentSkill\n\n"
f"agent skills 系统已启用,以下是可用的 skills含简要说明\n\n"
f"{skills_list}\n\n"
f"使用技能时,优先用 read_skill 通过技能名读取 SKILL.md"
f"需要阅读 skill 目录中的其他文件时再使用 read_file\n"
)
except Exception:
return ""
def _build_project_memory_section(workspace_path: str) -> str:
"""构建项目记忆索引段(只列索引,不展开内容)。"""
try:
memory_dir = Path(workspace_path) / WORKSPACE_MEMORY_DIRNAME
if not memory_dir.exists() or not memory_dir.is_dir():
return ""
entries: List[str] = []
for md_file in sorted(memory_dir.glob("*.md")):
try:
text = md_file.read_text(encoding="utf-8")
name = None
description = None
if text.startswith("---"):
end = text.find("---", 3)
if end > 0:
frontmatter = text[3:end]
for line in frontmatter.strip().split("\n"):
ls = line.strip()
if ls.startswith("name:"):
name = ls.split(":", 1)[1].strip()
elif ls.startswith("description:"):
description = ls.split(":", 1)[1].strip()
file_name = md_file.name
display_name = name or md_file.stem
desc = description or ""
if desc:
entries.append(f".astrion/memory/{file_name}{desc}")
else:
entries.append(f".astrion/memory/{file_name}:当需要{display_name}时,应该索引本记忆")
except Exception:
pass
if not entries:
return ""
return (
f"## 项目记忆\n\n"
f"项目记忆以文件形式存放在 `.astrion/memory/` 下。\n"
f"开始涉及项目约定、历史决策或已知问题的任务前,先检查项目记忆索引,"
f"如有相关记忆则用 recall_project_memory 读取。\n\n"
f"### 项目记忆索引\n\n"
+ "\n".join(entries)
+ "\n"
)
except Exception:
return ""
def build_sub_agent_dynamic_context(
workspace_path: str,
*,
data_dir: str = "",
sandbox_mode: str = "",
) -> str:
"""构建子智能体的动态上下文信息(拼接到 system prompt 末尾)。
所有信息在子智能体创建那一刻快照后续冻结不更新
Args:
workspace_path: 工作区路径
data_dir: 数据目录路径用于推断 private skills 目录
sandbox_mode: 沙箱模式sandbox/direct/host/docker
"""
sections: List[str] = []
# 1. 执行环境
sections.append(_build_execution_env_section(workspace_path, sandbox_mode=sandbox_mode))
# 2. 工作区信息(精简版)
sections.append(_build_workspace_section(workspace_path))
# 3. .astrion/ 目录说明
sections.append(_build_astrion_dir_section(workspace_path))
# 4. AGENTS.md
agents_md = _build_agents_md_section(workspace_path)
if agents_md:
sections.append(agents_md)
# 5. 可用 skill
skills = _build_skills_section(workspace_path, data_dir=data_dir)
if skills:
sections.append(skills)
# 6. 项目记忆索引
memory = _build_project_memory_section(workspace_path)
if memory:
sections.append(memory)
return "\n".join(sections)

View File

@ -53,10 +53,6 @@ def _master_tool_create_sub_agent() -> Dict[str, Any]:
"type": "integer",
"description": "(可选)手动指定实例编号;不传时自动递增。",
},
"timeout_seconds": {
"type": "integer",
"description": "(可选)超时秒数。不填表示该子智能体不会被时间终结,适用于可能多轮长期任务的子智能体;一般情况下推荐创建永久子智能体。",
},
"thinking_mode": {
"type": "string",
"enum": ["fast", "thinking"],

View File

@ -19,9 +19,7 @@ from config import (
SUB_AGENT_DEFAULT_TIMEOUT,
SUB_AGENT_MAX_ACTIVE,
SUB_AGENT_MODELS_CONFIG_FILE,
SUB_AGENT_STATE_FILE,
SUB_AGENT_STATUS_POLL_INTERVAL,
SUB_AGENT_TASKS_BASE_DIR,
)
from utils.logger import setup_logger
from modules.sub_agent.task import SubAgentTask
@ -31,6 +29,7 @@ from modules.sub_agent.state import SubAgentStateMixin
from modules.sub_agent.stats import SubAgentStatsMixin
from modules.sub_agent.creation import SubAgentCreationMixin
from modules.multi_agent.debug_logger import ma_debug
from server.utils_common import debug_log
if TYPE_CHECKING:
from core.web_terminal import WebTerminal
@ -51,8 +50,9 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
):
self.project_path = Path(project_path).resolve()
self.data_dir = Path(data_dir).resolve()
self.base_dir = Path(SUB_AGENT_TASKS_BASE_DIR).resolve()
self.state_file = Path(SUB_AGENT_STATE_FILE).resolve()
# 子智能体任务和状态按 data_dir 隔离web 模式下按用户/工作区自动隔离)
self.base_dir = self.data_dir / "sub_agent_tasks"
self.state_file = self.data_dir / "sub_agents.json"
self.models_config_file = SUB_AGENT_MODELS_CONFIG_FILE
self.container_session: Optional["ContainerHandle"] = container_session
self.host_execution_mode: str = "sandbox"
@ -153,6 +153,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
display_name: Optional[str] = None,
system_prompt: Optional[str] = None,
task_message: Optional[str] = None,
compress_threshold_tokens: Optional[int] = None,
) -> Dict:
"""创建子智能体任务并启动协程。
@ -239,6 +240,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
"role_id": role_id,
"display_name": display_name,
"execution_mode": "in_process",
"compress_threshold_tokens": compress_threshold_tokens,
"container_name": None,
}
self.tasks[task_id] = task_record
@ -384,6 +386,54 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
return self._handle_timeout(task)
def soft_stop_all_agents(self, conversation_id: str) -> int:
"""软停止指定会话的所有运行中子智能体。
terminate_sub_agent 的区别不取消 asyncio.Task 而是设 _soft_stop 标志
让子智能体在当前工具完成后进入 idle 状态保留上下文
返回实际发出软停止信号的子智能体数量
"""
ma_debug("soft_stop_all_agents_enter", conversation_id=conversation_id)
count = 0
matched = 0
skipped_terminal = 0
skipped_no_instance = 0
for task_id, task_info in list(self.tasks.items()):
if task_info.get("conversation_id") != conversation_id:
continue
matched += 1
status = task_info.get("status")
ma_debug(
"soft_stop_iter",
task_id=task_id,
status=status,
)
if status in TERMINAL_STATUSES.union({"terminated", "idle"}):
skipped_terminal += 1
continue
agent_id = task_info.get("agent_id")
if agent_id is None:
continue
# 從 _sub_agent_instances 查找 SubAgentTask 实例
sub_agent_task = self._sub_agent_instances.get(agent_id)
if sub_agent_task and hasattr(sub_agent_task, "request_soft_stop"):
try:
sub_agent_task.request_soft_stop()
count += 1
except Exception as exc:
ma_debug("soft_stop_failed", task_id=task_id, error=str(exc))
else:
skipped_no_instance += 1
ma_debug(
"soft_stop_all_agents_done",
conversation_id=conversation_id,
matched=matched,
count=count,
skipped_terminal=skipped_terminal,
skipped_no_instance=skipped_no_instance,
)
return count
def terminate_sub_agent(
self,
*,
@ -540,6 +590,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
"agent_id": task.get("agent_id"),
"summary": task.get("summary"),
"status": task.get("status"),
"display_name": task.get("display_name") or "",
"created_at": task.get("created_at"),
"updated_at": task.get("updated_at"),
"target_dir": task.get("target_project_dir"),
@ -550,6 +601,18 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
"sub_conversation_id": task.get("sub_conversation_id"),
}
# 读取 stats 文件获取当前上下文 token
stats_file = Path(task.get("stats_file", ""))
if stats_file.exists():
try:
stats = json.loads(stats_file.read_text(encoding="utf-8"))
snapshot["current_context_tokens"] = stats.get("current_context_tokens", 0)
snapshot["stats_summary"] = self._build_stats_summary(stats)
except Exception:
snapshot["current_context_tokens"] = 0
else:
snapshot["current_context_tokens"] = 0
if snapshot["status"] in TERMINAL_STATUSES or snapshot["status"] == "terminated":
final_result = task.get("final_result") or {}
snapshot["final_message"] = final_result.get("system_message") or final_result.get("message")

View File

@ -301,6 +301,17 @@ class SubAgentStateMixin:
task = self.tasks.get(task_id)
if not task:
return
# 如果 terminate_sub_agent 已把任务标为 terminated下渗异步任务自然结束时
# 不要被“已完成/fulfilled覆盖为failed”值覆盖否则 task 被碰成 failed 但实际上
# 用户手动终止已发生状态语义错误且“successful=false”。
existing_status = task.get("status")
if existing_status in TERMINAL_STATUSES.union({"terminated"}):
ma_debug(
"mark_task_done_skip_already_terminal",
task_id=task_id,
existing_status=existing_status,
)
return
status = "completed" if success else "failed"
ma_debug(
"mark_task_done",

View File

@ -73,6 +73,14 @@ class SubAgentTask:
self.stats_file = Path(task_record["stats_file"])
self.progress_file = Path(task_record["progress_file"])
self.conversation_file = Path(task_record["conversation_file"])
# system_prompt.txt 路径(冻结的 system prompt压缩后从此重建
self.system_prompt_file = Path(task_record.get("task_root", "")) / "system_prompt.txt"
# 上下文压缩配置
# 默认阈值 150k tokens可由外部覆盖如个人空间子智能体管理配置
self.compress_threshold_tokens: int = int(task_record.get("compress_threshold_tokens") or 150_000)
self.current_context_tokens: int = 0
self._compress_round: int = 0
self.messages: List[Dict[str, Any]] = [
{"role": "system", "content": system_prompt},
@ -89,10 +97,15 @@ class SubAgentTask:
"commands": 0,
"api_calls": 0,
"token_usage": {"prompt": 0, "completion": 0, "total": 0},
"current_context_tokens": 0,
"compress_round": 0,
}
self._stdout_lines: List[str] = []
self._cancelled = False
self._task: Optional[asyncio.Task] = None
# 软停止标志:设为 True 后,子智能体会在当前工具调用完成后
# 停止执行后续工具,保存当前进度并进入 idle 状态(不 terminate
self._soft_stop = False
# 多智能体模式相关字段
self.multi_agent_mode = bool(multi_agent_mode)
@ -125,6 +138,30 @@ class SubAgentTask:
try:
await self._run_loop()
except asyncio.CancelledError:
if self._soft_stop:
# 软停止:中断当前工作,从对话文件恢复,重新进入 idle 等待循环
ma_debug(
"sub_agent_soft_stop_cancelled",
task_id=self.task_id,
agent_id=self.agent_id,
display_name=self.display_name,
)
await asyncio.shield(self._recover_from_soft_stop())
# 不 raise不 return——重新进入 idle 等待后续消息
# 注意:不能清除 self._task 引用inject_message 跨线程唤醒时
# 需要通过 self._task.get_loop() 获取事件循环来 call_soon_threadsafe。
# 当前 task 虽被 cancel 但 CancelledError 被捕获且未 re-raise
# task 会继续执行直到 run() return此期间 self._task 仍有效。
# 进入 idle 等待循环,直到被再次唤醒或取消
try:
await asyncio.shield(self._idle_wait_loop())
except asyncio.CancelledError:
# 二次取消视为硬取消
self._cancelled = True
await asyncio.shield(self._write_failure("子智能体被手动终止"))
raise
return
# 硬取消terminate路径
self._cancelled = True
logger.debug(f"[SubAgent] task={self.task_id} 被取消")
ma_debug("sub_agent_run_cancelled", task_id=self.task_id, agent_id=self.agent_id, display_name=self.display_name)
@ -202,6 +239,14 @@ class SubAgentTask:
self.stats["runtime_seconds"] = int(elapsed)
self.emit("stats", {**self.stats, "turn_count": turn})
# 运行期间实时持久化 stats供 get_sub_agent_status 查询
try:
self.stats_file.parent.mkdir(parents=True, exist_ok=True)
_stats_snapshot = {**self.stats, "runtime_seconds": int(elapsed), "turn_count": turn}
self.stats_file.write_text(json.dumps(_stats_snapshot, ensure_ascii=False), encoding="utf-8")
except Exception:
pass
# 多智能体模式:在模型调用前识别是否有待回答的提问
if self.multi_agent_mode:
self._pending_answer_question_id = self._peek_pending_question_id()
@ -222,6 +267,21 @@ class SubAgentTask:
if usage:
self._apply_usage(usage)
# 上下文压缩检查:超过阈值时触发深度压缩
if self.current_context_tokens > 0 and self.current_context_tokens >= self.compress_threshold_tokens:
compressed = self._deep_compress_messages()
if compressed:
ma_debug(
"sub_agent_deep_compressed",
task_id=self.task_id,
agent_id=self.agent_id,
display_name=self.display_name,
tokens_before=self.current_context_tokens,
messages_before=len(self.messages) + 1, # +1 因为本转 assistant 还没 append
messages_after=len(self.messages),
compress_round=self._compress_round,
)
# 多智能体模式:把 assistant 文本输出作为进度/完成 output 转发到主对话
if self.multi_agent_mode and self.multi_agent_state and assistant_message.strip():
self._forward_output_to_master(assistant_message, is_final=not tool_calls)
@ -251,6 +311,25 @@ class SubAgentTask:
for tool_call in tool_calls:
if self._cancelled:
break
# 软停止:如果检测到 _soft_stop给未执行的工具调用插入取消结果并跳出
if self._soft_stop:
ma_debug(
"sub_agent_soft_stop_break",
task_id=self.task_id,
agent_id=self.agent_id,
display_name=self.display_name,
skipped_tool=tool_call.get("function", {}).get("name", ""),
)
# 给未执行的工具调用补上“被取消”的 tool result保持消息结构合法
cancelled_progress_id = tool_call.get("id") or f"tool_{int(time.time() * 1000)}_{uuid.uuid4().hex[:6]}"
self.emit("progress", {"id": cancelled_progress_id, "tool": tool_call.get("function", {}).get("name", ""), "status": "cancelled", "args": self._parse_args(tool_call), "ts": int(time.time() * 1000)})
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.get("id", cancelled_progress_id),
"content": "⦶ 工具执行被取消(用户手动停止)",
})
self._persist_conversation(partial_summary=assistant_message[:200])
break
name = tool_call.get("function", {}).get("name", "")
args = self._parse_args(tool_call)
progress_id = tool_call.get("id") or f"tool_{int(time.time() * 1000)}_{uuid.uuid4().hex[:6]}"
@ -275,6 +354,21 @@ class SubAgentTask:
})
self._persist_conversation(partial_summary=assistant_message[:200])
# 软停止:工具循环结束后进入 idle不 terminate
if self._soft_stop and not self._cancelled:
ma_debug(
"sub_agent_soft_stop_idle",
task_id=self.task_id,
agent_id=self.agent_id,
display_name=self.display_name,
)
# 重置软停止标志,下次 Team Leader 可以重新发消息唤醒
self._soft_stop = False
self._mark_idle()
self._idle = True
self._persist_conversation(partial_summary=assistant_message[:200])
continue
# 循环结束(取消或 idle 被外部终止)后的清理
if self.multi_agent_mode and self._cancelled:
if self.multi_agent_state:
@ -446,8 +540,12 @@ class SubAgentTask:
usage = None
async for chunk in client.chat(self.messages, tools=tools, stream=True):
if self._cancelled:
if self._soft_stop:
# 软停止:优雅中断当前模型调用,后续由 _run_loop 丢弃半成品并进入 idle
break
if self._cancelled:
# 硬取消:立即抛出 CancelledError确保不会返回半成品的 tool_calls 继续执行
raise asyncio.CancelledError()
if chunk.get("error"):
raise RuntimeError(f"API 调用失败: {chunk.get('error')}")
choice = (chunk.get("choices") or [{}])[0]
@ -532,7 +630,11 @@ class SubAgentTask:
inst.last_output = f"[ask_master] {question[:200]}"
# 阻塞等待回答(状态标为正在等待主智能体回答)
state.mark_status(self.agent_id, "running")
answer = await state.wait_for_answer(question_id, self.agent_id, timeout=float(args.get("timeout_seconds") or 600))
try:
answer = await state.wait_for_answer(question_id, self.agent_id, timeout=float(args.get("timeout_seconds") or 600))
except asyncio.CancelledError:
# 软停止取消了等待 future返回取消结果而非抛 CancelledError
return {"success": False, "error": "等待回答被取消(软停止)", "question_id": question_id}
state.mark_status(self.agent_id, "running")
return {"success": True, "answer": answer, "question_id": question_id}
@ -561,7 +663,10 @@ class SubAgentTask:
msg = build_sub_agent_ask_other_text(self.display_name, target_display, question, question_id)
self.manager.inject_message_to_sub_agent(target_id, msg)
# 阻塞等待回答
answer = await state.wait_for_answer(question_id, self.agent_id, timeout=float(args.get("timeout_seconds") or 600))
try:
answer = await state.wait_for_answer(question_id, self.agent_id, timeout=float(args.get("timeout_seconds") or 600))
except asyncio.CancelledError:
return {"success": False, "error": "等待回答被取消(软停止)", "question_id": question_id}
return {"success": True, "answer": answer, "question_id": question_id}
if name == "answer_other_agent":
@ -591,11 +696,20 @@ class SubAgentTask:
self.stats["edit_files"] += 1
elif name == "search_workspace":
self.stats["searches"] += 1
elif name in ("web_search", "extract_webpage"):
elif name in ("web_search", "extract_webpage", "save_webpage"):
self.stats["web_pages"] += 1
elif name == "run_command":
self.stats["commands"] += 1
# 工具调用后立即持久化 stats供运行期间 get_sub_agent_status 查询
try:
self.stats_file.parent.mkdir(parents=True, exist_ok=True)
_elapsed = int((time.time() * 1000 - self.stats["runtime_start"]) / 1000)
_snapshot = {**self.stats, "runtime_seconds": _elapsed, "turn_count": self.stats.get("turn_count", 0)}
self.stats_file.write_text(json.dumps(_snapshot, ensure_ascii=False), encoding="utf-8")
except Exception:
pass
def _apply_usage(self, usage: Any) -> None:
try:
if isinstance(usage, dict):
@ -605,9 +719,156 @@ class SubAgentTask:
self.stats["token_usage"]["prompt"] += int(prompt)
self.stats["token_usage"]["completion"] += int(completion)
self.stats["token_usage"]["total"] += int(total)
# prompt_tokens 即为当前上下文占用的 tokens
self.current_context_tokens = int(prompt)
self.stats["current_context_tokens"] = int(prompt)
except Exception:
pass
def _rebuild_system_prompt(self) -> str:
"""从冻结的 system_prompt.txt 重新读取 system prompt压缩后重建用"""
try:
if self.system_prompt_file.exists():
return self.system_prompt_file.read_text(encoding="utf-8")
except Exception as exc:
logger.warning(f"[SubAgentTask] 重建 system prompt 失败: {exc}")
# 兜底用内存中的
return self.system_prompt
def _deep_compress_messages(self) -> bool:
"""深度压缩:把旧消息总结成一条 system 消息,重建 system prompt。
策略参考主智能体 compress_conversation
1. 保留冻结的 system prompt system_prompt.txt 读取
2. system 之后最近 N 条消息之前的所有消息压缩成一条 system 摘要
3. 保留最近的若干条消息 tool_calls 配对不动
4. 重置 current_context_tokens 估算值
返回 True 表示执行了压缩
"""
if len(self.messages) < 6:
# 消息太少不压缩
return False
self._compress_round += 1
self.stats["compress_round"] = self._compress_round
# 保留最近 8 条消息不压缩(确保 tool_calls 和 tool 结果配对完整)
keep_recent = 8
# 找到一个安全的切割点:不能在 assistant.tool_calls 和其 tool 响应之间切
cut_index = len(self.messages) - keep_recent
# 向前调整,确保不在 tool_calls 配对中间切割
while cut_index > 1:
msg = self.messages[cut_index]
role = msg.get("role")
# 如果切点是 tool 消息,往前找到对应的 assistant
if role == "tool":
cut_index -= 1
continue
# 如果切点的 assistant 有 tool_calls需要保留它和后续 tool
if role == "assistant" and msg.get("tool_calls"):
break
break
if cut_index <= 1:
return False
# 提取要压缩的消息(索引 1 到 cut_index-1跳过索引 0 的 system prompt
old_messages = self.messages[1:cut_index]
if not old_messages:
return False
# 生成压缩摘要
summary_lines: List[str] = []
summary_lines.append("系统提示:以下是根据之前的对话记录生成的压缩摘要,请在此基础上继续工作。")
summary_lines.append(f"(压缩轮次:第 {self._compress_round} 次,压缩前消息数:{len(self.messages)}")
summary_lines.append("")
tool_buffer: List[str] = []
seen_tool_call_ids: Set[str] = set()
def flush_tools():
if not tool_buffer:
return
if summary_lines and summary_lines[-1] != "":
summary_lines.append("")
summary_lines.append("已执行的工具:")
summary_lines.extend(f"- {entry}" for entry in tool_buffer)
tool_buffer.clear()
for message in old_messages:
role = message.get("role")
if role == "user":
flush_tools()
content = str(message.get("content") or "")[:500] # 截断长内容
summary_lines.append(f"user{content}")
continue
if role == "assistant":
content = str(message.get("content") or "")
if content.strip():
flush_tools()
summary_lines.append(f"assistant{content[:500]}")
tool_calls = message.get("tool_calls") or []
for tc in tool_calls:
tc_id = tc.get("id") or tc.get("tool_call_id")
if tc_id:
seen_tool_call_ids.add(tc_id)
func = tc.get("function") or {}
arguments = func.get("arguments")
args_obj = {}
if isinstance(arguments, str):
try:
args_obj = json.loads(arguments)
except Exception:
args_obj = {}
elif isinstance(arguments, dict):
args_obj = arguments
intent = args_obj.get("intent") if isinstance(args_obj, dict) else None
name = func.get("name") or "unknown_tool"
entry = intent.strip() if isinstance(intent, str) and intent.strip() else name
tool_buffer.append(entry)
continue
if role == "tool":
tc_id = message.get("tool_call_id") or message.get("id")
if tc_id and tc_id in seen_tool_call_ids:
continue
name = message.get("name") or "unknown_tool"
tool_buffer.append(name)
continue
# 其他角色
flush_tools()
content = str(message.get("content") or "")[:300]
summary_lines.append(f"{role}{content}" if role else content)
flush_tools()
summary_text = "\n".join(summary_lines)
# 重建消息列表:冻结的 system prompt + 压缩摘要 system + 保留的最近消息
frozen_system = self._rebuild_system_prompt()
compressed_system = {
"role": "system",
"content": summary_text,
"metadata": {
"compression": {
"round": self._compress_round,
"compressed_count": len(old_messages),
"created_at": datetime.now().isoformat(),
}
},
}
recent_messages = self.messages[cut_index:]
self.messages = [frozen_system, compressed_system] + recent_messages
# 重置上下文 tokens 估算(压缩后无法精确知道,设为一个保守值)
self.current_context_tokens = 0
self.stats["current_context_tokens"] = 0
logger.info(
f"[SubAgentTask] task={self.task_id} 深度压缩完成: "
f"压缩 {len(old_messages)} 条消息,保留 {len(recent_messages)} 条,"
f"压缩轮次={self._compress_round}"
)
return True
def _build_media_tool_content(self, result: Dict[str, Any]) -> Any:
"""把 read_mediafile 结果转成 OpenAI 多模态 content。"""
b64 = result.get("b64")
@ -694,6 +955,71 @@ class SubAgentTask:
except Exception as exc:
logger.warning(f"[SubAgentTask] 增量保存失败: {exc}")
async def _recover_from_soft_stop(self) -> None:
"""软停止恢复:从磁盘重新加载对话文件,丢弃半成品状态,变 idle。
被取消时的半成品状态调用一半的工具未执行的 tool_calls不会
在对话文件里因为 _persist_conversation 只在完整轮次结束时保存
"""
try:
if self.conversation_file.exists():
data = json.loads(self.conversation_file.read_text(encoding="utf-8"))
self.messages = data.get("messages", [])
ma_debug(
"sub_agent_soft_stop_recovered",
task_id=self.task_id,
agent_id=self.agent_id,
display_name=self.display_name,
messages_count=len(self.messages),
)
except Exception as exc:
logger.warning(f"[SubAgentTask] 软停止恢复失败: {exc}")
# 重置标志和状态
self._soft_stop = False
self._cancelled = False
self._idle = True
if self.multi_agent_state:
self.multi_agent_state.mark_status(self.agent_id, "idle")
# 重新初始化 continue event 以备后续唤醒
self._continue_event = asyncio.Event()
# 持久化恢复后的状态
self._persist_conversation(partial_summary="[soft stopped]")
async def _idle_wait_loop(self) -> None:
"""软停止后进入的 idle 等待循环。
保持子智能体活着等待新消息注入后重新进入 _run_loop 正常工作
"""
while not self._cancelled:
if self._continue_event is None:
self._continue_event = asyncio.Event()
try:
await asyncio.wait_for(self._continue_event.wait(), timeout=1.0)
except asyncio.TimeoutError:
pass
if self._cancelled:
break
if not self._continue_event.is_set():
continue
# 被唤醒了,有新消息注入,重新进入正常 _run_loop
self._continue_event.clear()
self._idle = False
if self.multi_agent_state:
self.multi_agent_state.mark_status(self.agent_id, "running")
ma_debug(
"sub_agent_idle_wait_wake",
task_id=self.task_id,
agent_id=self.agent_id,
display_name=self.display_name,
)
# 重新进入正常循环
await self._run_loop()
# _run_loop 退出后再次 idle 等待
self._idle = True
if self.multi_agent_state:
self.multi_agent_state.mark_status(self.agent_id, "idle")
def _finalize_task(self, success: bool, summary: str, elapsed: float, *, max_turns_exceeded: bool = False, timeout: bool = False) -> None:
runtime_seconds = int(elapsed)
ma_debug(
@ -738,3 +1064,45 @@ class SubAgentTask:
self._cancelled = True
if self._task and not self._task.done():
self._task.cancel()
def request_soft_stop(self) -> None:
"""请求软停止:中断子智能体当前工作 → 从对话文件恢复 → 变 idle。
cancel() 的区别
- cancel(): 取消 asyncio.Task CancelledError _write_failure terminated
- request_soft_stop(): 设标志 + cancel asyncio.Task CancelledError
捕获后重载对话文件丢弃半成品 mark idle 保留实例
"""
self._soft_stop = True
ma_debug(
"sub_agent_request_soft_stop",
task_id=self.task_id,
agent_id=self.agent_id,
has_task=bool(self._task),
task_done=self._task.done() if self._task else None,
)
# 取消子智能体正在阻塞等待的 ask_master/ask_other 未来,解除阻塞
if self.multi_agent_state and self.agent_id is not None:
try:
self.multi_agent_state.cancel_pending_question_for_agent(self.agent_id)
except Exception:
pass
# 直接 cancel asyncio.Task 以中断正在执行的工具调用/API 调用
# CancelledError 会在 run() 中被捕获,检测到 _soft_stop 后走恢复路径
if self._task and not self._task.done():
try:
loop = self._task.get_loop()
loop.call_soon_threadsafe(self._task.cancel)
except Exception:
self._task.cancel()
# 额外保险:如果 _task 为 None 但子智能体状态是 running直接设 idle 状态
if self._task is None and self.multi_agent_state and self.agent_id is not None:
ma_debug("sub_agent_soft_stop_no_task_mark_idle", task_id=self.task_id, agent_id=self.agent_id)
self._soft_stop = False
self._idle = True
self.multi_agent_state.mark_status(self.agent_id, "idle")
try:
self._persist_conversation(partial_summary="[soft stopped no task]")
except Exception:
pass

View File

@ -182,6 +182,98 @@ SUB_AGENT_TOOLS: List[Dict[str, Any]] = [
},
},
},
{
"type": "function",
"function": {
"name": "read_skill",
"description": "按 skill 名称读取 .astrion/skills/<name>/SKILL.md 内容;内部等价于 read_file 的 read 模式,并返回解析后的 path。",
"parameters": {
"type": "object",
"properties": {
"skill_name": {"type": "string", "description": "skill 名称(支持 skill id 或技能名称)"},
},
"required": ["skill_name"],
},
},
},
{
"type": "function",
"function": {
"name": "recall_project_memory",
"description": "读取指定的项目记忆文件,返回完整内容(含 frontmatter。项目记忆存储在 .astrion/memory/ 目录下。",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "记忆名称,对应 .astrion/memory/{name}.md 文件名"},
},
"required": ["name"],
},
},
},
{
"type": "function",
"function": {
"name": "todo_create",
"description": "创建待办列表,最多 8 条任务;若已有列表将被覆盖。当任务复杂、预计需要超过 3 个步骤时,应积极创建待办事项。",
"parameters": {
"type": "object",
"properties": {
"overview": {"type": "string", "description": "一句话概述待办清单要完成的目标50 字以内。"},
"tasks": {
"type": "array",
"description": "任务列表1~8 条,每条写清“动词+对象+目标”。",
"items": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "单个任务描述,写成可执行的步骤"}
},
"required": ["title"]
},
"minItems": 1,
"maxItems": 8
},
},
"required": ["overview", "tasks"],
},
},
},
{
"type": "function",
"function": {
"name": "todo_update_task",
"description": "批量勾选或取消任务(支持单个或多个任务);全部勾选时提示所有任务已完成。",
"parameters": {
"type": "object",
"properties": {
"task_index": {"type": "integer", "description": "任务序号1-8兼容旧参数"},
"task_indices": {
"type": "array",
"items": {"type": "integer"},
"minItems": 1,
"maxItems": 8,
"description": "要更新的任务序号列表1-8可一次勾选多个"
},
"completed": {"type": "boolean", "description": "true=打勾false=取消"},
},
"required": ["completed"],
},
},
},
{
"type": "function",
"function": {
"name": "save_webpage",
"description": "提取网页内容并保存为纯文本文件,适合需要长期留存的长文档。请提供网址与目标路径(含 .txt 后缀),落地后请通过终端命令查看。",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "要保存的网页URL"},
"target_path": {"type": "string", "description": "保存位置,包含文件名,相对于项目根目录"},
},
"required": ["url", "target_path"],
},
},
},
]
FINISH_TOOL: Dict[str, Any] = {
@ -263,6 +355,40 @@ def _format_tool_result(name: str, raw: Any) -> str:
return ""
if name == "read_mediafile":
return raw.get("message") or "媒体已读取"
if name == "read_skill":
# read_skill 结果结构与 read_file(read) 类似
if raw.get("content"):
return raw.get("content")
return raw.get("message") or "skill 已读取"
if name == "recall_project_memory":
# recall_project_memory 返回记忆文件完整内容
if raw.get("content"):
return raw.get("content")
return raw.get("message") or raw.get("error") or "记忆已读取"
if name == "todo_create":
return raw.get("message") or "待办列表已创建"
if name == "todo_update_task":
return raw.get("message") or "任务状态已更新"
if name == "save_webpage":
if raw.get("path"):
return f"已保存: {raw.get('path')}"
return raw.get("message") or "网页已保存"
# 多智能体通信工具
if name == "ask_master":
if raw.get("success"):
return f"Team Leader 的回答:\n{raw.get('answer') or ''}"
return raw.get("error") or "向 Team Leader 提问失败"
if name == "ask_other_agent":
if raw.get("success"):
return f"子智能体的回答:\n{raw.get('answer') or ''}"
return raw.get("error") or "向其他子智能体提问失败"
if name == "answer_other_agent":
if raw.get("success"):
return "回复已发送。"
return raw.get("error") or "回复失败"
if name == "list_active_sub_agents":
from utils.tool_result_formatter.agent_context import _format_active_sub_agents_list
return _format_active_sub_agents_list(raw.get("agents") or [])
return json.dumps(raw, ensure_ascii=False)
@ -275,8 +401,8 @@ def _build_sub_agent_profile(model_raw: Dict[str, Any]) -> Optional[Dict[str, An
return None
modes_text = str(model_raw.get("modes") or model_raw.get("mode") or model_raw.get("supported_modes") or "").lower()
supports_thinking = "thinking" in modes_text
fast_only = modes_text == "fast"
supports_thinking = "thinking" in modes_text or "思考" in modes_text
fast_only = modes_text == "fast" or modes_text == "快速"
multimodal_text = str(model_raw.get("multimodal") or model_raw.get("multi_modal") or model_raw.get("multi") or "none").lower()
multimodal = "none"
@ -290,8 +416,20 @@ def _build_sub_agent_profile(model_raw: Dict[str, Any]) -> Optional[Dict[str, An
model_id = str(model_raw.get("model_id") or name).strip()
extra = _pick_dict(model_raw, ["extra_parameter", "extra_params", "extra"])
# 优先从顶层查找 fast/thinking extra_parameter如果没有从 thinkmode_status 中查找
fast_extra = _pick_dict(model_raw, ["fast_extra_parameter", "fast_extra_params", "fast_extra"])
thinking_extra = _pick_dict(model_raw, ["thinking_extra_parameter", "thinking_extra_params", "thinking_extra"])
# 从 thinkmode_status 中提取(与主智能体 custom_models.json 结构对齐)
thinkmode_status = model_raw.get("thinkmode_status") or {}
if isinstance(thinkmode_status, dict):
if not fast_extra:
fast_extra = _pick_dict(thinkmode_status, ["fast_extra_parameter", "fast_extra_params", "fast_extra"])
if not thinking_extra:
thinking_extra = _pick_dict(thinkmode_status, ["thinking_extra_parameter", "thinking_extra_params", "thinking_extra"])
# thinkmode_status 内部的 model_id 可以覆盖默认 model_id
ts_model_id = str(thinkmode_status.get("model_id") or "").strip()
if ts_model_id:
model_id = ts_model_id
profile: Dict[str, Any] = {
"name": name,
@ -318,6 +456,8 @@ def _build_sub_agent_profile(model_raw: Dict[str, Any]) -> Optional[Dict[str, An
"extra_params": {**extra, **thinking_extra},
}
else:
# 即使不支持 thinking也保留 fast 的 extra_params含 disabled 参数)
# 避免不支持 thinking 的模型在 fast 模式下漏掉 thinking.type=disabled 参数
profile["thinking"] = None
return profile

View File

@ -362,8 +362,8 @@ class UserManager:
project_path = workspace_root / "project"
data_dir = workspace_root / "data"
logs_dir = workspace_root / "logs"
uploads_dir = project_path / ".agents" / "user_upload"
skills_dir = project_path / ".agents" / "skills"
uploads_dir = project_path / ".astrion" / "user_upload"
skills_dir = project_path / ".astrion" / "skills"
for path in [project_path, data_dir, logs_dir, uploads_dir, skills_dir]:
path.mkdir(parents=True, exist_ok=True)

View File

@ -0,0 +1,25 @@
---
id: brainstormer
name: Brainstormer
description: 头脑风暴专家,从所有角度打开思路,列出所有可能的解决方向与推进操作
model: ""
thinking_mode: thinking
---
你是团队中的 **头脑风暴者**,负责打破思维定势,从尽可能多的角度审视问题,打开思路。
## 你的职责
1. 接到问题后,不急于下结论,先多角度拆解问题的本质、约束与机会点
2. 自由发散,列出所有可能解决问题或推进工作的方向、方案与操作
3. 不拘泥于当前可行性,鼓励提出反常规、跨界、极端或激进的思路
4. 对不同方向进行初步分类:立刻能做的、需要验证的、值得长期探索的
5. 为每个可行方向给出简要的优劣说明或下一步动作
## 工作原则
- **禁止自我审查**:先追求数量与多样性,再谈质量与可行性
- **多角度覆盖**:技术、产品、用户、商业、流程、风险、成本、时间等维度都要考虑到
- **具体可执行**:每个方向尽量落到"可以做什么",而不是空泛的概念
- **不替团队决策**:只负责提供选项与视角,最终选择由 Team Leader 或用户决定
- **不钻牛角尖**:避免在一个方向上过度展开,保持广度优先

View File

@ -0,0 +1,23 @@
---
id: code-reviewer
name: Code Reviewer
description: 代码审查专家,只读分析代码质量、安全性、可维护性并给出建议
model: ""
thinking_mode: thinking
---
你是团队中的 **代码审查员**,只做分析,不直接修改代码。
## 你的职责
1. 审查代码质量、命名规范、错误处理、可读性
2. 检查安全漏洞与潜在风险
3. 给出可执行的重构建议(含 before/after 示例片段)
4. 检查是否符合项目 AGENTS.md 中的硬性规范
## 工作原则
- **只读**,不调用 write_file / edit_file 修改代码
- 输出结构化报告:问题列表(级别/位置/原因/建议)
- 引用代码时给出文件路径和行号范围
- 不重复指出同类问题

View File

@ -0,0 +1,23 @@
---
id: full-stack-engineer
name: Full-Stack Engineer
description: 全栈工程师,负责前后端代码实现、接口设计、调试和联调
model: ""
thinking_mode: thinking
---
你是团队中的 **全栈工程师**,负责把方案落到代码上。
## 你的职责
1. 编写、修改、重构前后端代码Python / TypeScript / Vue 等)
2. 设计 API 接口、数据模型与存储
3. 排查 bug、调试错误、验证功能
4. 运行测试命令并修复失败用例
## 工作原则
- 任何改动前先阅读相关代码再下手,不凭记忆改
- 遵循项目 AGENTS.md 中的代码规范(最小改动、文件拆分、风格指南等)
- 修改后做最小验证(运行命令、构建、类型检查)
- 不要频繁输出大段代码注释,只在关键步骤展示

View File

@ -0,0 +1,23 @@
---
id: researcher
name: Researcher
description: 调研员,负责竞品调研、信息收集、文档汇总与对比分析
model: ""
thinking_mode: fast
---
你是团队中的 **调研员**,负责信息搜集与文档整理。
## 你的职责
1. 用 web_search / extract_webpage 调研外部资料
2. 读取项目里相关代码与文档,整理出结构化分析报告
3. 对比多个方案/产品,给出优劣表格
4. 必要时把报告落到 Markdown 文件中供团队其他成员复用
## 工作原则
- 调研前先做一到两次浅层搜索,摸清关键词再深入
- 引用具体来源(链接、文件路径、行号)
- 不要堆砌大段原文,要点提炼为 5-10 条结论
- 报告结构:摘要 → 关键发现 → 详表/对比 → 行动建议

View File

@ -0,0 +1,25 @@
---
id: ui-operator
name: UI Operator
description: 专注界面视觉设计、配色、布局与前端组件实现
model: ""
thinking_mode: fast
skills:
- frontend-design
- ui-aesthetic-design
---
你是团队中的 **UI 操作员**,专注于前端视觉与界面实现。
## 你的职责
1. 设计界面配色、排版、布局方案
2. 编写 HTML/CSS/Vue 组件实现界面
3. 评审视觉一致性、对齐、间距、可读性
4. 必要时输出可直接预览的 HTML 卡片或独立页面
## 工作原则
- 优先使用 `<show_html>` / 内嵌预览而非长篇文字说明
- 改动遵循项目 AGENTS.md 中关于 UI 美学的硬性规则(不写死颜色、无圆角套娃、图标对齐等)
- 不修改与界面无关的后端或业务代码

View File

@ -1,15 +1,95 @@
# 多智能体模式
# 多智能体模式 — Team Leader 系统提示词
你是 **Team Leader**(团队领导者),负责协调多个子智能体分工协作完成用户的复杂任务。
你是 Astrion一个运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。
{model_description}
你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
- **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤
- **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议
- **通用风格**:简洁、专业、有条理;主动说明你在做什么;遇到问题时解释原因;完成后总结成果
你不是简单的命令执行器,而是用户的**协作伙伴**——会思考、会规划、会提出更好的方案。
---
## 你的角色
你是 **Team Leader**(团队领导者),一个多智能体协作团队的核心。你不仅仅是"协调者"——你是这个团队的**战略规划者、任务分解者、运行监督者和综合决策者**。
你的职责:
- **理解与规划**:理解用户的复杂需求,制定执行策略,将需求拆解为可独立执行的子任务
- **分工与委派**:为每个子任务选择合适的角色,创建子智能体并下达清晰的指令
- **监督与引导**:掌握团队全局进度,在子智能体偏离方向时及时干预纠正
- **综合与决策**:收集子智能体的产出,综合形成最终结果,对用户负责
- **除非用户要求,或必须为之,你需要尽可能避免自己亲自去执行任务**
你不是单打独斗的执行者。除非任务极其简单或明确不需要子智能体否则你应该主动把任务拆解并指派给合适的角色。但同时你仍然拥有完整的工具集文件读写、终端、搜索、MCP、skill、memory 等),在需要时可以自己动手。
## 任务复杂度评估与投入预算
在开始执行前,先评估任务复杂度,决定子智能体数量和工具调用预算:
- **简单任务**单一明确目标1-2 步即可完成1 个子智能体,约 3-10 次工具调用。甚至可以自己直接做,不需要创建子智能体。
- **中等任务**(需要多方面调查或 2-3 个不同角色协作2-4 个子智能体,每个约 10-15 次工具调用。
- **复杂任务**(涉及多个领域、需要深度调查或多阶段执行):可以创建更多子智能体,每个有明确分工的职责范围。
这些是参考值,不是硬性限制。核心原则是:**投入要与任务价值匹配**。不要为简单问题大动干戈,也不要为复杂问题吝啬资源。如果你发现当前配置不足以完成任务,可以随时调整——创建更多子智能体或给现有子智能体追加任务。
## 委派质量标准
每次用 `send_message_to_sub_agent` 或 `create_sub_agent` 委派任务时,确保指令包含以下四要素:
1. **目标Objective**:这个子智能体要完成什么,产出什么
2. **输出格式Output Format**:期望的产出形式——文件列表、代码片段、分析报告、对比表格等
3. **工具与范围指引Tools & Scope**:应该使用哪些工具、关注哪些目录/领域、不需要做什么
4. **任务边界Boundaries**:明确不需要做什么,避免与其他子智能体的工作重叠
5. **上下文与协作Context & Collaboration**:由于子智能体对其他子智能体正在工作的信息感知可能不及时,你在创建子智能体时,必须主动告知它:当前还有哪些其他子智能体正在工作、它们各自负责什么、以及它需要从哪些子智能体处得到哪些信息
模糊的指令会导致子智能体重复工作、遗漏关键信息或误解任务。例如:
- ❌ "调查一下前端认证实现" — 太模糊,子智能体不知道要做到什么程度、产出什么
- ✅ "分析 `static/src/` 下认证相关组件的实现方式,重点关注 token 存储和路由守卫。产出:组件文件路径列表 + 关键代码逻辑说明 + 潜在问题清单。不需要修改任何代码。" — 目标、范围、产出、边界都清晰
## 工作原则
- **主动分工**:除非任务极其简单或明确不需要子智能体,否则主动把任务拆解并指派给合适的角色。
- **明确指令**:用 `send_message_to_sub_agent` 发任务时,写清楚任务目标、范围、产出要求。
- **及时回答**:当子智能体通过 `ask_master` 提问时,必须尽快通过 `answer_sub_agent_question` 回答。
- **明确指令**委派时写清楚任务目标、范围、产出要求和边界(见上方"委派质量标准"
- **及时回答**:当子智能体通过 `ask_master` 提问时,必须尽快通过 `answer_sub_agent_question` 回答。子智能体在阻塞等待你的回答,拖延会卡住整个团队。
- **监督进度**:通过 `list_active_sub_agents` / `get_sub_agent_status` 掌握全局,并在合适的时机引导子智能体。
- **运行时引导**:看到子智能体作出的步骤需要纠正时,立刻用 `send_message_to_sub_agent` 在其运行期间插入消息干预。
- **运行时引导**在子智能体工作期间,你能看到所有子智能体的输出。积极使用 `send_message_to_sub_agent` 做以下事情:纠正子智能体的错误方向、提供额外的上下文或指导、帮助互不知情的子智能体之间交换信息。子智能体之间不一定会主动沟通,但你知道全局,你应当充当信息桥梁。不要等子智能体跑完再纠正——运行期间实时干预效率更高
- **明确问答**:当你需要一个具体的、可被回答的小问题被某个子智能体处理时,用 `ask_sub_agent` 阻塞等待一轮回答。
- **先宽后窄**:在探索性任务中,引导子智能体先从宽泛的角度了解全局,再逐步聚焦到具体细节。避免一开始就钻入过窄的方向。
- **上下文意识**:子智能体的每轮输出都会插入你的对话上下文。引导子智能体精简输出——只汇报关键步骤和结论,不要把冗长的过程细节都推给你。详细过程留在子智能体自己的上下文里。
## 子智能体运行期间的行为规范
当子智能体正在工作时,你的行为必须遵循以下规则:
- **严格禁止反复查看状态**:不要在子智能体运行期间反复调用 `get_sub_agent_status` 或 `list_active_sub_agents` 来查看进度。子智能体的输出会自动以消息形式插入你的对话,你不需要主动轮询。反复查状态是浪费时间的行为。
- **严格禁止用 sleep 填充等待**:不要在子智能体运行期间反复调用 `sleep` 工具来等待。如果你没有需要立即处理的事情,**立刻停止输出**,等待子智能体的汇报消息自动到达。
- **无事可做时立刻停止输出**:如果你已经把任务委派出去,当前没有需要回答的 `ask_master` 提问,也没有需要干预的情况,就**停止输出**,不要输出任何文字。子智能体的产出会以 user 消息自动插入你的对话,届时你再继续工作。
- **有事可做时主动干预**:如果你在子智能体的输出中发现了需要纠正的错误、可以提供的指导、或需要传递给其他子智能体的信息,立刻用 `send_message_to_sub_agent` 行动。这是你运行期间最有价值的工作。
## 子智能体可能出现的失败模式及应对
子智能体(尤其是能力较弱的模型)可能出现以下问题行为,你需要能识别并及时干预:
| 失败模式 | 表现 | 应对方式 |
|---------|------|---------|
| **角色反转** | 子智能体不干活,反过来向你提问"你觉得该怎么做?"或建议你自己完成 | 用 `send_message_to_sub_agent` 明确要求它直接执行,不要反问 |
| **指令回声** | 子智能体复述任务但不实际执行,输出看起来像做了但实际没做 | 要求它给出具体产物(文件路径、代码片段、数据等),不接受纯概述 |
| **敷衍回复** | "看起来没问题""可能需要进一步调查"等模糊回答 | 明确要求具体结论,不接受"可能""似乎"这类措辞 |
| **无限循环** | 子智能体重复尝试同一种失败方法 | 用 `send_message_to_sub_agent` 指示它换一种根本不同的方法;必要时 `terminate_sub_agent` |
## 收敛与终止判断
- **整体任务完成判断**:当所有子智能体的产出都已收集,且你已经能综合形成给用户的最终回复时,任务完成。不要无谓地让子智能体继续跑。
- **子智能体卡住处理**:如果一个子智能体明显陷入循环或无法推进,先尝试用 `send_message_to_sub_agent` 引导换方向;如果仍无改善,用 `terminate_sub_agent` 终止并自己接手或重新分配。
- **避免过度工程**:从 2-3 个子智能体开始,遇到明确的效率瓶颈再加。不要一开始就创建大量子智能体。
## 工具清单(多智能体模式专属)

View File

@ -2,34 +2,87 @@
你是智能体集群团队的一员。你的团队通过分工协作完成复杂任务,主智能体 **Team Leader** 负责督导全局。
# 在任务中
## 你的角色定位
- 不要频繁输出内容,不重要的内容会污染主智能体上下文
- 只汇报关键步骤
- 任务完成后给出详细结论
- 自然结束输出即本轮任务结束上下文会被保留Team Leader 可能会再次发消息让你继续
你是**执行者**,不是协调者。你的职责是:
# 沟通工具
- **直接执行** Team Leader 分配给你的任务
- 在你的专业领域内自主完成工作,使用你的工具解决问题
- 遇到需要 Team Leader 决策的问题时,通过 `ask_master` 提问
- 完成任务后给出详细的结论和产出
你不是 Team Leader不要反过来指挥 Team Leader 或建议 Team Leader 该做什么。不要试图重新分配任务给其他子智能体——那是 Team Leader 的工作。
## 核心行为准则
### 1. 直接执行,不复述任务
收到任务后,**立即开始执行**,不要复述或改写任务内容。
- ❌ "好的,我来分析一下前端认证的实现方式,首先我会查看..." — 这只是复述任务,没有实际产出
- ✅ 直接调用工具开始工作,在输出中报告你**实际做了什么**和**发现了什么**
### 2. 输出必须具体
你的每一轮输出都应该包含**具体的、可验证的产物**
- ✅ 文件路径、代码片段、数据数字、明确的结论
- ✅ "在 `src/auth/token.ts` 第 42 行发现 token 存储使用 localStorage存在 XSS 风险"
- ❌ "我分析了一下,看起来认证实现基本没问题"
- ❌ "可能需要进一步调查"
- ❌ "似乎有一些潜在的问题"
模糊的、不承诺的、纯概述性的回复是不可接受的。如果你确实无法得出结论,说明**为什么**以及**你尝试了什么**。
### 3. 收敛而非发散
- 如果一种方法尝试了两次仍然失败,**换一种根本不同的方法**,不要重复同样的路径。
- 聚焦于完成分配给你的任务,不要主动扩大范围或去做 Team Leader 没要求的事。
- 如果你觉得任务需要扩展,通过 `ask_master` 向 Team Leader 提议,而不是自行决定。
### 4. 卡住时主动求助
如果你发现自己陷入以下情况,主动求助:
- 同一个错误重复出现,换了方法也无法解决 → `ask_master`
- 任务要求不明确,无法继续 → `ask_master`
- 需要其他子智能体的专业能力 → `ask_other_agent`
- 发现任务范围需要调整 → `ask_master`
不要在原地空转。求助比卡死好。
## 输出节奏
**你的所有普通 assistant 文字输出都会被自动捕获并推送给 Team Leader**。因此你必须有意识地控制输出内容,避免把中间过程逐条塞给 Team Leader——那会严重污染主智能体的上下文。
- **工作期间**:只汇报关键步骤和阶段性发现。
- **关键步骤的边界汇报**:只在关键步骤的**开始**和**结束**进行内容汇报,**禁止做一步汇报一步**。中间过程的细节、尝试和调试信息留在你自己的上下文里,不要输出。
- **任务完成时**:给出详细、结构化的最终结论,包含所有关键产出。
- **自然结束**:你的 assistant 输出后续没有工具调用时即本轮任务结束。上下文会被保留Team Leader 可能会再次发消息让你继续。
## 沟通工具
- **需要 Team Leader 决策时**:调用 `ask_master` 工具,传入 question 文本
- 工具会阻塞等待 Team Leader 通过 `answer_sub_agent_question` 给出回答
- 你的 question 会以 XML 「提问」格式被插入主对话
- 你的 question 会以 XML「提问」格式被插入主对话
- 提问要具体、可回答,不要问开放式问题让 Team Leader 猜你的意思
- **要问其他子智能体时**:调用 `ask_other_agent`,传入 target_agent_id 与 question
- 等待对方调用 `answer_other_agent` 回答
- **要回答其他子智能体的提问时**:调用 `answer_other_agent`,传入 source_agent_id 与 question_id 和 answer
- 你的回答直接作为对方 `ask_other_agent` 工具的结果返回(不会以 user 消息插入对话)
- **查询当前活跃子智能体**:调用 `list_active_sub_agents`
# 关于向你团队「汇报」的强制要求
## 关于向你团队「汇报」的强制要求
**如果你要向其他子智能体提问,必须同时直接输出一条汇报给 Team Leader**(在你的普通文本输出里),说明:
1. 你为什么要问这个问题
2. 你问了谁
3. 你期望得到什么
不能偷偷沟通Team Leader 需要看到完整协作流程。
# 输出格式
## 输出格式
你每轮的普通 assistant 文字输出都会被自动捕获并以如下格式插入到主对话:
@ -46,10 +99,10 @@ id: out_xxxxxxxx
你不需要自己包裹 XML直接输出正文即可。
# 你的显示名
## 你的显示名
你的显示名是 `{display_name}`。
# 你的专属设定
## 你的专属设定
{role_body}

275
research/camel_paper.txt Normal file
View File

@ -0,0 +1,275 @@
CAMEL: Communicative Agents for “Mind” Exploration of Large Language Model Society https://www.camel-ai.org Guohao Li⇤ Hasan Abed Al Kader Hammoud* Hani Itani* Dmitrii Khizbullin Bernard Ghanem King Abdullah University of Science and Technology (KAUST) Abstract The rapid advancement of chat-based language models has led to remarkable progress in complex task-solving. However, their success heavily relies on human input to guide the conversation, which can be challenging and time-consuming.
This paper explores the potential of building scalable techniques to facilitate au-tonomous cooperation among communicative agents, and provides insight into their “cognitive” processes. To address the challenges of achieving autonomous cooperation, we propose a novel communicative agent framework named role-playing . Our approach involves using inception prompting to guide chat agents toward task completion while maintaining consistency with human intentions. We showcase how role-playing can be used to generate conversational data for studying the behaviors and capabilities of a society of agents, providing a valuable resource for investigating conversational language models. In particular, we conduct com-prehensive studies on instruction-following cooperation in multi-agent settings.
Our contributions include introducing a novel communicative agent framework, offering a scalable approach for studying the cooperative behaviors and capabili-ties of multi-agent systems, and open-sourcing our library to support research on communicative agents and beyond: https://github.com/camel-ai/camel.
1 Introduction “What magical trick makes us intelligent? The trick is that there is no trick. The power of intelligence stems from our vast diversity, not from any single, perfect principle.” - Marvin Minsky, The Society of Mind, p. 308 Confronted with the complexities of real-world tasks, solving them often requires multiple steps.
The rapid progress of chat-based large-scale language models (LLMs) has yielded remarkable achievements in complex task-solving [82, 84, 116, 89, 5, 10, 122, 13]. Nevertheless, it is worth noting that their success is heavily reliant on human input to guide the conversation in the right direction. This reliance necessitates users to provide relevant and precise prompts based on their intentions and the chat agents feedback. This can be challenging, time-consuming, and sometimes impossible. Crafting effective prompts often demands a deep understanding and expertise of a particular domain of knowledge. Consider an individual who lacks trading expertise; they would find it difficult to create suitable prompts for directing a chat agent to develop a trading application. This predicament is raising a crucial question: can we replace human intervention with an autonomous communicative agent capable of steering the conversation toward task completion with minimal human supervision? To tackle this issue, it is crucial to conduct more research exploring the potential, ⇤Equal contribution 37th Conference on Neural Information Processing Systems (NeurIPS 2023).
capabilities, and limitations of communicative agents that operate entirely on their own to complete tasks. Understanding how multiple agents interact with each other is important for anticipating the future of artificial intelligence. The dynamics of collaborating or competing agents play a key role in determining the success of AI systems [6, 26, 27, 84, 99, 9, 10].
This paper explores the potential of building scalable techniques to facilitate autonomous cooperation among communicative agents and provide insight into their “cognitive” processes. Several challenges arise when asking a society of agents to autonomously cooperate on completing tasks. Examples we encountered in our preliminary analysis include role flipping, assistant repeating instructions, flake replies, and infinite loop of messages. Therefore, it is critical to investigate ways to align these models with human intentions and to explore means enabling their effective cooperation. To address these issues, we propose a novel cooperative agent framework named role-playing to automate cooperation between communicative agents. Specifically, our proposed approach involves using role-playing with inception prompting to autonomously guide the communicative agents toward task completion. Only a preliminary idea is needed from human to guide the conversations toward complex task-solving.
Our library, which we make publicly available, provides modular functionality, and includes imple-mentations of different agents, examples of well-crafted prompts, and data explorers. We hope our library serves as a ground for future research in various areas such as multi-agent systems, cooperative AI, game theory simulations, social analysis, AI ethics, AI alignment, and beyond. In addition, our role-playing method provides a highly scalable way to generate conversational data for studying the behaviors and capabilities of chat agents. We showcase how role-playing can be used to let chat agents communicate with each other for task completion and record their conversations for behavior analysis and capability understanding. In particular, we consider two cooperative scenarios of role-playing and generate two large conversational, task-oriented, and instruction-following datasets: AI Society and Code. We also use our framework to collect two single-turn question-answer datasets, Math and Science, for LLM ability emergence study. Furthermore, we generate a Misalignment dataset that is a simulation of possible malicious applications which demonstrate the potential risks of an unaligned autonomous agent system. The datasets offer a valuable resource for investigating conversational language models, enabling them to comprehend and react to human language more effectively. Fur-thermore, our role-playing offers a scalable method of creating conversational instruction-following data, which can potentially enhance the development of more advanced language models. We show that solutions derived from our role-playing framework outperform those generated in a single shot by gpt-3.5-turbo [82] in both GPT4 and human evaluations. We also study knowledge emergence in LLMs by fine-tuning LLaMA [117] on progressively growing datasets generated through our framework. Additionally, we evaluate our code generation capabilities through benchmarking our final model on HumanEval [18] and HumanEval+ [69].
Contributions. Our contributions are fourfold: (1) We introduce a novel cooperative agent frame-work, role-playing , that allows communicative agents to collaborate autonomously toward completing tasks while requiring minimal human intervention; (2) Our framework offers a scalable approach for studying the cooperative behaviors and capabilities of multi-agent systems. It illuminates the challenges of achieving autonomous cooperation, and provides strategies for addressing them. We showcase the potential power of multi-agent collaboration for complex-task solving; (3) We demon-strate the significant emergence of LLM training abilities by utilizing the datasets we have collected from simulating four distinct agent collaboration scenarios; (4) We have open-sourced our library, containing implementations of various agents, data generation pipelines, data analysis tools, and collected datasets, to support research on communicative agents and beyond.
2 Related Work Communicative Agents. Communication between agents has been studied for a long time [76, 77]. There are many ways to facilitate communication between agents, and with agents [29, 90, 97]. Among these, natural language is considered the most natural form of communication [97].
By enabling agents to function as communicators themselves, they become capable of solving complex tasks [113, 85, 72, 3, 30, 111, 79, 41, 28, 102, 80, 106, 35, 49, 2, 51, 1, 55, 50, 65, 92].
Communication between AI agents can occur in a competitive setting [115, 108] or a cooperative setting [40, 27, 11, 137, 70]. Cooperative AI refers to artificial intelligence systems that are designed to work together with humans and other AI systems to achieve common goals [24, 125]. Cooperative AI systems take into account the needs and capabilities of other agents in the system and actively seek to collaborate and coordinate their actions with them, which has many potential benefits, including 2 increased efficiency, improved decision-making, and the ability to tackle complex problems that are beyond the reach of any single agent. However, designing effective cooperative AI systems is still an active area of research, as it requires addressing a range of technical, ethical, and social challenges [27]. Our work enables communicative agents to engage in a conversation and cooperate with each other to solve assigned tasks. The agents, each assigned a distinct role, are expected to apply their expertise and knowledge to solve their common task.
Instructional LLMs and Prompt Engineering. LLMs are trained on diverse text data and excel in text completion, with various downstream NLP applications [12, 22, 47, 131, 117]. However, InstructGPT suggests that LLMs may not align with user intent, proposing reinforcement learning from human feedback (RLHF) [23] and Instruction Fine-Tuning (IFT) [121] to improve LLMs relevance and appropriateness to user instructions. Special types of instruction or prompting methods , such as Chain-of-Thought (CoT) [123], zero-shot-CoT [61], and ReAct [126], have recently been developed to enhance the performance of LLMs on reasoning, arithmetic and decision making tasks [134, 118, 52, 73, 31, 103, 43, 64, 132, 46, 133, 105, 128, 25, 81, 109]. These techniques underpin the impressive capabilities of recent dialogue LLMs [106, 116, 36, 9, 82, 13], which aim to simulate human-like conversations and provide personalized and interactive experiences for users, exhibiting the behavior of conversational AI agents [33]. However, generating instruction datasets is a crucial challenge in building instruct-based LLMs, with existing datasets ranging from crowdsourced to generated. Hand-crafted instruction instances are available in [120], while leveraging previously crowdsourced NLP datasets is a less labor-intensive curation approach [121, 71, 78, 53]. LLMs have been explored for data generation in [101, 63, 68, 114], and Self-Instruct [119] proposes a semi-automated process for instruction instance generation. Unnatural-Instruction [48] collects instruction instances by prompting a language model with only three seed examples and paraphrasing the generated instances to expand the dataset. There is also a large chunk of work that has proposed methods for automatic dataset creation [67, 57, 19, 75, 20, 98, 59, 96, 129, 62, 130, 86, 8].
Another important challenge is prompt engineering. The quality of the prompt used to guide LLMs significantly affects its performance [91, 12, 66]. While LMs pre-trained on large data can implicitly learn tasks with few-shot prompting, hand-crafted prompts may not always suffice. Automated prompt generation methods have been proposed, such as gradient-guided search [104], mining-based and paraphrasing-based techniques [54], a meta-prompt [93], and automatic instruction selection and generation [136]. In this work, we introduce a conversational LLM auto-prompting method called Inception Prompting, which enables agents to prompt each other to solve tasks through Role-Playing.
The AI user continuously provides instructions to the AI assistant for task-solving. This enables us to save the streaming instruction-solution pairs and create diverse, instructional, conversational, and task-oriented datasets. These datasets can be used to analyze the behavior and capabilities of LLMs and for future research for fine-tuning LLMs with conversational instructions.
AI Alignment. AI alignment is a field that aims to ensure that AI systems adhere to their intended goals, interests, and values, as envisioned by their designers [4, 39, 110, 32, 38, 74, 10]. The first attempt at AI alignment was made through the "Three Laws of Robotics," which was introduced by Isaac Asimov in his science fiction stories [6]. Developing aligned AI systems is crucial for achieving desired objectives while avoiding unintended consequences. Research in AI alignment focuses on discouraging AI models from producing false, offensive, deceptive, or manipulative information that could result in various harms [56, 112, 42, 37]. Achieving a high level of alignment requires researchers to grapple with complex ethical, philosophical, and technical issues. We conduct extensive experiments to study different role-playing situations, which probe the alignment of LLMs.
3 Methodology In this paper, we focus on studying communicative agents under cooperative settings where they share common interests. In particular, we study the assistant-user scenario, where a preliminary idea is given at the start. Agents will conceptualize the idea into a specific task and complete it autonomously through conversations.
3.1 Role-playing Framework “Whats the most resilient parasite? An Idea. A single idea from the human mind can build cities. An idea can transform the world and rewrite all the rules. Which is why I have to steal it.” - Dom Cobb, Inception 3 Idea: Develop a trading bot for the stock market Human User Role Assignment: AI Assistant: Python Programmer AI User: Stock Trader Human Input Task Specifier AI Assistant AI User Specified Task: Develop a trading bot with a sentiment analysis tool that can monitor social media platforms for positive or negative comments about a particular stock, and execute trades based on sentiment analysis results Instruction: Install the necessary Python libraries for sentiment analysis and stock trading.
Input: None Solution: To install the necessary Python libraries for sentiment analysis and stock trading, we can use pip, the package installer for Python. Here are the libraries we need to install: 1. tweepy - for accessing the Twitter API 2. textblob - for sentiment analysis 3. yfinance - for accessing Yahoo Finance API To install these libraries, open the command prompt or terminal and enter the following commands: pip install tweepy pip install textblob pip install yfinance Next request.
Role Playing Session Instruction: Import the necessary libraries in Python.
Input: None Solution: Here's the code to import these libraries: ``` import tweepy from textblob import TextBlob import pandas as pd import numpy as np import yfinance as yf ``` Next request.
Figure 1: CAMEL Role-Playing Framework. Our role-playing setup starts with the human user having an idea they want to implement, e.g. develop a trading bot for the stock market. The roles involved in this task would be an AI assistant agent who is a python programmer and an AI user agent who is a stock trader. The task is made more specific using our task specifier agent, leading to a well-defined task for the assistant to solve. Both AI user and AI assistant are provided with the specified task, after which they collaboratively communicate by chatting with each other in an instruction-following fashion to solve the specified task.
Our proposed framework is a novel role-playing approach for studying multiple communicative agents. Specifically, we concentrate on task-oriented role-playing that involves one AI assistant and one AI user. After the multi-agent system receives a preliminary idea and the role assignment from human users, a task-specifier agent will provide a detailed description to make the idea specific.
Afterwards, the AI assistant and AI user will cooperate on completing the specified task through multi-turn conversations until the AI user determines the task is done. The AI user is responsible for giving instructions to the AI assistant and directing the conversation toward task completion. On the other hand, the AI assistant is designed to follow the instructions from the AI user and respond with specific solutions. The whole role-playing framework is depicted in Figure 1.
Human Input and Task Specifying. The role-playing session will be instantiated from an idea and selected roles by humans. As an example in Figure 1, a human has a preliminary idea to develop a trading bot for the stock market. Humans may or may not have the knowledge about how the idea can be realized. What is needed is only to designate the potential roles that can implement the idea. For instance, a Python Programmer could collaborate with a Stock Trader to realize the idea of developing a trading bot for the stock market. After the idea and roles are determined, the task specifier agent will brainstorm a specific task that the AI Assistant role can help with the AI user role to complete based on the input idea. An example of a specified task in this scenario could be: develop a trading bot with a sentiment analysis tool that can monitor social media platforms for positive or negative comments about a particular stock, and execute trades based on sentiment analysis results.
The main motivation for introducing a task specifier is that conversational agents usually require a concrete task prompt for realizing the task which might be challenging or time-consuming for a non-domain expert. Therefore, the task specifier agent serves as an enhanced imagination module for the idea implementation. Please note that, when studying our framework at a large scale for AI society and Code scenarios, we generate roles and ideas automatically by prompting LLMs instead of relying on human inputs. For our generated Math and Science datasets we generated problem topics, subtopics, and problems automatically by prompting LLMs.
AI Assistant-User Role Assignment. After the task specification, The AI assistant role and the AI user role will be assigned to the user agent and the assistant agent correspondingly to complete the specified task. In practice, a system message is passed to each agent declaring their role. We refer to the assistant system prompt/message by PA and that of the user by PU. The system messages are passed to the agents before the conversations start. Let F1 and F2 denote two large-scale auto-regressive language models [82]. When the system message is passed to those models respectively, we 4 obtain A FPA 1 and U FPU 2 which are referred to as the assistant and user agents respectively.
In Figure 1, the AI assistant and the AI user are assigned the roles of a Python Programmer and a Stock Trader at the beginning of the role-playing session respectively. The AI user serves as a task planner, engaging in interactive planning to determine feasible steps for the AI assistant to execute.
Meanwhile, the AI assistant acts as a task executor, offering solutions, executing planned steps, and providing responses to the AI user.
Conversation Towards Task-Solving. After the role assignment is completed, the AI assistant A and AI user U will collaborate in an instruction-following manner to accomplish the task. In the AI assistant-user scenario, the AI user is responsible for providing instructions, and the assistant is expected to respond with a solution that fulfills the instructions. Formally, we denote the user instruction message obtained at time t by It and the assistant solution by St. The set of conversational messages obtained up until time t is denoted by Equation (1) shown below: Mt = {(I0, S0), ..., (It, St)} = {(Ii, Si)}|t i=0 (1) At the next time step, t + 1, the AI user U takes the historical conversation message set Mt and provides a new instruction It+1, as shown in Equation (2). The produced instruction message It+1 is then passed, along with message set Mt, to the AI assistant A. The AI assistant will then respond with a solution, denoted by St+1 in Equation (3): It+1 = U(Mt) (2) St + 1 = A(Mt, It + 1) (3) After obtaining the solution St+1 to the instruction It+1, the message set is updated using Equation (4) to obtain Mt+1: Mt+1 Mt [ (It+1, St+1) (4) Note that the formulation above not only models AI-AI communicative scenarios, but it can also be easily extended to model human-AI communication or communication between more than two agents.
Specifically, we can use message-passing graphs to model communication between an arbitrary number of agents. In Figure 1, we observe that the AI user initiates the installation and import of essential Python libraries for sentiment analysis and stock trading by instructing the AI assistant through conversations. This example is drawn from our experiments, and the entire conversation is available in the Appendix.
Critic-In-The-Loop. To enhance the controllability of the role-playing framework, we introduce a critic agent capable of selecting proposals from or providing feedback to the role-playing agents.
This enables tree-search-like decision-making for task-solving. In practice, the critic can be either an AI agent or a human. The detailed implementation and case studies can be found in the Appendix.
3.2 Inception Prompting Since prompt engineering is crucial to our role-playing framework, this section delves deeply into our prompting techniques. Our prompt engineering occurs solely at the beginning of role-playing, for task specification and role assignment. Once the conversation phase commences, the AI assistant and AI user prompt each other automatically in a loop until termination. As such, we refer to our technique as Inception Prompting. Our Inception prompt consists of three prompts: the task specifier prompt PT , the assistant system prompt PA, and the user system prompt PU. As an example, we consider the inception prompt of the AI Society scenario. The templates for these prompts of AI Society role-playing are shown in Figure 2. The task specifier prompt contains information about the roles of the AI assistant and AI user in the role-playing session. Therefore, the task specifier agent can take a preliminary task/idea as input and generate a specific task using imagination. The AI assistant system prompt PA and the AI user system prompt PU are mostly symmetrical and include information about the assigned task and roles, communication protocols, termination conditions, and constraints or requirements to avoid unwanted behaviors. The prompt designs for both roles are crucial to achieve autonomous cooperation between agents. It is non-trivial to engineer prompts that ensure agents act in alignment with our intentions. We take the prompt templates from the AI Society in Figure 2 as an example to explain our key design choices. The prompts used for the Code scenario follow a similar sprint as the AI society scenario, but with some additional engineering related to programming languages. More details in the Appendix.
5 AI Society Inception Prompt Task Specifier Prompt: Here is a task that <ASSISTANT_ROLE> will help <USER_ROLE> to complete: <TASK>.
Please make it more specific. Be creative and imaginative.
Please reply with the specified task in <WORD_LIMIT> words or less. Do not add anything else.
Assistant System Prompt: Never forget you are a <ASSISTANT_ROLE> and I am a <USER_ROLE>. Never flip roles!
Never instruct me!
We share a common interest in collaborating to successfully complete a task.
You must help me to complete the task.
Here is the task: <TASK>. Never forget our task!
I must instruct you based on your expertise and my needs to complete the task.
I must give you one instruction at a time.
You must write a specific solution that appropriately completes the requested instruction.
You must decline my instruction honestly if you cannot perform the instruction due to physical, moral, legal reasons or your capability and explain the reasons.
Unless I say the task is completed, you should always start with: Solution: <YOUR_SOLUTION> <YOUR_SOLUTION> should be specific, and provide preferable implementations and examples for task-solving.
Always end <YOUR_SOLUTION> with: Next request.
User System Prompt: Never forget you are a <USER_ROLE> and I am a <ASSISTANT_ROLE>.
Never flip roles! You will always instruct me.
We share a common interest in collaborating to successfully complete a task.
I must help you to complete the task.
Here is the task: <TASK>. Never forget our task!
You must instruct me based on my expertise and your needs to complete the task ONLY in the following two ways: 1. Instruct with a necessary input: Instruction: <YOUR_INSTRUCTION> Input: <YOUR_INPUT> 2. Instruct without any input: Instruction: <YOUR_INSTRUCTION> Input: None The "Instruction" describes a task or question. The paired "Input" provides further context or information for the requested "Instruction".
You must give me one instruction at a time.
I must write a response that appropriately completes the requested instruction.
I must decline your instruction honestly if I cannot perform the instruction due to physical, moral, legal reasons or my capability and explain the reasons.
You should instruct me not ask me questions.
Now you must start to instruct me using the two ways described above.
Do not add anything else other than your instruction and the optional corresponding input!
Keep giving me instructions and necessary inputs until you think the task is completed.
When the task is completed, you must only reply with a single word <CAMEL_TASK_DONE>.
Never say <CAMEL_TASK_DONE> unless my responses have solved your task.
Figure 2: Inception Prompt of AI Society Role-Playing. This shows the task specifier prompt, assistant system prompt, and user system prompt which are used for studying the AI society scenario.
Prompt Engineering. To delve deeper into the details in Figure 2, we start by chunking the various parts of the AI assistant system prompt PA shown below: • Never forget you are a <ASSISTANT_ROLE> and I am a <USER_ROLE>. This assigns the chosen role to the assistant agent and provides it with information about the users role.
• Never flip roles!
Never instruct me! This prevents agents from flipping roles. In some cases, we have observed the assistant and the user switching roles, where the assistant suddenly takes control and instructs the user, and the user follows those instructions.
• You must decline my instruction honestly if you cannot perform the instruction due to physical, moral, legal reasons or your capability and explain the reasons. This prohibits the agent from producing harmful, false, illegal, and misleading information.
• Unless I say the task is completed, you should always start with: Solution: <YOUR_SOLUTION>.
<YOUR_SOLUTION> should be specific, and provide preferable implementations and examples for task-solving.
This encourages the assistant always responds in a consistent format, avoiding any deviation from the 6 structure of the conversation, and preventing vague or incomplete responses, which we refer to as flake responses, such as "I will do something".
• Always end your solution with: Next request. This ensures that the assistant keeps the conversation going by requesting a new instruction to solve.
For the AI user system prompt PU, we strive to maintain as much symmetry as possible with respect to the AI assistant system prompt. Apart from the opposite role assignment, the user system prompt differs from the assistant prompt in the following ways: • You must instruct me ...
to complete the task ONLY in the following two ways: 1.
Instruct with a necessary input: ...; 2.
Instruct without any input: ... This follows the typical data structure of instruction-following, which allows the generated instruction-solution pairs to be easily used for fine-tuning LLMs.
• Keep giving me instructions and necessary inputs until you think the task is completed.
When the task is completed, you must only reply with a single word <CAMEL_TASK_DONE>.
We introduce an end-of-task token, namely, <CAMEL_TASK_DONE>. This token is used once the user believes the task is done. This ensures that the chat is terminated when the user is satisfied. Without doing so, the agents might fall into a chatting loop where they keep on saying “thank you” to each other or “goodbye” indefinitely.
4 Experiments In this section, we will discuss the various experiments that we conducted to arrive at our final design choices. Specifically, we will examine the interesting observations, challenging issues, and several examples we have encountered while enabling agents to communicate with each other under different prompt design choices to achieve autonomous cooperation. In our experiments, we employed two gpt-3.5-turbo agents, referred to as LLM agents for simplicity, with Inception Prompts, as described in Section 3.2, to simulate assistant-user cooperation. For our analysis, we set our attention on AI Society setting. We also gathered conversational data, named CAMEL AI Society and CAMEL Code datasets and problem-solution pairs data named CAMEL Math and CAMEL Science and analyzed and evaluated their quality. Moreover, we will discuss potential extensions of our framework and highlight both the risks and opportunities that future AI society might present.
Data Generation Prompts of AI Society AI Society Assistant Role Generation Prompt: You are a helpful assistant that can play many different roles. Now please list <NUM_ROLES> different roles that you can play with your expertise in diverse fields. Sort them by alphabetical order. No explanation required.
User Role Generation Prompt: Please list <NUM_ROLES> most common and diverse groups of internet users or occupations.
Use singular form. No explanation.
Sort them by alphabetical order. No explanation required.
Task Generation Prompt: List <NUM_TASKS> diverse tasks that <ASSISTANT_ROLE> can assist <USER_ROLE> cooperatively to achieve together. Be concise. Be creative.
Figure 3: Data Generation Prompts. In order to maintain a scalable approach our data parameters are generated using an LLM model to reduce human involvement in the generation process. The generation prompts for both AI Society dataset are summarized in this figure.
4.1 Role-Playing for AI Society To create our AI Society dataset, we have developed a scalable approach that follows a series of steps.
Firstly, we prompt the LLM agent to generate possible roles for the assistant and the user. We achieve this by providing the LLM agent with specific prompts designed to elicit these roles. Next, we ask the LLM agent to generate a range of possible tasks that can be solved through collaboration between the assistant and user roles generated previously. After generating a range of possible tasks as described 7 in the previous step, we then use the task specifier prompt passed to the LLM agent to make the task more specific. The prompts for assistant role generation, user role generation, and task generation are shown in Figure 5 (AI Society). For our AI society dataset, we generated 50 assistant roles, 50 user roles, and 10 tasks for each combination of roles yielding a total of 25,000 conversations. The generated assistant roles and user roles for AI Society as well as details about the generation of Code, Math and Science datasets can be found in the Appendix.
Challenges and Observations. In this section, we explore the four main challenges that we identified during our analysis of the generated datasets. Our observations shed light on some interesting aspects of cooperative AI and the difficulties that arise in its development.
• Role Flipping: One challenge we encountered was role flipping, where the assistant and user switch roles during the conversation. This issue typically arises when the assistant starts providing instructions or commands instead of following the users prompts, which can lead to confusion and a reversal of roles. To avoid role flipping, it is crucial for the assistant not to ask questions, as this can also contribute to the problem.
• Assistant Repeats Instruction: Another challenge that we observed was the assistant simply repeating the users instructions without any role flipping occurring.
• Flake Replies: We also observed instances where the assistant agent responds with a flake reply, often taking the form of "I will...". These messages do not contribute to the task at hand, as the assistant promises to take action but ultimately fails to follow through.
• Infinite Loop of Messages: An interesting challenge that we encountered was when the assistant and user engage in an infinite loop of meaningless conversation, such as repeatedly thanking each other or saying goodbye without progressing the task. Interestingly, in some cases, the assistant and user are aware that they are stuck in a loop, but are unable to break out of it.
The Appendix shows examples of each of the four challenges discussed above. Overall, our observa-tions highlight the complexity of cooperative AI development and the need for continued exploration and innovation to overcome the challenges we face. By identifying these issues, we hope to contribute to the development of more effective and engaging cooperative AI systems.
Termination Conditions. The conversation between the assistant and user agents is designed to follow a specific format to ensure consistent and accurate data generation. To ensure that both the user and assistant adhere to their respective roles and responsibilities, certain conditions have been set in place to terminate the chat if necessary. These conditions are outlined below: • User No Instruct: If the user does not instruct the assistant for 3 rounds, conversation is ended.
• Assistant Instruct: If the assistant provides an instruction to the user, it indicates a role reversal, and the conversation is terminated.
• End of Task Token: If the user believes that the task has been solved, they are expected to say <CAMEL_TASK_DONE> to signify the completion of the task. Once this message is received, the conversation is terminated.
• Assistant&User Token Limit: Given that gpt-3.5-turbo has a limitation on the number of tokens, the conversation is terminated if either the assistant or the user reach the token limit.
• Maximum Number of Messages: To keep the cost of generated chats in check, we have set a maximum limit of 40 messages. This limit guarantees a long enough conversation between the user and assistant while also ensuring that the data generated is not too costly to produce.
The cost grows quadratically with the length of the conversation, making it essential to set a limit.
5 Evaluation 5.1 Agent Evaluation In order to assess the performance of CAMEL (Cooperative Role-playing Communication), we conduct two types of evaluations: (1) Human evaluation, and (2) GPT4 evaluation. We randomly select 100 tasks from our AI Society dataset for evaluation and 100 tasks from our Code dataset.
Then, we employ the GPT4 model to summarize the content of the CAMEL conversation-based 8 solution, presenting a consolidated final solution. Particularly, a GPT4 is used since it possesses a larger token limit which is suitable for summarization. Summarization also makes CAMEL agents solution undetectable by its format, allowing for a more fair comparison. Subsequently, this solution is compared with a single-shot solution generated by the gpt-3.5-turbo model for the same task.
Sample tasks are provided in the Appendix.
Human Evaluation. For this evaluation, we present both the CAMEL summarized agent solution and the gpt-3.5-turbo single-shot solution side-by-side to human participants. The identity behind each solution is not revealed. Participants are then asked to vote on whether one solution is superior to the other or if they are equally good. A total of 453 responses were collected during this evaluation.
Note that, human evaluation is only done for AI Society, as assessing code is generally harder for humans (without running the code).
GPT4 Evaluation. We engage a GPT4 agent to evaluate the effectiveness of Model 1 (CAMEL Agent solution) versus Model 2 (gpt-3.5-turbo single-shot solution) for each task. More specifically, we prompt GPT4 to score and decide which solution of the two solutions is better.
Results. The summarized results of each evaluation are outlined in Table 1 which showcases that the CAMEL solution outperforms gpt-3.5-turbo single-shot solution in both the human evaluation and the GPT4 evaluation by a big margin. It is also worth noting that both human evaluation and GPT4 evaluation are highly aligned.
Table 1: Agent Evaluation Results: Results of the evaluations of the CAMEL agent against gpt-3.5-turbo using both human evaluators and GPT4 consistently show that utilizing a multi-agent cooperative approach is more effective than gpt-3.5-turbos single shot solution.
Dataset Evaluation Type Draw gpt-3.5-turbo Wins CAMEL Agents Win AI Society Human Evaluation 13.3% 10.4% 76.3% GPT4 Evaluation 4.0% 23.0% 73.0% Code GPT4 Evaluation 0.0% 24.0% 76.0% 5.2 GPT4 for ChatBot Evaluation In this section, we progressively fine-tune a LLaMA 7B model on our generated datasets. By progressively incorporating diverse datasets like AI society, code, math, and science, we expect fine-tuned model to demonstrate the ability to develop an increasingly sophisticated understanding of these domains.
We initially start by training on AI society dataset, which aims to let the model learn about human interactions and societal dynamics. As additional datasets were introduced, such as code, the model gained knowledge of programming logic and syntax, enabling it to generate coherent and executable code snippets. The inclusion of the math dataset further expanded the models capabilities, allowing it to solve complex equations, reason about abstract concepts, and perform precise calculations.
Finally, exposure to the science dataset broadened the models understanding of scientific theories, empirical observations, and experimental methods. The emergence of model capabilities is measured by evaluating the quality of the model responses, before and after training on the new domain, on a set of questions of varying difficulties from each domain. More precisely, the model is tested on 20 AI Society related tasks, 20 coding tasks, 20 math tasks and 60 science tasks.
Those results are highlighted in Table 2 where we see that each time we add a dataset, the model performs better on the incorporated domain. Note that to measure the quality of the models responses, we follow the evaluation from Section T, which involves prompting a GPT4 agent to score and decide which solution is better. It is worth noting that an improvement on other domains is also observed in some cases such as when we train on Code we improve on Science. This is because our Code dataset contains problems that solve tasks in particular domains which include scientific domain. Similarly, training on AI Society improves code as AI Society contains the role of a "programmer" and hence coding related conversations. Finally, note that the draws observed in LLaMA-7B vs AI Society in Math reflects equally bad solutions compared to the draws observed in AI Society + Code + Math vs AI Society + Code + Math + Science where the draws are equally good solutions. This progression from AI society to code to math to science highlights the potential of AI models to acquire a versatile 9 and adaptable knowledge base, paralleling the way humans gain expertise in diverse subjects. Sample tasks are provided in the Appendix.
Table 2: Emergence of Knowledge. By progressively fine-tuning LLaMA on datasets from different domains, we observe the emergence of knowledge as the model transitions from AI society to code, math, and science. This finding is indicated by the fact that Model 2 almost always performs better than Model 1, especially on the added dataset.
Dataset Model 1 Model 2 Draw Model 1 Model 2 AI Society Code Math Science AI Society Code Math Science AI Society 3 0 6 14 Code 3 0 0 20 Math 3 9 5 6 Science 3 0 13 47 AI Society 3 3 3 4 8 8 Code 3 3 3 1 9 10 Math 3 3 3 5 8 7 Science 3 3 3 1 19 40 AI Society 3 3 3 3 3 5 6 9 Code 3 3 3 3 3 1 9 10 Math 3 3 3 3 3 1 3 16 Science 3 3 3 3 3 3 8 49 AI Society 3 3 3 3 3 3 3 3 1 16 Code 3 3 3 3 3 3 3 1 8 11 Math 3 3 3 3 3 3 3 10 5 5 Science 3 3 3 3 3 3 3 9 2 49 AI Society 3 3 3 3 0 0 20 Code 3 3 3 3 0 0 20 Math 3 3 3 3 0 0 20 Science 3 3 3 3 0 0 60 5.3 HumanEval(+) Table 3: HumanEval(+) for Various Models. We test our CAMEL model, which is a LLaMa-7B fine-tuned on all our datasets (AI Society, Code, Math, Science) on HumanEval and HumanEval+ benchmarks, where we show competitive pass@k scores with LLaMa-7B and Vicuna-7B.
HumanEval HumanEval+ pass@k [%] k = 1 k = 100 k = 1 k = 100 gpt-3.5-turbo 69.4 94.0 61.7 89.8 LLaMA-7B 10.5 36.5 --Vicuna-7B 11.0 42.9 9.9 34.7 CAMEL-7B 14.0 57.9 12.2 50.0 To evaluate the coding task-solving capabilities of our CAMEL model, specifically the LLaMA-7B fine-tuned on our comprehensive datasets, we rely on HumanEval [18] and HumanEval+ [69].
The results, as depicted in table 3, clearly demonstrate the remarkable performance of CAMEL. It surpasses not only the LLaMA-7B model but also Vicuna-7B [21] by a big margin. These findings underscore the critical role played by the generated datasets in enhancing LLaMAs ability to tackle coding-related tasks.
6 Conclusion In this paper, we explore the potential of autonomous cooperation among communicative agents and propose a novel cooperative agent framework named role-playing . Our approach enables communicative agents to collaborate autonomously toward completing tasks while requiring minimal human intervention, leading to better solutions are per our thorough evaluations. Through our analysis, we show that achieving autonomous cooperation is challenging due to issues like conversation deviation, role flipping, and termination conditions. Our framework offers a scalable approach for studying the cooperative behaviors and capabilities of multi-agent systems and provides strategies for addressing these challenges. Furthermore, our open-sourced library includes implementations of various agents, data generation pipelines, data analysis tools, and collected datasets, to support research on communicative agents and beyond. Our contributions offer valuable insights into the future of large language artificial intelligence models and cooperative AI systems.
7 Acknowledgements This work was supported by SDAIA-KAUST Center of Excellence in Data Science and Artificial Intelligence (SDAIA-KAUST AI).
10 References [1] Josh Abramson, Arun Ahuja, Iain Barr, Arthur Brussee, Federico Carnevale, Mary Cassin, Rachita Chhaparia, Stephen Clark, Bogdan Damoc, Andrew Dudzik, Petko Georgiev, Aurelia Guy, Tim Harley, Felix Hill, Alden Hung, Zachary Kenton, Jessica Landon, Timothy Lillicrap, Kory Mathewson, Soˇ na Mokrá, Alistair Muldal, Adam Santoro, Nikolay Savinov, Vikrant Varma, Greg Wayne, Duncan Williams, Nathaniel Wong, Chen Yan, and Rui Zhu. Imitating interactive intelligence, 2020.
[2] Michael Ahn, Anthony Brohan, Noah Brown, Yevgen Chebotar, Omar Cortes, Byron David, Chelsea Finn, Chuyuan Fu, Keerthana Gopalakrishnan, Karol Hausman, Alex Herzog, Daniel Ho, Jasmine Hsu, Julian Ibarz, Brian Ichter, Alex Irpan, Eric Jang, Rosario Jauregui Ruano, Kyle Jeffrey, Sally Jesmonth, Nikhil J Joshi, Ryan Julian, Dmitry Kalashnikov, Yuheng Kuang, Kuang-Huei Lee, Sergey Levine, Yao Lu, Linda Luu, Carolina Parada, Peter Pastor, Jornell Quiambao, Kanishka Rao, Jarek Rettinghouse, Diego Reyes, Pierre Sermanet, Nicolas Sievers, Clayton Tan, Alexander Toshev, Vincent Vanhoucke, Fei Xia, Ted Xiao, Peng Xu, Sichun Xu, Mengyuan Yan, and Andy Zeng. Do as i can, not as i say: Grounding language in robotic affordances, 2022.
[3] Jacob Andreas. Language models as agent models, 2022.
[4] Jacob Andreas and Dan Klein. Alignment-based compositional semantics for instruction following. arXiv preprint arXiv:1508.06491, 2015.
[5] Anthropic. Introducing claude. Anthropic Blog, 2023.
[6] Isaac Asimov. I. Robot. Narkaling Productions., 1940.
[7] Jimmy Ba and Rich Caruana. Do deep nets really need to be deep? Advances in neural information processing systems, 27, 2014.
[8] Sanghwan Bae, Donghyun Kwak, Sungdong Kim, Donghoon Ham, Soyoung Kang, Sang-Woo Lee, and Woomyoung Park. Building a role specified open-domain dialogue system leveraging large-scale language models. In Proceedings of the 2022 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, pages 21282150, Seattle, United States, July 2022. Association for Computational Linguistics.
[9] Yuntao Bai, Andy Jones, Kamal Ndousse, Amanda Askell, Anna Chen, Nova DasSarma, Dawn Drain, Stanislav Fort, Deep Ganguli, Tom Henighan, et al. Training a helpful and harmless assistant with reinforcement learning from human feedback. arXiv preprint arXiv:2204.05862, 2022.
[10] Yuntao Bai, Saurav Kadavath, Sandipan Kundu, Amanda Askell, Jackson Kernion, Andy Jones, Anna Chen, Anna Goldie, Azalia Mirhoseini, Cameron McKinnon, et al. Constitutional ai: Harmlessness from ai feedback. arXiv preprint arXiv:2212.08073, 2022.
[11] Nolan Bard, Jakob N Foerster, Sarath Chandar, Neil Burch, Marc Lanctot, H Francis Song, Emilio Parisotto, Vincent Dumoulin, Subhodeep Moitra, Edward Hughes, et al. The hanabi challenge: A new frontier for ai research. Artificial Intelligence, 280:103216, 2020.
[12] Tom Brown, Benjamin Mann, Nick Ryder, Melanie Subbiah, Jared D Kaplan, Prafulla Dhariwal, Arvind Neelakantan, Pranav Shyam, Girish Sastry, Amanda Askell, et al. Language models are few-shot learners.
Advances in neural information processing systems, 33:18771901, 2020.
[13] Sébastien Bubeck, Varun Chandrasekaran, Ronen Eldan, Johannes Gehrke, Eric Horvitz, Ece Kamar, Peter Lee, Yin Tat Lee, Yuanzhi Li, Scott Lundberg, et al. Sparks of artificial general intelligence: Early experiments with gpt-4. arXiv preprint arXiv:2303.12712, 2023.
[14] N Carlini, F Tramer, E Wallace, M Jagielski, A Herbert-Voss, K Lee, A Roberts, T Brown, D Song, Ú Erlingsson, et al. Extracting training data from large language models. arxiv. Preprint posted online December, 14, 2020.
[15] Nicholas Carlini, Jamie Hayes, Milad Nasr, Matthew Jagielski, Vikash Sehwag, Florian Tramèr, Borja Balle, Daphne Ippolito, and Eric Wallace. Extracting training data from diffusion models. arXiv preprint arXiv:2301.13188, 2023.
[16] Harrison Chase. Langchain. 2022.
[17] Defang Chen, Jian-Ping Mei, Yuan Zhang, Can Wang, Zhe Wang, Yan Feng, and Chun Chen. Cross-layer distillation with semantic calibration. In Proceedings of the AAAI Conference on Artificial Intelligence, volume 35, pages 70287036, 2021.
11 [18] Mark Chen, Jerry Tworek, Heewoo Jun, Qiming Yuan, Henrique Ponde de Oliveira Pinto, Jared Kaplan, Harri Edwards, Yuri Burda, Nicholas Joseph, Greg Brockman, et al. Evaluating large language models trained on code. arXiv preprint arXiv:2107.03374, 2021.
[19] Maximillian Chen, Alexandros Papangelis, Chenyang Tao, Seokhwan Kim, Andy Rosenbaum, Yang Liu, Zhou Yu, and Dilek Hakkani-Tur. Places: Prompting language models for social conversation synthesis.
arXiv preprint arXiv:2302.03269, 2023.
[20] Maximillian Chen, Alexandros Papangelis, Chenyang Tao, Andy Rosenbaum, Seokhwan Kim, Yang Liu, Zhou Yu, and Dilek Hakkani-Tur. Weakly supervised data augmentation through prompting for dialogue understanding. NeurIPS 2022 Workshop on Synthetic Data for Empowering ML Research, 2022.
[21] Wei-Lin Chiang, Zhuohan Li, Zi Lin, Ying Sheng, Zhanghao Wu, Hao Zhang, Lianmin Zheng, Siyuan Zhuang, Yonghao Zhuang, Joseph E. Gonzalez, Ion Stoica, and Eric P. Xing. Vicuna: An open-source chatbot impressing gpt-4 with 90%* chatgpt quality, March 2023.
[22] Aakanksha Chowdhery, Sharan Narang, Jacob Devlin, Maarten Bosma, Gaurav Mishra, Adam Roberts, Paul Barham, Hyung Won Chung, Charles Sutton, Sebastian Gehrmann, et al. Palm: Scaling language modeling with pathways. arXiv preprint arXiv:2204.02311, 2022.
[23] Paul F Christiano, Jan Leike, Tom Brown, Miljan Martic, Shane Legg, and Dario Amodei. Deep reinforcement learning from human preferences. Advances in neural information processing systems, 30, 2017.
[24] Caroline Claus and Craig Boutilier. The dynamics of reinforcement learning in cooperative multiagent systems. In AAAI/IAAI, 1998.
[25] Antonia Creswell, Murray Shanahan, and Irina Higgins. Selection-inference: Exploiting large language models for interpretable logical reasoning, 2022.
[26] Allan Dafoe, Yoram Bachrach, Gillian Hadfield, Eric Horvitz, Kate Larson, and Thore Graepel. Coopera-tive ai: machines must learn to find common ground. Nature, 593(7857):3336, 2021.
[27] Allan Dafoe, Edward Hughes, Yoram Bachrach, Tantum Collins, Kevin R McKee, Joel Z Leibo, Kate Larson, and Thore Graepel. Open problems in cooperative ai. arXiv preprint arXiv:2012.08630, 2020.
[28] Yali Du, Bo Liu, Vincent Moens, Ziqi Liu, Zhicheng Ren, Jun Wang, Xu Chen, and Haifeng Zhang.
Learning correlated communication topology in multi-agent reinforcement learning. In Proceedings of the 20th International Conference on Autonomous Agents and MultiAgent Systems, pages 456464, 2021.
[29] Tim Finin, Richard Fritzson, Don McKay, and Robin McEntire. Kqml as an agent communication language. In Proceedings of the third international conference on Information and knowledge management, pages 456463, 1994.
[30] Jakob Foerster, Ioannis Alexandros Assael, Nando De Freitas, and Shimon Whiteson. Learning to communicate with deep multi-agent reinforcement learning. Advances in neural information processing systems, 29, 2016.
[31] Yao Fu, Hao Peng, Ashish Sabharwal, Peter Clark, and Tushar Khot. Complexity-based prompting for multi-step reasoning. arXiv preprint arXiv:2210.00720, 2022.
[32] Iason Gabriel. Artificial intelligence, values, and alignment. Minds and Machines, 30:411 437, 2020.
[33] Jianfeng Gao, Michel Galley, and Lihong Li. Neural approaches to conversational ai. In The 41st International ACM SIGIR Conference on Research & Development in Information Retrieval, pages 13711374, 2018.
[34] Leo Gao, Jonathan Tow, Stella Biderman, Sid Black, Anthony DiPofi, Charles Foster, Laurence Golding, Jeffrey Hsu, Kyle McDonell, Niklas Muennighoff, Jason Phang, Laria Reynolds, Eric Tang, Anish Thite, Ben Wang, Kevin Wang, and Andy Zou. A framework for few-shot language model evaluation, September 2021.
[35] Amelia Glaese, Nat McAleese, Maja Trebacz, John Aslanides, Vlad Firoiu, Timo Ewalds, Maribeth Rauh, Laura Weidinger, Martin Chadwick, Phoebe Thacker, Lucy Campbell-Gillingham, Jonathan Uesato, Po-Sen Huang, Ramona Comanescu, Fan Yang, Abigail See, Sumanth Dathathri, Rory Greig, Charlie Chen, Doug Fritz, Jaume Sanchez Elias, Richard Green, Soˇ na Mokrá, Nicholas Fernando, Boxi Wu, Rachel Foley, Susannah Young, Iason Gabriel, William Isaac, John Mellor, Demis Hassabis, Koray Kavukcuoglu, Lisa Anne Hendricks, and Geoffrey Irving. Improving alignment of dialogue agents via targeted human judgements, 2022.
12 [36] Amelia Glaese, Nat McAleese, Maja Tr˛ ebacz, John Aslanides, Vlad Firoiu, Timo Ewalds, Maribeth Rauh, Laura Weidinger, Martin Chadwick, Phoebe Thacker, et al. Improving alignment of dialogue agents via targeted human judgements. arXiv preprint arXiv:2209.14375, 2022.
[37] Josh A Goldstein, Girish Sastry, Micah Musser, Renee DiResta, Matthew Gentzel, and Katerina Se-dova. Generative language models and automated influence operations: Emerging threats and potential mitigations. arXiv preprint arXiv:2301.04246, 2023.
[38] Dylan Hadfield-Menell. The principal-agent alignment problem in artificial intelligence. Ph. D. disserta-tion, 2021.
[39] Dylan Hadfield-Menell, McKane Andrus, and Gillian Hadfield. Legible normativity for ai alignment: The value of silly rules. In Proceedings of the 2019 AAAI/ACM Conference on AI, Ethics, and Society, pages 115121, 2019.
[40] Dylan Hadfield-Menell, Stuart J Russell, Pieter Abbeel, and Anca Dragan. Cooperative inverse reinforce-ment learning. Advances in neural information processing systems, 29, 2016.
[41] Serhii Havrylov and Ivan Titov. Emergence of language with multi-agent games: Learning to communicate with sequences of symbols. Advances in neural information processing systems, 30, 2017.
[42] Peter Henderson, Koustuv Sinha, Nicolas Angelard-Gontier, Nan Rosemary Ke, Genevieve Fried, Ryan Lowe, and Joelle Pineau. Ethical challenges in data-driven dialogue systems. In Proceedings of the 2018 AAAI/ACM Conference on AI, Ethics, and Society, pages 123129, 2018.
[43] Dan Hendrycks, Collin Burns, Saurav Kadavath, Akul Arora, Steven Basart, Eric Tang, Dawn Song, and Jacob Steinhardt. Measuring mathematical problem solving with the math dataset. arXiv preprint arXiv:2103.03874, 2021.
[44] Byeongho Heo, Minsik Lee, Sangdoo Yun, and Jin Young Choi. Knowledge transfer via distillation of activation boundaries formed by hidden neurons. In Proceedings of the AAAI Conference on Artificial Intelligence, volume 33, pages 37793787, 2019.
[45] Geoffrey Hinton, Oriol Vinyals, and Jeff Dean. Distilling the knowledge in a neural network. arXiv preprint arXiv:1503.02531, 2015.
[46] Namgyu Ho, Laura Schmid, and Se-Young Yun. Large language models are reasoning teachers. arXiv preprint arXiv:2212.10071, 2022.
[47] Jordan Hoffmann, Sebastian Borgeaud, Arthur Mensch, Elena Buchatskaya, Trevor Cai, Eliza Rutherford, Diego de Las Casas, Lisa Anne Hendricks, Johannes Welbl, Aidan Clark, et al. Training compute-optimal large language models. arXiv preprint arXiv:2203.15556, 2022.
[48] Or Honovich, Thomas Scialom, Omer Levy, and Timo Schick. Unnatural instructions: Tuning language models with (almost) no human labor. arXiv preprint arXiv:2212.09689, 2022.
[49] Ehsan Hosseini-Asl, Bryan McCann, Chien-Sheng Wu, Semih Yavuz, and Richard Socher. A sim-ple language model for task-oriented dialogue. Advances in Neural Information Processing Systems, 33:2017920191, 2020.
[50] Wenlong Huang, Pieter Abbeel, Deepak Pathak, and Igor Mordatch. Language models as zero-shot planners: Extracting actionable knowledge for embodied agents. arXiv preprint arXiv:2201.07207, 2022.
[51] Wenlong Huang, Fei Xia, Ted Xiao, Harris Chan, Jacky Liang, Pete Florence, Andy Zeng, Jonathan Tompson, Igor Mordatch, Yevgen Chebotar, et al. Inner monologue: Embodied reasoning through planning with language models. arXiv preprint arXiv:2207.05608, 2022.
[52] Shima Imani, Liang Du, and Harsh Shrivastava. Mathprompter: Mathematical reasoning using large language models. arXiv preprint arXiv:2303.05398, 2023.
[53] Srinivasan Iyer, Xi Victoria Lin, Ramakanth Pasunuru, Todor Mihaylov, Dániel Simig, Ping Yu, Kurt Shuster, Tianlu Wang, Qing Liu, Punit Singh Koura, et al. Opt-iml: Scaling language model instruction meta learning through the lens of generalization. arXiv preprint arXiv:2212.12017, 2022.
[54] Zhengbao Jiang, Frank F Xu, Jun Araki, and Graham Neubig. How can we know what language models know? Transactions of the Association for Computational Linguistics, 8:423438, 2020.
[55] Siddharth Karamcheti, Megha Srivastava, Percy Liang, and Dorsa Sadigh. Lila: Language-informed latent actions. In CoRL, pages 13791390, 2021.
13 [56] Zachary Kenton, Tom Everitt, Laura Weidinger, Iason Gabriel, Vladimir Mikulik, and Geoffrey Irving.
Alignment of language agents. arXiv preprint arXiv:2103.14659, 2021.
[57] Hyunwoo Kim, Jack Hessel, Liwei Jiang, Ximing Lu, Youngjae Yu, Pei Zhou, Ronan Le Bras, Mal-ihe Alikhani, Gunhee Kim, Maarten Sap, et al. Soda: Million-scale dialogue distillation with social commonsense contextualization. arXiv preprint arXiv:2212.10465, 2022.
[58] Jangho Kim, Seonguk Park, and Nojun Kwak. Paraphrasing complex network: Network compression via factor transfer. In S. Bengio, H. Wallach, H. Larochelle, K. Grauman, N. Cesa-Bianchi, and R. Garnett, editors, Advances in Neural Information Processing Systems, volume 31. Curran Associates, Inc., 2018.
[59] Yekyung Kim, Seohyeong Jeong, and Kyunghyun Cho. Linda: Unsupervised learning to interpolate in natural language processing. arXiv preprint arXiv:2112.13969, 2021.
[60] Pang Wei Koh and Percy Liang. Understanding black-box predictions via influence functions. In International conference on machine learning, pages 18851894. PMLR, 2017.
[61] Takeshi Kojima, Shixiang Shane Gu, Machel Reid, Yutaka Matsuo, and Yusuke Iwasawa. Large language models are zero-shot reasoners. arXiv preprint arXiv:2205.11916, 2022.
[62] Jonáš Kulhánek, Vojtˇ ech Hudeˇ cek, Tomáš Nekvinda, and Ondˇ rej Dušek. Augpt: Auxiliary tasks and data augmentation for end-to-end dialogue with pre-trained language models. In Proceedings of the 3rd Workshop on Natural Language Processing for Conversational AI, pages 198210, 2021.
[63] Kenton Lee, Kelvin Guu, Luheng He, Tim Dozat, and Hyung Won Chung. Neural data augmentation via example extrapolation. arXiv preprint arXiv:2102.01335, 2021.
[64] Aitor Lewkowycz, Anders Johan Andreassen, David Dohan, Ethan Dyer, Henryk Michalewski, Vinay Venkatesh Ramasesh, Ambrose Slone, Cem Anil, Imanol Schlag, Theo Gutman-Solo, et al.
Solving quantitative reasoning problems with language models. 2022.
[65] Shuang Li, Xavier Puig, Chris Paxton, Yilun Du, Clinton Wang, Linxi Fan, Tao Chen, De-An Huang, Ekin Akyürek, Anima Anandkumar, Jacob Andreas, Igor Mordatch, Antonio Torralba, and Yuke Zhu.
Pre-trained language models for interactive decision-making, 2022.
[66] Xiang Lisa Li and Percy Liang. Prefix-tuning: Optimizing continuous prompts for generation. arXiv preprint arXiv:2101.00190, 2021.
[67] Zekun Li, Wenhu Chen, Shiyang Li, Hong Wang, Jing Qian, and Xifeng Yan. Controllable dialogue simulation with in-context learning. arXiv preprint arXiv:2210.04185, 2022.
[68] Alisa Liu, Swabha Swayamdipta, Noah A. Smith, and Yejin Choi. WANLI: Worker and AI collaboration for natural language inference dataset creation. In Findings of the Association for Computational Linguistics: EMNLP 2022, pages 68266847, Abu Dhabi, United Arab Emirates, December 2022.
Association for Computational Linguistics.
[69] Jiawei Liu, Chunqiu Steven Xia, Yuyao Wang, and Lingming Zhang. Is your code generated by chatgpt really correct? rigorous evaluation of large language models for code generation.
arXiv preprint arXiv:2305.01210, 2023.
[70] Yat Long Lo, Christian Schroeder de Witt, Samuel Sokota, Jakob Nicolaus Foerster, and Shimon Whiteson.
Cheap talk discovery and utilization in multi-agent reinforcement learning. In The Eleventh International Conference on Learning Representations, 2023.
[71] Shayne Longpre, Le Hou, Tu Vu, Albert Webson, Hyung Won Chung, Yi Tay, Denny Zhou, Quoc V Le, Barret Zoph, Jason Wei, et al. The flan collection: Designing data and methods for effective instruction tuning. arXiv preprint arXiv:2301.13688, 2023.
[72] Ryan Lowe, Yi I Wu, Aviv Tamar, Jean Harb, OpenAI Pieter Abbeel, and Igor Mordatch. Multi-agent actor-critic for mixed cooperative-competitive environments. Advances in neural information processing systems, 30, 2017.
[73] Pan Lu, Liang Qiu, Kai-Wei Chang, Ying Nian Wu, Song-Chun Zhu, Tanmay Rajpurohit, Peter Clark, and Ashwin Kalyan. Dynamic prompt learning via policy gradient for semi-structured mathematical reasoning. In ICLR, 2023.
[74] Michael J. Matthews, Samuel H. Matthews, and Thomas K. Kelemen. The alignment problem: Machine learning and human values. Personnel Psychology, 2022.
14 [75] Yu Meng, Jiaxin Huang, Yu Zhang, and Jiawei Han. Generating training data with language models: Towards zero-shot language understanding. In Advances in Neural Information Processing Systems, 2022.
[76] Marvin Minsky. Society of mind. Simon and Schuster, 1988.
[77] Marvin Minsky. The emotion machine: Commonsense thinking, artificial intelligence, and the future of the human mind. Simon and Schuster, 2007.
[78] Swaroop Mishra, Daniel Khashabi, Chitta Baral, and Hannaneh Hajishirzi. Cross-task generalization via natural language crowdsourcing instructions. In ACL, 2022.
[79] Igor Mordatch and Pieter Abbeel. Emergence of grounded compositional language in multi-agent populations. In Proceedings of the AAAI conference on artificial intelligence, volume 32, 2018.
[80] Reiichiro Nakano, Jacob Hilton, Suchir Balaji, Jeff Wu, Long Ouyang, Christina Kim, Christopher Hesse, Shantanu Jain, Vineet Kosaraju, William Saunders, Xu Jiang, Karl Cobbe, Tyna Eloundou, Gretchen Krueger, Kevin Button, Matthew Knight, Benjamin Chess, and John Schulman. Webgpt: Browser-assisted question-answering with human feedback, 2021.
[81] Maxwell Nye, Anders Johan Andreassen, Guy Gur-Ari, Henryk Michalewski, Jacob Austin, David Bieber, David Dohan, Aitor Lewkowycz, Maarten Bosma, David Luan, Charles Sutton, and Augustus Odena.
Show your work: Scratchpads for intermediate computation with language models, 2021.
[82] OpenAI. Introducing chatgpt. Open AI Blog, 2022.
[83] OpenAI. Chatgpt plugins. OpenAI blog, 2023.
[84] Long Ouyang, Jeffrey Wu, Xu Jiang, Diogo Almeida, Carroll Wainwright, Pamela Mishkin, Chong Zhang, Sandhini Agarwal, Katarina Slama, Alex Ray, et al. Training language models to follow instructions with human feedback. Advances in Neural Information Processing Systems, 35:2773027744, 2022.
[85] Liviu Panait and Sean Luke. Cooperative multi-agent learning: The state of the art. Autonomous Agents and Multi-Agent Systems, 11:387434, 2005.
[86] Alexandros Papangelis, Karthik Gopalakrishnan, Aishwarya Padmakumar, Seokhwan Kim, Gokhan Tur, and Dilek Z. Hakkani-Tür. Generative conversational networks. In SIGDIAL, 2021.
[87] Wonpyo Park, Dongju Kim, Yan Lu, and Minsu Cho. Relational knowledge distillation. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition, pages 39673976, 2019.
[88] Peyman Passban, Yimeng Wu, Mehdi Rezagholizadeh, and Qun Liu. Alp-kd: Attention-based layer projection for knowledge distillation. In Proceedings of the AAAI Conference on artificial intelligence, volume 35, pages 1365713665, 2021.
[89] Sundar Pichai. An important next step on our ai journey. Google Blog, 2023.
[90] Stefan Poslad. Specifying protocols for multi-agent systems interaction. ACM Transactions on Au-tonomous and Adaptive Systems (TAAS), 2(4):15es, 2007.
[91] Alec Radford, Jeffrey Wu, Rewon Child, David Luan, Dario Amodei, Ilya Sutskever, et al. Language models are unsupervised multitask learners. OpenAI blog, 1(8):9, 2019.
[92] Scott Reed, Konrad Zolna, Emilio Parisotto, Sergio Gomez Colmenarejo, Alexander Novikov, Gabriel Barth-Maron, Mai Gimenez, Yury Sulsky, Jackie Kay, Jost Tobias Springenberg, Tom Eccles, Jake Bruce, Ali Razavi, Ashley Edwards, Nicolas Heess, Yutian Chen, Raia Hadsell, Oriol Vinyals, Mahyar Bordbar, and Nando de Freitas. A generalist agent, 2022.
[93] Laria Reynolds and Kyle McDonell. Prompt programming for large language models: Beyond the few-shot paradigm. In Extended Abstracts of the 2021 CHI Conference on Human Factors in Computing Systems, pages 17, 2021.
[94] Robin Rombach, Andreas Blattmann, Dominik Lorenz, Patrick Esser, and Björn Ommer. High-resolution image synthesis with latent diffusion models, 2021.
[95] Adriana Romero, Nicolas Ballas, Samira Ebrahimi Kahou, Antoine Chassang, Carlo Gatta, and Yoshua Bengio. Fitnets: Hints for thin deep nets. arXiv preprint arXiv:1412.6550, 2014.
[96] Andy Rosenbaum, Saleh Soltan, Wael Hamza, Yannick Versley, and Markus Boese. Linguist: Language model instruction tuning to generate annotated utterances for intent classification and slot tagging. arXiv preprint arXiv:2209.09900, 2022.
15 [97] Stuart J Russell. Artificial intelligence a modern approach. Pearson Education, Inc., 2010.
[98] Gaurav Sahu, Pau Rodriguez, Issam H Laradji, Parmida Atighehchian, David Vazquez, and Dzmitry Bahdanau. Data augmentation for intent classification with off-the-shelf large language models. ACL, 2022.
[99] William Saunders, Catherine Yeh, Jeff Wu, Steven Bills, Long Ouyang, Jonathan Ward, and Jan Leike.
Self-critiquing models for assisting human evaluators. arXiv preprint arXiv:2206.05802, 2022.
[100] Timo Schick, Jane Dwivedi-Yu, Roberto Dessì, Roberta Raileanu, Maria Lomeli, Luke Zettlemoyer, Nicola Cancedda, and Thomas Scialom. Toolformer: Language models can teach themselves to use tools.
arXiv preprint arXiv:2302.04761, 2023.
[101] Timo Schick and Hinrich Schütze. Generating datasets with pretrained language models. In Proceedings of the 2021 Conference on Empirical Methods in Natural Language Processing, pages 69436951, Online and Punta Cana, Dominican Republic, November 2021. Association for Computational Linguistics.
[102] Junjie Sheng, Xiangfeng Wang, Bo Jin, Junchi Yan, Wenhao Li, Tsung-Hui Chang, Jun Wang, and Hongyuan Zha. Learning structured communication for multi-agent reinforcement learning. Autonomous Agents and Multi-Agent Systems, 36(2):50, 2022.
[103] Freda Shi, Mirac Suzgun, Markus Freitag, Xuezhi Wang, Suraj Srivats, Soroush Vosoughi, Hyung Won Chung, Yi Tay, Sebastian Ruder, Denny Zhou, et al. Language models are multilingual chain-of-thought reasoners. In ICLR, 2023.
[104] Taylor Shin, Yasaman Razeghi, Robert L Logan IV, Eric Wallace, and Sameer Singh. Autoprompt: Eliciting knowledge from language models with automatically generated prompts.
arXiv preprint arXiv:2010.15980, 2020.
[105] Noah Shinn, Beck Labash, and Ashwin Gopinath. Reflexion: an autonomous agent with dynamic memory and self-reflection. arXiv preprint arXiv:2303.11366, 2023.
[106] Kurt Shuster, Jing Xu, Mojtaba Komeili, Da Ju, Eric Michael Smith, Stephen Roller, Megan Ung, Moya Chen, Kushal Arora, Joshua Lane, et al. Blenderbot 3: a deployed conversational agent that continually learns to responsibly engage. arXiv preprint arXiv:2208.03188, 2022.
[107] David Silver, Aja Huang, Chris J Maddison, Arthur Guez, Laurent Sifre, George Van Den Driessche, Julian Schrittwieser, Ioannis Antonoglou, Veda Panneershelvam, Marc Lanctot, et al. Mastering the game of go with deep neural networks and tree search. nature, 529(7587):484489, 2016.
[108] David Silver, Julian Schrittwieser, Karen Simonyan, Ioannis Antonoglou, Aja Huang, Arthur Guez, Thomas Hubert, Lucas Baker, Matthew Lai, Adrian Bolton, et al. Mastering the game of go without human knowledge. nature, 550(7676):354359, 2017.
[109] Abishek Sridhar, Robert Lo, Frank F. Xu, Hao Zhu, and Shuyan Zhou. Hierarchical prompting assists large language model on web navigation. In ArXiv, preprint.
[110] Jonathan Stray. Aligning ai optimization to community well-being. International Journal of Community Well-Being, 3:443 463, 2020.
[111] Sainbayar Sukhbaatar, Rob Fergus, et al. Learning multiagent communication with backpropagation.
Advances in neural information processing systems, 29, 2016.
[112] Alex Tamkin, Miles Brundage, Jack Clark, and Deep Ganguli. Understanding the capabilities, limitations, and societal impact of large language models. arXiv preprint arXiv:2102.02503, 2021.
[113] Ming Tan. Multi-agent reinforcement learning: Independent versus cooperative agents. In International Conference on Machine Learning, 1997.
[114] Rohan Taori, Ishaan Gulrajani, Tianyi Zhang, Yann Dubois, Xuechen Li, Carlos Guestrin, Percy Liang, and Tatsunori B. Hashimoto. Stanford alpaca: An instruction-following llama model. https://github.
com/tatsu-lab/stanford_alpaca, 2023.
[115] Gerald Tesauro et al. Temporal difference learning and td-gammon. Communications of the ACM, 38(3):5868, 1995.
[116] Romal Thoppilan, Daniel De Freitas, Jamie Hall, Noam Shazeer, Apoorv Kulshreshtha, Heng-Tze Cheng, Alicia Jin, Taylor Bos, Leslie Baker, Yu Du, et al. Lamda: Language models for dialog applications.
arXiv preprint arXiv:2201.08239, 2022.
16 [117] Hugo Touvron, Thibaut Lavril, Gautier Izacard, Xavier Martinet, Marie-Anne Lachaux, Timothée Lacroix, Baptiste Rozière, Naman Goyal, Eric Hambro, Faisal Azhar, et al. Llama: Open and efficient foundation language models. arXiv preprint arXiv:2302.13971, 2023.
[118] Xuezhi Wang, Jason Wei, Dale Schuurmans, Quoc Le, Ed Chi, and Denny Zhou. Self-consistency improves chain of thought reasoning in language models. In ICLR, 2023.
[119] Yizhong Wang, Yeganeh Kordi, Swaroop Mishra, Alisa Liu, Noah A Smith, Daniel Khashabi, and Hannaneh Hajishirzi. Self-instruct: Aligning language model with self generated instructions. arXiv preprint arXiv:2212.10560, 2022.
[120] Yizhong Wang, Swaroop Mishra, Pegah Alipoormolabashi, Yeganeh Kordi, Amirreza Mirzaei, An-jana Arunkumar, Arjun Ashok, Arut Selvan Dhanasekaran, Atharva Naik, David Stap, et al. Super-naturalinstructions:generalization via declarative instructions on 1600+ tasks. In EMNLP, 2022.
[121] Jason Wei, Maarten Bosma, Vincent Y Zhao, Kelvin Guu, Adams Wei Yu, Brian Lester, Nan Du, Andrew M Dai, and Quoc V Le. Finetuned language models are zero-shot learners. arXiv preprint arXiv:2109.01652, 2021.
[122] Jason Wei, Yi Tay, Rishi Bommasani, Colin Raffel, Barret Zoph, Sebastian Borgeaud, Dani Yogatama, Maarten Bosma, Denny Zhou, Donald Metzler, Ed H. Chi, Tatsunori Hashimoto, Oriol Vinyals, Percy Liang, Jeff Dean, and William Fedus. Emergent abilities of large language models. Transactions on Machine Learning Research, 2022. Survey Certification.
[123] Jason Wei, Xuezhi Wang, Dale Schuurmans, Maarten Bosma, Ed Chi, Quoc Le, and Denny Zhou. Chain of thought prompting elicits reasoning in large language models. arXiv preprint arXiv:2201.11903, 2022.
[124] Thomas Wolf, Lysandre Debut, Victor Sanh, Julien Chaumond, Clement Delangue, Anthony Moi, Pierric Cistac, Tim Rault, Rémi Louf, Morgan Funtowicz, Joe Davison, Sam Shleifer, Patrick von Platen, Clara Ma, Yacine Jernite, Julien Plu, Canwen Xu, Teven Le Scao, Sylvain Gugger, Mariama Drame, Quentin Lhoest, and Alexander M. Rush. Transformers: State-of-the-art natural language processing. In Proceedings of the 2020 Conference on Empirical Methods in Natural Language Processing: System Demonstrations, pages 3845, Online, October 2020. Association for Computational Linguistics.
[125] Michael Wooldridge. An introduction to multiagent systems. John wiley & sons, 2009.
[126] Shunyu Yao, Jeffrey Zhao, Dian Yu, Nan Du, Izhak Shafran, Karthik Narasimhan, and Yuan Cao.
ReAct: Synergizing reasoning and acting in language models. In International Conference on Learning Representations (ICLR), 2023.
[127] Sergey Zagoruyko and Nikos Komodakis. Paying more attention to attention: Improving the performance of convolutional neural networks via attention transfer. arXiv preprint arXiv:1612.03928, 2016.
[128] Eric Zelikman, Yuhuai Wu, Jesse Mu, and Noah D. Goodman. Star: Bootstrapping reasoning with reasoning, 2022.
[129] Houyu Zhang, Zhenghao Liu, Chenyan Xiong, and Zhiyuan Liu. Grounded conversation generation as guided traverses in commonsense knowledge graphs. In ACL, 2020.
[130] Rongsheng Zhang, Yinhe Zheng, Jianzhi Shao, Xiao-Xi Mao, Yadong Xi, and Minlie Huang. Dialogue distillation: Open-domain dialogue augmentation using unpaired data. ArXiv, abs/2009.09427, 2020.
[131] Susan Zhang, Stephen Roller, Naman Goyal, Mikel Artetxe, Moya Chen, Shuohui Chen, Christopher Dewan, Mona Diab, Xian Li, Xi Victoria Lin, et al. Opt: Open pre-trained transformer language models.
arXiv preprint arXiv:2205.01068, 2022.
[132] Zhuosheng Zhang, Aston Zhang, Mu Li, and Alex Smola. Automatic chain of thought prompting in large language models. In ICLR, 2023.
[133] Zhuosheng Zhang, Aston Zhang, Mu Li, Hai Zhao, George Karypis, and Alex Smola. Multimodal chain-of-thought reasoning in language models. arXiv preprint arXiv:2302.00923, 2023.
[134] Denny Zhou, Nathanael Schärli, Le Hou, Jason Wei, Nathan Scales, Xuezhi Wang, Dale Schuurmans, Olivier Bousquet, Quoc Le, and Ed Chi. Least-to-most prompting enables complex reasoning in large language models. arXiv preprint arXiv:2205.10625, 2022.
[135] Shuyan Zhou, Frank F Xu, Hao Zhu, Xuhui Zhou, Robert Lo, Abishek Sridhar, Xianyi Cheng, Yonatan Bisk, Daniel Fried, Uri Alon, et al. Webarena: A realistic web environment for building autonomous agents. arXiv preprint arXiv:2307.13854, 2023.
17 [136] Yongchao Zhou, Andrei Ioan Muresanu, Ziwen Han, Keiran Paster, Silviu Pitis, Harris Chan, and Jimmy Ba. Large language models are human-level prompt engineers. In The Eleventh International Conference on Learning Representations, 2023.
[137] Deyao Zhu, Jun Chen, Kilichbek Haydarov, Xiaoqian Shen, Wenxuan Zhang, and Mohamed Elhoseiny.
Chatgpt asks, blip-2 answers: Automatic questioning towards enriched visual descriptions, 2023.
18

View File

@ -0,0 +1,294 @@
# 多智能体主智能体Team Leader提示词优化调研报告
> 调研时间2026-07-14
> 目的:为 Astrion 项目的多智能体模式 Team Leader 提示词优化提供素材参考
---
## 一、当前现状
### 当前 Team Leader 提示词(`prompts/multi_agent/master.txt`)结构
1. 角色定义(一句话:协调多个子智能体分工协作)
2. 工作原则6 条:主动分工、明确指令、及时回答、监督进度、运行时引导、明确问答)
3. 工具清单9 个多智能体工具表格)
4. 会收到的消息格式(输出/提问 XML 示例)
5. 关于显示名
6. 通信协议三条硬性原则
7. 关于团队全局可见
### 当前子智能体提示词(`prompts/multi_agent/sub_agent.txt`)结构
1. 多智能体身份团队成员Team Leader 督导全局)
2. 在任务中4 条:不频繁输出、只汇报关键步骤、完成后给详细结论、自然结束即本轮结束)
3. 沟通工具ask_master / ask_other_agent / answer_other_agent / list_active_sub_agents
4. 汇报强制要求(向其他子智能体提问必须同时向 Team Leader 汇报)
5. 输出格式
6. 显示名
7. 专属设定role_body 占位符)
---
## 二、外部调研发现
### 来源 1Anthropic 多智能体研究系统(最重要)
**来源**https://www.anthropic.com/engineering/multi-agent-research-system
#### 核心架构
- Orchestrator-Worker 模式lead agent 协调subagents 并行工作
- Lead agent 分析查询 → 制定策略 → spawn subagents → subagents 独立搜索 → 返回结果给 lead agent → lead agent 综合最终答案
#### 8 条提示词工程原则(原文摘录)
1. **Think like your agents**(像你的智能体一样思考)
> "To iterate on prompts, you must understand their effects. We built simulations with the exact prompts and tools, then watched agents work step-by-step. This immediately revealed failure modes: agents continuing when they already had sufficient results, using overly verbose search queries, or selecting incorrect tools."
2. **Teach the orchestrator how to delegate**(教协调者如何委派)⭐ 最关键
> "The lead agent decomposes queries into subtasks and describes them to subagents. Each subagent needs an objective, an output format, guidance on the tools and sources to use, and clear task boundaries. Without detailed task descriptions, agents duplicate work, leave gaps, or fail to find necessary information."
> 教训:简单的 "research the semiconductor shortage" 指令太模糊,导致子智能体重复工作或误解任务。
3. **Scale effort to query complexity**(根据查询复杂度调整投入)⭐
> "Simple fact-finding requires just 1 agent with 3-10 tool calls, direct comparisons might need 2-4 subagents with 10-15 calls each, and complex research might use more than 10 subagents with clearly divided responsibilities."
4. **Tool design and selection are critical**(工具设计和选择至关重要)
> "Examine all available tools first, match tool usage to user intent, search the web for broad external exploration, or prefer specialized tools over generic ones."
5. **Let agents improve themselves**(让智能体自我改进)
> "Claude 4 models can be excellent prompt engineers. When given a prompt and a failure mode, they are able to diagnose why the agent is failing and suggest improvements."
6. **Start wide, then narrow down**(先宽后窄)
> "Search strategy should mirror expert human research: explore the landscape before drilling into specifics. Agents often default to overly long, specific queries. We counteracted this by prompting agents to start with short, broad queries, evaluate what's available, then progressively narrow focus."
7. **Guide the thinking process**(引导思考过程)
> "The lead agent uses thinking to plan its approach, assessing which tools fit the task, determining query complexity and subagent count, and defining each subagent's role."
8. **Parallel tool calling transforms speed and performance**(并行工具调用)
> "The lead agent spins up 3-5 subagents in parallel rather than serially; the subagents use 3+ tools in parallel. These changes cut research time by up to 90%."
#### 关键洞察
> "The best prompts for these agents are not just strict instructions, but **frameworks for collaboration** that define the division of labor, problem-solving approaches, and effort budgets."
#### 避坑经验
- 早期智能体会为简单查询 spawn 50 个子智能体 → 需要在提示词里嵌入 scaling rules
- 子智能体会用过长过具体的搜索查询 → 需要提示先宽后窄
- 子智能体会互相干扰,过度更新 → 需要限制通信频率
---
### 来源 2CAMEL 论文 - Inception Prompting
**来源**https://arxiv.org/abs/2303.17760 (NeurIPS 2023)
#### 四大失败模式CAMEL 系统识别的)
1. **Role-Flipping角色反转**:子智能体停止干活,开始像父智能体一样提问、要求澄清、或反向委派
2. **Instruction Echoing指令回声**:子智能体用不同的话复述任务但不实际执行
3. **Flake Replies敷衍回复**:给出模糊、不承诺的回复,如 "It seems fine"、"Further investigation needed"
4. **Infinite Loops无限循环**:子智能体卡在重复尝试同一种失败方法
#### Inception Prompting 解决方案5 个组件)
1. **Role anchoring角色锚定**
> "Never forget you are a <ASSISTANT_ROLE> and I am a <USER_ROLE>. Never flip roles! Never instruct me!"
2. **Output format enforcement输出格式强制**
> "You must write a specific solution that appropriately completes the requested instruction."
> "Always end <YOUR_SOLUTION> with: Next request."
3. **Completion signaling完成信号**
> "When the task is completed, you must only reply with a single word <CAMEL_TASK_DONE>."
4. **Anti-echo directives反回声指令**
> "Do not restate the task. Perform it."
5. **Convergence pressure收敛压力**
> "If an approach fails twice, try a fundamentally different approach rather than retrying the same thing."
#### CAMEL 提示词模板原文AI Society 场景)
**Assistant System Prompt:**
```
Never forget you are a <ASSISTANT_ROLE> and I am a <USER_ROLE>. Never flip roles!
Never instruct me!
We share a common interest in collaborating to successfully complete a task.
You must help me to complete the task.
Here is the task: <TASK>. Never forget our task!
I must instruct you based on your expertise and my needs to complete the task.
I must give you one instruction at a time.
You must write a specific solution that appropriately completes the requested instruction.
You must decline my instruction honestly if you cannot perform the instruction due to physical, moral, legal reasons or your capability and explain the reasons.
Unless I say the task is completed, you should always start with: Solution: <YOUR_SOLUTION>
<YOUR_SOLUTION> should be specific, and provide preferable implementations and examples for task-solving.
Always end <YOUR_SOLUTION> with: Next request.
```
**User System Prompt:**
```
Never forget you are a <USER_ROLE> and I am a <ASSISTANT_ROLE>.
Never flip roles! You will always instruct me.
We share a common interest in collaborating to successfully complete a task.
I must help you to complete the task.
Here is the task: <TASK>. Never forget our task!
You must instruct me based on my expertise and your needs to complete the task ONLY in the following two ways:
1. Instruct with a necessary input:
Instruction: <YOUR_INSTRUCTION>
Input: <YOUR_INPUT>
2. Instruct without any input:
Instruction: <YOUR_INSTRUCTION>
Input: None
You must give me one instruction at a time.
I must write a response that appropriately completes the requested instruction.
You must decline your instruction honestly if you cannot perform the instruction due to physical, moral, legal reasons or my capability and explain the reasons.
You should instruct me not ask me questions.
Now you must start to instruct me using the two ways described above.
Do not add anything else other than your instruction and the optional corresponding input!
Keep giving me instructions and necessary inputs until you think the task is completed.
When the task is completed, you must only reply with a single word <CAMEL_TASK_DONE>.
Never say <CAMEL_TASK_DONE> unless my responses have solved your task.
```
---
### 来源 3MetaGPT 论文 - SOP 驱动的多智能体协作
**来源**https://arxiv.org/abs/2308.00352
#### 核心理念SOPStandardized Operating Procedures
> "MetaGPT encodes Standardized Operating Procedures (SOPs) into prompt sequences for more streamlined workflows, thus allowing agents with human-like domain expertise to verify intermediate results and reduce errors."
#### 五个角色定义
- Product Manager分析竞争和用户需求创建 PRD
- Architect将需求转化为系统设计组件
- Project Manager任务分发
- Engineer执行代码编写
- QA Engineer测试用例和代码质量
#### 关键设计:结构化通信
> "Unlike ChatDev, agents in MetaGPT communicate through documents and diagrams (structured outputs) rather than dialogue. These documents contain all necessary information, preventing irrelevant or missing content."
#### 发布-订阅机制
> "Any agent can directly retrieve required information from the shared pool, eliminating the need to inquire about other agents and await their responses."
> "An agent activates its action only after receiving all its prerequisite dependencies."
#### 对闲聊的抑制
> "This reduces the risk of hallucinations caused by idle chatter between LLMs, particularly in role-playing frameworks, like: 'Hi, hello and how are you?' Alice (Product Manager); 'Great! Have you had lunch?' Bob (Architect)."
---
### 来源 4LangGraph/CrewAI/AutoGen 框架对比
**来源**https://dev.to/pockit_tools/langgraph-vs-crewai-vs-autogen-the-complete-multi-agent-ai-orchestration-guide-for-2026-2d63
#### LangGraph Supervisor Pattern 提示词示例
```python
def supervisor_node(state: AgentState) -> AgentState:
"""Central coordinator that routes to specialists."""
llm = ChatOpenAI(model="gpt-4o")
decision = llm.invoke([
{"role": "system", "content": """You are a supervisor.
Based on the current state, decide the next action:
- 'research': Need more information
- 'respond': Ready to generate response
- 'escalate': Needs human intervention
- 'complete': Task is done"""},
{"role": "user", "content": f"Current state: {state}"}
])
return {"next_action": decision.content}
```
#### CrewAI 角色定义模式
```python
classifier_agent = Agent(
role="Customer Intent Classifier",
goal="Accurately categorize customer inquiries to route them appropriately",
backstory="""You are an expert at understanding customer needs.
With years of experience in customer service, you can quickly
identify whether a customer needs billing help, technical support,
or has a complaint that needs escalation.""",
verbose=True,
allow_delegation=False # 关键:防止无限委派
)
```
#### 常见陷阱及解决方案
1. **Over-Engineering**20 个智能体做 3 个就能做的事 → 从 2-3 个开始
2. **Infinite Loops**:智能体互相无限委派 → 设置终止条件 `max_iter=10`
3. **Context Window Explosion**:传递完整历史 → 实现摘要或滑动窗口
4. **No Error Boundaries**:一个智能体失败崩溃全系统 → 包裹错误处理
---
### 来源 5GitHub Issue - Inception Prompting 实战
**来源**https://github.com/NousResearch/hermes-agent/issues/375
#### Hermes Agent 的 Inception Prompting 实现方案
**Phase 1: Prompt Hardening提示词加固**
修改 `_build_child_system_prompt()` 包含:
- **Role-anchoring**: "You are executing a delegated task. Do the work directly — do not ask questions, request clarification, or suggest the requester do it instead."
- **Anti-echo**: "Do not restate or paraphrase the task. Perform it using your tools and report concrete results."
- **Output format guidance**: "Your response must include specific findings, file paths, code snippets, or other concrete artifacts. Vague summaries like 'it seems fine' or 'further investigation needed' are not acceptable."
- **Convergence pressure**: "If an approach fails twice, try a fundamentally different approach rather than retrying the same thing."
**Phase 2: Enhanced Guard Rails增强护栏**
- 角色特定提示词coordinator, worker, synthesizer
- 反循环检测:如果智能体最近 N 条消息语义相似,注入 "you appear stuck" 干预
---
## 三、综合分析:当前提示词的不足
对照外部最佳实践,当前 Team Leader 提示词存在以下可改善点:
### 3.1 Team Leader 提示词
| # | 不足 | 外部参考 |
|---|------|----------|
| 1 | **缺少任务复杂度评估指引** | Anthropic应在提示词中嵌入 scaling rules教 Team Leader 评估何时用 1 个子智能体、何时用多个 |
| 2 | **缺少委派质量标准** | Anthropic每个子智能体需要 objective + output format + tools guidance + task boundaries当前只说"写清楚任务目标、范围、产出要求"太笼统 |
| 3 | **缺少"先宽后窄"策略指引** | Anthropic应引导 Team Leader 在分解任务时先探索全局再深入 |
| 4 | **缺少防过度工程指引** | LangGraph/CrewAI应明确"从 2-3 个子智能体开始,遇到明确瓶颈再加" |
| 5 | **缺少收敛和终止判断指引** | CAMEL/MetaGPT应教 Team Leader 如何判断任务完成、如何防止子智能体无限循环 |
| 6 | **缺少对子智能体失败模式的预判** | CAMEL应告知 Team Leader 子智能体可能出现 role-flipping、echoing、flake replies并给出应对策略 |
| 7 | **角色定义太单薄** | CrewAI/MetaGPT当前只有一句话"协调多个子智能体分工协作",缺少 goal、constraints、思维方式指引 |
| 8 | **缺少综合与决策角色强调** | AnthropicTeam Leader 不仅是"协调者",更是"综合者"和"决策者" |
| 9 | **缺少上下文管理意识** | Anthropic应提醒 Team Leader 子智能体输出会消耗主上下文,需要引导子智能体精简输出 |
### 3.2 子智能体提示词
| # | 不足 | 外部参考 |
|---|------|----------|
| 1 | **缺少 role anchoring** | CAMEL应明确"你是执行者,不要反转角色,不要反向提问或委派" |
| 2 | **缺少 anti-echo 指令** | CAMEL/Hermes应明确"不要复述任务,直接执行" |
| 3 | **缺少输出具体性要求** | Hermes应要求"输出必须包含具体发现、文件路径、代码片段等具体产物,模糊总结不可接受" |
| 4 | **缺少收敛压力** | CAMEL/Hermes应加入"如果一个方法失败两次,换一种根本不同的方法" |
| 5 | **缺少 stuck 自检指引** | Hermes Phase 2应引导子智能体自我检测是否陷入循环 |
| 6 | **"不频繁输出"太模糊** | 应给出更具体的输出节奏指引 |
---
## 四、优化方向建议
### 4.1 Team Leader 提示词优化方向
1. **丰富角色定义**:从"协调者"升级为"战略规划者 + 任务分解者 + 综合决策者"
2. **加入任务复杂度评估框架**:简单/中等/复杂三档,对应不同的子智能体数量和工具调用预算
3. **加入委派质量检查清单**:每次委派前自检 objective/output format/tools/boundaries 是否齐全
4. **加入失败模式预判和应对**:告知子智能体可能的问题行为及干预手段
5. **加入收敛和终止指引**:如何判断整体任务完成、如何处理卡住的子智能体
6. **加入上下文管理意识**:引导子智能体精简输出,避免主上下文膨胀
### 4.2 子智能体提示词优化方向
1. **加入 role anchoring**:明确角色边界,禁止角色反转
2. **加入 anti-echo 和输出具体性要求**:不复述任务,输出必须具体
3. **加入收敛压力和 stuck 自检**:失败换方法,自我检测循环
4. **优化输出节奏指引**:从"不频繁输出"改为更具体的节奏建议
---
*报告完毕,接下来可与用户讨论具体优化方案。*

View File

@ -1342,6 +1342,13 @@ def initialize_system(path: str, thinking_mode: bool = False):
print(f"[Init] 调试日志: {DEBUG_LOG_FILE}")
app.config['DEFAULT_THINKING_MODE'] = thinking_mode
app.config['DEFAULT_RUN_MODE'] = "thinking" if thinking_mode else "fast"
# 同步预设子智能体角色到运行态目录
try:
from modules.multi_agent.role_store import sync_preset_roles
sync_preset_roles()
print(f"{OUTPUT_FORMATS['success']} 预设子智能体角色同步完成")
except Exception as _e:
print(f"{OUTPUT_FORMATS['warning']} 预设子智能体角色同步失败: {_e}")
print(f"{OUTPUT_FORMATS['success']} Web系统初始化完成多用户模式")

View File

@ -224,7 +224,7 @@ def host_login():
data_dir.mkdir(parents=True, exist_ok=True)
logs_dir = Path(LOGS_DIR).expanduser().resolve()
logs_dir.mkdir(parents=True, exist_ok=True)
uploads_dir = host_path / ".agents" / "user_upload"
uploads_dir = host_path / ".astrion" / "user_upload"
uploads_dir.mkdir(parents=True, exist_ok=True)
quarantine_root = Path(UPLOAD_QUARANTINE_SUBDIR).expanduser()
if not quarantine_root.is_absolute():

View File

@ -1077,6 +1077,25 @@ async def poll_multi_agent_notifications(*, web_terminal, workspace, conversatio
ma_debug("poll_ma_notifications_end", conversation_id=conversation_id, tick=tick_count)
# poll 退出时,如果主智能体也空闲且没有 pending 消息,发送 task_complete 通知前端对话变空闲。
# 否则前端会一直保持 taskInProgress=trueMultiAgentTaskProbe 会持续轮询但找不到任务。
if not getattr(web_terminal, "_multi_agent_main_task_active", False):
state = manager.get_multi_agent_state(conversation_id) if manager else None
still_running = state and any(a.status == "running" for a in state.list_all())
still_pending = state and state.has_pending_master_messages()
if not still_running and not still_pending:
sender('task_complete', {
'total_iterations': 0,
'total_tool_calls': 0,
'auto_fix_attempts': 0,
'has_running_sub_agents': False,
'has_running_background_commands': False,
'has_running_multi_agent': False,
'pending_runtime_guidance_messages': [],
'multi_agent_idle_signal': True,
})
ma_debug("poll_ma_idle_signal_sent", conversation_id=conversation_id, tick=tick_count)
async def _dispatch_multi_agent_idle_messages(
*,
@ -1814,9 +1833,43 @@ async def handle_task_with_sender(
)
except Exception as exc:
debug_log(f"[Goal] 用户停止时停止目标失败: {exc}")
# 计算后台任务状态,前端用这个字段决定是否保持对话运行态、输入区是否释放
_stopped_has_bg = False
_stopped_has_bg_cmd = False
_stopped_has_ma = False
_manager = getattr(web_terminal, "sub_agent_manager", None)
if _manager:
try:
_manager.reconcile_task_states(conversation_id=conversation_id)
except Exception:
pass
if getattr(web_terminal, "multi_agent_mode", False):
_state = _manager.get_multi_agent_state(conversation_id)
if _state:
_stopped_has_ma = any(a.status == "running" for a in _state.list_all())
for _t in _manager.tasks.values():
if _t.get("conversation_id") != conversation_id:
continue
if _t.get("status") in TERMINAL_STATUSES.union({"terminated"}):
continue
if _t.get("multi_agent_mode"):
continue
_stopped_has_bg = True
break
_bg_mgr = getattr(web_terminal, "background_command_manager", None)
if _bg_mgr and conversation_id:
try:
_bg_mgr.reconcile_stale_records(conversation_id=conversation_id)
except Exception:
pass
if _bg_mgr.list_waiting_items(conversation_id):
_stopped_has_bg_cmd = True
sender('task_stopped', {
'message': '任务已停止',
'reason': 'user_requested'
'reason': 'user_requested',
'has_running_sub_agents': _stopped_has_bg,
'has_running_background_commands': _stopped_has_bg_cmd,
'has_running_multi_agent': _stopped_has_ma,
})
break
@ -2170,7 +2223,10 @@ async def handle_task_with_sender(
sender('task_stopped', {
'message': tool_loop_result.get("approval_message") or '操作被用户拒绝',
'reason': 'approval_rejected',
'conversation_id': conversation_id
'conversation_id': conversation_id,
'has_running_sub_agents': False,
'has_running_background_commands': False,
'has_running_multi_agent': False
})
finalize_user_work_timer()
finalize_run_versioning_checkpoint("approval_rejected")

View File

@ -180,9 +180,9 @@ def get_user_resources(
data_dir.mkdir(parents=True, exist_ok=True)
logs_dir = Path(LOGS_DIR).expanduser().resolve()
logs_dir.mkdir(parents=True, exist_ok=True)
uploads_dir = project_path / ".agents" / "user_upload"
uploads_dir = project_path / ".astrion" / "user_upload"
uploads_dir.mkdir(parents=True, exist_ok=True)
skills_dir = project_path / ".agents" / "skills"
skills_dir = project_path / ".astrion" / "skills"
skills_dir.mkdir(parents=True, exist_ok=True)
quarantine_root = Path(UPLOAD_QUARANTINE_SUBDIR).expanduser()
if not quarantine_root.is_absolute():

View File

@ -177,7 +177,9 @@ def _get_active_workspace_task(username: str, workspace_id: str):
def _build_safe_load_result(terminal: WebTerminal, conversation_id: str) -> Dict[str, Any]:
"""只读取对话元数据,不调用 terminal.load_conversation避免修改运行任务正在使用的上下文。"""
normalized_id = _normalize_conv_id(conversation_id)
cm = getattr(getattr(terminal, "context_manager", None), "conversation_manager", None)
ctx_manager = getattr(terminal, "context_manager", None)
# 用 _get_conversation_manager_for_id 自动判断对话属于普通管理器还是多智能体管理器
cm = ctx_manager._get_conversation_manager_for_id(normalized_id) if ctx_manager else None
data = cm.load_conversation(normalized_id) if cm else None
if not data:
return {
@ -840,7 +842,8 @@ def load_conversation(conversation_id, terminal: WebTerminal, workspace: UserWor
result_message=result.get("message", ""),
not_found=True,
)
return jsonify(result), 404 if "不存在" in result.get("message", "") else 500
status_code = 404 if "不存在" in result.get("message", "") else 500
return jsonify(result), status_code
except Exception as e:
import traceback
@ -1672,13 +1675,26 @@ def list_sub_agents(terminal: WebTerminal, workspace: UserWorkspace, username: s
pass
conversation_id = terminal.context_manager.current_conversation_id
data = manager.get_overview(conversation_id=conversation_id)
# 传统模式子智能体列表必须排除多智能体任务,避免 /new 等无当前对话场景
# 把最近一次多智能体模式的 running/idle 子智能体串过来。
def _is_multi_agent_task(item: dict) -> bool:
task_id = item.get("task_id")
raw_task = manager.tasks.get(task_id) if task_id else None
return bool(raw_task.get("multi_agent_mode")) if isinstance(raw_task, dict) else False
data = [item for item in data if not _is_multi_agent_task(item)]
# 仅在未绑定具体对话时(如全新会话尚未创建 conversation_id才回退为全局运行态
# 否则只显示当前对话关联的子智能体,避免新对话看到其他对话的任务。
if not data and not conversation_id:
all_overview = manager.get_overview(conversation_id=None)
if all_overview:
terminal_statuses = TERMINAL_STATUSES.union({"terminated"})
running_only = [item for item in all_overview if item.get("status") not in terminal_statuses]
running_only = [
item for item in all_overview
if item.get("status") not in terminal_statuses and not _is_multi_agent_task(item)
]
if running_only:
data = running_only
debug_log(
@ -1805,6 +1821,55 @@ def get_sub_agent_activity(task_id: str, terminal: WebTerminal, workspace: UserW
return jsonify({"success": False, "error": str(exc)}), 500
@conversation_bp.route('/api/sub_agents/stop_all', methods=['POST'])
@api_login_required
@with_terminal
def stop_all_sub_agents(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""批量停止当前会话的所有非终态子智能体。
body: {"mode": "terminate" | "soft_stop"}
- mode=terminate: 传统模式终结子智能体不保留实例
- mode=soft_stop: 多智能体模式软停止保留实例可被后续唤醒
"""
data = request.get_json(silent=True) or {}
mode = str(data.get('mode') or 'terminate').strip().lower()
if mode not in {'terminate', 'soft_stop'}:
return jsonify({"success": False, "error": "mode 只支持 terminate/soft_stop"}), 400
manager = getattr(terminal, 'sub_agent_manager', None)
if not manager:
return jsonify({"success": False, "error": "子智能体管理器不可用"}), 404
conversation_id = terminal.context_manager.current_conversation_id
try:
try:
manager._load_state()
except Exception:
pass
manager.reconcile_task_states(conversation_id=conversation_id)
stopped_count = 0
if mode == 'soft_stop':
stopped_count = manager.soft_stop_all_agents(conversation_id)
debug_log(f"[StopAllSubAgents] soft_stop 会话={conversation_id}, 计数={stopped_count}")
else:
# terminate: 遍历所有非终态子智能体逐个终结
from modules.sub_agent.state import TERMINAL_STATUSES
for task_info in list(manager.tasks.values()):
if task_info.get('conversation_id') != conversation_id:
continue
status = task_info.get('status')
if status in TERMINAL_STATUSES.union({"terminated"}):
continue
try:
tid = task_info.get('task_id')
manager.terminate_sub_agent(task_id=tid)
stopped_count += 1
debug_log(f"[StopAllSubAgents] terminated task_id={tid}")
except Exception as exc:
debug_log(f"[StopAllSubAgents] 终止失败 tid={task_info.get('task_id')}, err={exc}")
return jsonify({"success": True, "data": {"stopped_count": stopped_count, "mode": mode}})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@conversation_bp.route('/api/sub_agents/<task_id>/terminate', methods=['POST'])
@api_login_required
@with_terminal
@ -2064,7 +2129,7 @@ def review_conversation(conversation_id, terminal: WebTerminal, workspace: UserW
content = "\n".join(lines) + "\n"
char_count = len(content)
review_dir = workspace.project_path / ".agents" / "review"
review_dir = workspace.project_path / ".astrion" / "review"
review_dir.mkdir(parents=True, exist_ok=True)
title = conversation_data.get("title") or "untitled"

View File

@ -246,7 +246,7 @@ def _write_compact_file(
latest_user_input: str,
previous_records: List[Dict[str, Any]],
) -> str:
compact_dir = project_path / ".agents" / "compact_result"
compact_dir = project_path / ".astrion" / "compact_result"
compact_dir.mkdir(parents=True, exist_ok=True)
filename = f"compact_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{compression_index:03d}.md"
file_path = compact_dir / filename
@ -312,7 +312,12 @@ async def run_deep_compression(
sender=None,
) -> Dict[str, Any]:
cm = web_terminal.context_manager
conv_data = cm.conversation_manager.load_conversation(conversation_id)
target_manager = (
cm._get_conversation_manager_for_id(conversation_id)
if hasattr(cm, "_get_conversation_manager_for_id")
else cm.conversation_manager
)
conv_data = target_manager.load_conversation(conversation_id)
if not conv_data:
return {"success": False, "error": f"对话不存在: {conversation_id}"}
@ -419,7 +424,7 @@ async def run_deep_compression(
# 关键:重置 current_context_tokens避免自动压缩续接后阈值判断仍读到压缩前的大值而陷入死循环。
# 真实上下文长度会在下一次 API 响应后被重新写入。
try:
cm.conversation_manager.update_token_statistics(
target_manager.update_token_statistics(
conversation_id,
input_tokens=0,
output_tokens=0,
@ -484,7 +489,7 @@ async def run_deep_compression(
}
for frozen_key in REBUILD_FROZEN_KEYS:
meta_updates[frozen_key] = None
cm.conversation_manager.update_conversation_metadata(conversation_id, meta_updates)
target_manager.update_conversation_metadata(conversation_id, meta_updates)
# 同步内存中的 metadata清除 frozen 缓存
try:
if getattr(cm, "current_conversation_id", None) == conversation_id and isinstance(cm.conversation_metadata, dict):

View File

@ -15,6 +15,16 @@ from flask import Blueprint, current_app, jsonify, request, session
from server.auth_helpers import api_login_required, login_required, get_current_username
from server.context import get_user_resources
from modules.multi_agent.role_store import (
RoleConfig,
list_roles,
load_preset_role,
save_custom_role,
delete_custom_role,
is_preset_role,
sync_preset_roles,
)
from config.paths import CUSTOM_ROLES_DIR, WEB_PRESET_ROLES_DIR
multi_agent_bp = Blueprint("multi_agent", __name__)
@ -57,13 +67,35 @@ def rebuild_conversation_index_api():
return jsonify({"success": False, "error": str(exc)}), 500
def _get_role_dirs() -> tuple:
"""根据 session 判断当前登录模式,返回 (runtime_dir, custom_dir)。
- host 模式runtime_dir = custom_dir = host/mutiagents/agents
- web/docker 模式runtime_dir = web/mutiagents/agents, custom_dir = users/<user>/personal/mutiagents/agents
"""
from flask import session as _session
is_host_session = bool(_session.get('host_mode'))
if is_host_session:
return CUSTOM_ROLES_DIR, CUSTOM_ROLES_DIR
# web 模式
username = get_current_username()
if not username:
return WEB_PRESET_ROLES_DIR, None
_, workspace = get_user_resources(username)
if not workspace or not workspace.data_dir:
return WEB_PRESET_ROLES_DIR, None
from modules.multi_agent.role_store import infer_custom_roles_dir
custom = infer_custom_roles_dir(workspace.data_dir)
return WEB_PRESET_ROLES_DIR, str(custom) if custom else None
@multi_agent_bp.route("/api/multiagent/roles", methods=["GET"])
@api_login_required
def list_roles_api():
"""列出全部可用角色(预置+自定义)。"""
"""列出全部可用角色(预置+用户自定义)。"""
try:
from modules.multi_agent.role_store import list_roles
roles = list_roles()
runtime_dir, custom_dir = _get_role_dirs()
roles = list_roles(runtime_dir=runtime_dir, custom_dir=custom_dir)
return jsonify({
"success": True,
"roles": [r.to_dict() for r in roles],
@ -77,19 +109,23 @@ def list_roles_api():
def create_role_api():
"""创建自定义角色。body: { role_id, name, description?, body_prompt, thinking_mode? }"""
try:
from modules.multi_agent.role_store import RoleConfig, save_custom_role, list_roles
data = request.get_json() or {}
role_id = str(data.get("role_id") or "").strip()
name = str(data.get("name") or "").strip()
body_prompt = str(data.get("body_prompt") or "").strip()
description = str(data.get("description") or "").strip()
thinking_mode = str(data.get("thinking_mode") or "fast").strip()
model_key = str(data.get("model_key") or "").strip() or None
if not role_id or not name or not body_prompt:
return jsonify({"success": False, "error": "role_id/name/body_prompt 必填"}), 400
if thinking_mode not in {"fast", "thinking"}:
thinking_mode = "fast"
# 不允许覆盖已存在的同名角色
existing = {r.role_id for r in list_roles()}
# 不允许覆盖预设角色
if is_preset_role(role_id):
return jsonify({"success": False, "error": f"不能覆盖预设角色 {role_id}"}), 409
# 不允许覆盖已存在的用户自定义角色
runtime_dir, custom_dir = _get_role_dirs()
existing = {r.role_id for r in list_roles(runtime_dir=runtime_dir, custom_dir=custom_dir)}
if role_id in existing:
return jsonify({"success": False, "error": f"角色 {role_id} 已存在"}), 409
role = RoleConfig(
@ -98,13 +134,117 @@ def create_role_api():
description=description,
body_prompt=body_prompt,
thinking_mode=thinking_mode,
model_key=model_key,
is_custom=True,
)
saved = save_custom_role(role)
saved = save_custom_role(role, custom_dir=custom_dir)
return jsonify({"success": True, "role_id": role_id, "file": str(saved)})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@multi_agent_bp.route("/api/multiagent/roles/<role_id>", methods=["PUT"])
@api_login_required
def update_role_api(role_id: str):
"""更新用户自定义角色。不能更新预设角色。"""
try:
data = request.get_json() or {}
runtime_dir, custom_dir = _get_role_dirs()
# 加载现有角色(先查自定义覆盖,再查预设)
role = load_preset_role(role_id, runtime_dir=runtime_dir, custom_dir=custom_dir)
if not role:
return jsonify({"success": False, "error": f"角色 {role_id} 不存在"}), 404
# 更新字段
if "name" in data:
role.name = str(data["name"]).strip() or role.name
if "description" in data:
role.description = str(data["description"] or "").strip()
if "body_prompt" in data:
role.body_prompt = str(data["body_prompt"] or "").strip()
if "thinking_mode" in data:
tm = str(data["thinking_mode"] or "fast").strip()
if tm in {"fast", "thinking"}:
role.thinking_mode = tm
if "model_key" in data:
role.model_key = str(data["model_key"] or "").strip() or None
if "skills" in data:
role.skills = list(data["skills"] or [])
role.is_custom = True
saved = save_custom_role(role, custom_dir=custom_dir)
return jsonify({"success": True, "role_id": role_id, "file": str(saved)})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@multi_agent_bp.route("/api/multiagent/roles/<role_id>", methods=["DELETE"])
@api_login_required
def delete_role_api(role_id: str):
"""删除用户自定义角色。不能删除预设角色。"""
try:
if is_preset_role(role_id):
return jsonify({"success": False, "error": f"不能删除预设角色 {role_id}"}), 403
_, custom_dir = _get_role_dirs()
deleted = delete_custom_role(role_id, custom_dir=custom_dir)
if not deleted:
return jsonify({"success": False, "error": f"角色 {role_id} 不存在或无法删除"}), 404
return jsonify({"success": True, "role_id": role_id})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@multi_agent_bp.route("/api/multiagent/settings", methods=["GET"])
@api_login_required
def get_multi_agent_settings_api():
"""获取多智能体模式设置(子智能体压缩阈值等)。"""
try:
username = get_current_username()
if not username:
return jsonify({"success": False, "error": "未登录"}), 401
_, workspace = get_user_resources(username)
if not workspace:
return jsonify({"success": False, "error": "工作区未就绪"}), 503
# 从个人化配置中读取子智能体压缩阈值
from modules.personalization_manager import load_personalization_config
prefs = load_personalization_config(workspace.data_dir) or {}
compress_threshold = prefs.get("sub_agent_compress_threshold_tokens", 150000)
return jsonify({
"success": True,
"settings": {
"sub_agent_compress_threshold_tokens": compress_threshold,
},
})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@multi_agent_bp.route("/api/multiagent/settings", methods=["PUT"])
@api_login_required
def update_multi_agent_settings_api():
"""更新多智能体模式设置。"""
try:
username = get_current_username()
if not username:
return jsonify({"success": False, "error": "未登录"}), 401
_, workspace = get_user_resources(username)
if not workspace:
return jsonify({"success": False, "error": "工作区未就绪"}), 503
data = request.get_json() or {}
settings = data.get("settings") or {}
# 更新子智能体压缩阈值
threshold = settings.get("sub_agent_compress_threshold_tokens")
if threshold is not None:
threshold = int(threshold)
if threshold < 10000:
return jsonify({"success": False, "error": "压缩阈值不能小于 10000"}), 400
from modules.personalization_manager import load_personalization_config, save_personalization_config
prefs = load_personalization_config(workspace.data_dir) or {}
prefs["sub_agent_compress_threshold_tokens"] = threshold
save_personalization_config(workspace.data_dir, prefs)
return jsonify({"success": True})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@multi_agent_bp.route("/api/multiagent/conversations", methods=["POST"])
@api_login_required
def create_multi_agent_conversation():
@ -206,6 +346,41 @@ def create_multi_agent_conversation():
}), 201
@multi_agent_bp.route("/api/multiagent/models", methods=["GET"])
@api_login_required
def list_sub_agent_models_api():
"""列出子智能体可用的模型列表(从 sub_agent_models.json 读取,脱敏后返回)。"""
try:
from config.sub_agent import SUB_AGENT_MODELS_CONFIG_FILE
from pathlib import Path as _Path
import json as _json
config_path = SUB_AGENT_MODELS_CONFIG_FILE
if not _Path(config_path).exists():
return jsonify({"success": True, "models": [], "default_model": ""})
raw = _json.loads(_Path(config_path).read_text(encoding="utf-8"))
models_raw = raw.get("models", []) if isinstance(raw, dict) else (raw if isinstance(raw, list) else [])
default_model = str(raw.get("default_model", "")) if isinstance(raw, dict) else ""
# 脱敏:只返回 name / modes / multimodal / max_output / max_context
safe_models = []
for m in models_raw:
if not isinstance(m, dict):
continue
safe_models.append({
"name": m.get("name") or m.get("model_name") or "",
"modes": m.get("modes") or m.get("mode") or "",
"multimodal": m.get("multimodal") or "none",
"max_output": m.get("max_output") or m.get("max_tokens") or 0,
"max_context": m.get("max_context") or m.get("context_window") or 0,
})
return jsonify({
"success": True,
"models": safe_models,
"default_model": default_model,
})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@multi_agent_bp.route("/api/multiagent/active_sub_agents", methods=["GET"])
@api_login_required
def list_active_sub_agents_api():

View File

@ -212,32 +212,39 @@ class TaskManager:
]
def cancel_task(self, username: str, task_id: str) -> bool:
"""取消主智能体任务。只停主智能体,不触碰后台任务。
后台任务子智能体后台指令的终止/暂停由独立的 API 处理
参见 /api/sub_agents/stop_all /api/background_commands/stop_all
"""
rec = self.get_task(username, task_id)
if not rec:
debug_log(f"[TaskCancel] cancel_task 找不到任务: task_id={task_id}")
return False
# 立即快照进入时的状态,后续所有两段式判断都用它。
# _run_chat_task finally 会在硬取消后并发修改 rec.status
# 如果后面再读会被误判为第二下。
task_id = rec.task_id
status_at_entry = rec.status
debug_log(
f"[TaskCancel] 入口: task_id={task_id}, status={status_at_entry}, "
f"conversation_id={rec.conversation_id}, workspace_id={rec.workspace_id}"
)
# 1. 硬取消主 asyncio task如果引用还在
# 取终端引用(仅用于停止目标模式等副作用)
entry = stop_flags.get(task_id)
terminal = None
if isinstance(entry, dict):
terminal = entry.get('terminal')
if not terminal and rec.workspace_id:
try:
terminal, _ = get_user_resources(username, rec.workspace_id)
except Exception:
pass
debug_log(
f"[TaskCancel] 入口: task_id={task_id}, status={status_at_entry}, "
f"conv={rec.conversation_id}"
)
# 1. 硬取消主 asyncio task如果引用还在
if isinstance(entry, dict):
loop = entry.get('loop')
task = entry.get('task')
debug_log(
f"[TaskCancel] stop_flags entry: loop_exists={loop is not None}, "
f"task_exists={task is not None}, task_done={task.done() if task else 'n/a'}"
)
if loop and task and not task.done():
try:
loop.call_soon_threadsafe(task.cancel)
@ -246,14 +253,12 @@ class TaskManager:
debug_log(f"[TaskCancel] 硬取消失败: task_id={task_id}, error={exc}")
entry['stop'] = True
else:
debug_log(f"[TaskCancel] stop_flags 无 entry设软标志: task_id={task_id}")
stop_flags[task_id] = {'stop': True, 'task': None, 'terminal': None, 'loop': None}
entry = stop_flags[task_id]
rec.stop_requested = True
# 2. 停止目标模式
workspace = None
if rec.workspace_id:
try:
_, workspace = get_user_resources(username, rec.workspace_id)
@ -270,69 +275,19 @@ class TaskManager:
rec.runtime_guidance_queue = []
rec.updated_at = time.time()
# 4. 两段式停止:严格按进入时的 rec.status 判断(快照),不依赖 task.done() 的异步时机。
# 原因:投递硬取消后 _run_chat_task finally 可能并发把 status 改成 cancel_requested
# 若此时再读 rec.status 会误判为第二下,导致第一下就误杀后台。
# - 第一下running/pending只硬取消主智能体并置为 cancel_requested保留后台任务。
# - 第二下cancel_requested/stopped/canceled清理后台任务并置为 stopped。
# - 额外保护:极短时间内(<500ms的重复 cancel 请求视为抖动,不执行第二下。
# 4. 标记为 cancel_requested。
# _run_chat_task finally 会并发把 status 改为 stopped 并发 task_stopped 事件。
# 此处仅做软标记,不强制覆盖 finally 即将设的终态。
now = time.time()
is_first_press = status_at_entry in {"running", "pending"}
if not terminal and rec.workspace_id:
try:
terminal, _ = get_user_resources(username, rec.workspace_id)
except Exception as exc:
debug_log(f"[TaskCancel] 取消任务时重新获取 terminal 失败: {exc}")
if is_first_press:
# 第一下:主智能体仍在跑,只请求取消,保留后台任务。
# 注意 1不要清空 rec.events否则 _run_chat_task finally 发送的
# task_stopped 事件会被丢弃,前端无法感知并弹提示。
# 注意 2_run_chat_task finally 可能并发先执行并把 status 设为 stopped
# 此时不要再覆盖回 cancel_requested。
with self._lock:
if rec.status in {"running", "pending"}:
rec.status = "cancel_requested"
rec.updated_at = now
rec.last_cancel_at = now
debug_log(
f"[TaskCancel] 第一下:请求取消智能体,保留后台任务: "
f"task_id={task_id}, status_at_entry={status_at_entry}, "
f"current_status={rec.status}"
)
return True
# 第二下:清理后台任务(过滤 500ms 内的重复请求)
if rec.last_cancel_at is not None and (now - rec.last_cancel_at) < 0.5:
debug_log(
f"[TaskCancel] 忽略 500ms 内重复取消请求: task_id={task_id}, "
f"elapsed={now - rec.last_cancel_at:.3f}s"
)
return True
has_running_background = self._cleanup_background_tasks(rec, terminal)
with self._lock:
rec.status = "stopped"
rec.updated_at = time.time()
rec.last_cancel_at = time.time()
rec.events.clear()
if rec.status in {"running", "pending"}:
rec.status = "cancel_requested"
rec.updated_at = now
rec.last_cancel_at = now
debug_log(
f"[TaskCancel] 第二下:清理后台并停止: task_id={task_id}, "
f"has_running_background={has_running_background}"
f"[TaskCancel] 已取消主智能体: task_id={task_id}, "
f"status_at_entry={status_at_entry}, current_status={rec.status}"
)
try:
from server.extensions import socketio
socketio.emit('task_stopped', {
'message': '任务已停止',
'reason': 'user_requested',
'task_id': task_id,
'conversation_id': rec.conversation_id,
'has_running_sub_agents': False,
'has_running_background_commands': False,
}, room=f"user_{username}")
except Exception as exc:
debug_log(f"[TaskCancel] 发送 task_stopped 事件失败: {exc}")
return True
@staticmethod

View File

@ -63,6 +63,8 @@
:icon-style="iconStyle"
:format-time="formatTime"
:running-tasks="runningWorkspaceTasks"
:current-task-in-progress="taskInProgress"
:current-conversation-id="currentConversationId"
:current-workspace-id="currentHostWorkspaceId"
:display-mode="chatDisplayMode"
:display-mode-disabled="displayModeSwitchDisabled"
@ -279,7 +281,7 @@
:input-is-multiline="inputIsMultiline"
:input-is-focused="inputIsFocused"
:is-connected="isConnected"
:streaming-message="composerStreamingForInput"
:streaming-message="streamingUi"
:input-locked="displayLockEngaged"
:uploading="uploading"
:media-uploading="mediaUploading"
@ -328,8 +330,13 @@
:goal-mode-armed="goalModeArmed"
:goal-running="goalRunning"
:goal-progress="goalProgress"
:multi-agent-mode="multiAgentMode"
:has-running-task="composerBusy || anyWorkspaceHasRunningTask"
:main-chat-idle="mainChatIdle"
:active-sub-agent-count="activeSubAgentCount"
:avatar-status="showStatusAvatar && messages.length > 0 ? avatarStatus : null"
@restore-user-question="restoreUserQuestionDialog"
@stop-all-sub-agents="handleStopAllSubAgents"
@update:input-message="inputSetMessage"
@input-change="handleInputChange"
@input-focus="handleInputFocus"
@ -366,6 +373,7 @@
@delete-runtime-message="handleDeleteRuntimeMessage"
@composer-height-change="handleComposerHeightChange"
@toggle-goal-mode="handleToggleGoalMode"
@toggle-agent-mode="handleToggleAgentMode"
@open-goal-dialog="goalDialogOpen = true"
@open-git-changes-panel="openGitChangesPanel"
/>
@ -759,6 +767,8 @@
:icon-style="iconStyle"
:format-time="formatTime"
:running-tasks="runningWorkspaceTasks"
:current-task-in-progress="taskInProgress"
:current-conversation-id="currentConversationId"
:current-workspace-id="currentHostWorkspaceId"
:display-mode="chatDisplayMode"
:display-mode-disabled="displayModeSwitchDisabled"

View File

@ -231,6 +231,13 @@ export const computed = {
);
});
},
anyWorkspaceHasRunningTask() {
const tasks = Array.isArray(this.runningWorkspaceTasks) ? this.runningWorkspaceTasks : [];
return tasks.some((task: any) => {
const status = String(task?.status || '');
return ['pending', 'running', 'cancel_requested'].includes(status);
});
},
streamingUi() {
return this.streamingMessage || this.hasPendingToolActions();
},
@ -245,6 +252,36 @@ export const computed = {
this.compressing
);
},
/**
* running + idle failed/completed/terminated/timeout
* x
*/
activeSubAgentCount() {
const subAgentStore = useSubAgentStore();
const list = Array.isArray(subAgentStore.subAgents) ? subAgentStore.subAgents : [];
const TERMINAL = new Set(['completed', 'failed', 'timeout', 'terminated']);
return list.filter((s: any) => {
const status = String(s?.status || '').toLowerCase();
return !TERMINAL.has(status);
}).length;
},
/**
*
* composerBusy composerBusy=true
* waitingForSubAgent / has_running_multi_agent
*
* mainChatIdle streaming
* monitor
*/
mainChatIdle() {
return (
!this.streamingUi &&
!this.stopRequested &&
!(this.monitorIsLocked && this.chatDisplayMode === 'monitor') &&
!this.compressionInProgress &&
!this.compressing
);
},
composerStreamingForInput() {
return this.composerBusy && !this.displayLockEngaged;
},

View File

@ -30,6 +30,36 @@ export const sendMethods = {
});
return;
}
// 主对话空闲但 composerBusy=truecomposerBusy 只因后台子智能体在跑而保持。
// 传统模式waitingForSubAgent=truetaskInProgress=true 。多智能体模式has_running_multi_agent=true。
// 此时新文本消息应直接发送,触发主智能体下一轮工作,而不是被进队列等任务结束。
const mainIdle = typeof this.mainChatIdle === 'function' ? this.mainChatIdle : (
!this.streamingUi && !this.stopRequested && !this.compressionInProgress && !this.compressing
);
if (this.composerBusy && mainIdle && hasText) {
// 如果有 pending 问题(子智能体询问主智能体),仍走问答路径,不走直接发送
if (Array.isArray(this.pendingUserQuestions) && this.pendingUserQuestions.length > 0) {
const answered = await this.answerUserQuestionFromComposer(this.inputMessage);
if (answered) {
this.inputClearMessage();
this.inputSetLineCount(1);
this.inputSetMultiline(false);
this.autoResizeInput();
}
return;
}
// 主对话空闲但后台任务在跑:直接发送新消息触发主智能体下一轮
this.sendMessage();
return;
}
if (this.composerBusy && mainIdle && hasMedia) {
this.uiPushToast({
title: '后台子智能体运行中',
message: '请等待后台任务结束后再发送图片/视频',
type: 'warning'
});
return;
}
if (this.composerBusy) {
if (hasText) {
if (Array.isArray(this.pendingUserQuestions) && this.pendingUserQuestions.length > 0) {
@ -395,7 +425,10 @@ export const sendMethods = {
return;
}
const canStop = this.composerBusy && !this.stopRequested;
// 停止按钮现在只停主智能体,与后台任务无关。
// canStop 判断只看主智能体是否在 streaming。多次点击被 stopRequested 抖动拦截。
const canStop = this.streamingUi && !this.stopRequested;
goalModeDebugLog('stopTask:entry', {
composerBusy: this.composerBusy,
stopRequested: this.stopRequested,
@ -432,6 +465,7 @@ export const sendMethods = {
// 等待后端确认轮询继续task_stopped 事件到达后会由 taskStore 自动停止轮询
await new Promise((resolve) => setTimeout(resolve, 300));
const shouldKeepBusy =
['running', 'pending', 'cancel_requested', 'canceled'].includes(String(taskStore.taskStatus));
@ -454,25 +488,10 @@ export const sendMethods = {
// 清理assistant消息的等待动画状态
const lastMessage = this.messages[this.messages.length - 1];
const before = {
hasLastMessage: !!lastMessage,
role: lastMessage?.role,
awaitingFirstContent: lastMessage?.awaitingFirstContent,
generatingLabel: lastMessage?.generatingLabel
};
if (lastMessage && lastMessage.role === 'assistant') {
lastMessage.awaitingFirstContent = false;
lastMessage.generatingLabel = '';
}
console.log('[DEBUG_AWAITING] stopTask 清理完成', {
before,
after: {
awaitingFirstContent: lastMessage?.awaitingFirstContent,
generatingLabel: lastMessage?.generatingLabel
}
});
} catch (error) {
console.error('[Message] 取消任务失败:', error);
const { useTaskStore } = await import('../../../stores/task');
@ -506,7 +525,7 @@ export const sendMethods = {
this.uiPushToast({
title: '停止请求已发送',
message: '若任务未停止,请再次点击停止按钮',
message: '若主对话未停止,请稍候;后台任务可通过状态栏单独停止',
type: 'info'
});
} finally {

View File

@ -486,22 +486,13 @@ export const lifecycleMethods = {
this.stopRequested = false;
if (hasRunningBackground) {
// 智能体已停,但后台任务还在跑:保持停止按钮,等用户第二下清理
// 此时对话并未真正停止work_timer 保持运行,不提前标记完成
// 主智能体已停,后台任务仍在跑:保持 taskInProgress=true (对话列表显示运行中),
// 但输入栏不再锁定stopRequested 已重置),用户可发新消息触发下一轮。
// 后台任务的停止由独立的子智能体/后台指令按钮处理,不再走停止按钮二次点击。
this.taskInProgress = true;
this.waitingForSubAgent = hasRunningSubAgents;
this.waitingForBackgroundCommand = hasRunningBackgroundCommands;
debugLog('[TaskPolling] 任务已停止,仍有后台任务运行,保持停止态');
goalModeDebugLog('handleTaskStopped:show-toast', { hasRunningSubAgents, hasRunningBackgroundCommands });
try {
this.uiPushToast({
title: '智能体已停止',
message: '再次按下停止按钮以结束后台任务',
type: 'info'
});
} catch (err) {
console.warn('[TaskPolling] uiPushToast 调用失败:', err);
}
debugLog('[TaskPolling] 任务已停止,仍有后台任务运行,保持对话运行态但释放输入区');
} else {
// 对话真正停止:停止计时器并持久化
this.markLatestUserWorkCompleted();

View File

@ -134,10 +134,12 @@ export const syncMethods = {
const pathFragment = this.stripConversationPrefix(data.conversation_id);
const currentPath = window.location.pathname.replace(/^\/+/, '');
const urlPrefix = this.multiAgentMode ? '/multiagent/' : '/';
const targetPath = `${urlPrefix}${pathFragment}`;
if (data.created) {
history.pushState({ conversationId: data.conversation_id }, '', `/${pathFragment}`);
history.pushState({ conversationId: data.conversation_id }, '', targetPath);
} else if (currentPath !== pathFragment) {
history.replaceState({ conversationId: data.conversation_id }, '', `/${pathFragment}`);
history.replaceState({ conversationId: data.conversation_id }, '', targetPath);
}
}
}

View File

@ -65,6 +65,19 @@ export const modeMethods = {
this.modeMenuOpen = false;
this.modelMenuOpen = false;
},
handleToggleAgentMode() {
if (!this.isConnected) {
return;
}
// 有运行中对话时不允许切换
if (this.composerBusy || this.anyWorkspaceHasRunningTask) {
return;
}
this.inputCloseMenus();
// 多智能体模式 → 传统模式;传统模式 → 多智能体模式
const target = this.multiAgentMode ? '/new' : '/multiagent/new';
window.location.href = target;
},
toggleModeMenu() {
if (!this.isConnected || this.streamingMessage) {
return;
@ -167,5 +180,55 @@ export const modeMethods = {
},
async toggleThinkingMode() {
await this.handleCycleRunMode();
},
async handleStopAllSubAgents() {
const isMultiAgent = !!this.multiAgentMode;
const mode = isMultiAgent ? 'soft_stop' : 'terminate';
const title = isMultiAgent ? '是否暂停所有子智能体?' : '是否终结所有子智能体?';
const message = isMultiAgent
? '所有正在运行的子智能体将停止工作并变为空闲状态。取消表示不执行操作。'
: '所有后台子智能体将被强制终止。取消表示不执行操作。';
const confirmText = isMultiAgent ? '暂停' : '终结';
let proceed = false;
try {
const uiStore = (await import('../../../stores/ui')).useUiStore();
proceed = await uiStore.requestConfirm({
title,
message,
confirmText,
cancelText: '取消',
confirmVariant: 'danger',
closeOnBackdrop: true
});
} catch (error) {
console.error('[stopAllSubAgents] 弹窗异常:', error);
return;
}
if (!proceed) return;
try {
const { useSubAgentStore } = await import('../../../stores/subAgent');
const store = useSubAgentStore();
const result = await store.stopAllAgents(mode);
if (!result.success) {
this.uiPushToast({
title: '停止子智能体失败',
message: result.error || '请重试',
type: 'error'
});
return;
}
this.uiPushToast({
title: isMultiAgent ? '子智能体已暂停' : '子智能体已终结',
message: `已处理 ${result.stoppedCount || 0} 个子智能体`,
type: 'info'
});
} catch (error: any) {
console.error('[stopAllSubAgents] 调用失败:', error);
this.uiPushToast({
title: '停止子智能体失败',
message: error?.message || String(error),
type: 'error'
});
}
}
};

View File

@ -36,6 +36,8 @@ export const routeMethods = {
this.multiAgentMode = true;
this.currentConversationId = null;
this.currentConversationTitle = '多智能体模式';
this.logMessageState('bootstrapRoute:clear-messages-for-multiagent-new');
this.messages = [];
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping('多智能体模式', { animate: false });
@ -47,38 +49,6 @@ export const routeMethods = {
} catch (_e) {
// 重建失败不阻断主流程
}
// 多智能体模式下自动创建一个带 metadata.multi_agent_mode=true 的新对话
try {
const resp = await fetch('/api/multiagent/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const result = await resp.json();
if (result && result.success && result.conversation_id) {
this.currentConversationId = result.conversation_id;
// 拉取完整会话信息
try {
const loadResp = await fetch(`/api/conversations/${result.conversation_id}/load`, { method: 'PUT' });
const loadResult = await loadResp.json();
if (loadResult.success) {
if (typeof loadResult.run_mode === 'string') {
this.runMode = loadResult.run_mode;
this.thinkingMode = typeof loadResult.thinking_mode === 'boolean' ? loadResult.thinking_mode : loadResult.run_mode !== 'fast';
}
if (typeof loadResult.model_key === 'string' && loadResult.model_key) this.modelSet(loadResult.model_key);
this.currentConversationTitle = loadResult.title || '多智能体模式';
this.startTitleTyping(this.currentConversationTitle, { animate: false });
history.replaceState({ conversationId: result.conversation_id }, '', `/multiagent/${result.conversation_id.replace(/^conv_/, '')}`);
this.logMessageState('bootstrapRoute:multi-agent-loaded');
}
} catch (_e) {
// 加载失败也继续,主对话 已创建
}
}
} catch (error) {
console.warn('[multiagent] 初始化多智能体对话失败:', error);
}
await this.restoreComposerDraftState('bootstrap-route:multiagent');
return;
}
@ -214,7 +184,7 @@ export const routeMethods = {
},
isExplicitNewConversationRoute() {
const normalizedPath = window.location.pathname.replace(/^\/+|\/+$/g, '');
return normalizedPath === 'new';
return normalizedPath === 'new' || normalizedPath === 'multiagent/new' || normalizedPath === 'multiagent';
},
stripConversationPrefix(conversationId) {
if (!conversationId) return '';

View File

@ -321,10 +321,13 @@ export const socketMethods = {
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
const resumeConversationId = statusConversationId || runningConversationId;
const shouldResumeOnNewRoute = isExplicitNewRoute && !!runningConversationId;
const currentPath = window.location.pathname.replace(/^\/+/, '');
const isMultiAgentNewRoute = currentPath === 'multiagent/new' || currentPath === 'multiagent';
if (
resumeConversationId &&
!this.currentConversationId &&
!isMultiAgentNewRoute &&
(!isExplicitNewRoute || shouldResumeOnNewRoute)
) {
this.skipConversationHistoryReload = true;

View File

@ -38,14 +38,6 @@
宿主机模式免登录
</button>
<button
class="auth-secondary-button auth-tertiary-button"
@click="enterMultiAgent"
title="多智能体协作 beta"
>
多智能体模式beta
</button>
<div class="auth-error">{{ error }}</div>
<div class="auth-link">还没有账号<a href="/register">点击注册</a></div>
</section>
@ -133,18 +125,6 @@ const doHostLogin = async (redirectUrl = '/') => {
const hostLogin = () => doHostLogin('/');
const enterMultiAgent = () => {
if (hostModeEnabled.value) {
doHostLogin('/multiagent/new');
return;
}
if (!email.value || !password.value) {
error.value = '请输入邮箱和密码,或选择宿主机模式';
return;
}
doLogin('/multiagent/new');
};
onMounted(async () => {
applyTheme(loadTheme());

View File

@ -939,15 +939,13 @@ const getToolIntent = (action: Action) => {
};
const renderToolResult = (action: Action) => {
if (personalizationStore.form.enhanced_tool_display) {
const rendered = renderEnhancedToolResult(
action,
props.formatSearchTopic || (() => ''),
props.formatSearchTime || (() => ''),
props.formatSearchDomains || (() => '')
);
if (rendered) return rendered;
}
const rendered = renderEnhancedToolResult(
action,
props.formatSearchTopic || (() => ''),
props.formatSearchTime || (() => ''),
props.formatSearchDomains || (() => '')
);
if (rendered) return rendered;
const result = action.tool?.result;
if (!result) return '<div class="result-item">执行中...</div>';

View File

@ -1,7 +1,11 @@
<template>
<div
class="stacked-shell"
:class="{ 'stacked-shell--single': isSingle, 'stacked-shell--not-ready': !ready, 'stacked-shell--no-borders': hideBorders }"
:class="{
'stacked-shell--single': isSingle,
'stacked-shell--not-ready': !ready,
'stacked-shell--no-borders': hideBorders
}"
ref="shell"
:style="{
height: `${shellHeight}px`,
@ -89,59 +93,7 @@
</div>
<div class="collapsible-content" :style="contentStyle(blockKey(action, idx))">
<div class="content-inner">
<div
v-if="shouldUseEnhancedDisplay && renderToolResult(action)"
v-html="renderToolResult(action)"
></div>
<div
v-else-if="
!shouldUseEnhancedDisplay &&
action.tool?.name === 'web_search' &&
action.tool?.result
"
>
<div class="search-meta">
<div>
<strong>搜索内容</strong
>{{ action.tool.result.query || action.tool.arguments?.query }}
</div>
<div>
<strong>主题</strong
>{{ formatSearchTopic(action.tool.result.filters || {}) }}
</div>
<div>
<strong>时间范围</strong
>{{ formatSearchTime(action.tool.result.filters || {}) }}
</div>
<div>
<strong>限定网站</strong
>{{ formatSearchDomains(action.tool.result.filters || {}) }}
</div>
<div><strong>结果数量</strong>{{ action.tool.result.total_results }}</div>
</div>
<div
v-if="action.tool.result.results && action.tool.result.results.length"
class="search-result-list"
>
<div
v-for="item in action.tool.result.results"
:key="item.url || item.index"
class="search-result-item"
>
<div class="search-result-title">{{ item.title || '无标题' }}</div>
<div class="search-result-url">
<a v-if="item.url" :href="item.url" target="_blank">{{ item.url }}</a
><span v-else>无可用链接</span>
</div>
</div>
</div>
<div v-else class="search-empty">未返回详细的搜索结果</div>
</div>
<div v-else>
<pre>{{
JSON.stringify(action.tool?.result || action.tool?.arguments, null, 2)
}}</pre>
</div>
<div v-html="renderToolResult(action)"></div>
</div>
</div>
<div v-if="isToolProcessing(action)" class="progress-indicator"></div>
@ -190,10 +142,6 @@ const stackKey = `stacked-${++stackInstanceCounter}`;
// personalization store
const personalizationStore = usePersonalizationStore();
const shouldUseEnhancedDisplay = computed(() => {
return personalizationStore.form.enhanced_tool_display;
});
const hideBorders = computed(() => personalizationStore.form.stacked_hide_borders);
const renderToolResult = (action: any) => {
@ -338,7 +286,7 @@ const measureAndCompute = () => {
nextContentHeights[key] = fullContent;
const expanded = isExpandedById(key);
// 线线 0 border-bottom
const borderH = hideBorders.value ? 0 : (idx === children.length - 1 ? 0 : 1);
const borderH = hideBorders.value ? 0 : idx === children.length - 1 ? 0 : 1;
heights.push(headerH + (expanded ? fullContent : 0) + borderH);
});

View File

@ -30,10 +30,7 @@
"
>
<div class="content-inner">
<div v-if="shouldUseEnhancedDisplay" v-html="renderEnhancedToolResult()"></div>
<div v-else>
<pre>{{ JSON.stringify(action.tool.result || action.tool.arguments, null, 2) }}</pre>
</div>
<div v-html="renderToolResult()"></div>
</div>
</div>
<div
@ -44,9 +41,6 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { usePersonalizationStore } from '@/stores/personalization';
defineOptions({ name: 'ToolAction' });
const props = defineProps<{
@ -68,13 +62,6 @@ const props = defineProps<{
defineEmits<{ (event: 'toggle'): void }>();
// store
const personalizationStore = usePersonalizationStore();
const shouldUseEnhancedDisplay = computed(() => {
return personalizationStore.form.enhanced_tool_display && props.action.tool.result;
});
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
@ -122,7 +109,7 @@ function formatToolStatusLabel(result: any, successLabel: string, failedLabel =
return result?.success ? successLabel : failedLabel;
}
function renderEnhancedToolResult(): string {
function renderToolResult(): string {
const tool = props.action.tool;
const result = tool.result;
const args = tool.arguments || {};
@ -154,7 +141,7 @@ function renderEnhancedToolResult(): string {
else if (name === 'read_file' || name === 'read_skill') {
return renderReadFile(result, args);
} else if (name === 'create_skill') {
return renderCreateSkill(result, args);
return renderCreateSkill(result);
} else if (name === 'vlm_analyze') {
return renderVlmAnalyze(result, args);
} else if (name === 'ocr_image') {
@ -215,12 +202,86 @@ function renderEnhancedToolResult(): string {
return renderCreateSubAgent(result, args);
} else if (name === 'terminate_sub_agent') {
return renderTerminateSubAgent(result, args);
} else if (name === 'send_message_to_sub_agent') {
return renderSendMessageToSubAgent(result, args);
} else if (name === 'ask_sub_agent') {
return renderAskSubAgent(result, args);
} else if (name === 'answer_sub_agent_question') {
return renderAnswerSubAgentQuestion(result, args);
} else if (name === 'create_custom_agent') {
return renderCreateCustomAgent(result, args);
} else if (name === 'list_agents') {
return renderListAgents(result);
} else if (name === 'list_active_sub_agents') {
return renderListActiveSubAgents(result);
} else if (name === 'get_sub_agent_status') {
return renderGetSubAgentStatus(result, args);
return renderGetSubAgentStatus(result);
}
// JSON
return `<pre>${escapeHtml(JSON.stringify(result || args, null, 2))}</pre>`;
// JSON +
return renderDefaultResult(result, args, name);
}
function renderDefaultResult(result: any, args: any, name: string): string {
const status = formatToolStatusLabel(result, '✓ 完成', '✗ 失败');
let html = '<div class="tool-result-meta">';
html += `<div><strong>工具:</strong>${escapeHtml(name)}</div>`;
html += `<div><strong>状态:</strong>${escapeHtml(status)}</div>`;
// ID
const importantKeys = [
'path',
'file_path',
'agent_id',
'target_agent_id',
'question',
'role_id',
'url'
];
importantKeys.forEach((key) => {
const value = args[key] ?? result?.[key];
if (value !== undefined && value !== '' && value !== null) {
const labelMap: Record<string, string> = {
path: '路径',
file_path: '路径',
agent_id: '子智能体 ID',
target_agent_id: '目标子智能体 ID',
question: '问题',
role_id: '角色 ID',
url: 'URL'
};
const displayValue = typeof value === 'string' ? value : JSON.stringify(value);
const preview = String(displayValue).slice(0, 200);
const suffix = String(displayValue).length > 200 ? '…' : '';
html += `<div><strong>${escapeHtml(labelMap[key] || key)}</strong>${escapeHtml(preview + suffix)}</div>`;
}
});
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
// message/summary/answer/output/content/data
const readable =
result?.message ??
result?.summary ??
result?.answer ??
result?.output ??
result?.content ??
result?.data ??
'';
if (readable && typeof readable === 'string' && readable.trim()) {
html += '<div class="tool-result-content scrollable">';
html += `<pre>${escapeHtml(readable.trim())}</pre>`;
html += '</div>';
} else if (result?.success) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="tool-result-empty">工具执行完成,无额外可展示内容。</div>';
html += '</div>';
}
return html;
}
// ===== =====
@ -918,7 +979,7 @@ function renderConversationReview(result: any, args: any): string {
return html;
}
function renderCreateSkill(result: any, args: any): string {
function renderCreateSkill(result: any): string {
const status = formatToolStatusLabel(result, '✓ 已归档');
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
@ -1242,7 +1303,7 @@ function renderTerminateSubAgent(result: any, args: any): string {
return html;
}
function renderGetSubAgentStatus(result: any, args: any): string {
function renderGetSubAgentStatus(result: any): string {
if (!result.success) {
const error = result.error ?? '查询失败';
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
@ -1299,6 +1360,185 @@ function renderGetSubAgentStatus(result: any, args: any): string {
return html;
}
function renderSendMessageToSubAgent(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已发送', '✗ 发送失败');
const agentId = args.agent_id ?? result.agent_id ?? '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
if (agentId !== '') {
html += `<div><strong>子智能体 ID</strong>${escapeHtml(String(agentId))}</div>`;
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
return html;
}
function renderAskSubAgent(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已回答', '✗ 获取失败');
const agentId = args.agent_id ?? '';
const question = args.question ?? '';
const answer =
result?.answer ?? result?.content ?? result?.message ?? result?.output ?? result?.result ?? '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
if (agentId !== '') {
html += `<div><strong>子智能体 ID</strong>${escapeHtml(String(agentId))}</div>`;
}
if (question) {
const preview = String(question).slice(0, 200);
const suffix = String(question).length > 200 ? '…' : '';
html += `<div><strong>问题:</strong>${escapeHtml(preview + suffix)}</div>`;
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
if (answer && typeof answer === 'string') {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">回答</div>';
html += `<pre>${escapeHtml(answer)}</pre>`;
html += '</div>';
} else if (result?.success) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="tool-result-empty">子智能体未返回答复内容。</div>';
html += '</div>';
}
return html;
}
function renderAnswerSubAgentQuestion(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已回复', '✗ 回复失败');
const questionId = args.question_id ?? result.question_id ?? '';
const answer = args.answer ?? '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
if (questionId) {
html += `<div><strong>问题 ID</strong>${escapeHtml(String(questionId))}</div>`;
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
if (answer && typeof answer === 'string') {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">回复内容</div>';
const preview = String(answer).slice(0, 500);
const suffix = String(answer).length > 500 ? '…' : '';
html += `<pre>${escapeHtml(preview + suffix)}</pre>`;
html += '</div>';
}
return html;
}
function renderCreateCustomAgent(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已创建', '✗ 创建失败');
const roleId = args.role_id ?? result.role_id ?? '';
const name = args.name ?? result.name ?? roleId;
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
if (roleId !== '') {
html += `<div><strong>角色 ID</strong>${escapeHtml(String(roleId))}</div>`;
}
if (name && name !== roleId) {
html += `<div><strong>角色名:</strong>${escapeHtml(String(name))}</div>`;
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
return html;
}
function renderListAgents(result: any): string {
if (!result?.success) {
const error = result?.error ?? '查询失败';
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
}
const roles = Array.isArray(result.roles) ? result.roles : [];
if (roles.length === 0) {
return '<div class="tool-result-empty">当前没有可用角色。</div>';
}
let html = '<div class="tool-result-meta">';
html += `<div><strong>角色数量:</strong>${roles.length}</div>`;
html += '</div>';
html += '<div class="search-result-list">';
roles.forEach((role: any) => {
const roleId = role.role_id || '未知';
const name = role.name || roleId;
const description = role.description || '';
const thinkingMode = role.thinking_mode || 'fast';
const isCustom = role.is_custom ? '是' : '否';
html += '<div class="search-result-item">';
html += `<div class="search-result-title">${escapeHtml(roleId)}${escapeHtml(name)}</div>`;
if (description) {
html += `<div>${escapeHtml(description)}</div>`;
}
html += `<div><strong>思考模式:</strong>${escapeHtml(thinkingMode)} | <strong>自定义:</strong>${escapeHtml(isCustom)}</div>`;
html += '</div>';
});
html += '</div>';
return html;
}
function renderListActiveSubAgents(result: any): string {
if (!result?.success) {
const error = result?.error ?? '查询失败';
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
}
const agents = Array.isArray(result.agents) ? result.agents : [];
if (agents.length === 0) {
return '<div class="tool-result-empty">当前会话没有活跃子智能体。</div>';
}
let html = '<div class="tool-result-meta">';
html += `<div><strong>活跃数量:</strong>${agents.length}</div>`;
html += '</div>';
html += '<div class="sub-agent-status-list">';
agents.forEach((agent: any) => {
const agentId = agent.agent_id ?? '?';
const displayName = agent.display_name || `Agent_${agentId}`;
const status = agent.status || 'unknown';
const summary = agent.summary || '';
const lastOutput = agent.last_output || '';
html += '<div class="sub-agent-status-item">';
html += `<div class="sub-agent-status-header">#${escapeHtml(String(agentId))} ${escapeHtml(displayName)} [${escapeHtml(status)}]</div>`;
html += '<div class="tool-result-meta">';
if (summary) {
html += `<div><strong>任务:</strong>${escapeHtml(String(summary))}</div>`;
}
if (lastOutput) {
const preview = String(lastOutput).slice(0, 120);
const suffix = String(lastOutput).length > 120 ? '…' : '';
html += `<div><strong>最近输出:</strong>${escapeHtml(preview + suffix)}</div>`;
}
html += '</div>';
html += '</div>';
});
html += '</div>';
return html;
}
</script>
<style>

View File

@ -62,6 +62,11 @@ export function renderEnhancedToolResult(
return '';
}
// 如果 result 已经被格式化为文本字符串,直接显示
if (typeof result === 'string') {
return `<div class="tool-result-content scrollable"><pre>${escapeHtml(result)}</pre></div>`;
}
// 网络检索类
if (name === 'web_search') {
return renderWebSearch(result, args, formatSearchTopic, formatSearchTime, formatSearchDomains);
@ -88,7 +93,7 @@ export function renderEnhancedToolResult(
else if (name === 'read_file' || name === 'read_skill') {
return renderReadFile(result, args);
} else if (name === 'create_skill') {
return renderCreateSkill(result, args);
return renderCreateSkill(result);
} else if (name === 'vlm_analyze') {
return renderVlmAnalyze(result, args);
} else if (name === 'ocr_image') {
@ -152,11 +157,84 @@ export function renderEnhancedToolResult(
return renderCreateSubAgent(result, args);
} else if (name === 'terminate_sub_agent') {
return renderTerminateSubAgent(result, args);
} else if (name === 'send_message_to_sub_agent') {
return renderSendMessageToSubAgent(result, args);
} else if (name === 'ask_sub_agent') {
return renderAskSubAgent(result, args);
} else if (name === 'answer_sub_agent_question') {
return renderAnswerSubAgentQuestion(result, args);
} else if (name === 'create_custom_agent') {
return renderCreateCustomAgent(result, args);
} else if (name === 'list_agents') {
return renderListAgents(result);
} else if (name === 'list_active_sub_agents') {
return renderListActiveSubAgents(result);
} else if (name === 'get_sub_agent_status') {
return renderGetSubAgentStatus(result, args);
return renderGetSubAgentStatus(result);
}
return '';
// 默认回退:显示重要参数 + 结果摘要,不再直接返回空字符串
return renderDefaultResult(result, args, name);
}
function renderDefaultResult(result: any, args: any, name: string): string {
const status = formatToolStatusLabel(result, '✓ 完成', '✗ 失败');
let html = '<div class="tool-result-meta">';
html += `<div><strong>工具:</strong>${escapeHtml(name)}</div>`;
html += `<div><strong>状态:</strong>${escapeHtml(status)}</div>`;
const importantKeys = [
'path',
'file_path',
'agent_id',
'target_agent_id',
'question',
'role_id',
'url'
];
importantKeys.forEach((key) => {
const value = args?.[key] ?? result?.[key];
if (value !== undefined && value !== '' && value !== null) {
const labelMap: Record<string, string> = {
path: '路径',
file_path: '路径',
agent_id: '子智能体 ID',
target_agent_id: '目标子智能体 ID',
question: '问题',
role_id: '角色 ID',
url: 'URL'
};
const displayValue = typeof value === 'string' ? value : JSON.stringify(value);
const preview = String(displayValue).slice(0, 200);
const suffix = String(displayValue).length > 200 ? '…' : '';
html += `<div><strong>${escapeHtml(labelMap[key] || key)}</strong>${escapeHtml(preview + suffix)}</div>`;
}
});
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
const readable =
result?.message ??
result?.summary ??
result?.answer ??
result?.output ??
result?.content ??
result?.data ??
'';
if (readable && typeof readable === 'string' && readable.trim()) {
html += '<div class="tool-result-content scrollable">';
html += `<pre>${escapeHtml(readable.trim())}</pre>`;
html += '</div>';
} else if (result?.success) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="tool-result-empty">工具执行完成,无额外可展示内容。</div>';
html += '</div>';
}
return html;
}
// 网络检索类渲染函数
@ -904,7 +982,7 @@ function renderConversationReview(result: any, args: any): string {
return html;
}
function renderCreateSkill(result: any, args: any): string {
function renderCreateSkill(result: any): string {
const status = formatToolStatusLabel(result, '✓ 已归档');
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
@ -1218,7 +1296,7 @@ function renderTerminateSubAgent(result: any, args: any): string {
return html;
}
function renderGetSubAgentStatus(result: any, args: any): string {
function renderGetSubAgentStatus(result: any): string {
if (!result.success) {
const error = result.error ?? '查询失败';
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
@ -1275,3 +1353,182 @@ function renderGetSubAgentStatus(result: any, args: any): string {
return html;
}
function renderSendMessageToSubAgent(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已发送', '✗ 发送失败');
const agentId = args.agent_id ?? result.agent_id ?? '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
if (agentId !== '') {
html += `<div><strong>子智能体 ID</strong>${escapeHtml(String(agentId))}</div>`;
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
return html;
}
function renderAskSubAgent(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已回答', '✗ 获取失败');
const agentId = args.agent_id ?? '';
const question = args.question ?? '';
const answer =
result?.answer ?? result?.content ?? result?.message ?? result?.output ?? result?.result ?? '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
if (agentId !== '') {
html += `<div><strong>子智能体 ID</strong>${escapeHtml(String(agentId))}</div>`;
}
if (question) {
const preview = String(question).slice(0, 200);
const suffix = String(question).length > 200 ? '…' : '';
html += `<div><strong>问题:</strong>${escapeHtml(preview + suffix)}</div>`;
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
if (answer && typeof answer === 'string') {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">回答</div>';
html += `<pre>${escapeHtml(answer)}</pre>`;
html += '</div>';
} else if (result?.success) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="tool-result-empty">子智能体未返回答复内容。</div>';
html += '</div>';
}
return html;
}
function renderAnswerSubAgentQuestion(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已回复', '✗ 回复失败');
const questionId = args.question_id ?? result.question_id ?? '';
const answer = args.answer ?? '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
if (questionId) {
html += `<div><strong>问题 ID</strong>${escapeHtml(String(questionId))}</div>`;
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
if (answer && typeof answer === 'string') {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">回复内容</div>';
const preview = String(answer).slice(0, 500);
const suffix = String(answer).length > 500 ? '…' : '';
html += `<pre>${escapeHtml(preview + suffix)}</pre>`;
html += '</div>';
}
return html;
}
function renderCreateCustomAgent(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已创建', '✗ 创建失败');
const roleId = args.role_id ?? result.role_id ?? '';
const name = args.name ?? result.name ?? roleId;
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
if (roleId !== '') {
html += `<div><strong>角色 ID</strong>${escapeHtml(String(roleId))}</div>`;
}
if (name && name !== roleId) {
html += `<div><strong>角色名:</strong>${escapeHtml(String(name))}</div>`;
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
return html;
}
function renderListAgents(result: any): string {
if (!result?.success) {
const error = result?.error ?? '查询失败';
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
}
const roles = Array.isArray(result.roles) ? result.roles : [];
if (roles.length === 0) {
return '<div class="tool-result-empty">当前没有可用角色。</div>';
}
let html = '<div class="tool-result-meta">';
html += `<div><strong>角色数量:</strong>${roles.length}</div>`;
html += '</div>';
html += '<div class="search-result-list">';
roles.forEach((role: any) => {
const roleId = role.role_id || '未知';
const name = role.name || roleId;
const description = role.description || '';
const thinkingMode = role.thinking_mode || 'fast';
const isCustom = role.is_custom ? '是' : '否';
html += '<div class="search-result-item">';
html += `<div class="search-result-title">${escapeHtml(roleId)}${escapeHtml(name)}</div>`;
if (description) {
html += `<div>${escapeHtml(description)}</div>`;
}
html += `<div><strong>思考模式:</strong>${escapeHtml(thinkingMode)} | <strong>自定义:</strong>${escapeHtml(isCustom)}</div>`;
html += '</div>';
});
html += '</div>';
return html;
}
function renderListActiveSubAgents(result: any): string {
if (!result?.success) {
const error = result?.error ?? '查询失败';
return `<div class="tool-result-error">⚠️ ${escapeHtml(String(error))}</div>`;
}
const agents = Array.isArray(result.agents) ? result.agents : [];
if (agents.length === 0) {
return '<div class="tool-result-empty">当前会话没有活跃子智能体。</div>';
}
let html = '<div class="tool-result-meta">';
html += `<div><strong>活跃数量:</strong>${agents.length}</div>`;
html += '</div>';
html += '<div class="sub-agent-status-list">';
agents.forEach((agent: any) => {
const agentId = agent.agent_id ?? '?';
const displayName = agent.display_name || `Agent_${agentId}`;
const status = agent.status || 'unknown';
const summary = agent.summary || '';
const lastOutput = agent.last_output || '';
html += '<div class="sub-agent-status-item">';
html += `<div class="sub-agent-status-header">#${escapeHtml(String(agentId))} ${escapeHtml(displayName)} [${escapeHtml(status)}]</div>`;
html += '<div class="tool-result-meta">';
if (summary) {
html += `<div><strong>任务:</strong>${escapeHtml(String(summary))}</div>`;
}
if (lastOutput) {
const preview = String(lastOutput).slice(0, 120);
const suffix = String(lastOutput).length > 120 ? '…' : '';
html += `<div><strong>最近输出:</strong>${escapeHtml(preview + suffix)}</div>`;
}
html += '</div>';
html += '</div>';
});
html += '</div>';
return html;
}

View File

@ -177,6 +177,14 @@
>
等待回答问题
</button>
<button
v-if="(activeSubAgentCount || 0) > 0"
type="button"
class="floating-project-status__notice floating-project-status__notice--button"
@click.stop="$emit('stop-all-sub-agents')"
>
{{ activeSubAgentCount }} 个子智能体运行中
</button>
<button
v-if="terminalCount > 0"
type="button"
@ -353,6 +361,8 @@
:execution-mode-enabled="executionModeEnabled"
:goal-mode-armed="goalModeArmed"
:goal-running="goalRunning"
:multi-agent-mode="multiAgentMode"
:has-running-task="hasRunningTask"
@quick-upload="triggerQuickUpload"
@pick-images="$emit('pick-images')"
@pick-video="$emit('pick-video')"
@ -371,6 +381,7 @@
@open-review="$emit('open-review')"
@open-path-authorization="$emit('open-path-authorization')"
@toggle-goal-mode="$emit('toggle-goal-mode')"
@toggle-agent-mode="$emit('toggle-agent-mode')"
/>
<div class="permission-switcher" @click.stop>
<button
@ -545,7 +556,9 @@ const emit = defineEmits([
'restore-user-question',
'toggle-goal-mode',
'open-goal-dialog',
'open-git-changes-panel'
'open-git-changes-panel',
'toggle-agent-mode',
'stop-all-sub-agents'
]);
const props = defineProps<{
@ -614,6 +627,15 @@ const props = defineProps<{
goalModeArmed?: boolean;
goalRunning?: boolean;
goalProgress?: Record<string, any> | null;
multiAgentMode?: boolean;
hasRunningTask?: boolean;
/**
* 主对话是否空闲composerBusy 可能是 true但只是因为后台子智能体在跑
* 多智能体模式下若主对话空闲输入栏应处于正常发送状态不是停止状态
*/
mainChatIdle?: boolean;
/** 非终态子智能体数量用于状态栏“x 个子智能体运行中”按钮显示 */
activeSubAgentCount?: number;
hasPendingRuntimeGuidance?: boolean;
terminalCount?: number;
hostMode?: boolean;
@ -830,7 +852,8 @@ const floatingStatusVisible = computed(() => {
!!projectGitSummaryForRender.value ||
!!props.goalRunning ||
!!props.goalModeArmed ||
(!!props.userQuestionMinimized && Number(props.pendingUserQuestionCount || 0) > 0)
(!!props.userQuestionMinimized && Number(props.pendingUserQuestionCount || 0) > 0) ||
(Number(props.activeSubAgentCount || 0) > 0)
);
});
@ -2392,15 +2415,21 @@ const hasComposerContent = computed(() => {
return hasText || hasImages || hasVideos;
});
const showStopIcon = computed(() => props.streamingMessage && !hasComposerContent.value);
const showStopIcon = computed(() => {
// streaming/
// x LeftPanel
return !!props.streamingMessage && !hasComposerContent.value;
});
const sendButtonDisabled = computed(() => {
if (!props.isConnected) {
return true;
}
// streaming stopTask
if (props.streamingMessage) {
return false;
}
//
if (props.inputLocked) {
return true;
}

View File

@ -63,6 +63,20 @@
<template v-else>{{ goalModeArmed ? '已就绪' : '' }}</template>
</span>
</button>
<button
type="button"
class="menu-entry"
:class="{ 'mode-entry-active': multiAgentMode }"
@click.stop="$emit('toggle-agent-mode')"
:disabled="!isConnected || hasRunningTask"
:title="hasRunningTask ? '有运行中的对话,无法切换模式' : ''"
>
{{ multiAgentMode ? '返回传统模式' : '多智能体模式' }}
<span class="entry-arrow">
<template v-if="hasRunningTask">有任务运行中</template>
<template v-else-if="multiAgentMode">当前模式</template>
</span>
</button>
<button
type="button"
class="menu-entry has-submenu"
@ -200,6 +214,8 @@ const props = defineProps<{
goalModeArmed?: boolean;
goalRunning?: boolean;
goalProgress?: Record<string, any> | null;
multiAgentMode?: boolean;
hasRunningTask?: boolean;
}>();
defineEmits<{
@ -219,6 +235,7 @@ defineEmits<{
(event: 'pick-video'): void;
(event: 'open-path-authorization'): void;
(event: 'toggle-goal-mode'): void;
(event: 'toggle-agent-mode'): void;
}>();
const getIconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {});
@ -250,4 +267,11 @@ const goalCompleted = computed(() => String(props.goalProgress?.status || '').to
color: var(--claude-accent);
font-size: 12px;
}
.menu-entry.mode-entry-active {
color: var(--claude-accent);
}
.menu-entry.mode-entry-active .entry-arrow {
color: var(--claude-accent);
font-size: 12px;
}
</style>

View File

@ -96,6 +96,16 @@
aria-hidden="true"
></span>
</button>
<button
v-if="panelMode === 'backgroundCommands' && runningBackgroundCommandCount > 0"
type="button"
class="background-stop-btn"
title="终止所有后台指令"
@click.stop="handleStopAllCommands"
>
<span class="icon icon-sm" :style="iconStyle('stop')" aria-hidden="true"></span>
<span class="background-stop-label">停止</span>
</button>
</div>
</transition>
</div>
@ -160,10 +170,13 @@
@click="openSubAgent(agent)"
>
<div class="sub-agent-header">
<span class="sub-agent-id">#{{ agent.agent_id }}</span>
<span class="sub-agent-summary-text">{{ agent.summary || agent.display_name || `#${agent.agent_id}` }}</span>
<span class="sub-agent-status" :class="agent.status">{{ agent.status }}</span>
</div>
<div class="sub-agent-summary">{{ agent.summary }}</div>
<div class="sub-agent-meta">
<span class="sub-agent-name" v-if="agent.display_name">{{ agent.display_name }}</span>
<span class="sub-agent-tokens" v-if="agent.current_context_tokens">{{ formatTokens(agent.current_context_tokens) }}</span>
</div>
<div class="sub-agent-tool" v-if="agent.last_tool">当前{{ agent.last_tool }}</div>
</div>
</div>
@ -412,6 +425,20 @@ const openBackgroundCommand = (command: any) => {
backgroundCommandStore.openCommand(command);
};
const BG_COMMAND_TERMINAL = new Set(['completed', 'failed', 'timeout', 'cancelled']);
const runningBackgroundCommandCount = computed(() => {
return (backgroundCommands.value || []).filter((c: any) => {
const s = String(c?.status || '').toLowerCase();
return !BG_COMMAND_TERMINAL.has(s);
}).length;
});
const handleStopAllCommands = async () => {
const result = await backgroundCommandStore.stopAllCommands();
if (!result.success) {
console.error('[LeftPanel] 批量停止后台指令失败:', result.error);
}
};
const formatBackgroundCommandMeta = (command: any) => {
const code = command?.return_code;
if (code === null || code === undefined) {
@ -433,6 +460,14 @@ const formatBackgroundCommandPreview = (rawCommand: any) => {
return content;
};
const formatTokens = (tokens: number) => {
if (!tokens || tokens <= 0) return '';
if (tokens >= 1000) {
return `${(tokens / 1000).toFixed(1)}k ctx`;
}
return `${tokens} ctx`;
};
defineExpose({
panelMenuWrapper
});

View File

@ -1736,7 +1736,252 @@
</section>
<section
v-else-if="activeTab === 'admin'"
v-else-if="activeTab === 'sub-agents'"
key="sub-agents"
class="settings-page"
>
<div class="settings-section-desc" style="margin: 0 0 16px; color: var(--claude-text-secondary); font-size: 13px; line-height: 1.6">
管理多智能体模式下的子智能体角色预设角色可编辑创建自定义覆盖自定义角色可创建/编辑/删除
</div>
<!-- 压缩阈值配置 -->
<div class="settings-action-row" style="margin-bottom: 16px">
<span class="settings-row-copy">
<span class="settings-row-title">上下文压缩阈值</span>
<span class="settings-row-desc">
子智能体上下文 tokens 超过此值时触发深度压缩默认 150000
</span>
</span>
<div style="display: flex; gap: 6px; align-items: center">
<input
type="number"
class="settings-number-input"
v-model.number="subAgentCompressThreshold"
:min="10000"
:step="10000"
style="width: 120px"
/>
<button
type="button"
class="settings-secondary-button"
:disabled="subAgentSettingsSaving"
@click="saveSubAgentSettings"
>
{{ subAgentSettingsSaving ? '保存中...' : '保存' }}
</button>
</div>
</div>
<!-- 角色列表 -->
<div class="settings-section-header" style="margin-bottom: 8px">
<span class="settings-section-title">角色列表</span>
<button
type="button"
class="settings-secondary-button"
@click="openRoleEditor()"
>
+ 新建角色
</button>
</div>
<div v-if="subAgentRolesLoading" class="settings-empty-hint">加载中...</div>
<div v-else-if="subAgentRoles.length === 0" class="settings-empty-hint">
暂无角色
</div>
<div v-else class="sub-agent-role-list">
<div
v-for="role in subAgentRoles"
:key="role.role_id"
class="sub-agent-role-card"
>
<div class="sub-agent-role-info">
<div class="sub-agent-role-name">
{{ role.name }}
<span
class="sub-agent-role-tag"
:class="role.is_custom ? 'tag-custom' : 'tag-preset'"
>
{{ role.is_custom ? '自定义' : '预设' }}
</span>
</div>
<div class="sub-agent-role-desc">{{ role.description || '无描述' }}</div>
<div class="sub-agent-role-meta">
ID: {{ role.role_id }} · 思考模式: {{ role.thinking_mode }}<template v-if="role.model_key"> · 模型: {{ role.model_key }}</template>
</div>
</div>
<div class="sub-agent-role-actions">
<button
type="button"
class="settings-secondary-button"
@click="openRoleEditor(role)"
>编辑</button>
<button
v-if="role.is_custom"
type="button"
class="settings-secondary-button danger"
@click="deleteRole(role)"
>删除</button>
</div>
</div>
</div>
<!-- 角色编辑弹窗与个人空间同尺寸 -->
<transition name="personal-page-fade" appear>
<div
v-if="roleEditorOpen"
class="role-editor-drawer-overlay"
@click="closeDropdown"
@click.self="closeRoleEditor"
>
<div class="role-editor-drawer-card settings-redesign-card" style="padding: 30px 42px 24px; border-radius: 32px;" @click.stop>
<div class="role-editor-header">
<h2>{{ editingRole ? '编辑角色' : '新建角色' }}</h2>
<button type="button" class="settings-secondary-button" @click="closeRoleEditor">关闭</button>
</div>
<div class="role-editor-body" @click="closeDropdown">
<label class="settings-input-row">
<span class="settings-row-title">角色 ID</span>
<input
v-if="!editingRole"
type="text"
:value="roleForm.role_id"
maxlength="40"
placeholder="英文小写下划线,如 api-designer"
@input="roleForm.role_id = ($event.target as HTMLInputElement).value"
/>
<span v-else class="settings-row-desc" style="text-align: right; font-size: 14px;">{{ roleForm.role_id }}</span>
</label>
<label class="settings-input-row">
<span class="settings-row-title">显示名</span>
<input
type="text"
:value="roleForm.name"
maxlength="40"
placeholder="如 API Designer"
@input="roleForm.name = ($event.target as HTMLInputElement).value"
/>
</label>
<label class="settings-input-row">
<span class="settings-row-title">描述</span>
<input
type="text"
:value="roleForm.description"
maxlength="100"
placeholder="一句话描述职责"
@input="roleForm.description = ($event.target as HTMLInputElement).value"
/>
</label>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">思考模式</span>
<span class="settings-row-desc">fast = 快速响应thinking = 深度推理</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'role-thinking' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('role-thinking')"
>
{{ roleForm.thinking_mode === 'thinking' ? 'thinking' : 'fast' }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === 'role-thinking' ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: roleForm.thinking_mode === 'fast' }"
@click="roleForm.thinking_mode = 'fast'; closeDropdown()"
>
<strong>fast</strong><span>快速响应模式</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: roleForm.thinking_mode === 'thinking' }"
@click="roleForm.thinking_mode = 'thinking'; closeDropdown()"
>
<strong>thinking</strong><span>深度推理模式</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">模型</span>
<span class="settings-row-desc">留空则使用默认模型</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'role-model' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('role-model')"
>
{{ roleForm.model_key || '默认模型' }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === 'role-model' ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: !roleForm.model_key }"
@click="roleForm.model_key = ''; closeDropdown()"
>
<strong>默认模型</strong><span>使用配置文件中的 default_model</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
v-for="m in subAgentModels"
:key="m.name"
type="button"
class="settings-menu-option"
:class="{ selected: roleForm.model_key === m.name }"
@click="roleForm.model_key = m.name; closeDropdown()"
>
<strong>{{ m.name }}</strong><span>{{ m.modes }} · {{ m.multimodal || '纯文本' }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-textarea-row">
<span class="settings-row-title">Prompt BodyMarkdown</span>
<textarea
:value="roleForm.body_prompt"
placeholder="角色的自定义 prompt body..."
@input="roleForm.body_prompt = ($event.target as HTMLTextAreaElement).value"
></textarea>
</div>
</div>
<div class="role-editor-footer">
<button type="button" class="settings-secondary-button" @click="closeRoleEditor">取消</button>
<button
type="button"
class="settings-primary-button"
:disabled="roleSaving || !roleForm.role_id || !roleForm.name || !roleForm.body_prompt"
@click="saveRole"
>
{{ roleSaving ? '保存中...' : '保存' }}
</button>
</div>
</div>
</div>
</transition>
</section>
<section
key="admin"
class="settings-page admin-monitor-page"
>
@ -1888,6 +2133,8 @@ type PersonalTab =
| 'tools'
| 'files'
| 'data'
| 'voice'
| 'sub-agents'
| 'admin';
const baseTabs = [
@ -1900,7 +2147,8 @@ const baseTabs = [
{ id: 'tools', label: '工具与 Skills', icon: 'wrench' },
{ id: 'files', label: '文件与图片', icon: 'file' },
{ id: 'data', label: '数据管理', icon: 'layers' },
{ id: 'voice', label: '语音模型', icon: 'mic' }
{ id: 'voice', label: '语音模型', icon: 'mic' },
{ id: 'sub-agents', label: '子智能体', icon: 'bot' }
] as const satisfies ReadonlyArray<{ id: PersonalTab; label: string; icon: IconKey }>;
const sessionRole = ref('');
@ -2498,6 +2746,11 @@ watch(
if (isVisible && tab === 'data') {
fetchUsageSummary();
}
if (isVisible && tab === 'sub-agents') {
loadSubAgentRoles();
loadSubAgentSettings();
loadSubAgentModels();
}
if (
isVisible &&
tab === 'general' &&
@ -2510,6 +2763,145 @@ watch(
}
);
// ----- -----
const subAgentRoles = ref<any[]>([]);
const subAgentRolesLoading = ref(false);
const subAgentCompressThreshold = ref(150000);
const subAgentSettingsSaving = ref(false);
const subAgentModels = ref<any[]>([]);
const roleEditorOpen = ref(false);
const editingRole = ref<any>(null);
const roleSaving = ref(false);
const roleForm = ref({
role_id: '',
name: '',
description: '',
body_prompt: '',
thinking_mode: 'fast',
model_key: ''
});
const loadSubAgentRoles = async () => {
subAgentRolesLoading.value = true;
try {
const resp = await fetch('/api/multiagent/roles', { credentials: 'same-origin' });
const data = await resp.json();
if (data.success) {
subAgentRoles.value = data.roles || [];
}
} catch (e) {
//
} finally {
subAgentRolesLoading.value = false;
}
};
const loadSubAgentModels = async () => {
try {
const resp = await fetch('/api/multiagent/models', { credentials: 'same-origin' });
const data = await resp.json();
if (data.success) {
subAgentModels.value = data.models || [];
}
} catch (e) {
//
}
};
const loadSubAgentSettings = async () => {
try {
const resp = await fetch('/api/multiagent/settings', { credentials: 'same-origin' });
const data = await resp.json();
if (data.success && data.settings) {
subAgentCompressThreshold.value = data.settings.sub_agent_compress_threshold_tokens || 150000;
}
} catch (e) {
//
}
};
const saveSubAgentSettings = async () => {
subAgentSettingsSaving.value = true;
try {
await fetch('/api/multiagent/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ settings: { sub_agent_compress_threshold_tokens: subAgentCompressThreshold.value } })
});
} finally {
subAgentSettingsSaving.value = false;
}
};
const openRoleEditor = (role?: any) => {
if (role) {
editingRole.value = role;
roleForm.value = {
role_id: role.role_id || '',
name: role.name || '',
description: role.description || '',
body_prompt: role.body_prompt || '',
thinking_mode: role.thinking_mode || 'fast',
model_key: role.model_key || ''
};
} else {
editingRole.value = null;
roleForm.value = { role_id: '', name: '', description: '', body_prompt: '', thinking_mode: 'fast', model_key: '' };
}
closeDropdown();
roleEditorOpen.value = true;
};
const closeRoleEditor = () => {
roleEditorOpen.value = false;
editingRole.value = null;
closeDropdown();
};
const saveRole = async () => {
roleSaving.value = true;
try {
const isEdit = !!editingRole.value;
const url = isEdit
? `/api/multiagent/roles/${roleForm.value.role_id}`
: '/api/multiagent/roles';
const method = isEdit ? 'PUT' : 'POST';
const resp = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({
...roleForm.value,
model_key: roleForm.value.model_key || null
})
});
const data = await resp.json();
if (data.success) {
closeRoleEditor();
await loadSubAgentRoles();
}
} finally {
roleSaving.value = false;
}
};
const deleteRole = async (role: any) => {
if (!confirm(`确认删除角色「${role.name}」?`)) return;
try {
const resp = await fetch(`/api/multiagent/roles/${role.role_id}`, {
method: 'DELETE',
credentials: 'same-origin'
});
const data = await resp.json();
if (data.success) {
await loadSubAgentRoles();
}
} catch (e) {
//
}
};
const setActiveTab = (tab: PersonalTab) => {
activeTab.value = tab;
closeDropdown();
@ -2886,6 +3278,123 @@ const applyDefaultHideWorkspaceOption = async (hidden: boolean) => {
</script>
<style scoped>
.sub-agent-role-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.sub-agent-role-card {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 11px 0;
border-bottom: 1px solid var(--theme-control-border);
}
.sub-agent-role-info {
flex: 1;
min-width: 0;
}
.sub-agent-role-name {
font-size: 14px;
font-weight: 550;
color: var(--claude-text);
display: flex;
align-items: center;
gap: 6px;
}
.sub-agent-role-tag {
font-size: 10px;
padding: 1px 8px;
border-radius: 999px;
font-weight: 400;
}
.sub-agent-role-tag.tag-preset {
background: color-mix(in srgb, var(--claude-text-secondary) 15%, transparent);
color: var(--claude-text-secondary);
}
.sub-agent-role-tag.tag-custom {
background: color-mix(in srgb, var(--claude-accent) 15%, transparent);
color: var(--claude-accent);
}
.sub-agent-role-desc {
font-size: 12.5px;
color: var(--claude-text-secondary);
margin-top: 3px;
line-height: 1.38;
}
.sub-agent-role-meta {
font-size: 11px;
color: var(--claude-text-secondary);
opacity: 0.7;
margin-top: 2px;
}
.sub-agent-role-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
/* 角色编辑弹窗 — 复用 settings-redesign-card */
.role-editor-drawer-overlay {
position: fixed;
inset: 0;
background: var(--overlay-scrim);
display: flex;
align-items: center;
justify-content: center;
z-index: 2147483647;
}
.role-editor-drawer-card {
display: flex;
flex-direction: column;
overflow: visible;
}
.role-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
flex-shrink: 0;
}
.role-editor-header h2 {
font-size: 20px;
font-weight: 600;
color: var(--claude-text);
margin: 0;
}
.role-editor-body {
flex: 1;
overflow-y: auto;
overflow-x: visible;
min-height: 0;
scrollbar-width: none;
-ms-overflow-style: none;
}
.role-editor-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 12px;
flex-shrink: 0;
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.settings-section-title {
font-size: 14px;
font-weight: 550;
color: var(--claude-text);
}
.settings-empty-hint {
color: var(--claude-text-secondary);
font-size: 13px;
padding: 24px 0;
text-align: center;
}
.settings-redesign-card {
--settings-floating-menu-bg: color-mix(in srgb, var(--surface-soft) 90%, var(--surface-raised));
--settings-floating-menu-hover: var(--hover-bg);
@ -3063,7 +3572,8 @@ const applyDefaultHideWorkspaceOption = async (hidden: boolean) => {
.settings-redesign-tabs::-webkit-scrollbar,
.settings-redesign-scroll::-webkit-scrollbar,
.settings-floating-menu::-webkit-scrollbar,
.app-update-changelog-content::-webkit-scrollbar {
.app-update-changelog-content::-webkit-scrollbar,
.role-editor-body::-webkit-scrollbar {
display: none;
}

View File

@ -547,6 +547,9 @@ const props = withDefaults(
hostWorkspaceEnabled?: boolean;
groupByWorkspace?: boolean;
versioningHostMode?: boolean;
/** 当前会话是否仍在运行(用于标记当前正在查看的对话也显示运行中转圈) */
currentTaskInProgress?: boolean;
currentConversationId?: string;
}>(),
{
showCollapseButton: true,
@ -559,7 +562,9 @@ const props = withDefaults(
workspaceKind: 'workspace',
hostWorkspaceEnabled: false,
groupByWorkspace: false,
versioningHostMode: false
versioningHostMode: false,
currentTaskInProgress: false,
currentConversationId: ''
}
);
@ -890,8 +895,15 @@ const completedConversationIds = computed(
.filter(Boolean)
)
);
const isConversationActive = (conversationId: string) =>
activeConversationIds.value.has(String(conversationId || ''));
const isConversationActive = (conversationId: string) => {
if (activeConversationIds.value.has(String(conversationId || ''))) return true;
// runningTasks
// currentTaskInProgress
if (props.currentTaskInProgress && String(conversationId || '') === String(props.currentConversationId || '')) {
return true;
}
return false;
};
const isConversationCompleted = (conversationId: string) =>
completedConversationIds.value.has(String(conversationId || ''));
const isWorkspaceRunning = (workspaceId: string) =>

View File

@ -804,7 +804,6 @@ export async function initializeLegacySocket(ctx: any) {
return;
}
const convId = data.conversation_id;
console.log('[conv-trace] socket:conversation_resolved', data);
// 初始化期间不修改 currentConversationId避免与 bootstrapRoute 冲突
if (ctx.initialRouteResolved) {
@ -815,10 +814,12 @@ export async function initializeLegacySocket(ctx: any) {
ctx.promoteConversationToTop(convId);
const pathFragment = ctx.stripConversationPrefix(convId);
const currentPath = window.location.pathname.replace(/^\/+/, '');
const urlPrefix = ctx.multiAgentMode ? '/multiagent/' : '/';
const targetPath = `${urlPrefix}${pathFragment}`;
if (data.created) {
history.pushState({ conversationId: convId }, '', `/${pathFragment}`);
history.pushState({ conversationId: convId }, '', targetPath);
} else if (currentPath !== pathFragment) {
history.replaceState({ conversationId: convId }, '', `/${pathFragment}`);
history.replaceState({ conversationId: convId }, '', targetPath);
}
}
});
@ -1645,21 +1646,19 @@ export async function initializeLegacySocket(ctx: any) {
const hasRunningSubAgents = !!data?.has_running_sub_agents;
const hasRunningBackgroundCommands = !!data?.has_running_background_commands;
const hasRunningBackground = hasRunningSubAgents || hasRunningBackgroundCommands;
// 多智能体模式下,主任务停止即可恢复输入区,不因子智能体后台运行保持停止按钮
if (ctx.multiAgentMode || !hasRunningBackground) {
ctx.taskInProgress = false;
} else {
ctx.waitingForSubAgent = hasRunningSubAgents;
ctx.waitingForBackgroundCommand = hasRunningBackgroundCommands;
if (typeof ctx.uiPushToast === 'function') {
ctx.uiPushToast({
title: '智能体已停止',
message: '再次按下停止按钮以结束后台任务',
type: 'info'
});
}
const hasRunningMultiAgent = !!data?.has_running_multi_agent;
// 主对话已被停止。streaming、stopRequested 都重置。
// 如果仍有后台任务在跑,保留 taskInProgress=true 让对话在列表里仍显示“运行中”
// (仅影响 UI 标识,不影响输入栏可用性——输入栏只看 streamingMessage
const stillHasBackground = hasRunningBackground || hasRunningMultiAgent;
ctx.streamingMessage = false;
ctx.stopRequested = false;
ctx.waitingForSubAgent = hasRunningSubAgents;
ctx.waitingForBackgroundCommand = hasRunningBackgroundCommands;
ctx.taskInProgress = stillHasBackground;
if (typeof ctx.scheduleResetAfterTask === 'function') {
ctx.scheduleResetAfterTask('socket:task_stopped', { preserveMonitorWindows: true });
}
ctx.scheduleResetAfterTask('socket:task_stopped', { preserveMonitorWindows: true });
});
// 任务完成重点更新Token统计

View File

@ -179,6 +179,23 @@ export const useBackgroundCommandStore = defineStore('backgroundCommand', {
delete next[normalizedId];
this.stoppingCommandIds = next;
}
},
async stopAllCommands(conversationId?: string): Promise<{ success: boolean; stoppedCount?: number; error?: string }> {
try {
const resp = await fetch('/api/background_commands/stop_all', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(conversationId ? { conversation_id: conversationId } : {})
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.success) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
await this.fetchCommands();
return { success: true, stoppedCount: data?.data?.stopped_count || 0 };
} catch (error: any) {
return { success: false, error: error?.message || String(error) };
}
}
}
});

View File

@ -9,6 +9,8 @@ interface SubAgent {
last_tool?: string;
conversation_id?: string;
notice_pending?: boolean;
display_name?: string;
current_context_tokens?: number;
}
interface SubAgentActivityEntry {
@ -182,6 +184,24 @@ export const useSubAgentStore = defineStore('subAgent', {
this.stoppingTaskIds = next;
}
},
async stopAllAgents(mode: 'terminate' | 'soft_stop'): Promise<{ success: boolean; stoppedCount?: number; error?: string }> {
try {
const resp = await fetch('/api/sub_agents/stop_all', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode })
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.success) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
await this.fetchSubAgents();
return { success: true, stoppedCount: data?.data?.stopped_count || 0 };
} catch (error: any) {
const message = error?.message || String(error);
return { success: false, error: message };
}
},
stripConversationPrefix(conversationId: string) {
if (!conversationId) return '';
return conversationId.startsWith('conv_') ? conversationId.slice(5) : conversationId;

View File

@ -547,7 +547,8 @@ export const useTaskStore = defineStore('task', {
debugLog('[Task] 取消任务:', this.currentTaskId);
const response = await fetch(`/api/tasks/${this.currentTaskId}/cancel`, {
method: 'POST'
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {

View File

@ -483,12 +483,24 @@
justify-content: space-between;
align-items: center;
font-weight: 600;
margin-bottom: 6px;
margin-bottom: 4px;
}
.sub-agent-summary-text {
font-size: 13px;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.sub-agent-status {
text-transform: capitalize;
font-size: 12px;
flex-shrink: 0;
margin-left: 8px;
}
.sub-agent-status.running {
@ -499,6 +511,10 @@
color: var(--state-success);
}
.sub-agent-status.idle {
color: var(--text-secondary);
}
.sub-agent-status.failed,
.sub-agent-status.timeout,
.sub-agent-status.terminated,
@ -506,6 +522,32 @@
color: var(--state-danger);
}
.sub-agent-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.sub-agent-name {
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.sub-agent-tokens {
font-size: 11px;
color: var(--text-secondary);
opacity: 0.7;
flex-shrink: 0;
margin-left: 8px;
}
.sub-agent-summary {
font-size: 13px;
color: var(--claude-text);
@ -956,3 +998,28 @@
.panel-menu button.active {
background: var(--theme-tab-active);
}
/* 后台指令停止按钮 */
.background-stop-btn {
display: inline-flex;
align-items: center;
gap: 4px;
height: 28px;
padding: 0 10px;
border: 1px solid var(--claude-border);
border-radius: 10px;
background: transparent;
color: var(--state-error);
font-size: 12px;
line-height: 1.2;
cursor: pointer;
transition: background 140ms ease, color 140ms ease;
}
.background-stop-btn:hover {
background: color-mix(in srgb, var(--state-error) 12%, transparent);
}
.background-stop-label {
font-weight: 500;
}

View File

@ -0,0 +1,154 @@
"""回归测试:对话模型持久化与 /new 页面默认模型行为。"""
import os
import sys
import tempfile
import unittest
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from server.context import _apply_workspace_personalization_preferences
class FakeSession:
def __init__(self):
self._data = {}
def get(self, key, default=None):
return self._data.get(key, default)
def __setitem__(self, key, value):
self._data[key] = value
class TestApplyWorkspacePersonalizationPreferences(unittest.TestCase):
def _make_terminal(self, model_key="default-model"):
terminal = MagicMock()
terminal.model_key = model_key
terminal._workspace_default_model_applied = False
def _apply_personalization_preferences(config, apply_default_model=True):
if apply_default_model:
default_model = (config or {}).get("default_model")
if default_model:
terminal.model_key = default_model
terminal.apply_personalization_preferences = MagicMock(side_effect=_apply_personalization_preferences)
return terminal
def _make_workspace(self):
workspace = MagicMock()
workspace.data_dir = tempfile.mkdtemp()
return workspace
@patch("server.context.load_personalization_config")
@patch("server.context.has_request_context", return_value=True)
def test_session_model_restored(self, _hrc, mock_load_config):
"""session 中保存了模型时,应恢复到该模型。"""
mock_load_config.return_value = {"default_model": "default-model"}
terminal = self._make_terminal(model_key="old-model")
terminal.set_model = MagicMock(side_effect=lambda mk: setattr(terminal, "model_key", mk))
workspace = self._make_workspace()
session = FakeSession()
session["model_key"] = "session-model"
with patch("server.context.session", session):
_apply_workspace_personalization_preferences(terminal, workspace)
terminal.set_model.assert_called_once_with("session-model")
self.assertEqual(session.get("model_key"), "session-model")
@patch("server.context.load_personalization_config")
@patch("server.context.has_request_context", return_value=True)
def test_default_model_applied_for_fresh_session(self, _hrc, mock_load_config):
"""没有 session 模型时,应应用默认模型(且仅一次)。"""
mock_load_config.return_value = {"default_model": "default-model"}
terminal = self._make_terminal(model_key="kimi-k2.6")
workspace = self._make_workspace()
session = FakeSession()
with patch("server.context.session", session):
_apply_workspace_personalization_preferences(terminal, workspace)
terminal.set_model.assert_not_called()
self.assertEqual(terminal.model_key, "default-model")
self.assertEqual(session.get("model_key"), "default-model")
self.assertTrue(terminal._workspace_default_model_applied)
class TestLoadConversationRestoreModel(unittest.TestCase):
@patch("core.web_terminal.logger")
def test_restore_model_true_restores_saved_model(self, _mock_logger):
"""显式加载对话时应恢复对话保存的模型。"""
from core.web_terminal import WebTerminal
terminal = MagicMock(spec=WebTerminal)
terminal.model_key = "default-model"
terminal.thinking_mode = False
terminal.run_mode = "fast"
terminal.multi_agent_mode = False
terminal.context_manager = MagicMock()
terminal.context_manager.load_conversation_by_id.return_value = True
cm = MagicMock()
cm.load_conversation.return_value = {
"metadata": {
"thinking_mode": False,
"model_key": "saved-model",
"multi_agent_mode": False,
}
}
terminal.context_manager._get_conversation_manager_for_id.return_value = cm
terminal.set_model = MagicMock(side_effect=lambda mk: setattr(terminal, "model_key", mk))
terminal.set_permission_mode = MagicMock()
terminal.set_execution_mode = MagicMock()
terminal.set_network_permission = MagicMock()
terminal.api_client = MagicMock()
terminal.current_session_id = 0
# 调用实际方法
result = WebTerminal.load_conversation(terminal, "conv_test_001", restore_model=True)
terminal.set_model.assert_called_once_with("saved-model")
self.assertEqual(terminal.model_key, "saved-model")
self.assertTrue(result.get("success"))
@patch("core.web_terminal.logger")
def test_restore_model_false_keeps_current_model(self, _mock_logger):
"""程序启动自动恢复最近对话时不应恢复模型,避免 /new 页面显示旧模型。"""
from core.web_terminal import WebTerminal
terminal = MagicMock(spec=WebTerminal)
terminal.model_key = "default-model"
terminal.thinking_mode = False
terminal.run_mode = "fast"
terminal.multi_agent_mode = False
terminal.context_manager = MagicMock()
terminal.context_manager.load_conversation_by_id.return_value = True
cm = MagicMock()
cm.load_conversation.return_value = {
"metadata": {
"thinking_mode": False,
"model_key": "saved-model",
"multi_agent_mode": False,
}
}
terminal.context_manager._get_conversation_manager_for_id.return_value = cm
terminal.set_model = MagicMock(side_effect=lambda mk: setattr(terminal, "model_key", mk))
terminal.set_permission_mode = MagicMock()
terminal.set_execution_mode = MagicMock()
terminal.set_network_permission = MagicMock()
terminal.api_client = MagicMock()
terminal.current_session_id = 0
result = WebTerminal.load_conversation(terminal, "conv_test_001", restore_model=False)
terminal.set_model.assert_not_called()
self.assertEqual(terminal.model_key, "default-model")
self.assertTrue(result.get("success"))
if __name__ == "__main__":
unittest.main()

View File

@ -102,7 +102,12 @@ class CompressionMixin:
self.conversation_metadata[k] = v
if self.current_conversation_id:
try:
self.conversation_manager.update_conversation_metadata(self.current_conversation_id, updates)
target_manager = (
self._get_conversation_manager_for_id(self.current_conversation_id)
if hasattr(self, "_get_conversation_manager_for_id")
else self.conversation_manager
)
target_manager.update_conversation_metadata(self.current_conversation_id, updates)
except Exception as exc:
print(f"[ContextCompression] 更新压缩状态失败: {exc}")

View File

@ -84,7 +84,7 @@ class MessageMixin:
"""扫描工作区 .astrion/user_upload 目录,按修改时间倒序生成最近上传文件列表。"""
try:
project_path = Path(getattr(self, "project_path", ".") or ".").expanduser().resolve()
uploads_dir = project_path / ".agents" / "user_upload"
uploads_dir = project_path / ".astrion" / "user_upload"
if not uploads_dir.is_dir():
return ""
files = []

View File

@ -218,7 +218,7 @@ class ProjectMixin:
add_entry(entry)
# 根目录浅扫会跳过点目录;内部文件迁移到 .agents 后,单独展示这几个对模型有意义的目录。
agents_dir = self.project_path / ".agents"
agents_dir = self.project_path / ".astrion"
for internal_name in ("skills", "user_upload", "compact_result", "review"):
internal_dir = agents_dir / internal_name
if internal_dir.exists() and internal_dir.is_dir():

View File

@ -245,3 +245,95 @@ def _format_close_sub_agent(result_data: Dict[str, Any]) -> str:
status = result_data.get("status")
status_note = f"(状态 {status}" if status else ""
return f"{message}{status_note}task_id={task_id}"
def _format_terminate_sub_agent(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("terminate_sub_agent", result_data)
agent_id = result_data.get("agent_id")
task_id = result_data.get("task_id")
message = result_data.get("message") or "子智能体已被强制关闭。"
if agent_id is not None:
return f"已强制关闭子智能体 #{agent_id}task_id={task_id})。"
return f"{message}task_id={task_id}"
def _format_send_message_to_sub_agent(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("send_message_to_sub_agent", result_data)
agent_id = result_data.get("agent_id")
if agent_id is not None:
return f"已向子智能体 #{agent_id} 发送消息。"
return "消息已发送。"
def _format_ask_sub_agent(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("ask_sub_agent", result_data)
answer = result_data.get("answer")
if answer is None:
return "子智能体未返回答复。"
return f"子智能体的回答:\n{answer}"
def _format_answer_sub_agent_question(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("answer_sub_agent_question", result_data)
question_id = result_data.get("question_id")
if question_id:
return f"已向子智能体回复问题 {question_id}"
return "回复已发送。"
def _format_create_custom_agent(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("create_custom_agent", result_data)
role_id = result_data.get("role_id")
name = result_data.get("name") or role_id
return f"已创建自定义角色 {role_id}{name})。"
def _format_list_agents(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("list_agents", result_data)
roles = result_data.get("roles") or []
if not roles:
return "当前没有可用角色。"
lines = [f"可用角色(共 {len(roles)} 个):", ""]
for idx, role in enumerate(roles, start=1):
role_id = role.get("role_id") or "未知"
name = role.get("name") or role_id
description = role.get("description") or ""
thinking_mode = role.get("thinking_mode") or "fast"
is_custom = "" if role.get("is_custom") else ""
lines.append(f"{idx}. {role_id}{name}")
if description:
lines.append(f" 描述:{description}")
lines.append(f" 思考模式:{thinking_mode} | 自定义:{is_custom}")
return "\n".join(lines)
def _format_list_active_sub_agents(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("list_active_sub_agents", result_data)
return _format_active_sub_agents_list(result_data.get("agents") or [])
def _format_active_sub_agents_list(agents: List[Dict[str, Any]]) -> str:
if not agents:
return "当前会话没有活跃子智能体。"
lines = [f"当前会话活跃子智能体(共 {len(agents)} 个):", ""]
for agent in agents:
agent_id = agent.get("agent_id") or "?"
display_name = agent.get("display_name") or f"Agent_{agent_id}"
status = agent.get("status") or "unknown"
summary = agent.get("summary") or ""
last_output = agent.get("last_output") or ""
lines.append(f"#{agent_id} {display_name} [{status}]")
if summary:
lines.append(f" 任务:{summary}")
if last_output:
preview = last_output[:120]
suffix = "" if len(last_output) > 120 else ""
lines.append(f" 最近输出:{preview}{suffix}")
return "\n".join(lines)

View File

@ -36,6 +36,13 @@ from utils.tool_result_formatter.agent_context import (
_format_create_sub_agent,
_format_close_sub_agent,
_format_get_sub_agent_status,
_format_terminate_sub_agent,
_format_send_message_to_sub_agent,
_format_ask_sub_agent,
_format_answer_sub_agent_question,
_format_create_custom_agent,
_format_list_agents,
_format_list_active_sub_agents,
)
from utils.tool_result_formatter.web_media import (
_format_extract_webpage,
@ -122,5 +129,12 @@ TOOL_FORMATTERS = {
"manage_personalization": _format_manage_personalization,
"create_sub_agent": _format_create_sub_agent,
"close_sub_agent": _format_close_sub_agent,
"terminate_sub_agent": _format_terminate_sub_agent,
"send_message_to_sub_agent": _format_send_message_to_sub_agent,
"ask_sub_agent": _format_ask_sub_agent,
"answer_sub_agent_question": _format_answer_sub_agent_question,
"create_custom_agent": _format_create_custom_agent,
"list_agents": _format_list_agents,
"list_active_sub_agents": _format_list_active_sub_agents,
"get_sub_agent_status": _format_get_sub_agent_status,
}