agent-Specialization/utils/tool_result_formatter/agent_context.py
JOJO 70e16fccfb feat(tools): 工具动态加载——低频工具按需注入,对话文件快照为准
- 新增 core/tool_loading.py:可延迟注册表(8 类目 18 工具:工作流/子智能体/
  对话回顾/记忆写入/个性化/技能/MCP/彩蛋)、快照与状态迁移、目录渲染、守门判定
- 常驻 load_tools 工具:按名在 tool result 返回完整 JSON 定义后直接调用;
  执行守门拦截未加载的延迟工具并引导先加载;只读/计划模式放行
- 对话创建时快照 metadata.tool_loading(enabled/deferred_set/initial_exposed/
  loaded/pending),创建即钉死,个人空间设置只影响新建对话;老对话无字段
  不生效(全量工具,零迁移);多智能体对话 v1 不启用
- prompt 目录走现有冻结机制(frozen_tool_loading_prompt,skills 段之后),
  创建即冻结终身不变,不破坏前缀缓存;tools 数组恒为「构建集−deferred_set」
- 压缩三钩子(深度/浅度/手动)重置 loaded→空、pending→deferred_set,
  frozen_tool_loading_prompt 纳入 REBUILD_FROZEN_KEYS
- 个人空间新增「按需加载的工具」设置区(总开关默认开+类目分组勾选,
  双语 i18n),GET /api/personalization 下发注册表
- load_tools 上下文 formatter 保留完整 JSON 定义(定义靠 tool result 存续);
  前端渲染名称+状态徽章;新增 19 个单元测试
- AGENTS.md 新增 §13(硬约束与新增可延迟工具 checklist)
2026-09-10 17:07:15 +08:00

416 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import json
from typing import Any, Dict, List, Optional, Tuple
from utils.tool_result_formatter.common import (
_format_failure, _preview_text, _summarize_output_block, _summarize_todo_tasks
)
from modules.i18n import tr
def _format_conversation_search(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("conversation_search", result_data)
results = result_data.get("results") or []
header = f"找到 {len(results)} 个当前工作区内的历史对话"
filters: List[str] = []
keywords = result_data.get("keywords")
if isinstance(keywords, list):
normalized_keywords = [str(item).strip() for item in keywords if str(item or "").strip()]
else:
normalized_keywords = []
if normalized_keywords:
filters.append(f"关键词:{' / '.join(normalized_keywords)}")
elif result_data.get("query"):
filters.append(f"关键词:{result_data.get('query')}")
if result_data.get("start_date") or result_data.get("end_date"):
filters.append(f"日期:{result_data.get('start_date') or '不限'} ~ {result_data.get('end_date') or '不限'}")
if result_data.get("excluded_conversation_id"):
filters.append("已排除当前对话")
if filters:
header += "" + "".join(filters) + ""
if not results:
return header + "\n未找到匹配对话。"
lines = [header]
for idx, item in enumerate(results, start=1):
lines.append(f"{idx}. {item.get('id')}")
lines.append(f" 标题:{item.get('title') or '未命名对话'}")
if item.get("total_messages") is not None or item.get("total_tools") is not None:
lines.append(
f" 规模:{int(item.get('total_messages') or 0)} 条消息,{int(item.get('total_tools') or 0)} 个工具"
)
if item.get("first_user_message"):
lines.append(f" 首条用户消息:{item.get('first_user_message')}")
if item.get("created_at"):
lines.append(f" 创建时间:{item.get('created_at')}")
if item.get("updated_at"):
lines.append(f" 更新时间:{item.get('updated_at')}")
return "\n".join(lines)
def _format_conversation_review(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("conversation_review", result_data)
if result_data.get("mode") == "read" and result_data.get("content"):
lines = ["对话回顾内容:"]
if result_data.get("title"):
lines.append(f"标题:{result_data.get('title')}")
if result_data.get("char_count") is not None:
lines.append(f"字符数:{result_data.get('char_count')}")
lines.append("")
lines.append(str(result_data.get("content") or ""))
return "\n".join(lines)
path = result_data.get("path") or ""
if result_data.get("too_long"):
lines = [
f"对话回顾内容太长({result_data.get('char_count')} 字符),已保存到文件:",
str(path),
"请使用 read_file 分段或查找阅读该文件。",
]
if result_data.get("title"):
lines.insert(1, f"标题:{result_data.get('title')}")
return "\n".join(lines)
lines = [
"已生成对话回顾文件:",
str(path),
]
if result_data.get("title"):
lines.append(f"标题:{result_data.get('title')}")
if result_data.get("char_count") is not None:
lines.append(f"字符数:{result_data.get('char_count')}")
lines.append("请使用 read_file 读取该文件。")
return "\n".join(lines)
def _format_todo_create(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("todo_create", result_data)
todo = (result_data.get("todo_list") or {}).copy()
overview = todo.get("overview") or "未命名任务"
total = len(todo.get("tasks") or [])
return f"已创建 TODO{overview}(共 {total} 项)"
def _format_todo_update_task(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("todo_update_task", result_data)
message = result_data.get("message") or tr("fmt_agent.todo_update_default")
todo = result_data.get("todo_list") or {}
tasks = todo.get("tasks") or []
total = len(tasks)
done = sum(1 for t in tasks if t.get("status") == "done")
progress_note = f"进度 {done}/{total}" if total else ""
return f"{message}{progress_note}".strip("")
def _format_update_memory(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("update_memory", result_data)
operation = result_data.get("operation") or "write"
idx = result_data.get("index")
count = result_data.get("count")
if operation == "append":
suffix = f"(共 {count} 条)" if count is not None else ""
return f"记忆已追加新条目{suffix}"
if operation == "replace":
return f"记忆第 {idx} 条已替换。"
if operation == "delete":
suffix = f"(剩余 {count} 条)" if count is not None else ""
return f"记忆第 {idx} 条已删除{suffix}"
return f"记忆已更新。"
def _format_sub_agent_stats(stats: Optional[Dict[str, Any]]) -> str:
if not isinstance(stats, dict):
return ""
def _to_int(value: Any) -> int:
try:
return max(0, int(value))
except (TypeError, ValueError):
return 0
api_calls = _to_int(stats.get("api_calls") or stats.get("api_call_count") or stats.get("turn_count"))
files_read = _to_int(stats.get("files_read"))
edit_files = _to_int(stats.get("edit_files"))
searches = _to_int(stats.get("searches"))
web_pages = _to_int(stats.get("web_pages"))
commands = _to_int(stats.get("commands"))
lines = [
tr("fmt_agent2.api_calls", n=api_calls),
tr("fmt_agent2.files_read", n=files_read),
tr("fmt_agent2.edit_files", n=edit_files),
tr("fmt_agent2.searches", n=searches),
tr("fmt_agent2.web_pages", n=web_pages),
tr("fmt_agent2.commands", n=commands),
]
return "\n".join(lines)
def _format_create_sub_agent(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("create_sub_agent", result_data)
# 多智能体模式:对模型只暴露角色内编号显示名,全局 agent_id/task_id 为内部细节
display_name = result_data.get("display_name")
status = result_data.get("status")
if display_name:
return tr("fmt_agent2.created_with_status", name=display_name, status=status or "running")
agent_id = result_data.get("agent_id")
task_id = result_data.get("task_id")
refs = result_data.get("copied_references") or []
ref_note = tr("fmt_agent2.ref_count_note", n=len(refs)) if refs else ""
deliver_dir = result_data.get("deliverables_dir")
deliver_note = tr("fmt_agent2.deliver_dir_note", dir=deliver_dir) if deliver_dir else ""
header = tr(
"fmt_agent2.created_header",
agent_id=agent_id,
task_id=task_id,
status=status,
ref_note=ref_note,
deliver_note=deliver_note,
)
stats_text = _format_sub_agent_stats(
result_data.get("stats") or (result_data.get("final_result") or {}).get("stats")
)
summary = result_data.get("message") or result_data.get("summary")
elapsed_seconds = result_data.get("runtime_seconds")
if elapsed_seconds is None:
elapsed_seconds = result_data.get("elapsed_seconds")
lines = [header]
if stats_text:
lines.append(stats_text)
if status == "completed" and isinstance(elapsed_seconds, (int, float)):
lines.append(tr("fmt_agent2.running_seconds", n=int(round(elapsed_seconds))))
if summary and status in {"completed", "failed", "timeout", "terminated"}:
lines.append(str(summary))
return "\n".join(lines)
def _format_wait_sub_agent(result_data: Dict[str, Any]) -> str:
task_id = result_data.get("task_id")
agent_id = result_data.get("agent_id")
status = result_data.get("status")
stats_value = result_data.get("stats")
if not isinstance(stats_value, dict) and status == "timeout":
stats_value = {}
stats_text = _format_sub_agent_stats(stats_value)
elapsed_seconds = result_data.get("runtime_seconds")
if elapsed_seconds is None:
elapsed_seconds = result_data.get("elapsed_seconds")
if result_data.get("success"):
copied_path = result_data.get("copied_path") or result_data.get("deliverables_path")
message = result_data.get("message") or tr("fmt_agent2.task_completed")
deliver_note = (
tr("fmt_agent2.copied_to", path=copied_path)
if copied_path
else tr("fmt_agent2.deliver_generated")
)
lines = [tr("fmt_agent2.completed_header", agent_id=agent_id, task_id=task_id)]
if stats_text:
lines.append(stats_text)
if isinstance(elapsed_seconds, (int, float)):
lines.append(tr("fmt_agent2.running_seconds", n=int(round(elapsed_seconds))))
lines.append(message)
lines.append(deliver_note)
return "\n".join(lines)
message = result_data.get("message") or result_data.get("error") or tr("fmt_agent2.task_failed")
lines = [tr("fmt_agent2.abnormal_header", agent_id=agent_id, task_id=task_id, status=status)]
if stats_text:
lines.append(stats_text)
lines.append(message)
return "\n".join(lines)
def _format_get_sub_agent_status(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("get_sub_agent_status", result_data)
results = result_data.get("results") or []
if not results:
return tr("fmt_agent2.no_status_results")
blocks = []
for item in results:
# 多智能体模式:优先使用角色内编号显示名,不暴露全局 agent_id
agent_id = item.get("agent_id")
label = item.get("display_name") or f"#{agent_id}"
if not item.get("found"):
blocks.append(tr("fmt_agent2.label_not_found", label=label))
continue
status = item.get("status")
summary = None
final_result = item.get("final_result") or {}
elapsed_seconds = None
if isinstance(final_result, dict):
summary = final_result.get("message") or final_result.get("summary")
elapsed_seconds = final_result.get("runtime_seconds")
if elapsed_seconds is None:
elapsed_seconds = final_result.get("elapsed_seconds")
if not summary:
summary = item.get("summary") or ""
stats_text = _format_sub_agent_stats(item.get("stats"))
if status == "completed":
lines = [tr("fmt_agent2.label_completed", label=label)]
elif status == "terminated":
lines = [tr("fmt_agent2.label_terminated", label=label)]
elif status in {"failed", "timeout"}:
lines = [tr("fmt_agent2.label_abnormal", label=label, status=status)]
else:
lines = [tr("fmt_agent2.label_status", label=label, status=status)]
if stats_text:
lines.append(stats_text)
if status == "completed" and isinstance(elapsed_seconds, (int, float)):
lines.append(tr("fmt_agent2.running_seconds", n=int(round(elapsed_seconds))))
if summary:
lines.append(str(summary))
blocks.append("\n".join(lines))
return "\n\n".join(blocks)
def _format_load_tools(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("load_tools", result_data)
tools = result_data.get("tools") or []
names = [
name for name in ((t.get("function") or {}).get("name") for t in tools)
if name
]
header = result_data.get("message") or tr(
"fmt_tool_loading.loaded", n=len(tools), names=", ".join(names)
)
parts = [header]
already = result_data.get("already_loaded") or []
if already:
parts.append(tr("fmt_tool_loading.already", names=", ".join(already)))
# 关键:完整 JSON 定义必须保留在上下文输出里——模型后续调用的依据就是
# 这份 schema若只写摘要历史重建后模型将失去参数结构与压缩重置
# 同源设计:定义只存在于 tool result 中)。
parts.append(tr("fmt_tool_loading.defs_header"))
parts.append("```json\n" + json.dumps(tools, ensure_ascii=False, indent=2) + "\n```")
return "\n\n".join(parts)
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)
display_name = result_data.get("display_name")
if display_name:
return tr("fmt_agent2.force_closed_display", name=display_name)
agent_id = result_data.get("agent_id")
task_id = result_data.get("task_id")
message = result_data.get("message") or tr("fmt_agent2.force_closed")
if agent_id is not None:
return tr("fmt_agent2.force_closed_id", agent_id=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)
display_name = result_data.get("display_name")
if display_name:
return tr("fmt_agent2.sent_to_display", name=display_name)
agent_id = result_data.get("agent_id")
if agent_id is not None:
return tr("fmt_agent2.sent_to_id", agent_id=agent_id)
return tr("fmt_agent2.sent_plain")
def _format_stop_sub_agent(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("stop_sub_agent", result_data)
display_name = result_data.get("display_name")
message = result_data.get("message") or tr("fmt_agent2.paused")
if display_name:
# manager 返回的 message 已是「{显示名} 已暂停…」格式,直接返回避免重复
return message
agent_id = result_data.get("agent_id")
if agent_id is not None:
return tr("fmt_agent2.paused_id", agent_id=agent_id, message=message)
return message
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 tr("fmt_agent2.answered_question", question_id=question_id)
return tr("fmt_agent2.answered_plain")
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
if result_data.get("overwritten"):
return f"已覆盖更新角色 {role_id}{name})。新设定对之后创建的实例生效,运行中的实例不受影响。"
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 ""
# 只暴露角色内编号显示名,不暴露全局 agent_id
lines.append(f"{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)
def _format_submit_plan(result_data: Dict[str, Any]) -> str:
"""submit_plan 工具结果 → 对话上下文摘要(计划批准流的两态)。"""
status = str(result_data.get("status") or "").strip()
plan_file = str(result_data.get("plan_file") or "").strip()
comment = str(result_data.get("comment") or "").strip()
message = str(result_data.get("message") or "").strip()
if status == "approved":
parts = [f"✅ 用户已批准计划({plan_file}),运行模式已切换为「执行」。"]
if comment:
parts.append(f"用户批准意见:{comment}")
parts.append("现在可以开始按计划实施。")
return "\n".join(parts)
if status == "rejected":
parts = [f"❌ 用户拒绝了计划({plan_file}),仍处于计划模式。"]
if comment:
parts.append(f"用户意见:{comment}")
else:
parts.append("用户未填写具体意见。")
parts.append("请修订计划文档后重新提交。")
return "\n".join(parts)
# 校验失败 / 未在计划模式 / 超时等其他状态
if message:
return f"⚠️ submit_plan 未完成({status or 'unknown'}{message}"
return _format_failure("submit_plan", result_data)