feat(tier2): 软停后 finish 逼近递进升压(工单 c)——连续 N 轮不 finish 经 on_acting 注入升压收尾指令逼软着陆
软停(budget_soft_tripped)已有 on_system_prompt 强提醒 + on_acting 拦生成类工具,但那是被动系统提示, M3 可能烧推理不理会(80011 实录:软停触发后仍烧到 step_cap/硬地板才停)。本次在共享 CircuitBreakerMiddleware 加「finish 逼近升压」:on_reply 用现有事件计数面数软停后连续无-finish 的 acting 轮(ToolCallStartEvent +1; 一推理轮无 tool call=finish 尝试则轮边界复位),达 N=3 轮升第一档(提醒收尾)、达 2N=6 轮升第二档(强制立即 finish、禁新增大额生成);升压指令经 on_acting 注入工具结果(拦截语并入 / 收尾类结果尾部附加),跨档只注一次。 只改观察/注入路径与状态跟踪:不动任何熔断阈值 / finish 语义 / 九门;无新增配置键(_softstop_rounds 派生); _softstop_rounds=0 时 on_acting 与升压前逐字节等价(兼容既有 cheap 共享面)。cheap_budget 同源共享,不改 cheap 文件。 新增 tests/test_softstop_escalation.py(5 用例):升压逐档升级 / 透传附加 / finish 尝试复位 / 不足 N 轮零升压 / 硬地板仍兜底。 验证:tier2/gen-worker 102 passed;cheap-worker 共享软停面 29 passed(test_budget_two_stage 等)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2e4dd56b76
commit
eb135a9041
214
tier2/gen-worker/tests/test_softstop_escalation.py
Normal file
214
tier2/gen-worker/tests/test_softstop_escalation.py
Normal file
@ -0,0 +1,214 @@
|
||||
"""软停后「finish 逼近」递进升压单测(工单 c)。
|
||||
|
||||
守的行为:软停(budget_soft_tripped)后,agent 连续 N 轮(SOFT_STOP_ESCALATE_ROUNDS=3)仍在动手而非
|
||||
finish 时,on_acting 主动把递进升压的收尾指令注入工具结果——第一档提醒收尾、第二档强制立即 finish 且禁
|
||||
新增大额生成——让软停线软着陆,而非像 80011 实录那样软停后仍烧到 step_cap/硬地板才停。
|
||||
|
||||
计数面复用现有 on_reply 事件计数(ModelCallStartEvent 起一轮 / ToolCallStartEvent = 本轮动手);
|
||||
一推理轮无 tool call = finish 尝试,轮边界复位升压计数。硬地板等失控保护阈值不受升压影响(升压是加法)。
|
||||
|
||||
跑:PYTHONPATH=tier2/gen-worker cheap-worker/.venv/bin/python -m pytest tier2/gen-worker/tests/test_softstop_escalation.py -v
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from agentscope.message import TextBlock, ToolResultState
|
||||
from agentscope.tool import ToolResponse
|
||||
|
||||
from worker.middleware import (
|
||||
CircuitBreakerMiddleware, Tier2CircuitBreak, SOFT_STOP_ESCALATE_ROUNDS,
|
||||
)
|
||||
|
||||
_N = SOFT_STOP_ESCALATE_ROUNDS # 3
|
||||
|
||||
|
||||
# ── 假事件:on_reply 按 type(evt).__name__ 分派,类名必须与 AgentScope 事件同名 ──
|
||||
class ModelCallStartEvent:
|
||||
"""一次模型推理(=一轮)的起点。"""
|
||||
|
||||
|
||||
class ToolCallStartEvent:
|
||||
"""本轮产了 tool call(= 未 finish);推理相位产出,早于 on_acting。"""
|
||||
|
||||
def __init__(self, tcid: str = "tc", name: str = "build") -> None:
|
||||
self.tool_call_id = tcid
|
||||
self.tool_call_name = name
|
||||
|
||||
|
||||
class ToolResultEndEvent:
|
||||
"""工具结果终态(success 不触 stuck;用于把一轮凑完整)。"""
|
||||
|
||||
def __init__(self, tcid: str = "tc", state: str = "success") -> None:
|
||||
self.tool_call_id = tcid
|
||||
self.state = state
|
||||
|
||||
|
||||
class _FakeModel:
|
||||
model = "MiniMax-M3"
|
||||
|
||||
|
||||
class _ToolCall:
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
self.id = "id-" + name
|
||||
|
||||
|
||||
async def _passthrough(**_kwargs):
|
||||
class _Resp:
|
||||
usage = None # 无 usage → 不折算,不干扰软停/升压断言
|
||||
return _Resp()
|
||||
|
||||
|
||||
async def _sentinel_handler(**_kwargs):
|
||||
yield "TOOL_RAN" # 透传哨兵(字符串,非 ToolResponse → 不会被升压附加)
|
||||
|
||||
|
||||
async def _toolresponse_handler(**_kwargs):
|
||||
# 真实工具终态 ToolResponse(升压走「附加到工具结果尾部」路时用它验证附加)。
|
||||
yield ToolResponse(content=[TextBlock(text="原始工具输出")], state=ToolResultState.SUCCESS)
|
||||
|
||||
|
||||
def _soft_tripped_breaker(**kw) -> CircuitBreakerMiddleware:
|
||||
"""已软停的 soft 档 breaker;其余闸放宽到打不着,专注测升压(pricing_params 传入避免测试内打网关取价)。"""
|
||||
b = CircuitBreakerMiddleware(
|
||||
soft_budget=True, rmb_hard_limit=10.0, # 软停线 ¥10 → 硬地板 ¥15(×1.5)
|
||||
max_tool_calls=999, max_model_calls=999,
|
||||
wall_timeout_s=9999.0, step_timeout_s=9999.0,
|
||||
pricing_params={"pricing": {}, "qpu": 1, "usd_rate": 0},
|
||||
**kw,
|
||||
)
|
||||
b._rmb_gate_active = True
|
||||
b.budget_soft_tripped = True # 直接置软停态(等价越软停线后);本组用例专注升压逻辑
|
||||
return b
|
||||
|
||||
|
||||
def _drive(b: CircuitBreakerMiddleware, events: list) -> None:
|
||||
"""把一串假事件喂过 on_reply(消费完毕),驱动其轮计数/复位。"""
|
||||
async def handler(**_kwargs):
|
||||
for e in events:
|
||||
yield e
|
||||
|
||||
async def run():
|
||||
async for _ in b.on_reply(None, {}, handler):
|
||||
pass
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def _drive_tool_rounds(b: CircuitBreakerMiddleware, n: int) -> None:
|
||||
"""驱动 n 个「动手轮」(每轮 = 模型推理 + 一次 tool call + 成功结果),软停后每轮使 _softstop_rounds +1。"""
|
||||
events: list = []
|
||||
for i in range(n):
|
||||
tcid = f"tc-{id(events)}-{i}"
|
||||
events += [ModelCallStartEvent(), ToolCallStartEvent(tcid, "build"),
|
||||
ToolResultEndEvent(tcid, "success")]
|
||||
_drive(b, events)
|
||||
|
||||
|
||||
def _drive_finish_attempt(b: CircuitBreakerMiddleware) -> None:
|
||||
"""驱动一次「finish 尝试」:一推理轮全程无 tool call,再起下一轮 ModelCallStart 触发轮边界结算 → 复位。"""
|
||||
_drive(b, [ModelCallStartEvent(), ModelCallStartEvent()])
|
||||
|
||||
|
||||
def _acting_collect(b: CircuitBreakerMiddleware, name: str, handler=None) -> list:
|
||||
"""同步跑一次 on_acting,返回产出列表。"""
|
||||
async def run():
|
||||
out = []
|
||||
async for item in b.on_acting(None, {"tool_call": _ToolCall(name)},
|
||||
handler or _sentinel_handler):
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
return asyncio.run(run())
|
||||
|
||||
|
||||
def _acting_text(b: CircuitBreakerMiddleware, name: str, handler=None) -> str:
|
||||
"""取 on_acting 产出的文本(拦截路 = ERROR ToolResponse 文本;透传路 = ToolResponse/哨兵串拼接)。"""
|
||||
out = _acting_collect(b, name, handler)
|
||||
for item in out:
|
||||
if isinstance(item, ToolResponse):
|
||||
return "".join(getattr(bk, "text", "") for bk in item.content)
|
||||
return "".join(x for x in out if isinstance(x, str))
|
||||
|
||||
|
||||
# ── 门①:软停后连续 N 轮无 finish → 升压文案注入且逐档升级 ──
|
||||
def test_escalation_injects_and_upgrades_after_n_rounds():
|
||||
b = _soft_tripped_breaker()
|
||||
# 连续 N 轮动手不 finish → 第一档(提醒尽快收尾)。
|
||||
_drive_tool_rounds(b, _N)
|
||||
assert b._softstop_rounds == _N, "软停后 N 个动手轮应累计 N"
|
||||
out1 = _acting_text(b, "write_file") # 被拦生成类:文本 = 拦截 base + 第一档升压
|
||||
assert "软停线" in out1 and "finish" in out1, "应保留原拦截语义"
|
||||
assert "尽快" in out1 and "收尾" in out1, "第一档应注入『提醒收尾』"
|
||||
assert "禁止" not in out1, "第一档还不到『禁新增生成』的强度"
|
||||
assert b._softstop_level == 1
|
||||
|
||||
# 再连续 N 轮仍不 finish(累计 2N)→ 升到第二档(强制立即 finish、禁新增生成)。
|
||||
_drive_tool_rounds(b, _N)
|
||||
assert b._softstop_rounds == 2 * _N
|
||||
out2 = _acting_text(b, "write_file")
|
||||
assert "立即" in out2 and "禁止" in out2 and "新增" in out2, "第二档应强制立即 finish、禁新增大额生成"
|
||||
assert b._softstop_level == 2
|
||||
assert out1 != out2, "两档文案应逐档升级(第二档更硬)"
|
||||
|
||||
|
||||
# ── 门①补充:收尾类工具透传时,升压指令附加到其真实工具结果尾部(on_acting 注入路验证)──
|
||||
def test_escalation_augments_passthrough_tool_result():
|
||||
b = _soft_tripped_breaker()
|
||||
_drive_tool_rounds(b, _N) # 跨第一档
|
||||
out = _acting_collect(b, "read_file", _toolresponse_handler) # read_file 非生成类 → 放行
|
||||
resp = [x for x in out if isinstance(x, ToolResponse)][-1]
|
||||
joined = "".join(getattr(bk, "text", "") for bk in resp.content)
|
||||
assert "原始工具输出" in joined, "原工具结果必须保留"
|
||||
assert "尽快" in joined and "收尾" in joined, "升档瞬间应把第一档升压指令附加到工具结果尾部"
|
||||
|
||||
|
||||
# ── 门②:期间出现 finish 尝试 → 不注入 / 复位 ──
|
||||
def test_finish_attempt_resets_escalation():
|
||||
b = _soft_tripped_breaker()
|
||||
_drive_tool_rounds(b, _N) # rounds=N
|
||||
_acting_text(b, "write_file") # 触第一档,level=1
|
||||
assert b._softstop_rounds == _N and b._softstop_level == 1
|
||||
|
||||
# 一推理轮无 tool call = finish 尝试 → 轮边界复位升压计数与档位。
|
||||
_drive_finish_attempt(b)
|
||||
assert b._softstop_rounds == 0, "finish 尝试后应复位轮计数"
|
||||
assert b._softstop_level == 0, "finish 尝试后应复位升压档"
|
||||
|
||||
# 复位后不足 N 轮 → on_acting 不再注入升压(回到升压前:只拦生成类、不升压)。
|
||||
# 注:拦截 base 文案本身含「立即调用 finish」,故用升压独有标记判断——「尽快」仅第一档、「禁止」仅第二档。
|
||||
out = _acting_text(b, "write_file")
|
||||
assert "软停线" in out and "finish" in out, "拦截语义仍在"
|
||||
assert "尽快" not in out and "禁止" not in out, "复位后未达 N 轮不应再注入升压文案"
|
||||
assert b._softstop_level == 0
|
||||
|
||||
|
||||
# ── 边界 / 兼容:软停后不足 N 轮 → 零升压(与升压前逐字节等价)──
|
||||
def test_no_escalation_below_n_rounds_backward_compat():
|
||||
b = _soft_tripped_breaker()
|
||||
_drive_tool_rounds(b, _N - 1) # rounds=2 < 3
|
||||
assert b._softstop_rounds == _N - 1
|
||||
# 生成类:仍拦截,文本 = base(无升压)。用升压独有标记判断(base 本身含「立即调用 finish」):
|
||||
# 「尽快」仅第一档、「禁止」仅第二档,两者皆不在 base。
|
||||
txt = _acting_text(b, "write_file")
|
||||
assert "软停线" in txt and "finish" in txt
|
||||
assert "尽快" not in txt and "禁止" not in txt, "未达 N 轮不注入升压"
|
||||
assert b._softstop_level == 0
|
||||
# 收尾类:透传哨兵原样(未附加)。
|
||||
assert _acting_collect(b, "read_file", _sentinel_handler) == ["TOOL_RAN"]
|
||||
|
||||
|
||||
# ── 门③:硬地板行为不变,升压不禁用失控保护(仍 fail-closed 兜底)──
|
||||
def test_hard_floor_still_fail_closed_under_escalation():
|
||||
b = _soft_tripped_breaker() # rmb_hard_limit=10 → 硬地板 15
|
||||
_drive_tool_rounds(b, 2 * _N) # 升到第二档
|
||||
_acting_text(b, "write_file")
|
||||
assert b._softstop_level == 2, "前置:已升到第二档"
|
||||
|
||||
# 已花越硬地板(¥15)→ 下一次模型调用仍必 fail-closed 抛熔断(升压不改熔断判定阈值)。
|
||||
b.spent_rmb = 20.0
|
||||
with pytest.raises(Tier2CircuitBreak) as ei:
|
||||
asyncio.run(b.on_model_call(None, {"current_model": _FakeModel()}, _passthrough))
|
||||
assert ei.value.kind == "budget", "硬地板越线必须 fail-closed(budget)"
|
||||
assert "硬地板" in ei.value.reason
|
||||
@ -177,6 +177,27 @@ DEFAULT_RMB_HARD_LIMIT = genconfig.get("budget", "rmb_hard_limit", 50.0) # 富
|
||||
# finish 是收尾链本体,均放行。
|
||||
DEFAULT_SOFT_STOP_BLOCKED_TOOLS = frozenset({"write_file", "write_source", "scaffold_init"})
|
||||
|
||||
# ── 软停后「finish 逼近」递进升压(工单 c)──
|
||||
# 软停(budget_soft_tripped)已由 on_system_prompt 注入「立即 finish」强提醒 + on_acting 拦新增生成类工具,
|
||||
# 但这是被动系统提示;A1 T8 实测 M3 可能烧推理不理会,80011 生产实录也是「软停触发后仍烧到 step_cap/硬地板才停」。
|
||||
# 故软停后再数「连续多少轮仍不 finish」,达阈值即经 on_acting 主动把递进升压的收尾指令注进工具结果,逼软着陆。
|
||||
# N=3 的依据:软停后一次合法收尾序列很短——「读一眼当前工程 → 跑一次门/构建自查 → finish」约 2~3 个工具调用;
|
||||
# 故给 3 轮宽限覆盖正常收尾,第 3 轮后仍在动手(而非 finish)就升压。3 远小于 step_cap(genconfig 现值 100 步),
|
||||
# 能在烧穿硬地板/步顶前把 agent 逼向 finish,又不误伤正常的最后自查。第二档在 2×N=6 轮触发(更硬)。
|
||||
SOFT_STOP_ESCALATE_ROUNDS = 3
|
||||
|
||||
# 两档收尾文案(第一档提醒收尾、第二档强制立即 finish 且禁新增大额生成;经 on_acting 注入进工具结果/拦截语)。
|
||||
# 索引 = 升压档位:0=不升压(占位 None)、1=第一档、2=第二档。文案措辞逐档加硬,便于 agent(与测试)区分档位。
|
||||
_SOFT_STOP_ESCALATE_L1 = (
|
||||
"提醒:本次生成的 ¥ 预算已过软停线,且已连续多轮未收尾。请尽快基于当前工程的最好状态收敛、"
|
||||
"补齐能过门的最小可玩闭环后调用 finish,不要再展开新的探索或大改。"
|
||||
)
|
||||
_SOFT_STOP_ESCALATE_L2 = (
|
||||
"预算已严重逼近上限:请【立即】调用 finish 交付当前工程,禁止再新增任何大额生成调用(写源码 / 重开工程)。"
|
||||
"再不收尾将触发硬熔断、本次作废——现在就 finish。"
|
||||
)
|
||||
_SOFT_STOP_ESCALATE_TEXTS = (None, _SOFT_STOP_ESCALATE_L1, _SOFT_STOP_ESCALATE_L2)
|
||||
|
||||
# stuck 失败签名归一化:长 hex(≥8 位,如 tool_call_id/session id)与数字串 → "#",
|
||||
# 使「同类错误、仅路径/id/行号不同」仍判为同签名(设计意图=「反复同错」死圈;80009 事故实证:
|
||||
# 旧实现签名恒为状态值 "error",4 次异类临时错也误熔断,且不留工具名/错误文本 → 生产盲区)。
|
||||
@ -308,6 +329,15 @@ class CircuitBreakerMiddleware(MiddlewareBase):
|
||||
self._rmb_gate_active = self._pricing_params is not None # 有计费参数才按金额拦,否则降级次数闸
|
||||
# ¥ 软预算越限标记(soft 档:越限设 True + 放行本次,agent 收 on_system_prompt 强提醒后尽快 finish)。
|
||||
self.budget_soft_tripped = False
|
||||
# (工单 c)软停后 finish 逼近升压:连续无-finish 的 acting 轮计数 + 已注入的最高升压档(防同档重复注入)。
|
||||
# 计数在 on_reply 维护(软停后每个 ToolCallStartEvent +1;一推理轮无 tool call=finish 尝试则复位);
|
||||
# 注入在 on_acting(按轮数派生档位,升档时把收尾指令附加到工具结果/并进拦截语)。
|
||||
self._softstop_rounds = 0
|
||||
self._softstop_level = 0
|
||||
# on_reply 轮内状态(判「一轮无 finish」用):本推理轮是否已产 tool call + 是否已见过首个 ModelCallStart
|
||||
# (首轮无「上一轮」可结算,避免起手误判 finish)。
|
||||
self._sr_round_had_tool = False
|
||||
self._sr_seen_model_call = False
|
||||
# 触发记录(供编排器/调试读)。
|
||||
self.tripped: dict | None = None
|
||||
|
||||
@ -353,14 +383,34 @@ class CircuitBreakerMiddleware(MiddlewareBase):
|
||||
|
||||
evt_type = type(evt).__name__
|
||||
|
||||
# ── ② budget:每次模型推理计一次 ──
|
||||
# ── ② budget:每次模型推理计一次(+ 工单 c:软停后 finish 逼近升压的轮边界结算)──
|
||||
if evt_type == "ModelCallStartEvent":
|
||||
# (工单 c)轮边界:新推理轮开始前先结算上一轮——软停后若上一轮全程无 tool call,即一次 finish
|
||||
# 尝试(被续修压制或真放行都算「agent 试图收尾」),复位升压计数,给它这轮 finish 后重新计的宽限
|
||||
# (续修上限 / 硬地板兜底,复位不会让 ¥ 无界)。仅 soft 档软停后生效,不碰正常路。
|
||||
if self.soft_budget and self.budget_soft_tripped and self._sr_seen_model_call \
|
||||
and not self._sr_round_had_tool:
|
||||
if self._softstop_rounds or self._softstop_level:
|
||||
print(
|
||||
f"[tier2-circuit] 软停后检出 finish 尝试(上一推理轮无 tool call)→ 复位 finish 逼近升压"
|
||||
f"(原 rounds={self._softstop_rounds} level={self._softstop_level})",
|
||||
flush=True,
|
||||
)
|
||||
self._softstop_rounds = 0
|
||||
self._softstop_level = 0
|
||||
self._sr_seen_model_call = True
|
||||
self._sr_round_had_tool = False
|
||||
self.model_calls += 1
|
||||
if self.model_calls > self.max_model_calls:
|
||||
self._trip("budget", f"模型推理次数超预算闸(>{self.max_model_calls})")
|
||||
|
||||
# ── ① step_cap:每次工具调用计一步 ──
|
||||
# ── ① step_cap:每次工具调用计一步(+ 工单 c:记本轮已动手 + 软停后累计无-finish acting 轮)──
|
||||
if evt_type == "ToolCallStartEvent":
|
||||
self._sr_round_had_tool = True # 本推理轮产了 tool call = 未 finish(轮边界据此判 finish 复位)
|
||||
if self.soft_budget and self.budget_soft_tripped:
|
||||
# 软停后又一个 acting 轮(agent 选择继续动手而非 finish);逼近升压的计数面(on_acting 据此派生档位)。
|
||||
# 注:批量并发工具会各计一次——烧得越快即升压越早,可接受(与「防继续烧」目标一致)。
|
||||
self._softstop_rounds += 1
|
||||
self.tool_calls += 1
|
||||
if self.tool_calls > self.max_tool_calls:
|
||||
self._trip("step_cap", f"工具调用步数超硬顶(>{self.max_tool_calls})")
|
||||
@ -501,32 +551,88 @@ class CircuitBreakerMiddleware(MiddlewareBase):
|
||||
|
||||
return _wrap()
|
||||
|
||||
# ── 软停后工具面收窄:on_acting 拦「新增生成面」工具(裁决:越软停线只许收尾类动作)──
|
||||
# ── 软停后工具面收窄 + finish 逼近升压:on_acting 拦「新增生成面」工具 + 连续无-finish 递进升压(工单 c)──
|
||||
async def on_acting(self, agent, input_kwargs, next_handler):
|
||||
"""soft 档且已软停时,拦截生成类工具调用(write_file/write_source/scaffold_init),其余透传。
|
||||
"""soft 档且已软停时:拦截生成类工具(write_file/write_source/scaffold_init)+ 软停后连续 N 轮
|
||||
仍不 finish 则把递进升压的收尾指令注入工具结果,其余透传。
|
||||
|
||||
拦截方式(AgentScope 2.0.2 源码核验):on_acting 是单工具 I/O 洋葱钩子(_agent.py:1585),
|
||||
不调 next_handler、直接 yield 一个 state=ERROR 的 ToolResponse 即为该工具的最终结果——消费方
|
||||
_execute_tool_call(:1452)据 tool_call.id 组 ToolResultBlock,不依赖 ToolResponse 自身 id。
|
||||
agent 收到错误文本会转向收尾(finish/check/build 恒放行);若它反复重试同一被拦工具,
|
||||
stuck 闸(连续同签名失败)会兜底熔断。hard 档 / 未软停 → 恒透传,零行为变化。
|
||||
|
||||
finish 逼近升压(工单 c):软停已由 on_system_prompt 强提醒 + 拦生成类工具,但那是被动系统提示;
|
||||
A1 T8 实测 M3 可能烧推理不理会(80011 实录=软停触发后仍烧到 step_cap/硬地板才停)。故软停后再数
|
||||
「连续多少个 acting 轮仍不 finish」(_softstop_rounds,on_reply 维护、finish 尝试即复位):达 N 轮升第一档
|
||||
(提醒尽快收尾),达 2N 轮升第二档(强制立即 finish、禁新增大额生成)。升压指令经本钩子主动注进工具结果
|
||||
(被拦生成类 → 并进拦截语;收尾类透传 → 附加到结果尾部),比被动系统提示更贴脸,逼软停线软着陆。
|
||||
升压只在跨档瞬间注一次(_softstop_escalation_pending 记录已注档,防每轮重复刷屏);_softstop_rounds=0
|
||||
(未升档 / 复位后)时本钩子行为与升压前逐字节等价——只拦生成类、收尾类原样透传。
|
||||
"""
|
||||
if self.soft_budget and self.budget_soft_tripped:
|
||||
# 按 on_reply 维护的无-finish acting 轮数派生升压档;升档瞬间取一次收尾文案(否则 None,不注入)。
|
||||
escalate = self._softstop_escalation_pending()
|
||||
tool_call = input_kwargs.get("tool_call") if isinstance(input_kwargs, dict) else None
|
||||
name = getattr(tool_call, "name", None)
|
||||
if name in self._soft_stop_blocked_tools:
|
||||
# 生成类工具:拦截(只许收尾);升档时把升压收尾指令并进拦截文本,催得更硬。
|
||||
base = (
|
||||
"本次生成的 ¥ 预算已过软停线,只允许收尾类动作(finish / check / build / 跑门 / 只读),"
|
||||
f"不再接受新增生成调用({name} 已拦截)。请基于当前工程的最好状态立即调用 finish 交付。")
|
||||
text = base if not escalate else base + "\n" + escalate
|
||||
print(f"[tier2-circuit] ¥ 软停后拦截生成类工具 {name}(只许收尾:finish/构建/跑门/只读)。",
|
||||
flush=True)
|
||||
yield ToolResponse(
|
||||
content=[TextBlock(text=(
|
||||
"本次生成的 ¥ 预算已过软停线,只允许收尾类动作(finish / check / build / 跑门 / 只读),"
|
||||
f"不再接受新增生成调用({name} 已拦截)。请基于当前工程的最好状态立即调用 finish 交付。"))],
|
||||
content=[TextBlock(text=text)],
|
||||
state=ToolResultState.ERROR,
|
||||
)
|
||||
return
|
||||
# 收尾类工具:放行;若本轮升档,把升压收尾指令附加到工具结果尾部(agent 下一轮读得到,主动催 finish)。
|
||||
async for item in next_handler(**input_kwargs):
|
||||
if escalate and isinstance(item, ToolResponse):
|
||||
self._augment_toolresponse(item, escalate)
|
||||
escalate = None # _acting 仅一个终态 ToolResponse;附加一次即可,防御性置空避免重复
|
||||
yield item
|
||||
return
|
||||
async for item in next_handler(**input_kwargs):
|
||||
yield item
|
||||
|
||||
def _softstop_escalation_pending(self) -> Optional[str]:
|
||||
"""按软停后累计的无-finish acting 轮数派生当前升压档;高于已注入档 → 返回该档收尾文案并记录,否则 None。
|
||||
|
||||
档位(SOFT_STOP_ESCALATE_ROUNDS=N):< N 轮不升压(0);[N, 2N) 第一档(提醒收尾);≥ 2N 第二档(强制立即
|
||||
finish)。只在跨档瞬间返回一次文案(记录 _softstop_level = 已注入的最高档),同档后续轮返回 None 不重复注入。
|
||||
"""
|
||||
n = SOFT_STOP_ESCALATE_ROUNDS
|
||||
desired = min(2, self._softstop_rounds // n) if n > 0 else 0
|
||||
if desired <= self._softstop_level:
|
||||
return None
|
||||
self._softstop_level = desired
|
||||
print(
|
||||
f"[tier2-circuit] 软停后连续 {self._softstop_rounds} 轮未 finish → 升压第 {desired} 档收尾指令注入。",
|
||||
flush=True,
|
||||
)
|
||||
return _SOFT_STOP_ESCALATE_TEXTS[desired]
|
||||
|
||||
@staticmethod
|
||||
def _augment_toolresponse(resp, extra_text: str) -> None:
|
||||
"""把升压收尾指令就地附加到工具结果 content 尾部(保留 state/metadata/id;best-effort,失败原样放行不抛)。
|
||||
|
||||
就地改(而非重建 ToolResponse)以保全其余字段;AgentScope 本身也往工具输出追加 <system-reminder>
|
||||
(截断提醒,_agent.py 核验),此处同法追加一条收尾提醒,agent 读工具结果时一并看到。
|
||||
"""
|
||||
try:
|
||||
reminder = "\n<system-reminder>" + extra_text + "</system-reminder>"
|
||||
content = getattr(resp, "content", None)
|
||||
if isinstance(content, list):
|
||||
content.append(TextBlock(text=reminder)) # 就地 append(pydantic 不对 in-place list 变更重校验)
|
||||
elif isinstance(content, str):
|
||||
resp.content = content + reminder
|
||||
# 其它/None:不动,静默跳过(不因附加失败而中断收尾放行)。
|
||||
except Exception as exc: # noqa: BLE001 —— 附加失败原样放行工具结果,绝不中断生成
|
||||
print(f"[tier2-circuit] 升压收尾指令附加失败(原样放行工具结果):{exc}", flush=True)
|
||||
|
||||
def _extract_credential_key(self, current_model) -> Optional[str]:
|
||||
"""best-effort 从 current_model 抽「本局 per-POST 凭据的 api_key」(治本取价口径,cutover 接缝 B)。
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user