状态、并发与清理#

状态有归属者,也有生命周期#

按生命周期选择存放位置#

生命周期

合适的位置

示例

单次回调

局部变量

计算得到的载荷

单次请求

以 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 上用全新的状态成功运行。已经持久化的过往消息不会被自动删除或修改。

发布前测试扩展#

至少要覆盖以下情况:

  1. 成功,以及正常的工具错误。

  2. 状态尚未完全初始化时就发生失败。

  3. 模型流式输出期间以及等待外部输入期间的取消。

  4. 两个会话共用同一个扩展实例。

  5. 重复和迟到的外部回复。

  6. 成功、失败和取消之后的第二次请求。

  7. 注册名称/工具名称冲突,以及相同优先级之间的顺序。

文档测试运行的就是这些教程中的类,而不是另一份副本。核心运行时测试套件还额外覆盖状态机与外部事件的竞态。

避免阻塞异步钩子#

不要在事件循环线程上调用同步的终端输入,或执行缓慢的文件系统/网络操作。请按情况使用异步库或 asyncio.to_thread。不要在 await 周围加自己的线程锁。ExternalEventExtension 已经安全地处理了跨线程的响应投递。