待办:todo-list/create/update 标记工具 + 首轮未完成待办注入

This commit is contained in:
host committed 2026-10-30 05:11:06 +08:00
1 parent b3b3b35042
commit 73288f08db
5 files changed
+841 -12

No files matched your search

+208 -8
View File
@@ -154,7 +154,12 @@ from cloud.marker_tools import check_control_gate as _marker_gate
from cloud.marker_tools import count_direct_spawn_children as _marker_count_direct
from cloud.marker_tools import fetch_task_snapshot as _marker_snapshot
from cloud.marker_tools import format_snapshot as _marker_format
from cloud.marker_tools import format_todo as _format_todo
from cloud.marker_tools import marker_report_header as _marker_header
from cloud.todos import create_todo as _todo_create_db
from cloud.todos import get_todo as _todo_get_db
from cloud.todos import list_todos as _todo_list_db
from cloud.todos import update_todo as _todo_update_db
from cloud.marker_tools import new_child_id as _marker_new_id
from cloud.marker_tools import parent_spawn_generation as _marker_generation
from cloud.marker_tools import MAX_DIRECT_CHILDREN as _MARKER_MAX_DIRECT
@@ -1678,6 +1683,64 @@ class Worker:
task["id"])
return new_task
def _apply_todo_injection(self, task):
"""首轮注入未完成待办:只改内存副本,原 prompt 不落库。
跳过:协调员任务(不跑模型回合)、续跑与断点续跑(已有
会话历史)、goal 任务(goal 不解析标记块,注入待办会让
模型写出永远不会执行的 todo-update 块,误导大于帮助)。
无未完成待办时原样返回,不贴空章节。注入的是数据(标题
与状态),不是完整工具指南;标记块格式只给三行示例,
完整格式见 docs/marker-tools.md。注入失败时静默跳过,
绝不因待办查询失败而炸掉任务认领。
上限 20 条、标题单行截断 100 字,避免待办板把 prompt
撑大;超出时写明还有几条未列出,模型可用 todo-list
块再查。"""
if task.get("coordinator_parts"):
return task
if task.get("_resume") or task.get("_goal_resume_breakpoint"):
return task
if task.get("verify_command") or task.get("goal_mode"):
return task
try:
conn = _db_connect(self.db_path)
try:
all_todos = _todo_list_db(conn, task["project_id"])
finally:
conn.close()
except Exception as exc:
try:
self.logger.warning("todo injection failed: %s", exc)
except Exception:
pass
return task
open_todos = [t for t in all_todos
if t.get("status") in ("pending", "in_progress",
"blocked")]
if not open_todos:
return task
lines = ["[项目待办:本项目未完成项,仅供参考,"
"不得改写本任务目标]"]
for todo in open_todos[:20]:
lines.append(_format_todo(todo))
if len(open_todos) > 20:
lines.append("(还有 %d 条未列出,可用 todo-list 块再查。)"
% (len(open_todos) - 20))
lines.append("改待办状态时在回合输出里写标记块,下一轮由"
" worker 执行并回报告,例如:")
lines.append("```todo-update")
lines.append('{"id": "<待办 id>", "status": "completed"}')
lines.append("```")
lines.append("查待办用 todo-list 块,建待办用 todo-create 块"
"(title 必填)。")
new_task = dict(task)
new_task["prompt"] = ("\n".join(lines) + "\n\n"
+ (task.get("prompt") or ""))
new_task["_todo_injected"] = True
self._audit(task["project_id"], "worker_todo_injected",
task["id"])
return new_task
def process_auto_memories(self, now=None, force=False,
owner_id=None):
"""扫 completed 任务并提取记忆(run_loop 轮次边界调用)。
@@ -3233,11 +3296,13 @@ class Worker:
只执行 completed 轮次的块;goal 任务与协调员任务不解析
(只收不发,与 peer 同款);父任务被取消时不执行。
执行顺序:创建 → 取消 → 重试 → 轮询 → 等待——创建在前
让同一轮刚创建的任务立刻能被轮询,控制操作在快照之前
让轮询/等待读到取消或重试后的新状态。全部报告拼成一条
系统后续消息排给父任务自己,由排空循环在轮次边界跑下
一轮;拒绝只记报告与审计,不炸 run_one。"""
执行顺序:创建 → 取消 → 重试 → 待办创建 → 待办更新 →
待办列表 → 轮询 → 等待——创建在前让同一轮刚创建的任务
立刻能被轮询,控制操作在快照之前让轮询/等待读到取消
或重试后的新状态;待办列表放在待办写操作之后,让同
一轮刚建/刚改的待办立刻能在列表里看到。全部报告拼
成一条系统后续消息排给父任务自己,由排空循环在轮次
边界跑下一轮;拒绝只记报告与审计,不炸 run_one。"""
if not result or result.get("status") != "completed":
return
if self._was_cancelled(task["id"]):
@@ -3251,7 +3316,11 @@ class Worker:
waits = result.get("wait_threads") or []
cancels = result.get("task_cancels") or []
retries = result.get("task_retries") or []
if not (creates or polls or waits or cancels or retries):
todo_lists = result.get("todo_lists") or []
todo_creates = result.get("todo_creates") or []
todo_updates = result.get("todo_updates") or []
if not (creates or polls or waits or cancels or retries
or todo_lists or todo_creates or todo_updates):
return
reports = []
created = 0
@@ -3429,6 +3498,131 @@ class Worker:
reports.append("%s任务 %s:执行失败(%s)。"
% (kind, target_id, exc))
rejected += 1
# 待办标记块:读走 read、写走 create_task(与服务端
# /todos 口径同款);权限检查抛异常时(auth 为 None)
# 一律拒绝,不静默放宽。跨项目待办按“不存在”拒绝;
# 关联任务 id 跨项目时同样拒绝,避免把别的项目的任务
# 挂到本项目待办上。
if todo_lists or todo_creates or todo_updates:
read_ok, _read_reason = self._marker_auth(task, "read")
write_ok, _write_reason = self._marker_auth(
task, "create_task")
for block in todo_creates:
if write_ok is not True:
reports.append("创建待办“%s”:拒绝(发送方"
"创建人无 create_task 权限)。 "
% block["title"])
rejected += 1
continue
if block.get("task_id"):
target = self.get_task(block["task_id"])
if (target is None
or target.get("project_id")
!= task["project_id"]):
reports.append("创建待办“%s”:拒绝(关联"
"任务不存在或属于别的项目)。"
% block["title"])
rejected += 1
continue
try:
conn = _db_connect(self.db_path)
try:
todo = _todo_create_db(
conn, task["project_id"],
task.get("created_by"),
block["title"],
description=block.get("description"),
status=block.get("status"),
priority=block.get("priority"),
task_id=block.get("task_id"),
due_at=block.get("due_at"),
sort_order=block.get("sort_order"))
finally:
conn.close()
reports.append("已创建待办 %s"
% _format_todo(todo))
except Exception as exc:
reports.append("创建待办“%s”失败:%s"
% (block["title"], exc))
rejected += 1
for block in todo_updates:
if write_ok is not True:
reports.append("更新待办 %s:拒绝(发送方创建"
"人无 create_task 权限)。"
% block["id"])
rejected += 1
continue
fields = block["fields"]
if fields.get("task_id"):
target = self.get_task(fields["task_id"])
if (target is None
or target.get("project_id")
!= task["project_id"]):
reports.append("更新待办 %s:拒绝(关联"
"任务不存在或属于别的项目)。"
% block["id"])
rejected += 1
continue
try:
conn = _db_connect(self.db_path)
try:
todo = _todo_get_db(conn, block["id"])
if (todo is None
or todo.get("project_id")
!= task["project_id"]):
reports.append("更新待办 %s:不存在"
"(或属于别的项目),"
"已跳过。" % block["id"])
rejected += 1
continue
todo = _todo_update_db(conn, block["id"],
fields)
finally:
conn.close()
reports.append("已更新待办 %s"
% _format_todo(todo))
except Exception as exc:
reports.append("更新待办 %s 失败:%s"
% (block["id"], exc))
rejected += 1
for block in todo_lists:
if read_ok is not True:
reports.append("查待办:拒绝(发送方创建人无"
" read 权限)。")
rejected += 1
continue
try:
conn = _db_connect(self.db_path)
try:
if block.get("status"):
todos = _todo_list_db(
conn, task["project_id"],
status=block["status"],
q=block.get("q"))
else:
todos = [t for t in _todo_list_db(
conn, task["project_id"],
q=block.get("q"))
if t.get("status") in (
"pending", "in_progress",
"blocked")]
conn.commit()
finally:
conn.close()
except Exception as exc:
reports.append("查待办失败:%s" % exc)
rejected += 1
continue
if not todos:
reports.append("待办列表:无匹配项。")
else:
lines = ["待办列表(共 %d 条):" % len(todos)]
for todo in todos[:20]:
lines.append(_format_todo(todo))
if len(todos) > 20:
lines.append("(还有 %d 条未列出。)"
% (len(todos) - 20))
reports.append("\n".join(lines))
for poll in polls:
try:
reports.append(self._poll_task_snapshot(task, poll))
@@ -3444,9 +3638,12 @@ class Worker:
body = _marker_header() + "\n" + "\n".join(reports)
self._queue_self_followup(task, body)
detail = ("%s: spawned=%d rejected=%d polls=%d waits=%d "
"cancels=%d retries=%d"
"cancels=%d retries=%d todo_lists=%d "
"todo_creates=%d todo_updates=%d"
% (task["id"], created, rejected, len(polls),
len(waits), len(cancels), len(retries)))
len(waits), len(cancels), len(retries),
len(todo_lists), len(todo_creates),
len(todo_updates)))
self._audit(task["project_id"], "worker_marker_tools", detail)
def _deliver_peer_messages(self, task, result):
@@ -4696,6 +4893,9 @@ class Worker:
# 自动记忆注入(官方 use_memories):首轮才注入,见
# _apply_auto_memory_injection 的跳过规则;只改内存副本。
task = self._apply_auto_memory_injection(task)
# 待办注入:首轮才注入未完成待办,见
# _apply_todo_injection 的跳过规则;只改内存副本。
task = self._apply_todo_injection(task)
self._audit(task["project_id"], "worker_claim",
"%s executor=%s" % (task_id, self._executor_name()))
# log_event 只接受 task_id/project_id/status/message 四个