跳转至

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

有两种接入方式。最简单的是关闭内置页面:

from lovia.web import create_app

app = create_app(agent, ui=False)   # 没有 GET /,也没有 /static;只有 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 路由抛出的错误都使用同一种响应体:

{"detail": {"code": "session_not_found", "message": "session not found", "hint": "..."}}

客户端应根据 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 的临时内容。

延伸阅读