@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
package/src/channel.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
3
|
+
import { ClawroomClient } from "@clawroom/sdk";
|
|
3
4
|
// import { collectSkills } from "./skill-reporter.js";
|
|
4
5
|
import { getClawroomRuntime } from "./runtime.js";
|
|
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/agents";
|
|
12
12
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
13
13
|
|
|
14
14
|
interface ClawroomAccountConfig {
|
|
15
|
-
|
|
16
|
-
token?: string; // deprecated, fallback
|
|
15
|
+
token?: string;
|
|
17
16
|
endpoint?: string;
|
|
18
17
|
skills?: string[];
|
|
19
18
|
enabled?: boolean;
|
|
@@ -24,7 +23,7 @@ interface ResolvedClawroomAccount {
|
|
|
24
23
|
name: string;
|
|
25
24
|
enabled: boolean;
|
|
26
25
|
configured: boolean;
|
|
27
|
-
|
|
26
|
+
token: string;
|
|
28
27
|
endpoint: string;
|
|
29
28
|
skills: string[];
|
|
30
29
|
}
|
|
@@ -40,7 +39,7 @@ function resolveClawroomAccount(opts: {
|
|
|
40
39
|
accountId?: string | null;
|
|
41
40
|
}): ResolvedClawroomAccount {
|
|
42
41
|
const section = readClawroomSection(opts.cfg);
|
|
43
|
-
const
|
|
42
|
+
const token = section.token ?? "";
|
|
44
43
|
const endpoint = section.endpoint || DEFAULT_ENDPOINT;
|
|
45
44
|
const skills = Array.isArray(section.skills) ? section.skills : [];
|
|
46
45
|
const enabled = section.enabled !== false;
|
|
@@ -49,8 +48,8 @@ function resolveClawroomAccount(opts: {
|
|
|
49
48
|
accountId: opts.accountId ?? DEFAULT_ACCOUNT_ID,
|
|
50
49
|
name: "ClawRoom",
|
|
51
50
|
enabled,
|
|
52
|
-
configured:
|
|
53
|
-
|
|
51
|
+
configured: token.length > 0,
|
|
52
|
+
token,
|
|
54
53
|
endpoint,
|
|
55
54
|
skills,
|
|
56
55
|
};
|
|
@@ -58,7 +57,7 @@ function resolveClawroomAccount(opts: {
|
|
|
58
57
|
|
|
59
58
|
// ── Persistent client per gateway lifecycle ───────────────────────
|
|
60
59
|
|
|
61
|
-
let activeClient:
|
|
60
|
+
let activeClient: ClawroomClient | null = null;
|
|
62
61
|
|
|
63
62
|
// ── Channel plugin definition ────────────────────────────────────────
|
|
64
63
|
|
|
@@ -105,9 +104,10 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
105
104
|
deliveryMode: "direct",
|
|
106
105
|
|
|
107
106
|
sendText: async ({ to, text }) => {
|
|
107
|
+
void text;
|
|
108
108
|
// Outbound path — task results are sent directly by executors,
|
|
109
|
-
// this is a fallback if
|
|
110
|
-
//
|
|
109
|
+
// this is a fallback if OpenClaw routing triggers outbound delivery.
|
|
110
|
+
// Direct agent delivery is handled by the executors.
|
|
111
111
|
return { channel: "clawroom", messageId: to, to };
|
|
112
112
|
},
|
|
113
113
|
},
|
|
@@ -118,16 +118,19 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
118
118
|
|
|
119
119
|
if (!account.configured) {
|
|
120
120
|
throw new Error(
|
|
121
|
-
"ClawRoom is not configured: set channels.clawroom.
|
|
121
|
+
"ClawRoom is not configured: set channels.clawroom.token in your OpenClaw config.",
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const runtime = getClawroomRuntime();
|
|
126
126
|
const log = ctx.log ?? undefined;
|
|
127
127
|
|
|
128
|
-
const client = new
|
|
128
|
+
const client = new ClawroomClient({
|
|
129
129
|
endpoint: account.endpoint,
|
|
130
|
-
|
|
130
|
+
token: account.token,
|
|
131
|
+
deviceId: `openclaw:${account.accountId}`,
|
|
132
|
+
skills: account.skills,
|
|
133
|
+
kind: "openclaw",
|
|
131
134
|
log,
|
|
132
135
|
});
|
|
133
136
|
|
|
@@ -155,40 +158,22 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
155
158
|
});
|
|
156
159
|
};
|
|
157
160
|
|
|
158
|
-
|
|
159
|
-
client.
|
|
160
|
-
client.onDisconnect(() => publishDisconnected());
|
|
161
|
-
client.onFatal((reason) => {
|
|
162
|
-
log?.error?.(`[clawroom] fatal error: ${reason}`);
|
|
163
|
-
ctx.setStatus({
|
|
164
|
-
accountId: account.accountId,
|
|
165
|
-
running: false,
|
|
166
|
-
connected: false,
|
|
167
|
-
lastStopAt: Date.now(),
|
|
168
|
-
lastStartAt: null,
|
|
169
|
-
lastError: `Fatal: ${reason}`,
|
|
170
|
-
});
|
|
171
|
-
});
|
|
161
|
+
client.onConnected(() => publishConnected());
|
|
162
|
+
client.onDisconnected(() => publishDisconnected());
|
|
172
163
|
|
|
164
|
+
if (activeClient && activeClient !== client) {
|
|
165
|
+
activeClient.disconnect();
|
|
166
|
+
}
|
|
173
167
|
setupTaskExecutor({ client: client, runtime, log });
|
|
174
168
|
setupChatExecutor({ client: client, runtime, log });
|
|
175
169
|
client.connect();
|
|
176
170
|
activeClient = client;
|
|
177
171
|
|
|
178
|
-
publishDisconnected("connecting via
|
|
179
|
-
|
|
180
|
-
// Health check: if client somehow stopped, restart it.
|
|
181
|
-
const healthCheck = setInterval(() => {
|
|
182
|
-
if (!client.isAlive) {
|
|
183
|
-
log?.warn?.("[clawroom] client died unexpectedly, restarting...");
|
|
184
|
-
client.connect();
|
|
185
|
-
}
|
|
186
|
-
}, 30_000);
|
|
172
|
+
publishDisconnected("connecting via agent polling...");
|
|
187
173
|
|
|
188
174
|
// Keep alive until gateway shuts down — only then disconnect
|
|
189
175
|
await new Promise<void>((resolve) => {
|
|
190
176
|
ctx.abortSignal.addEventListener("abort", () => {
|
|
191
|
-
clearInterval(healthCheck);
|
|
192
177
|
client.disconnect();
|
|
193
178
|
activeClient = null;
|
|
194
179
|
ctx.setStatus({
|
|
@@ -235,24 +220,3 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
|
|
|
235
220
|
}),
|
|
236
221
|
},
|
|
237
222
|
};
|
|
238
|
-
|
|
239
|
-
// ── Helpers ──────────────────────────────────────────────────────────
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Build a stable device identifier. We use the machine hostname from the
|
|
243
|
-
* runtime environment when available, falling back to a random id.
|
|
244
|
-
*/
|
|
245
|
-
function resolveDeviceId(ctx: {
|
|
246
|
-
runtime?: unknown;
|
|
247
|
-
}): string {
|
|
248
|
-
// The RuntimeEnv may expose hostname or machineId depending on version
|
|
249
|
-
const r = ctx.runtime as Record<string, unknown>;
|
|
250
|
-
if (typeof r.machineId === "string" && r.machineId) {
|
|
251
|
-
return r.machineId;
|
|
252
|
-
}
|
|
253
|
-
if (typeof r.hostname === "string" && r.hostname) {
|
|
254
|
-
return r.hostname;
|
|
255
|
-
}
|
|
256
|
-
// Fallback: random id for this session
|
|
257
|
-
return `agent-${Math.random().toString(36).slice(2, 10)}`;
|
|
258
|
-
}
|
package/src/chat-executor.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AgentChatProfile, ClawroomClient, ServerChatMessage } from "@clawroom/sdk";
|
|
3
|
+
import { extractToolNames, reportPluginReflectionSoon } from "./reflections.js";
|
|
3
4
|
|
|
4
5
|
/** Default timeout for chat reply subagent (2 minutes). */
|
|
5
6
|
const CHAT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
@@ -11,7 +12,7 @@ const CHAT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
|
11
12
|
* 3. Send reply back to channel
|
|
12
13
|
*/
|
|
13
14
|
export function setupChatExecutor(opts: {
|
|
14
|
-
client:
|
|
15
|
+
client: ClawroomClient;
|
|
15
16
|
runtime: PluginRuntime;
|
|
16
17
|
log?: {
|
|
17
18
|
info?: (message: string, ...args: unknown[]) => void;
|
|
@@ -20,8 +21,27 @@ export function setupChatExecutor(opts: {
|
|
|
20
21
|
};
|
|
21
22
|
}): void {
|
|
22
23
|
const { client, runtime, log } = opts;
|
|
24
|
+
const pendingMessages: ServerChatMessage[] = [];
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
const flushPending = (agentId: string) => {
|
|
27
|
+
if (pendingMessages.length === 0) return;
|
|
28
|
+
const queued = pendingMessages.splice(0, pendingMessages.length);
|
|
29
|
+
for (const msg of queued) {
|
|
30
|
+
void handleChatMention({ client, runtime, msg, agentId, log });
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
client.onConnected((agentId) => {
|
|
35
|
+
flushPending(agentId);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
client.onChatMessage((messages: ServerChatMessage[]) => {
|
|
39
|
+
const agentId = client.agentId;
|
|
40
|
+
if (!agentId) {
|
|
41
|
+
log?.warn?.("[clawroom:chat] received chat before agent registration completed");
|
|
42
|
+
pendingMessages.push(...messages);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
25
45
|
for (const msg of messages) {
|
|
26
46
|
void handleChatMention({ client, runtime, msg, agentId, log });
|
|
27
47
|
}
|
|
@@ -29,9 +49,9 @@ export function setupChatExecutor(opts: {
|
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
async function handleChatMention(opts: {
|
|
32
|
-
client:
|
|
52
|
+
client: ClawroomClient;
|
|
33
53
|
runtime: PluginRuntime;
|
|
34
|
-
msg:
|
|
54
|
+
msg: ServerChatMessage;
|
|
35
55
|
agentId: string;
|
|
36
56
|
log?: {
|
|
37
57
|
info?: (message: string, ...args: unknown[]) => void;
|
|
@@ -45,31 +65,72 @@ async function handleChatMention(opts: {
|
|
|
45
65
|
log?.info?.(`[clawroom:chat] processing mention in channel ${msg.channelId}: "${msg.content.slice(0, 80)}"`);
|
|
46
66
|
|
|
47
67
|
// Send typing indicator
|
|
48
|
-
client.sendTyping(
|
|
68
|
+
client.sendTyping(msg.channelId).catch(() => {});
|
|
49
69
|
|
|
50
70
|
// Build context from recent messages
|
|
51
|
-
const contextLines = msg.context
|
|
52
|
-
.map((c) => `[${c.senderName}]: ${c.content}`)
|
|
71
|
+
const contextLines = (msg.context ?? [])
|
|
72
|
+
.map((c: { senderName: string; content: string }) => `[${c.senderName}]: ${c.content}`)
|
|
53
73
|
.join("\n");
|
|
74
|
+
const attachmentLines = msg.attachments && msg.attachments.length > 0
|
|
75
|
+
? msg.attachments.map((attachment: { filename: string; mimeType: string; byteSize?: number; downloadUrl: string }) => {
|
|
76
|
+
const size = typeof attachment.byteSize === "number" ? ` (${attachment.byteSize} bytes)` : "";
|
|
77
|
+
return `- ${attachment.filename} [${attachment.mimeType}]${size} ${attachment.downloadUrl}`;
|
|
78
|
+
}).join("\n")
|
|
79
|
+
: "(none)";
|
|
80
|
+
const agentProfileBlock = formatAgentProfile(msg.agentProfile);
|
|
54
81
|
|
|
55
|
-
const isMention =
|
|
82
|
+
const isMention = msg.isMention ?? false;
|
|
83
|
+
const isFollowThroughWake = msg.wakeReason === "follow_through";
|
|
84
|
+
const isGoalDriftWake = msg.wakeReason === "goal_drift";
|
|
85
|
+
const isTriggerWake = msg.wakeReason === "trigger";
|
|
86
|
+
const isCoordinationWake = msg.wakeReason === "coordination";
|
|
56
87
|
const agentMessage = [
|
|
57
88
|
isMention
|
|
58
89
|
? "You were @mentioned in a chat channel. Reply directly and helpfully."
|
|
90
|
+
: isCoordinationWake
|
|
91
|
+
? `You were woken for persisted coordination on an active tracked mission.${msg.triggerReason ? ` ${msg.triggerReason}` : ""}`
|
|
92
|
+
: isFollowThroughWake
|
|
93
|
+
? `Your follow-through engine woke you up because an open loop needs attention.${msg.triggerReason ? ` ${msg.triggerReason}` : ""}`
|
|
94
|
+
: isGoalDriftWake
|
|
95
|
+
? `Potential goal drift was detected in this channel.${msg.triggerReason ? ` ${msg.triggerReason}` : ""}`
|
|
96
|
+
: isTriggerWake
|
|
97
|
+
? `A scheduled or proactive trigger woke you up.${msg.triggerReason ? ` ${msg.triggerReason}` : ""}`
|
|
59
98
|
: "A new message appeared in a channel you're in. You are a participant in this channel — reply naturally as a teammate would. If the message is a greeting or directed at the group, respond. Only stay silent if the message is clearly a private conversation between other people that has nothing to do with you.",
|
|
60
99
|
"",
|
|
100
|
+
"## Your saved profile",
|
|
101
|
+
agentProfileBlock,
|
|
102
|
+
"",
|
|
61
103
|
"## Recent messages",
|
|
62
104
|
contextLines,
|
|
63
105
|
"",
|
|
64
|
-
|
|
106
|
+
"## Attachments",
|
|
107
|
+
attachmentLines,
|
|
108
|
+
"",
|
|
109
|
+
isMention ? "## Message that mentioned you" : isCoordinationWake ? "## Message that woke you" : "## Latest message",
|
|
65
110
|
msg.content,
|
|
66
111
|
].join("\n");
|
|
67
112
|
|
|
113
|
+
let typingInterval: ReturnType<typeof setInterval> | null = null;
|
|
114
|
+
let shouldDeleteSession = false;
|
|
115
|
+
|
|
68
116
|
try {
|
|
117
|
+
reportPluginReflectionSoon(client, {
|
|
118
|
+
scope: "chat",
|
|
119
|
+
status: "started",
|
|
120
|
+
summary: `Started reply for message ${msg.messageId}.`,
|
|
121
|
+
channelId: msg.channelId,
|
|
122
|
+
messageId: msg.messageId,
|
|
123
|
+
detail: {
|
|
124
|
+
wakeReason: msg.wakeReason ?? null,
|
|
125
|
+
isMention,
|
|
126
|
+
},
|
|
127
|
+
}, log);
|
|
128
|
+
|
|
69
129
|
// Keep typing indicator alive during execution
|
|
70
|
-
|
|
71
|
-
client.sendTyping(
|
|
130
|
+
typingInterval = setInterval(() => {
|
|
131
|
+
client.sendTyping(msg.channelId).catch(() => {});
|
|
72
132
|
}, 3000);
|
|
133
|
+
typingInterval.unref();
|
|
73
134
|
|
|
74
135
|
const { runId } = await runtime.subagent.run({
|
|
75
136
|
sessionKey,
|
|
@@ -77,33 +138,74 @@ async function handleChatMention(opts: {
|
|
|
77
138
|
message: agentMessage,
|
|
78
139
|
extraSystemPrompt: isMention
|
|
79
140
|
? "You are responding to a direct @mention in ClawRoom. " +
|
|
141
|
+
"Always follow your configured Role, System Prompt, and Memory for this reply. " +
|
|
80
142
|
"You MUST reply with a helpful response. " +
|
|
81
143
|
"Keep your reply SHORT and conversational (1-3 sentences). " +
|
|
82
144
|
"Do NOT include file paths, system info, or markdown headers."
|
|
145
|
+
: isCoordinationWake
|
|
146
|
+
? "You are coordinating a tracked multi-agent mission in ClawRoom. " +
|
|
147
|
+
"Always follow your configured Role, System Prompt, and Memory for this turn. " +
|
|
148
|
+
"Clarify the root task until the next execution step is concrete. Stay read-only while clarifying or planning. " +
|
|
149
|
+
"Create or follow up only on bounded child work when needed, synthesize returned results yourself, and drive convergence instead of open-ended chatter. " +
|
|
150
|
+
"If coordination drifts, decide directly or surface one concrete blocker. " +
|
|
151
|
+
"Do not respond with SKIP."
|
|
152
|
+
: isFollowThroughWake
|
|
153
|
+
? "Your follow-through engine woke you up in ClawRoom because an open loop needs progress. " +
|
|
154
|
+
"Always follow your configured Role, System Prompt, and Memory for this reply. " +
|
|
155
|
+
"Move the work forward, close the loop, or ask the minimum clarifying question needed. " +
|
|
156
|
+
"Do not ignore the wake-up. Keep replies SHORT (1-3 sentences)."
|
|
157
|
+
: isGoalDriftWake
|
|
158
|
+
? "Potential goal drift was detected in ClawRoom. " +
|
|
159
|
+
"Always follow your configured Role, System Prompt, and Memory for this reply. " +
|
|
160
|
+
"Surface the misalignment clearly and ask for re-alignment if needed. " +
|
|
161
|
+
"Do not respond with SKIP. Keep replies SHORT (1-3 sentences)."
|
|
162
|
+
: isTriggerWake
|
|
163
|
+
? "A scheduled or proactive trigger woke you up in ClawRoom. " +
|
|
164
|
+
"Always follow your configured Role, System Prompt, and Memory for this reply. " +
|
|
165
|
+
"Do the triggered work directly, then report concise progress or outcome. " +
|
|
166
|
+
"Do not respond with SKIP unless the trigger is obviously stale or invalid."
|
|
83
167
|
: "You are a member of a chat channel in ClawRoom. " +
|
|
168
|
+
"Always follow your configured Role, System Prompt, and Memory for this reply. " +
|
|
84
169
|
"Respond naturally as a teammate. If someone greets the channel, greet back. " +
|
|
85
170
|
"If someone asks a question, help if you can. " +
|
|
86
171
|
"Only respond with exactly SKIP if the message is clearly a private exchange between others that doesn't involve you at all. " +
|
|
87
172
|
"Keep replies SHORT (1-3 sentences).",
|
|
88
173
|
lane: "clawroom",
|
|
89
174
|
});
|
|
175
|
+
shouldDeleteSession = true;
|
|
90
176
|
|
|
91
177
|
const waitResult = await runtime.subagent.waitForRun({
|
|
92
178
|
runId,
|
|
93
179
|
timeoutMs: CHAT_TIMEOUT_MS,
|
|
94
180
|
});
|
|
95
181
|
|
|
96
|
-
clearInterval(typingInterval);
|
|
97
|
-
|
|
98
182
|
if (waitResult.status === "error") {
|
|
99
183
|
log?.error?.(`[clawroom:chat] subagent error: ${waitResult.error}`);
|
|
100
|
-
|
|
184
|
+
reportPluginReflectionSoon(client, {
|
|
185
|
+
scope: "chat",
|
|
186
|
+
status: "error",
|
|
187
|
+
summary: `Reply failed for message ${msg.messageId}.`,
|
|
188
|
+
channelId: msg.channelId,
|
|
189
|
+
messageId: msg.messageId,
|
|
190
|
+
responseExcerpt: waitResult.error ?? null,
|
|
191
|
+
detail: {
|
|
192
|
+
error: waitResult.error ?? "Unknown error",
|
|
193
|
+
},
|
|
194
|
+
}, log);
|
|
195
|
+
await client.sendChatReply(msg.channelId, "Sorry, I encountered an error processing your message.");
|
|
101
196
|
return;
|
|
102
197
|
}
|
|
103
198
|
|
|
104
199
|
if (waitResult.status === "timeout") {
|
|
105
200
|
log?.error?.(`[clawroom:chat] subagent timeout for message ${msg.messageId}`);
|
|
106
|
-
|
|
201
|
+
reportPluginReflectionSoon(client, {
|
|
202
|
+
scope: "chat",
|
|
203
|
+
status: "timeout",
|
|
204
|
+
summary: `Reply timed out for message ${msg.messageId}.`,
|
|
205
|
+
channelId: msg.channelId,
|
|
206
|
+
messageId: msg.messageId,
|
|
207
|
+
}, log);
|
|
208
|
+
await client.sendChatReply(msg.channelId, "Sorry, I took too long to respond. Please try again.");
|
|
107
209
|
return;
|
|
108
210
|
}
|
|
109
211
|
|
|
@@ -112,26 +214,62 @@ async function handleChatMention(opts: {
|
|
|
112
214
|
limit: 10,
|
|
113
215
|
});
|
|
114
216
|
|
|
115
|
-
const reply = extractLastAssistantMessage(messages);
|
|
217
|
+
const reply = normalizeChatReplyContent(extractLastAssistantMessage(messages));
|
|
116
218
|
|
|
117
219
|
// If agent chose to skip (awareness message, not relevant), don't reply
|
|
118
|
-
|
|
220
|
+
const replyNorm = reply.trim().toLowerCase().replace(/[_\-\s]/g, "");
|
|
221
|
+
if (replyNorm === "skip" || replyNorm === "noreply" || replyNorm === "pass") {
|
|
119
222
|
log?.info?.(`[clawroom:chat] skipping ${msg.messageId} (not relevant)`);
|
|
120
|
-
|
|
223
|
+
reportPluginReflectionSoon(client, {
|
|
224
|
+
scope: "chat",
|
|
225
|
+
status: "skipped",
|
|
226
|
+
summary: `Skipped reply for message ${msg.messageId}.`,
|
|
227
|
+
channelId: msg.channelId,
|
|
228
|
+
messageId: msg.messageId,
|
|
229
|
+
toolsUsed: extractToolNames(messages),
|
|
230
|
+
responseExcerpt: reply.slice(0, 400),
|
|
231
|
+
}, log);
|
|
121
232
|
return;
|
|
122
233
|
}
|
|
123
234
|
|
|
124
235
|
log?.info?.(`[clawroom:chat] replying to ${msg.messageId}: "${reply.slice(0, 80)}"`);
|
|
125
|
-
await client.sendChatReply(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
236
|
+
await client.sendChatReply(msg.channelId, reply);
|
|
237
|
+
reportPluginReflectionSoon(client, {
|
|
238
|
+
scope: "chat",
|
|
239
|
+
status: "completed",
|
|
240
|
+
summary: `Replied in channel ${msg.channelId}.`,
|
|
241
|
+
channelId: msg.channelId,
|
|
242
|
+
messageId: msg.messageId,
|
|
243
|
+
toolsUsed: extractToolNames(messages),
|
|
244
|
+
responseExcerpt: reply.slice(0, 400),
|
|
245
|
+
detail: {
|
|
246
|
+
wakeReason: msg.wakeReason ?? null,
|
|
247
|
+
isMention,
|
|
248
|
+
},
|
|
249
|
+
}, log);
|
|
131
250
|
} catch (err) {
|
|
132
251
|
const reason = err instanceof Error ? err.message : String(err);
|
|
133
252
|
log?.error?.(`[clawroom:chat] unexpected error: ${reason}`);
|
|
134
|
-
|
|
253
|
+
reportPluginReflectionSoon(client, {
|
|
254
|
+
scope: "chat",
|
|
255
|
+
status: "error",
|
|
256
|
+
summary: `Unexpected chat executor failure for message ${msg.messageId}.`,
|
|
257
|
+
channelId: msg.channelId,
|
|
258
|
+
messageId: msg.messageId,
|
|
259
|
+
responseExcerpt: reason.slice(0, 400),
|
|
260
|
+
detail: {
|
|
261
|
+
error: reason,
|
|
262
|
+
},
|
|
263
|
+
}, log);
|
|
264
|
+
await client.sendChatReply(msg.channelId, "Sorry, something went wrong. Please try again.");
|
|
265
|
+
} finally {
|
|
266
|
+
if (typingInterval) clearInterval(typingInterval);
|
|
267
|
+
if (shouldDeleteSession) {
|
|
268
|
+
await runtime.subagent.deleteSession({
|
|
269
|
+
sessionKey,
|
|
270
|
+
deleteTranscript: true,
|
|
271
|
+
}).catch(() => {});
|
|
272
|
+
}
|
|
135
273
|
}
|
|
136
274
|
}
|
|
137
275
|
|
|
@@ -152,3 +290,24 @@ function extractLastAssistantMessage(messages: unknown[]): string {
|
|
|
152
290
|
}
|
|
153
291
|
return "I'm not sure how to respond to that.";
|
|
154
292
|
}
|
|
293
|
+
|
|
294
|
+
function normalizeChatReplyContent(content: string): string {
|
|
295
|
+
return content
|
|
296
|
+
.replace(/^(?:[ \t]*\r?\n)+/, "")
|
|
297
|
+
.trimEnd();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function formatAgentProfile(profile: AgentChatProfile): string {
|
|
301
|
+
return [
|
|
302
|
+
`Role: ${profile.role}`,
|
|
303
|
+
"",
|
|
304
|
+
"System Prompt:",
|
|
305
|
+
profile.systemPrompt,
|
|
306
|
+
"",
|
|
307
|
+
"Memory:",
|
|
308
|
+
profile.memory,
|
|
309
|
+
"",
|
|
310
|
+
"Continuity Packet:",
|
|
311
|
+
profile.continuityPacket,
|
|
312
|
+
].join("\n");
|
|
313
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ClawroomClient } from "@clawroom/sdk";
|
|
2
|
+
|
|
3
|
+
export type ReflectionScope = "chat" | "task";
|
|
4
|
+
export type ReflectionStatus = "started" | "completed" | "skipped" | "timeout" | "error";
|
|
5
|
+
type ReflectionParams = {
|
|
6
|
+
scope: ReflectionScope;
|
|
7
|
+
status: ReflectionStatus;
|
|
8
|
+
summary: string;
|
|
9
|
+
channelId?: string | null;
|
|
10
|
+
taskId?: string | null;
|
|
11
|
+
messageId?: string | null;
|
|
12
|
+
toolsUsed?: string[];
|
|
13
|
+
responseExcerpt?: string | null;
|
|
14
|
+
detail?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function reportPluginReflection(
|
|
18
|
+
client: ClawroomClient,
|
|
19
|
+
params: ReflectionParams,
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
await client.sendReflection({
|
|
22
|
+
scope: params.scope,
|
|
23
|
+
status: params.status,
|
|
24
|
+
summary: params.summary,
|
|
25
|
+
channelId: params.channelId ?? null,
|
|
26
|
+
taskId: params.taskId ?? null,
|
|
27
|
+
messageId: params.messageId ?? null,
|
|
28
|
+
toolsUsed: params.toolsUsed ?? [],
|
|
29
|
+
responseExcerpt: params.responseExcerpt ?? null,
|
|
30
|
+
detail: {
|
|
31
|
+
source: "plugin",
|
|
32
|
+
...(params.detail ?? {}),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function reportPluginReflectionSoon(
|
|
38
|
+
client: ClawroomClient,
|
|
39
|
+
params: ReflectionParams,
|
|
40
|
+
log?: { warn?: (message: string, ...args: unknown[]) => void },
|
|
41
|
+
): void {
|
|
42
|
+
void reportPluginReflection(client, params).catch((error) => {
|
|
43
|
+
log?.warn?.(`[clawroom:${params.scope}] reflection report failed`, error);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function extractToolNames(messages: unknown[]): string[] {
|
|
48
|
+
const names = new Set<string>();
|
|
49
|
+
for (const message of messages) {
|
|
50
|
+
const msg = message as Record<string, unknown> | undefined;
|
|
51
|
+
if (!msg || !Array.isArray(msg.content)) continue;
|
|
52
|
+
for (const block of msg.content) {
|
|
53
|
+
const item = block as Record<string, unknown>;
|
|
54
|
+
if (item.type !== "tool_use" && item.type !== "tool_call") continue;
|
|
55
|
+
const name = item.name ?? item.function;
|
|
56
|
+
if (typeof name === "string" && name) names.add(name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return Array.from(names);
|
|
60
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -3,4 +3,5 @@ import { createPluginRuntimeStore } from "openclaw/plugin-sdk";
|
|
|
3
3
|
|
|
4
4
|
const { setRuntime: setClawroomRuntime, getRuntime: getClawroomRuntime } =
|
|
5
5
|
createPluginRuntimeStore<PluginRuntime>("ClawRoom runtime not initialized");
|
|
6
|
+
|
|
6
7
|
export { getClawroomRuntime, setClawroomRuntime };
|