From d9acfbfc0beb793d95a970ad8e98067273b2d3f8 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 6 Oct 2026 13:07:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7=E5=90=8E=E7=9A=84=E6=97=B6?= =?UTF-8?q?=E9=95=BF=E5=8F=96=E5=88=B0=200.1=20=E7=A7=92=E3=80=81=E5=BE=80?= =?UTF-8?q?=E4=B8=8B=E5=8F=96=EF=BC=9A=E6=A0=B8=E6=9F=A5=20agent=20?= =?UTF-8?q?=E8=A7=A6=E9=A1=B6=E6=97=B6=E6=97=B6=E9=97=B4=E7=BA=BF=E5=86=99?= =?UTF-8?q?=E3=80=8C=E6=97=B6=E9=95=BF=E4=B8=8A=E9=99=90=2069.7=20?= =?UTF-8?q?=E7=A7=92=E3=80=8D=EF=BC=8C=E4=B8=8D=E5=86=99=2069.7012=20?= =?UTF-8?q?=E8=BF=99=E6=A0=B7=E4=B8=80=E4=B8=B2=E5=B0=8F=E6=95=B0=EF=BC=9B?= =?UTF-8?q?=E5=BE=80=E4=B8=8B=E5=8F=96=E4=B8=8D=E4=BC=9A=E8=B6=85=E5=87=BA?= =?UTF-8?q?=E7=AE=97=E5=A5=BD=E7=9A=84=E4=BD=99=E9=87=8F=EF=BC=8C=E5=B7=AE?= =?UTF-8?q?=E4=B8=80=E7=82=B9=E4=B8=8D=E5=A4=9F=E7=9A=84=E7=85=A7=E6=A0=B7?= =?UTF-8?q?=E4=B8=8D=E5=BC=80=E5=A7=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 另把 segment、allot 的说明补全:deadline 为 None 时放的是 None(里面看不到外面的截止时刻),配置的是不限时按收尾宽限算最短时长。 --- backend/app/engine/deadline.py | 12 +++++++----- backend/tests/test_run_deadline.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/backend/app/engine/deadline.py b/backend/app/engine/deadline.py index 90078d92..7e71785a 100644 --- a/backend/app/engine/deadline.py +++ b/backend/app/engine/deadline.py @@ -17,6 +17,7 @@ from __future__ import annotations import asyncio import contextlib +import math from collections.abc import AsyncIterator from contextvars import ContextVar from dataclasses import dataclass @@ -42,7 +43,7 @@ _SEGMENT: ContextVar[Segment | None] = ContextVar("agentlab_segment", default=No @contextlib.asynccontextmanager async def segment(deadline: float | None, seconds: float) -> AsyncIterator[None]: """runner 用:这一段执行的截止时刻(asyncio.Timeout.when())和总时长,退出时清掉。写成异步的上下文管理器,好和 - 时长上限写在同一个 async with 里。deadline 为 None(没有上限)时什么都不放。""" + 时长上限写在同一个 async with 里。deadline 为 None(没有上限)时放 None:里面看不到外面的截止时刻。""" token = _SEGMENT.set(Segment(float(deadline), float(seconds)) if deadline is not None else None) try: yield @@ -81,13 +82,14 @@ def allot(timeout_s: float | None, *, grace: float) -> Allot: grace 是到点之后收尾那次调用的宽限(verify_agent.SETTLE_GRACE_S)。 一项最坏要用「时长上限 + 收尾宽限」。剩下的时间先扣掉给下游的余量、再扣掉收尾宽限,就是这一项查库最多能用的时长: - 比配置的短就收紧到它。收紧之后不到 min(配置的时长上限, 收尾宽限) 就不再开始:收尾宽限是留给一次模型调用的时间, - 查库的时长比它还短,连一轮都做不完,只会白花一次收尾的钱、记成没能核查。配置的时长上限本来就比它短的,按配置的 - 整段算。没有上限时原样放行。""" + 比配置的短就收紧到它(取到 0.1 秒,往下取:时间线上写「时长上限 69.7 秒」,不写一串小数)。收紧之后不到 + min(配置的时长上限, 收尾宽限) 就不再开始:收尾宽限是留给一次模型调用的时间,查库的时长比它还短,连一轮都做不完, + 只会白花一次收尾的钱、记成没能核查。配置的时长上限本来就比它短的,按配置的整段算;配置的是不限的,按收尾宽限算。 + 没有上限时原样放行。""" found = _SEGMENT.get() if found is None: return Allot(True, timeout_s) - room = found.deadline - _now() - reserve(found.seconds) - grace + room = math.floor((found.deadline - _now() - reserve(found.seconds) - grace) * 10) / 10 limit = room if timeout_s is None else min(timeout_s, room) floor = grace if timeout_s is None else min(timeout_s, grace) if limit < floor: diff --git a/backend/tests/test_run_deadline.py b/backend/tests/test_run_deadline.py index 078ca1ed..06c83407 100644 --- a/backend/tests/test_run_deadline.py +++ b/backend/tests/test_run_deadline.py @@ -76,6 +76,16 @@ async def test_a_configured_limit_shorter_than_the_grace_runs_whole_or_not_at_al assert deadline.allot(10, grace=30.0) == deadline.Allot(False, None, seconds=600) +async def test_a_tightened_limit_is_cut_to_a_tenth_of_a_second(clock): + """收紧到的时长取到 0.1 秒、往下取:时间线上写「时长上限 69.7 秒」,不写 69.7012;往下取不会超出算好的余量。""" + async with deadline.segment(clock.now + 159.7012, 600): + assert deadline.allot(90, grace=30.0).timeout_s == 69.7 + async with deadline.segment(clock.now + 120.05, 600): + assert deadline.allot(90, grace=30.0).timeout_s == 30.0 + async with deadline.segment(clock.now + 119.99, 600): + assert not deadline.allot(90, grace=30.0).start, "差一点也是不够:往下取之后不到 30 秒" + + async def test_an_unlimited_item_is_bounded_by_the_deadline(clock): """配置的是不限(None):有截止时刻时照样按剩余时间收紧,不然它能一直查到运行被中止。""" async with deadline.segment(clock.now + 400, 600):