HTTP API¶
内置聊天页面和 JSON + SSE API 相互独立。你可以关闭页面,只保留接口,再接入自己的前端。
服务启动后,访问 /api/docs 可查看交互式 API 文档。
| 集成方式 | 适合场景 |
|---|---|
create_app(agent, ui=False) |
独立服务,只关闭内置页面 |
create_app(...) |
需要完整 ASGI 应用,但由自己启动服务器 |
build_api_router(...) |
已有 FastAPI 应用,需要复用其中间件和生命周期 |
app.mount(prefix, create_app(...)) |
已有应用,需要把完整的 lovia 应用挂在某个路径前缀下 |
只提供 API¶
有两种接入方式。最简单的是关闭内置页面:
也可以将 router 挂载到自己的 FastAPI 应用,由现有应用统一管理中间件、认证和生命周期:
from fastapi import FastAPI
from lovia.web import ChatStore, RouterDeps, build_api_router
deps = RouterDeps(agents={"bot": agent}, store=ChatStore.in_memory())
app = FastAPI(lifespan=deps.lifespan)
app.include_router(build_api_router(deps))
RouterDeps 是普通 dataclass,只有 agents 和 store 为必填字段。
max_turns、budget、retry、tracer、approval_timeout、max_background_runs、
scheduler_poll 及标题设置均有与 create_app() 相同的默认值。
deps.lifespan 负责 API 背后的后台机制:启动时把上个进程遗留的 running 运行记录标记为中断,
并启动定时任务轮询;关闭时让进行中的 Run 停在可恢复的 checkpoint,并关闭各对话的工作区及其
后台进程。不接入它,定时任务永远不会触发。应用已有自己的 lifespan 时,在其中嵌套进入:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# 自己的启动逻辑
async with deps.lifespan():
yield
# 自己的关闭逻辑
app = FastAPI(lifespan=lifespan)
Agent 带有 Subagents Plugin 时,还需调用一次 wire_subagents(deps)
(见 Web UI)。
挂载 create_app() 应用¶
create_app() 构建的应用也可以挂在你自己应用的某个路径前缀下。Starlette 只运行外层应用的
lifespan,被挂载应用的 lifespan 永远不会启动,而且没有任何提示,因此要在外层 lifespan 中进入它:
lovia_app = create_app(agent)
@asynccontextmanager
async def lifespan(app: FastAPI):
async with lovia_app.state.deps.lifespan():
yield
app = FastAPI(lifespan=lifespan)
app.mount("/lovia", lovia_app) # 页面位于 /lovia/,API 位于 /lovia/api/...
认证¶
通过 create_app(token=...) 配置 token 后,build_api_router 注册的业务路由都需要认证。
serve(agent) 绑定非回环地址且未指定认证方式时,会自动生成 token。
普通请求以及 POST /api/chat/stream、POST /api/chat/reconnect 应发送
Authorization: Bearer <token>。GET /api/events 使用 EventSource,无法自定义请求头,
内置 UI 因此通过 lovia_token cookie 认证。GET /healthz 由 create_app 在认证依赖之外提供,
无论使用 token 还是 auth 都保持开放。凭据缺失或错误时,
服务端返回 401,错误码为 server_token;模型 Provider 拒绝 API Key 则是另一回事,
会以运行错误 provider_auth 出现。
直接挂载 build_api_router 时,需要自行添加认证依赖,例如
lovia.web.auth.token_dependency(token) 或其他 FastAPI 依赖;健康检查也需要自己提供,
因为 /healthz 不属于该 router。
/api/docs 和 /api/openapi.json 由 FastAPI 应用本身提供,不属于上述业务路由,默认保持
公开;其中只包含接口定义,不包含会话或工作区数据。如需限制访问,请在应用层另行处理。
错误¶
API 路由抛出的错误都使用同一种响应体:
客户端应根据 code 判断错误类型,不要匹配 message 的文字;hint 出现时是建议的处理方式。
有两类例外沿用 FastAPI 自身的格式:请求校验失败(422,detail 是列表),以及自定义
auth= 依赖抛出的错误。内置浏览器客户端的 apiError(response)
会把这三种格式统一处理。
| 错误码 | 状态码 | 含义 |
|---|---|---|
invalid_request |
400 / 422 | 输入格式错误或为空,被路由拒绝 |
server_token |
401 | 缺少 server token,或 token 无效 |
local_origin_required |
403 | 未配置认证时,从非本机 Host 修改配置 |
path_denied |
403 | 工作区策略拒绝访问该路径 |
agent_not_found |
404 | 没有以该名称提供服务的 Agent |
session_not_found |
404 | 聊天不存在 |
schedule_not_found |
404 | 定时任务不存在 |
model_not_found |
404 | 模型配置不存在 |
file_not_found |
404 | 工作区文件或目录不存在 |
turn_not_found |
404 | rewind 或 fork 指定的用户 Turn 不存在 |
image_not_found |
404 | 该 Tool 结果位置没有可提供的图片 |
ui_data_not_found |
404 | 本聊天中没有该 ref 对应的组件数据 |
approval_not_found |
404 | 没有匹配的待审批请求 |
question_not_found |
404 | 没有匹配的待回答 ask_human 问题 |
process_not_found |
404 | 后台进程不存在 |
run_not_found |
404 | 没有可取消或可重连的 Run |
feature_unavailable |
404 / 501 | 服务端或 Agent 不具备该功能(工作区、Memory、checkpointer、提问通道或 rewind) |
run_active |
409 | 该聊天已有 Run 在执行 |
run_stopping |
409 | 已停止的 Run 仍在收尾,请稍后重试 |
run_unfinished |
409 | 该聊天的上一个 Run 被中断且仍可恢复;复制整个聊天要等它结束 |
nothing_to_fork |
409 | fork 结果会是空的:该位置之前没有已完成的历史 |
run_cancelled |
409 | 阻塞式 Turn 的 Run 被停止(取消、删除聊天或服务端关闭) |
agent_unregistered |
409 | 中断的 Run 所属 Agent 已不再提供服务 |
schedule_not_fired |
409 | 定时任务暂时无法触发 |
model_exists |
409 | 该 id 的模型配置已存在 |
model_in_use |
409 | 默认聊天模型不能删除 |
file_too_large |
413 | 超过工作区读取或上传上限 |
unsupported_file_type |
415 | 不支持内联预览,或扩展名不允许上传 |
too_many_runs |
429 | 已达到并发 Run 上限 |
run_failed |
500 | 阻塞式 Turn 的 Run 失败,message 为其错误 |
端点¶
| 方法与路径 | 用途 |
|---|---|
GET /healthz |
存活检查(由 create_app 提供,不属于 build_api_router) |
GET /api/info |
标题、Agent 列表、默认 Agent、版本和功能开关 |
GET /api/agents · GET /api/agents/{name} |
查看 Agent 的 instructions、工具和能力 |
POST /api/chat |
执行一次阻塞式 Turn → {output, session_id, usage}:托管 Run,请求等待其结束(详见) |
POST /api/chat/stream |
SSE:启动 Run,或连接到 Session 中正在执行的 Run(新消息会作为追加指令注入) |
POST /api/chat/reconnect?session_id= |
SSE:重新连接,或从 Checkpoint 恢复中断的 Run |
POST /api/chat/approve |
处理未决审批:{session_id, call_id, decision} |
POST /api/chat/cancel?session_id= |
取消正在执行的 Run(保留已完成的 Turn) |
POST /api/chat/inject / uninject |
为正在执行的 Run 排队或撤回追加指令 |
GET /api/sessions?q=&limit=&offset= |
列出或搜索对话(置顶优先,支持分页);DELETE 清空全部对话 |
GET /api/runs |
正在运行的服务端托管任务。run_id 与该 Run 的生命周期事件和运行记录一致,自动续跑和恢复后都不变 |
GET /api/runs/history?session_id=&source=&since=&run_id=&limit=&offset= |
查询持久化的运行记录,包括结果、错误、时长和 token 用量;since 按结束时间过滤,run_id 查询单个 Run |
GET /api/events |
SSE:订阅进程级生命周期事件(不重放历史事件) |
GET / PATCH / DELETE /api/sessions/{id} |
查看 transcript;重命名或置顶;删除(连同其后台任务和绑定它的定时任务)。每条用户消息带 ref("<run_id>:<n>"),刷新和回退后不变;其 Run 结束之前为 null。每次 Run 的最后一条回答带 duration(秒);记录了用时的 Run,其消息时间戳为真实时间。来自另一个对话的聊天带 origin:kind 为 "fork"(分支)或 "spawn"(由那个对话的 agent 用 start_session 启动);列表中的 origin_kind 与之一致 |
GET /api/sessions/{id}/todos |
当前 Todo 列表,从 Transcript 重建 |
POST /api/sessions/{id}/followups |
生成追问建议,返回 {followups: [...]};功能未启用或建议器未返回内容时为 []。该操作可能消耗模型 token,因此使用 POST |
POST /api/sessions/{id}/rewind |
从某条用户消息起删除后续内容;用它的 ref 指定,或用 user_turn(它在用户消息中的索引,从 0 开始);ref 已不在聊天中时返回 404,运行中返回 409,不支持 rewind 时返回 501 |
POST /api/sessions/{id}/fork |
{ref?} → {session_id}:把聊天已完成的历史复制成一个新的独立聊天;给 ref 时只复制到该用户消息之前,不给则复制整个聊天。新聊天的详情带 origin: {kind: "fork", session_id, ref, title, exists};删除任一聊天都不影响另一个。fork 复制的是对话,不是工作区:文件保持当前状态。有 Run 正在运行、正在收尾或可恢复时,复制整个聊天返回 409 |
GET /api/sessions/{id}/export?format=md\|json\|txt |
导出聊天 |
GET /api/sessions/{id}/ui-data/{ref} |
UI 组件的完整 props,由本聊天运行中的 put_ui_data 保存(详情) |
GET / POST /api/schedules, GET / PATCH / DELETE /api/schedules/{id}, POST .../run |
定时运行:列出、创建、改时间/暂停、删除、立即触发 |
GET /api/schedules/{id}/runs |
按时间倒序列出定时任务的运行记录 |
GET /api/workspace · /files · /recent · /file · /raw |
读取 Agent 工作区中的文件 |
POST /api/workspace/upload?agent= |
将一个文件上传到工作区的 uploads/ 目录;受扩展名白名单和大小上限约束 |
GET / PUT /api/memory?agent= |
读取 / 替换 Memory Notes({content, used, budget, dreamed_at}) |
POST /api/memory/dream?agent= |
立即整理 Memory Notes,返回 {content, used, budget, dreamed_at, before, after} |
GET /api/config · POST /api/config/models · PUT/DELETE /api/config/models/{id} · PUT /api/config/roles · PUT /api/config/search · PUT/GET /api/config/skills · POST /api/config/test |
UI 内部接口,服务于 CLI 内置的设置界面,不属于稳定 API:CLI 默认 Agent 的模型配置与 Skill 目录——仅当 /api/info 报告 features.model_config 时存在。密钥只写不读(null 保留、"" 清除;读取返回 {set, hint});写入会整体校验、持久化并热切换所服务的 Agent。GET /api/config/skills 返回逐目录的发现报告 |
POST /api/chat 和 /api/chat/stream 的请求体为 {message, session_id?, agent?, attachments?, context?},
POST /api/schedules 同样接受 context。context 是一个不超过 64 KB 的 JSON 对象,即
create_app(run_deps=...) 以 trigger.client_context 收到的客户端上下文(见运行依赖)。
加入一个进行中运行的消息不会改变那次运行的上下文。
/api/chat/stream 还接受 busy,决定对话已有 Run 在执行时如何处理这条消息:"inject"(默认)
把它作为追加指令注入该 Run;"reject" 改为返回 409 run_active,绝不注入——用于必须单独
启动一次 Run 的消息,例如排队到本轮结束后再发送的 follow-up。请求到达时对话已经忙碌,会在执行
任何逻辑、写入任何数据之前拒绝;期间另有 Run 启动时同样拒绝,既不注入,也不启动新 Run。
生命周期事件¶
GET /api/events 使用 GET + EventSource,推送以下事件:
| 生命周期事件 | 数据 |
|---|---|
run_started |
{session_id, run_id, agent, source} |
run_finished |
{session_id, run_id, status, error, source} |
session_created |
{session_id, agent, title} |
session_retitled |
{session_id, title} |
config_changed |
{configured, model, profile_id, name}:重新拉取 /api/config 即可同步 |
事件流不重放历史;客户端每次连接或重连时,
应先通过 /api/sessions 和 /api/runs 获取一次当前状态,再处理后续事件。订阅者处理过慢时,
服务端会关闭连接,客户端仍按上述流程恢复。如需补查断线期间已经结束的 Run,可调用
/api/runs/history 并传入 since。
其他行为¶
- 同一 Session 中已有 Run 正在执行时,再次发起聊天流会连接到现有 Run,而不是另起 Run 或报错。
- Workspace 的浏览和读取路由始终以只读方式访问工作区,不受 Agent 自身模式影响,并沿用
Agent 的
denied_paths。POST /api/workspace/upload是唯一的写入例外,只能在uploads/目录中新建文件,并会检查扩展名和大小。文件列表会过滤__pycache__、*.pyc、venv、node_modules等可重新生成的内容,也不会显示隐藏文件。
阻塞式 Turn¶
POST /api/chat 与聊天流启动同样的托管 Run,并在 Run 结束后返回结果。因此在整个 Turn
期间 Run 都占用该聊天:同一聊天的另一个 /api/chat 返回 409 run_active;聊天流的消息、
子 Agent 的报告或定时任务的触发都会作为追加指令注入这个 Run,此时 output 是 Run 最后的
回答。/api/chat/cancel、删除聊天或服务端关闭都会停止它(409 run_cancelled);失败返回
500 run_failed;它也计入并发 Run 上限(429)。客户端中途断开不会停止它:Run 会像断开的
聊天流一样执行完毕并写入聊天。
阻塞式 Turn 的审批请求没有客户端能看到,因此需要审批的 Tool 调用不会等待决定:由 Agent 的
approval_handler 决定,否则拒绝——与 Runner.run 相同。
聊天 SSE 流¶
POST /api/chat/stream 和 /api/chat/reconnect 返回 text/event-stream。每条消息由
event: 和 data: 组成,对应 Runner 的类型化事件;data:
使用 JSON 编码。
字段后的 ? 表示该字段可能不存在。
| SSE 事件 | 数据 |
|---|---|
session |
{session_id, run_id}:每个聊天流的第一条事件,标明对话和所跟随的 Run(即其生命周期事件中的 run_id) |
snapshot |
{session_id, status, entries}:重连时的当前状态,包含已完成的 Turn |
text_delta |
{delta} |
reasoning_delta |
{delta} |
output_discarded |
{}:清除当前 Turn 已显示的增量内容 |
message_completed |
{message}:完整的模型回复 |
user_injected |
{content, turn} |
tool_call |
{id, name, arguments}:arguments 是原始 JSON 字符串 |
tool_result |
{id, name, result, is_error, images?}:images 列出 {index, mime_type},图片本身通过 GET /api/sessions/{id}/tool-images/{call_id}/{index} 获取 |
todo |
{call_id, name, todos}:结构化 todo 更新,替代该调用的 tool_result |
approval_required |
{id, name, arguments} → 通过 POST /api/chat/approve 回答 |
handoff |
{from, to} |
turn_started |
{turn, agent} |
retrying |
{attempt, max_attempts, delay, message}:模型调用遇到暂时性错误,delay 秒后进行第 attempt + 1 次尝试 |
context_compacted |
{session_id, reason, reactive, summary, tokens_before, tokens_after, detail}:上下文压缩通知 |
error |
{type, message, code, status_code?, retryable?, hint?}:Tool 错误,或聊天流终止前的运行错误 |
refs |
{refs}:刚结束的 Run 中各条用户消息的 ref,按顺序排列;在 done 之前发送。未配置 checkpointer 时不发送;Run 中含有流里从未显示过的用户条目(结构化输出的修复提示)时也不发送,下次加载对话时会补上 |
timing |
{finished_at, duration}:刚结束的 Run 在其 segment 中记录的完成时间(epoch 秒)和用时(秒),实时显示因此与刷新后一致;在 done 之前发送。segment 未记录用时时不发送 |
done |
{output, usage}:Run 成功结束 |
error 事件的 code 标明错误类别;status_code 和 retryable 来自 Provider 错误,
hint 来自带有提示的 lovia 错误:
| 运行错误码 | 含义 |
|---|---|
tool_error |
Tool 调用失败;模型会看到错误,通常能继续处理 |
provider_auth |
Provider 拒绝了凭据(401 / 403) |
rate_limited |
Provider 限流(429) |
overloaded |
Provider 过载(503 / 529) |
timeout |
Provider 请求超时 |
network |
与 Provider 的连接失败 |
provider_error |
其他 Provider 错误 |
context_overflow |
提示词超出模型的上下文窗口 |
budget_exceeded |
触发了 RunBudget 限制 |
max_turns |
Run 达到 max_turns 仍未完成 |
cancelled |
Run 被取消 |
guardrail |
Guardrail 拒绝了输入或输出 |
output_invalid |
输出无法解析为 Agent 的 output_type |
internal |
其他错误 |
聊天流不使用 Last-Event-Id。连接中断后,客户端重新 POST /api/chat/reconnect,会依次收到
最新的 snapshot、当前 Turn 的事件回放和后续实时事件;尚未处理的 approval_required
也会重放。如果客户端处理过慢导致连接关闭,恢复方式相同。以 : 开头的注释行是保活信号,
应直接忽略。
内置浏览器客户端¶
lovia/web/static/js/api.js 是零依赖客户端,封装了聊天、Session、定时任务、Workspace 和
Memory 等接口。解析 JSON 的方法在请求失败时抛出带有 status、code 和 hint 的 Error。
流式方法(streamChat、reconnect)和不返回数据的操作(approve、answer、cancel、
deleteSession、deleteAllSessions)则直接返回原始 Response;失败时可用
apiError(response) 构造同样的 Error,该函数也已导出,可用于自己的 fetch 调用。
readSSE(response) 用于异步遍历流中的 {event, data}。
import { api, readSSE } from "./api.js";
const res = await api.streamChat({ message: "hello" });
for await (const { event, data } of readSSE(res)) {
if (event === "text_delta") render(data.delta);
}
可以把它复制到自己的前端里,也可以将其作为其他语言客户端的参考实现。代码量很小,便于移植。 它和页面的其他 JavaScript 模块一样不在稳定范围内。
ChatStore¶
ChatStore 组合了 API 所需的几类存储:保存 transcript 的 Session、保存标题、时间戳、
置顶状态和可恢复 active_run_id 的 ChatMeta 表,以及 checkpointer、定时任务表和运行记录表。
ChatStore.sqlite(path, wal=True) 将这些数据放进同一个文件,并使用 WAL 模式,
使共用该文件的几个存储不必互相排队(见 Web 服务);ChatStore.in_memory() 适合
测试和演示;ChatStore(session=..., meta_path=...) 可以接入自定义 Session 后端,
同时保留聊天元数据功能。
使用建议¶
build_api_router本身不包含认证或限流。create_app(token=...)可以加上 token 验证,serve()在非回环地址上从不匿名开放 API(见认证)。单一共享 token 只适合单用户场景;多用户身份、权限和配额应由网关 负责。cors_origins默认为空,只有显式配置后才会发送 CORS 响应头。- 聊天 SSE 由 POST 发起。
/api/chat/stream和/api/chat/reconnect需要使用fetch+ reader,不能使用原生EventSource;GET /api/events则专门供EventSource使用。 tool_result里的result是原始值(JSON-safe 形态),和ToolCallCompleted一样有双重性。需要结构化数据时读取result,否则回退到字符串。snapshot按 Turn 记录,不按 token 记录。 如果在回复中途重连,服务端会重新发送 当前 Turn 已产生的 delta。客户端应按 Turn 做幂等渲染,或在收到snapshot后清空当前 Turn 的临时内容。
延伸阅读¶
- Web 服务端:承载这些路由的服务端配置
- 流式输出:同一套事件的进程内形式
- 示例:
27_web_api.py