feat: 命令台子代理链路(主任务/模型子代理/委派子任务 + 过程调用实时同步)

host:链路条目带 thread 归属;delegate 向父链路文件写 agent-start/done 标记。服务端:GET /tasks/{id}/agents 聚合父链路 + 子链路文件成代理链(状态口径:委派以结束标记为准、无标记即进行中;模型子代理无结束信号时父任务运行中为进行中、否则未知)。命令台:新增子代理链路区,随 SSE 流防抖刷新;扁平链路也渲染 agent 标记。
This commit is contained in:
host committed 2026-10-27 11:51:11 +08:00
1 parent 34cd0eeba7
commit c4d3ec9bff
6 files changed
+366 -14

No files matched your search

+57 -12
View File
@@ -214,6 +214,8 @@ def _summarize_trace_item(method, params):
返回 [] 表示该通知不进链路(userMessage 等)。
ts 取通知的 completedAtMs/startedAtMs(毫秒 epoch),
没有时为 None,服务端展示时按无时间处理。
thread 取通知的 threadId(模型内子代理与主任务同文件时,
服务端凭它把条目归属到不同代理;没有时为 None,归主任务)。
"""
item = (params or {}).get("item")
if not isinstance(item, dict):
@@ -221,8 +223,11 @@ def _summarize_trace_item(method, params):
item_type = item.get("type")
item_id = item.get("id") or ""
ts = params.get("completedAtMs") or params.get("startedAtMs")
thread = params.get("threadId")
if not thread and isinstance(params.get("thread"), dict):
thread = params["thread"].get("id")
if item_type == "commandExecution":
return [{"kind": "exec", "id": item_id, "ts": ts,
return [{"kind": "exec", "id": item_id, "ts": ts, "thread": thread,
"command": (item.get("command") or "")[:500],
"cwd": item.get("cwd") or "",
"exitCode": item.get("exitCode"),
@@ -235,6 +240,7 @@ def _summarize_trace_item(method, params):
path = ch.get("path") or ""
entries.append({"kind": "file",
"id": item_id + ":" + path, "ts": ts,
"thread": thread,
"path": path, "change": ch.get("kind") or "",
"status": item.get("status") or ""})
return entries
@@ -242,32 +248,60 @@ def _summarize_trace_item(method, params):
text = item.get("text") or ""
if not text:
return []
return [{"kind": "message", "id": item_id, "ts": ts,
return [{"kind": "message", "id": item_id, "ts": ts, "thread": thread,
"phase": item.get("phase") or "", "text": text[:300]}]
if item_type == "reasoning":
summary = item.get("summary") or item.get("content") or []
text = " ".join(s for s in summary if isinstance(s, str)).strip()
if not text:
return []
return [{"kind": "reasoning", "id": item_id, "ts": ts,
return [{"kind": "reasoning", "id": item_id, "ts": ts, "thread": thread,
"summary": text[:200]}]
if item_type == "mcpToolCall":
return [{"kind": "tool", "id": item_id, "ts": ts,
return [{"kind": "tool", "id": item_id, "ts": ts, "thread": thread,
"server": item.get("server") or "",
"tool": item.get("tool") or "",
"status": item.get("status") or ""}]
if item_type == "functionCallOutput":
return [{"kind": "call", "id": item_id, "ts": ts,
return [{"kind": "call", "id": item_id, "ts": ts, "thread": thread,
"name": item.get("name") or ""}]
if item_type == "plan":
text = item.get("text") or ""
if not text:
return []
return [{"kind": "plan", "id": item_id, "ts": ts,
return [{"kind": "plan", "id": item_id, "ts": ts, "thread": thread,
"text": text[:300]}]
return []
def _trace_file_path(state_path, task_name):
"""链路文件路径:state 文件同目录下的 host-traces/<清洗后名字>.jsonl。
与 Server.set_trace_file、cloud 的 _trace_file_name 同款清洗,
三边算出同一名字,delegate 的父链路标记才能落进父任务文件。
"""
safe = re.sub(r"[^A-Za-z0-9_.-]", "-", str(task_name))
return os.path.join(
os.path.dirname(os.path.abspath(state_path)),
"host-traces", safe + ".jsonl")
def _append_trace_marker(state_path, task_name, entry):
"""往另一任务的链路文件追加一条标记(delegate 向父链路报到用)。
尽力而为:目录建不出、写失败都不影响回合本身。父链路的
行数上限由属主进程自己的计数器管,这里不重复计数(标记
一轮至多两条,超限风险可忽略)。
"""
try:
path = _trace_file_path(state_path, task_name)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
except Exception:
pass
class Server:
"""单个 app-server 进程,多 thread 并发。"""
@@ -302,12 +336,8 @@ class Server:
时 trace_path 保持 None,不影响回合。
"""
try:
safe = re.sub(r"[^A-Za-z0-9_.-]", "-", str(task_name))
trace_dir = os.path.join(
os.path.dirname(os.path.abspath(state_path)),
"host-traces")
os.makedirs(trace_dir, exist_ok=True)
self.trace_path = os.path.join(trace_dir, safe + ".jsonl")
self.trace_path = _trace_file_path(state_path, task_name)
os.makedirs(os.path.dirname(self.trace_path), exist_ok=True)
self._trace_lines = 0
except Exception:
self.trace_path = None
@@ -2566,11 +2596,26 @@ def main():
"created_at": time.time(), "turns": []}
state["tasks"][args.name] = sub
save_state(args.state, state)
# 子代理链路:向父任务链路文件报到(开始 + 结束各一条),
# 服务端凭标记把子链路文件拼进父任务的代理链;子任务自己
# 的过程条目仍写它自己的链路文件(main 起动时按 --name 定)。
child_trace = (os.path.basename(srv.trace_path)
if srv.trace_path else None)
_append_trace_marker(
args.state, args.parent_task,
{"kind": "agent-start", "id": "agent:" + args.name,
"name": args.name, "prompt": (args.task or "")[:300],
"trace": child_trace, "ts": int(time.time() * 1000)})
result = run_task_turn(srv, state, sub, args.task,
args.model, args.effort, args.sandbox,
timeout=args.turn_timeout,
interrupt_file=_interrupt_request_path(args.state, args.name))
save_state(args.state, state)
_append_trace_marker(
args.state, args.parent_task,
{"kind": "agent-done", "id": "agent:" + args.name + ":done",
"name": args.name, "status": sub["status"],
"trace": child_trace, "ts": int(time.time() * 1000)})
print(json.dumps({"sub_task": args.name, "status": sub["status"],
"text": result.get("text", "")},
indent=2, ensure_ascii=False))