feat: 会话内 LLM 自判 goal / 自动化标记工具,不靠关键词
agent 在回合输出里写 goal-convert 块,worker 在轮次边界 排成 /goal 自后续消息、走已有 followup 转换路径把本会话 转成 goal;写 automation-create 块,worker 在轮次边界 创建自动化定义(权限 create_task、配额整轮门、创建人跟 父任务走、name 缺失自动生成、首次时刻已过去单块拒绝、 bind heartbeat 可绑回本会话)。 automations 新增 first_run_at / run_once:首次时刻只管 首次、之后按间隔算;只跑一次跑过后 is_due / due / claim 三处都不再到期(SQL 到期条件与 Python 口径共用同一 _DUE_SQL);API 创建与更新同步接受这两个字段。 刻意不向 LLM 自动注入工具指南:避免 prompt 臃肿,格式 只写进 docs/marker-tools.md 与 README,需要时由用户在 会话里手动告诉 agent;服务端不按关键词再猜意图。 单测 cloud/tests/test_session_goal_automation.py 12 项, 相关回归 218 项全过。
This commit is contained in:
1 parent
f1aab5415c
commit
e81ba8d2bf
8 files changed
+892
-51
No files matched your search
+123
-11
@@ -150,6 +150,8 @@ from cloud.models import ModelRouter, is_fallbackable, record_fallback
|
||||
from cloud.auto_memory import AutoMemoryStore
|
||||
from cloud.auto_memory import build_injection_prefix as _auto_prefix
|
||||
from cloud.auto_memory import process_due_tasks as _auto_process_due
|
||||
from cloud.automations import create_automation as _auto_create
|
||||
from cloud.lifecycle import bind_automation as _life_bind
|
||||
from cloud.marker_tools import ancestry_relation as _marker_ancestry
|
||||
from cloud.marker_tools import check_control_gate as _marker_gate
|
||||
from cloud.marker_tools import count_direct_spawn_children as _marker_count_direct
|
||||
@@ -3492,14 +3494,21 @@ class Worker:
|
||||
|
||||
只执行 completed 轮次的块;goal 任务与协调员任务不解析
|
||||
(只收不发,与 peer 同款);父任务被取消时不执行。
|
||||
执行顺序:创建 → 取消 → 重试 → 待办创建 → 待办更新 →
|
||||
待办列表 → 任务列表 → 任务成本 → 配额 → 任务文件 →
|
||||
轮询 → 等待——创建在前让同一轮刚创建的任务立刻能被
|
||||
轮询,控制操作在快照之前让轮询/等待读到取消或重试后
|
||||
的新状态;待办列表放在待办写操作之后,让同一轮刚建/
|
||||
刚改的待办立刻能在列表里看到。全部报告拼成一条系统
|
||||
后续消息排给父任务自己,由排空循环在轮次边界跑下
|
||||
一轮;拒绝只记报告与审计,不炸 run_one。"""
|
||||
执行顺序:创建 → 自动化创建 → 取消 → 重试 → 待办创建 →
|
||||
待办更新 → 待办列表 → 任务列表 → 任务成本 → 配额 →
|
||||
任务文件 → 轮询 → 等待——创建在前让同一轮刚创建的
|
||||
任务立刻能被轮询,控制操作在快照之前让轮询/等待读到
|
||||
取消或重试后的新状态;待办列表放在待办写操作之后,
|
||||
让同一轮刚建/刚改的待办立刻能在列表里看到。全部报告
|
||||
拼成一条系统后续消息排给父任务自己,由排空循环在轮次
|
||||
边界跑下一轮;拒绝只记报告与审计,不炸 run_one。
|
||||
goal-convert 不在这里直接转:它排成一条“/goal <目标>”
|
||||
的自后续消息,放在报告消息之后——排空先跑报告那轮
|
||||
(agent 看到自动化等工具的确认),再跑 /goal 那条,
|
||||
走已有的 followup /goal 转换路径(plan 冲突、CAS、
|
||||
终局 queued 审计都在那条路里,不在这里另起一套)。
|
||||
这样转 goal 后报告不会丢在 goal 循环外,也不会出现
|
||||
两套转换逻辑打架。"""
|
||||
if not result or result.get("status") != "completed":
|
||||
return
|
||||
if self._was_cancelled(task["id"]):
|
||||
@@ -3520,14 +3529,17 @@ class Worker:
|
||||
task_costs = result.get("task_costs") or []
|
||||
quota_status = result.get("quota_status") or []
|
||||
task_files = result.get("task_files") or []
|
||||
goal_converts = result.get("goal_converts") or []
|
||||
automation_creates = result.get("automation_creates") or []
|
||||
if not (creates or polls or waits or cancels or retries
|
||||
or todo_lists or todo_creates or todo_updates
|
||||
or task_lists or task_costs or quota_status
|
||||
or task_files):
|
||||
or task_files or goal_converts or automation_creates):
|
||||
return
|
||||
reports = []
|
||||
created = 0
|
||||
rejected = 0
|
||||
auto_created = 0
|
||||
if creates:
|
||||
quota_error = None
|
||||
try:
|
||||
@@ -3616,6 +3628,93 @@ class Worker:
|
||||
except Exception as exc:
|
||||
reports.append("创建失败:%s" % exc)
|
||||
rejected += len(creates) - created
|
||||
if automation_creates:
|
||||
# 自动化创建:权限与配额是整轮门(与 task-create 同款,
|
||||
# 配额超限时整轮跳过,不逐块半建);单块的首次时刻
|
||||
# 已过去、重名等只拒该块。创建人跟父任务走,绝不
|
||||
# 用 worker 自己的身份建——否则自动化到期产出的任务
|
||||
# 会算到错误的人头上,配额门也绕过去了。
|
||||
auth_ok, _auth_reason = self._marker_auth(
|
||||
task, "create_task")
|
||||
quota_error = None
|
||||
try:
|
||||
quota_error = self._quota_exceeded(task, None)
|
||||
except Exception as exc:
|
||||
quota_error = "quota_check_failed: %s" % exc
|
||||
if auth_ok is not True:
|
||||
reports.append(
|
||||
"自动化创建跳过:发送方创建人无 create_task "
|
||||
"权限或权限查不到,本轮 %d 个自动化块全部跳过。"
|
||||
% len(automation_creates))
|
||||
rejected += len(automation_creates)
|
||||
elif quota_error is not None:
|
||||
reports.append(
|
||||
"自动化创建跳过:父任务配额检查未通过(%s),"
|
||||
"本轮 %d 个自动化块全部跳过。"
|
||||
% (quota_error, len(automation_creates)))
|
||||
rejected += len(automation_creates)
|
||||
else:
|
||||
now = time.time()
|
||||
for idx, block in enumerate(automation_creates):
|
||||
first = block.get("first_run_at")
|
||||
if first is not None and first < now - 60:
|
||||
reports.append(
|
||||
"自动化创建跳过:首次运行时刻已过去"
|
||||
"(%s),请给一个未来时刻再写块。"
|
||||
% block.get("prompt", "")[:40])
|
||||
rejected += 1
|
||||
continue
|
||||
name = block.get("name") or (
|
||||
"auto-%s-%d-%d"
|
||||
% (task["id"][:8], int(now), idx))
|
||||
try:
|
||||
conn = _db_connect(self.db_path)
|
||||
try:
|
||||
auto = _auto_create(
|
||||
conn, task["project_id"], name,
|
||||
block["prompt"], task.get("created_by"),
|
||||
block["interval_seconds"],
|
||||
block.get("repo_url"), now=now,
|
||||
first_run_at=first,
|
||||
run_once=block.get("once", False))
|
||||
bind = block.get("bind")
|
||||
if bind:
|
||||
try:
|
||||
_life_bind(
|
||||
conn, auto["id"],
|
||||
task["project_id"], task["id"],
|
||||
bind, now=now)
|
||||
except Exception as exc:
|
||||
reports.append(
|
||||
"已创建自动化 %s(%s),但绑定"
|
||||
"到本会话失败(%s),定义本身"
|
||||
"仍有效,到期独立跑。"
|
||||
% (auto["id"], name, exc))
|
||||
auto_created += 1
|
||||
continue
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as exc:
|
||||
reports.append(
|
||||
"自动化创建失败(%s):%s。"
|
||||
% (block.get("prompt", "")[:40], exc))
|
||||
rejected += 1
|
||||
continue
|
||||
auto_created += 1
|
||||
nxt = auto.get("next_run_at")
|
||||
nxt_text = (
|
||||
time.strftime(
|
||||
"%Y-%m-%d %H:%M", time.localtime(nxt))
|
||||
if nxt else "不再到期")
|
||||
reports.append(
|
||||
"已创建自动化 %s(%s),间隔 %d 秒,"
|
||||
"下次 %s%s,到期后由执行器经 claim 抢单"
|
||||
"建任务%s。"
|
||||
% (auto["id"], name,
|
||||
block["interval_seconds"], nxt_text,
|
||||
"(只跑一次)" if block.get("once") else "",
|
||||
",已绑到本会话唤醒"
|
||||
if block.get("bind") == "heartbeat" else ""))
|
||||
for kind, blocks, action in (("取消", cancels, "create_task"),
|
||||
("重试", retries, "approve_merge")):
|
||||
for block in blocks:
|
||||
@@ -4071,16 +4170,26 @@ class Worker:
|
||||
if reports:
|
||||
body = _marker_header() + "\n" + "\n".join(reports)
|
||||
self._queue_self_followup(task, body)
|
||||
if goal_converts:
|
||||
# 顺序关键:报告消息先排、/goal 消息后排,排空先跑
|
||||
# 报告那轮、再转 goal;反过来排会先转 goal,报告
|
||||
# 就送不到 goal 循环里了。plan 冲突等拒绝由排空的
|
||||
# /goal 路径处理(标 failed 停排空),这里不预判。
|
||||
goal_target = goal_converts[0]["goal"]
|
||||
self._queue_self_followup(
|
||||
task, "/goal %s" % goal_target)
|
||||
detail = ("%s: spawned=%d rejected=%d polls=%d waits=%d "
|
||||
"cancels=%d retries=%d todo_lists=%d "
|
||||
"todo_creates=%d todo_updates=%d task_lists=%d "
|
||||
"task_costs=%d quota=%d task_files=%d"
|
||||
"task_costs=%d quota=%d task_files=%d "
|
||||
"automations=%d goal_converts=%d"
|
||||
% (task["id"], created, rejected, len(polls),
|
||||
len(waits), len(cancels), len(retries),
|
||||
len(todo_lists), len(todo_creates),
|
||||
len(todo_updates), len(task_lists),
|
||||
len(task_costs), len(quota_status),
|
||||
len(task_files)))
|
||||
len(task_files), auto_created,
|
||||
len(goal_converts)))
|
||||
self._audit(task["project_id"], "worker_marker_tools", detail)
|
||||
|
||||
def _deliver_peer_messages(self, task, result):
|
||||
@@ -5769,6 +5878,9 @@ class Worker:
|
||||
if fu.get("sender_task_id"):
|
||||
fu_prompt = _peer_prefix(fu["sender_task_id"]) + fu_prompt
|
||||
fu_task["prompt"] = fu_prompt
|
||||
# 后续消息轮不重复加指南:首轮指南已在同一
|
||||
# thread 历史里,后续轮靠历史想起工具;每轮
|
||||
# 都加会让指南在历史里越积越多。
|
||||
fu_result, fu_error = self._run_attempt(
|
||||
fu_task, quota_check=_quota_hit)
|
||||
usage_total = sum_usage(
|
||||
|
||||
Reference in new issue
Block a user