在后台运行子智能体#
启动一个子智能体而不等待它,让父智能体继续工作,等报告准备好后再读取。对子智能体的工具限制并不是操作系统层面的沙箱。
运行示例#
在仓库根目录执行:
uv run python docs/_examples/async_subagent.py
输出包含:
Background task finished; child session is separate
这个程序使用确定性的离线模型,不需要 API 密钥,并由文档测试套件执行。
模型如何管理任务#
AsyncSubAgentExtension 会注册五个工具:
工具 |
用途 |
|---|---|
|
用已配置的子智能体配置启动一个任务,并立即返回它的 |
|
列出本会话的所有任务及其状态。 |
|
查看单个任务的状态,不含报告;从不等待。 |
|
返回报告;需要时最多等待 |
|
停止一个正在运行的任务。 |
任务结束后,会在父智能体下一次调用模型之前,或会话的下一个请求开始时,以普通智能体消息的形式通知它。模型已经读取过结果的任务不会再次通知。
async def main() -> None:
definition = SubAgentDefinition(
name="audit",
description="audit a code area and report what it finds.",
system_prompt="You audit code and return a short report.",
model=ChildModel(),
)
extension = AsyncSubAgentExtension([definition])
client = await create_agent(
ParentModel(),
config=AgentRunConfig(session_id="parent"),
extensions=[extension, ToolGuidelinesExtension()],
)
try:
print((await client.run("Audit the login code")).content)
[task] = extension.tasks("parent")
assert task.status is SubAgentTaskStatus.COMPLETED and task.session_id != "parent"
print("Background task finished; child session is separate")
finally:
await extension.close()
通过子类化来定制#
只重写你需要的部分,而不是复制整个扩展:
class TerseNotices(AsyncSubAgentExtension):
def build_tools(self, context, subagents):
# Drop cancellation for this deployment.
return tuple(t for t in super().build_tools(context, subagents) if t.name != "task_cancel")
def completion_notice(self, task):
return f"{task.task_id} is {task.status.value}."
async def task_finished(self, task):
metrics.record(task.subagent_type, task.elapsed)
task_info 决定每个工具返回的内容,task_started 和 task_finished 用来观察生命周期(它们的错误只会被记录,不会抛进任务里),_create_child 则改变每个子智能体的构建方式。任务在扩展对象的生命周期内保存在内存中,并且只对启动它们的会话可见。应用关闭时请调用 await extension.close()。
完整源码#
1"""Start a subagent in the background, keep working, and read its report later."""
2
3import asyncio
4import json
5from collections.abc import AsyncIterator
6
7from zett_agent.agent import AgentRunConfig
8from zett_agent.client import create_agent
9from zett_agent.extensions.async_subagent import AsyncSubAgentExtension, SubAgentTaskStatus
10from zett_agent.extensions.subagent import SubAgentDefinition
11from zett_agent.extensions.tool_guidelines import ToolGuidelinesExtension
12from zett_agent.messages import AssistantMessage, ToolCall, ToolMessage
13from zett_agent.model import ModelEvent, ModelRequest, ModelResponse, RetryOptions
14
15
16class ParentModel:
17 """Start a task, do other work, then wait for the report."""
18
19 retry = RetryOptions(max_retries=0)
20
21 async def stream(self, request: ModelRequest) -> AsyncIterator[ModelEvent]:
22 results = [message for message in request.messages if isinstance(message, ToolMessage)]
23 if not results:
24 call = ToolCall(
25 "start-1",
26 "task_start",
27 {"description": "Audit auth", "prompt": "List the auth entry points", "subagent_type": "audit"},
28 )
29 message = AssistantMessage(tool_calls=(call,))
30 elif len(results) == 1:
31 # The task is running; ask for its report and wait up to ten seconds.
32 task_id = json.loads(results[0].text)["task_id"]
33 call = ToolCall("output-1", "task_output", {"task_id": task_id, "wait_seconds": 10})
34 message = AssistantMessage(tool_calls=(call,))
35 else:
36 report = json.loads(results[-1].text)["content"]
37 message = AssistantMessage(content=f"Background report: {report}")
38 yield ModelEvent.completed(ModelResponse(message))
39
40
41class ChildModel:
42 retry = RetryOptions(max_retries=0)
43
44 async def stream(self, request: ModelRequest) -> AsyncIterator[ModelEvent]:
45 await asyncio.sleep(0.05) # stands in for real work the parent does not wait for
46 yield ModelEvent.completed(ModelResponse(AssistantMessage(content="login, logout, refresh")))
47
48
49async def main() -> None:
50 definition = SubAgentDefinition(
51 name="audit",
52 description="audit a code area and report what it finds.",
53 system_prompt="You audit code and return a short report.",
54 model=ChildModel(),
55 )
56 extension = AsyncSubAgentExtension([definition])
57 client = await create_agent(
58 ParentModel(),
59 config=AgentRunConfig(session_id="parent"),
60 extensions=[extension, ToolGuidelinesExtension()],
61 )
62 try:
63 print((await client.run("Audit the login code")).content)
64 [task] = extension.tasks("parent")
65 assert task.status is SubAgentTaskStatus.COMPLETED and task.session_id != "parent"
66 print("Background task finished; child session is separate")
67 finally:
68 await extension.close()
69
70
71if __name__ == "__main__":
72 asyncio.run(main())
下一步:阻塞式的版本见 运行一个隔离的子智能体,或参阅 组合内置扩展 与其他能力对比。