服务定位
kira-agent 是 Kira 的独立 AI agent 服务,持有 chat agent run + SSE 流。它从 kira-be 拆出,目标是让 agent 长任务跟 kira-be 的 CRUD 流量解耦 —— deploy kira-be 不再卡住 LLM 长任务。- 公网入口
https://agentapi.kira.art,浏览器直连(不经 kira-be 透传) - 核心 6 个 agent endpoint 都在这里(
/task、/task/:id、/stream、/thread/:id/current-task、/messages等),外加根信息GET /(返{service:"kira-agent",version})、GET /health,以及从 kira-be 搬来的POST /video/submit/:taskId(重试失败视频生成) - kira-be 的
src/ai/*已是 stale/dead code,agent 逻辑全在本服务
kira-be 不再持有 agent。任何指向
POST /agent/streaming 的旧文档/客户端都已失效 —— 新客户端直连 agentapi.kira.art。技术栈
| 类别 | 技术 | 说明 |
|---|---|---|
| 运行时 | Bun 1.3+ | Bun.serve,idleTimeout=0(SSE 长连接) |
| Web 框架 | Hono | eitherAuth 中间件 + streamSSE |
| LLM | Alibaba Qwen3.7-Plus(DashScope 直连) | 所有 tier(lite/nova/ultra)同一模型,详见 Models |
| AI SDK | Vercel AI SDK(ToolLoopAgent) | tool loop,stepCountIs(10) |
| Broker / Cache | Aiven Dragonfly(rediss://,TLS) | 独立实例 kira-agent-dragonfly,run↔stream 解耦 |
| DB | shared Supabase(PostgreSQL) | messages / threads / thread_version / user_profiles |
| 存储 | Cloudflare R2(经 kira-cdn) | 生成图/视频资产,cdn-client.ts |
| 异步任务 | Trigger.dev Cloud | 触发视频 task(tasks.trigger(taskId, payload, { tags, priority }),SDK @trigger.dev/sdk/v3),由 kira-video-worker 消费 |
| 实时 | Centrifugo | 视频结果回推(由 worker 触发) |
| 观测 | OpenTelemetry → kira-otel-collector → Dash0 | traces / metrics / logs |
| 分析 | PostHog | chat_started / chat_completion / tool_usage |
项目结构
src
bootstrap.ts
index.ts
env.ts
auth
ai
hono
lib
telemetry
核心设计:run ↔ stream 解耦
agent run 跟 SSE handler 通过 Redis broker 解耦。浏览器断开 ≠ agent 断开。POST /task起一个 detached agent run promise,登记到 in-memory registry,然后立刻返回201 {taskId}。run 持续把 chunk 批量XADD到agent:stream:{taskId}。GET /stream/:taskId是独立 reader,用一条独占 blocking 连接XREAD BLOCK STREAMS agent:stream:{taskId} <lastId>。浏览器断了 reader 退出,run 不停。重连(可能落到任意 instance)从Last-Event-ID续读。- run 跑完
onFinish写 DB(messages+thread_version)、触发视频 Trigger.dev task(tasks.trigger,经TRIGGER_SECRET_KEY鉴权),再给 stream/task 设短 TTL 自然蒸发。
多 instance 协作 —— 不用 PUB/SUB
服务多 instance(fra / nrt / sjc 三区),协调全靠 Redis key,没有 pub/sub:- dedup lock:
SET NX agent:lock:{threadId} {instanceId}:{taskId} EX 60。同一 thread 不能并发跑两个 task。run 每 20s heartbeat 续 TTL(check-and-set,只有 owner 能续);锁过期 = 原 owner 死了。 - abort 信号:
SET agent:abort:{taskId} 1 EX 600,run loop 每 2sEXISTS轮询命中就controller.abort()。简单 poll,无 pub/sub。 - registry:in-memory
Map只为 SIGTERM graceful drain 用(本机要知道等谁跑完)。
Task 生命周期
aborted 不是独立 state,而是 completed 的子情况 —— 由”DB 写不写”区分。详见 Task Status。
SSE 帧约定
reader 把 stream entry 翻译成标准 SSE 帧。每帧带id(= Redis stream entry id),供客户端做 Last-Event-ID 续读。
event: done 或 event: error 就关闭连接。详见 Stream API。
Graceful drain(SIGTERM)
不依赖Bun.serve 的 closeActiveConnections(行为不确定),自己实现:
超时 abortAll
仍有在途 →
abortAll() 让 SDK 清 LLM 连接,再给 60s cleanup window(onFinish 写 DB / XADD error frame)。fly.toml 的 kill_signal=SIGTERM + kill_timeout=300s 兜底。Bun.serve({ idleTimeout: 0 }) 禁掉默认 10s idle 关 socket(否则 LLM 思考期间 SSE 会被误关触发 fly-proxy PU02)。
部署
| 项 | 值 |
|---|---|
| Fly app | kira-agent |
| 主区域 | sjc(primary_region);运行时多区 fra / nrt / sjc |
| VM | performance,4 cpu / 8 GB |
| internal_port | 8090 |
| 进程管理 | auto_stop_machines=off,auto_start_machines=on,min_machines_running=1 |
| 公网入口 | agentapi.kira.art(经 Cloudflare 路由) |
| secrets | fly secrets set ...,清单见 .env.example,真值在 Notion Key 页 ## kira-agent |
观测
OTel HTTP instrumentation(@hono/otel)自动产生 server span + http.server.request.duration。run / tool 维度的 metric、log、span 全部走 kira-otel-collector → Dash0:
ai.chat.count{model, kira_model}—— run 启动计数ai.tool.duration{tool, success, error_type}—— 每个 tool 的耗时直方图- run-finish 计数(completed / aborted / failed)
- PostHog 业务事件
chat_started/chat_completion/tool_usage用同一task_id串联