@clawroom/openclaw 0.4.0 → 0.5.1
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 +3 -3
- package/node_modules/@clawroom/protocol/package.json +17 -0
- package/node_modules/@clawroom/protocol/src/index.ts +102 -0
- package/node_modules/@clawroom/sdk/package.json +25 -0
- package/node_modules/@clawroom/sdk/src/client.ts +180 -0
- package/node_modules/@clawroom/sdk/src/index.ts +20 -0
- package/node_modules/@clawroom/sdk/src/machine-client.ts +209 -0
- package/node_modules/@clawroom/sdk/src/protocol.ts +20 -0
- package/node_modules/@clawroom/sdk/src/ws-transport.ts +123 -0
- package/package.json +2 -2
- package/src/channel.ts +19 -46
- package/src/client.ts +1 -1
package/README.md
CHANGED
|
@@ -29,11 +29,11 @@ openclaw gateway restart
|
|
|
29
29
|
|
|
30
30
|
## Release
|
|
31
31
|
|
|
32
|
-
The repository includes a GitHub Actions workflow that publishes `@clawroom/protocol`, `@clawroom/sdk`, and `@clawroom/openclaw` to npm when a release tag is pushed.
|
|
32
|
+
The repository includes a GitHub Actions workflow that publishes `@clawroom/protocol`, `@clawroom/sdk`, `@clawroom/bridge`, and `@clawroom/openclaw` to npm when a release tag is pushed.
|
|
33
33
|
|
|
34
34
|
To publish a new version:
|
|
35
35
|
|
|
36
|
-
1. Update the package versions in `protocol/package.json`, `sdk/package.json`, and `plugin/package.json`.
|
|
36
|
+
1. Update the package versions in `protocol/package.json`, `sdk/package.json`, `bridge/package.json`, and `plugin/package.json`.
|
|
37
37
|
2. Commit and push the release commit to GitHub.
|
|
38
38
|
3. Push a release tag, for example `git tag plugin-0.2.3 && git push origin plugin-0.2.3`.
|
|
39
|
-
4. Watch `.github/workflows/release-plugin.yml` until all
|
|
39
|
+
4. Watch `.github/workflows/release-plugin.yml` until all publish jobs succeed.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clawroom/protocol",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "ClawRoom shared types — models, messages, and protocol definitions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/clawroom/clawroom.git"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./src/index.ts"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export interface AgentHeartbeat {
|
|
2
|
+
type: "agent.heartbeat";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface AgentResultFile {
|
|
6
|
+
filename: string;
|
|
7
|
+
mimeType: string;
|
|
8
|
+
data: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AgentComplete {
|
|
12
|
+
type: "agent.complete";
|
|
13
|
+
taskId: string;
|
|
14
|
+
output: string;
|
|
15
|
+
attachments?: AgentResultFile[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AgentProgress {
|
|
19
|
+
type: "agent.progress";
|
|
20
|
+
taskId: string;
|
|
21
|
+
message: string;
|
|
22
|
+
percent?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AgentFail {
|
|
26
|
+
type: "agent.fail";
|
|
27
|
+
taskId: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AgentChatReply {
|
|
32
|
+
type: "agent.chat.reply";
|
|
33
|
+
channelId: string;
|
|
34
|
+
content: string;
|
|
35
|
+
replyTo?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentTyping {
|
|
39
|
+
type: "agent.typing";
|
|
40
|
+
channelId: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type AgentMessage =
|
|
44
|
+
| AgentHeartbeat
|
|
45
|
+
| AgentComplete
|
|
46
|
+
| AgentProgress
|
|
47
|
+
| AgentFail
|
|
48
|
+
| AgentChatReply
|
|
49
|
+
| AgentTyping;
|
|
50
|
+
|
|
51
|
+
export interface ServerTask {
|
|
52
|
+
type: "server.task";
|
|
53
|
+
taskId: string;
|
|
54
|
+
title: string;
|
|
55
|
+
description: string;
|
|
56
|
+
input: string;
|
|
57
|
+
skillTags: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ServerChatMessage {
|
|
61
|
+
messageId: string;
|
|
62
|
+
channelId: string;
|
|
63
|
+
content: string;
|
|
64
|
+
context: Array<{
|
|
65
|
+
id: string;
|
|
66
|
+
senderType: string;
|
|
67
|
+
senderName: string;
|
|
68
|
+
content: string;
|
|
69
|
+
createdAt: number;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type ServerMessage = ServerTask | ServerChatMessage;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Runtime definitions — shared between bridge, server, and UI.
|
|
77
|
+
*
|
|
78
|
+
* bridgeManaged = true: executed by bridge daemon (spawns CLI process, messages via /api/bridge/poll)
|
|
79
|
+
* bridgeManaged = false: executed by something else (e.g. OpenClaw plugin, messages via /api/machines/poll)
|
|
80
|
+
*/
|
|
81
|
+
export interface RuntimeDef {
|
|
82
|
+
id: string;
|
|
83
|
+
displayName: string;
|
|
84
|
+
/** CLI binary name to detect on PATH (empty = not detectable) */
|
|
85
|
+
binary: string;
|
|
86
|
+
/** Whether the bridge daemon spawns and manages a CLI process for this runtime */
|
|
87
|
+
bridgeManaged: boolean;
|
|
88
|
+
/** Badge color for UI */
|
|
89
|
+
badgeColor: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const RUNTIMES: RuntimeDef[] = [
|
|
93
|
+
{ id: "claude-code", displayName: "Claude Code", binary: "claude", bridgeManaged: true, badgeColor: "bg-[#7c5cbf] text-white" },
|
|
94
|
+
{ id: "codex", displayName: "Codex", binary: "codex", bridgeManaged: true, badgeColor: "bg-[#2ea043] text-white" },
|
|
95
|
+
{ id: "qwen-code", displayName: "Qwen Code", binary: "qwen", bridgeManaged: true, badgeColor: "bg-[#1a73e8] text-white" },
|
|
96
|
+
{ id: "kimi", displayName: "Kimi", binary: "kimi", bridgeManaged: true, badgeColor: "bg-[#ff6b35] text-white" },
|
|
97
|
+
{ id: "openclaw", displayName: "OpenClaw", binary: "", bridgeManaged: false, badgeColor: "bg-[var(--color-border)] text-[var(--color-text-secondary)]" },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/** Bridge-managed runtimes: server skips dequeue in /api/machines/poll (they use /api/bridge/poll instead) */
|
|
101
|
+
export const BRIDGE_MANAGED_RUNTIME_IDS = new Set(RUNTIMES.filter((r) => r.bridgeManaged).map((r) => r.id));
|
|
102
|
+
export const RUNTIME_MAP = new Map(RUNTIMES.map((r) => [r.id, r]));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clawroom/sdk",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "ClawRoom SDK — polling client and protocol types for connecting any agent to ClawRoom",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/clawroom/clawroom.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"clawroom",
|
|
15
|
+
"sdk",
|
|
16
|
+
"agent",
|
|
17
|
+
"workspace"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@clawroom/protocol": "^0.5.1"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentMessage,
|
|
3
|
+
ServerTask,
|
|
4
|
+
ServerChatMessage,
|
|
5
|
+
} from "./protocol.js";
|
|
6
|
+
import { WsTransport } from "./ws-transport.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
|
|
9
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
10
|
+
const POLL_INTERVAL_MS = 10_000;
|
|
11
|
+
|
|
12
|
+
export type TaskCallback = (task: ServerTask) => void;
|
|
13
|
+
export type ChatCallback = (messages: ServerChatMessage[]) => void;
|
|
14
|
+
|
|
15
|
+
export type ClawroomClientOptions = {
|
|
16
|
+
endpoint?: string;
|
|
17
|
+
token: string;
|
|
18
|
+
deviceId: string;
|
|
19
|
+
skills: string[];
|
|
20
|
+
kind?: string;
|
|
21
|
+
wsUrl?: string;
|
|
22
|
+
log?: {
|
|
23
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
24
|
+
warn?: (message: string, ...args: unknown[]) => void;
|
|
25
|
+
error?: (message: string, ...args: unknown[]) => void;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ClawRoom SDK client.
|
|
31
|
+
* WebSocket primary for real-time push, HTTP polling as fallback.
|
|
32
|
+
*/
|
|
33
|
+
export class ClawroomClient {
|
|
34
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
35
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
36
|
+
protected stopped = false;
|
|
37
|
+
protected readonly httpBase: string;
|
|
38
|
+
protected readonly options: ClawroomClientOptions;
|
|
39
|
+
protected taskCallbacks: TaskCallback[] = [];
|
|
40
|
+
protected chatCallbacks: ChatCallback[] = [];
|
|
41
|
+
private wsTransport: WsTransport | null = null;
|
|
42
|
+
|
|
43
|
+
constructor(options: ClawroomClientOptions) {
|
|
44
|
+
this.options = options;
|
|
45
|
+
this.httpBase = (options.endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private get wsUrl(): string {
|
|
49
|
+
if (this.options.wsUrl) return this.options.wsUrl;
|
|
50
|
+
return this.httpBase.replace(/\/api\/agents$/, "").replace(/^http/, "ws") + "/api/ws";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
connect(): void {
|
|
54
|
+
this.stopped = false;
|
|
55
|
+
this.startHeartbeat();
|
|
56
|
+
void this.register();
|
|
57
|
+
|
|
58
|
+
// WebSocket transport
|
|
59
|
+
this.wsTransport = new WsTransport({
|
|
60
|
+
url: this.wsUrl,
|
|
61
|
+
token: this.options.token,
|
|
62
|
+
log: { info: (m) => this.options.log?.info?.(m), warn: (m) => this.options.log?.warn?.(m) },
|
|
63
|
+
onConnected: () => { this.options.log?.info?.("[clawroom] WebSocket connected"); },
|
|
64
|
+
onDisconnected: () => { this.options.log?.info?.("[clawroom] WebSocket disconnected"); },
|
|
65
|
+
onMessage: (msg) => {
|
|
66
|
+
if (msg.type === "task" && msg.task) {
|
|
67
|
+
for (const cb of this.taskCallbacks) cb(msg.task);
|
|
68
|
+
}
|
|
69
|
+
if (msg.type === "chat" && Array.isArray(msg.messages)) {
|
|
70
|
+
for (const cb of this.chatCallbacks) cb(msg.messages);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
this.wsTransport.connect();
|
|
75
|
+
|
|
76
|
+
// HTTP poll fallback — only when WS is down
|
|
77
|
+
this.stopPolling();
|
|
78
|
+
this.pollTimer = setInterval(() => {
|
|
79
|
+
if (this.wsTransport?.connected) return;
|
|
80
|
+
void this.pollTick();
|
|
81
|
+
}, POLL_INTERVAL_MS);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
disconnect(): void {
|
|
85
|
+
this.stopped = true;
|
|
86
|
+
this.stopHeartbeat();
|
|
87
|
+
this.stopPolling();
|
|
88
|
+
this.wsTransport?.disconnect();
|
|
89
|
+
this.wsTransport = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
send(message: AgentMessage): void {
|
|
93
|
+
this.sendViaHttp(message).catch(() => {});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onTask(cb: TaskCallback): void { this.taskCallbacks.push(cb); }
|
|
97
|
+
onChatMessage(cb: ChatCallback): void { this.chatCallbacks.push(cb); }
|
|
98
|
+
|
|
99
|
+
// ── Heartbeat ─────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
private startHeartbeat(): void {
|
|
102
|
+
this.stopHeartbeat();
|
|
103
|
+
this.heartbeatTimer = setInterval(() => {
|
|
104
|
+
if (!this.stopped) void this.register();
|
|
105
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private stopHeartbeat(): void {
|
|
109
|
+
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private stopPolling(): void {
|
|
113
|
+
if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
protected async register(): Promise<void> {
|
|
117
|
+
try {
|
|
118
|
+
await this.httpRequest("POST", "/heartbeat", {
|
|
119
|
+
deviceId: this.options.deviceId,
|
|
120
|
+
skills: this.options.skills,
|
|
121
|
+
kind: this.options.kind ?? "openclaw",
|
|
122
|
+
});
|
|
123
|
+
this.onPollSuccess(undefined);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
this.options.log?.warn?.(`[clawroom] heartbeat error: ${err}`);
|
|
126
|
+
this.onPollError(err);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected async pollTick(): Promise<void> {
|
|
131
|
+
if (this.stopped) return;
|
|
132
|
+
try {
|
|
133
|
+
const res = await this.httpRequest("POST", "/poll", {});
|
|
134
|
+
this.onPollSuccess(res?.agentId);
|
|
135
|
+
if (res.task) {
|
|
136
|
+
this.options.log?.info?.(`[clawroom] received task ${res.task.taskId}: ${res.task.title}`);
|
|
137
|
+
for (const cb of this.taskCallbacks) cb(res.task);
|
|
138
|
+
}
|
|
139
|
+
if (res.chat && Array.isArray(res.chat) && res.chat.length > 0) {
|
|
140
|
+
this.options.log?.info?.(`[clawroom] received ${res.chat.length} chat mention(s)`);
|
|
141
|
+
for (const cb of this.chatCallbacks) cb(res.chat);
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
this.options.log?.warn?.(`[clawroom] poll error: ${err}`);
|
|
145
|
+
this.onPollError(err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected onPollSuccess(_agentId: string | undefined): void {}
|
|
150
|
+
protected onPollError(_err: unknown): void {}
|
|
151
|
+
|
|
152
|
+
// ── HTTP ──────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
private async sendViaHttp(message: AgentMessage): Promise<void> {
|
|
155
|
+
switch (message.type) {
|
|
156
|
+
case "agent.complete": await this.httpRequest("POST", "/complete", { taskId: message.taskId, output: message.output, attachments: message.attachments }); break;
|
|
157
|
+
case "agent.fail": await this.httpRequest("POST", "/fail", { taskId: message.taskId, reason: message.reason }); break;
|
|
158
|
+
case "agent.progress": await this.httpRequest("POST", "/progress", { taskId: message.taskId, message: message.message, percent: message.percent }); break;
|
|
159
|
+
case "agent.heartbeat": await this.httpRequest("POST", "/heartbeat", {}); break;
|
|
160
|
+
case "agent.chat.reply": await this.httpRequest("POST", "/chat/reply", { channelId: message.channelId, content: message.content, replyTo: message.replyTo }); break;
|
|
161
|
+
case "agent.typing": await this.httpRequest("POST", "/typing", { channelId: message.channelId }); break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected async httpRequest(method: string, path: string, body: unknown): Promise<any> {
|
|
166
|
+
const res = await fetch(`${this.httpBase}${path}`, {
|
|
167
|
+
method,
|
|
168
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.options.token}` },
|
|
169
|
+
body: JSON.stringify(body),
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
const text = await res.text().catch(() => "");
|
|
173
|
+
this.onHttpError(res.status, text);
|
|
174
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
175
|
+
}
|
|
176
|
+
return res.json();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
protected onHttpError(_status: number, _text: string): void {}
|
|
180
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { ClawroomClient } from "./client.js";
|
|
2
|
+
export type { ClawroomClientOptions } from "./client.js";
|
|
3
|
+
export { ClawroomMachineClient } from "./machine-client.js";
|
|
4
|
+
export type { ClawroomMachineClientOptions } from "./machine-client.js";
|
|
5
|
+
export { WsTransport } from "./ws-transport.js";
|
|
6
|
+
export type { WsTransportOptions } from "./ws-transport.js";
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
AgentMessage,
|
|
10
|
+
AgentHeartbeat,
|
|
11
|
+
AgentComplete,
|
|
12
|
+
AgentProgress,
|
|
13
|
+
AgentResultFile,
|
|
14
|
+
AgentFail,
|
|
15
|
+
AgentChatReply,
|
|
16
|
+
AgentTyping,
|
|
17
|
+
ServerMessage,
|
|
18
|
+
ServerTask,
|
|
19
|
+
ServerChatMessage,
|
|
20
|
+
} from "./protocol.js";
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawRoom Machine Client — machine-level auth with multi-agent support.
|
|
3
|
+
* WebSocket primary for real-time push, HTTP polling as fallback.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import { WsTransport } from "./ws-transport.js";
|
|
8
|
+
|
|
9
|
+
export type ClawroomMachineClientOptions = {
|
|
10
|
+
endpoint?: string;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
hostname?: string;
|
|
13
|
+
capabilities?: string[];
|
|
14
|
+
wsUrl?: string;
|
|
15
|
+
log?: {
|
|
16
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
17
|
+
warn?: (message: string, ...args: unknown[]) => void;
|
|
18
|
+
error?: (message: string, ...args: unknown[]) => void;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type AgentWork = {
|
|
23
|
+
agentId: string;
|
|
24
|
+
agentName: string;
|
|
25
|
+
task: { type: "server.task"; taskId: string; title: string; description: string; input: string; skillTags: string[] } | null;
|
|
26
|
+
chat: Array<{ messageId: string; channelId: string; content: string; isMention: boolean; context: unknown[] }> | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type MachineHeartbeatResponse = {
|
|
30
|
+
machineId: string;
|
|
31
|
+
agents?: Array<{ id: string }>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type MachinePollResponse = {
|
|
35
|
+
machineId: string;
|
|
36
|
+
agents: AgentWork[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export class ClawroomMachineClient {
|
|
40
|
+
private options: ClawroomMachineClientOptions;
|
|
41
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
42
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
43
|
+
private taskHandler: ((agentId: string, task: AgentWork["task"]) => void) | null = null;
|
|
44
|
+
private chatHandler: ((agentId: string, messages: NonNullable<AgentWork["chat"]>) => void) | null = null;
|
|
45
|
+
private _connected = false;
|
|
46
|
+
private _stopped = false;
|
|
47
|
+
private wsTransport: WsTransport | null = null;
|
|
48
|
+
private subscribedAgentIds = new Set<string>();
|
|
49
|
+
private recentChatIds = new Map<string, number>();
|
|
50
|
+
|
|
51
|
+
constructor(options: ClawroomMachineClientOptions) {
|
|
52
|
+
this.options = options;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private get baseUrl(): string {
|
|
56
|
+
return this.options.endpoint ?? "http://localhost:3000/api/machines";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private get wsUrl(): string {
|
|
60
|
+
if (this.options.wsUrl) return this.options.wsUrl;
|
|
61
|
+
return this.baseUrl.replace(/\/api\/machines$/, "").replace(/^http/, "ws") + "/api/ws";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async httpRequest(method: string, path: string, body?: unknown): Promise<unknown> {
|
|
65
|
+
const url = `${this.baseUrl}${path}`;
|
|
66
|
+
const res = await fetch(url, {
|
|
67
|
+
method,
|
|
68
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.options.apiKey}` },
|
|
69
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
|
72
|
+
return res.json();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onAgentTask(handler: (agentId: string, task: AgentWork["task"]) => void) { this.taskHandler = handler; }
|
|
76
|
+
onAgentChat(handler: (agentId: string, messages: NonNullable<AgentWork["chat"]>) => void) { this.chatHandler = handler; }
|
|
77
|
+
|
|
78
|
+
async sendAgentComplete(agentId: string, taskId: string, output: string, attachments?: Array<{ filename: string; mimeType: string; data: string }>) {
|
|
79
|
+
await this.httpRequest("POST", "/complete", { agentId, taskId, output, attachments });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async sendAgentFail(agentId: string, taskId: string, reason: string) {
|
|
83
|
+
await this.httpRequest("POST", "/fail", { agentId, taskId, reason });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async sendAgentChatReply(agentId: string, channelId: string, content: string) {
|
|
87
|
+
await this.httpRequest("POST", "/chat-reply", { agentId, channelId, content });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async sendAgentTyping(agentId: string, channelId: string) {
|
|
91
|
+
await this.httpRequest("POST", "/typing", { agentId, channelId });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get connected() { return this._connected; }
|
|
95
|
+
get stopped() { return this._stopped; }
|
|
96
|
+
|
|
97
|
+
private rememberChat(messageId: string): boolean {
|
|
98
|
+
if (!messageId) return true;
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
for (const [id, seenAt] of this.recentChatIds) {
|
|
101
|
+
if (now - seenAt > 10 * 60_000) this.recentChatIds.delete(id);
|
|
102
|
+
}
|
|
103
|
+
if (this.recentChatIds.has(messageId)) return false;
|
|
104
|
+
this.recentChatIds.set(messageId, now);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private syncAgentSubscriptions(agentIds: string[]) {
|
|
109
|
+
if (agentIds.length === 0) return;
|
|
110
|
+
for (const agentId of agentIds) {
|
|
111
|
+
this.subscribedAgentIds.add(agentId);
|
|
112
|
+
this.wsTransport?.send({ type: "subscribe_agent", agentId });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async pollOnce() {
|
|
117
|
+
if (!this._connected) return;
|
|
118
|
+
try {
|
|
119
|
+
const result = (await this.httpRequest("POST", "/poll", {})) as MachinePollResponse;
|
|
120
|
+
this.syncAgentSubscriptions(result.agents.map((agent) => agent.agentId));
|
|
121
|
+
for (const agent of result.agents) {
|
|
122
|
+
if (agent.task && this.taskHandler) this.taskHandler(agent.agentId, agent.task);
|
|
123
|
+
if (agent.chat && agent.chat.length > 0 && this.chatHandler) {
|
|
124
|
+
const freshMessages = agent.chat.filter((message) => this.rememberChat(message.messageId));
|
|
125
|
+
if (freshMessages.length > 0) this.chatHandler(agent.agentId, freshMessages);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
this.options.log?.warn?.(`[machine] poll error: ${err}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
connect() {
|
|
134
|
+
this._stopped = false;
|
|
135
|
+
const hostname = this.options.hostname ?? os.hostname();
|
|
136
|
+
const hbBody = { hostname, capabilities: this.options.capabilities };
|
|
137
|
+
|
|
138
|
+
// Heartbeat every 30s (always HTTP)
|
|
139
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
140
|
+
try {
|
|
141
|
+
const result = (await this.httpRequest("POST", "/heartbeat", hbBody)) as MachineHeartbeatResponse;
|
|
142
|
+
this.syncAgentSubscriptions((result.agents ?? []).map((agent) => agent.id));
|
|
143
|
+
if (!this._connected) { this._connected = true; this.options.log?.info?.("[machine] connected"); }
|
|
144
|
+
} catch {
|
|
145
|
+
if (this._connected) { this._connected = false; this.options.log?.warn?.("[machine] disconnected"); }
|
|
146
|
+
}
|
|
147
|
+
}, 30_000);
|
|
148
|
+
|
|
149
|
+
this.httpRequest("POST", "/heartbeat", hbBody)
|
|
150
|
+
.then((result) => {
|
|
151
|
+
const data = result as MachineHeartbeatResponse;
|
|
152
|
+
this._connected = true;
|
|
153
|
+
this.syncAgentSubscriptions((data.agents ?? []).map((agent) => agent.id));
|
|
154
|
+
this.options.log?.info?.("[machine] connected");
|
|
155
|
+
})
|
|
156
|
+
.catch((err) => this.options.log?.warn?.(`[machine] initial heartbeat failed: ${err}`));
|
|
157
|
+
|
|
158
|
+
// WebSocket transport
|
|
159
|
+
this.wsTransport = new WsTransport({
|
|
160
|
+
url: this.wsUrl,
|
|
161
|
+
token: this.options.apiKey,
|
|
162
|
+
log: { info: (m) => this.options.log?.info?.(m), warn: (m) => this.options.log?.warn?.(m) },
|
|
163
|
+
onConnected: () => {
|
|
164
|
+
this.syncAgentSubscriptions([...this.subscribedAgentIds]);
|
|
165
|
+
this.options.log?.info?.("[machine] WebSocket connected");
|
|
166
|
+
},
|
|
167
|
+
onDisconnected: () => {
|
|
168
|
+
this.options.log?.info?.("[machine] WebSocket disconnected, HTTP polling active");
|
|
169
|
+
},
|
|
170
|
+
onMessage: (msg) => {
|
|
171
|
+
if (msg.type === "message") {
|
|
172
|
+
const m = msg.message;
|
|
173
|
+
const targetAgentId = msg.agentId ?? m?.agentId;
|
|
174
|
+
const messageId = m?.id ?? msg.messageId ?? "";
|
|
175
|
+
if (m && targetAgentId && this.chatHandler && this.rememberChat(messageId)) {
|
|
176
|
+
this.chatHandler(targetAgentId, [{
|
|
177
|
+
messageId,
|
|
178
|
+
channelId: m.channelId ?? "",
|
|
179
|
+
content: m.content ?? "",
|
|
180
|
+
isMention: msg.isMention ?? false,
|
|
181
|
+
context: msg.context ?? [],
|
|
182
|
+
}]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (msg.type === "task" && msg.agentId && this.taskHandler) {
|
|
186
|
+
this.taskHandler(msg.agentId, msg.task);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
this.wsTransport.connect();
|
|
191
|
+
|
|
192
|
+
// Keep HTTP polling active even when WS is up.
|
|
193
|
+
// Machine WS chat delivery is best-effort; polling is the durable delivery path.
|
|
194
|
+
this.pollTimer = setInterval(async () => {
|
|
195
|
+
await this.pollOnce();
|
|
196
|
+
}, 10_000);
|
|
197
|
+
|
|
198
|
+
void this.pollOnce();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
disconnect() {
|
|
202
|
+
this._stopped = true;
|
|
203
|
+
this._connected = false;
|
|
204
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
205
|
+
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
206
|
+
this.wsTransport?.disconnect();
|
|
207
|
+
this.wsTransport = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AgentHeartbeat,
|
|
3
|
+
AgentResultFile,
|
|
4
|
+
AgentComplete,
|
|
5
|
+
AgentProgress,
|
|
6
|
+
AgentFail,
|
|
7
|
+
AgentChatReply,
|
|
8
|
+
AgentTyping,
|
|
9
|
+
AgentMessage,
|
|
10
|
+
ServerTask,
|
|
11
|
+
ServerChatMessage,
|
|
12
|
+
ServerMessage,
|
|
13
|
+
RuntimeDef,
|
|
14
|
+
} from "@clawroom/protocol";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
RUNTIMES,
|
|
18
|
+
BRIDGE_MANAGED_RUNTIME_IDS,
|
|
19
|
+
RUNTIME_MAP,
|
|
20
|
+
} from "@clawroom/protocol";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared WebSocket transport with auto-reconnect and fallback support.
|
|
3
|
+
* Used by both ClawroomClient and ClawroomMachineClient.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type WsTransportOptions = {
|
|
7
|
+
url: string;
|
|
8
|
+
token: string;
|
|
9
|
+
onMessage: (msg: any) => void;
|
|
10
|
+
onConnected: () => void;
|
|
11
|
+
onDisconnected: () => void;
|
|
12
|
+
log?: {
|
|
13
|
+
info?: (message: string) => void;
|
|
14
|
+
warn?: (message: string) => void;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class WsTransport {
|
|
19
|
+
private ws: any = null;
|
|
20
|
+
private options: WsTransportOptions;
|
|
21
|
+
private _connected = false;
|
|
22
|
+
private stopped = false;
|
|
23
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
24
|
+
private reconnectDelay = 1000;
|
|
25
|
+
private failCount = 0;
|
|
26
|
+
|
|
27
|
+
static readonly MAX_RECONNECT_DELAY = 60_000;
|
|
28
|
+
static readonly MAX_RECONNECT_FAILS = 10;
|
|
29
|
+
|
|
30
|
+
constructor(options: WsTransportOptions) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get connected() { return this._connected; }
|
|
35
|
+
|
|
36
|
+
async connect() {
|
|
37
|
+
if (this.stopped) return;
|
|
38
|
+
|
|
39
|
+
let WebSocket: any;
|
|
40
|
+
try {
|
|
41
|
+
WebSocket = (await import("ws")).default;
|
|
42
|
+
} catch {
|
|
43
|
+
this.options.log?.info?.("ws module not available, skipping WebSocket");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
this.ws = new WebSocket(this.options.url);
|
|
49
|
+
} catch {
|
|
50
|
+
this.scheduleReconnect();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.ws.on("open", () => {
|
|
55
|
+
this.reconnectDelay = 1000;
|
|
56
|
+
this.ws.send(JSON.stringify({ type: "auth", token: this.options.token }));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.ws.on("message", (raw: any) => {
|
|
60
|
+
let msg: any;
|
|
61
|
+
try { msg = JSON.parse(raw.toString()); } catch { return; }
|
|
62
|
+
|
|
63
|
+
if (msg.type === "auth_ok") {
|
|
64
|
+
this._connected = true;
|
|
65
|
+
this.failCount = 0;
|
|
66
|
+
this.options.onConnected();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (msg.type === "auth_error") {
|
|
71
|
+
this._connected = false;
|
|
72
|
+
this.options.log?.warn?.(`WS auth failed: ${msg.error ?? "unknown"}`);
|
|
73
|
+
this.ws?.close();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (msg.type === "pong") return;
|
|
78
|
+
|
|
79
|
+
this.options.onMessage(msg);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.ws.on("close", () => {
|
|
83
|
+
const wasConnected = this._connected;
|
|
84
|
+
this._connected = false;
|
|
85
|
+
if (wasConnected) this.options.onDisconnected();
|
|
86
|
+
if (!this.stopped) this.scheduleReconnect();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.ws.on("error", () => {
|
|
90
|
+
this._connected = false;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
send(msg: any) {
|
|
95
|
+
if (this._connected && this.ws?.readyState === 1) {
|
|
96
|
+
this.ws.send(JSON.stringify(msg));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
disconnect() {
|
|
101
|
+
this.stopped = true;
|
|
102
|
+
this._connected = false;
|
|
103
|
+
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
|
|
104
|
+
if (this.ws) { this.ws.close(); this.ws = null; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private scheduleReconnect() {
|
|
108
|
+
if (this.stopped || this.reconnectTimer) return;
|
|
109
|
+
|
|
110
|
+
this.failCount++;
|
|
111
|
+
if (this.failCount > WsTransport.MAX_RECONNECT_FAILS) {
|
|
112
|
+
this.options.log?.warn?.(`WS reconnect failed ${this.failCount} times, giving up. HTTP polling active.`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const jitter = Math.random() * 3000;
|
|
117
|
+
this.reconnectTimer = setTimeout(() => {
|
|
118
|
+
this.reconnectTimer = null;
|
|
119
|
+
this.connect();
|
|
120
|
+
}, Math.min(this.reconnectDelay + jitter, WsTransport.MAX_RECONNECT_DELAY));
|
|
121
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, WsTransport.MAX_RECONNECT_DELAY);
|
|
122
|
+
}
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawroom/openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "OpenClaw channel plugin for ClawRoom",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@clawroom/sdk"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@clawroom/sdk": "^0.
|
|
25
|
+
"@clawroom/sdk": "^0.5.1"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"openclaw": "*"
|
package/src/channel.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
3
|
-
import { collectSkills } from "./skill-reporter.js";
|
|
3
|
+
// import { collectSkills } from "./skill-reporter.js";
|
|
4
4
|
import { getClawroomRuntime } from "./runtime.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ClawroomPluginClient } from "./client.js";
|
|
6
6
|
import { setupTaskExecutor } from "./task-executor.js";
|
|
7
7
|
import { setupChatExecutor } from "./chat-executor.js";
|
|
8
8
|
|
|
9
9
|
// ── Config resolution ────────────────────────────────────────────────
|
|
10
10
|
|
|
11
|
-
const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/
|
|
11
|
+
const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/machines";
|
|
12
12
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
13
13
|
|
|
14
14
|
interface ClawroomAccountConfig {
|
|
15
|
-
|
|
15
|
+
api_key?: string;
|
|
16
|
+
token?: string; // deprecated, fallback
|
|
16
17
|
endpoint?: string;
|
|
17
18
|
skills?: string[];
|
|
18
19
|
enabled?: boolean;
|
|
@@ -23,7 +24,7 @@ interface ResolvedClawroomAccount {
|
|
|
23
24
|
name: string;
|
|
24
25
|
enabled: boolean;
|
|
25
26
|
configured: boolean;
|
|
26
|
-
|
|
27
|
+
apiKey: string;
|
|
27
28
|
endpoint: string;
|
|
28
29
|
skills: string[];
|
|
29
30
|
}
|
|
@@ -39,7 +40,7 @@ function resolveClawroomAccount(opts: {
|
|
|
39
40
|
accountId?: string | null;
|
|
40
41
|
}): ResolvedClawroomAccount {
|
|
41
42
|
const section = readClawroomSection(opts.cfg);
|
|
42
|
-
const
|
|
43
|
+
const apiKey = section.api_key ?? section.token ?? "";
|
|
43
44
|
const endpoint = section.endpoint || DEFAULT_ENDPOINT;
|
|
44
45
|
const skills = Array.isArray(section.skills) ? section.skills : [];
|
|
45
46
|
const enabled = section.enabled !== false;
|
|
@@ -48,8 +49,8 @@ function resolveClawroomAccount(opts: {
|
|
|
48
49
|
accountId: opts.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
49
50
|
name: "ClawRoom",
|
|
50
51
|
enabled,
|
|
51
|
-
configured:
|
|
52
|
-
|
|
52
|
+
configured: apiKey.length > 0,
|
|
53
|
+
apiKey,
|
|
53
54
|
endpoint,
|
|
54
55
|
skills,
|
|
55
56
|
};
|
|
@@ -57,7 +58,7 @@ function resolveClawroomAccount(opts: {
|
|
|
57
58
|
|
|
58
59
|
// ── Persistent client per gateway lifecycle ───────────────────────
|
|
59
60
|
|
|
60
|
-
let activeClient:
|
|
61
|
+
let activeClient: ClawroomPluginClient | null = null;
|
|
61
62
|
|
|
62
63
|
// ── Channel plugin definition ────────────────────────────────────────
|
|
63
64
|
|
|
@@ -104,17 +105,9 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
104
105
|
deliveryMode: "direct",
|
|
105
106
|
|
|
106
107
|
sendText: async ({ to, text }) => {
|
|
107
|
-
// Outbound
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
// to deliver a reply through the channel outbound path.
|
|
111
|
-
if (activeClient) {
|
|
112
|
-
activeClient.send({
|
|
113
|
-
type: "agent.complete",
|
|
114
|
-
taskId: to,
|
|
115
|
-
output: text,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
108
|
+
// Outbound path — task results are sent directly by executors,
|
|
109
|
+
// this is a fallback if openclaw routing triggers outbound delivery.
|
|
110
|
+
// No-op for machine client (executors handle it).
|
|
118
111
|
return { channel: "clawroom", messageId: to, to };
|
|
119
112
|
},
|
|
120
113
|
},
|
|
@@ -125,24 +118,16 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
125
118
|
|
|
126
119
|
if (!account.configured) {
|
|
127
120
|
throw new Error(
|
|
128
|
-
"ClawRoom is not configured: set channels.clawroom.
|
|
121
|
+
"ClawRoom is not configured: set channels.clawroom.api_key in your OpenClaw config.",
|
|
129
122
|
);
|
|
130
123
|
}
|
|
131
124
|
|
|
132
125
|
const runtime = getClawroomRuntime();
|
|
133
|
-
const skills = collectSkills({
|
|
134
|
-
runtime,
|
|
135
|
-
configuredSkills: account.skills,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const deviceId = resolveDeviceId(ctx);
|
|
139
126
|
const log = ctx.log ?? undefined;
|
|
140
127
|
|
|
141
|
-
const client = new
|
|
128
|
+
const client = new ClawroomPluginClient({
|
|
142
129
|
endpoint: account.endpoint,
|
|
143
|
-
|
|
144
|
-
deviceId,
|
|
145
|
-
skills,
|
|
130
|
+
apiKey: account.apiKey,
|
|
146
131
|
log,
|
|
147
132
|
});
|
|
148
133
|
|
|
@@ -170,23 +155,11 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
170
155
|
});
|
|
171
156
|
};
|
|
172
157
|
|
|
173
|
-
// Wire up
|
|
158
|
+
// Wire up polling events to OpenClaw health status
|
|
174
159
|
client.onWelcome(() => publishConnected());
|
|
175
|
-
client.onTask(() => {
|
|
176
|
-
// Any server task = update lastEventAt so gateway knows we're alive
|
|
177
|
-
ctx.setStatus({
|
|
178
|
-
accountId: account.accountId,
|
|
179
|
-
running: true,
|
|
180
|
-
connected: true,
|
|
181
|
-
lastEventAt: Date.now(),
|
|
182
|
-
lastStartAt: Date.now(),
|
|
183
|
-
lastStopAt: null,
|
|
184
|
-
lastError: null,
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
160
|
client.onDisconnect(() => publishDisconnected());
|
|
188
|
-
client.onFatal((reason
|
|
189
|
-
log?.error?.(`[clawroom] fatal error
|
|
161
|
+
client.onFatal((reason) => {
|
|
162
|
+
log?.error?.(`[clawroom] fatal error: ${reason}`);
|
|
190
163
|
ctx.setStatus({
|
|
191
164
|
accountId: account.accountId,
|
|
192
165
|
running: false,
|
package/src/client.ts
CHANGED
|
@@ -30,7 +30,7 @@ export class ClawroomPluginClient {
|
|
|
30
30
|
onAgentChat(handler: (agentId: string, messages: any[]) => void) { this.inner.onAgentChat(handler); }
|
|
31
31
|
|
|
32
32
|
async sendComplete(agentId: string, taskId: string, output: string, attachments?: Array<{ filename: string; mimeType: string; data: string }>) {
|
|
33
|
-
await this.inner.sendAgentComplete(agentId, taskId, output);
|
|
33
|
+
await this.inner.sendAgentComplete(agentId, taskId, output, attachments);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
async sendFail(agentId: string, taskId: string, reason: string) {
|