任务列表 task-list 标记 + 首轮项目上下文注入

This commit is contained in:
host committed 2026-10-30 05:11:06 +08:00
1 parent 73288f08db
commit 2fd73c1121
5 files changed
+510 -13

No files matched your search

+75 -3
View File
@@ -1,6 +1,6 @@
"""任务内标记工具(marker tools):task-create / task-poll / task-read /
wait-threads / task-cancel / task-retry / todo-list / todo-create /
todo-update。
todo-update / task-list。
设计(与 peer_messages、task_spawn 同款原则,不另起一套):
@@ -52,12 +52,16 @@ todo-update。
查已完成项须显式传 status;写操作只在本项目内生效,
跨项目待办一律按“不存在”拒绝;删除不走标记块(硬删除
只在控制台 / REST DELETE 里做,避免模型误删整条)。
- task-list:列出本项目任务(新建倒序),让模型在不知道
任务 id 时也能分诊——查运行中与失败的任务、捡失败任务
重试。不传 status 时查全部状态并附各状态计数;只读,
跨项目任务不出现,报告最多列 limit 条(默认 20,上限 50)。
上限口径(本模块最终定的值,文档与代码一致):
- 每轮创建、轮询、取消、重试各最多 5 块;todo-list、
todo-create、todo-update 各最多 5 块;wait 每轮最多
2 块,每块最多 8 个目标。8 这个数是刻意与服务端
todo-create、todo-update、task-list 各最多 5 块;wait
每轮最多 2 块,每块最多 8 个目标。8 这个数是刻意与服务端
POST /tasks/{id}/wait 的“一次最多 8 个线程”对齐的;
spawn 分支原先的每块 10 个在此收紧到 8 个,不要再用 10。
- 超时口径与服务端 /wait 不同,且是故意的:服务端 /wait
@@ -122,6 +126,11 @@ MAX_TODO_UPDATES_PER_TURN = 5
MAX_TODO_TITLE_CHARS = 500
MAX_TODO_DESCRIPTION_CHARS = 20000
MAX_TODO_QUERY_CHARS = 500
TASK_STATUSES = ("queued", "running", "completed", "failed",
"interrupted", "merged", "merge-failed")
MAX_TASK_LISTS_PER_TURN = 5
DEFAULT_TASK_LIST_LIMIT = 20
MAX_TASK_LIST_LIMIT = 50
_CREATE_RE = re.compile(r"```task-create\s*\n(.*?)\n```", re.DOTALL)
_POLL_RE = re.compile(
@@ -139,6 +148,8 @@ _TODO_CREATE_RE = re.compile(
r"```(todo-create|todo_create)\s*\n(.*?)\n```", re.DOTALL)
_TODO_UPDATE_RE = re.compile(
r"```(todo-update|todo_update)\s*\n(.*?)\n```", re.DOTALL)
_TASK_LIST_RE = re.compile(
r"```(task-list|task_list)\s*\n(.*?)\n```", re.DOTALL)
def row_get(row, name):
@@ -384,6 +395,45 @@ def extract_todo_updates(text):
return out
def extract_task_lists(text):
"""提取 task-list 块;不传 status 时查全部状态(与 todo-list
默认只查未完成不同——任务分诊首先要看到失败与运行中的,
全部状态的计数与截断报告才是可用的概览)。q 是 prompt
子串,limit 钳制 1-50(默认 20);status 非法、q 非字符
串或超长、limit 非整数时整块跳过;每轮最多 5 块。"""
if not text:
return []
out = []
for match in _TASK_LIST_RE.finditer(str(text)):
if len(out) >= MAX_TASK_LISTS_PER_TURN:
break
try:
payload = json.loads(match.group(2))
except (TypeError, ValueError):
continue
if not isinstance(payload, dict):
continue
status = payload.get("status")
if status is not None and status not in TASK_STATUSES:
continue
q = payload.get("q")
if q is not None:
if not isinstance(q, str) or not q.strip():
continue
q = q.strip()
if len(q) > MAX_TODO_QUERY_CHARS:
continue
limit = payload.get("limit")
if limit is None:
limit = DEFAULT_TASK_LIST_LIMIT
elif isinstance(limit, bool) or not isinstance(limit, int):
continue
else:
limit = max(1, min(MAX_TASK_LIST_LIMIT, limit))
out.append({"status": status, "q": q, "limit": limit})
return out
_SPAWN_SUBJECT_ZH = ("新建的任务", "派生任务", "新建任务",
"子任务", "子代理", "新任务", "新会话")
_SPAWN_SUBJECT_EN = ("child tasks", "spawned tasks", "new tasks",
@@ -866,3 +916,25 @@ def format_todo(todo, max_title=100):
if task_id:
parts.append("task_id=%s" % task_id)
return " ".join(parts)
def format_task(task, max_prompt=80):
"""任务单行(task-list 报告用):状态 + id + 创建人 + prompt
截断;prompt 里的换行压成空格,避免一行任务撑开多行报告;
创建时间有值时附 UTC 时间。"""
prompt = str(task.get("prompt") or "").replace("\n", " ").strip()
if len(prompt) > max_prompt:
prompt = prompt[:max_prompt] + "…"
line = ("• [%s] %s (by=%s)"
% (task.get("status"), task.get("id"),
task.get("created_by") or ""))
created_at = task.get("created_at")
if created_at:
try:
line += " created=%s" % time.strftime(
"%Y-%m-%dT%H:%M:%SZ", time.gmtime(float(created_at)))
except (TypeError, ValueError, OverflowError):
pass
if prompt:
line += " " + prompt
return line