取消并复用智能体#

关闭被中断的流,清理扩展状态,并完成一次新的请求。

运行#

在仓库根目录下执行:

uv run python docs/_examples/cancellation.py

输出包含:

Cancelled request; extension state cleared

这个程序在需要时使用确定性的离线模型和临时存储。它不需要 API 密钥,并由文档测试套件执行。

离开时关闭流#

对于可能提前停止的流,请用aclosing包起来。该示例在第一个文本增量处跳出,退出上下文管理器,然后复用客户端完成一次完整的请求:

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

如果扩展保留了请求级状态,那么在取消时也要像成功和出错时一样清理它。WorkTracker 演示了这三种路径。关闭流不会回滚已经完成的工具操作;设计有副作用的工具时请留意这一区别。

完整源码#

下载 cancellation.py。

 1"""Close a streaming request and then safely reuse the same Agent."""
 2
 3import asyncio
 4from collections.abc import AsyncIterator
 5from contextlib import aclosing
 6
 7from zett_agent.agent import AgentRunContext
 8from zett_agent.client import create_agent
 9from zett_agent.events import (
10    AgentEventType,
11    AgentPhase,
12)
13from zett_agent.extensions.base import AgentExtension
14from zett_agent.extensions.events import (
15    ExtensionEvent,
16    RunCancelledEvent,
17)
18from zett_agent.messages import AssistantMessage
19from zett_agent.model import (
20    ModelEvent,
21    ModelRequest,
22    ModelResponse,
23    RetryOptions,
24)
25
26
27class WorkTracker(AgentExtension):
28    def __init__(self) -> None:
29        self.active: set[AgentRunContext] = set()
30
31    async def before_run(self, context: AgentRunContext) -> None:
32        self.active.add(context)
33
34    async def on_event(self, context: AgentRunContext, event: ExtensionEvent) -> None:
35        if isinstance(event, RunCancelledEvent):
36            self.active.discard(context)
37
38    async def on_error(self, context: AgentRunContext, error: Exception) -> None:
39        self.active.discard(context)
40
41    async def on_success(self, context: AgentRunContext, result: AssistantMessage) -> None:
42        self.active.discard(context)
43
44
45class StreamingModel:
46    retry = RetryOptions(max_retries=0)
47
48    async def stream(self, request: ModelRequest) -> AsyncIterator[ModelEvent]:
49        yield ModelEvent.text("Partial")
50        yield ModelEvent.text(" answer")
51        yield ModelEvent.completed(ModelResponse(AssistantMessage(content="Partial answer")))
52
53
54async def main() -> None:
55    tracker = WorkTracker()
56    client = await create_agent(StreamingModel(), extensions=[tracker])
57    # stream() is annotated AsyncIterator but is an async generator, which aclosing needs.
58    async with aclosing(client.stream("Start")) as events:  # type: ignore[type-var]
59        async for event in events:
60            if event.type == AgentEventType.TEXT_DELTA:
61                break
62    assert client.agent.state.phase == AgentPhase.CANCELLED and not tracker.active
63    print("Cancelled request; extension state cleared")
64    print((await client.run("Try again")).content)
65    assert client.agent.state.phase == AgentPhase.COMPLETED and not tracker.active
66
67
68if __name__ == "__main__":
69    asyncio.run(main())

下一步:消费端的处理模式见把流式输出接入终端或界面,扩展的清理见状态、并发与清理。