Add task progress tool
This commit is contained in:
parent
15ecf45c93
commit
8616ba2b56
|
|
@ -50,6 +50,7 @@ from tools.seedance import SeedanceTool
|
||||||
from tools.seedream import SeedreamTool
|
from tools.seedream import SeedreamTool
|
||||||
from tools.shell import ShellTool
|
from tools.shell import ShellTool
|
||||||
from tools.skill_tool import LoadSkillTool
|
from tools.skill_tool import LoadSkillTool
|
||||||
|
from tools.task_progress import TaskProgressTool
|
||||||
from tools.web_fetch import WebFetchTool
|
from tools.web_fetch import WebFetchTool
|
||||||
from tools.web_search import WebSearchTool
|
from tools.web_search import WebSearchTool
|
||||||
|
|
||||||
|
|
@ -426,6 +427,9 @@ def build_agent(
|
||||||
ur_path = user_root(workspace_dir, uid)
|
ur_path = user_root(workspace_dir, uid)
|
||||||
|
|
||||||
tools = {}
|
tools = {}
|
||||||
|
tp = TaskProgressTool(base_dir=tool_base, user_root=ur_path)
|
||||||
|
tools[tp.name] = tp
|
||||||
|
|
||||||
for cls in (ReadTool, WriteTool, EditTool, GlobTool, GrepTool, ShellTool):
|
for cls in (ReadTool, WriteTool, EditTool, GlobTool, GrepTool, ShellTool):
|
||||||
t = cls(base_dir=tool_base, user_root=ur_path)
|
t = cls(base_dir=tool_base, user_root=ur_path)
|
||||||
tools[t.name] = t
|
tools[t.name] = t
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,35 @@ def _message_chars(msg: dict[str, Any]) -> int:
|
||||||
return len(str(msg))
|
return len(str(msg))
|
||||||
|
|
||||||
|
|
||||||
def _compact_tool_call_arguments(raw: Any, max_chars: int) -> tuple[Any, bool]:
|
def _compact_task_progress_arguments(raw: Any) -> tuple[Any, bool]:
|
||||||
|
if not isinstance(raw, str):
|
||||||
|
return raw, False
|
||||||
|
try:
|
||||||
|
parsed = json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
parsed = {}
|
||||||
|
if not isinstance(parsed, dict):
|
||||||
|
parsed = {}
|
||||||
|
marker: dict[str, Any] = {
|
||||||
|
"_compacted": True,
|
||||||
|
"tool": "task_progress",
|
||||||
|
"note": "old UI progress details omitted from context",
|
||||||
|
}
|
||||||
|
action = parsed.get("action")
|
||||||
|
if isinstance(action, str) and action:
|
||||||
|
marker["action"] = action
|
||||||
|
steps = parsed.get("steps")
|
||||||
|
if isinstance(steps, list):
|
||||||
|
marker["step_count"] = len(steps)
|
||||||
|
step = parsed.get("step")
|
||||||
|
if isinstance(step, dict) and isinstance(step.get("id"), str):
|
||||||
|
marker["step_id"] = step["id"]
|
||||||
|
return json.dumps(marker, ensure_ascii=False), True
|
||||||
|
|
||||||
|
|
||||||
|
def _compact_tool_call_arguments(raw: Any, max_chars: int, tool_name: str = "") -> tuple[Any, bool]:
|
||||||
|
if tool_name == "task_progress":
|
||||||
|
return _compact_task_progress_arguments(raw)
|
||||||
if not isinstance(raw, str) or len(raw) <= max_chars:
|
if not isinstance(raw, str) or len(raw) <= max_chars:
|
||||||
return raw, False
|
return raw, False
|
||||||
marker: dict[str, Any] = {
|
marker: dict[str, Any] = {
|
||||||
|
|
@ -85,9 +113,11 @@ def _compact_assistant_tool_calls(
|
||||||
if not isinstance(fn, dict):
|
if not isinstance(fn, dict):
|
||||||
continue
|
continue
|
||||||
before = fn.get("arguments")
|
before = fn.get("arguments")
|
||||||
|
tool_name = fn.get("name") if isinstance(fn.get("name"), str) else ""
|
||||||
after, did_compact = _compact_tool_call_arguments(
|
after, did_compact = _compact_tool_call_arguments(
|
||||||
before,
|
before,
|
||||||
max_chars=max(0, max_arg_chars),
|
max_chars=max(0, max_arg_chars),
|
||||||
|
tool_name=tool_name,
|
||||||
)
|
)
|
||||||
if did_compact:
|
if did_compact:
|
||||||
fn["arguments"] = after
|
fn["arguments"] = after
|
||||||
|
|
@ -152,6 +182,8 @@ def prepare_messages_with_stats(
|
||||||
if new_msg.get("name") == "load_skill":
|
if new_msg.get("name") == "load_skill":
|
||||||
new_msg["content"] = _compact_load_skill_content(before)
|
new_msg["content"] = _compact_load_skill_content(before)
|
||||||
compacted_skill_messages += int(new_msg["content"] != before)
|
compacted_skill_messages += int(new_msg["content"] != before)
|
||||||
|
elif new_msg.get("name") == "task_progress":
|
||||||
|
new_msg["content"] = "[task_progress updated; UI-only details omitted from context]"
|
||||||
else:
|
else:
|
||||||
new_msg["content"] = _compact_old_tool_content(
|
new_msg["content"] = _compact_old_tool_content(
|
||||||
before,
|
before,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,13 @@
|
||||||
- `shell` —— 执行命令(默认 60s 超时)
|
- `shell` —— 执行命令(默认 60s 超时)
|
||||||
- `run_python` —— 在子进程里跑 Python (数据处理、生成 .pptx/.docx、画图等)。非短小一次性代码时,先用 `write` 把 `.py` 文件写到 task_dir,再用 `run_python(script_path="...")` 执行;避免大段源码进入对话历史。
|
- `run_python` —— 在子进程里跑 Python (数据处理、生成 .pptx/.docx、画图等)。非短小一次性代码时,先用 `write` 把 `.py` 文件写到 task_dir,再用 `run_python(script_path="...")` 执行;避免大段源码进入对话历史。
|
||||||
- `load_skill` —— 加载某个 skill 的完整指引
|
- `load_skill` —— 加载某个 skill 的完整指引
|
||||||
|
- `task_progress` —— 给 Web 前端发布/更新用户可见的进度步骤列表。只在多步骤任务使用;开始时设 3-7 个关键步骤,每完成或进入一个关键步骤时更新一次。
|
||||||
|
|
||||||
|
## 进度展示约定
|
||||||
|
- 多步骤任务开始后,用 `task_progress(action="set_plan", steps=[...])` 发布一份简短计划。
|
||||||
|
- 步骤标题面向用户,写“理解需求 / 实现功能 / 运行测试 / 输出结果”这类阶段,不要写每个文件读写或每次 shell 命令。
|
||||||
|
- 只在关键状态变化时用 `update_step`:当前步骤进入 `in_progress`,完成后改 `completed`。不要高频更新,不要把普通 tool 调用都转成进度步骤。
|
||||||
|
- 简单问答、单次文件读取、很小的改动不需要调用 `task_progress`。
|
||||||
|
|
||||||
## 媒体生成工具(按需可用,未配置 ARK_API_KEY 时该工具不会出现)
|
## 媒体生成工具(按需可用,未配置 ARK_API_KEY 时该工具不会出现)
|
||||||
- `seedream` —— 豆包图像生成。产物自动落 `<task_dir>/figures/`。每次 **¥0.22**(联网 `search=true` 加 ¥0.05)。
|
- `seedream` —— 豆包图像生成。产物自动落 `<task_dir>/figures/`。每次 **¥0.22**(联网 `search=true` 加 ¥0.05)。
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ description: 科学问题分析 / 拆解 / 引导。用户提出模糊的高层
|
||||||
|
|
||||||
**本 skill 不直接产出最终交付物**(不查文献 / 不写本子 / 不跑模型),只产出 `analysis.md` —— 一个共识性的"问题理解 + 路线图"文件。
|
**本 skill 不直接产出最终交付物**(不查文献 / 不写本子 / 不跑模型),只产出 `analysis.md` —— 一个共识性的"问题理解 + 路线图"文件。
|
||||||
|
|
||||||
|
进度展示建议:用 `task_progress` 标记「PICO 规范化 / Issue Tree 拆解 / 分支深化 / 实施路线图」四个阶段;用户确认卡点仍按正文工作流执行。
|
||||||
|
|
||||||
## 何时用
|
## 何时用
|
||||||
|
|
||||||
- 用户说"我想搞清楚 / 想研究 / 不知道从哪入手 / 这个问题怎么拆 / 我们能做什么"
|
- 用户说"我想搞清楚 / 想研究 / 不知道从哪入手 / 这个问题怎么拆 / 我们能做什么"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ description: 修改、调试、实现代码相关任务。当用户要求修 bug
|
||||||
- 没有专属 scripts 或 templates,因为代码任务的多样性来自代码本身
|
- 没有专属 scripts 或 templates,因为代码任务的多样性来自代码本身
|
||||||
|
|
||||||
## 原则
|
## 原则
|
||||||
|
- 多步骤代码任务适合用 `task_progress` 维护进度,推荐阶段:理解需求 → 定位代码 → 实现修改 → 运行验证 → 总结结果。
|
||||||
- **先看后改**: 用 grep/glob 定位,read 读出修改点的上下文,再 edit。盲改会出错。
|
- **先看后改**: 用 grep/glob 定位,read 读出修改点的上下文,再 edit。盲改会出错。
|
||||||
- **改动最小**: 只动必要行,不顺手重构、不改无关空白
|
- **改动最小**: 只动必要行,不顺手重构、不改无关空白
|
||||||
- **edit 唯一匹配**: old_str 必须在文件里出现且仅出现一次,不够唯一就多带上下文
|
- **edit 唯一匹配**: old_str 必须在文件里出现且仅出现一次,不够唯一就多带上下文
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ description: 生成 PowerPoint 演示文稿 (.pptx) 文件。✅ 触发:用户
|
||||||
|
|
||||||
把材料变成可演示的 .pptx。**先定调,再出稿,再验收** —— 不要一口气把整份 deck 丢出去。
|
把材料变成可演示的 .pptx。**先定调,再出稿,再验收** —— 不要一口气把整份 deck 丢出去。
|
||||||
|
|
||||||
|
进度展示建议:多页 deck 任务用 `task_progress` 标记「摄取素材 / 八条对齐 / 逐页生成 / 质量检查 / 交付」等关键阶段;不要把每一页的内部文件写入都作为进度步骤。
|
||||||
|
|
||||||
## 资源
|
## 资源
|
||||||
- `scripts/pptx_helpers.py` —— **版式工具箱模块**:配色/字体常量 + `new_presentation`/`load`/`add_slide`/`set_palette` + `add_textbox`/`add_rect`/`add_dot`/`add_badge`/`page_title`/`apply_brand` 等 helper。每页 `import pptx_helpers as P` 调用,**不要把 helper 源码默写进 run_python**
|
- `scripts/pptx_helpers.py` —— **版式工具箱模块**:配色/字体常量 + `new_presentation`/`load`/`add_slide`/`set_palette` + `add_textbox`/`add_rect`/`add_dot`/`add_badge`/`page_title`/`apply_brand` 等 helper。每页 `import pptx_helpers as P` 调用,**不要把 helper 源码默写进 run_python**
|
||||||
- `references/design_principles.md` —— 画布尺寸 + 字号/配色/留白/字数预算等硬规则
|
- `references/design_principles.md` —— 画布尺寸 + 字号/配色/留白/字数预算等硬规则
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ description: 撰写中国科研项目申报书 / 课题任务书 (国家重点
|
||||||
|
|
||||||
把课题信息变成可提交的申报书 .docx。**先定基金类型 → 八条对齐 → 逐章起草 → 验收渲染** —— 不要一口气出全文。
|
把课题信息变成可提交的申报书 .docx。**先定基金类型 → 八条对齐 → 逐章起草 → 验收渲染** —— 不要一口气出全文。
|
||||||
|
|
||||||
|
进度展示建议:申报书任务用 `task_progress` 标记「摄取素材 / 基金类型与八条对齐 / 逐章起草 / 质量检查 / 渲染交付」等关键阶段;章节内的每段确认不需要都单独更新进度。
|
||||||
|
|
||||||
## 资源
|
## 资源
|
||||||
|
|
||||||
下面所有路径都相对 **`<skill_dir>`** —— `load_skill` 返回头里的 `[skill=proposal, dir=<绝对路径>]`,用这个绝对路径拼脚本/资源,不要假设 cwd。
|
下面所有路径都相对 **`<skill_dir>`** —— `load_skill` 返回头里的 `[skill=proposal, dir=<绝对路径>]`,用这个绝对路径拼脚本/资源,不要假设 cwd。
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,54 @@ class ContextCompactionTests(unittest.TestCase):
|
||||||
self.assertNotIn("A" * 100, tc["function"]["arguments"])
|
self.assertNotIn("A" * 100, tc["function"]["arguments"])
|
||||||
self.assertEqual(stats["compacted_tool_call_arguments"], 1)
|
self.assertEqual(stats["compacted_tool_call_arguments"], 1)
|
||||||
|
|
||||||
|
def test_compacts_old_task_progress_arguments_to_plan_marker(self) -> None:
|
||||||
|
args = json.dumps({
|
||||||
|
"action": "set_plan",
|
||||||
|
"steps": [
|
||||||
|
{"id": "s1", "title": "理解需求", "status": "completed"},
|
||||||
|
{"id": "s2", "title": "实现功能", "status": "in_progress"},
|
||||||
|
],
|
||||||
|
}, ensure_ascii=False)
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "rules"},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": "tc1",
|
||||||
|
"type": "function",
|
||||||
|
"function": {"name": "task_progress", "arguments": args},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
] + [{"role": "user", "content": f"recent {i}"} for i in range(12)]
|
||||||
|
|
||||||
|
prepared, stats = prepare_messages_with_stats(messages)
|
||||||
|
|
||||||
|
compacted_args = json.loads(prepared[1]["tool_calls"][0]["function"]["arguments"])
|
||||||
|
self.assertTrue(compacted_args["_compacted"])
|
||||||
|
self.assertEqual(compacted_args["tool"], "task_progress")
|
||||||
|
self.assertEqual(compacted_args["action"], "set_plan")
|
||||||
|
self.assertEqual(compacted_args["step_count"], 2)
|
||||||
|
self.assertNotIn("理解需求", prepared[1]["tool_calls"][0]["function"]["arguments"])
|
||||||
|
self.assertEqual(stats["compacted_tool_call_arguments"], 1)
|
||||||
|
|
||||||
|
def test_old_task_progress_tool_result_uses_tiny_marker(self) -> None:
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "rules"},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": "tc1",
|
||||||
|
"name": "task_progress",
|
||||||
|
"content": json.dumps({"ok": True, "steps": [{"title": "A" * 2000}]}),
|
||||||
|
},
|
||||||
|
{"role": "user", "content": "next"},
|
||||||
|
]
|
||||||
|
|
||||||
|
prepared, stats = prepare_messages_with_stats(messages, keep_recent=1)
|
||||||
|
|
||||||
|
self.assertEqual(prepared[1]["content"], "[task_progress updated; UI-only details omitted from context]")
|
||||||
|
self.assertEqual(stats["compacted_tool_messages"], 1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from tools.task_progress import TaskProgressTool
|
||||||
|
|
||||||
|
|
||||||
|
class TaskProgressToolTests(unittest.TestCase):
|
||||||
|
def test_schema_exposes_set_update_and_clear_actions(self) -> None:
|
||||||
|
schema = TaskProgressTool().schema
|
||||||
|
|
||||||
|
fn = schema["function"]
|
||||||
|
self.assertEqual(fn["name"], "task_progress")
|
||||||
|
action_enum = fn["parameters"]["properties"]["action"]["enum"]
|
||||||
|
self.assertEqual(action_enum, ["set_plan", "update_step", "clear"])
|
||||||
|
|
||||||
|
def test_execute_returns_short_ui_only_result(self) -> None:
|
||||||
|
out = TaskProgressTool().execute(
|
||||||
|
action="set_plan",
|
||||||
|
steps=[
|
||||||
|
{"id": "s1", "title": "理解需求", "status": "completed"},
|
||||||
|
{"id": "s2", "title": "实现功能", "status": "in_progress"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(out)
|
||||||
|
self.assertEqual(data, {"ok": True, "action": "set_plan", "step_count": 2})
|
||||||
|
self.assertLess(len(out), 80)
|
||||||
|
|
||||||
|
def test_execute_normalizes_update_step_without_echoing_title(self) -> None:
|
||||||
|
out = TaskProgressTool().execute(
|
||||||
|
action="update_step",
|
||||||
|
step={"id": "s2", "title": "实现功能", "status": "completed"},
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(out)
|
||||||
|
self.assertEqual(data, {"ok": True, "action": "update_step", "step_id": "s2"})
|
||||||
|
self.assertNotIn("实现功能", out)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
"""UI-only task progress tool.
|
||||||
|
|
||||||
|
The tool gives the model a structured way to publish a short user-visible plan.
|
||||||
|
Its result is intentionally tiny; the full plan stays in the assistant tool_call
|
||||||
|
arguments for Web rendering and is compacted out of older LLM context.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .base import Tool
|
||||||
|
|
||||||
|
|
||||||
|
class TaskProgressTool(Tool):
|
||||||
|
name = "task_progress"
|
||||||
|
description = (
|
||||||
|
"Publish or update a concise user-visible progress checklist for the current task. "
|
||||||
|
"Use only for meaningful multi-step work: set the plan once, update a step when it "
|
||||||
|
"starts or completes, and clear it only when it is no longer relevant. This is a UI "
|
||||||
|
"progress signal, not a work product."
|
||||||
|
)
|
||||||
|
parameters = {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["set_plan", "update_step", "clear"],
|
||||||
|
"description": "set_plan replaces the checklist; update_step changes one step; clear removes it.",
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Required for set_plan. Keep to 3-7 user-meaningful steps.",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "Stable short id, e.g. s1."},
|
||||||
|
"title": {"type": "string", "description": "Short user-visible step title."},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pending", "in_progress", "completed"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["id", "title", "status"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Required for update_step.",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string"},
|
||||||
|
"title": {"type": "string"},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pending", "in_progress", "completed"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["id", "status"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["action"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute(self, **kwargs: Any) -> str:
|
||||||
|
action = str(kwargs.get("action") or "")
|
||||||
|
out: dict[str, Any] = {"ok": True, "action": action}
|
||||||
|
if action == "set_plan":
|
||||||
|
steps = kwargs.get("steps")
|
||||||
|
out["step_count"] = len(steps) if isinstance(steps, list) else 0
|
||||||
|
elif action == "update_step":
|
||||||
|
step = kwargs.get("step")
|
||||||
|
if isinstance(step, dict) and step.get("id"):
|
||||||
|
out["step_id"] = str(step["id"])
|
||||||
|
return json.dumps(out, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
|
@ -439,6 +439,33 @@
|
||||||
margin: 4px 0 0; padding: 8px; background: var(--code-bg); border-radius: var(--r-sm);
|
margin: 4px 0 0; padding: 8px; background: var(--code-bg); border-radius: var(--r-sm);
|
||||||
overflow-x: auto; max-height: 300px; white-space: pre-wrap;
|
overflow-x: auto; max-height: 300px; white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
.task-progress {
|
||||||
|
margin-top: 8px; padding: 8px 10px;
|
||||||
|
border: 1px solid var(--border-soft); border-radius: var(--r-md);
|
||||||
|
background: #fafafa; font-size: 12px;
|
||||||
|
}
|
||||||
|
.task-progress .tp-title {
|
||||||
|
margin-bottom: 5px; color: var(--muted); font-family: var(--mono); font-size: 11px;
|
||||||
|
}
|
||||||
|
.task-progress .tp-list { display: grid; gap: 4px; }
|
||||||
|
.task-progress .tp-step {
|
||||||
|
display: grid; grid-template-columns: 18px minmax(0, 1fr);
|
||||||
|
align-items: start; gap: 6px; min-height: 18px;
|
||||||
|
}
|
||||||
|
.task-progress .tp-mark {
|
||||||
|
width: 18px; height: 18px; border-radius: 50%;
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
border: 1px solid var(--border); background: #fff; color: var(--muted);
|
||||||
|
font-size: 11px; line-height: 1;
|
||||||
|
}
|
||||||
|
.task-progress .tp-step.completed .tp-mark {
|
||||||
|
color: #fff; background: var(--c-green); border-color: var(--c-green);
|
||||||
|
}
|
||||||
|
.task-progress .tp-step.in_progress .tp-mark {
|
||||||
|
color: var(--accent); border-color: var(--accent); background: var(--accent-soft);
|
||||||
|
}
|
||||||
|
.task-progress .tp-text { overflow-wrap: anywhere; line-height: 1.45; }
|
||||||
|
.task-progress .tp-step.completed .tp-text { color: var(--muted); text-decoration: line-through; }
|
||||||
/* media tool 摘要 banner(model / size / cost / elapsed,折叠态也可见) */
|
/* media tool 摘要 banner(model / size / cost / elapsed,折叠态也可见) */
|
||||||
.tool-banner {
|
.tool-banner {
|
||||||
display: inline-flex; flex-wrap: wrap; gap: 6px;
|
display: inline-flex; flex-wrap: wrap; gap: 6px;
|
||||||
|
|
|
||||||
|
|
@ -445,6 +445,80 @@ function setRunHint(run, text) {
|
||||||
if (state.taskId === run.taskId) $("chat-hint").textContent = text;
|
if (state.taskId === run.taskId) $("chat-hint").textContent = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeProgressStatus(status) {
|
||||||
|
return ["pending", "in_progress", "completed"].includes(status) ? status : "pending";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProgressStep(step) {
|
||||||
|
if (!step || typeof step !== "object") return null;
|
||||||
|
const id = String(step.id || "").trim();
|
||||||
|
const title = String(step.title || "").trim();
|
||||||
|
const status = normalizeProgressStatus(step.status);
|
||||||
|
if (!id || !title) return null;
|
||||||
|
return { id, title, status };
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyProgressAction(progress, args) {
|
||||||
|
if (!args || typeof args !== "object") return progress;
|
||||||
|
const action = args.action || "";
|
||||||
|
if (action === "clear") return [];
|
||||||
|
if (action === "set_plan") {
|
||||||
|
const steps = Array.isArray(args.steps) ? args.steps.map(normalizeProgressStep).filter(Boolean) : [];
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
if (action === "update_step") {
|
||||||
|
const raw = args.step;
|
||||||
|
if (!raw || typeof raw !== "object") return progress;
|
||||||
|
const id = String(raw.id || "").trim();
|
||||||
|
if (!id) return progress;
|
||||||
|
let found = false;
|
||||||
|
const next = progress.map((s) => {
|
||||||
|
if (s.id !== id) return s;
|
||||||
|
found = true;
|
||||||
|
return {
|
||||||
|
id: s.id,
|
||||||
|
title: String(raw.title || s.title || "").trim(),
|
||||||
|
status: normalizeProgressStatus(raw.status || s.status),
|
||||||
|
};
|
||||||
|
}).filter(s => s.title);
|
||||||
|
if (!found && raw.title) {
|
||||||
|
next.push({
|
||||||
|
id,
|
||||||
|
title: String(raw.title).trim(),
|
||||||
|
status: normalizeProgressStatus(raw.status),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProgressHtml(steps) {
|
||||||
|
if (!Array.isArray(steps) || !steps.length) return "";
|
||||||
|
const mark = (status) => status === "completed" ? "✓" : (status === "in_progress" ? "…" : "");
|
||||||
|
const rows = steps.map((s) => `
|
||||||
|
<div class="tp-step ${escapeHtml(s.status)}">
|
||||||
|
<span class="tp-mark">${escapeHtml(mark(s.status))}</span>
|
||||||
|
<span class="tp-text">${escapeHtml(s.title)}</span>
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
return `<div class="task-progress"><div class="tp-title">进度</div><div class="tp-list">${rows}</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProgressInto(card, steps) {
|
||||||
|
let el = card.querySelector(":scope > .task-progress");
|
||||||
|
if (!Array.isArray(steps) || !steps.length) {
|
||||||
|
if (el) el.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const html = renderProgressHtml(steps);
|
||||||
|
if (el) {
|
||||||
|
el.outerHTML = html;
|
||||||
|
} else {
|
||||||
|
card.insertAdjacentHTML("beforeend", html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderMessages(msgs) {
|
function renderMessages(msgs) {
|
||||||
const wrap = $("chat-stream");
|
const wrap = $("chat-stream");
|
||||||
wrap.innerHTML = "";
|
wrap.innerHTML = "";
|
||||||
|
|
@ -482,6 +556,7 @@ function renderMessages(msgs) {
|
||||||
lastAsstModel = m.model_profile;
|
lastAsstModel = m.model_profile;
|
||||||
}
|
}
|
||||||
if (role === "tool") {
|
if (role === "tool") {
|
||||||
|
if ((p.name || "") === "task_progress") continue;
|
||||||
// 嵌进上一个 assistant 的 tool_call(简化:直接独立显示)
|
// 嵌进上一个 assistant 的 tool_call(简化:直接独立显示)
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = "msg tool";
|
card.className = "msg tool";
|
||||||
|
|
@ -516,6 +591,8 @@ function renderMessages(msgs) {
|
||||||
}
|
}
|
||||||
if (Array.isArray(p.tool_calls) && p.tool_calls.length) {
|
if (Array.isArray(p.tool_calls) && p.tool_calls.length) {
|
||||||
const wd = _workingDirName(state.taskMeta && state.taskMeta.working_dir);
|
const wd = _workingDirName(state.taskMeta && state.taskMeta.working_dir);
|
||||||
|
let progressSteps = [];
|
||||||
|
let sawProgress = false;
|
||||||
for (const tc of p.tool_calls) {
|
for (const tc of p.tool_calls) {
|
||||||
const fn = (tc.function && tc.function.name) || "?";
|
const fn = (tc.function && tc.function.name) || "?";
|
||||||
let argsObj = {};
|
let argsObj = {};
|
||||||
|
|
@ -524,6 +601,11 @@ function renderMessages(msgs) {
|
||||||
argsObj = JSON.parse((tc.function && tc.function.arguments) || "{}");
|
argsObj = JSON.parse((tc.function && tc.function.arguments) || "{}");
|
||||||
args = JSON.stringify(argsObj, null, 2);
|
args = JSON.stringify(argsObj, null, 2);
|
||||||
} catch (e) { args = (tc.function && tc.function.arguments) || ""; }
|
} catch (e) { args = (tc.function && tc.function.arguments) || ""; }
|
||||||
|
if (fn === "task_progress") {
|
||||||
|
progressSteps = applyProgressAction(progressSteps, argsObj);
|
||||||
|
sawProgress = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const label = toolActivityLabel(fn, argsObj);
|
const label = toolActivityLabel(fn, argsObj);
|
||||||
const isProducer = ARTIFACT_PRODUCING_TOOLS.has(fn);
|
const isProducer = ARTIFACT_PRODUCING_TOOLS.has(fn);
|
||||||
const rels = isProducer ? pickFresh(extractArtifactRels(args, wd)) : [];
|
const rels = isProducer ? pickFresh(extractArtifactRels(args, wd)) : [];
|
||||||
|
|
@ -532,6 +614,7 @@ function renderMessages(msgs) {
|
||||||
${renderArtifactBarHtml(rels, isProducer)}
|
${renderArtifactBarHtml(rels, isProducer)}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
if (sawProgress) html += renderProgressHtml(progressSteps);
|
||||||
}
|
}
|
||||||
card.innerHTML = html;
|
card.innerHTML = html;
|
||||||
highlightIn(card);
|
highlightIn(card);
|
||||||
|
|
@ -942,6 +1025,11 @@ function handleSseEvent(ev, asstCard, ctx) {
|
||||||
const fn = (ev.data && ev.data.name) || "?";
|
const fn = (ev.data && ev.data.name) || "?";
|
||||||
const args = (ev.data && ev.data.args) || "";
|
const args = (ev.data && ev.data.args) || "";
|
||||||
const argsStr = typeof args === "string" ? args : JSON.stringify(args, null, 2);
|
const argsStr = typeof args === "string" ? args : JSON.stringify(args, null, 2);
|
||||||
|
if (fn === "task_progress") {
|
||||||
|
ctx.progressSteps = applyProgressAction(ctx.progressSteps || [], args);
|
||||||
|
renderProgressInto(asstCard, ctx.progressSteps);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const label = toolActivityLabel(fn, args);
|
const label = toolActivityLabel(fn, args);
|
||||||
const det = document.createElement("details");
|
const det = document.createElement("details");
|
||||||
det.className = "tool-call";
|
det.className = "tool-call";
|
||||||
|
|
@ -962,6 +1050,7 @@ function handleSseEvent(ev, asstCard, ctx) {
|
||||||
const txt = (ev.data && ev.data.result) || "";
|
const txt = (ev.data && ev.data.result) || "";
|
||||||
const txtStr = typeof txt === "string" ? txt : JSON.stringify(txt, null, 2);
|
const txtStr = typeof txt === "string" ? txt : JSON.stringify(txt, null, 2);
|
||||||
const toolName = (ev.data && ev.data.name) || "";
|
const toolName = (ev.data && ev.data.name) || "";
|
||||||
|
if (toolName === "task_progress") return;
|
||||||
const banner = extractMediaBanner(toolName, txtStr);
|
const banner = extractMediaBanner(toolName, txtStr);
|
||||||
const det = document.createElement("details");
|
const det = document.createElement("details");
|
||||||
det.className = "tool-call";
|
det.className = "tool-call";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue