CMDC Gateway API 参考

Copy Markdown View Source

所有端点的完整请求/响应规范。Base URL 示例: http://localhost:4000


目录


认证

所有 /v1/* 端点需要 API Key 认证。支持两种方式(二选一):

方式请求头示例
X-API-Key 头X-API-KeyX-API-Key: sk-abc123
Bearer TokenAuthorizationAuthorization: Bearer sk-abc123

API Key 在服务端配置中绑定 tenant_id,用于多租户隔离:

config :cmdc_gateway, CMDCGateway.Plugs.Auth,
  api_keys: %{
    "sk-abc123" => "tenant-a",
    "sk-xyz789" => "tenant-b"
  }

认证失败响应:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": "unauthorized",
  "message": "Missing or invalid API key"
}

/healthz 端点无需认证。


通用错误格式

所有错误响应遵循统一 JSON 结构:

{
  "error": "<error_code>",
  "message": "<人类可读描述>"
}
HTTP 状态码error 代码触发条件
400bad_request缺少必要参数(如 prompt 的 text、approve 的 approvalId
401unauthorizedAPI Key 无效、缺失、或未配置
404not_foundSession ID 不存在
404session_deadSession 存在但 Agent 进程已终止
422create_failedAgent 创建失败(模型不支持、参数非法等)
422registration_failed回调工具注册失败
429rate_limited超出请求频率限制

429 特殊响应头:

HTTP/1.1 429 Too Many Requests
Retry-After: 15
Content-Type: application/json

{
  "error": "rate_limited",
  "message": "Rate limit exceeded. Retry after 15 seconds.",
  "retryAfter": 15
}

404 路由不匹配:

{
  "error": "not_found",
  "message": "No route matches GET /v1/unknown"
}

健康检查

检查 Gateway 服务状态,无需认证。

GET /healthz

请求:

GET /healthz HTTP/1.1
Host: localhost:4000

无请求体、无认证。

响应 200:

{
  "status": "ok",
  "version": "0.6.0",
  "sessions": {
    "active": 5
  },
  "meter": {
    "tracked_keys": 3
  },
  "timestamp": "2026-04-08T12:00:00.000000Z"
}
字段类型说明
statusstring固定 "ok"
versionstringGateway 版本号
sessions.activeinteger当前存活的 Session 数量
meter.tracked_keysinteger有用量记录的 API Key 数量
timestampstringISO 8601 UTC 时间戳

Agent Card / A2A

Agent Card 用于外部 registry / orchestrator 发现 Gateway 的协议能力。A2A JSON-RPC 端点复用相同 API Key 认证;/.well-known/agent.json 可按部署公开。

GET /.well-known/agent.json

响应 200:

{
  "a2aVersion": "1.0",
  "name": "cmdc-gateway",
  "version": "0.6.0",
  "capabilities": {
    "a2a": true,
    "jsonRpc": true,
    "streaming": true,
    "serverSentEvents": true,
    "webSocket": true,
    "webhook": true,
    "taskPolling": true,
    "groupEvents": true,
    "auditProjection": true,
    "sessionReplay": true,
    "workflowReplay": true
  },
  "endpoints": {
    "a2aSend": {"method": "POST", "url": "http://localhost:4000/v1/a2a/tasks/send"},
    "a2aSendSubscribe": {"method": "POST", "url": "http://localhost:4000/v1/a2a/tasks/sendSubscribe"},
    "a2aSendWithWebhook": {"method": "POST", "url": "http://localhost:4000/v1/a2a/tasks/sendWithWebhook"},
    "a2aTaskStatus": {"method": "GET", "url": "http://localhost:4000/v1/a2a/tasks/{taskId}"}
  }
}

POST /v1/a2a/tasks/send

同步 JSON-RPC task。Gateway 会创建或复用 task-scoped session,投递单次 prompt,并等待 Agent 完成或超时。

{
  "jsonrpc": "2.0",
  "id": "req-1",
  "method": "tasks/send",
  "params": {
    "id": "task-001",
    "message": {
      "role": "user",
      "parts": [{"type": "text", "text": "总结这个仓库"}]
    },
    "agent_config": {
      "model": "deepseek:deepseek-chat",
      "workingDir": "."
    },
    "timeout_ms": 60000
  }
}

POST /v1/a2a/tasks/sendSubscribe

流式 JSON-RPC task。请求体与 tasks/send 相同,响应为 SSE,事件 payload 为 A2A TaskStatusUpdateEvent / TaskArtifactUpdateEvent 形态。

POST /v1/a2a/tasks/sendWithWebhook

异步 webhook task。立即返回 accepted;后续状态通过 callbackUrl 接收。

{
  "jsonrpc": "2.0",
  "id": "req-2",
  "method": "tasks/sendWithWebhook",
  "params": {
    "id": "task-002",
    "callbackUrl": "https://client.example.com/cmdc/webhook",
    "webhookSecret": "shared-secret",
    "message": {
      "role": "user",
      "parts": [{"type": "text", "text": "跑一次长任务"}]
    }
  }
}

Webhook 会携带 X-CMDC-Signature: sha256=<hex>;签名输入为原始 JSON body。 同步派发失败后进入 dead-letter dispatcher,后台按指数退避重试。

GET /v1/a2a/tasks/:task_id

查询 webhook / SSE 客户端漏接时的短期 task 兜底状态。

{
  "taskId": "task-002",
  "status": "completed",
  "lastEvent": "completed",
  "payload": {"event": "task.completed", "taskId": "task-002"},
  "updatedAtMs": 1780267200000,
  "ttlUntilMs": 1780267800000
}

该缓存由 CMDCGateway.TaskStore 提供,默认短期 ETS 存储,不替代业务持久化。


Session 管理

创建 Session

创建一个 CMDC Agent Session,Agent 进程随即启动并进入 idle 状态。

POST /v1/sessions

请求头:

Content-Type: application/json
X-API-Key: sk-abc123

请求体:

{
  "model": "deepseek:deepseek-chat",
  "sessionId": "my-session-001",
  "systemPrompt": "你是一个专业的编程助手",
  "workingDir": "/home/user/project",
  "tools": ["CMDC.Tool.Shell", "CMDC.Tool.ReadFile", "CMDC.Tool.WriteFile"],
  "plugins": ["CMDC.Plugin.Builtin.ApprovalGuard"],
  "blueprint": "CMDC.Blueprint.Base",
  "groupId": "agentops-run-001",
  "eventBufferSize": 512,
  "maxSteeringQueue": 5,
  "maxTurns": 50,
  "maxTokens": 4096,
  "skillsDirs": ["/home/user/skills"],
  "messages": [
    {"role": "user", "content": "之前我们在审查 billing 模块"},
    {"role": "assistant", "content": "我已经看过入口文件,下一步看测试。"}
  ],
  "providerOpts": {
    "temperature": 0.7,
    "top_p": 0.95
  }
}

请求体字段:

字段类型必填默认值说明
modelstringLLM 模型标识。格式 provider:model,如 "deepseek:deepseek-chat""anthropic:claude-sonnet-4-20250514"
sessionIdstring自动生成 16 位 hex自定义 Session ID,用于后续所有操作的标识符
systemPromptstringBlueprint 默认值Agent 系统提示词,决定 Agent 的行为和人格
workingDirstring"."工具(Shell、ReadFile 等)的工作根目录
toolsstring[][]启用的 CMDC Tool 模块名列表,使用 Elixir 模块全名
pluginsstring[][]启用的 CMDC Plugin 模块名列表
blueprintstringnilBlueprint 模块名,定义 Agent 的完整配置模板
groupId / group_idstringnilcore 0.6 group event stream 标识
hibernateAfterMs / hibernate_after_msintegercore 默认idle 后 hibernate 配置
eventBufferSize / event_buffer_sizeinteger0per-session EventBus ring buffer 大小;开启后可用 SSE since replay
maxSteeringQueue / max_steering_queueintegercore 默认steering queue 上限
memorystring[][]AGENTS.md 等服务端工作目录内记忆文件
subagentsobject[][]子 Agent 安全 JSON 规格,字段同 core whitelist
interruptImmuneTools / interrupt_immune_toolsstring[][]abort/steer 时不杀掉的工具名
responseFormat / response_formatobjectnil透传给 core 的响应格式约束
messagesobject[][]安全历史导入;仅允许 user / assistant / tool_result
maxTurnsinteger100最大 Agent 轮次数(一次 prompt → response 为一轮)
maxTokensinteger模型默认LLM 单次回复最大输出 token 数
skillsDirsstring[][]Skill 文件扫描目录列表
providerOptsobject{}透传给 LLM Provider 的额外参数(如 temperaturetop_p

messages 导入限制:

  • 角色只允许 "user""assistant""tool_result""system" 会返回 invalid_messages
  • 单次最多 100 条,单条文本最多 32KB,总 JSON 内容最多 256KB。
  • assistant.toolCalls[] 只接受 callId/call_idnamearguments object。
  • Gateway 会重新构造 %CMDC.Message{},忽略外部 id/parent_id;任何 __struct__ / module 字段都会被拒绝。
  • 任意 skillSelector / skill_selector 模块注入不通过 public JSON;请在宿主 Elixir app 服务端配置。

响应 201 Created:

{
  "sessionId": "my-session-001",
  "status": "created",
  "groupId": "agentops-run-001",
  "importedMessages": 2
}
字段类型说明
sessionIdstringSession 标识符(自定义或自动生成)
statusstring固定 "created"
groupIdstring | null当前 session 的 group id
importedMessagesinteger本次安全导入的历史消息数

响应 422 Unprocessable Entity:

{
  "error": "create_failed",
  "message": ":unknown_provider"
}

cURL 示例:

curl -X POST http://localhost:4000/v1/sessions \
  -H "X-API-Key: sk-abc123" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek:deepseek-chat",
    "systemPrompt": "你是助手",
    "tools": ["CMDC.Tool.Shell"],
    "maxTurns": 20
  }'

查询 Session

获取 Session 的当前运行状态。GET /v1/sessions/:idGET /v1/sessions/:id/status 当前返回相同结构;保留两个路径是为了兼容旧 SDK 和 dashboard。

GET /v1/sessions/:id

GET /v1/sessions/:id/status

路径参数:

参数类型说明
idstringSession ID

请求:

GET /v1/sessions/my-session-001 HTTP/1.1
X-API-Key: sk-abc123

响应 200:

{
  "sessionId": "my-session-001",
  "state": "idle",
  "model": "deepseek:deepseek-chat",
  "groupId": "agentops-run-001",
  "workingDir": "/home/user/project",
  "turns": 3,
  "toolCalls": 5,
  "totalTokens": 12800,
  "costUsd": "0.0123",
  "tokenUsage": {
    "promptTokens": 8200,
    "completionTokens": 4600,
    "totalTokens": 12800,
    "costUsd": "0.0123",
    "cachedTokens": 0
  },
  "uptimeMs": 45200,
  "activeSinceMs": null,
  "timestampMs": 1780267200000,
  "messagesCount": 8,
  "pendingTools": [],
  "pendingApprovals": [],
  "queues": {
    "promptQueue": 0,
    "steeringQueue": 0
  },
  "eventBufferSize": 512,
  "lastEventIndex": 42,
  "eventBufferCount": 42,
  "maxSteeringQueue": 5,
  "hibernateAfterMs": 60000
}
字段类型说明
sessionIdstringSession ID
statestringAgent 状态机当前状态,如 "idle" / "running" / "streaming" / "executing_tools"
modelstring当前模型,模型切换为异步控制,最终值以 status 为准
groupIdstring | null当前 session 所属 group
workingDirstring服务端校验后的工作目录
turnsinteger已完成的对话轮次数
toolCallsinteger累计工具调用次数
totalTokensinteger累计消耗 token 数
costUsdnumber | string | nullcore 统计的成本估算
tokenUsageobjectcore CMDC.TokenUsage 摘要
uptimeMsintegerSession 存活时间(毫秒)
pendingToolsarray等待中的工具调用摘要
pendingApprovalsarray等待人类审批的请求
queues.promptQueueintegerprompt 队列长度
queues.steeringQueueintegersteering 队列长度
eventBufferSizeintegersession replay ring buffer 配置大小
lastEventIndexinteger当前 EventBus 最新事件 index
eventBufferCountintegerring buffer 当前保留事件数

响应 404:

{
  "error": "not_found",
  "message": "Session my-session-001 not found"
}

删除 Session

停止 Agent 进程,清理 Session 数据和已注册的回调工具。

DELETE /v1/sessions/:id

路径参数:

参数类型说明
idstringSession ID

请求:

DELETE /v1/sessions/my-session-001 HTTP/1.1
X-API-Key: sk-abc123

响应 200:

{
  "sessionId": "my-session-001",
  "status": "deleted"
}
字段类型说明
sessionIdstring被删除的 Session ID
statusstring固定 "deleted"

副作用:

  1. Agent 进程被 CMDC.stop/1 终止
  2. SessionStore 中的条目被删除
  3. 该 Session 注册的所有 CallbackTool 被清理
  4. 活跃的 SSE/WebSocket 连接收到最终事件后断开

Prompt API

向 Agent 发送用户消息(异步)。消息被投递到 Agent 状态机后立即返回 202,后续处理结果通过 SSE 或 WebSocket 推送。

POST /v1/sessions/:id/prompt

路径参数:

参数类型说明
idstringSession ID

请求头:

Content-Type: application/json
X-API-Key: sk-abc123

请求体:

{
  "text": "帮我用 Python 写一个快速排序"
}
字段类型必填说明
textstring用户消息文本。也接受 "prompt" 作为别名字段

响应 202 Accepted:

{
  "requestId": "a1b2c3d4e5f67890",
  "sessionId": "my-session-001",
  "queued": false
}
字段类型说明
requestIdstring本次请求的唯一 ID(16 位 hex)
sessionIdstringSession ID
queuedboolean是否排队等待(Agent 正忙时为 true

响应 400:

{
  "error": "bad_request",
  "message": "Missing 'text' field"
}

典型调用流程:

1. POST /v1/sessions/:id/prompt    202 (消息已投递)
2. GET  /v1/sessions/:id/events    SSE 流接收处理事件
   - event: agent_start
   - event: message_delta (多次 token)
   - event: tool_execution_start (如果 Agent 调用了工具)
   - event: tool_execution_end
   - event: agent_end (本轮完成)

cURL 示例:

curl -X POST http://localhost:4000/v1/sessions/my-session-001/prompt \
  -H "X-API-Key: sk-abc123" \
  -H "Content-Type: application/json" \
  -d '{"text": "帮我用 Python 写一个快速排序"}'

事件流

SSE 事件流

建立 Server-Sent Events 长连接,实时接收 Agent 事件推送。

GET /v1/sessions/:id/events

路径参数:

参数类型说明
idstringSession ID

请求:

GET /v1/sessions/my-session-001/events HTTP/1.1
X-API-Key: sk-abc123
Accept: text/event-stream

Query 参数:

参数类型说明
sinceinteger从指定 EventBus index 之后 replay;也可使用 Last-Event-ID 请求头
typesstring逗号分隔的 core event type 白名单,如 message_delta,agent_end
modestring默认轻量事件;audit 时投影为 audit_event

since/types 只支持 session SSE。group SSE 是 live-only,带 replay 参数会返回 group_replay_not_supported

响应头:

HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache
X-Accel-Buffering: no
Transfer-Encoding: chunked

SSE 数据格式:

每个事件由 event: 行(事件类型)和 data: 行(JSON 载荷)组成,以双换行分隔:

event: agent_start
data: {}

event: message_delta
data: {"delta":"你"}

event: message_delta
data: {"delta":"好"}

event: message_delta
data: {"delta":"!"}

event: agent_end
data: {"messageCount":2,"lastMessage":{"content":"你好!","role":"assistant"},"tokenUsage":{"promptTokens":50,"completionTokens":5,"totalTokens":55}}

心跳:

每 30 秒发送一次 SSE 注释行,保持连接:

: heartbeat

连接终止条件:

  • 客户端主动断开
  • 收到 agent_endagent_abort 事件后 SSE 流自动关闭
  • 60 秒无事件超时自动断开

cURL 示例:

curl -N http://localhost:4000/v1/sessions/my-session-001/events \
  -H "X-API-Key: sk-abc123"

Group SSE 事件流

订阅同一 groupId 下多个 session 的实时事件,适合 AgentOps run console 按 group 聚合展示。

GET /v1/groups/:group_id/events

Query 参数:

参数类型说明
modestring默认轻量事件;audit 时投影为 audit_event

group SSE 是 live-only:sincetypesLast-Event-ID 都会返回 group_replay_not_supported。断线补帧请使用每个 session 的 /v1/sessions/:id/events?since=...&types=...

事件示例:

event: message_delta
data: {"sessionId":"my-session-001","groupId":"agentops-run-001","delta":"hello"}

SSE 事件类型详解

基础事件按 Agent 生命周期阶段分组;启用 cmdc_rag_arcana 时还会输出 RAG / GraphRAG / Eval 专用 trace 事件。

1. 会话生命周期

agent_start — Agent 开始处理当前 prompt

event: agent_start
data: {}
data 字段类型说明
(空对象)无额外数据

agent_end — Agent 完成当前 prompt 处理

event: agent_end
data: {"messageCount":4,"lastMessage":{"content":"代码已完成","role":"assistant"},"tokenUsage":{"promptTokens":500,"completionTokens":200,"totalTokens":700}}
data 字段类型说明
messageCountinteger当前对话总消息数
lastMessageobject | null最后一条 assistant 消息,含 content(string) 和 role(string)
tokenUsageobjectToken 用量统计
tokenUsage.promptTokensinteger输入 token 数
tokenUsage.completionTokensinteger输出 token 数
tokenUsage.totalTokensinteger总 token 数

agent_abort — Agent 被中止

event: agent_abort
data: {"reason":"max_turns_exceeded"}
data 字段类型说明
reasonstring中止原因,如 "max_turns_exceeded""user_cancelled""aborted"

prompt_received — Agent 确认收到用户 prompt

event: prompt_received
data: {"text":"帮我写个排序算法"}
data 字段类型说明
textstring用户发送的 prompt 文本

2. 流式响应

message_start — LLM 开始生成回复

event: message_start
data: {}
data 字段类型说明
(空对象)无额外数据

message_delta — 流式文本片段(逐 token)

event: message_delta
data: {"delta":"def quick_sort"}
data 字段类型说明
deltastring文本增量片段,客户端应追加到当前消息

一次 LLM 回复会产生大量 message_delta 事件,客户端应逐个追加拼接完整回复。


thinking_start — Agent 思考链(Chain-of-Thought)开始

event: thinking_start
data: {}
data 字段类型说明
(空对象)仅在模型支持 thinking 功能时触发

thinking_delta — 思考链文本片段

event: thinking_delta
data: {"delta":"让我分析一下这个问题..."}
data 字段类型说明
deltastring思考过程的文本增量

3. 工具执行

tool_calls — Agent 本轮决定调用的工具数量

event: tool_calls
data: {"count":2}
data 字段类型说明
countinteger本轮工具调用总数

tool_execution_start — 工具开始执行

event: tool_execution_start
data: {"toolName":"Shell","callId":"tc_a1b2c3","args":{"command":"python3 sort.py","working_dir":"."}}
data 字段类型说明
toolNamestring工具名称
callIdstring本次调用的唯一 ID
argsobject工具调用参数(key-value 均为 string,超过 1024 字节的值会被截断并追加 ...[truncated]

tool_execution_end — 工具执行完成

event: tool_execution_end
data: {"toolName":"Shell","callId":"tc_a1b2c3","status":"ok","result":"[1, 2, 3, 4, 5]"}
data 字段类型说明
toolNamestring工具名称
callIdstring调用 ID(与 tool_execution_startcallId 对应)
statusstring执行状态:"ok" 成功 / "error" 失败
resultstring执行结果文本(超过 4096 字节会被截断并追加 ...[truncated]
ragobject | null仅 RAG 工具成功时出现,含 queryanswercitationsgroundingpipelineRunSummarygraphEvidencegraphStatusingestStatus 摘要;默认不包含 chunk 原文

RAG 工具包括 rag_searchrag_answerrag_pipeline_answerrag_graph_searchrag_graph_statusrag_ingest_status。Gateway 会保留 原始 result 字段以兼容旧客户端,同时补充 rag 结构化摘要供 AgentOps Trace Viewer / Run Console 使用。


3.1 RAG / GraphRAG Trace

以下事件来自 cmdc_rag_arcana Phase 18 插件、pipeline、maintenance、GraphRAG 和 Eval telemetry bridge。事件 payload 统一 camelCase,并默认移除 text / chunkText / content / prompt / chunks / results 等正文或 chunk 字段。

事件说明
rag_acl_blockedcollection ACL 阻断
rag_retrievedrag_search / rag_graph_search 检索摘要
rag_answeredrag_answer / rag_pipeline_answer 回答摘要
rag_citation_usedcitation provenance
rag_pipeline_stepPipeline step timeline
rag_ingestion_progressingestion job 进度
rag_reembed_progressreembed job 进度
rag_graph_progressGraphRAG rebuild/embed/community 进度
rag_graph_auditGraphRAG 只读查询审计
rag_eval_progressRAG Eval / 发布门禁进度

示例:

event: rag_citation_used
data: {"toolName":"rag_answer","callId":"tc_1","collections":["policies"],"citationCount":1,"citation":{"documentId":"doc-1","sourceUri":"kb://policies/approval"}}

4. 人机交互(HITL)

approval_required — Agent 需要人类审批才能继续

event: approval_required
data: {"approvalId":"apr_x7y8z9","toolName":"Shell","args":{"command":"rm -rf /tmp/old"},"hint":"危险操作:删除文件","requestedAt":"2026-04-08T12:00:00Z"}
data 字段类型说明
approvalIdstring审批请求 ID,用于后续 approve / reject 操作
toolNamestring待审批的工具名称
argsobject工具调用参数
hintstring审批提示信息(可能为空字符串)
requestedAtstring | null审批请求时间(ISO 8601)

收到此事件后,Agent 会暂停等待,直到客户端发送 approvereject


approval_resolved — 审批已决定

event: approval_resolved
data: {"approvalId":"apr_x7y8z9","status":"approved"}
data 字段类型说明
approvalIdstring审批请求 ID
statusstring审批结果:"approved" / "rejected"

ask_user — Agent 向用户提问,等待回答

event: ask_user
data: {"ref":"ask_001","question":"你希望使用哪种排序算法?","options":["快速排序","归并排序","堆排序"]}
data 字段类型说明
refstring提问引用 ID,用于后续 respond 操作
questionstringAgent 的问题文本
optionsarray | null可选的预设选项列表,null 表示自由文本回答

收到此事件后,Agent 会暂停等待,直到客户端发送 respond


5. 错误

error — 运行时错误

event: error
data: {"reason":"Provider stream failed: connection timeout"}
data 字段类型说明
reasonstring错误原因描述

WebSocket 双向通信

全双工连接,出站推送事件 + 入站接收控制消息。适合需要实时双向交互的场景(如审批、对话式 Agent)。

WS /v1/sessions/:id/ws

连接参数:

参数位置类型必填说明
id路径stringSession ID
api_keyQuery StringstringAPI Key(WebSocket 不支持自定义请求头,故用 query 传递)

连接 URL 示例:

ws://localhost:4000/v1/sessions/my-session-001/ws?api_key=sk-abc123

连接失败响应:

场景HTTP 状态码响应体
未提供 API Key401{"error":"unauthorized"}
Session 不存在404{"error":"session_not_found"}

连接配置:

参数说明
idle_timeout300,000 ms (5 分钟)无消息超时断开
heartbeat30 秒WebSocket ping 帧保活

入站消息(客户端 → Gateway)

所有入站消息为 JSON 格式,必须包含 action 字段。

1. 发送 Prompt

{
  "action": "prompt",
  "text": "帮我写一个 Hello World"
}
字段类型必填说明
actionstring固定 "prompt"
textstring用户消息文本

成功响应:

{"ok": true, "action": "prompt"}

2. 审批通过

{
  "action": "approve",
  "approvalId": "apr_x7y8z9"
}
字段类型必填说明
actionstring固定 "approve"
approvalIdstring来自 approval_required 事件的审批 ID

成功响应:

{"ok": true, "action": "approve"}

3. 审批拒绝

{
  "action": "reject",
  "approvalId": "apr_x7y8z9"
}
字段类型必填说明
actionstring固定 "reject"
approvalIdstring来自 approval_required 事件的审批 ID

成功响应:

{"ok": true, "action": "reject"}

4. 回答 Agent 提问

{
  "action": "respond",
  "ref": "ask_001",
  "response": "快速排序"
}
字段类型必填说明
actionstring固定 "respond"
refstring来自 ask_user 事件的引用 ID
responsestring用户的回答文本

成功响应:

{"ok": true, "action": "respond"}

入站错误响应:

场景响应
JSON 解析失败{"error": "invalid_json", "message": "Failed to parse JSON"}
缺少 action 字段{"error": "missing_action", "message": "Message must contain 'action' field"}
未知 action{"error": "unknown_action", "action": "xxx"}
Session 进程已终止{"error": "session_not_found"}

出站消息(Gateway → 客户端)

与 SSE 事件内容一致,但封装为 JSON 对象:

{
  "event": "<event_type>",
  "data": { ... }
}

示例:

{"event": "agent_start", "data": {}}
{"event": "message_delta", "data": {"delta": "Hello"}}
{"event": "message_delta", "data": {"delta": " World"}}
{"event": "tool_execution_start", "data": {"toolName": "Shell", "callId": "tc_001", "args": {"command": "echo hi"}}}
{"event": "tool_execution_end", "data": {"toolName": "Shell", "callId": "tc_001", "status": "ok", "result": "hi\n"}}
{"event": "approval_required", "data": {"approvalId": "apr_001", "toolName": "Shell", "args": {"command": "rm -rf /tmp"}, "hint": "危险操作", "requestedAt": null}}
{"event": "agent_end", "data": {"messageCount": 3, "lastMessage": {"content": "完成", "role": "assistant"}, "tokenUsage": {"promptTokens": 100, "completionTokens": 50, "totalTokens": 150}}}

JavaScript 客户端示例:

const ws = new WebSocket('ws://localhost:4000/v1/sessions/my-session-001/ws?api_key=sk-abc123');

ws.onopen = () => {
  ws.send(JSON.stringify({ action: 'prompt', text: '你好' }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  switch (msg.event) {
    case 'message_delta':
      process.stdout.write(msg.data.delta);
      break;
    case 'approval_required':
      // 自动审批示例
      ws.send(JSON.stringify({ action: 'approve', approvalId: msg.data.approvalId }));
      break;
    case 'ask_user':
      ws.send(JSON.stringify({ action: 'respond', ref: msg.data.ref, response: '是的' }));
      break;
    case 'agent_end':
      console.log('\nAgent 完成');
      ws.close();
      break;
  }
};

Workflow / AgentOps Replay

Gateway 只读取并翻译 workflow event ledger,不启动 workflow executor,不持有 RunStore,也不处理审批人解析、RBAC 或业务审计。

宿主应用需要在服务端配置事件来源:

config :cmdc_gateway,
  workflow_event_source: MyApp.WorkflowRunStore

事件来源模块必须实现 list_events/2events/2,签名为 (run_id, opts) -> {:ok, events} | {:error, reason}opts 支持 limitafter_idafter_seqtypenode_id

GET /v1/workflows/runs/:run_id/events

Query 参数:

参数类型说明
limitinteger单页数量,默认 50,最大 500
afterIdstring上一页最后一个 event id
afterSeqinteger上一页最后一个 seq
typestring原始 RunEvent.type 过滤,如 human_task.created
nodeIdstring节点 ID 过滤

响应 200:

{
  "runId": "run_123",
  "events": [
    {
      "event": "workflow.human_task.created",
      "run_id": "run_123",
      "node_id": "legal_review",
      "trace_id": "trace_123",
      "span_id": "run_123:legal_review:human_task.created",
      "timestamp": "2026-06-01T00:00:00.000Z",
      "payload": {
        "task_id": "task_legal",
        "approval_mode": "any_of",
        "correlation_id": "corr_legal"
      }
    }
  ],
  "page": {
    "limit": 50,
    "count": 1,
    "nextCursor": "run_123:evt:3",
    "nextSeq": 3
  }
}

标准事件名:

RunEvent.typeGateway event
run.started/resumed/paused/waiting/completed/failed/cancelled/retryworkflow.run.*
orchestrator.node.started/completed/failed/skipped/retryworkflow.node.*
orchestrator.node.*node_type=forkworkflow.fork.*
orchestrator.node.*node_type=joinworkflow.join.*
orchestrator.edge.signaled / edge.signaled / signal.edgeworkflow.signal.edge
human_task.created/progress/completed/timeout/failedworkflow.human_task.*
human_task.decision_recordedworkflow.human_task.decision

未配置事件来源:

{
  "error": "workflow_event_source_not_configured",
  "message": "Configure :cmdc_gateway, :workflow_event_source with a module exposing list_events/2 or events/2"
}

企业平台必须自行实现:

  • ApprovalService:审批人解析、通知、幂等提交和 CMDCOrchestrator.submit_human_task_decision/4 调用。
  • RunStore:生产级 event ledger、分页索引、并发锁和恢复游标。
  • audit hooks:RBAC/ABAC、审批业务审计、租户隔离和合规留痕。

控制 API

REST 版本的控制接口,适合不使用 WebSocket 的场景(如服务端集成、CLI 工具)。功能与 WebSocket 入站消息等价。

审批通过

POST /v1/sessions/:id/approve

路径参数:

参数类型说明
idstringSession ID

请求头:

Content-Type: application/json
X-API-Key: sk-abc123

请求体:

{
  "approvalId": "apr_x7y8z9"
}
字段类型必填说明
approvalIdstring来自 approval_required 事件的审批 ID

响应 200:

{
  "ok": true,
  "action": "approve",
  "approvalId": "apr_x7y8z9"
}

响应 400:

{
  "error": "bad_request",
  "message": "Missing 'approvalId'"
}

审批拒绝

POST /v1/sessions/:id/reject

路径参数:

参数类型说明
idstringSession ID

请求头:

Content-Type: application/json
X-API-Key: sk-abc123

请求体:

{
  "approvalId": "apr_x7y8z9"
}
字段类型必填说明
approvalIdstring来自 approval_required 事件的审批 ID

响应 200:

{
  "ok": true,
  "action": "reject",
  "approvalId": "apr_x7y8z9"
}

响应 400:

{
  "error": "bad_request",
  "message": "Missing 'approvalId'"
}

回答 Agent 提问

POST /v1/sessions/:id/respond

路径参数:

参数类型说明
idstringSession ID

请求头:

Content-Type: application/json
X-API-Key: sk-abc123

请求体:

{
  "ref": "ask_001",
  "response": "快速排序"
}
字段类型必填说明
refstring来自 ask_user 事件的引用 ID
responsestring用户的回答文本

响应 200:

{
  "ok": true,
  "action": "respond",
  "ref": "ask_001"
}

响应 400:

{
  "error": "bad_request",
  "message": "Missing 'ref' or 'response'"
}

切换模型

POST /v1/sessions/:id/switch_model

异步调用 CMDC.switch_model/2CMDC.switch_model/3。返回 202 只表示 控制请求已进入 Agent;最终结果请监听 model_switched / model_switch_failed 事件或查询 status。

请求体:

{
  "model": "openai:gpt-4o",
  "providerOpts": {
    "baseUrl": "https://llm.example.com",
    "timeout": 30000
  }
}

providerOpts 只接受已存在 atom key,未知 key 返回 invalid_provider_opts,避免外部 JSON 动态创建 atom。

响应 202:

{
  "ok": true,
  "action": "switch_model",
  "model": "openai:gpt-4o",
  "async": true,
  "providerOptsKeys": ["base_url", "timeout"]
}

运行时工具控制

POST /v1/sessions/:id/attach_tool

单个 Tool attach,tool 必须是已加载的 Elixir Tool 模块名。

{
  "tool": "CMDC.Tool.ReadFile"
}

DELETE /v1/sessions/:id/tools/:tool_name

按 Tool name 卸载单个工具。需要批量或原子替换时使用 /v1/sessions/:id/tools/batch


批量工具控制

POST /v1/sessions/:id/tools/batch

批量暴露 core 0.6 的 CMDC.attach_tools/2detach_tools/2replace_tools/2。 这是原子控制面:请求进入 core 前会先完成 Gateway 侧 JSON 校验;core validation 失败时工具表不变。

注意:该端点与 POST /v1/sessions/:id/tools 回调工具注册不同。/tools/batch 只操作已加载且服务端 allowlist 允许的 Elixir Tool 模块。

请求体:

{
  "action": "replace",
  "tools": ["CMDC.Tool.ReadFile", "CMDC.Tool.Grep"]
}
字段类型必填说明
actionstring"attach" / "detach" / "replace"
toolsstring[]attach/replace 为 Tool 模块名;detach 为 tool name 字符串

服务端 allowlist:

config :cmdc_gateway, :tool_module_allowlist,
  [CMDC.Tool.ReadFile, CMDC.Tool.Grep, "CMDC.Tool.ListDir"]

未配置时默认允许 CMDC 内置 Tool。生产环境可以收窄该列表;CMDC.Tool.Shell 这类高权限工具尤其建议按租户部署策略显式配置。

响应 200:

{
  "ok": true,
  "action": "replace_tools",
  "attached": ["grep"],
  "detached": ["shell"]
}

attach 成功时 action"attach_tools"attached 为新增工具名; detach 成功时 action"detach_tools"detached 为移除工具名; replace 成功时同时返回 diff。

校验失败:

{
  "error": "tool_not_allowed",
  "message": "batch tool request failed validation",
  "details": [
    {"index": 0, "tool": "CMDC.Tool.Shell", "reason": "tool_not_allowed"}
  ]
}

core 原子校验失败返回 422:

{
  "error": "attach_tools_failed",
  "message": "core validation failed; no tools were changed",
  "details": [
    {"target": "CMDC.Tool.ReadFile", "reason": "already_attached"}
  ]
}

成功批量变更会通过 SSE/WS 发出 tools_updated

{
  "attached": ["read_file"],
  "detached": []
}

中段注入与中止

POST /v1/sessions/:id/steer

把一条 steering 文本插入当前或下一轮 Agent 执行。

{
  "text": "优先检查测试失败,不要重构无关代码"
}

响应 200:

{
  "ok": true,
  "action": "steer",
  "ref": "#Reference<0.1.2.3>"
}

POST /v1/sessions/:id/abort

异步中止当前执行。killTools / kill_tools 可取 "none""killable""all"clearQueue / clear_queue 为 boolean。

{
  "reason": "user_cancelled",
  "killTools": "killable",
  "clearQueue": true
}

响应 202:

{
  "ok": true,
  "action": "abort",
  "opts": {
    "reason": "user_cancelled",
    "kill_tools": "killable",
    "clear_queue": true
  },
  "async": true
}

Plugin opts 热更新

PATCH /v1/sessions/:id/plugins/:plugin/opts

调用 core 0.6 CMDC.update_plugin_opts/3。Gateway 只允许服务端 :plugin_opts_allowlist 中的 Plugin 被更新。

config :cmdc_gateway, :plugin_opts_allowlist,
  [CMDC.Plugin.Builtin.CostGuard]

请求体:

{
  "opts": {
    "maxTokens": 120000
  }
}

opts / pluginOpts / plugin_opts 均可;key 必须能转换为已存在 atom。

响应 202:

{
  "ok": true,
  "async": true,
  "sessionId": "my-session-001",
  "plugin": "CMDC.Plugin.Builtin.CostGuard"
}

Checkpoint / Resume

POST /v1/sessions/:id/checkpoints

保存当前 session checkpoint。checkpoint backend 只能由服务端配置,客户端传 backend / backendModule / backendOpts 会被拒绝。

{
  "label": "before-risky-change",
  "metadata": {
    "source": "dashboard"
  }
}

响应 201:

{
  "checkpointId": "chk_123",
  "sessionId": "my-session-001",
  "label": "before-risky-change",
  "metadata": {"source": "dashboard"},
  "strippedFields": ["pending_approvals"],
  "savedAt": "2026-06-01T00:00:00Z",
  "schemaVersion": 1
}

POST /v1/sessions/resume

从已保存 checkpoint 恢复到新的 Gateway session。

{
  "sessionId": "my-session-001",
  "checkpointId": "chk_123",
  "newSessionId": "my-session-001-resumed",
  "workingDir": "/home/user/project",
  "groupId": "agentops-run-001",
  "eventBufferSize": 512,
  "providerOpts": {
    "temperature": 0.2
  }
}

请求体字段:

字段类型必填说明
sessionId / session_idstringcheckpoint 来源 session
checkpointId / checkpoint_idstring不传时由 backend 决定默认 checkpoint
newSessionId / targetSessionIdstring新 session id;默认自动生成
workingDir / working_dirstring恢复后的工作目录,仍经过 WorkingDirPolicy 校验
groupId / group_idstring恢复 session 所属 group
eventBufferSize / event_buffer_sizeinteger新 session replay ring buffer
hibernateAfterMs / hibernate_after_msinteger新 session hibernate 配置
providerOpts / provider_optsobject透传 provider opts,只接受已存在 atom key

响应 201:

{
  "sessionId": "my-session-001-resumed",
  "status": "resumed",
  "checkpointId": "chk_123",
  "groupId": "agentops-run-001"
}

Provider Registry

Provider Registry 是 admin-only 控制面,用于运行时注册服务端 provider profile。admin key 通过 :admin_api_keys 配置;普通 API key 返回 403。

config :cmdc_gateway, :admin_api_keys, ["admin-key"]

GET /v1/provider_profiles

{
  "profiles": [
    {
      "name": "prod-openai",
      "provider": "openai",
      "optsKeys": ["api_key", "base_url"],
      "registeredAtMs": 1780267200000
    }
  ]
}

POST /v1/provider_profiles

{
  "name": "prod-openai",
  "provider": "openai",
  "opts": {
    "apiKey": "sk-prod",
    "baseUrl": "https://api.openai.com/v1"
  }
}

响应只回显 optsKeys,不回显密钥值。Gateway JSON API 明确拒绝 resolverFn / resolver_fn;resolver 函数只能由宿主 Elixir app 服务端配置。

DELETE /v1/provider_profiles/:name

{
  "ok": true,
  "name": "prod-openai",
  "status": "deleted"
}

统计与历史

用量统计

获取 Session 运行状态 + API Key 维度的用量计量数据。

GET /v1/sessions/:id/stats

路径参数:

参数类型说明
idstringSession ID

请求:

GET /v1/sessions/my-session-001/stats HTTP/1.1
X-API-Key: sk-abc123

响应 200:

{
  "sessionId": "my-session-001",
  "state": "idle",
  "turns": 5,
  "toolCalls": 12,
  "totalTokens": 25600,
  "costUsd": "0.0251",
  "tokenUsage": {
    "promptTokens": 15000,
    "completionTokens": 10600,
    "totalTokens": 25600,
    "costUsd": "0.0251",
    "cachedTokens": 2048
  },
  "uptimeMs": 120000,
  "meter": {
    "promptCount": 5,
    "totalPromptTokens": 15000,
    "totalCompletionTokens": 10600,
    "totalTokens": 25600,
    "totalCostUsd": "0.0251",
    "cachedTokens": 2048,
    "lastActivityAt": "2026-06-01T00:00:00Z"
  },
  "eventBufferSize": 512,
  "lastEventIndex": 42,
  "eventBufferCount": 42
}
字段类型说明
sessionIdstringSession ID
statestringAgent 状态机当前状态
turnsinteger已完成的对话轮次数
toolCallsinteger累计工具调用次数
totalTokensinteger累计消耗 token 数(Agent 侧统计)
costUsdnumber | string | nullAgent 侧成本估算
tokenUsageobjectAgent 侧 CMDC.TokenUsage 摘要
uptimeMsintegerSession 存活时间(毫秒)
meter.promptCountinteger该 API Key 发送的 prompt 次数(跨 Session 累计)
meter.totalPromptTokensinteger该 API Key 的累计输入 token 数
meter.totalCompletionTokensinteger该 API Key 的累计输出 token 数
meter.totalTokensinteger该 API Key 的累计总 token 数
meter.totalCostUsdnumber | string | null该 API Key 的累计成本估算
meter.cachedTokensinteger该 API Key 累计缓存命中 token
meter.lastActivityAtstring | null最近一次计量活动时间
eventBufferSizeintegersession replay ring buffer 配置大小
lastEventIndexinteger当前 EventBus 最新事件 index
eventBufferCountintegerring buffer 当前保留事件数

meter 数据是 API Key 维度的全局统计,不仅限于当前 Session。


对话历史

获取 Session 的完整消息列表。

GET /v1/sessions/:id/messages

路径参数:

参数类型说明
idstringSession ID

请求:

GET /v1/sessions/my-session-001/messages HTTP/1.1
X-API-Key: sk-abc123

响应 200:

{
  "sessionId": "my-session-001",
  "messages": [
    {
      "id": "msg_001",
      "role": "system",
      "content": "你是一个专业的编程助手",
      "toolCalls": null,
      "callId": null,
      "name": null,
      "isError": false
    },
    {
      "id": "msg_002",
      "role": "user",
      "content": "帮我用 Python 写一个快速排序",
      "toolCalls": null,
      "callId": null,
      "name": null,
      "isError": false
    },
    {
      "id": "msg_003",
      "role": "assistant",
      "content": "好的,这是一个 Python 快速排序实现:\n\n```python\ndef quick_sort(arr):\n    ...\n```",
      "toolCalls": [
        {
          "call_id": "tc_001",
          "name": "WriteFile",
          "arguments": {"path": "sort.py", "content": "def quick_sort(arr): ..."}
        }
      ],
      "callId": null,
      "name": null,
      "isError": false
    },
    {
      "id": "msg_004",
      "role": "tool_result",
      "content": "File written successfully",
      "toolCalls": null,
      "callId": "tc_001",
      "name": "WriteFile",
      "isError": false
    }
  ]
}

Message 对象字段:

字段类型说明
idstring消息唯一 ID
rolestring角色:"system" / "user" / "assistant" / "tool_result"
contentstring | null消息文本内容
toolCallsarray | nullassistant 消息可能包含,工具调用列表
toolCalls[].call_idstring工具调用 ID
toolCalls[].namestring工具名称
toolCalls[].argumentsobject工具调用参数
callIdstring | nulltool_result 消息包含,关联的工具调用 ID
namestring | nulltool_result 消息包含,工具名称
isErrorboolean是否为错误消息

SDK 兼容与迁移说明

cmdc_gateway 0.6 对 HTTP/JSON 客户端保持加性升级:已有路径、字段名和 成功状态码尽量不变,新能力通过新增字段或新增端点暴露。

兼容承诺

  • Python / Node / Go 旧 SDK 可以继续只依赖 sessionIdstatusrequestId、 SSE event/data 和 HITL approvalId/ref 字段;新增字段可以忽略。
  • POST /v1/sessions/:id/prompt 仍异步返回 202;排队语义仍通过 queued 表达。
  • POST /v1/sessions/:id/approve 默认 autoResume=truereject 默认 autoResume=false;显式 autoResume: false / auto_resume: false 会被保留。
  • GET /v1/sessions/:id/events 不带 since/types 时仍是实时 SSE;带 since 后才启用 replay。
  • POST /v1/sessions/:id/tools 仍是外部 HTTP callback tool 注册; POST /v1/sessions/:id/tools/batch 是 core Tool attach/detach/replace, 两者不能混用。

0.6 迁移重点

  • 创建 session 时建议显式传 eventBufferSize,否则断线后无法通过 since 补 replay。
  • 需要跨 session 聚合展示时传 groupId 并订阅 /v1/groups/:group_id/events;group stream 不保证 replay。
  • 恢复会话优先使用 checkpoint/resume。messages 仅用于客户端已持有的、 受大小限制的安全历史导入。
  • workingDir 会被服务端 WorkingDirPolicy 约束;旧客户端如果传绝对路径, 需要确保该路径位于服务端允许 root 内,且路径段不含 symlink escape。
  • providerOptsplugin opts 和 Provider Profile opts 只接受已存在 atom key。SDK 不应发送任意动态 key。
  • skillSelector、checkpoint backend、provider resolver、plugin module 注入均不接受 public JSON,请迁移到宿主 Elixir app 服务端配置。

最小 replay 调用示例

curl -N \
  "http://localhost:4000/v1/sessions/my-session/events?since=42&types=message_delta,agent_end" \
  -H "X-API-Key: sk-abc123" \
  -H "Accept: text/event-stream"

Gateway 边界

Phase 20E 明确以下能力不下沉为 public HTTP JSON API:

core 能力Gateway 边界原因
skill_selector不接受 skillSelector / skill_selector JSON 字段任意模块注入风险;Skill 选择器应由宿主 Elixir app 在服务端配置
system-wide telemetry audit不提供 /v1/audit/events 之类全局 SSEGateway 的 ?mode=audit 只投影当前 EventBus 事件;全局 telemetry 审计应由宿主 app 使用 :telemetry.attach_many/4 接入观测栈
group replayGET /v1/groups/:group_id/events 是 live-only;带 since/types/Last-Event-ID 返回 group_replay_not_supportedcore 0.6 只提供 per-session ring buffer replay
CMDC.monitor/1 / demonitor/2不提供 HTTP monitor endpoint这是 BEAM 进程内生命周期集成;跨语言客户端请使用 SSE/WS terminal events 和 HTTP status
checkpoint backend / provider resolver / plugin module不接受客户端 JSON 指定任意模块或 resolver 函数这些属于服务端信任边界和部署配置

外部系统恢复会话优先使用 checkpoint/resume。仅当宿主系统已经有安全、尺寸受控的 JSON 历史时,才使用 POST /v1/sessionsmessages 导入。


回调工具注册

为 Session 动态注册外部 HTTP 回调工具。注册后 Agent 可像使用内置工具一样调用它,Gateway 会代理 HTTP 请求到你的服务。

POST /v1/sessions/:id/tools

路径参数:

参数类型说明
idstringSession ID

请求头:

Content-Type: application/json
X-API-Key: sk-abc123

请求体:

{
  "name": "query_database",
  "description": "Run a SQL query on the production database",
  "callbackUrl": "https://my-service.example.com/tools/query",
  "parameters": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "SQL query to execute"
      },
      "database": {
        "type": "string",
        "description": "Target database name",
        "enum": ["production", "staging"]
      }
    },
    "required": ["query"]
  },
  "timeoutMs": 30000
}
字段类型必填默认值说明
namestring工具名称,Agent 调用时使用此名称
descriptionstring"External tool: <name>"工具描述,帮助 Agent 理解何时使用此工具
callbackUrlstring你的 HTTP 服务端点 URL,Gateway 向此 URL 发 POST
parametersobject{"type":"object","properties":{}}JSON Schema 格式的参数定义,Agent 根据此 schema 构造调用参数
timeoutMsinteger30000HTTP 请求超时时间(毫秒)

响应 201 Created:

{
  "ok": true,
  "sessionId": "my-session-001",
  "toolName": "query_database"
}

响应 422:

{
  "error": "registration_failed",
  "message": "Missing required fields: name, callbackUrl"
}

回调执行流程

当 Agent 决定调用已注册的回调工具时:

1. Gateway → 你的服务(POST 请求)

Gateway 向 callbackUrl 发送 POST 请求:

POST https://my-service.example.com/tools/query HTTP/1.1
Content-Type: application/json

{
  "callId": "tc_a1b2c3d4",
  "toolName": "query_database",
  "args": {
    "query": "SELECT count(*) FROM users WHERE active = true",
    "database": "production"
  },
  "sessionId": "my-session-001"
}
字段类型说明
callIdstring工具调用唯一 ID
toolNamestring工具名称
argsobjectAgent 传入的调用参数
sessionIdstringSession ID

2. 你的服务 → Gateway(HTTP 响应)

成功返回(HTTP 2xx):

{
  "result": "Active users: 42,851"
}
字段类型说明
resultstring工具执行结果文本,将作为工具返回值传给 Agent

错误返回(HTTP 2xx,业务错误):

{
  "error": "Permission denied: read-only user cannot execute write queries"
}
字段类型说明
errorstring错误描述,Agent 会收到此错误信息并据此调整策略

HTTP 非 2xx 状态码或网络错误会被 Gateway 转换为工具执行失败,Agent 会收到包含错误详情的 tool_execution_end 事件(status: "error")。


完整回调工具示例(Python Flask)

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/tools/query', methods=['POST'])
def handle_query():
    data = request.json
    call_id = data['callId']
    args = data['args']
    query = args.get('query', '')
    
    try:
        result = execute_sql(query)  # 你的业务逻辑
        return jsonify({"result": str(result)})
    except Exception as e:
        return jsonify({"error": str(e)})

if __name__ == '__main__':
    app.run(port=9999)

对应注册请求:

curl -X POST http://localhost:4000/v1/sessions/my-session-001/tools \
  -H "X-API-Key: sk-abc123" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "query_database",
    "description": "Execute SQL queries on the database",
    "callbackUrl": "http://localhost:9999/tools/query",
    "parameters": {
      "type": "object",
      "properties": {
        "query": {"type": "string", "description": "SQL query"}
      },
      "required": ["query"]
    }
  }'