feat: add a content event and stall accounting to the transport port

T1(计划 §4):观测原语与端口契约(issue #27/#28 底层)。

- Transport.complete 加必填 kw content_event(顺序在 first_token_event
  后,无默认值,同款约定):流式每增量 chunk 置位一次,非流式/对冲路
  恒不置位,None = 不观测
- TransportResult 增 stall_ms/stall_count(带默认 None):超阈值相邻
  增量 chunk 沉默总额与段数,chunk<2/非流式 → None,尾部不计
- SourceConfig 增 stall_threshold_ms(默认 1000,正整数拒 bool,
  _validate_watchdog 同款落点)
- _complete_stream 记增量 chunk 时间戳(空 chunk/usage 帧/keepalive
  不产生记录点)+ 模块级纯函数 _stall_from_chunk_times(严格大于
  阈值才减);max_inter_token_ms 原语义一字不动
- §2.2 同步清单全量:11 实现 + 11 调用点(重跑 grep 复核零遗漏)
- 批次 A 12 用例先红(TypeError/ImportError/AttributeError)后绿,
  证据 tests/outputs/138/t1/
This commit is contained in:
2026-09-14 00:35:35 -04:00
parent 6f3a620c73
commit 901a193514
15 changed files with 398 additions and 13 deletions
+7
View File
@@ -51,6 +51,12 @@ class Transport(Protocol):
`first_token_event` 同一约定(1.3.7 对冲 H2): `None` = 调用方不观测首 token
(未启用对冲);非流式实现**永不置位**(物理上无中途信号,事件自然退化为纯时间
阈值),流式实现在首个增量(内容或思考)到达时置位。
`content_event` 同一约定(1.3.8 中段沉默对冲,issue #27/#28): `None` = 调用方
不观测增量 chunk(未启用中段对冲);**流式**实现每到达一个增量 chunk(`content`
或 `reasoning_content`,与看门狗/首 token 活性口径一致)置位一次;空 chunk、
usage 帧、SSE keepalive 不算;非流式实现**永不置位**;对冲路恒收 `None`
(v1 不梯次,设计 §5.2 N3③)。
"""
async def complete(
@@ -63,6 +69,7 @@ class Transport(Protocol):
call_id: str,
reasoning_effort: Effort | None,
first_token_event: asyncio.Event | None,
content_event: asyncio.Event | None,
) -> TransportResult: ...