mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 20:32:04 +00:00
845809486a
为什么:动作确认、工具 / 代码审批、审批驳回即终止、填写 / 编辑被驳回、双盲裁决的驳回以前抛普通 NodeError,运行记成 failed。 通知推「实例失败」,定时推「运行失败」,发布单把驳回过的试运行说成「试运行失败了」并挡住发布,记录页还给「继续运行」 (接着跑只会读到同一个驳回答复、再停一次)。人的决定被说成故障,每个消费方各写一份映射,已经漏了四处(05-已知遗留 (a)、总方案 X9)。 做了什么: - engine/context 新增 NodeRejected(NodeError),带 node_id、kind、decision(reject / expired_reject)、actor、note;六处驳回改抛它, 原话一字不变。待办超时驳回的答复带 expired,Decision.expired 据此记成 expired_reject。 - runner._drive 在通用 except 之前单独接住:status=cancelled、end_reason=rejected、error 写原话、error_node 写那一步, run.cancelled 带 reason/node_id/label/kind/decision/actor/note;关审批、回收沙箱、关流、封存走取消的那一套。手动停止 (执行中取消、放弃断点上的运行)写 end_reason=stopped,run.cancelled 带 reason=stopped。 - 继续运行:被驳回的 409 code=run_rejected「被驳回的运行不能接着跑:改好内容后重新发起」;存量 failed+驳回(失败那一步上 最近一条处理完的待办是驳回,和实例页同一份判断 resolve.legacy_rejected)也拒。RunOut 带 end_reason。 - apps/resolve.instance_state:cancelled+rejected 和存量 failed+驳回说同一句(「「付款」未确认(被驳回)」「「x」被驳回」)。 - notify_run:状态那一句取自 instance_state,被驳回的走「实例结束」、ok=False,走到「已退回」成果的不再写「已完成」; 手动停止照旧不推。顺带修 04-对外集成与触达 B1:发起人是认证身份(run.origin.actor_verified)时 users 传 [started_by], 个人渠道订阅了实例完成 / 失败终于收得到;自报署名不算。 - 定时 webhook:cancelled+rejected 按「运行结束」推「未出具(被驳回)」、写驳回的那一步,不写备注和原话、不画成功色; 手动停止仍不推。 - 发布单:试运行被驳回不算 plan.trial_failed,记 warning plan.trial_rejected「最近一次试运行在「x」被驳回、没有走完」(带白话)。 - (h) 重启时排队中的运行改成 failed 时补发 run.failed(同一句原因),时间线里有了失败原因这一行;不写结束时间、不封存,同以前。 - (f) tasks.tick / actions.tick 交回这一拍处理过的 id 列表(调度循环只记条数),测试改成只断言自己造的待办;先加了预置 一条无关到期待办的 fixture,旧的按条数断言当场失败(3 == 1),改完通过。 已封存的老运行不改写。测试:新增 test_run_rejected(五种关卡参数化、超时驳回、继续运行拒绝含存量、手动停止、补发 run.failed、 instance_state 新老形状同一句)、test_notify_run(驳回走实例结束、手动停止不推、已退回不写已完成、个人渠道 B1 两条); webhook、发布单、动作确认、双盲、审批的老断言跟上。去掉 runner 里接 NodeRejected 的分支回跑,新测试 8 条失败。
90 lines
3.6 KiB
Python
90 lines
3.6 KiB
Python
"""人工审批的回复怎么读。全引擎只此一份。
|
|
|
|
以前有四份:human 节点、agent 节点、工具节点、代码节点各读各的,口径不一。
|
|
后果不是风格问题:工具节点和代码节点对字符串一律 bool(),回复"拒绝"、"no"
|
|
都被当成批准,工具真的执行了;而 {"decision": "approve"} 反被判成拒绝。
|
|
|
|
规则只有一条:**只认明确的批准,看不懂的一律按拒绝处理。** 审批这道门放错了
|
|
的代价(危险工具被执行)远大于拦错了的代价(再批一次)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
_YES = frozenset({
|
|
"approve", "approved", "yes", "y", "true", "ok", "allow",
|
|
"同意", "通过", "批准", "允许", "可以",
|
|
})
|
|
_NO = frozenset({
|
|
"reject", "rejected", "deny", "denied", "no", "n", "false",
|
|
"拒绝", "驳回", "不同意", "不通过", "不允许", "不可以",
|
|
})
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Decision:
|
|
#: 明确批准了。审批类的关卡(工具、代码、human 的 approve 模式)只认这个。
|
|
approved: bool
|
|
#: 明确拒绝了。填内容类的关卡(human 的 input / edit 模式)只在这时才停:
|
|
#: 那两种模式的回复本身就是内容,没说"不"就是交了稿。
|
|
rejected: bool
|
|
note: str = ""
|
|
#: 人工改过的工具参数
|
|
args: dict[str, Any] | None = None
|
|
#: 人工改过的代码
|
|
code: str | None = None
|
|
#: human 节点收到的内容(input / edit 模式)
|
|
value: Any = None
|
|
#: 审批卡上点的是「始终允许」:批准这次,并且这个工具以后改由门控把关(tools/trust.py)。
|
|
#: 只认明确的 True,而且只跟着批准走
|
|
always: bool = False
|
|
#: 这次驳回是待办超时、按「超时驳回」替人做的(process/tasks.py 的 tick),不是有人点了驳回。只跟着驳回走,
|
|
#: 节点据此在 NodeRejected 上记 decision=expired_reject
|
|
expired: bool = False
|
|
|
|
|
|
def _word(value: Any) -> str:
|
|
return value.strip().lower() if isinstance(value, str) else ""
|
|
|
|
|
|
def _verdict(value: Any) -> bool | None:
|
|
"""True / False / None(没表态)。"""
|
|
if isinstance(value, bool):
|
|
return value
|
|
word = _word(value)
|
|
if word in _YES:
|
|
return True
|
|
if word in _NO:
|
|
return False
|
|
return None
|
|
|
|
|
|
def read_decision(response: Any) -> Decision:
|
|
if isinstance(response, dict):
|
|
verdict = _verdict(response["approved"]) if "approved" in response \
|
|
else _verdict(response.get("decision"))
|
|
args = response.get("args")
|
|
code = response.get("code")
|
|
return Decision(
|
|
approved=verdict is True,
|
|
rejected=verdict is False,
|
|
note=str(response.get("note") or response.get("comment") or ""),
|
|
args=args if isinstance(args, dict) else None,
|
|
code=code if isinstance(code, str) and code else None,
|
|
value=response.get("value"),
|
|
always=verdict is True and response.get("always") is True,
|
|
expired=verdict is False and response.get("expired") is True,
|
|
)
|
|
verdict = _verdict(response)
|
|
# 不是一个是/否词的字符串("拒绝,这个表是生产库")既算不上批准,本身又
|
|
# 是理由——留作备注,agent 转告模型时才不会只剩一句"未说明"
|
|
note = response.strip() if isinstance(response, str) and verdict is None else ""
|
|
return Decision(
|
|
approved=verdict is True,
|
|
rejected=verdict is False,
|
|
note=note,
|
|
# 字符串回复本身就是内容:input 模式下用户可能直接回一句话
|
|
value=response,
|
|
)
|