@clawroom/openclaw 0.5.0 → 0.5.19
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/node_modules/@clawroom/protocol/package.json +17 -0
- package/node_modules/@clawroom/protocol/src/index.ts +170 -0
- package/node_modules/@clawroom/sdk/package.json +25 -0
- package/node_modules/@clawroom/sdk/src/client.ts +430 -0
- package/node_modules/@clawroom/sdk/src/index.ts +22 -0
- package/node_modules/@clawroom/sdk/src/machine-client.ts +356 -0
- package/node_modules/@clawroom/sdk/src/protocol.ts +22 -0
- package/node_modules/@clawroom/sdk/src/ws-transport.ts +218 -0
- package/package.json +2 -2
- package/src/channel.ts +23 -59
- package/src/chat-executor.ts +185 -26
- package/src/reflections.ts +60 -0
- package/src/runtime.ts +1 -0
- package/src/task-executor.ts +114 -20
- package/src/client.ts +0 -56
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clawroom/protocol",
|
|
3
|
+
"version": "0.5.19",
|
|
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,170 @@
|
|
|
1
|
+
export interface AgentHeartbeat {
|
|
2
|
+
type: "agent.heartbeat";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface AgentWorkRef {
|
|
6
|
+
workId: string;
|
|
7
|
+
leaseToken: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AgentResultFile {
|
|
11
|
+
filename: string;
|
|
12
|
+
mimeType: string;
|
|
13
|
+
data: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AgentComplete {
|
|
17
|
+
type: "agent.complete";
|
|
18
|
+
taskId: string;
|
|
19
|
+
output: string;
|
|
20
|
+
attachments?: AgentResultFile[];
|
|
21
|
+
workRef?: AgentWorkRef;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AgentProgress {
|
|
25
|
+
type: "agent.progress";
|
|
26
|
+
taskId: string;
|
|
27
|
+
message: string;
|
|
28
|
+
workRef?: AgentWorkRef;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AgentFail {
|
|
32
|
+
type: "agent.fail";
|
|
33
|
+
taskId: string;
|
|
34
|
+
reason: string;
|
|
35
|
+
workRef?: AgentWorkRef;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentChatReply {
|
|
39
|
+
type: "agent.chat.reply";
|
|
40
|
+
channelId: string;
|
|
41
|
+
content: string;
|
|
42
|
+
replyTo?: string;
|
|
43
|
+
workRefs?: AgentWorkRef[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AgentTyping {
|
|
47
|
+
type: "agent.typing";
|
|
48
|
+
channelId: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type AgentMessage =
|
|
52
|
+
| AgentHeartbeat
|
|
53
|
+
| AgentComplete
|
|
54
|
+
| AgentProgress
|
|
55
|
+
| AgentFail
|
|
56
|
+
| AgentChatReply
|
|
57
|
+
| AgentTyping;
|
|
58
|
+
|
|
59
|
+
export interface ServerTask {
|
|
60
|
+
type: "server.task";
|
|
61
|
+
taskId: string;
|
|
62
|
+
title: string;
|
|
63
|
+
description: string;
|
|
64
|
+
input: string;
|
|
65
|
+
channelId?: string | null;
|
|
66
|
+
workId: string;
|
|
67
|
+
leaseToken: string;
|
|
68
|
+
taskRole?: "root" | "child";
|
|
69
|
+
assignedAgentId?: string | null;
|
|
70
|
+
assignedAgentName?: string | null;
|
|
71
|
+
executionBrief?: ServerTaskExecutionBrief | null;
|
|
72
|
+
taskDiscussionContext?: ServerTaskDiscussionEntry[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface AgentChatProfile {
|
|
76
|
+
role: string;
|
|
77
|
+
systemPrompt: string;
|
|
78
|
+
memory: string;
|
|
79
|
+
continuityPacket: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ServerTaskExecutionBrief {
|
|
83
|
+
goal: string;
|
|
84
|
+
firstActions: string[];
|
|
85
|
+
blockingUnknowns: string[];
|
|
86
|
+
nonBlockingUnknowns: string[];
|
|
87
|
+
doneDefinition: string;
|
|
88
|
+
nextCheckpoint: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ServerTaskDiscussionEntry {
|
|
92
|
+
id: string;
|
|
93
|
+
senderType: string;
|
|
94
|
+
senderName: string;
|
|
95
|
+
content: string;
|
|
96
|
+
createdAt: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ChatAttachmentRef {
|
|
100
|
+
id?: string;
|
|
101
|
+
filename: string;
|
|
102
|
+
mimeType: string;
|
|
103
|
+
byteSize?: number;
|
|
104
|
+
downloadUrl: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ServerChatMessage {
|
|
108
|
+
type: "server.chat";
|
|
109
|
+
kind: "chat_reply" | "wake";
|
|
110
|
+
workId: string;
|
|
111
|
+
leaseToken: string;
|
|
112
|
+
messageId: string;
|
|
113
|
+
channelId: string;
|
|
114
|
+
taskId?: string | null;
|
|
115
|
+
content: string;
|
|
116
|
+
attachments?: ChatAttachmentRef[];
|
|
117
|
+
isMention?: boolean;
|
|
118
|
+
wakeReason?: "mention" | "dm" | "follow_through" | "goal_drift" | "trigger" | "broadcast" | "routed" | "coordination" | "review";
|
|
119
|
+
triggerReason?: string;
|
|
120
|
+
agentProfile: AgentChatProfile;
|
|
121
|
+
context: Array<{
|
|
122
|
+
id: string;
|
|
123
|
+
senderType: string;
|
|
124
|
+
senderName: string;
|
|
125
|
+
content: string;
|
|
126
|
+
createdAt: number;
|
|
127
|
+
}>;
|
|
128
|
+
replyToMessageId?: string | null;
|
|
129
|
+
senderName?: string;
|
|
130
|
+
senderType?: string;
|
|
131
|
+
createdAt?: number;
|
|
132
|
+
channelMembers?: Array<{ id: string; name: string; type: string }>;
|
|
133
|
+
taskTitle?: string | null;
|
|
134
|
+
taskRole?: "root" | "child";
|
|
135
|
+
assignedAgentId?: string | null;
|
|
136
|
+
assignedAgentName?: string | null;
|
|
137
|
+
executionBrief?: ServerTaskExecutionBrief | null;
|
|
138
|
+
taskDiscussionContext?: ServerTaskDiscussionEntry[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type ServerMessage = ServerTask | ServerChatMessage;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Runtime definitions — shared between bridge, server, and UI.
|
|
145
|
+
*
|
|
146
|
+
* bridgeManaged = true: executed by bridge daemon (spawns CLI process, messages via /api/bridge/poll)
|
|
147
|
+
* bridgeManaged = false: executed by something else (e.g. OpenClaw plugin, messages via /api/machines/poll)
|
|
148
|
+
*/
|
|
149
|
+
export interface RuntimeDef {
|
|
150
|
+
id: string;
|
|
151
|
+
displayName: string;
|
|
152
|
+
/** CLI binary name to detect on PATH (empty = not detectable) */
|
|
153
|
+
binary: string;
|
|
154
|
+
/** Whether the bridge daemon spawns and manages a CLI process for this runtime */
|
|
155
|
+
bridgeManaged: boolean;
|
|
156
|
+
/** Badge color for UI */
|
|
157
|
+
badgeColor: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const RUNTIMES: RuntimeDef[] = [
|
|
161
|
+
{ id: "claude-code", displayName: "Claude Code", binary: "claude", bridgeManaged: true, badgeColor: "bg-[#7c5cbf] text-white" },
|
|
162
|
+
{ id: "codex", displayName: "Codex", binary: "codex", bridgeManaged: true, badgeColor: "bg-[#2ea043] text-white" },
|
|
163
|
+
{ id: "qwen-code", displayName: "Qwen Code", binary: "qwen", bridgeManaged: true, badgeColor: "bg-[#1a73e8] text-white" },
|
|
164
|
+
{ id: "kimi", displayName: "Kimi", binary: "kimi", bridgeManaged: true, badgeColor: "bg-[#ff6b35] text-white" },
|
|
165
|
+
{ id: "openclaw", displayName: "OpenClaw", binary: "", bridgeManaged: false, badgeColor: "bg-[var(--color-border)] text-[var(--color-text-secondary)]" },
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
/** Bridge-managed runtimes: server skips dequeue in /api/machines/poll (they use /api/bridge/poll instead) */
|
|
169
|
+
export const BRIDGE_MANAGED_RUNTIME_IDS = new Set(RUNTIMES.filter((r) => r.bridgeManaged).map((r) => r.id));
|
|
170
|
+
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.19",
|
|
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,430 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentChatProfile,
|
|
3
|
+
AgentResultFile,
|
|
4
|
+
AgentMessage,
|
|
5
|
+
AgentWorkRef,
|
|
6
|
+
ServerTask,
|
|
7
|
+
ServerChatMessage,
|
|
8
|
+
} from "./protocol.js";
|
|
9
|
+
import { WsTransport } from "./ws-transport.js";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
|
|
12
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
13
|
+
const POLL_INTERVAL_MS = 10_000;
|
|
14
|
+
const DEFAULT_AGENT_CHAT_PROFILE: AgentChatProfile = {
|
|
15
|
+
role: "No role defined",
|
|
16
|
+
systemPrompt: "No system prompt configured",
|
|
17
|
+
memory: "No memory recorded yet",
|
|
18
|
+
continuityPacket: "No continuity packet available yet",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type TaskCallback = (task: ServerTask) => void;
|
|
22
|
+
export type ChatCallback = (messages: ServerChatMessage[]) => void;
|
|
23
|
+
export type ConnectedCallback = (agentId: string) => void;
|
|
24
|
+
export type DisconnectedCallback = () => void;
|
|
25
|
+
export type Unsubscribe = () => void;
|
|
26
|
+
|
|
27
|
+
export type ClawroomClientOptions = {
|
|
28
|
+
endpoint?: string;
|
|
29
|
+
token: string;
|
|
30
|
+
deviceId: string;
|
|
31
|
+
skills: string[];
|
|
32
|
+
kind?: string;
|
|
33
|
+
wsUrl?: string;
|
|
34
|
+
log?: {
|
|
35
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
36
|
+
warn?: (message: string, ...args: unknown[]) => void;
|
|
37
|
+
error?: (message: string, ...args: unknown[]) => void;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type AgentHeartbeatResponse = {
|
|
42
|
+
ok?: boolean;
|
|
43
|
+
agentId?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type AgentPollResponse = {
|
|
47
|
+
agentId?: string;
|
|
48
|
+
task: ServerTask | null;
|
|
49
|
+
chat: ServerChatMessage[] | null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* ClawRoom SDK client.
|
|
54
|
+
* WebSocket primary for real-time push, HTTP polling as fallback.
|
|
55
|
+
*/
|
|
56
|
+
export class ClawroomClient {
|
|
57
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
58
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
59
|
+
private heartbeatInFlight = false;
|
|
60
|
+
private pollInFlight = false;
|
|
61
|
+
protected stopped = false;
|
|
62
|
+
protected readonly httpBase: string;
|
|
63
|
+
protected readonly options: ClawroomClientOptions;
|
|
64
|
+
protected taskCallbacks: TaskCallback[] = [];
|
|
65
|
+
protected chatCallbacks: ChatCallback[] = [];
|
|
66
|
+
private connectedCallbacks: ConnectedCallback[] = [];
|
|
67
|
+
private disconnectedCallbacks: DisconnectedCallback[] = [];
|
|
68
|
+
private wsTransport: WsTransport | null = null;
|
|
69
|
+
private recentChatIds = new Set<string>();
|
|
70
|
+
private connectedAgentId: string | null = null;
|
|
71
|
+
private isConnectedValue = false;
|
|
72
|
+
|
|
73
|
+
constructor(options: ClawroomClientOptions) {
|
|
74
|
+
this.options = options;
|
|
75
|
+
this.httpBase = (options.endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private get wsUrl(): string {
|
|
79
|
+
if (this.options.wsUrl) return this.options.wsUrl;
|
|
80
|
+
return this.httpBase.replace(/\/api\/agents$/, "").replace(/^http/, "ws") + "/api/ws";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get connected(): boolean {
|
|
84
|
+
return this.isConnectedValue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get agentId(): string | null {
|
|
88
|
+
return this.connectedAgentId;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get isAlive(): boolean {
|
|
92
|
+
return !this.stopped;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
connect(): void {
|
|
96
|
+
this.stopped = false;
|
|
97
|
+
this.startHeartbeat();
|
|
98
|
+
void this.register();
|
|
99
|
+
|
|
100
|
+
// WebSocket transport
|
|
101
|
+
this.wsTransport?.disconnect();
|
|
102
|
+
this.wsTransport = null;
|
|
103
|
+
this.wsTransport = new WsTransport({
|
|
104
|
+
url: this.wsUrl,
|
|
105
|
+
token: this.options.token,
|
|
106
|
+
log: { info: (m) => this.options.log?.info?.(m), warn: (m) => this.options.log?.warn?.(m) },
|
|
107
|
+
onConnected: () => { this.options.log?.info?.("[clawroom] WebSocket connected"); },
|
|
108
|
+
onDisconnected: () => { this.options.log?.info?.("[clawroom] WebSocket disconnected"); },
|
|
109
|
+
onMessage: (msg) => {
|
|
110
|
+
if (msg.type === "task" && msg.task) {
|
|
111
|
+
const task = msg.task as ServerTask;
|
|
112
|
+
void this.dispatchTask(task);
|
|
113
|
+
}
|
|
114
|
+
if (msg.type === "chat" && Array.isArray(msg.messages)) {
|
|
115
|
+
const fresh = msg.messages.filter((m: ServerChatMessage) => this.rememberChat(m.workId ?? m.messageId));
|
|
116
|
+
if (fresh.length > 0) {
|
|
117
|
+
for (const cb of this.chatCallbacks) cb(fresh);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const message = getRecord(msg.message);
|
|
121
|
+
if (msg.type === "message" && message) {
|
|
122
|
+
const agentProfile = resolveAgentChatProfile(msg.agentProfile, message.agentProfile);
|
|
123
|
+
const delivered = [{
|
|
124
|
+
workId: getString(message.workId),
|
|
125
|
+
leaseToken: getString(message.leaseToken),
|
|
126
|
+
messageId: getString(message.id, getString(msg.messageId)),
|
|
127
|
+
channelId: getString(message.channelId),
|
|
128
|
+
content: getString(message.content),
|
|
129
|
+
attachments: getAttachments(msg.attachments) ?? getAttachments(message.attachments),
|
|
130
|
+
context: getContext(msg.context),
|
|
131
|
+
isMention: typeof msg.isMention === "boolean" ? msg.isMention : false,
|
|
132
|
+
wakeReason: getWakeReason(msg.wakeReason) ?? getWakeReason(message.wakeReason),
|
|
133
|
+
triggerReason: getOptionalString(msg.triggerReason) ?? getOptionalString(message.triggerReason),
|
|
134
|
+
agentProfile,
|
|
135
|
+
}] as ServerChatMessage[];
|
|
136
|
+
const fresh = delivered.filter((m) => this.rememberChat(m.workId ?? m.messageId));
|
|
137
|
+
if (fresh.length > 0) {
|
|
138
|
+
for (const cb of this.chatCallbacks) cb(fresh);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
this.wsTransport.connect();
|
|
144
|
+
|
|
145
|
+
// Keep HTTP polling active even when WS is up.
|
|
146
|
+
// WS is the fast path; polling is the durable delivery path.
|
|
147
|
+
this.stopPolling();
|
|
148
|
+
this.pollTimer = setInterval(() => {
|
|
149
|
+
void this.pollTick();
|
|
150
|
+
}, POLL_INTERVAL_MS);
|
|
151
|
+
|
|
152
|
+
void this.pollTick();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
disconnect(): void {
|
|
156
|
+
this.stopped = true;
|
|
157
|
+
this.stopHeartbeat();
|
|
158
|
+
this.stopPolling();
|
|
159
|
+
this.wsTransport?.disconnect();
|
|
160
|
+
this.wsTransport = null;
|
|
161
|
+
this.recentChatIds.clear();
|
|
162
|
+
this.markDisconnected();
|
|
163
|
+
this.connectedAgentId = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async send(message: AgentMessage): Promise<void> {
|
|
167
|
+
await this.sendViaHttp(message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
onTask(cb: TaskCallback): Unsubscribe {
|
|
171
|
+
this.taskCallbacks.push(cb);
|
|
172
|
+
return () => this.offTask(cb);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
offTask(cb: TaskCallback): void {
|
|
176
|
+
removeCallback(this.taskCallbacks, cb);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
onChatMessage(cb: ChatCallback): Unsubscribe {
|
|
180
|
+
this.chatCallbacks.push(cb);
|
|
181
|
+
return () => this.offChatMessage(cb);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
offChatMessage(cb: ChatCallback): void {
|
|
185
|
+
removeCallback(this.chatCallbacks, cb);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
onConnected(cb: ConnectedCallback): Unsubscribe {
|
|
189
|
+
this.connectedCallbacks.push(cb);
|
|
190
|
+
return () => this.offConnected(cb);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
offConnected(cb: ConnectedCallback): void {
|
|
194
|
+
removeCallback(this.connectedCallbacks, cb);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
onDisconnected(cb: DisconnectedCallback): Unsubscribe {
|
|
198
|
+
this.disconnectedCallbacks.push(cb);
|
|
199
|
+
return () => this.offDisconnected(cb);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
offDisconnected(cb: DisconnectedCallback): void {
|
|
203
|
+
removeCallback(this.disconnectedCallbacks, cb);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async sendComplete(taskId: string, output: string, attachments?: AgentResultFile[], workRef?: AgentWorkRef): Promise<void> {
|
|
207
|
+
await this.send({ type: "agent.complete", taskId, output, attachments, workRef });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async sendFail(taskId: string, reason: string, workRef?: AgentWorkRef): Promise<void> {
|
|
211
|
+
await this.send({ type: "agent.fail", taskId, reason, workRef });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async sendProgress(taskId: string, message: string, workRef?: AgentWorkRef): Promise<void> {
|
|
215
|
+
await this.send({ type: "agent.progress", taskId, message, workRef });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async sendChatReply(channelId: string, content: string, replyTo?: string, workRefs?: AgentWorkRef[]): Promise<void> {
|
|
219
|
+
await this.send({ type: "agent.chat.reply", channelId, content, replyTo, workRefs });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async sendTyping(channelId: string): Promise<void> {
|
|
223
|
+
await this.send({ type: "agent.typing", channelId });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async sendReflection(reflection: {
|
|
227
|
+
scope: "chat" | "task";
|
|
228
|
+
status: string;
|
|
229
|
+
summary: string;
|
|
230
|
+
channelId?: string | null;
|
|
231
|
+
taskId?: string | null;
|
|
232
|
+
messageId?: string | null;
|
|
233
|
+
toolsUsed?: string[];
|
|
234
|
+
responseExcerpt?: string | null;
|
|
235
|
+
detail?: Record<string, unknown>;
|
|
236
|
+
}): Promise<void> {
|
|
237
|
+
await this.httpRequest("POST", "/reflections", reflection);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Heartbeat ─────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
private startHeartbeat(): void {
|
|
243
|
+
this.stopHeartbeat();
|
|
244
|
+
this.heartbeatTimer = setInterval(() => {
|
|
245
|
+
if (!this.stopped) void this.register();
|
|
246
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private stopHeartbeat(): void {
|
|
250
|
+
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private stopPolling(): void {
|
|
254
|
+
if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
protected async register(): Promise<void> {
|
|
258
|
+
if (this.heartbeatInFlight) return;
|
|
259
|
+
this.heartbeatInFlight = true;
|
|
260
|
+
try {
|
|
261
|
+
const response = await this.httpRequest("POST", "/heartbeat", {
|
|
262
|
+
deviceId: this.options.deviceId,
|
|
263
|
+
skills: this.options.skills,
|
|
264
|
+
kind: this.options.kind ?? "openclaw",
|
|
265
|
+
}) as AgentHeartbeatResponse;
|
|
266
|
+
this.onPollSuccess(response.agentId);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
this.options.log?.warn?.(`[clawroom] heartbeat error: ${err}`);
|
|
269
|
+
if (!this.wsTransport?.connected) {
|
|
270
|
+
this.onPollError(err);
|
|
271
|
+
}
|
|
272
|
+
} finally {
|
|
273
|
+
this.heartbeatInFlight = false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
protected async pollTick(): Promise<void> {
|
|
278
|
+
if (this.stopped || this.pollInFlight) return;
|
|
279
|
+
this.pollInFlight = true;
|
|
280
|
+
try {
|
|
281
|
+
const res = await this.httpRequest("POST", "/poll", {}) as AgentPollResponse;
|
|
282
|
+
this.onPollSuccess(res?.agentId);
|
|
283
|
+
if (res.task) {
|
|
284
|
+
await this.dispatchTask(res.task);
|
|
285
|
+
}
|
|
286
|
+
if (res.chat && Array.isArray(res.chat) && res.chat.length > 0) {
|
|
287
|
+
const fresh = res.chat.filter((m: ServerChatMessage) => this.rememberChat(m.workId ?? m.messageId));
|
|
288
|
+
if (fresh.length > 0) {
|
|
289
|
+
this.options.log?.info?.(`[clawroom] received ${fresh.length} chat mention(s)`);
|
|
290
|
+
for (const cb of this.chatCallbacks) cb(fresh);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
this.options.log?.warn?.(`[clawroom] poll error: ${err}`);
|
|
295
|
+
this.onPollError(err);
|
|
296
|
+
} finally {
|
|
297
|
+
this.pollInFlight = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
protected onPollSuccess(agentId: string | undefined): void {
|
|
302
|
+
this.markConnected(agentId);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
protected onPollError(err: unknown): void {
|
|
306
|
+
void err;
|
|
307
|
+
if (this.wsTransport?.connected) return;
|
|
308
|
+
this.markDisconnected();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── HTTP ──────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
private async sendViaHttp(message: AgentMessage): Promise<void> {
|
|
314
|
+
switch (message.type) {
|
|
315
|
+
case "agent.complete": await this.httpRequest("POST", "/complete", { taskId: message.taskId, output: message.output, attachments: message.attachments, workRef: message.workRef }); break;
|
|
316
|
+
case "agent.fail": await this.httpRequest("POST", "/fail", { taskId: message.taskId, reason: message.reason, workRef: message.workRef }); break;
|
|
317
|
+
case "agent.progress": await this.httpRequest("POST", "/progress", { taskId: message.taskId, message: message.message, workRef: message.workRef }); break;
|
|
318
|
+
case "agent.heartbeat": await this.httpRequest("POST", "/heartbeat", {}); break;
|
|
319
|
+
case "agent.chat.reply": await this.httpRequest("POST", "/chat/reply", {
|
|
320
|
+
channelId: message.channelId,
|
|
321
|
+
content: message.content,
|
|
322
|
+
replyTo: message.replyTo,
|
|
323
|
+
workRefs: message.workRefs,
|
|
324
|
+
}); break;
|
|
325
|
+
case "agent.typing": await this.httpRequest("POST", "/typing", { channelId: message.channelId }); break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
protected async httpRequest(method: string, path: string, body: unknown): Promise<unknown> {
|
|
330
|
+
const res = await fetch(`${this.httpBase}${path}`, {
|
|
331
|
+
method,
|
|
332
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.options.token}` },
|
|
333
|
+
body: JSON.stringify(body),
|
|
334
|
+
});
|
|
335
|
+
const text = await res.text().catch(() => "");
|
|
336
|
+
if (!res.ok) {
|
|
337
|
+
this.onHttpError(res.status, text);
|
|
338
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
339
|
+
}
|
|
340
|
+
if (!text) return null;
|
|
341
|
+
try {
|
|
342
|
+
return JSON.parse(text) as unknown;
|
|
343
|
+
} catch {
|
|
344
|
+
throw new Error(`Invalid JSON response from ${path}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
protected onHttpError(status: number, text: string): void {
|
|
349
|
+
void status;
|
|
350
|
+
void text;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private rememberChat(messageId?: string): boolean {
|
|
354
|
+
if (!messageId) return false;
|
|
355
|
+
if (this.recentChatIds.has(messageId)) return false;
|
|
356
|
+
this.recentChatIds.add(messageId);
|
|
357
|
+
if (this.recentChatIds.size > 1000) {
|
|
358
|
+
const oldest = this.recentChatIds.values().next().value;
|
|
359
|
+
if (oldest) this.recentChatIds.delete(oldest);
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private async dispatchTask(task: ServerTask): Promise<void> {
|
|
365
|
+
this.options.log?.info?.(`[clawroom] received task ${task.taskId}: ${task.title}`);
|
|
366
|
+
for (const cb of this.taskCallbacks) cb(task);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private markConnected(agentId?: string): void {
|
|
370
|
+
if (agentId) this.connectedAgentId = agentId;
|
|
371
|
+
if (!this.connectedAgentId || this.isConnectedValue) return;
|
|
372
|
+
this.isConnectedValue = true;
|
|
373
|
+
for (const callback of this.connectedCallbacks) callback(this.connectedAgentId);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private markDisconnected(): void {
|
|
377
|
+
if (!this.isConnectedValue) return;
|
|
378
|
+
this.isConnectedValue = false;
|
|
379
|
+
for (const callback of this.disconnectedCallbacks) callback();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function removeCallback<T>(callbacks: T[], callback: T): void {
|
|
384
|
+
const index = callbacks.indexOf(callback);
|
|
385
|
+
if (index !== -1) callbacks.splice(index, 1);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function getRecord(value: unknown): Record<string, unknown> | null {
|
|
389
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
390
|
+
return value as Record<string, unknown>;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function getString(value: unknown, fallback = ""): string {
|
|
394
|
+
return typeof value === "string" ? value : fallback;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function getOptionalString(value: unknown): string | undefined {
|
|
398
|
+
return typeof value === "string" ? value : undefined;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function getAttachments(value: unknown): ServerChatMessage["attachments"] | undefined {
|
|
402
|
+
return Array.isArray(value) ? value as ServerChatMessage["attachments"] : undefined;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function getContext(value: unknown): ServerChatMessage["context"] {
|
|
406
|
+
return Array.isArray(value) ? value as ServerChatMessage["context"] : [];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function getWakeReason(value: unknown): ServerChatMessage["wakeReason"] | undefined {
|
|
410
|
+
return typeof value === "string" ? value as ServerChatMessage["wakeReason"] : undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function getAgentChatProfile(value: unknown): AgentChatProfile | null {
|
|
414
|
+
const record = getRecord(value);
|
|
415
|
+
if (!record) return null;
|
|
416
|
+
return {
|
|
417
|
+
role: getString(record.role, DEFAULT_AGENT_CHAT_PROFILE.role),
|
|
418
|
+
systemPrompt: getString(record.systemPrompt, DEFAULT_AGENT_CHAT_PROFILE.systemPrompt),
|
|
419
|
+
memory: getString(record.memory, DEFAULT_AGENT_CHAT_PROFILE.memory),
|
|
420
|
+
continuityPacket: getString(record.continuityPacket, DEFAULT_AGENT_CHAT_PROFILE.continuityPacket),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function resolveAgentChatProfile(...values: unknown[]): AgentChatProfile {
|
|
425
|
+
for (const value of values) {
|
|
426
|
+
const profile = getAgentChatProfile(value);
|
|
427
|
+
if (profile) return profile;
|
|
428
|
+
}
|
|
429
|
+
return DEFAULT_AGENT_CHAT_PROFILE;
|
|
430
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
AgentChatProfile,
|
|
10
|
+
ChatAttachmentRef,
|
|
11
|
+
AgentMessage,
|
|
12
|
+
AgentHeartbeat,
|
|
13
|
+
AgentComplete,
|
|
14
|
+
AgentProgress,
|
|
15
|
+
AgentResultFile,
|
|
16
|
+
AgentFail,
|
|
17
|
+
AgentChatReply,
|
|
18
|
+
AgentTyping,
|
|
19
|
+
ServerMessage,
|
|
20
|
+
ServerTask,
|
|
21
|
+
ServerChatMessage,
|
|
22
|
+
} from "./protocol.js";
|