Packages

CMDC Gateway — HTTP + SSE + WebSocket 协议网关,接入 CMDC Agent 能力

Current section

Files

Jump to
cmdc_gateway lib cmdc_gateway agent_card.ex
Raw

lib/cmdc_gateway/agent_card.ex

defmodule CMDCGateway.AgentCard do
@moduledoc """
A2A 兼容的 Agent Card 元数据。
对应 *Agentic Design Patterns* 第 15 章 Inter-Agent Communication (A2A) 协议
规范的"Agent 自描述名片",通过 `GET /.well-known/agent.json` 暴露,
允许外部 Agent registry / orchestrator 自动发现本 Gateway 暴露的 CMDC Agent
能力清单。
## 字段约定
[Google A2A Spec](https://github.com/google/a2a-protocol) 保持兼容:
- `name` — Agent 标识名(默认 `cmdc-gateway`
- `description` — 人类可读描述
- `version` — Gateway 版本
- `provider` — 提供方信息
- `capabilities` — 支持的能力(streaming, tools, hitl, ...)
- `endpoints` — 标准 RPC / 流式端点 URL
- `extensions` — CMDC 专属扩展(plugins / steering / model_switch / ...)
## 配置覆盖
通过 `config :cmdc_gateway, :agent_card, %{...}` 覆盖默认字段,
尤其适合多租户场景下为每个部署单元定制 `name` / `description`
"""
alias CMDCGateway, as: GW
@doc """
根据当前请求构建 Agent Card JSON map。
"""
@spec build(Plug.Conn.t()) :: map()
def build(%Plug.Conn{} = conn) do
base = base_url(conn)
overrides = Application.get_env(:cmdc_gateway, :agent_card, %{})
Map.merge(
%{
a2aVersion: "1.0",
name: "cmdc-gateway",
description:
"CMDC Gateway — 暴露 Elixir/OTP CMDC Agent Kernel 的 HTTP / SSE / WebSocket 协议网关",
version: GW.version(),
provider: %{
organization: "CMDC",
url: "https://cnb.cool/tupleyun/cmdc"
},
capabilities: %{
streaming: true,
serverSentEvents: true,
webSocket: true,
tools: true,
humanInTheLoop: true,
modelSwitching: true,
steering: true,
memoryFlush: true,
plugins: true
},
endpoints: %{
createSession: %{method: "POST", url: "#{base}/v1/sessions"},
prompt: %{method: "POST", url: "#{base}/v1/sessions/{sessionId}/prompt"},
events: %{method: "GET", url: "#{base}/v1/sessions/{sessionId}/events"},
status: %{method: "GET", url: "#{base}/v1/sessions/{sessionId}/status"},
messages: %{method: "GET", url: "#{base}/v1/sessions/{sessionId}/messages"},
delete: %{method: "DELETE", url: "#{base}/v1/sessions/{sessionId}"},
approve: %{method: "POST", url: "#{base}/v1/sessions/{sessionId}/approve"},
reject: %{method: "POST", url: "#{base}/v1/sessions/{sessionId}/reject"},
respond: %{method: "POST", url: "#{base}/v1/sessions/{sessionId}/respond"},
switchModel: %{method: "POST", url: "#{base}/v1/sessions/{sessionId}/switch_model"},
attachTool: %{method: "POST", url: "#{base}/v1/sessions/{sessionId}/attach_tool"},
detachTool: %{
method: "DELETE",
url: "#{base}/v1/sessions/{sessionId}/tools/{toolName}"
},
steer: %{method: "POST", url: "#{base}/v1/sessions/{sessionId}/steer"},
abort: %{method: "POST", url: "#{base}/v1/sessions/{sessionId}/abort"},
webSocket: %{
method: "GET",
url: ws_base(base) <> "/v1/sessions/{sessionId}/ws",
protocol: "wss"
}
},
extensions: %{
cmdcVersion: "0.2.0",
plugins: [
"MemoryFlush",
"ModelRouter",
"Recovery",
"OutputFilter",
"Reflection",
"CostGuard"
],
eventCatalog:
~w(agent_start agent_end agent_abort prompt_received message_start message_delta thinking_start thinking_delta tool_calls tool_execution_start tool_execution_end tool_call_unknown tool_attached tool_detached approval_required approval_resolved ask_user model_switched memory_flushed plugin_event steering error)
},
authentication: %{
schemes: ["api_key"],
headers: ["x-api-key", "authorization"]
}
},
atomize_keys(overrides)
)
end
# ==========================================================================
# Private
# ==========================================================================
defp base_url(%Plug.Conn{scheme: scheme, host: host, port: port}) do
scheme_str = Atom.to_string(scheme)
case {scheme, port} do
{:http, 80} -> "#{scheme_str}://#{host}"
{:https, 443} -> "#{scheme_str}://#{host}"
_ -> "#{scheme_str}://#{host}:#{port}"
end
end
defp ws_base(base) do
base
|> String.replace_prefix("https://", "wss://")
|> String.replace_prefix("http://", "ws://")
end
defp atomize_keys(map) when is_map(map), do: map
defp atomize_keys(_), do: %{}
end