@clawroom/sdk 0.5.1 → 0.5.2
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/package.json +1 -1
- package/src/client.ts +63 -6
- package/src/index.ts +1 -0
- package/src/machine-client.ts +54 -8
- package/src/protocol.ts +1 -0
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AgentChatProfile,
|
|
2
3
|
AgentMessage,
|
|
3
4
|
ServerTask,
|
|
4
5
|
ServerChatMessage,
|
|
@@ -8,6 +9,12 @@ import { WsTransport } from "./ws-transport.js";
|
|
|
8
9
|
const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
|
|
9
10
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
10
11
|
const POLL_INTERVAL_MS = 10_000;
|
|
12
|
+
const DEFAULT_AGENT_CHAT_PROFILE: AgentChatProfile = {
|
|
13
|
+
role: "No role defined",
|
|
14
|
+
systemPrompt: "No system prompt configured",
|
|
15
|
+
memory: "No memory recorded yet",
|
|
16
|
+
focus: "No focus items",
|
|
17
|
+
};
|
|
11
18
|
|
|
12
19
|
export type TaskCallback = (task: ServerTask) => void;
|
|
13
20
|
export type ChatCallback = (messages: ServerChatMessage[]) => void;
|
|
@@ -39,6 +46,7 @@ export class ClawroomClient {
|
|
|
39
46
|
protected taskCallbacks: TaskCallback[] = [];
|
|
40
47
|
protected chatCallbacks: ChatCallback[] = [];
|
|
41
48
|
private wsTransport: WsTransport | null = null;
|
|
49
|
+
private recentChatIds = new Set<string>();
|
|
42
50
|
|
|
43
51
|
constructor(options: ClawroomClientOptions) {
|
|
44
52
|
this.options = options;
|
|
@@ -67,18 +75,40 @@ export class ClawroomClient {
|
|
|
67
75
|
for (const cb of this.taskCallbacks) cb(msg.task);
|
|
68
76
|
}
|
|
69
77
|
if (msg.type === "chat" && Array.isArray(msg.messages)) {
|
|
70
|
-
|
|
78
|
+
const fresh = msg.messages.filter((m: ServerChatMessage) => this.rememberChat(m.messageId));
|
|
79
|
+
if (fresh.length > 0) {
|
|
80
|
+
for (const cb of this.chatCallbacks) cb(fresh);
|
|
81
|
+
this.ackChatBestEffort(fresh.map((m: ServerChatMessage) => m.messageId));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (msg.type === "message" && msg.message) {
|
|
85
|
+
const agentProfile = msg.agentProfile ?? msg.message.agentProfile ?? DEFAULT_AGENT_CHAT_PROFILE;
|
|
86
|
+
const delivered = [{
|
|
87
|
+
messageId: msg.message.id ?? msg.messageId,
|
|
88
|
+
channelId: msg.message.channelId ?? "",
|
|
89
|
+
content: msg.message.content ?? "",
|
|
90
|
+
context: Array.isArray(msg.context) ? msg.context : [],
|
|
91
|
+
isMention: msg.isMention ?? false,
|
|
92
|
+
agentProfile,
|
|
93
|
+
}] as ServerChatMessage[];
|
|
94
|
+
const fresh = delivered.filter((m) => this.rememberChat(m.messageId));
|
|
95
|
+
if (fresh.length > 0) {
|
|
96
|
+
for (const cb of this.chatCallbacks) cb(fresh);
|
|
97
|
+
this.ackChatBestEffort(fresh.map((m) => m.messageId));
|
|
98
|
+
}
|
|
71
99
|
}
|
|
72
100
|
},
|
|
73
101
|
});
|
|
74
102
|
this.wsTransport.connect();
|
|
75
103
|
|
|
76
|
-
// HTTP
|
|
104
|
+
// Keep HTTP polling active even when WS is up.
|
|
105
|
+
// WS is the fast path; polling is the durable delivery path.
|
|
77
106
|
this.stopPolling();
|
|
78
107
|
this.pollTimer = setInterval(() => {
|
|
79
|
-
if (this.wsTransport?.connected) return;
|
|
80
108
|
void this.pollTick();
|
|
81
109
|
}, POLL_INTERVAL_MS);
|
|
110
|
+
|
|
111
|
+
void this.pollTick();
|
|
82
112
|
}
|
|
83
113
|
|
|
84
114
|
disconnect(): void {
|
|
@@ -137,8 +167,12 @@ export class ClawroomClient {
|
|
|
137
167
|
for (const cb of this.taskCallbacks) cb(res.task);
|
|
138
168
|
}
|
|
139
169
|
if (res.chat && Array.isArray(res.chat) && res.chat.length > 0) {
|
|
140
|
-
|
|
141
|
-
|
|
170
|
+
const fresh = res.chat.filter((m: ServerChatMessage) => this.rememberChat(m.messageId));
|
|
171
|
+
if (fresh.length > 0) {
|
|
172
|
+
this.options.log?.info?.(`[clawroom] received ${fresh.length} chat mention(s)`);
|
|
173
|
+
for (const cb of this.chatCallbacks) cb(fresh);
|
|
174
|
+
this.ackChatBestEffort(fresh.map((m: ServerChatMessage) => m.messageId));
|
|
175
|
+
}
|
|
142
176
|
}
|
|
143
177
|
} catch (err) {
|
|
144
178
|
this.options.log?.warn?.(`[clawroom] poll error: ${err}`);
|
|
@@ -162,7 +196,7 @@ export class ClawroomClient {
|
|
|
162
196
|
}
|
|
163
197
|
}
|
|
164
198
|
|
|
165
|
-
protected async httpRequest(method: string, path: string, body: unknown): Promise<
|
|
199
|
+
protected async httpRequest(method: string, path: string, body: unknown): Promise<unknown> {
|
|
166
200
|
const res = await fetch(`${this.httpBase}${path}`, {
|
|
167
201
|
method,
|
|
168
202
|
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.options.token}` },
|
|
@@ -177,4 +211,27 @@ export class ClawroomClient {
|
|
|
177
211
|
}
|
|
178
212
|
|
|
179
213
|
protected onHttpError(_status: number, _text: string): void {}
|
|
214
|
+
|
|
215
|
+
private rememberChat(messageId?: string): boolean {
|
|
216
|
+
if (!messageId) return true;
|
|
217
|
+
if (this.recentChatIds.has(messageId)) return false;
|
|
218
|
+
this.recentChatIds.add(messageId);
|
|
219
|
+
if (this.recentChatIds.size > 1000) {
|
|
220
|
+
const oldest = this.recentChatIds.values().next().value;
|
|
221
|
+
if (oldest) this.recentChatIds.delete(oldest);
|
|
222
|
+
}
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private ackChatBestEffort(messageIds: string[]): void {
|
|
227
|
+
void this.ackChat(messageIds).catch((err) => {
|
|
228
|
+
this.options.log?.warn?.(`[clawroom] chat ack error: ${err}`);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async ackChat(messageIds: string[]): Promise<void> {
|
|
233
|
+
const ids = Array.from(new Set(messageIds.filter(Boolean)));
|
|
234
|
+
if (ids.length === 0) return;
|
|
235
|
+
await this.httpRequest("POST", "/chat/ack", { messageIds: ids });
|
|
236
|
+
}
|
|
180
237
|
}
|
package/src/index.ts
CHANGED
package/src/machine-client.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
|
+
import type { AgentChatProfile, ServerChatMessage } from "./protocol.js";
|
|
7
8
|
import { WsTransport } from "./ws-transport.js";
|
|
8
9
|
|
|
9
10
|
export type ClawroomMachineClientOptions = {
|
|
@@ -23,7 +24,14 @@ type AgentWork = {
|
|
|
23
24
|
agentId: string;
|
|
24
25
|
agentName: string;
|
|
25
26
|
task: { type: "server.task"; taskId: string; title: string; description: string; input: string; skillTags: string[] } | null;
|
|
26
|
-
chat:
|
|
27
|
+
chat: ServerChatMessage[] | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DEFAULT_AGENT_CHAT_PROFILE: AgentChatProfile = {
|
|
31
|
+
role: "No role defined",
|
|
32
|
+
systemPrompt: "No system prompt configured",
|
|
33
|
+
memory: "No memory recorded yet",
|
|
34
|
+
focus: "No focus items",
|
|
27
35
|
};
|
|
28
36
|
|
|
29
37
|
type MachineHeartbeatResponse = {
|
|
@@ -42,6 +50,8 @@ export class ClawroomMachineClient {
|
|
|
42
50
|
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
43
51
|
private taskHandler: ((agentId: string, task: AgentWork["task"]) => void) | null = null;
|
|
44
52
|
private chatHandler: ((agentId: string, messages: NonNullable<AgentWork["chat"]>) => void) | null = null;
|
|
53
|
+
private connectHandler: ((machineId: string) => void) | null = null;
|
|
54
|
+
private disconnectHandler: (() => void) | null = null;
|
|
45
55
|
private _connected = false;
|
|
46
56
|
private _stopped = false;
|
|
47
57
|
private wsTransport: WsTransport | null = null;
|
|
@@ -74,6 +84,8 @@ export class ClawroomMachineClient {
|
|
|
74
84
|
|
|
75
85
|
onAgentTask(handler: (agentId: string, task: AgentWork["task"]) => void) { this.taskHandler = handler; }
|
|
76
86
|
onAgentChat(handler: (agentId: string, messages: NonNullable<AgentWork["chat"]>) => void) { this.chatHandler = handler; }
|
|
87
|
+
onConnected(handler: (machineId: string) => void) { this.connectHandler = handler; }
|
|
88
|
+
onDisconnected(handler: () => void) { this.disconnectHandler = handler; }
|
|
77
89
|
|
|
78
90
|
async sendAgentComplete(agentId: string, taskId: string, output: string, attachments?: Array<{ filename: string; mimeType: string; data: string }>) {
|
|
79
91
|
await this.httpRequest("POST", "/complete", { agentId, taskId, output, attachments });
|
|
@@ -91,6 +103,12 @@ export class ClawroomMachineClient {
|
|
|
91
103
|
await this.httpRequest("POST", "/typing", { agentId, channelId });
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
async ackAgentChat(agentId: string, messageIds: string[]) {
|
|
107
|
+
const ids = Array.from(new Set(messageIds.filter(Boolean)));
|
|
108
|
+
if (ids.length === 0) return;
|
|
109
|
+
await this.httpRequest("POST", "/chat-ack", { agentId, messageIds: ids });
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
get connected() { return this._connected; }
|
|
95
113
|
get stopped() { return this._stopped; }
|
|
96
114
|
|
|
@@ -105,6 +123,12 @@ export class ClawroomMachineClient {
|
|
|
105
123
|
return true;
|
|
106
124
|
}
|
|
107
125
|
|
|
126
|
+
private ackAgentChatBestEffort(agentId: string, messageIds: string[]) {
|
|
127
|
+
void this.ackAgentChat(agentId, messageIds).catch((err) => {
|
|
128
|
+
this.options.log?.warn?.(`[machine] chat ack error: ${err}`);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
private syncAgentSubscriptions(agentIds: string[]) {
|
|
109
133
|
if (agentIds.length === 0) return;
|
|
110
134
|
for (const agentId of agentIds) {
|
|
@@ -113,6 +137,22 @@ export class ClawroomMachineClient {
|
|
|
113
137
|
}
|
|
114
138
|
}
|
|
115
139
|
|
|
140
|
+
private markConnected(machineId: string) {
|
|
141
|
+
if (!this._connected) {
|
|
142
|
+
this._connected = true;
|
|
143
|
+
this.options.log?.info?.("[machine] connected");
|
|
144
|
+
this.connectHandler?.(machineId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private markDisconnected() {
|
|
149
|
+
if (this._connected) {
|
|
150
|
+
this._connected = false;
|
|
151
|
+
this.options.log?.warn?.("[machine] disconnected");
|
|
152
|
+
this.disconnectHandler?.();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
116
156
|
private async pollOnce() {
|
|
117
157
|
if (!this._connected) return;
|
|
118
158
|
try {
|
|
@@ -122,7 +162,10 @@ export class ClawroomMachineClient {
|
|
|
122
162
|
if (agent.task && this.taskHandler) this.taskHandler(agent.agentId, agent.task);
|
|
123
163
|
if (agent.chat && agent.chat.length > 0 && this.chatHandler) {
|
|
124
164
|
const freshMessages = agent.chat.filter((message) => this.rememberChat(message.messageId));
|
|
125
|
-
if (freshMessages.length > 0)
|
|
165
|
+
if (freshMessages.length > 0) {
|
|
166
|
+
this.chatHandler(agent.agentId, freshMessages);
|
|
167
|
+
this.ackAgentChatBestEffort(agent.agentId, freshMessages.map((message) => message.messageId));
|
|
168
|
+
}
|
|
126
169
|
}
|
|
127
170
|
}
|
|
128
171
|
} catch (err) {
|
|
@@ -140,18 +183,17 @@ export class ClawroomMachineClient {
|
|
|
140
183
|
try {
|
|
141
184
|
const result = (await this.httpRequest("POST", "/heartbeat", hbBody)) as MachineHeartbeatResponse;
|
|
142
185
|
this.syncAgentSubscriptions((result.agents ?? []).map((agent) => agent.id));
|
|
143
|
-
|
|
186
|
+
this.markConnected(result.machineId);
|
|
144
187
|
} catch {
|
|
145
|
-
|
|
188
|
+
this.markDisconnected();
|
|
146
189
|
}
|
|
147
190
|
}, 30_000);
|
|
148
191
|
|
|
149
192
|
this.httpRequest("POST", "/heartbeat", hbBody)
|
|
150
193
|
.then((result) => {
|
|
151
194
|
const data = result as MachineHeartbeatResponse;
|
|
152
|
-
this._connected = true;
|
|
153
195
|
this.syncAgentSubscriptions((data.agents ?? []).map((agent) => agent.id));
|
|
154
|
-
this.
|
|
196
|
+
this.markConnected(data.machineId);
|
|
155
197
|
})
|
|
156
198
|
.catch((err) => this.options.log?.warn?.(`[machine] initial heartbeat failed: ${err}`));
|
|
157
199
|
|
|
@@ -173,13 +215,17 @@ export class ClawroomMachineClient {
|
|
|
173
215
|
const targetAgentId = msg.agentId ?? m?.agentId;
|
|
174
216
|
const messageId = m?.id ?? msg.messageId ?? "";
|
|
175
217
|
if (m && targetAgentId && this.chatHandler && this.rememberChat(messageId)) {
|
|
176
|
-
|
|
218
|
+
const agentProfile = msg.agentProfile ?? m.agentProfile ?? DEFAULT_AGENT_CHAT_PROFILE;
|
|
219
|
+
const messages = [{
|
|
177
220
|
messageId,
|
|
178
221
|
channelId: m.channelId ?? "",
|
|
179
222
|
content: m.content ?? "",
|
|
180
223
|
isMention: msg.isMention ?? false,
|
|
181
224
|
context: msg.context ?? [],
|
|
182
|
-
|
|
225
|
+
agentProfile,
|
|
226
|
+
}];
|
|
227
|
+
this.chatHandler(targetAgentId, messages);
|
|
228
|
+
this.ackAgentChatBestEffort(targetAgentId, messages.map((message) => message.messageId));
|
|
183
229
|
}
|
|
184
230
|
}
|
|
185
231
|
if (msg.type === "task" && msg.agentId && this.taskHandler) {
|