Kiro CLI 封装 REST API 实战:ACP 协议双通道架构设计与 FastAPI 集成详解(附完整源码)

Kiro CLI 只有 stdio 接口,想让别的服务调它?门都没有。

我最近在搞一个自动化编码流水线,需要把亚马逊云科技的 Kiro CLI 集成进来。结果发现这玩意儿只能在终端里交互,没有 HTTP 接口,没有 SDK,连个 socket 都没有。说白了,它就是个"你打字我回答"的命令行工具。

但我就是要让它变成 REST API。折腾了一周,踩了不少坑,总算搞出来了。今天把整个过程分享出来。

先说结论:两个文件,600 行搞定

最终产出就两个文件:

kiro-api/
├── acp_client.py   # ACP JSON-RPC 2.0 客户端(~300 行)
└── server.py       # FastAPI HTTP 服务(~280 行)

用 FastAPI 起个服务,端口 8642,7 个 REST 端点,支持多轮会话和模型切换。完整实现代码见下文。

ACP 协议:Kiro CLI 的"隐藏接口"

Kiro CLI 有个子命令 kiro-cli acp,启动后通过 stdio 进行 JSON-RPC 2.0 双向通信。这就是 ACP(Agent Communication Protocol)协议。

核心方法就这几个:

方法 方向 干嘛的
initialize Client → Kiro 握手,声明能力
session/new Client → Kiro 创建编码会话
session/prompt Client → Kiro 发任务,等结果
session/update Kiro → Client 流式推文本块(通知)
session/request_permission Kiro → Client 敏感操作审批

看着挺清晰对吧?但坑全藏在细节里。

模型切换:试了 8 种方式,全失败

我一开始的想法很简单:通过 ACP 协议指定模型。比如在 session/new 里加个 model 参数。

结果呢?

尝试方式 结果
session/new 加 model 参数 被忽略,还是默认模型
session/setModel 方法 Method not found
session/configure 方法 Method not found
_kiro.dev/commands/execute/model 进程直接崩了(反序列化错误)
kiro-cli acp --model X unexpected argument
kiro-cli-chat acp --model X unexpected argument
环境变量 KIRO_MODEL 被忽略
~/.kiro/settings/cli.json 配置 被忽略

8 种方式,全军覆没。 差点掀桌子。

抓包分析了半天,结论是:ACP v1.25 协议设计上就不支持运行时切换模型。这不是 bug,是 feature(苦笑)。

但我发现了个替代方案:kiro-cli chat --model X --no-interactive。这个命令支持指定模型,但只能一次性运行,不支持多轮会话。

双通道架构:既要又要的工程方案

既然一条路走不通,那就两条路一起走。

HTTP 客户端(curl / Python / JS)
        |
        | HTTP REST (port 8642)
        v
  FastAPI Server (server.py)
        |
   -----+--------------
   |                   |
   | model=auto        | model=指定模型
   v                   v
 ACP 通道            Chat 通道
 acp_client.py       subprocess
 (常驻进程)          (一次性进程)
 多轮会话            单次调用

这就是双通道架构的核心思路:

ACP 通道 Chat 通道
协议 JSON-RPC 2.0 over stdio subprocess + 文本解析
模型选择 不支持(固定 auto) 支持 --model 参数
多轮会话 支持 不支持
性能 无冷启动,延迟低 每次冷启动约 3 秒
适用场景 多轮对话、上下文复用 指定模型的单次任务

设计原则:优先走 ACP 通道(性能好、支持多轮),只有在需要指定模型时才降级到 Chat 通道。

关键实现:Event + Pending Map 同步语义

ACP 底层是异步的 stdio 通信。你往 stdin 写一条请求,stdout 上可能先来几条通知,然后才是你的响应。怎么把异步变同步?

核心思路:每个请求分配唯一 id,用 Event + Pending Map 做同步等待

# 发请求时:注册等待
self._pending[req_id] = (threading.Event(), [None, None])

# 写入 stdin
self._proc.stdin.write(json_msg + "\n")

# 阻塞等待
event.wait(timeout)

# 读线程收到响应后:按 id 匹配,写入结果并唤醒
if msg["id"] in self._pending:
    holder[0] = msg["result"]
    event.set()  # 唤醒等待线程

这套机制保证了:不管 Kiro 推了多少通知过来,你的请求一定能拿到对应的响应。

踩坑记录:session/update 才是文本载体

这个坑我踩了好几个小时。

session/prompt 发出去之后,最终响应长这样:

{"jsonrpc": "2.0", "id": 3, "result": {"stopReason": "end_turn"}}

里面没有文本内容! 一开始我以为是解析错了,反复检查了好几遍。

实际的文本是通过多个 session/update 通知逐块推过来的:

← {"method": "session/update", "content": {"type": "text", "text": "```python"}}
← {"method": "session/update", "content": {"type": "text", "text": "\ndef fibonacci(n):"}}
← ...(更多文本块)
← {"jsonrpc": "2.0", "id": 3, "result": {"stopReason": "end_turn"}}

所以正确的处理方式是:在 _session_updates[session_id] 列表里收集所有 chunk,收到 end_turn"".join() 拼接。

这个在官方文档里没明确说。 得自己抓包才能发现。

权限请求:不回复就永远挂死

又一个坑。Kiro 在执行文件写入、终端命令这些敏感操作之前,会发一个权限请求:

{"jsonrpc": "2.0", "id": 99, "method": "session/request_permission",
 "params": {"toolCall": {"title": "Creating app.py"}}}

注意,这是个带 id 的 request,不是通知。你必须回复,否则 Kiro 会永远等下去。 整个请求就挂死了。

在 headless 模式下,我的处理很简单:自动审批。

def _handle_permission_request(self, msg_id, params):
    self._send_response(msg_id, {"optionId": "allow_always"})

Chat 通道的输出清洗

Chat 通道的坑在于:kiro-cli chat 的 stdout 是给人看的,不是给程序解析的。

原始输出里混着这些东西:

  • ASCII 艺术 banner
  • “Did you know?” 提示框
  • “Model: claude-opus-4.6 | Plan: KIRO PRO+” 信息行
  • > 前缀的实际回复内容 ← 这才是我要的
  • “▸ Credits: 0.09 • Time: 3s” 页脚

清洗流程:

  1. 正则去掉所有 ANSI 转义码
  2. 跳过 banner、提示框、Model/Plan 行
  3. 找到 > 标记,提取后续内容
  4. 遇到 ▸ Credits 截止,丢弃页脚

说实话,这种文本解析挺脆弱的。Kiro CLI 版本一更新,输出格式可能就变了。但目前没有更好的办法。

对外 REST API:7 个端点

最终 FastAPI 服务提供这些接口:

接口 说明
POST /prompt 快捷调用,支持指定模型
POST /sessions 创建多轮会话(仅 auto 模型)
POST /sessions/{id}/prompt 向已有会话发送任务
GET /sessions 列出所有活跃会话
DELETE /sessions/{id} 删除指定会话
GET /models 列出可用模型
GET / 健康检查

调用示例:

# 快速提问
curl -X POST http://localhost:8642/prompt \
  -H "Content-Type: application/json" \
  -d '{"prompt": "写一个 Python 快速排序", "model": "auto"}'

# 创建会话 → 多轮对话
curl -X POST http://localhost:8642/sessions
curl -X POST http://localhost:8642/sessions/abc123/prompt \
  -d '{"prompt": "帮我重构这段代码"}'

stdio 双向通信的死锁陷阱

这个问题比较隐蔽。Kiro CLI 进程的 stdout 和 stderr 都有输出。如果你只读 stdout 不管 stderr,stderr 的缓冲区满了之后,整个进程会阻塞——因为写不进去了。

所以 acp_client.py 里必须单独开一个线程来排空 stderr:

# 专门的 stderr 读取线程,防止缓冲区满导致死锁
def _drain_stderr(self):
    for line in self._proc.stderr:
        pass  # 读掉就行,不需要处理

这个坑在 Windows 上尤其明显,因为 Windows 的管道缓冲区更小。Linux 上也不能忽视,高频输出时照样会阻塞。

已知限制

说完好处,也得说局限:

  • 无鉴权:API 没做认证,只适合内网用,别暴露到公网
  • 单 ACP 连接:高并发请求需要排队处理
  • 会话不持久化:服务重启后会话丢失
  • Chat 模式无上下文:每次都是新进程,不支持多轮

这些限制在个人使用和团队内网场景下可以接受。如果要上生产,还需要加鉴权、连接池、持久化这些工程化的东西。

总结

类别 结论
协议限制 ACP v1.25 不支持运行时切换模型,协议设计限制
架构决策 双通道:多轮走 ACP,指定模型走 Chat
关键发现 session/update 通知才是文本载体
关键发现 权限请求必须回复,否则永久挂起
关键发现 Chat 输出混杂 UI 元素,需专门清洗
关键发现 stdio 双向通信必须读写线程分离

把 CLI 工具封装成 API,核心难点不在 HTTP 层,而在于理解协议的实际行为。官方文档没覆盖的部分,只能自己调试和抓包。

完整源码已在上文逐段展示,可直接复制使用。


本文基于亚马逊云科技官方博客内容整理,结合实际开发经验撰写。原文参见:将 Kiro CLI 封装为 REST API:双通道架构实践

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐