@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.
@@ -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: ClawroomPluginClient;
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
- client.onAgentTask((agentId: string, task: any) => {
19
- const key = `${agentId}:${task.taskId}`;
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 ${task.taskId}: ${task.title}`);
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: ClawroomPluginClient;
60
+ client: ClawroomClient;
29
61
  runtime: PluginRuntime;
30
- task: any;
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. Complete it and provide a SHORT summary (2-3 sentences). " +
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
- await client.sendFail(agentId, task.taskId, waitResult.error ?? "Agent execution failed");
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
- await client.sendFail(agentId, task.taskId, "Agent execution timed out");
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(agentId, task.taskId, output, files.length > 0 ? files : undefined);
76
- await runtime.subagent.deleteSession({ sessionKey, deleteTranscript: true });
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
- await client.sendFail(agentId, task.taskId, reason);
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 fp = (input as any).file_path ?? (input as any).path;
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(paths: string[], log?: { warn?: (m: string, ...a: unknown[]) => void; info?: (m: string, ...a: unknown[]) => void }): AgentResultFile[] {
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
- }