agentbridge-openclaw-skill 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # @agentbridge/openclaw-skill
2
+
3
+ OpenClaw 插件,将你的 Agent 接入 AgentBridge,实现跨 Agent 通信、智能路由和企业级治理。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ # 方式一:从本地路径安装
9
+ openclaw plugins install /path/to/agentbridge-openclaw-skill
10
+
11
+ # 方式二:从 npm 安装(发布后)
12
+ openclaw plugins install @agentbridge/openclaw-skill
13
+ ```
14
+
15
+ 安装后重启 Gateway:
16
+
17
+ ```bash
18
+ openclaw gateway stop
19
+ openclaw gateway
20
+ ```
21
+
22
+ ## 配置
23
+
24
+ 在 OpenClaw 的 `~/.openclaw/openclaw.json` 中,`plugins.entries` 下添加:
25
+
26
+ ```json
27
+ {
28
+ "hooks": {
29
+ "enabled": true,
30
+ "token": "your-hooks-token",
31
+ "path": "/hooks"
32
+ },
33
+ "plugins": {
34
+ "entries": {
35
+ "@agentbridge/openclaw-skill": {
36
+ "enabled": true,
37
+ "config": {
38
+ "consoleEndpoint": "http://localhost:18080",
39
+ "workerEndpoint": "http://localhost:18081",
40
+ "bridgeId": "prod-bridge",
41
+ "agentId": "my-openclaw-agent",
42
+ "apiKey": "your-api-key",
43
+ "apiSecret": "your-api-secret",
44
+ "delivery": "poll",
45
+ "tags": ["openclaw", "assistant"],
46
+ "skills": [
47
+ {"name": "web_search", "description": "Search the web for information"},
48
+ {"name": "code_review", "description": "Review code and suggest improvements"}
49
+ ],
50
+ "autoRegister": true,
51
+ "autoReply": true,
52
+ "gatewayPort": 18789,
53
+ "gatewayToken": "your-gateway-token",
54
+ "hooksToken": "your-hooks-token"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ 注意:`hooks.token` 必须与 `gateway.auth.token` 不同,否则 OpenClaw 会拒绝启动。
63
+
64
+ ## 验证安装
65
+
66
+ ```bash
67
+ # 检查插件是否加载
68
+ openclaw plugins list | grep agentbridge
69
+
70
+ # 检查 Gateway 日志
71
+ openclaw health
72
+ # 应看到: AgentBridge: registered 3 tools (send, poll, discover)
73
+ ```
74
+
75
+ ## 插件做了什么
76
+
77
+ 启动时,插件会:
78
+
79
+ 1. **注册 3 个工具**给你的 OpenClaw Agent:
80
+ - `agentbridge_send` — 向其他 Agent 发送消息(支持智能路由)
81
+ - `agentbridge_poll` — 手动检查消息(通常不需要,后台监听会自动处理)
82
+ - `agentbridge_discover` — 发现可用的 Agent 及其技能
83
+
84
+ 2. **启动后台消息监听**:
85
+ - `delivery: "poll"`(默认)— 每 5 秒自动轮询
86
+ - `delivery: "sse"` — 通过 SSE 长连接实时接收,零延迟,自动重连
87
+
88
+ 3. **自动回复**(需启用 hooks):收到消息后通过 Gateway `/hooks/agent` 端点注入给 LLM,Agent 可自动调用 `agentbridge_send` 回复发送方
89
+
90
+ 4. **自动注册到 AgentBridge**,把 OpenClaw 上的其他工具作为技能注册
91
+
92
+ ## 使用方式
93
+
94
+ ### 发送消息
95
+
96
+ 在 OpenClaw 对话中直接说:
97
+
98
+ ```
99
+ 你: "让 DBA Agent 帮我分析 rds-prod-01 的慢查询"
100
+
101
+ Agent 自动调用 agentbridge_send:
102
+ receiver: "?skill=slow_query_analysis"
103
+ message: "分析 rds-prod-01 的慢查询"
104
+ ```
105
+
106
+ ### 智能路由
107
+
108
+ `agentbridge_send` 的 `receiver` 参数支持:
109
+
110
+ | 格式 | 示例 | 行为 |
111
+ |------|------|------|
112
+ | Agent ID | `dba-agent` | 直接发送 |
113
+ | 技能查询 | `?skill=slow_query_analysis` | 路由到具备该技能的 Agent(round-robin) |
114
+ | 标签查询 | `?tag=ops` | 路由到带该标签的 Agent |
115
+ | 意图查询 | `?intent=分析数据库性能` | 通过 Orchestrator Agent + LLM 智能路由 |
116
+
117
+ ### 发现 Agent
118
+
119
+ ```
120
+ 你: "查看 AgentBridge 上有哪些可用的 Agent"
121
+
122
+ Agent 调用 agentbridge_discover,返回所有 Agent 及其技能列表
123
+ ```
124
+
125
+ ### 接收消息
126
+
127
+ 消息会自动出现在 Gateway 日志中(后台监听)。也可以手动检查:
128
+
129
+ ```
130
+ 你: "检查一下有没有新消息"
131
+
132
+ Agent 调用 agentbridge_poll
133
+ ```
134
+
135
+ ## 投递模式
136
+
137
+ | 模式 | 配置 | 延迟 | 说明 |
138
+ |------|------|------|------|
139
+ | Poll(默认) | `"delivery": "poll"` | ~5 秒 | 定时轮询,简单可靠 |
140
+ | SSE | `"delivery": "sse"` | 实时 | 长连接推送,自动重连,附带低频 fallback poll 兜底 |
141
+
142
+ SSE 模式推荐用于需要实时响应的场景。
143
+
144
+ ## 配置项
145
+
146
+ | 配置项 | 必填 | 默认值 | 说明 |
147
+ |--------|------|--------|------|
148
+ | `consoleEndpoint` | 否 | `http://localhost:18080` | AgentBridge Console 地址 |
149
+ | `workerEndpoint` | 否 | `http://localhost:18081` | AgentBridge Worker Gateway 地址 |
150
+ | `bridgeId` | 否 | `prod-bridge` | 要加入的 Bridge ID |
151
+ | `agentId` | 是 | — | Agent 在 AgentBridge 中的唯一标识 |
152
+ | `agentName` | 否 | 自动生成 | Agent 显示名称 |
153
+ | `apiKey` | 否 | — | Console API Key |
154
+ | `apiSecret` | 否 | — | Console API Secret |
155
+ | `delivery` | 否 | `poll` | 客户端监听模式:`poll`(定时轮询)/ `sse`(实时推送 + fallback poll)。注意:Agent 在 AgentBridge 服务端始终注册为 poll 投递,避免 SSE 连接冲突 |
156
+ | `tags` | 否 | `[]` | Agent 标签 |
157
+ | `skills` | 否 | `[]` | Agent 技能列表,格式 `[{"name":"x","description":"y"}]`。OpenClaw Plugin SDK 不提供查询已注册工具的 API,需手动声明 |
158
+ | `autoRegister` | 否 | `true` | 是否自动注册到 AgentBridge |
159
+ | `pollIntervalMs` | 否 | `5000` | Poll 模式轮询间隔(毫秒),设为 0 关闭后台监听 |
160
+ | `sseFallbackPollMs` | 否 | `30000` | SSE 模式下 fallback poll 间隔(毫秒),兜底 SSE 断连期间的消息丢失,设为 0 关闭 |
161
+ | `autoReply` | 否 | `true` | 收到消息后是否自动注入 Gateway 触发 LLM 回复 |
162
+ | `maxAutoReplyDepth` | 否 | `3` | 同一发送方在滑动窗口内最多自动回复次数(防乒乓循环) |
163
+ | `replyWindowMs` | 否 | `60000` | 自动回复滑动窗口时长(毫秒) |
164
+ | `gatewayPort` | 否 | `18789` | 本地 Gateway 端口(用于 autoReply 注入) |
165
+ | `gatewayToken` | 否 | — | Gateway auth token |
166
+ | `hooksToken` | 否 | — | Gateway hooks token(需在 openclaw.json 中启用 hooks) |
167
+
168
+ ## 技能注册
169
+
170
+ 在配置中声明要暴露给 AgentBridge 的 skills:
171
+
172
+ ```json
173
+ "skills": [
174
+ {"name": "web_search", "description": "Search the web for information"},
175
+ {"name": "code_review", "description": "Review code and suggest improvements"}
176
+ ]
177
+ ```
178
+
179
+ 只有声明的 skills 会注册到 AgentBridge,其他 Agent 可以通过 `?skill=web_search` 路由到你的 OpenClaw Agent。
180
+
181
+ 未声明的工具(如 `exec`、`write` 等)不会暴露,确保安全性。AgentBridge 自身的工具(`agentbridge_send/poll/discover`)也不需要声明。
182
+
183
+ ## 完整示例
184
+
185
+ ```bash
186
+ # 1. 确保 AgentBridge 在运行
187
+ docker-compose up -d
188
+
189
+ # 2. 安装插件
190
+ openclaw plugins install /path/to/agentbridge-openclaw-skill
191
+
192
+ # 3. 配置(编辑 ~/.openclaw/openclaw.json 添加上面的 plugins 配置)
193
+
194
+ # 4. 重启 Gateway
195
+ openclaw gateway stop && openclaw gateway
196
+
197
+ # 5. 验证
198
+ openclaw health
199
+ # → AgentBridge: registered 3 tools (send, poll, discover)
200
+ # → AgentBridge: poll listener started (every 5s)
201
+
202
+ # 6. 开始对话
203
+ openclaw tui
204
+ # > 查看 AgentBridge 上有哪些 Agent
205
+ # > 让 DBA Agent 分析 rds-prod-01 的慢查询
206
+ ```
package/index.ts ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * @agentbridge/openclaw-skill — OpenClaw plugin for AgentBridge.
3
+ *
4
+ * Connects your OpenClaw agent to AgentBridge for cross-agent communication,
5
+ * intelligent routing, and auto-registration of tools as skills.
6
+ *
7
+ * When autoReply is enabled (default), incoming messages are injected into the
8
+ * OpenClaw agent via the Gateway HTTP API, triggering an LLM turn that can
9
+ * decide to reply using agentbridge_send.
10
+ */
11
+
12
+ import { AgentBridgeClient } from "./src/agentbridge-client.js";
13
+ import { createSendTool, createPollTool, createDiscoverTool } from "./src/tools.js";
14
+
15
+ export default function register(api: any) {
16
+ const pluginCfg = api.pluginConfig || {};
17
+ const agentId = pluginCfg.agentId;
18
+
19
+ if (!agentId) {
20
+ // Silently skip during CLI validation (pluginConfig is incomplete)
21
+ return;
22
+ }
23
+
24
+ const consoleEndpoint = pluginCfg.consoleEndpoint || "http://localhost:18080";
25
+ const workerEndpoint = pluginCfg.workerEndpoint || "http://localhost:18081";
26
+ const bridgeId = pluginCfg.bridgeId || "prod-bridge";
27
+
28
+ const client = new AgentBridgeClient({
29
+ consoleEndpoint, workerEndpoint, bridgeId,
30
+ agentId: agentId,
31
+ apiKey: pluginCfg.apiKey,
32
+ apiSecret: pluginCfg.apiSecret,
33
+ delivery: pluginCfg.delivery || "poll",
34
+ });
35
+
36
+ // Register tools
37
+ api.registerTool(createSendTool(client));
38
+ api.registerTool(createPollTool(client));
39
+
40
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
41
+ if (pluginCfg.apiKey) headers["X-API-Key"] = pluginCfg.apiKey;
42
+ if (pluginCfg.apiSecret) headers["X-API-Secret"] = pluginCfg.apiSecret;
43
+ api.registerTool(createDiscoverTool(consoleEndpoint, headers));
44
+
45
+ api.logger.info("AgentBridge: registered 3 tools (send, poll, discover)");
46
+
47
+ // --- Auto-reply: inject incoming messages into OpenClaw agent via Gateway API ---
48
+ const autoReply = pluginCfg.autoReply !== false;
49
+ const gatewayPort = pluginCfg.gatewayPort || api.gateway?.port || 18789;
50
+ const gatewayToken = pluginCfg.gatewayToken || api.gateway?.auth?.token || "";
51
+ const hooksToken = pluginCfg.hooksToken || "";
52
+ const maxAutoReplyDepth = pluginCfg.maxAutoReplyDepth ?? 3;
53
+
54
+ // Track recent auto-replies to prevent ping-pong loops.
55
+ // Key: sender agentId, Value: { count, firstSeen }
56
+ const replyTracker = new Map<string, { count: number; firstSeen: number }>();
57
+ const replyWindowMs = pluginCfg.replyWindowMs ?? 60_000;
58
+
59
+ /**
60
+ * Forward an incoming AgentBridge message to the local OpenClaw Gateway,
61
+ * triggering an LLM agent turn. The agent sees the message and can use
62
+ * agentbridge_send to reply.
63
+ *
64
+ * Tries multiple Gateway API paths in order:
65
+ * 1. POST /hooks/agent (webhook hook — requires hooks.enabled=true)
66
+ * 2. POST /v1/chat/completions (OpenAI-compatible endpoint)
67
+ */
68
+ async function injectToAgent(sender: string, content: string) {
69
+ if (!autoReply) return;
70
+
71
+ // --- Loop guard: limit auto-replies per sender within a sliding window ---
72
+ const now = Date.now();
73
+ const tracker = replyTracker.get(sender);
74
+ if (tracker) {
75
+ if (now - tracker.firstSeen < replyWindowMs) {
76
+ if (tracker.count >= maxAutoReplyDepth) {
77
+ api.logger.warn(
78
+ `AgentBridge: suppressed auto-reply to ${sender} (${tracker.count} replies in ${Math.round((now - tracker.firstSeen) / 1000)}s, limit=${maxAutoReplyDepth})`
79
+ );
80
+ return;
81
+ }
82
+ tracker.count++;
83
+ } else {
84
+ // Window expired, reset
85
+ replyTracker.set(sender, { count: 1, firstSeen: now });
86
+ }
87
+ } else {
88
+ replyTracker.set(sender, { count: 1, firstSeen: now });
89
+ }
90
+
91
+ const baseUrl = `http://127.0.0.1:${gatewayPort}`;
92
+ const prompt = `[AgentBridge message from ${sender}]\n${content}\n\n` +
93
+ `Reply to ${sender} using agentbridge_send tool.`;
94
+ const hdrs: Record<string, string> = { "Content-Type": "application/json" };
95
+ // Use hooksToken for /hooks/agent, fall back to gatewayToken
96
+ const hookAuth = hooksToken || gatewayToken;
97
+ if (hookAuth) hdrs["Authorization"] = `Bearer ${hookAuth}`;
98
+
99
+ // Try 1: /hooks/agent
100
+ try {
101
+ const resp = await fetch(`${baseUrl}/hooks/agent`, {
102
+ method: "POST", headers: hdrs,
103
+ body: JSON.stringify({
104
+ message: prompt,
105
+ wakeMode: "now",
106
+ }),
107
+ });
108
+ if (resp.ok) {
109
+ api.logger.info(`AgentBridge: injected via /hooks/agent from ${sender}`);
110
+ return;
111
+ }
112
+ } catch { /* fall through */ }
113
+
114
+ // Try 2: /v1/chat/completions (OpenAI-compatible)
115
+ try {
116
+ const chatHdrs: Record<string, string> = { "Content-Type": "application/json" };
117
+ if (gatewayToken) chatHdrs["Authorization"] = `Bearer ${gatewayToken}`;
118
+ const resp = await fetch(`${baseUrl}/v1/chat/completions`, {
119
+ method: "POST", headers: chatHdrs,
120
+ body: JSON.stringify({
121
+ model: "openclaw",
122
+ messages: [
123
+ { role: "system", content: "You are an AI agent connected to AgentBridge. When you receive messages from other agents, process them and reply using the agentbridge_send tool." },
124
+ { role: "user", content: prompt },
125
+ ],
126
+ }),
127
+ });
128
+ if (resp.ok) {
129
+ api.logger.info(`AgentBridge: injected via /v1/chat/completions from ${sender}`);
130
+ return;
131
+ }
132
+ } catch { /* fall through */ }
133
+
134
+ api.logger.warn(`AgentBridge: could not inject message from ${sender} (no working Gateway API)`);
135
+ }
136
+
137
+ // Background message listener
138
+ const delivery = pluginCfg.delivery || "poll";
139
+ const pollIntervalMs = pluginCfg.pollIntervalMs ?? 5000;
140
+ const sseFallbackPollMs = pluginCfg.sseFallbackPollMs ?? 30_000;
141
+
142
+ // Dedup set: track recently seen messageIds to avoid double-processing
143
+ // when SSE + fallback poll both deliver the same message.
144
+ const seenMessageIds = new Set<string>();
145
+ const SEEN_MAX = 500;
146
+
147
+ function dedup(msg: any): boolean {
148
+ const id = msg?.id || (msg as any)?.extensions?.["ab-traceid"];
149
+ if (!id) return false; // no id → can't dedup, process it
150
+ if (seenMessageIds.has(id)) return true; // already seen
151
+ seenMessageIds.add(id);
152
+ if (seenMessageIds.size > SEEN_MAX) {
153
+ // Evict oldest entries (Set preserves insertion order)
154
+ const iter = seenMessageIds.values();
155
+ for (let i = 0; i < 100; i++) iter.next();
156
+ // Rebuild without the oldest 100
157
+ const keep = [...seenMessageIds].slice(100);
158
+ seenMessageIds.clear();
159
+ keep.forEach(k => seenMessageIds.add(k));
160
+ }
161
+ return false;
162
+ }
163
+
164
+ function handleIncomingMessage(msg: any, channel: string) {
165
+ if (dedup(msg)) return;
166
+ const data = msg?.data || msg;
167
+ const sender = msg?.extensions?.["ab-sender"] || (data as any)?.from || "unknown";
168
+ const content = typeof data === "object" ? JSON.stringify(data) : String(data);
169
+ api.logger.info(`AgentBridge [${channel}]: message from ${sender}: ${content.slice(0, 100)}`);
170
+ injectToAgent(sender, content);
171
+ }
172
+
173
+ if (delivery === "sse") {
174
+ // SSE mode: real-time push via persistent connection
175
+ setTimeout(() => {
176
+ const disconnect = client.connectSSE((msg: any) => {
177
+ handleIncomingMessage(msg, "SSE");
178
+ });
179
+ api.logger.info("AgentBridge: SSE listener connected (real-time push)");
180
+ api.on?.("shutdown", disconnect);
181
+ }, 5000);
182
+
183
+ // Fallback poll: catch messages missed during SSE reconnect gaps
184
+ if (sseFallbackPollMs > 0) {
185
+ let fallbackInterval: ReturnType<typeof setInterval> | null = null;
186
+
187
+ async function fallbackPoll() {
188
+ try {
189
+ const messages = await client.poll(10);
190
+ for (const msg of messages) {
191
+ handleIncomingMessage(msg, "fallback-poll");
192
+ }
193
+ } catch { /* ignore */ }
194
+ }
195
+
196
+ setTimeout(() => {
197
+ fallbackInterval = setInterval(fallbackPoll, sseFallbackPollMs);
198
+ api.logger.info(`AgentBridge: SSE fallback poll started (every ${sseFallbackPollMs / 1000}s)`);
199
+ }, 10000); // start later than SSE to let SSE connect first
200
+
201
+ api.on?.("shutdown", () => { if (fallbackInterval) clearInterval(fallbackInterval); });
202
+ }
203
+ } else if (pollIntervalMs > 0) {
204
+ // Poll mode: periodic background polling
205
+ let pollInterval: ReturnType<typeof setInterval> | null = null;
206
+
207
+ async function backgroundPoll() {
208
+ try {
209
+ const messages = await client.poll(10);
210
+ for (const msg of messages) {
211
+ handleIncomingMessage(msg, "poll");
212
+ }
213
+ } catch { /* ignore */ }
214
+ }
215
+
216
+ setTimeout(() => {
217
+ pollInterval = setInterval(backgroundPoll, pollIntervalMs);
218
+ api.logger.info(`AgentBridge: poll listener started (every ${pollIntervalMs / 1000}s)`);
219
+ }, 5000);
220
+
221
+ api.on?.("shutdown", () => { if (pollInterval) clearInterval(pollInterval); });
222
+ }
223
+
224
+ // Auto-register with AgentBridge
225
+ if (pluginCfg.autoRegister !== false) {
226
+ setTimeout(async () => {
227
+ try {
228
+ const skills = Array.isArray(pluginCfg.skills) ? pluginCfg.skills : [];
229
+ await client.register(
230
+ pluginCfg.agentName || `OpenClaw Agent (${agentId})`,
231
+ skills,
232
+ pluginCfg.tags || []
233
+ );
234
+ api.logger.info(`AgentBridge: registered '${agentId}' with ${skills.length} skills`);
235
+ } catch (err) {
236
+ api.logger.error(`AgentBridge: registration failed: ${err}`);
237
+ }
238
+ }, 3000);
239
+ }
240
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "id": "@agentbridge/openclaw-skill",
3
+ "name": "AgentBridge",
4
+ "description": "Connect OpenClaw to AgentBridge for cross-agent communication and intelligent routing",
5
+ "entrypoint": "index.ts",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "consoleEndpoint": { "type": "string", "default": "http://localhost:18080" },
10
+ "workerEndpoint": { "type": "string", "default": "http://localhost:18081" },
11
+ "bridgeId": { "type": "string", "default": "prod-bridge" },
12
+ "agentId": { "type": "string" },
13
+ "apiKey": { "type": "string" },
14
+ "apiSecret": { "type": "string" },
15
+ "delivery": { "type": "string", "default": "poll" },
16
+ "tags": { "type": "array", "items": { "type": "string" }, "default": [] },
17
+ "autoRegister": { "type": "boolean", "default": true }
18
+ },
19
+ "required": []
20
+ }
21
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "agentbridge-openclaw-skill",
3
+ "version": "1.0.0",
4
+ "description": "OpenClaw plugin — connect to AgentBridge for cross-agent communication and intelligent routing.",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "openclaw": {
8
+ "extensions": ["./index.ts"]
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src/",
13
+ "openclaw.plugin.json",
14
+ "README.md"
15
+ ],
16
+ "keywords": ["openclaw", "agentbridge", "a2a", "agent-to-agent", "routing", "plugin"],
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/agentbridge/agentbridge",
21
+ "directory": "agentbridge-openclaw-skill"
22
+ },
23
+ "dependencies": {}
24
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Lightweight AgentBridge HTTP client for Node.js.
3
+ * Handles Console API (registration) and Worker Gateway API (messaging).
4
+ */
5
+
6
+ export interface AgentBridgeConfig {
7
+ consoleEndpoint: string;
8
+ workerEndpoint: string;
9
+ bridgeId: string;
10
+ apiKey?: string;
11
+ apiSecret?: string;
12
+ agentId: string;
13
+ delivery?: "poll" | "sse" | "webhook";
14
+ }
15
+
16
+ export interface Skill {
17
+ name: string;
18
+ description: string;
19
+ inputSchema?: Record<string, unknown>;
20
+ }
21
+
22
+ export class AgentBridgeClient {
23
+ private config: AgentBridgeConfig;
24
+ private token: string | null = null;
25
+
26
+ constructor(config: AgentBridgeConfig) {
27
+ this.config = config;
28
+ }
29
+
30
+ private consoleHeaders(): Record<string, string> {
31
+ const h: Record<string, string> = { "Content-Type": "application/json" };
32
+ if (this.config.apiKey) h["X-API-Key"] = this.config.apiKey;
33
+ if (this.config.apiSecret) h["X-API-Secret"] = this.config.apiSecret;
34
+ return h;
35
+ }
36
+
37
+ private workerHeaders(): Record<string, string> {
38
+ const h: Record<string, string> = { "Content-Type": "application/json" };
39
+ if (this.token) h["Authorization"] = `Bearer ${this.token}`;
40
+ return h;
41
+ }
42
+
43
+ /**
44
+ * Register this agent with AgentBridge Console.
45
+ * Skills are auto-extracted from OpenClaw tools and passed here.
46
+ *
47
+ * NOTE: Server-side delivery is always "poll" regardless of client config.
48
+ * The client-side delivery setting (sse/poll) only controls how the plugin
49
+ * listens for messages locally. This avoids SSE connection conflicts when
50
+ * multiple OpenClaw processes (gateway + agent CLI) share the same agentId.
51
+ */
52
+ async register(name: string, skills: Skill[] = [], tags: string[] = []): Promise<void> {
53
+ const body = {
54
+ agentId: this.config.agentId,
55
+ name,
56
+ bridgeIds: [this.config.bridgeId],
57
+ protocol: "HTTP",
58
+ delivery: "poll",
59
+ accessMode: "HIGHCODE",
60
+ status: "active",
61
+ skills,
62
+ tags,
63
+ };
64
+
65
+ const resp = await fetch(`${this.config.consoleEndpoint}/api/v1/agents`, {
66
+ method: "POST",
67
+ headers: this.consoleHeaders(),
68
+ body: JSON.stringify(body),
69
+ });
70
+
71
+ if (resp.status === 400) {
72
+ // Agent might already exist — try update
73
+ const updateResp = await fetch(
74
+ `${this.config.consoleEndpoint}/api/v1/agents/${this.config.agentId}`,
75
+ { method: "PUT", headers: this.consoleHeaders(), body: JSON.stringify(body) }
76
+ );
77
+ if (!updateResp.ok) throw new Error(`Register/update failed: ${await updateResp.text()}`);
78
+ } else if (!resp.ok) {
79
+ throw new Error(`Register failed: ${await resp.text()}`);
80
+ }
81
+
82
+ // Issue JWT token
83
+ const tokenResp = await fetch(`${this.config.consoleEndpoint}/api/v1/tokens`, {
84
+ method: "POST",
85
+ headers: this.consoleHeaders(),
86
+ body: JSON.stringify({
87
+ agentId: this.config.agentId,
88
+ bridgeId: this.config.bridgeId,
89
+ scopes: ["send", "receive"],
90
+ expirationMinutes: 1440, // 24h
91
+ }),
92
+ });
93
+ if (!tokenResp.ok) throw new Error(`Token issue failed: ${await tokenResp.text()}`);
94
+ const tokenData = await tokenResp.json() as { token: string };
95
+ this.token = tokenData.token;
96
+ }
97
+
98
+ /** Send a message through the Worker Gateway. */
99
+ async send(receiver: string, data: unknown, taskId?: string): Promise<{ messageId: string }> {
100
+ // Ensure we have a token
101
+ if (!this.token) {
102
+ await this.issueToken();
103
+ }
104
+
105
+ const body: Record<string, unknown> = {
106
+ type: "com.agentbridge.task.dispatch",
107
+ receiver,
108
+ data,
109
+ taskId: taskId || `oc-${this.config.agentId}-${Date.now().toString(36)}`,
110
+ };
111
+
112
+ const resp = await fetch(`${this.config.workerEndpoint}/api/v1/messages`, {
113
+ method: "POST",
114
+ headers: this.workerHeaders(),
115
+ body: JSON.stringify(body),
116
+ });
117
+ if (!resp.ok) throw new Error(`Send failed: ${await resp.text()}`);
118
+ return resp.json() as Promise<{ messageId: string }>;
119
+ }
120
+
121
+ /** Poll for messages from the Worker Gateway. */
122
+ async poll(maxMessages = 10): Promise<unknown[]> {
123
+ if (!this.token) {
124
+ await this.issueToken();
125
+ }
126
+ const url = `${this.config.workerEndpoint}/api/v1/messages/poll?agentId=${
127
+ encodeURIComponent(this.config.agentId)}&maxMessages=${maxMessages}`;
128
+ const resp = await fetch(url, { headers: this.workerHeaders() });
129
+ if (!resp.ok) return [];
130
+ return resp.json() as Promise<unknown[]>;
131
+ }
132
+
133
+ /** Issue a JWT token from Console. */
134
+ private async issueToken(): Promise<void> {
135
+ const tokenResp = await fetch(`${this.config.consoleEndpoint}/api/v1/tokens`, {
136
+ method: "POST",
137
+ headers: this.consoleHeaders(),
138
+ body: JSON.stringify({
139
+ agentId: this.config.agentId,
140
+ bridgeId: this.config.bridgeId,
141
+ scopes: ["send", "receive"],
142
+ expirationMinutes: 1440,
143
+ }),
144
+ });
145
+ if (!tokenResp.ok) throw new Error(`Token issue failed: ${await tokenResp.text()}`);
146
+ const tokenData = await tokenResp.json() as { token: string };
147
+ this.token = tokenData.token;
148
+ }
149
+
150
+ /**
151
+ * Connect to SSE endpoint for real-time message push.
152
+ * Calls onMessage for each received event. Returns an abort function.
153
+ */
154
+ connectSSE(onMessage: (data: unknown) => void): () => void {
155
+ const controller = new AbortController();
156
+ const url = `${this.config.workerEndpoint}/api/v1/events`;
157
+
158
+ const connect = async () => {
159
+ try {
160
+ const resp = await fetch(url, {
161
+ headers: { ...this.workerHeaders(), "Accept": "text/event-stream" },
162
+ signal: controller.signal,
163
+ });
164
+ if (!resp.ok || !resp.body) {
165
+ // Server rejected — retry after delay
166
+ if (!controller.signal.aborted) setTimeout(connect, 5000);
167
+ return;
168
+ }
169
+
170
+ const reader = resp.body.getReader();
171
+ const decoder = new TextDecoder();
172
+ let buffer = "";
173
+
174
+ while (true) {
175
+ const { done, value } = await reader.read();
176
+ if (done) break;
177
+ buffer += decoder.decode(value, { stream: true });
178
+
179
+ const lines = buffer.split("\n");
180
+ buffer = lines.pop() || "";
181
+
182
+ let eventData = "";
183
+ for (const line of lines) {
184
+ if (line.startsWith("data:")) {
185
+ eventData += line.slice(5).trim();
186
+ } else if (line === "" && eventData) {
187
+ try {
188
+ onMessage(JSON.parse(eventData));
189
+ } catch {
190
+ onMessage({ raw: eventData });
191
+ }
192
+ eventData = "";
193
+ }
194
+ }
195
+ }
196
+
197
+ // Stream ended (server closed connection / kicked by another client).
198
+ // Auto-reconnect unless explicitly aborted.
199
+ if (!controller.signal.aborted) {
200
+ setTimeout(connect, 3000);
201
+ }
202
+ } catch (err: any) {
203
+ if (err?.name !== "AbortError") {
204
+ // Network error — reconnect after delay
205
+ setTimeout(connect, 3000);
206
+ }
207
+ }
208
+ };
209
+
210
+ connect();
211
+ return () => controller.abort();
212
+ }
213
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Extracts AgentBridge skills from OpenClaw registered tools.
3
+ *
4
+ * Converts OpenClaw's TypeBox-based tool schemas into AgentBridge's
5
+ * skill format: {name, description, inputSchema}.
6
+ */
7
+
8
+ import type { Skill } from "./agentbridge-client.js";
9
+
10
+ interface OpenClawTool {
11
+ name: string;
12
+ description?: string;
13
+ input?: unknown; // TypeBox schema
14
+ }
15
+
16
+ /**
17
+ * Extracts skills from OpenClaw tools registered via the plugin API.
18
+ * Filters out AgentBridge's own tools (agentbridge_*) to avoid circular registration.
19
+ */
20
+ export function extractSkillsFromOpenClawTools(tools: OpenClawTool[]): Skill[] {
21
+ return tools
22
+ .filter((t) => !t.name.startsWith("agentbridge_"))
23
+ .map((tool) => {
24
+ const skill: Skill = {
25
+ name: tool.name,
26
+ description: tool.description || "",
27
+ };
28
+
29
+ // Convert TypeBox schema to JSON Schema if possible
30
+ if (tool.input && typeof tool.input === "object") {
31
+ try {
32
+ // TypeBox objects have a toJSON-compatible structure
33
+ const schema = tool.input as Record<string, unknown>;
34
+ if (schema.type || schema.properties) {
35
+ skill.inputSchema = schema as Record<string, unknown>;
36
+ }
37
+ } catch {
38
+ // Skip schema extraction on error
39
+ }
40
+ }
41
+
42
+ return skill;
43
+ });
44
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * OpenClaw agent tools for AgentBridge communication.
3
+ *
4
+ * OpenClaw tool interface:
5
+ * name: string
6
+ * description: string
7
+ * parameters: TypeBox schema or JSON Schema object
8
+ * execute(_id: string, params: T): Promise<{content: [{type:"text", text:string}]}>
9
+ */
10
+
11
+ import type { AgentBridgeClient } from "./agentbridge-client.js";
12
+
13
+ function textResult(text: string) {
14
+ return { content: [{ type: "text" as const, text }] };
15
+ }
16
+
17
+ export function createSendTool(client: AgentBridgeClient) {
18
+ return {
19
+ name: "agentbridge_send",
20
+ description:
21
+ "Send a message to another agent through AgentBridge. " +
22
+ "Receiver can be an agent ID, ?skill=X, ?tag=X, or ?intent=text.",
23
+ parameters: {
24
+ type: "object",
25
+ properties: {
26
+ receiver: { type: "string", description: "Target agent ID or query" },
27
+ message: { type: "string", description: "Message content" },
28
+ taskId: { type: "string", description: "Task ID for correlation" },
29
+ },
30
+ required: ["receiver", "message"],
31
+ },
32
+ async execute(_id: string, params: { receiver: string; message: string; taskId?: string }) {
33
+ try {
34
+ const result = await client.send(params.receiver, { content: params.message }, params.taskId);
35
+ return textResult(`Message sent to ${params.receiver}. MessageId: ${result.messageId}`);
36
+ } catch (err) {
37
+ return textResult(`Send failed: ${err instanceof Error ? err.message : String(err)}`);
38
+ }
39
+ },
40
+ };
41
+ }
42
+
43
+ export function createPollTool(client: AgentBridgeClient) {
44
+ return {
45
+ name: "agentbridge_poll",
46
+ description: "Poll for incoming messages from other agents through AgentBridge.",
47
+ parameters: {
48
+ type: "object",
49
+ properties: {
50
+ maxMessages: { type: "number", description: "Max messages to retrieve (default 5)" },
51
+ },
52
+ },
53
+ async execute(_id: string, params: { maxMessages?: number }) {
54
+ try {
55
+ const messages = await client.poll(params?.maxMessages || 5);
56
+ if (messages.length === 0) {
57
+ return textResult("No messages waiting.");
58
+ }
59
+ return textResult(`Received ${messages.length} message(s):\n${JSON.stringify(messages, null, 2)}`);
60
+ } catch (err) {
61
+ return textResult(`Poll failed: ${err instanceof Error ? err.message : String(err)}`);
62
+ }
63
+ },
64
+ };
65
+ }
66
+
67
+ export function createDiscoverTool(consoleEndpoint: string, headers: Record<string, string>) {
68
+ return {
69
+ name: "agentbridge_discover",
70
+ description: "Discover available agents and their skills on AgentBridge.",
71
+ parameters: {
72
+ type: "object",
73
+ properties: {
74
+ query: { type: "string", description: "Search query (name, skill, tag)" },
75
+ tags: { type: "string", description: "Filter by tags (comma-separated)" },
76
+ },
77
+ },
78
+ async execute(_id: string, params: { query?: string; tags?: string }) {
79
+ try {
80
+ const urlParams = new URLSearchParams();
81
+ if (params?.query) urlParams.set("query", params.query);
82
+ if (params?.tags) urlParams.set("tags", params.tags);
83
+ const resp = await fetch(`${consoleEndpoint}/api/v1/agents/discover?${urlParams}`, { headers });
84
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
85
+ const agents = (await resp.json()) as Array<Record<string, unknown>>;
86
+ if (agents.length === 0) {
87
+ return textResult("No agents found.");
88
+ }
89
+ const summary = agents.map((a: any) =>
90
+ `- ${a.agentId} (${a.name || '?'}): skills=${(a.skills||[]).map((s:any)=>s.name).join(',') || 'none'}, tags=${(a.tags||[]).join(',') || 'none'}, status=${a.status}`
91
+ ).join('\n');
92
+ return textResult(`Found ${agents.length} agent(s):\n${summary}`);
93
+ } catch (err) {
94
+ return textResult(`Discover failed: ${err instanceof Error ? err.message : String(err)}`);
95
+ }
96
+ },
97
+ };
98
+ }