Skip to content

Commit ad4baef

Browse files
authored
feat: refactor AgentApp to inherit from FastAPI with interrupt support (#416)
1 parent 0462721 commit ad4baef

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3826
-970
lines changed

README.md

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777

7878
## 🆕 NEWS
7979

80+
* **[2026-02]** A major architectural refactor of `AgentApp`. By adopting direct inheritance from `FastAPI` and deprecating the previous factory pattern, `AgentApp` now offers seamless integration with the full FastAPI ecosystem, significantly boosting extensibility. Furthermore, we've introduced a **Distributed Interrupt Service**, enabling manual task preemption during agent execution and allowing developers to customize state persistence and recovery logic flexibly.
8081
* **[2026-01]** Added **asynchronous sandbox** implementations (`BaseSandboxAsync`, `GuiSandboxAsync`, `BrowserSandboxAsync`, `FilesystemSandboxAsync`, `MobileSandboxAsync`) enabling non-blocking, concurrent tool execution in async program. Improved `run_ipython_cell` and `run_shell_command` methods with enhanced **concurrency and parallel execution** capabilities for more efficient sandbox operations.
8182
* **[2025-12]** We have released **AgentScope Runtime v1.0**, introducing a unified “Agent as API” white-box development experience, with enhanced multi-agent collaboration, state persistence, and cross-framework integration. This release also streamlines abstractions and modules to ensure consistency between development and production environments. Please refer to the **[CHANGELOG](https://runtime.agentscope.io/en/CHANGELOG.html)** for full update details and migration guide.
8283

@@ -141,14 +142,15 @@ pip install -e .
141142

142143
This example demonstrates how to create an agent API server using agentscope `ReActAgent` and `AgentApp`. To run a minimal `AgentScope` Agent with AgentScope Runtime, you generally need to implement:
143144

144-
1. **`@agent_app.init`**Initialize services/resources at startup
145+
1. **`Define lifespan`**Use `contextlib.asynccontextmanager` to manage resource initialization (e.g., state services) at startup and cleanup on exit.
145146
2. **`@agent_app.query(framework="agentscope")`** – Core logic for handling requests, **must use** `stream_printing_messages` to `yield msg, last` for streaming output
146-
3. **`@agent_app.shutdown`** – Clean up services/resources on exit
147147

148148

149149
```python
150150
import os
151+
from contextlib import asynccontextmanager
151152

153+
from fastapi import FastAPI
152154
from agentscope.agent import ReActAgent
153155
from agentscope.model import DashScopeChatModel
154156
from agentscope.formatter import DashScopeChatFormatter
@@ -160,23 +162,32 @@ from agentscope.session import RedisSession
160162
from agentscope_runtime.engine import AgentApp
161163
from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest
162164

163-
agent_app = AgentApp(
164-
app_name="Friday",
165-
app_description="A helpful assistant",
166-
)
167-
168-
169-
@agent_app.init
170-
async def init_func(self):
165+
# 1. Define lifespan manager
166+
@asynccontextmanager
167+
async def lifespan(app: FastAPI):
168+
"""Manage resources during service startup and shutdown"""
169+
# Startup: Initialize Session manager
171170
import fakeredis
172171

173172
fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
174173
# NOTE: This FakeRedis instance is for development/testing only.
175174
# In production, replace it with your own Redis client/connection
176175
# (e.g., aioredis.Redis)
177-
self.session = RedisSession(connection_pool=fake_redis.connection_pool)
176+
app.state.session = RedisSession(connection_pool=fake_redis.connection_pool)
177+
178+
yield # Service is running
178179

180+
# Shutdown: Add cleanup logic here (e.g., closing database connections)
181+
print("AgentApp is shutting down...")
179182

183+
# 2. Create AgentApp instance
184+
agent_app = AgentApp(
185+
app_name="Friday",
186+
app_description="A helpful assistant",
187+
lifespan=lifespan,
188+
)
189+
190+
# 3. Define request handling logic
180191
@agent_app.query(framework="agentscope")
181192
async def query_func(
182193
self,
@@ -204,7 +215,8 @@ async def query_func(
204215
)
205216
agent.set_console_output_enabled(enabled=False)
206217

207-
await self.session.load_session_state(
218+
# Load state
219+
await agent_app.state.session.load_session_state(
208220
session_id=session_id,
209221
user_id=user_id,
210222
agent=agent,
@@ -216,13 +228,14 @@ async def query_func(
216228
):
217229
yield msg, last
218230

219-
await self.session.save_session_state(
231+
# Save state
232+
await agent_app.state.session.save_session_state(
220233
session_id=session_id,
221234
user_id=user_id,
222235
agent=agent,
223236
)
224237

225-
238+
# 4. Run the application
226239
agent_app.run(host="127.0.0.1", port=8090)
227240
```
228241

README_zh.md

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777

7878
## 🆕 新闻
7979

80+
* **[2026-02]** 我们对 `AgentApp` 进行了核心架构重构。新版本采用直接继承 `FastAPI` 的设计,废弃了原有的工厂类模式,使开发者能够直接利用完整的 FastAPI 生态,显著提升了应用的可扩展性。此外,新版本引入了分布式**任务中断管理服务**,支持在 Agent 推理过程中进行实时干预,并允许灵活自定义中断前后的状态保存与恢复逻辑。
8081
* **[2026-01]** 新增 **异步沙箱** 实现(`BaseSandboxAsync``GuiSandboxAsync``BrowserSandboxAsync``FilesystemSandboxAsync``MobileSandboxAsync`),支持在异步编程中进行非阻塞的并发工具执行。
8182
同时优化了 `run_ipython_cell``run_shell_command` 方法的 **并发与并行执行能力**,提升沙箱运行效率。
8283
* **[2025-12]** 我们发布了 **AgentScope Runtime v1.0**,该版本引入统一的 “Agent 作为 API” 白盒化开发体验,并全面强化多智能体协作、状态持久化与跨框架组合能力,同时对抽象与模块进行了简化优化,确保开发与生产环境一致性。完整更新内容与迁移说明请参考 **[CHANGELOG](https://runtime.agentscope.io/zh/CHANGELOG.html)**
@@ -143,14 +144,14 @@ pip install -e .
143144
这个示例演示了如何使用 AgentScope 的 `ReActAgent``AgentApp` 创建一个代理 API 服务器。
144145
要在 AgentScope Runtime 中运行一个最小化的 `AgentScope` Agent,通常需要实现以下内容:
145146

146-
1. **`@agent_app.init`**在启动时初始化服务或资源
147+
1. **`定义生命周期 (lifespan) `**使用 contextlib.asynccontextmanager 管理服务启动时的资源初始化(如状态服务)和退出时的清理
147148
2. **`@agent_app.query(framework="agentscope")`** – 处理请求的核心逻辑,**必须使用** `stream_printing_messages``yield msg, last` 来实现流式输出
148-
3. **`@agent_app.shutdown`** – 在退出时清理服务或资源
149-
150149

151150
```python
152151
import os
152+
from contextlib import asynccontextmanager
153153

154+
from fastapi import FastAPI
154155
from agentscope.agent import ReActAgent
155156
from agentscope.model import DashScopeChatModel
156157
from agentscope.formatter import DashScopeChatFormatter
@@ -162,23 +163,32 @@ from agentscope.session import RedisSession
162163
from agentscope_runtime.engine import AgentApp
163164
from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest
164165

165-
agent_app = AgentApp(
166-
app_name="Friday",
167-
app_description="A helpful assistant",
168-
)
169-
170-
171-
@agent_app.init
172-
async def init_func(self):
166+
# 1. 定义生命周期管理器
167+
@asynccontextmanager
168+
async def lifespan(app: FastAPI):
169+
"""管理服务启动和关闭时的资源"""
170+
# 启动时:初始化 Session 管理器
173171
import fakeredis
174172

175173
fake_redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
176174
# 注意:这个 FakeRedis 实例仅用于开发/测试。
177175
# 在生产环境中,请替换为你自己的 Redis 客户端/连接
178176
#(例如 aioredis.Redis)。
179-
self.session = RedisSession(connection_pool=fake_redis.connection_pool)
177+
app.state.session = RedisSession(connection_pool=fake_redis.connection_pool)
180178

179+
yield # 服务运行中
181180

181+
# 关闭时:可以在此处添加清理逻辑(如关闭数据库连接)
182+
print("AgentApp is shutting down...")
183+
184+
# 2. 创建 AgentApp 实例
185+
agent_app = AgentApp(
186+
app_name="Friday",
187+
app_description="A helpful assistant",
188+
lifespan=lifespan,
189+
)
190+
191+
# 3. 定义请求处理逻辑
182192
@agent_app.query(framework="agentscope")
183193
async def query_func(
184194
self,
@@ -206,7 +216,8 @@ async def query_func(
206216
)
207217
agent.set_console_output_enabled(enabled=False)
208218

209-
await self.session.load_session_state(
219+
# 加载状态
220+
await agent_app.state.session.load_session_state(
210221
session_id=session_id,
211222
user_id=user_id,
212223
agent=agent,
@@ -218,13 +229,14 @@ async def query_func(
218229
):
219230
yield msg, last
220231

221-
await self.session.save_session_state(
232+
# 保存状态
233+
await agent_app.state.session.save_session_state(
222234
session_id=session_id,
223235
user_id=user_id,
224236
agent=agent,
225237
)
226238

227-
239+
# 4. 启动应用
228240
agent_app.run(host="127.0.0.1", port=8090)
229241
```
230242

cookbook/en/CHANGELOG.md

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# CHANGELOG
22

33
## v1.1.0
4+
AgentScope Runtime v1.1.0 **simplifies persistence and session continuity** by removing Runtime-side custom Memory/Session service abstractions and **standardizing on the Agent framework’s native persistence modules**. This reduces mental overhead, avoids duplicated concepts, and ensures the persistence behavior is consistent with the underlying agent framework.
45

5-
AgentScope Runtime v1.1.0 focuses on **simplifying persistence and session continuity** by removing Runtime-side custom Memory/Session service abstractions and **standardizing on the Agent framework’s native persistence modules**. This reduces mental overhead, avoids duplicated concepts, and ensures the persistence behavior is consistent with the underlying agent framework.
6+
Additionally, this version introduces a **major refactor of the `AgentApp` core architecture**: switching to direct inheritance from `FastAPI`, introducing **standard lifespan management**, and adding **task interruption** and **concurrency conflict control** for distributed scenarios.
67

78
**Background & Necessity of the Changes**
89

@@ -19,11 +20,28 @@ In v1.0, Runtime provided custom **Session History** and **Long-term Memory** se
1920

2021
To address this, v1.1.0 **deprecates and removes** these Runtime-side services/adapters, and recommends using the **Agent framework’s own persistence modules** (e.g., `JSONSession`, built-in memory implementations) directly in the `AgentApp` lifecycle.
2122

23+
**Furthermore, v1.1.0 refactors the core architecture of `AgentApp`, shifting from a "factory creation pattern" to "direct inheritance from `FastAPI`".** This change stems from the following considerations:
24+
25+
1. **Addressing limitations of the factory pattern**
26+
In previous versions, `AgentApp` held a FastAPI instance internally through a factory class. This "black box" approach made it difficult for developers to leverage native FastAPI features (e.g., complex middleware, custom route decorators, and dependency injection), and custom lifecycle hooks like `@app.init` deviated from standard web development practices.
27+
28+
2. **Embracing native FastAPI ecosystem and extensibility**
29+
By **directly inheriting from the `FastAPI` class**, `AgentApp` is now a standard FastAPI application. This grants developers full control and seamless integration with FastAPI's community plugins. The class-based architecture also allows us to elegantly introduce advanced features like **Task Interruption** and **State Race Condition Control** via `Mixin` classes, making `AgentApp` a core component that is both standard-compliant and deeply optimized for Agent-specific scenarios.
30+
31+
### Added
32+
33+
- **Distributed Task Interruption**: Introduced `InterruptMixin` with backend support (Local/Redis), allowing manual interruption via custom interfaces and supporting interrupt signal broadcasting in distributed clusters.
34+
- **Distributed Race Condition Control**: Introduced atomic state-machine checks (Compare-and-Swap) during task startup to effectively prevent duplicate concurrent execution of the same Session ID in distributed environments.
35+
- **Native FastAPI Extensibility**: Since `AgentApp` now inherits from `FastAPI`, developers can use native methods like `@app.get` and `app.add_middleware` with full compatibility.
36+
2237
### Changed
2338

24-
- Recommended persistence pattern:
39+
- **Recommended persistence pattern**:
2540
- Use the agent framework’s **Memory** modules directly (e.g., `InMemoryMemory`, Redis-backed memory if provided by the framework).
2641
- Use the agent framework’s **Session** modules (e.g., `JSONSession`) to load/save agent session state during `query`.
42+
- **Architectural Refactor**: `AgentApp` has shifted from a **factory class pattern** to **direct inheritance from `FastAPI`**.
43+
- **Lifecycle Unification**: Unified the lifecycle management of internal framework resources and user-defined resources.
44+
2745

2846
### Breaking Changes
2947

@@ -35,17 +53,25 @@ To address this, v1.1.0 **deprecates and removes** these Runtime-side services/a
3553
- Runtime long-term memory services/adapters
3654
- `AgentScopeSessionHistoryMemory(...)`-style adapter usage
3755
must be migrated to the Agent framework’s built-in persistence approach.
56+
2. **Removal of the Factory Pattern**
57+
- `FastAPIAppFactory` is deprecated. Users should now instantiate `AgentApp` objects directly.
58+
3. **Deprecation of Custom Decorator Hooks**
59+
- The `@app.init` and `@app.shutdown` decorators are deprecated and marked for removal.
60+
- **Migration Advice**: Please use the standard FastAPI `lifespan` asynchronous context manager (see the example in the Migration Guide below).
3861

3962
#### Migration Guide (v1.0 → v1.1)
4063

41-
##### Recommended Pattern (Use Agent framework modules for persistence)
64+
##### Recommended Pattern (Unified Lifecycle, Interruption Handling, and Native Persistence)
4265

43-
Use `JSONSession` or other submodule to persist/load the agent’s session state, and use `InMemoryMemory()` (or other framework-provided memory) directly in AgentScope:
66+
In v1.1.0, we recommend using the **lifespan asynchronous context manager** instead of the old decorators to manage resources. Additionally, catch `asyncio.CancelledError` within the `query` logic to respond to interrupt signals, and use the Agent framework’s native **Session/Memory** modules for state persistence:
4467

4568
```python
4669
# -*- coding: utf-8 -*-
70+
import asyncio
4771
import os
72+
from contextlib import asynccontextmanager
4873

74+
from fastapi import FastAPI
4975
from agentscope.agent import ReActAgent
5076
from agentscope.model import DashScopeChatModel
5177
from agentscope.formatter import DashScopeChatFormatter
@@ -57,23 +83,27 @@ from agentscope.session import JSONSession
5783
from agentscope_runtime.engine.app import AgentApp
5884
from agentscope_runtime.engine.schemas.agent_schemas import AgentRequest
5985

86+
# Standard FastAPI lifespan management
87+
# (Replaces deprecated @app.init/@app.shutdown)
88+
@asynccontextmanager
89+
async def lifespan(app: FastAPI):
90+
# Use JSONSession here
91+
app.state.session = JSONSession(save_dir="./sessions")
92+
try:
93+
yield
94+
finally:
95+
# No Runtime state/session services to stop in v1.1
96+
pass
97+
98+
# AgentApp now inherits directly from FastAPI
6099
agent_app = AgentApp(
61100
app_name="Friday",
62101
app_description="A helpful assistant",
102+
lifespan=lifespan,
103+
# Optional: Enable distributed interrupt by providing interrupt_redis_url
104+
# interrupt_redis_url="redis://localhost"
63105
)
64106

65-
66-
@agent_app.init
67-
async def init_func(self):
68-
self.session = JSONSession(save_dir="./sessions") # Use JSONSession here
69-
70-
71-
@agent_app.shutdown
72-
async def shutdown_func(self):
73-
# No Runtime state/session services to stop in v1.1
74-
pass
75-
76-
77107
@agent_app.query(framework="agentscope")
78108
async def query_func(
79109
self,
@@ -102,15 +132,43 @@ async def query_func(
102132
formatter=DashScopeChatFormatter(),
103133
)
104134

105-
await self.session.load_session_state(session_id=session_id, agent=agent)
106-
107-
async for msg, last in stream_printing_messages(
108-
agents=[agent],
109-
coroutine_task=agent(msgs),
110-
):
111-
yield msg, last
135+
await agent_app.state.session.load_session_state(
136+
session_id=session_id,
137+
agent=agent,
138+
)
112139

113-
await self.session.save_session_state(session_id=session_id, agent=agent)
140+
try:
141+
async for msg, last in stream_printing_messages(
142+
agents=[agent],
143+
coroutine_task=agent(msgs),
144+
):
145+
yield msg, last
146+
147+
except asyncio.CancelledError:
148+
# Handling Interruptions (New in v1.1.0)
149+
await agent.interrupt() # Explicitly halt the underlying agent execution
150+
151+
# Re-raise to ensure AgentApp correctly updates the task state to STOPPED
152+
raise
153+
154+
finally:
155+
# Persistence: Save state regardless of normal completion or interruption
156+
await agent_app.state.session.save_session_state(
157+
session_id=session_id,
158+
agent=agent
159+
)
160+
161+
# Optional: Explicit endpoint to trigger task interruption
162+
@agent_app.post("/stop")
163+
async def stop_task(request: AgentRequest):
164+
await agent_app.stop_chat(
165+
user_id=request.user_id,
166+
session_id=request.session_id,
167+
)
168+
return {
169+
"status": "success",
170+
"message": "Interrupt signal broadcasted.",
171+
}
114172

115173

116174
agent_app.run()

0 commit comments

Comments
 (0)