feat(v3): T27 会话式智能体(多轮上下文/停止/对话式 UI,对齐 dsh 范式)
- ToolLoop 增 history 参数(既往轮次折叠为对话上下文,近 6 轮)
- 会话制:AgentSession/SessionStore 持久化 agent_runs/sessions/{sid}.json,
/agent/sessions CRUD + /agent 带 session_id(继承会话工作区与执行者配置,busy 并发控制)
- POST /agent/{id}/cancel:取消运行中任务
- AgentView 重构为对话式:会话列表 + 居中消息流(用户蓝气泡/助手白卡)+ 底部 composer
(工作区/执行者/命令 chips,Enter 发送,运行中红色停止钮),工具过程折叠收纳
- 工具步数统计统一至事件写入层并透出 status.tool_calls
- fix(tests): 会话存储测试隔离,防测试数据泄漏进真实 sessions 目录
- 测试 +4,全量 281 passed
This commit is contained in:
+83
-6
@@ -501,12 +501,13 @@ try:
|
||||
|
||||
@app.post("/agent", tags=["agent"])
|
||||
async def agent_run(req: dict):
|
||||
"""提交智能体任务:{"task", "pool_id"?, "workspace"?}。
|
||||
"""提交智能体任务:{"task", "pool_id"?, "workspace"?, "executor_pool_id"?, "session_id"?}。
|
||||
|
||||
workspace 为用户选择的工作目录(绝对路径);缺省用设置里的 agent.workspace_dir。
|
||||
workspace 为用户选择的工作目录;缺省继承会话目录,再缺省用设置默认值。
|
||||
session_id 提供时任务在会话内执行(多轮上下文 + 轮次记录)。
|
||||
立即返回 request_id;过程事件经 GET /agent/{id}/stream (SSE) 推送。
|
||||
"""
|
||||
from gateway.agent import get_agent_service, new_request_id
|
||||
from gateway.agent import get_agent_service, get_session_store, new_request_id
|
||||
from router_system.tools import WorkspaceTools
|
||||
|
||||
task = str((req or {}).get("task") or "").strip()
|
||||
@@ -518,7 +519,23 @@ try:
|
||||
|
||||
s = settings_store().to_dict()
|
||||
agent_cfg = s.get("agent", {})
|
||||
|
||||
# 会话(可选):须存在且空闲;工作区缺省继承会话目录
|
||||
session = None
|
||||
session_id = str((req or {}).get("session_id") or "").strip()
|
||||
if session_id:
|
||||
session = get_session_store().get(session_id)
|
||||
if session is None:
|
||||
raise HTTPException(status_code=404, detail=f"会话不存在: {session_id}")
|
||||
if session.data.get("busy"):
|
||||
raise HTTPException(status_code=409, detail="该会话有任务正在运行,请稍候")
|
||||
# 会话级配置继承(创建时指定,后续轮次沿用)
|
||||
if not pool_id:
|
||||
pool_id = str(session.data.get("pool_id") or "")
|
||||
|
||||
ws_raw = str((req or {}).get("workspace") or "").strip()
|
||||
if not ws_raw and session is not None:
|
||||
ws_raw = str(session.data.get("workspace") or "")
|
||||
if ws_raw:
|
||||
ws_path = Path(ws_raw)
|
||||
if not ws_path.exists():
|
||||
@@ -533,6 +550,8 @@ try:
|
||||
|
||||
# 两级模式(D7):显式指定执行者(本地小模型)时,规划=chat、执行=executor_chat
|
||||
executor_pool_id = str((req or {}).get("executor_pool_id") or "").strip()
|
||||
if not executor_pool_id and session is not None:
|
||||
executor_pool_id = str(session.data.get("executor_pool_id") or "")
|
||||
executor_chat = None
|
||||
executor_model = ""
|
||||
if executor_pool_id:
|
||||
@@ -565,8 +584,11 @@ try:
|
||||
if info is None:
|
||||
raise HTTPException(status_code=503, detail="智能体同时运行任务已达上限")
|
||||
|
||||
s = settings_store().to_dict()
|
||||
agent_cfg = s.get("agent", {})
|
||||
if session is not None:
|
||||
session.data["busy"] = True
|
||||
if not session.data.get("workspace"):
|
||||
session.data["workspace"] = workspace_dir
|
||||
get_session_store().save(session)
|
||||
|
||||
async def _run():
|
||||
try:
|
||||
@@ -579,6 +601,7 @@ try:
|
||||
shell_timeout_s=int(agent_cfg.get("shell_timeout_s", 20)),
|
||||
executor_chat=executor_chat,
|
||||
max_handoffs=int(agent_cfg.get("max_handoffs", 2)),
|
||||
session=session,
|
||||
)
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
@@ -587,11 +610,65 @@ try:
|
||||
info.error = str(exc)
|
||||
info.finished_at = __import__("time").time()
|
||||
service._write_status(info)
|
||||
finally:
|
||||
if session is not None:
|
||||
session.data["busy"] = False
|
||||
get_session_store().save(session)
|
||||
|
||||
info.asyncio_task = asyncio.create_task(_run())
|
||||
return {"request_id": request_id, "status": "running", "model": model,
|
||||
"workspace": workspace_dir, "mode": mode,
|
||||
"executor_model": executor_model}
|
||||
"executor_model": executor_model, "session_id": session_id or None}
|
||||
|
||||
@app.post("/agent/{request_id}/cancel", tags=["agent"])
|
||||
async def agent_cancel(request_id: str):
|
||||
"""停止运行中的智能体任务。"""
|
||||
from gateway.agent import get_agent_service
|
||||
service = get_agent_service()
|
||||
info = service.get(request_id)
|
||||
if info is None:
|
||||
raise HTTPException(status_code=404, detail=f"智能体任务不存在: {request_id}")
|
||||
if info.state != "running":
|
||||
return {"ok": False, "detail": f"任务已结束({info.state})"}
|
||||
if info.asyncio_task is not None:
|
||||
info.asyncio_task.cancel()
|
||||
info.state = "failed"
|
||||
info.error = "cancelled_by_user"
|
||||
info.finished_at = __import__("time").time()
|
||||
service._write_status(info)
|
||||
return {"ok": True}
|
||||
|
||||
# ---------------- 会话(dsh 式多轮对话) ----------------
|
||||
@app.post("/agent/sessions", tags=["agent"])
|
||||
async def agent_session_create(req: dict = None):
|
||||
"""创建会话:{"title"?, "workspace"?, "pool_id"?, "executor_pool_id"?}"""
|
||||
from gateway.agent import get_session_store
|
||||
r = req or {}
|
||||
sess = get_session_store().create(
|
||||
title=str(r.get("title") or "").strip(),
|
||||
workspace=str(r.get("workspace") or "").strip(),
|
||||
pool_id=str(r.get("pool_id") or ""),
|
||||
executor_pool_id=str(r.get("executor_pool_id") or ""))
|
||||
return sess.view()
|
||||
|
||||
@app.get("/agent/sessions", tags=["agent"])
|
||||
async def agent_sessions():
|
||||
"""会话列表(按更新时间倒序)。"""
|
||||
from gateway.agent import get_session_store
|
||||
return get_session_store().list()
|
||||
|
||||
@app.get("/agent/sessions/{sid}", tags=["agent"])
|
||||
async def agent_session_detail(sid: str):
|
||||
from gateway.agent import get_session_store
|
||||
sess = get_session_store().get(sid)
|
||||
if sess is None:
|
||||
raise HTTPException(status_code=404, detail=f"会话不存在: {sid}")
|
||||
return sess.view()
|
||||
|
||||
@app.delete("/agent/sessions/{sid}", tags=["agent"])
|
||||
async def agent_session_delete(sid: str):
|
||||
from gateway.agent import get_session_store
|
||||
return {"ok": get_session_store().delete(sid)}
|
||||
|
||||
@app.get("/agent/fs", tags=["agent"])
|
||||
async def agent_fs_browse(path: str = ""):
|
||||
|
||||
Reference in New Issue
Block a user