feat(session): 落成事件出口,五个接缝全部有调用点
Event 从零字段变成 kind + run_id + model_binding + step,新增 EventKind(只有一种取值, 但第一天就带 kind,逼每个出口分发)。事件在「一步走完」原子落地之后发,只有这次进程里 真的执行过的步才发;投递失败接住、计数进 RunResult、继续跑,CancelledError 原样穿过。 契约套件那两条 xfail 关掉:一条要断言的是库发了几次、接缝自己看不到;另一条的前提是错的 ——审计纪律由意图日志承担不由事件流承担,改成在 unit 层验日志里原文与改写后的文本各有 位置。_project_observation 那段说「将来靠事件流送出去」的注释一并改对。 283 passed / 15 skipped / 2 xfailed,剩下两条 xfail 是原子写与前缀持久性,没有机器兜底。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,9 +8,9 @@
|
||||
一起」,与一次运行怎么称呼是两件事——`session` 在业界普遍指一个长期存在、可以来回对话的
|
||||
东西,而这里的治理单位是有界的、一次性的(`0006` 决策一)。
|
||||
|
||||
**事件出口现在不发任何事件。** `Event` 还没有字段,事件集与具名回调清单要独立成一份 design
|
||||
doc;在那之前发一条内容为空的事件既没用又会变成一份要兼容的形状。所以 `event_sink` 这个字段
|
||||
收下了但没有调用点,`RunResult.event_delivery_failures` 恒为 0。
|
||||
**事件只在一处发出去**:一步走完、`StepCompleted` 原子落地之后。顺序不能倒过来——先发后写的话,
|
||||
进程崩在两者之间会让观察者看见一步而存储里没有,而事件流的全部安全性建立在「它带的事实在存储
|
||||
里另有一份」上(`0013` 决策一与决策五)。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -38,6 +38,8 @@ from polyloop.ports import (
|
||||
Action,
|
||||
ActionExecutor,
|
||||
DecisionParser,
|
||||
Event,
|
||||
EventKind,
|
||||
EventSink,
|
||||
FinalAnswer,
|
||||
InvalidDecision,
|
||||
@@ -234,9 +236,10 @@ def _project_observation(
|
||||
得见的东西」,而那种东西必须能进参数快照。执行器每次现造一段文本的话,两次运行之间它可以
|
||||
变而不会有任何地方报错,于是「同一份配置跑出来的两次运行」在模型看来其实不同。
|
||||
|
||||
代价是执行器知道的细节丢了(「哪个参数不合法」只有它知道)。接受它,因为另一头的代价更
|
||||
大;要补的话将来靠事件流把执行器原文送出去做审计——**进历史的东西必须可复现,进审计的
|
||||
不必**。
|
||||
**执行器那段不进历史,但它没有丢**:「一步走完」那条记录落盘的是动作执行接缝的原样返回
|
||||
值,替换只发生在步记录的这一列上。被拒绝那一档下,它是日志里唯一的拒绝说明(「哪个参数
|
||||
不合法」只有执行器知道),所以不必再靠事件流把它送出去——事件可丢,而这段文本是审计要的
|
||||
(`0013` 决策六)。
|
||||
"""
|
||||
if outcome.status is ActionStatus.NOT_EXECUTED:
|
||||
return synthetic.action_rejected, True, 0
|
||||
@@ -256,13 +259,16 @@ class _Driver:
|
||||
frozen 的,共享它们没有问题。
|
||||
"""
|
||||
|
||||
__slots__ = ("_counters", "_definition", "_request", "_steps")
|
||||
__slots__ = ("_counters", "_definition", "_event_failures", "_request", "_steps")
|
||||
|
||||
def __init__(self, definition: AgentDefinition, request: RunRequest) -> None:
|
||||
self._definition = definition
|
||||
self._request = request
|
||||
self._counters = RunCounters()
|
||||
self._steps: list[StepRecord] = []
|
||||
#: 投递失败的次数。**住在这里而不是出口上**——出口挂在定义上、可以被并发的多次运行
|
||||
#: 共用,而这个数属于一次运行的结果。
|
||||
self._event_failures = 0
|
||||
|
||||
# -- 写入 ---------------------------------------------------------------
|
||||
|
||||
@@ -286,6 +292,33 @@ class _Driver:
|
||||
)
|
||||
self._steps.append(step)
|
||||
self._counters = self._counters.with_step_appended()
|
||||
await self._emit_step_finished(step)
|
||||
|
||||
async def _emit_step_finished(self, step: StepRecord) -> None:
|
||||
"""把这一步发给事件出口。**只有走到这里的步才发**,从日志里读回来直接跳过的不发。
|
||||
|
||||
补发已经完成的步等于宣称一件早就发生过的事刚刚发生,而接进度表的那一侧会多出一批
|
||||
重复行。观察者要补全前半段,从存储里读。
|
||||
|
||||
**投递失败接住、计数、继续跑**:事件是观察通道不是控制通道,一次运行不该因为进度回写
|
||||
的数据库连不上就终止。接的是 `Exception` 不是 `BaseException`——`CancelledError` 必须
|
||||
原样穿过(`CLAUDE.md` §1.6),在这一下把取消吞掉,取消就会晚一整步才生效。
|
||||
|
||||
**失败不转成一条事件从同一个出口再发一次**:那会自我喂食,一个持续失败的出口会让失败
|
||||
处理路径变成递归,而递归的表现是进程卡住或栈溢出,不是一条错误日志。
|
||||
"""
|
||||
try:
|
||||
await self._definition.event_sink.emit(
|
||||
Event(
|
||||
kind=EventKind.STEP_FINISHED,
|
||||
run_id=self._request.run_id,
|
||||
model_binding=self._request.model_binding,
|
||||
step=step,
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
self._event_failures += 1
|
||||
logger.exception("运行 %s 第 %d 步的事件投递失败", self._request.run_id, step.step_idx)
|
||||
|
||||
async def _finish(self, stop_reason: StopReason, final_answer: str | None = None) -> RunResult:
|
||||
"""写结束标记,然后返回结果。
|
||||
@@ -300,6 +333,7 @@ class _Driver:
|
||||
stop_reason=stop_reason,
|
||||
final_answer=final_answer,
|
||||
steps=tuple(self._steps),
|
||||
event_delivery_failures=self._event_failures,
|
||||
)
|
||||
await self._definition.store.write_run_finished(
|
||||
RunFinished(run_id=self._request.run_id, result=result)
|
||||
@@ -418,6 +452,7 @@ class _Driver:
|
||||
stop_reason=StopReason.CANCELLED,
|
||||
final_answer=None,
|
||||
steps=tuple(self._steps),
|
||||
event_delivery_failures=self._event_failures,
|
||||
)
|
||||
await self._drain_within_grace(
|
||||
asyncio.ensure_future(
|
||||
|
||||
Reference in New Issue
Block a user