成本配额与文件只读标记:task-cost/quota-status/task-files
This commit is contained in:
1 parent
2fd73c1121
commit
9d6430a4d0
5 files changed
+711
-14
No files matched your search
+121
-2
@@ -1,6 +1,6 @@
|
||||
"""任务内标记工具(marker tools):task-create / task-poll / task-read /
|
||||
wait-threads / task-cancel / task-retry / todo-list / todo-create /
|
||||
todo-update / task-list。
|
||||
todo-update / task-list / task-cost / quota-status / task-files。
|
||||
|
||||
设计(与 peer_messages、task_spawn 同款原则,不另起一套):
|
||||
|
||||
@@ -56,11 +56,24 @@ todo-update / task-list。
|
||||
任务 id 时也能分诊——查运行中与失败的任务、捡失败任务
|
||||
重试。不传 status 时查全部状态并附各状态计数;只读,
|
||||
跨项目任务不出现,报告最多列 limit 条(默认 20,上限 50)。
|
||||
- task-cost / quota-status:只读成本与配额。task-cost 查单个
|
||||
任务的成本(美元只计有价行,无价 token 单列);quota-status
|
||||
查发送方任务创建人当月的配额、已用与无价 token——派生子
|
||||
任务前先看配额,避免配额门在执行时才拦住。两者都不接受
|
||||
查别人的参数:quota 永远按发送方创建人算,task-cost 只
|
||||
放行派生链内或同创建人的目标(比服务端只要求 read 更严,
|
||||
防止在项目内猜 id 看别人的花费)。
|
||||
- task-files:读派生链内或同创建人任务的 worktree 文件。
|
||||
不传 path 时列顶层,传相对路径读文件(上限 20000 字,
|
||||
二进制文件拒绝);绝对路径、.. 逃逸、符号链接逃逸一律
|
||||
拒绝;cloud 任务与 worktree 在别机的任务拒绝(文件不在
|
||||
本机);目标没有本地 worktree 时按 404 报告,不编内容。
|
||||
|
||||
上限口径(本模块最终定的值,文档与代码一致):
|
||||
|
||||
- 每轮创建、轮询、取消、重试各最多 5 块;todo-list、
|
||||
todo-create、todo-update、task-list 各最多 5 块;wait
|
||||
todo-create、todo-update、task-list、task-cost、
|
||||
quota-status、task-files 各最多 5 块;wait
|
||||
每轮最多 2 块,每块最多 8 个目标。8 这个数是刻意与服务端
|
||||
POST /tasks/{id}/wait 的“一次最多 8 个线程”对齐的;
|
||||
spawn 分支原先的每块 10 个在此收紧到 8 个,不要再用 10。
|
||||
@@ -94,6 +107,7 @@ import。DB 助手函数只用 ? 占位符,与 peer_messages 同款写法。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
@@ -131,6 +145,12 @@ TASK_STATUSES = ("queued", "running", "completed", "failed",
|
||||
MAX_TASK_LISTS_PER_TURN = 5
|
||||
DEFAULT_TASK_LIST_LIMIT = 20
|
||||
MAX_TASK_LIST_LIMIT = 50
|
||||
MAX_TASK_COSTS_PER_TURN = 5
|
||||
MAX_QUOTA_PER_TURN = 5
|
||||
MAX_TASK_FILES_PER_TURN = 5
|
||||
MAX_TASK_FILE_CHARS = 20000
|
||||
MAX_TASK_FILE_LIST = 50
|
||||
MAX_TASK_FILE_PATH_CHARS = 1000
|
||||
|
||||
_CREATE_RE = re.compile(r"```task-create\s*\n(.*?)\n```", re.DOTALL)
|
||||
_POLL_RE = re.compile(
|
||||
@@ -150,6 +170,12 @@ _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)
|
||||
_TASK_COST_RE = re.compile(
|
||||
r"```(task-cost|task_cost)\s*\n(.*?)\n```", re.DOTALL)
|
||||
_QUOTA_RE = re.compile(
|
||||
r"```(quota-status|quota_status)\s*\n(.*?)\n```", re.DOTALL)
|
||||
_TASK_FILES_RE = re.compile(
|
||||
r"```(task-files|task_files)\s*\n(.*?)\n```", re.DOTALL)
|
||||
|
||||
|
||||
def row_get(row, name):
|
||||
@@ -434,6 +460,83 @@ def extract_task_lists(text):
|
||||
return out
|
||||
|
||||
|
||||
def extract_task_costs(text):
|
||||
"""提取 task-cost 块:{\"task_id\": \"...\"},与 task-cancel
|
||||
同款别名口径(task_id / to / to_task_id / thread_id);
|
||||
每轮最多 5 块。"""
|
||||
return _extract_controls(text, _TASK_COST_RE)
|
||||
|
||||
|
||||
def extract_quota_status(text):
|
||||
"""提取 quota-status 块:块体必须是 JSON 对象(可为空对象),
|
||||
不接受任何参数——配额永远按发送方任务的创建人算,块里写
|
||||
别人 id 也无效,避免用标记块查别人的配额;每轮最多 5 块
|
||||
(查询便宜,但多块只是重复报告)。"""
|
||||
if not text:
|
||||
return []
|
||||
out = []
|
||||
for match in _QUOTA_RE.finditer(str(text)):
|
||||
if len(out) >= MAX_QUOTA_PER_TURN:
|
||||
break
|
||||
try:
|
||||
payload = json.loads(match.group(2))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if not isinstance(payload, dict):
|
||||
continue
|
||||
out.append({})
|
||||
return out
|
||||
|
||||
|
||||
def extract_task_files(text):
|
||||
"""提取 task-files 块:task_id 必填,path 可选(默认列顶层);
|
||||
path 非字符串或超长时整块跳过;绝对路径与 .. 逃逸不在此
|
||||
层跳过——worker 的 safe_worktree_path 会拒绝并在报告里写
|
||||
清原因,让模型知道错在哪;每轮最多 5 块。"""
|
||||
if not text:
|
||||
return []
|
||||
out = []
|
||||
for match in _TASK_FILES_RE.finditer(str(text)):
|
||||
if len(out) >= MAX_TASK_FILES_PER_TURN:
|
||||
break
|
||||
try:
|
||||
payload = json.loads(match.group(2))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if not isinstance(payload, dict):
|
||||
continue
|
||||
task_id = payload.get("task_id")
|
||||
if not isinstance(task_id, str) or not task_id.strip():
|
||||
continue
|
||||
path = payload.get("path", "")
|
||||
if path is None:
|
||||
path = ""
|
||||
if not isinstance(path, str):
|
||||
continue
|
||||
if len(path) > MAX_TASK_FILE_PATH_CHARS:
|
||||
continue
|
||||
out.append({"task_id": task_id.strip(), "path": path})
|
||||
return out
|
||||
|
||||
|
||||
def safe_worktree_path(worktree, rel):
|
||||
"""worktree 内相对路径的安全解析:与服务端
|
||||
_safe_worktree_path 同款——绝对路径、.. 逃逸、符号链接
|
||||
逃逸一律返回 None;空串返回 worktree 本身。"""
|
||||
if not rel:
|
||||
return worktree
|
||||
if os.path.isabs(rel):
|
||||
return None
|
||||
base = os.path.realpath(worktree)
|
||||
target = os.path.realpath(os.path.join(base, rel))
|
||||
try:
|
||||
if os.path.commonpath([base, target]) != base:
|
||||
return None
|
||||
except ValueError:
|
||||
return None
|
||||
return target
|
||||
|
||||
|
||||
_SPAWN_SUBJECT_ZH = ("新建的任务", "派生任务", "新建任务",
|
||||
"子任务", "子代理", "新任务", "新会话")
|
||||
_SPAWN_SUBJECT_EN = ("child tasks", "spawned tasks", "new tasks",
|
||||
@@ -895,6 +998,22 @@ def marker_report_header():
|
||||
return "[任务工具报告]"
|
||||
|
||||
|
||||
def format_cost(task_id, report):
|
||||
"""成本单行:美元数只覆盖有价行,无价 actual token 单独列,
|
||||
不会被静默算成 0 美元(与 CostStore._report 同款口径)。"""
|
||||
usd = (report.get("usd_micros") or 0) / 1000000.0
|
||||
line = ("任务 %s 成本:$%.4f,总 token %d(actual %d,"
|
||||
"estimated %d),轮次 %d"
|
||||
% (task_id, usd, report.get("total_tokens", 0),
|
||||
report.get("actual_tokens", 0),
|
||||
report.get("estimated_tokens", 0),
|
||||
report.get("turns", 0)))
|
||||
unpriced = report.get("unpriced_actual_tokens", 0)
|
||||
if unpriced:
|
||||
line += ",其中无价 actual token %d 未计入美元" % unpriced
|
||||
return line
|
||||
|
||||
|
||||
def format_todo(todo, max_title=100):
|
||||
"""待办单行(报告与首轮注入共用):状态 + 标题(截断)+ id +
|
||||
优先级,due_at 有值时附 UTC 时间;标题里的换行压成空格,
|
||||
|
||||
Reference in new issue
Block a user