@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/task-executor.ts
CHANGED
|
@@ -1,35 +1,67 @@
|
|
|
1
|
-
import type { AgentResultFile } from "@clawroom/sdk";
|
|
1
|
+
import type { AgentResultFile, ClawroomClient } from "@clawroom/sdk";
|
|
2
2
|
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
3
|
-
import type { ClawroomPluginClient } from "./client.js";
|
|
4
3
|
import fs from "node:fs";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import { extractToolNames, reportPluginReflectionSoon } from "./reflections.js";
|
|
6
6
|
|
|
7
7
|
const SUBAGENT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
8
8
|
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
9
9
|
|
|
10
|
+
type TaskPayload = {
|
|
11
|
+
taskId: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
input?: string;
|
|
15
|
+
channelId?: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
10
18
|
export function setupTaskExecutor(opts: {
|
|
11
|
-
client:
|
|
19
|
+
client: ClawroomClient;
|
|
12
20
|
runtime: PluginRuntime;
|
|
13
21
|
log?: { info?: (m: string, ...a: unknown[]) => void; warn?: (m: string, ...a: unknown[]) => void; error?: (m: string, ...a: unknown[]) => void };
|
|
14
22
|
}): void {
|
|
15
23
|
const { client, runtime, log } = opts;
|
|
16
24
|
const activeTasks = new Set<string>();
|
|
25
|
+
const pendingTasks: TaskPayload[] = [];
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
const key = `${agentId}:${
|
|
27
|
+
const handleTask = (typedTask: TaskPayload, agentId: string) => {
|
|
28
|
+
const key = `${agentId}:${typedTask.taskId}`;
|
|
20
29
|
if (activeTasks.has(key)) return;
|
|
21
30
|
activeTasks.add(key);
|
|
22
|
-
log?.info?.(`[clawroom:executor] [${agentId}] executing task ${
|
|
23
|
-
void executeTask({ client, runtime, task, agentId, log }).finally(() => activeTasks.delete(key));
|
|
31
|
+
log?.info?.(`[clawroom:executor] [${agentId}] executing task ${typedTask.taskId}: ${typedTask.title}`);
|
|
32
|
+
void executeTask({ client, runtime, task: typedTask, agentId, log }).finally(() => activeTasks.delete(key));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const flushPending = (agentId: string) => {
|
|
36
|
+
if (pendingTasks.length === 0) return;
|
|
37
|
+
const queued = pendingTasks.splice(0, pendingTasks.length);
|
|
38
|
+
for (const task of queued) {
|
|
39
|
+
handleTask(task, agentId);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
client.onConnected((agentId) => {
|
|
44
|
+
flushPending(agentId);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
client.onTask((task) => {
|
|
48
|
+
const agentId = client.agentId;
|
|
49
|
+
const typedTask = task as TaskPayload;
|
|
50
|
+
if (!agentId) {
|
|
51
|
+
log?.warn?.("[clawroom:executor] received task before agent registration completed");
|
|
52
|
+
pendingTasks.push(typedTask);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
handleTask(typedTask, agentId);
|
|
24
56
|
});
|
|
25
57
|
}
|
|
26
58
|
|
|
27
59
|
async function executeTask(opts: {
|
|
28
|
-
client:
|
|
60
|
+
client: ClawroomClient;
|
|
29
61
|
runtime: PluginRuntime;
|
|
30
|
-
task:
|
|
62
|
+
task: TaskPayload;
|
|
31
63
|
agentId: string;
|
|
32
|
-
log?: { info?: (m: string, ...a: unknown[]) => void; error?: (m: string, ...a: unknown[]) => void };
|
|
64
|
+
log?: { info?: (m: string, ...a: unknown[]) => void; warn?: (m: string, ...a: unknown[]) => void; error?: (m: string, ...a: unknown[]) => void };
|
|
33
65
|
}): Promise<void> {
|
|
34
66
|
const { client, runtime, task, agentId, log } = opts;
|
|
35
67
|
const sessionKey = `clawroom:task:${agentId}:${task.taskId}`;
|
|
@@ -37,31 +69,58 @@ async function executeTask(opts: {
|
|
|
37
69
|
const parts: string[] = [`# Task: ${task.title}`];
|
|
38
70
|
if (task.description) parts.push("", task.description);
|
|
39
71
|
if (task.input) parts.push("", "## Input", "", task.input);
|
|
40
|
-
if (task.skillTags?.length) parts.push("", `Skills: ${task.skillTags.join(", ")}`);
|
|
41
72
|
const agentMessage = parts.join("\n");
|
|
73
|
+
let shouldDeleteSession = false;
|
|
42
74
|
|
|
43
75
|
try {
|
|
76
|
+
reportPluginReflectionSoon(client, {
|
|
77
|
+
scope: "task",
|
|
78
|
+
status: "started",
|
|
79
|
+
summary: `Started task ${task.taskId}: ${task.title}`,
|
|
80
|
+
taskId: task.taskId,
|
|
81
|
+
channelId: task.channelId ?? null,
|
|
82
|
+
}, log);
|
|
83
|
+
|
|
44
84
|
const { runId } = await runtime.subagent.run({
|
|
45
85
|
sessionKey,
|
|
46
86
|
idempotencyKey: `clawroom:${agentId}:${task.taskId}`,
|
|
47
87
|
message: agentMessage,
|
|
48
88
|
extraSystemPrompt:
|
|
49
|
-
"You are executing a task from ClawRoom.
|
|
89
|
+
"You are executing a tracked task from ClawRoom. Treat the task as the durable execution record. " +
|
|
90
|
+
"Stay within the task scope and done definition; do not widen the mission on your own. " +
|
|
91
|
+
"If the task is blocked, explain the concrete blocker instead of hand-waving. " +
|
|
92
|
+
"Complete it and provide a SHORT summary (2-3 sentences). " +
|
|
50
93
|
"Do NOT include local file paths or internal details. " +
|
|
51
94
|
"If you create output files, list them at the end prefixed with 'OUTPUT_FILE: /path'.",
|
|
52
95
|
lane: "clawroom",
|
|
53
96
|
});
|
|
97
|
+
shouldDeleteSession = true;
|
|
54
98
|
|
|
55
99
|
const waitResult = await runtime.subagent.waitForRun({ runId, timeoutMs: SUBAGENT_TIMEOUT_MS });
|
|
56
100
|
|
|
57
101
|
if (waitResult.status === "error") {
|
|
58
102
|
log?.error?.(`[clawroom:executor] [${agentId}] subagent error: ${waitResult.error}`);
|
|
59
|
-
|
|
103
|
+
reportPluginReflectionSoon(client, {
|
|
104
|
+
scope: "task",
|
|
105
|
+
status: "error",
|
|
106
|
+
summary: `Task ${task.taskId} failed.`,
|
|
107
|
+
taskId: task.taskId,
|
|
108
|
+
channelId: task.channelId ?? null,
|
|
109
|
+
responseExcerpt: (waitResult.error ?? "Agent execution failed").slice(0, 400),
|
|
110
|
+
}, log);
|
|
111
|
+
await client.sendFail(task.taskId, waitResult.error ?? "Agent execution failed");
|
|
60
112
|
return;
|
|
61
113
|
}
|
|
62
114
|
if (waitResult.status === "timeout") {
|
|
63
115
|
log?.error?.(`[clawroom:executor] [${agentId}] subagent timeout`);
|
|
64
|
-
|
|
116
|
+
reportPluginReflectionSoon(client, {
|
|
117
|
+
scope: "task",
|
|
118
|
+
status: "timeout",
|
|
119
|
+
summary: `Task ${task.taskId} timed out.`,
|
|
120
|
+
taskId: task.taskId,
|
|
121
|
+
channelId: task.channelId ?? null,
|
|
122
|
+
}, log);
|
|
123
|
+
await client.sendFail(task.taskId, "Agent execution timed out");
|
|
65
124
|
return;
|
|
66
125
|
}
|
|
67
126
|
|
|
@@ -72,12 +131,41 @@ async function executeTask(opts: {
|
|
|
72
131
|
const output = rawOutput.split("\n").filter((l) => !l.match(/OUTPUT_FILE:\s*/)).join("\n").replace(/`?\/\S+\/[^\s`]+`?/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
73
132
|
|
|
74
133
|
log?.info?.(`[clawroom:executor] [${agentId}] task completed${files.length > 0 ? ` (${files.length} files)` : ""}`);
|
|
75
|
-
await client.sendComplete(
|
|
76
|
-
|
|
134
|
+
await client.sendComplete(task.taskId, output, files.length > 0 ? files : undefined);
|
|
135
|
+
reportPluginReflectionSoon(client, {
|
|
136
|
+
scope: "task",
|
|
137
|
+
status: "completed",
|
|
138
|
+
summary: `Completed task ${task.taskId}: ${task.title}`,
|
|
139
|
+
taskId: task.taskId,
|
|
140
|
+
channelId: task.channelId ?? null,
|
|
141
|
+
toolsUsed: extractToolNames(messages),
|
|
142
|
+
responseExcerpt: output.slice(0, 400),
|
|
143
|
+
detail: {
|
|
144
|
+
attachmentCount: files.length,
|
|
145
|
+
},
|
|
146
|
+
}, log);
|
|
147
|
+
|
|
148
|
+
// Report completion in channel
|
|
149
|
+
if (task.channelId && output) {
|
|
150
|
+
const summary = output.length > 200 ? output.slice(0, 200) + "..." : output;
|
|
151
|
+
await client.sendChatReply(task.channelId, `✅ 完成任务「${task.title}」:${summary}`).catch(() => {});
|
|
152
|
+
}
|
|
77
153
|
} catch (err) {
|
|
78
154
|
const reason = err instanceof Error ? err.message : String(err);
|
|
79
155
|
log?.error?.(`[clawroom:executor] [${agentId}] error: ${reason}`);
|
|
80
|
-
|
|
156
|
+
reportPluginReflectionSoon(client, {
|
|
157
|
+
scope: "task",
|
|
158
|
+
status: "error",
|
|
159
|
+
summary: `Unexpected failure while executing task ${task.taskId}.`,
|
|
160
|
+
taskId: task.taskId,
|
|
161
|
+
channelId: task.channelId ?? null,
|
|
162
|
+
responseExcerpt: reason.slice(0, 400),
|
|
163
|
+
}, log);
|
|
164
|
+
await client.sendFail(task.taskId, reason);
|
|
165
|
+
} finally {
|
|
166
|
+
if (shouldDeleteSession) {
|
|
167
|
+
await runtime.subagent.deleteSession({ sessionKey, deleteTranscript: true }).catch(() => {});
|
|
168
|
+
}
|
|
81
169
|
}
|
|
82
170
|
}
|
|
83
171
|
|
|
@@ -107,7 +195,8 @@ function extractWrittenFiles(messages: unknown[]): string[] {
|
|
|
107
195
|
const raw = b.input ?? b.arguments;
|
|
108
196
|
const input = typeof raw === "string" ? (() => { try { return JSON.parse(raw); } catch { return null; } })() : raw;
|
|
109
197
|
if (input && typeof input === "object") {
|
|
110
|
-
const
|
|
198
|
+
const typedInput = input as { file_path?: unknown; path?: unknown };
|
|
199
|
+
const fp = typedInput.file_path ?? typedInput.path;
|
|
111
200
|
if (typeof fp === "string" && fp) files.add(fp);
|
|
112
201
|
}
|
|
113
202
|
}
|
|
@@ -127,7 +216,10 @@ const MIME_MAP: Record<string, string> = {
|
|
|
127
216
|
".gif": "image/gif", ".svg": "image/svg+xml", ".zip": "application/zip",
|
|
128
217
|
};
|
|
129
218
|
|
|
130
|
-
function readAllFiles(
|
|
219
|
+
function readAllFiles(
|
|
220
|
+
paths: string[],
|
|
221
|
+
log?: { warn?: (m: string, ...a: unknown[]) => void },
|
|
222
|
+
): AgentResultFile[] {
|
|
131
223
|
const results: AgentResultFile[] = [];
|
|
132
224
|
for (const fp of paths) {
|
|
133
225
|
try {
|
|
@@ -136,7 +228,9 @@ function readAllFiles(paths: string[], log?: { warn?: (m: string, ...a: unknown[
|
|
|
136
228
|
if (stat.size > MAX_FILE_SIZE) continue;
|
|
137
229
|
const data = fs.readFileSync(fp);
|
|
138
230
|
results.push({ filename: path.basename(fp), mimeType: MIME_MAP[path.extname(fp).toLowerCase()] ?? "application/octet-stream", data: data.toString("base64") });
|
|
139
|
-
} catch {
|
|
231
|
+
} catch (error) {
|
|
232
|
+
log?.warn?.(`[clawroom:executor] failed to read output file ${fp}:`, error);
|
|
233
|
+
}
|
|
140
234
|
}
|
|
141
235
|
return results;
|
|
142
236
|
}
|
package/src/client.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { ClawroomMachineClient } from "@clawroom/sdk";
|
|
2
|
-
import type { ClawroomMachineClientOptions } from "@clawroom/sdk";
|
|
3
|
-
|
|
4
|
-
type DisconnectCallback = () => void;
|
|
5
|
-
type WelcomeCallback = (machineId: string) => void;
|
|
6
|
-
type FatalCallback = (reason: string) => void;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Plugin-specific ClawRoom machine client.
|
|
10
|
-
* Wraps ClawroomMachineClient with OpenClaw lifecycle hooks.
|
|
11
|
-
*/
|
|
12
|
-
export class ClawroomPluginClient {
|
|
13
|
-
private inner: ClawroomMachineClient;
|
|
14
|
-
private disconnectCallbacks: DisconnectCallback[] = [];
|
|
15
|
-
private welcomeCallbacks: WelcomeCallback[] = [];
|
|
16
|
-
private fatalCallbacks: FatalCallback[] = [];
|
|
17
|
-
|
|
18
|
-
constructor(options: ClawroomMachineClientOptions) {
|
|
19
|
-
this.inner = new ClawroomMachineClient(options);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
get isAlive(): boolean { return !this.inner.stopped; }
|
|
23
|
-
get isConnected(): boolean { return this.inner.connected; }
|
|
24
|
-
|
|
25
|
-
onDisconnect(cb: DisconnectCallback): void { this.disconnectCallbacks.push(cb); }
|
|
26
|
-
onWelcome(cb: WelcomeCallback): void { this.welcomeCallbacks.push(cb); }
|
|
27
|
-
onFatal(cb: FatalCallback): void { this.fatalCallbacks.push(cb); }
|
|
28
|
-
|
|
29
|
-
onAgentTask(handler: (agentId: string, task: any) => void) { this.inner.onAgentTask(handler); }
|
|
30
|
-
onAgentChat(handler: (agentId: string, messages: any[]) => void) { this.inner.onAgentChat(handler); }
|
|
31
|
-
|
|
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);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async sendFail(agentId: string, taskId: string, reason: string) {
|
|
37
|
-
await this.inner.sendAgentFail(agentId, taskId, reason);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async sendChatReply(agentId: string, channelId: string, content: string) {
|
|
41
|
-
await this.inner.sendAgentChatReply(agentId, channelId, content);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async sendTyping(agentId: string, channelId: string) {
|
|
45
|
-
await this.inner.sendAgentTyping(agentId, channelId);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
connect(): void {
|
|
49
|
-
this.inner.connect();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
disconnect(): void {
|
|
53
|
-
this.inner.disconnect();
|
|
54
|
-
for (const cb of this.disconnectCallbacks) cb();
|
|
55
|
-
}
|
|
56
|
-
}
|