状态、并发与清理#
状态有归属者,也有生命周期#
生命周期 |
合适的位置 |
示例 |
|---|---|---|
单次回调 |
局部变量 |
计算得到的载荷 |
单次请求 |
以 AgentRunContext 为键的字典 |
工具审批或模型步数计数器 |
单个对话 |
以会话 ID 为键的映射,或持久化存储 |
累积的消息历史记录 |
应用进程 |
不可变的共享配置,或有界的汇点 |
模型客户端连接池或指标导出器 |
同一个 Agent 可以并发运行不同的会话。同一会话的重叠运行会被拒绝。agent.state这个便捷属性指向最近一次准备好的状态,因此共享的扩展不能靠它来选中自己的请求。请始终使用钩子传入的 context.state。
AgentRunContext 使用身份语义,因此适合作为请求局部的映射键。普通字典会强引用它;请在每条终止路径上移除对应条目。不要依赖垃圾回收来作为你的清理机制。
完整的清理模式#
class WorkTracker(AgentExtension):
def __init__(self) -> None:
self.active: set[AgentRunContext] = set()
async def before_run(self, context: AgentRunContext) -> None:
self.active.add(context)
async def on_event(self, context: AgentRunContext, event: ExtensionEvent) -> None:
if isinstance(event, RunCancelledEvent):
self.active.discard(context)
async def on_error(self, context: AgentRunContext, error: Exception) -> None:
self.active.discard(context)
async def on_success(self, context: AgentRunContext, result: AssistantMessage) -> None:
self.active.discard(context)
discard/pop(..., None)让清理过程能容忍尚未完全初始化或已经释放的状态。取消并不是由 on_error 处理的 Exception 子类;它在内部以 RunCancelledEvent 的形式投递。外部响应基类的子类必须调用 super() 来保留它的清理逻辑。
检查流被提前关闭#
async def main() -> None:
tracker = WorkTracker()
client = await create_agent(StreamingModel(), extensions=[tracker])
# stream() is annotated AsyncIterator but is an async generator, which aclosing needs.
async with aclosing(client.stream("Start")) as events: # type: ignore[type-var]
async for event in events:
if event.type == AgentEventType.TEXT_DELTA:
break
assert client.agent.state.phase == AgentPhase.CANCELLED and not tracker.active
print("Cancelled request; extension state cleared")
print((await client.run("Try again")).content)
assert client.agent.state.phase == AgentPhase.COMPLETED and not tracker.active
上下文管理器关闭流之后,阶段为 CANCELLED,扩展的活动集合为空。之后的请求可以在同一个 Agent 上用全新的状态成功运行。已经持久化的过往消息不会被自动删除或修改。
发布前测试扩展#
至少要覆盖以下情况:
成功,以及正常的工具错误。
状态尚未完全初始化时就发生失败。
模型流式输出期间以及等待外部输入期间的取消。
两个会话共用同一个扩展实例。
重复和迟到的外部回复。
成功、失败和取消之后的第二次请求。
注册名称/工具名称冲突,以及相同优先级之间的顺序。
文档测试运行的就是这些教程中的类,而不是另一份副本。核心运行时测试套件还额外覆盖状态机与外部事件的竞态。
避免阻塞异步钩子#
不要在事件循环线程上调用同步的终端输入,或执行缓慢的文件系统/网络操作。请按情况使用异步库或 asyncio.to_thread。不要在 await 周围加自己的线程锁。ExternalEventExtension 已经安全地处理了跨线程的响应投递。