@lobu/worker 2.8.0
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/dist/core/error-handler.d.ts +7 -0
- package/dist/core/error-handler.d.ts.map +1 -0
- package/dist/core/error-handler.js +58 -0
- package/dist/core/error-handler.js.map +1 -0
- package/dist/core/project-scanner.d.ts +9 -0
- package/dist/core/project-scanner.d.ts.map +1 -0
- package/dist/core/project-scanner.js +64 -0
- package/dist/core/project-scanner.js.map +1 -0
- package/dist/core/types.d.ts +102 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +8 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/url-utils.d.ts +5 -0
- package/dist/core/url-utils.d.ts.map +1 -0
- package/dist/core/url-utils.js +13 -0
- package/dist/core/url-utils.js.map +1 -0
- package/dist/core/workspace.d.ts +29 -0
- package/dist/core/workspace.d.ts.map +1 -0
- package/dist/core/workspace.js +104 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/embedded/just-bash-bootstrap.d.ts +21 -0
- package/dist/embedded/just-bash-bootstrap.d.ts.map +1 -0
- package/dist/embedded/just-bash-bootstrap.js +215 -0
- package/dist/embedded/just-bash-bootstrap.js.map +1 -0
- package/dist/gateway/gateway-integration.d.ts +57 -0
- package/dist/gateway/gateway-integration.d.ts.map +1 -0
- package/dist/gateway/gateway-integration.js +209 -0
- package/dist/gateway/gateway-integration.js.map +1 -0
- package/dist/gateway/message-batcher.d.ts +27 -0
- package/dist/gateway/message-batcher.d.ts.map +1 -0
- package/dist/gateway/message-batcher.js +102 -0
- package/dist/gateway/message-batcher.js.map +1 -0
- package/dist/gateway/sse-client.d.ts +74 -0
- package/dist/gateway/sse-client.d.ts.map +1 -0
- package/dist/gateway/sse-client.js +748 -0
- package/dist/gateway/sse-client.js.map +1 -0
- package/dist/gateway/types.d.ts +60 -0
- package/dist/gateway/types.d.ts.map +1 -0
- package/dist/gateway/types.js +6 -0
- package/dist/gateway/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +112 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions/builder.d.ts +8 -0
- package/dist/instructions/builder.d.ts.map +1 -0
- package/dist/instructions/builder.js +53 -0
- package/dist/instructions/builder.js.map +1 -0
- package/dist/instructions/providers.d.ts +13 -0
- package/dist/instructions/providers.d.ts.map +1 -0
- package/dist/instructions/providers.js +26 -0
- package/dist/instructions/providers.js.map +1 -0
- package/dist/modules/lifecycle.d.ts +18 -0
- package/dist/modules/lifecycle.d.ts.map +1 -0
- package/dist/modules/lifecycle.js +56 -0
- package/dist/modules/lifecycle.js.map +1 -0
- package/dist/openclaw/custom-tools.d.ts +17 -0
- package/dist/openclaw/custom-tools.d.ts.map +1 -0
- package/dist/openclaw/custom-tools.js +195 -0
- package/dist/openclaw/custom-tools.js.map +1 -0
- package/dist/openclaw/instructions.d.ts +15 -0
- package/dist/openclaw/instructions.d.ts.map +1 -0
- package/dist/openclaw/instructions.js +32 -0
- package/dist/openclaw/instructions.js.map +1 -0
- package/dist/openclaw/model-resolver.d.ts +30 -0
- package/dist/openclaw/model-resolver.d.ts.map +1 -0
- package/dist/openclaw/model-resolver.js +147 -0
- package/dist/openclaw/model-resolver.js.map +1 -0
- package/dist/openclaw/plugin-loader.d.ts +39 -0
- package/dist/openclaw/plugin-loader.d.ts.map +1 -0
- package/dist/openclaw/plugin-loader.js +347 -0
- package/dist/openclaw/plugin-loader.js.map +1 -0
- package/dist/openclaw/processor.d.ts +38 -0
- package/dist/openclaw/processor.d.ts.map +1 -0
- package/dist/openclaw/processor.js +182 -0
- package/dist/openclaw/processor.js.map +1 -0
- package/dist/openclaw/session-context.d.ts +44 -0
- package/dist/openclaw/session-context.d.ts.map +1 -0
- package/dist/openclaw/session-context.js +151 -0
- package/dist/openclaw/session-context.js.map +1 -0
- package/dist/openclaw/tool-policy.d.ts +23 -0
- package/dist/openclaw/tool-policy.d.ts.map +1 -0
- package/dist/openclaw/tool-policy.js +151 -0
- package/dist/openclaw/tool-policy.js.map +1 -0
- package/dist/openclaw/tools.d.ts +6 -0
- package/dist/openclaw/tools.d.ts.map +1 -0
- package/dist/openclaw/tools.js +158 -0
- package/dist/openclaw/tools.js.map +1 -0
- package/dist/openclaw/worker.d.ts +39 -0
- package/dist/openclaw/worker.d.ts.map +1 -0
- package/dist/openclaw/worker.js +1340 -0
- package/dist/openclaw/worker.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +304 -0
- package/dist/server.js.map +1 -0
- package/dist/shared/audio-provider-suggestions.d.ts +13 -0
- package/dist/shared/audio-provider-suggestions.d.ts.map +1 -0
- package/dist/shared/audio-provider-suggestions.js +105 -0
- package/dist/shared/audio-provider-suggestions.js.map +1 -0
- package/dist/shared/processor-utils.d.ts +6 -0
- package/dist/shared/processor-utils.d.ts.map +1 -0
- package/dist/shared/processor-utils.js +30 -0
- package/dist/shared/processor-utils.js.map +1 -0
- package/dist/shared/provider-auth-hints.d.ts +6 -0
- package/dist/shared/provider-auth-hints.d.ts.map +1 -0
- package/dist/shared/provider-auth-hints.js +51 -0
- package/dist/shared/provider-auth-hints.js.map +1 -0
- package/dist/shared/tool-display-config.d.ts +16 -0
- package/dist/shared/tool-display-config.d.ts.map +1 -0
- package/dist/shared/tool-display-config.js +67 -0
- package/dist/shared/tool-display-config.js.map +1 -0
- package/dist/shared/tool-implementations.d.ts +55 -0
- package/dist/shared/tool-implementations.d.ts.map +1 -0
- package/dist/shared/tool-implementations.js +519 -0
- package/dist/shared/tool-implementations.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.OpenClawWorker = void 0;
|
|
38
|
+
exports.estimatePromptTokenCost = estimatePromptTokenCost;
|
|
39
|
+
exports.resolveMemoryFlushConfig = resolveMemoryFlushConfig;
|
|
40
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const node_stream_1 = require("node:stream");
|
|
43
|
+
const promises_1 = require("node:stream/promises");
|
|
44
|
+
const core_1 = require("@lobu/core");
|
|
45
|
+
const pi_ai_1 = require("@mariozechner/pi-ai");
|
|
46
|
+
const pi_coding_agent_1 = require("@mariozechner/pi-coding-agent");
|
|
47
|
+
const Sentry = __importStar(require("@sentry/node"));
|
|
48
|
+
const error_handler_1 = require("../core/error-handler");
|
|
49
|
+
const project_scanner_1 = require("../core/project-scanner");
|
|
50
|
+
const workspace_1 = require("../core/workspace");
|
|
51
|
+
const gateway_integration_1 = require("../gateway/gateway-integration");
|
|
52
|
+
const builder_1 = require("../instructions/builder");
|
|
53
|
+
const providers_1 = require("../instructions/providers");
|
|
54
|
+
const audio_provider_suggestions_1 = require("../shared/audio-provider-suggestions");
|
|
55
|
+
const provider_auth_hints_1 = require("../shared/provider-auth-hints");
|
|
56
|
+
const tool_implementations_1 = require("../shared/tool-implementations");
|
|
57
|
+
const custom_tools_1 = require("./custom-tools");
|
|
58
|
+
const instructions_1 = require("./instructions");
|
|
59
|
+
const model_resolver_1 = require("./model-resolver");
|
|
60
|
+
const plugin_loader_1 = require("./plugin-loader");
|
|
61
|
+
const processor_1 = require("./processor");
|
|
62
|
+
const session_context_1 = require("./session-context");
|
|
63
|
+
const tool_policy_1 = require("./tool-policy");
|
|
64
|
+
const tools_1 = require("./tools");
|
|
65
|
+
const logger = (0, core_1.createLogger)("worker");
|
|
66
|
+
const MEMORY_FLUSH_STATE_CUSTOM_TYPE = "lobu.memory_flush_state";
|
|
67
|
+
const APPROX_IMAGE_TOKENS = 1200;
|
|
68
|
+
const DEFAULT_MEMORY_FLUSH_CONFIG = {
|
|
69
|
+
enabled: true,
|
|
70
|
+
softThresholdTokens: 4000,
|
|
71
|
+
systemPrompt: "Session nearing compaction. Store durable memories now.",
|
|
72
|
+
prompt: "Write any lasting notes to memory using available memory tools. Reply with NO_REPLY if nothing to store.",
|
|
73
|
+
};
|
|
74
|
+
function isLikelyImageGenerationRequest(prompt) {
|
|
75
|
+
const lower = prompt.toLowerCase();
|
|
76
|
+
const explicitToolInstruction = lower.includes("generateimage tool") || lower.includes("use generateimage");
|
|
77
|
+
const directShortcutEnabled = process.env.WORKER_ENABLE_DIRECT_IMAGE_SHORTCUT === "true";
|
|
78
|
+
return directShortcutEnabled && explicitToolInstruction;
|
|
79
|
+
}
|
|
80
|
+
function extractToolTextContent(result) {
|
|
81
|
+
if (!Array.isArray(result.content))
|
|
82
|
+
return "";
|
|
83
|
+
return result.content
|
|
84
|
+
.filter((item) => item?.type === "text" && typeof item.text === "string")
|
|
85
|
+
.map((item) => item.text)
|
|
86
|
+
.join("\n")
|
|
87
|
+
.trim();
|
|
88
|
+
}
|
|
89
|
+
function isRecord(value) {
|
|
90
|
+
return typeof value === "object" && value !== null;
|
|
91
|
+
}
|
|
92
|
+
function readStringOrFallback(value, fallback, allowEmpty = false) {
|
|
93
|
+
if (typeof value !== "string") {
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
const trimmed = value.trim();
|
|
97
|
+
if (!trimmed && !allowEmpty) {
|
|
98
|
+
return fallback;
|
|
99
|
+
}
|
|
100
|
+
return allowEmpty ? value : trimmed;
|
|
101
|
+
}
|
|
102
|
+
function readNonNegativeNumberOrFallback(value, fallback) {
|
|
103
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
104
|
+
return fallback;
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
function countCompactionsOnCurrentBranch(sessionManager) {
|
|
109
|
+
const branch = sessionManager.getBranch();
|
|
110
|
+
return branch.reduce((count, entry) => {
|
|
111
|
+
if (entry.type === "compaction") {
|
|
112
|
+
return count + 1;
|
|
113
|
+
}
|
|
114
|
+
return count;
|
|
115
|
+
}, 0);
|
|
116
|
+
}
|
|
117
|
+
function readLastFlushedCompactionCount(sessionManager) {
|
|
118
|
+
const branch = sessionManager.getBranch();
|
|
119
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
120
|
+
const entry = branch[i];
|
|
121
|
+
if (!entry)
|
|
122
|
+
continue;
|
|
123
|
+
if (entry.type !== "custom")
|
|
124
|
+
continue;
|
|
125
|
+
if (entry.customType !== MEMORY_FLUSH_STATE_CUSTOM_TYPE)
|
|
126
|
+
continue;
|
|
127
|
+
if (!isRecord(entry.data))
|
|
128
|
+
continue;
|
|
129
|
+
const compactionCount = entry.data.compactionCount;
|
|
130
|
+
if (typeof compactionCount === "number" &&
|
|
131
|
+
Number.isFinite(compactionCount) &&
|
|
132
|
+
compactionCount >= 0) {
|
|
133
|
+
return compactionCount;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function getLatestAssistantText(messages) {
|
|
139
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
140
|
+
const message = messages[i];
|
|
141
|
+
if (!isRecord(message) || message.role !== "assistant")
|
|
142
|
+
continue;
|
|
143
|
+
const content = message.content;
|
|
144
|
+
let text = "";
|
|
145
|
+
if (typeof content === "string") {
|
|
146
|
+
text = content;
|
|
147
|
+
}
|
|
148
|
+
else if (Array.isArray(content)) {
|
|
149
|
+
text = content
|
|
150
|
+
.flatMap((block) => {
|
|
151
|
+
if (!isRecord(block))
|
|
152
|
+
return [];
|
|
153
|
+
if (block.type !== "text")
|
|
154
|
+
return [];
|
|
155
|
+
return typeof block.text === "string" ? [block.text] : [];
|
|
156
|
+
})
|
|
157
|
+
.join("");
|
|
158
|
+
}
|
|
159
|
+
const normalized = text.trim().toUpperCase();
|
|
160
|
+
return {
|
|
161
|
+
text,
|
|
162
|
+
normalizedNoReply: normalized === "NO_REPLY",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function estimatePromptTokenCost(promptText, imageCount) {
|
|
168
|
+
const textTokens = Math.ceil(promptText.length / 4);
|
|
169
|
+
const imageTokens = Math.max(0, imageCount) * APPROX_IMAGE_TOKENS;
|
|
170
|
+
return textTokens + imageTokens;
|
|
171
|
+
}
|
|
172
|
+
function resolveMemoryFlushConfig(rawOptions) {
|
|
173
|
+
const compaction = isRecord(rawOptions.compaction)
|
|
174
|
+
? rawOptions.compaction
|
|
175
|
+
: undefined;
|
|
176
|
+
const memoryFlush = compaction && isRecord(compaction.memoryFlush)
|
|
177
|
+
? compaction.memoryFlush
|
|
178
|
+
: undefined;
|
|
179
|
+
return {
|
|
180
|
+
enabled: typeof memoryFlush?.enabled === "boolean"
|
|
181
|
+
? memoryFlush.enabled
|
|
182
|
+
: DEFAULT_MEMORY_FLUSH_CONFIG.enabled,
|
|
183
|
+
softThresholdTokens: readNonNegativeNumberOrFallback(memoryFlush?.softThresholdTokens, DEFAULT_MEMORY_FLUSH_CONFIG.softThresholdTokens),
|
|
184
|
+
systemPrompt: readStringOrFallback(memoryFlush?.systemPrompt, DEFAULT_MEMORY_FLUSH_CONFIG.systemPrompt),
|
|
185
|
+
prompt: readStringOrFallback(memoryFlush?.prompt, DEFAULT_MEMORY_FLUSH_CONFIG.prompt),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
class OpenClawWorker {
|
|
189
|
+
workspaceManager;
|
|
190
|
+
workerTransport;
|
|
191
|
+
config;
|
|
192
|
+
progressProcessor;
|
|
193
|
+
constructor(config) {
|
|
194
|
+
this.config = config;
|
|
195
|
+
this.workspaceManager = new workspace_1.WorkspaceManager(config.workspace);
|
|
196
|
+
this.progressProcessor = new processor_1.OpenClawProgressProcessor();
|
|
197
|
+
// Verify required environment variables
|
|
198
|
+
const gatewayUrl = process.env.DISPATCHER_URL;
|
|
199
|
+
const workerToken = process.env.WORKER_TOKEN;
|
|
200
|
+
if (!gatewayUrl || !workerToken) {
|
|
201
|
+
throw new Error("DISPATCHER_URL and WORKER_TOKEN environment variables are required");
|
|
202
|
+
}
|
|
203
|
+
if (!config.teamId) {
|
|
204
|
+
throw new Error("teamId is required for worker initialization");
|
|
205
|
+
}
|
|
206
|
+
if (!config.conversationId) {
|
|
207
|
+
throw new Error("conversationId is required for worker initialization");
|
|
208
|
+
}
|
|
209
|
+
this.workerTransport = new gateway_integration_1.HttpWorkerTransport({
|
|
210
|
+
gatewayUrl,
|
|
211
|
+
workerToken,
|
|
212
|
+
userId: config.userId,
|
|
213
|
+
channelId: config.channelId,
|
|
214
|
+
conversationId: config.conversationId,
|
|
215
|
+
originalMessageTs: config.responseId,
|
|
216
|
+
botResponseTs: config.botResponseId,
|
|
217
|
+
teamId: config.teamId,
|
|
218
|
+
platform: config.platform,
|
|
219
|
+
platformMetadata: config.platformMetadata,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Main execution workflow
|
|
224
|
+
*/
|
|
225
|
+
async execute() {
|
|
226
|
+
const executeStartTime = Date.now();
|
|
227
|
+
try {
|
|
228
|
+
this.progressProcessor.reset();
|
|
229
|
+
logger.info(`🚀 Starting OpenClaw worker for session: ${this.config.sessionKey}`);
|
|
230
|
+
logger.info(`[TIMING] Worker execute() started at: ${new Date(executeStartTime).toISOString()}`);
|
|
231
|
+
// Decode user prompt
|
|
232
|
+
const userPrompt = Buffer.from(this.config.userPrompt, "base64").toString("utf-8");
|
|
233
|
+
logger.info(`User prompt: ${userPrompt.substring(0, 100)}...`);
|
|
234
|
+
// Setup workspace
|
|
235
|
+
logger.info("Setting up workspace...");
|
|
236
|
+
await Sentry.startSpan({
|
|
237
|
+
name: "worker.workspace_setup",
|
|
238
|
+
op: "worker.setup",
|
|
239
|
+
attributes: {
|
|
240
|
+
"user.id": this.config.userId,
|
|
241
|
+
"session.key": this.config.sessionKey,
|
|
242
|
+
},
|
|
243
|
+
}, async () => {
|
|
244
|
+
await this.workspaceManager.setupWorkspace(this.config.userId, this.config.sessionKey);
|
|
245
|
+
const { initModuleWorkspace } = await Promise.resolve().then(() => __importStar(require("../modules/lifecycle")));
|
|
246
|
+
await initModuleWorkspace({
|
|
247
|
+
workspaceDir: this.workspaceManager.getCurrentWorkingDirectory(),
|
|
248
|
+
username: this.config.userId,
|
|
249
|
+
sessionKey: this.config.sessionKey,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
// Setup I/O directories for file handling
|
|
253
|
+
await this.setupIODirectories();
|
|
254
|
+
// Download input files if any
|
|
255
|
+
await this.downloadInputFiles();
|
|
256
|
+
// Generate custom instructions
|
|
257
|
+
let customInstructions = await (0, builder_1.generateCustomInstructions)([
|
|
258
|
+
new instructions_1.OpenClawCoreInstructionProvider(),
|
|
259
|
+
new instructions_1.OpenClawPromptIntentInstructionProvider(),
|
|
260
|
+
new providers_1.ProjectsInstructionProvider(),
|
|
261
|
+
], {
|
|
262
|
+
userId: this.config.userId,
|
|
263
|
+
agentId: this.config.agentId,
|
|
264
|
+
sessionKey: this.config.sessionKey,
|
|
265
|
+
workingDirectory: this.workspaceManager.getCurrentWorkingDirectory(),
|
|
266
|
+
userPrompt,
|
|
267
|
+
availableProjects: (0, project_scanner_1.listAppDirectories)(this.workspaceManager.getCurrentWorkingDirectory()),
|
|
268
|
+
});
|
|
269
|
+
// Call module onSessionStart hooks to allow modules to modify system prompt
|
|
270
|
+
try {
|
|
271
|
+
const { onSessionStart } = await Promise.resolve().then(() => __importStar(require("../modules/lifecycle")));
|
|
272
|
+
const moduleContext = await onSessionStart({
|
|
273
|
+
platform: this.config.platform,
|
|
274
|
+
channelId: this.config.channelId,
|
|
275
|
+
userId: this.config.userId,
|
|
276
|
+
conversationId: this.config.conversationId,
|
|
277
|
+
messageId: this.config.responseId,
|
|
278
|
+
workingDirectory: this.workspaceManager.getCurrentWorkingDirectory(),
|
|
279
|
+
customInstructions,
|
|
280
|
+
});
|
|
281
|
+
if (moduleContext.customInstructions) {
|
|
282
|
+
customInstructions = moduleContext.customInstructions;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
logger.error("Failed to call onSessionStart hooks:", error);
|
|
287
|
+
}
|
|
288
|
+
// Add file I/O instructions AFTER module hooks so they aren't overwritten
|
|
289
|
+
customInstructions += this.getFileIOInstructions();
|
|
290
|
+
// Execute AI session
|
|
291
|
+
logger.info(`[TIMING] Starting OpenClaw session at: ${new Date().toISOString()}`);
|
|
292
|
+
const aiStartTime = Date.now();
|
|
293
|
+
logger.info(`[TIMING] Total worker startup time: ${aiStartTime - executeStartTime}ms`);
|
|
294
|
+
if (isLikelyImageGenerationRequest(userPrompt)) {
|
|
295
|
+
logger.info("Direct image-generation shortcut triggered");
|
|
296
|
+
const gatewayUrl = process.env.DISPATCHER_URL;
|
|
297
|
+
const workerToken = process.env.WORKER_TOKEN;
|
|
298
|
+
if (!gatewayUrl || !workerToken) {
|
|
299
|
+
throw new Error("DISPATCHER_URL and WORKER_TOKEN are required for image generation");
|
|
300
|
+
}
|
|
301
|
+
const gatewayParams = {
|
|
302
|
+
gatewayUrl,
|
|
303
|
+
workerToken,
|
|
304
|
+
channelId: this.config.channelId,
|
|
305
|
+
conversationId: this.config.conversationId,
|
|
306
|
+
platform: this.config.platform,
|
|
307
|
+
};
|
|
308
|
+
const toolResult = await (0, tool_implementations_1.generateImage)(gatewayParams, {
|
|
309
|
+
prompt: userPrompt,
|
|
310
|
+
});
|
|
311
|
+
const toolText = extractToolTextContent(toolResult) || "Image request processed.";
|
|
312
|
+
await this.workerTransport.sendStreamDelta(toolText, false, true);
|
|
313
|
+
await this.workerTransport.signalDone();
|
|
314
|
+
logger.info("Direct image-generation shortcut completed");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
let firstOutputLogged = false;
|
|
318
|
+
const result = await Sentry.startSpan({
|
|
319
|
+
name: "worker.openclaw_execution",
|
|
320
|
+
op: "ai.inference",
|
|
321
|
+
attributes: {
|
|
322
|
+
"user.id": this.config.userId,
|
|
323
|
+
"session.key": this.config.sessionKey,
|
|
324
|
+
"conversation.id": this.config.conversationId,
|
|
325
|
+
agent: "OpenClaw",
|
|
326
|
+
},
|
|
327
|
+
}, async () => {
|
|
328
|
+
return await this.runAISession(userPrompt, customInstructions, async (update) => {
|
|
329
|
+
if (!firstOutputLogged && update.type === "output") {
|
|
330
|
+
logger.info(`[TIMING] First OpenClaw output at: ${new Date().toISOString()} (${Date.now() - aiStartTime}ms after start)`);
|
|
331
|
+
firstOutputLogged = true;
|
|
332
|
+
}
|
|
333
|
+
if (update.type === "output" && update.data) {
|
|
334
|
+
const delta = typeof update.data === "string" ? update.data : null;
|
|
335
|
+
if (delta) {
|
|
336
|
+
await this.workerTransport.sendStreamDelta(delta, false);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else if (update.type === "status_update") {
|
|
340
|
+
await this.workerTransport.sendStatusUpdate(update.data.elapsedSeconds, update.data.state);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
// Collect module data before sending final response
|
|
345
|
+
const { collectModuleData } = await Promise.resolve().then(() => __importStar(require("../modules/lifecycle")));
|
|
346
|
+
const moduleData = await collectModuleData({
|
|
347
|
+
workspaceDir: this.workspaceManager.getCurrentWorkingDirectory(),
|
|
348
|
+
userId: this.config.userId,
|
|
349
|
+
conversationId: this.config.conversationId,
|
|
350
|
+
});
|
|
351
|
+
this.workerTransport.setModuleData(moduleData);
|
|
352
|
+
// Handle result
|
|
353
|
+
if (result.success) {
|
|
354
|
+
const outputSnapshot = this.progressProcessor.getOutputSnapshot();
|
|
355
|
+
const hintGatewayUrl = process.env.DISPATCHER_URL;
|
|
356
|
+
const hintWorkerToken = process.env.WORKER_TOKEN;
|
|
357
|
+
const audioPermissionHint = hintGatewayUrl && hintWorkerToken
|
|
358
|
+
? await this.maybeBuildAudioPermissionHintMessage(outputSnapshot, hintGatewayUrl, hintWorkerToken)
|
|
359
|
+
: null;
|
|
360
|
+
const finalResult = this.progressProcessor.getFinalResult();
|
|
361
|
+
if (finalResult) {
|
|
362
|
+
const finalText = audioPermissionHint
|
|
363
|
+
? `${finalResult.text}\n\n${audioPermissionHint}`
|
|
364
|
+
: finalResult.text;
|
|
365
|
+
logger.info(`📤 Sending final result (${finalText.length} chars) with deduplication flag`);
|
|
366
|
+
await this.workerTransport.sendStreamDelta(finalText, false, finalResult.isFinal);
|
|
367
|
+
}
|
|
368
|
+
else if (audioPermissionHint) {
|
|
369
|
+
logger.info("📤 Sending audio permission settings hint to user");
|
|
370
|
+
await this.workerTransport.sendStreamDelta(`\n\n${audioPermissionHint}`, false);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
logger.info("Session completed successfully - all content already streamed");
|
|
374
|
+
}
|
|
375
|
+
await this.workerTransport.signalDone();
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
const errorMsg = result.error || "Unknown error";
|
|
379
|
+
const isTimeout = result.exitCode === 124;
|
|
380
|
+
if (isTimeout) {
|
|
381
|
+
logger.info(`Session timed out (exit code 124) - will be retried automatically, not showing error to user`);
|
|
382
|
+
throw new Error("SESSION_TIMEOUT");
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
const isAuthError = /no.credentials.configured|no_credentials|invalid.*api.key|incorrect.*api.key|token.*expired/i.test(errorMsg);
|
|
386
|
+
const userMessage = isAuthError
|
|
387
|
+
? "Your AI provider credentials are invalid or expired. End-user provider setup is not available in chat yet. Ask an admin to reconnect the base agent provider."
|
|
388
|
+
: `❌ Session failed: ${errorMsg}`;
|
|
389
|
+
await this.workerTransport.sendStreamDelta(userMessage, true, true);
|
|
390
|
+
if (isAuthError) {
|
|
391
|
+
await this.workerTransport.signalDone();
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
await this.workerTransport.signalError(new Error(errorMsg));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
logger.info(`Worker completed with ${result.success ? "success" : "failure"}`);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
await (0, error_handler_1.handleExecutionError)(error, this.workerTransport);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async cleanup() {
|
|
405
|
+
try {
|
|
406
|
+
logger.info("Cleaning up worker resources...");
|
|
407
|
+
logger.info("Worker cleanup completed");
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
logger.error("Error during cleanup:", error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
getWorkerTransport() {
|
|
414
|
+
return this.workerTransport;
|
|
415
|
+
}
|
|
416
|
+
getWorkingDirectory() {
|
|
417
|
+
return this.workspaceManager.getCurrentWorkingDirectory();
|
|
418
|
+
}
|
|
419
|
+
async maybeRunPreCompactionMemoryFlush(params) {
|
|
420
|
+
const { session, sessionManager, settingsManager, memoryFlushConfig, incomingPromptText, incomingImageCount, runSilentPrompt, } = params;
|
|
421
|
+
if (!memoryFlushConfig.enabled) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (!settingsManager.getCompactionEnabled()) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const contextUsage = session.getContextUsage();
|
|
428
|
+
if (!contextUsage) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const reserveTokens = settingsManager.getCompactionReserveTokens();
|
|
432
|
+
const currentCompactionCount = countCompactionsOnCurrentBranch(sessionManager);
|
|
433
|
+
const lastFlushedCompactionCount = readLastFlushedCompactionCount(sessionManager);
|
|
434
|
+
if (lastFlushedCompactionCount === currentCompactionCount) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const incomingPromptTokens = estimatePromptTokenCost(incomingPromptText, incomingImageCount);
|
|
438
|
+
const thresholdTokens = contextUsage.contextWindow -
|
|
439
|
+
reserveTokens -
|
|
440
|
+
memoryFlushConfig.softThresholdTokens;
|
|
441
|
+
const projectedContextTokens = contextUsage.tokens + incomingPromptTokens;
|
|
442
|
+
if (projectedContextTokens < thresholdTokens) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const flushPrompt = `${memoryFlushConfig.systemPrompt}\n\n${memoryFlushConfig.prompt}`;
|
|
446
|
+
logger.info(`Running silent pre-compaction memory flush: projected=${projectedContextTokens}, threshold=${thresholdTokens}, compactionCount=${currentCompactionCount}`);
|
|
447
|
+
try {
|
|
448
|
+
await runSilentPrompt(flushPrompt);
|
|
449
|
+
const lastAssistant = getLatestAssistantText(session.messages);
|
|
450
|
+
const outcome = lastAssistant?.normalizedNoReply === true ? "no_reply" : "stored";
|
|
451
|
+
sessionManager.appendCustomEntry(MEMORY_FLUSH_STATE_CUSTOM_TYPE, {
|
|
452
|
+
compactionCount: currentCompactionCount,
|
|
453
|
+
outcome,
|
|
454
|
+
timestamp: Date.now(),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
logger.warn(`Silent pre-compaction memory flush failed, continuing main prompt: ${error instanceof Error ? error.message : String(error)}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
// AI session
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
async runAISession(userPrompt, customInstructions, onProgress) {
|
|
465
|
+
let rawOptions;
|
|
466
|
+
try {
|
|
467
|
+
rawOptions = JSON.parse(this.config.agentOptions);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
logger.error(`Failed to parse agentOptions: ${error instanceof Error ? error.message : String(error)}`);
|
|
471
|
+
rawOptions = {};
|
|
472
|
+
}
|
|
473
|
+
const verboseLogging = rawOptions.verboseLogging === true;
|
|
474
|
+
const memoryFlushConfig = resolveMemoryFlushConfig(rawOptions);
|
|
475
|
+
this.progressProcessor.setVerboseLogging(verboseLogging);
|
|
476
|
+
// Fetch session context BEFORE model resolution so AGENT_DEFAULT_PROVIDER
|
|
477
|
+
// is available when resolveModelRef() needs a fallback provider.
|
|
478
|
+
const context = await (0, session_context_1.getOpenClawSessionContext)();
|
|
479
|
+
// Sync enabled skills to workspace filesystem so the agent can `cat` them.
|
|
480
|
+
// Remove stale skill directories to avoid serving removed/disabled skills.
|
|
481
|
+
const skillsWorkspaceDir = this.getWorkingDirectory();
|
|
482
|
+
const skillsRoot = path.join(skillsWorkspaceDir, ".skills");
|
|
483
|
+
await fs.mkdir(skillsRoot, { recursive: true });
|
|
484
|
+
const nextSkillNames = new Set(context.skillsConfig
|
|
485
|
+
.map((skill) => path.basename((skill.name || "").trim()))
|
|
486
|
+
.filter(Boolean));
|
|
487
|
+
const existingSkillEntries = await fs
|
|
488
|
+
.readdir(skillsRoot, { withFileTypes: true })
|
|
489
|
+
.catch(() => []);
|
|
490
|
+
for (const entry of existingSkillEntries) {
|
|
491
|
+
if (!entry.isDirectory())
|
|
492
|
+
continue;
|
|
493
|
+
if (!nextSkillNames.has(entry.name)) {
|
|
494
|
+
await fs.rm(path.join(skillsRoot, entry.name), {
|
|
495
|
+
recursive: true,
|
|
496
|
+
force: true,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
for (const skill of context.skillsConfig) {
|
|
501
|
+
const skillName = path.basename((skill.name || "").trim());
|
|
502
|
+
if (!skillName)
|
|
503
|
+
continue;
|
|
504
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(skillName)) {
|
|
505
|
+
logger.warn(`Skipping skill with invalid name: ${skillName}`);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const skillDir = path.join(skillsRoot, skillName);
|
|
509
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
510
|
+
await fs.writeFile(path.join(skillDir, "SKILL.md"), skill.content, "utf-8");
|
|
511
|
+
}
|
|
512
|
+
logger.info(`Synced ${context.skillsConfig.length} skill(s) to .skills/ directory`);
|
|
513
|
+
// Store credentials in a local map instead of mutating process.env
|
|
514
|
+
// to prevent leaking secrets between sessions via persistent env vars.
|
|
515
|
+
const credentialStore = new Map();
|
|
516
|
+
const pc = context.providerConfig;
|
|
517
|
+
if (pc.credentialEnvVarName) {
|
|
518
|
+
credentialStore.set("CREDENTIAL_ENV_VAR_NAME", pc.credentialEnvVarName);
|
|
519
|
+
}
|
|
520
|
+
if (pc.providerBaseUrlMappings) {
|
|
521
|
+
for (const [envVar, url] of Object.entries(pc.providerBaseUrlMappings)) {
|
|
522
|
+
credentialStore.set(envVar, url);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (pc.credentialPlaceholders) {
|
|
526
|
+
for (const [envVar, placeholder] of Object.entries(pc.credentialPlaceholders)) {
|
|
527
|
+
credentialStore.set(envVar, placeholder);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Register config-driven providers so resolveModelRef() can handle them
|
|
531
|
+
if (pc.configProviders) {
|
|
532
|
+
for (const [id, meta] of Object.entries(pc.configProviders)) {
|
|
533
|
+
(0, model_resolver_1.registerDynamicProvider)(id, meta);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const modelRef = typeof rawOptions.model === "string" ? rawOptions.model : "";
|
|
537
|
+
const { provider: rawProvider, modelId } = (0, model_resolver_1.resolveModelRef)(modelRef, {
|
|
538
|
+
defaultModel: pc.defaultModel,
|
|
539
|
+
defaultProvider: pc.defaultProvider,
|
|
540
|
+
});
|
|
541
|
+
// Map gateway slug to model-registry provider name (e.g. "z-ai" → "zai")
|
|
542
|
+
const provider = model_resolver_1.PROVIDER_REGISTRY_ALIASES[rawProvider] || rawProvider;
|
|
543
|
+
// Dynamic provider base URL from agentOptions.providerBaseUrlMappings
|
|
544
|
+
let providerBaseUrl;
|
|
545
|
+
const dynamicMappings = rawOptions.providerBaseUrlMappings;
|
|
546
|
+
if (dynamicMappings && typeof dynamicMappings === "object") {
|
|
547
|
+
const fallbackEnvVar = model_resolver_1.DEFAULT_PROVIDER_BASE_URL_ENV[rawProvider];
|
|
548
|
+
if (fallbackEnvVar && dynamicMappings[fallbackEnvVar]) {
|
|
549
|
+
providerBaseUrl = dynamicMappings[fallbackEnvVar];
|
|
550
|
+
}
|
|
551
|
+
for (const [envVar, url] of Object.entries(dynamicMappings)) {
|
|
552
|
+
if (!credentialStore.has(envVar)) {
|
|
553
|
+
credentialStore.set(envVar, url);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!providerBaseUrl) {
|
|
558
|
+
providerBaseUrl =
|
|
559
|
+
typeof rawOptions.providerBaseUrl === "string"
|
|
560
|
+
? rawOptions.providerBaseUrl.trim() || undefined
|
|
561
|
+
: undefined;
|
|
562
|
+
}
|
|
563
|
+
if (!providerBaseUrl) {
|
|
564
|
+
const baseUrlEnvVar = model_resolver_1.DEFAULT_PROVIDER_BASE_URL_ENV[rawProvider];
|
|
565
|
+
if (baseUrlEnvVar) {
|
|
566
|
+
const baseUrlValue = credentialStore.get(baseUrlEnvVar) || process.env[baseUrlEnvVar];
|
|
567
|
+
if (baseUrlValue) {
|
|
568
|
+
providerBaseUrl = baseUrlValue;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
let baseModel = (0, pi_ai_1.getModel)(provider, modelId);
|
|
573
|
+
if (!baseModel) {
|
|
574
|
+
// For OpenAI-compatible providers (e.g. nvidia, together-ai), create a
|
|
575
|
+
// dynamic model entry since these models aren't in the static registry.
|
|
576
|
+
const registryProvider = model_resolver_1.PROVIDER_REGISTRY_ALIASES[rawProvider] || rawProvider;
|
|
577
|
+
if (registryProvider === "openai" || rawProvider !== provider) {
|
|
578
|
+
logger.info(`Creating dynamic model entry for ${rawProvider}/${modelId} (openai-compatible)`);
|
|
579
|
+
baseModel = {
|
|
580
|
+
id: modelId,
|
|
581
|
+
name: modelId,
|
|
582
|
+
api: "openai-completions",
|
|
583
|
+
provider: registryProvider,
|
|
584
|
+
baseUrl: providerBaseUrl || "https://api.openai.com/v1",
|
|
585
|
+
reasoning: false,
|
|
586
|
+
input: ["text", "image"],
|
|
587
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
588
|
+
contextWindow: 128000,
|
|
589
|
+
maxTokens: 16384,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
throw new Error(`Model "${modelId}" not found for provider "${provider}". Check that the model ID is valid and registered in the model registry.`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const model = providerBaseUrl
|
|
597
|
+
? { ...baseModel, baseUrl: providerBaseUrl }
|
|
598
|
+
: baseModel;
|
|
599
|
+
const workspaceDir = this.getWorkingDirectory();
|
|
600
|
+
await fs.mkdir(path.join(workspaceDir, ".openclaw"), { recursive: true });
|
|
601
|
+
const sessionFile = path.join(workspaceDir, ".openclaw", "session.jsonl");
|
|
602
|
+
const providerStateFile = path.join(workspaceDir, ".openclaw", "provider.json");
|
|
603
|
+
// Detect provider change and reset session if needed
|
|
604
|
+
let sessionSummary;
|
|
605
|
+
try {
|
|
606
|
+
const raw = await fs.readFile(providerStateFile, "utf-8");
|
|
607
|
+
const prevState = JSON.parse(raw);
|
|
608
|
+
if (prevState.provider && prevState.provider !== provider) {
|
|
609
|
+
logger.info(`Provider changed from ${prevState.provider} to ${provider}, resetting session`);
|
|
610
|
+
// Read old session content for summary context
|
|
611
|
+
try {
|
|
612
|
+
const sessionContent = await fs.readFile(sessionFile, "utf-8");
|
|
613
|
+
const lineCount = sessionContent.split("\n").filter(Boolean).length;
|
|
614
|
+
if (lineCount > 0) {
|
|
615
|
+
// Provide a brief context note instead of a full summary
|
|
616
|
+
// to avoid an expensive API call to the new model
|
|
617
|
+
sessionSummary = `[System note: The AI provider was just changed from ${prevState.provider} to ${provider}. Previous conversation history (${lineCount} turns) has been cleared. Continue helping the user from this point forward.]`;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
// No existing session file
|
|
622
|
+
}
|
|
623
|
+
// Delete old session file to start fresh
|
|
624
|
+
try {
|
|
625
|
+
await fs.unlink(sessionFile);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
// File may not exist
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
// Log a warning for parse failures (vs. missing file which is expected on first run)
|
|
634
|
+
const isFileNotFound = error instanceof Error &&
|
|
635
|
+
error.code === "ENOENT";
|
|
636
|
+
if (!isFileNotFound) {
|
|
637
|
+
logger.warn(`Failed to read provider state file: ${error instanceof Error ? error.message : String(error)}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// Persist current provider state
|
|
641
|
+
await fs.writeFile(providerStateFile, JSON.stringify({ provider, modelId }), "utf-8");
|
|
642
|
+
const sessionManager = await (0, model_resolver_1.openOrCreateSessionManager)(sessionFile, workspaceDir);
|
|
643
|
+
const settingsManager = pi_coding_agent_1.SettingsManager.inMemory();
|
|
644
|
+
const toolsPolicy = (0, tool_policy_1.buildToolPolicy)({
|
|
645
|
+
toolsConfig: rawOptions.toolsConfig,
|
|
646
|
+
allowedTools: rawOptions.allowedTools,
|
|
647
|
+
disallowedTools: rawOptions.disallowedTools,
|
|
648
|
+
});
|
|
649
|
+
let embeddedBashOps;
|
|
650
|
+
if (process.env.DEPLOYMENT_MODE === "embedded") {
|
|
651
|
+
const { createEmbeddedBashOps } = await Promise.resolve().then(() => __importStar(require("../embedded/just-bash-bootstrap")));
|
|
652
|
+
embeddedBashOps = await createEmbeddedBashOps();
|
|
653
|
+
}
|
|
654
|
+
let tools = (0, tools_1.createOpenClawTools)(workspaceDir, {
|
|
655
|
+
bashOperations: embeddedBashOps,
|
|
656
|
+
}).filter((tool) => (0, tool_policy_1.isToolAllowedByPolicy)(tool.name, toolsPolicy));
|
|
657
|
+
if (toolsPolicy.bashPolicy.allowPrefixes.length > 0 ||
|
|
658
|
+
toolsPolicy.bashPolicy.denyPrefixes.length > 0) {
|
|
659
|
+
tools = tools.map((tool) => {
|
|
660
|
+
if (tool.name !== "bash") {
|
|
661
|
+
return tool;
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
...tool,
|
|
665
|
+
execute: async (toolCallId, params, signal, onUpdate) => {
|
|
666
|
+
const command = params && typeof params === "object" && "command" in params
|
|
667
|
+
? String(params.command ?? "")
|
|
668
|
+
: "";
|
|
669
|
+
(0, tool_policy_1.enforceBashCommandPolicy)(command, toolsPolicy.bashPolicy);
|
|
670
|
+
return tool.execute(toolCallId, params, signal, onUpdate);
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
const gatewayUrl = process.env.DISPATCHER_URL ?? "";
|
|
676
|
+
const workerToken = process.env.WORKER_TOKEN ?? "";
|
|
677
|
+
// Credential injection — resolve API key from the in-memory credential store,
|
|
678
|
+
// falling back to process.env only for values that were present at startup.
|
|
679
|
+
const authStorage = new pi_coding_agent_1.AuthStorage();
|
|
680
|
+
const credEnvVar = credentialStore.get("CREDENTIAL_ENV_VAR_NAME") || null;
|
|
681
|
+
const credValue = credEnvVar
|
|
682
|
+
? credentialStore.get(credEnvVar) || process.env[credEnvVar]
|
|
683
|
+
: null;
|
|
684
|
+
if (credEnvVar && credValue) {
|
|
685
|
+
authStorage.setRuntimeApiKey(provider, credValue);
|
|
686
|
+
logger.info(`Set runtime API key for ${provider}`);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
const fallbackEnvVar = (0, provider_auth_hints_1.getApiKeyEnvVarForProvider)(provider);
|
|
690
|
+
const fallbackValue = credentialStore.get(fallbackEnvVar) || process.env[fallbackEnvVar];
|
|
691
|
+
if (fallbackValue) {
|
|
692
|
+
authStorage.setRuntimeApiKey(provider, fallbackValue);
|
|
693
|
+
logger.info(`Set runtime API key for ${provider}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Re-resolve provider base URL after session context may have updated mappings
|
|
697
|
+
if (!providerBaseUrl) {
|
|
698
|
+
const baseUrlEnvVar = model_resolver_1.DEFAULT_PROVIDER_BASE_URL_ENV[rawProvider];
|
|
699
|
+
if (baseUrlEnvVar) {
|
|
700
|
+
const baseUrlValue = credentialStore.get(baseUrlEnvVar) || process.env[baseUrlEnvVar];
|
|
701
|
+
if (baseUrlValue) {
|
|
702
|
+
providerBaseUrl = baseUrlValue;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// Merge gateway instructions into custom instructions
|
|
707
|
+
const instructionParts = [context.gatewayInstructions, customInstructions];
|
|
708
|
+
// Prefer CLI backends from dynamic session context, fall back to env var
|
|
709
|
+
let cliBackendsFromEnv;
|
|
710
|
+
if (!pc.cliBackends?.length && process.env.CLI_BACKENDS) {
|
|
711
|
+
try {
|
|
712
|
+
cliBackendsFromEnv = JSON.parse(process.env.CLI_BACKENDS);
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
logger.error(`Failed to parse CLI_BACKENDS env var: ${error instanceof Error ? error.message : String(error)}`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const cliBackends = pc.cliBackends?.length
|
|
719
|
+
? pc.cliBackends
|
|
720
|
+
: cliBackendsFromEnv;
|
|
721
|
+
if (cliBackends?.length) {
|
|
722
|
+
const agentList = cliBackends
|
|
723
|
+
.map((b) => {
|
|
724
|
+
const cmd = `${b.command} ${(b.args || []).join(" ")}`;
|
|
725
|
+
const aliases = [b.name, b.providerId].filter((v, i, a) => v && a.indexOf(v) === i);
|
|
726
|
+
return `### ${aliases.join(" / ")}
|
|
727
|
+
Run via Bash exactly as shown (do NOT modify the command):
|
|
728
|
+
\`\`\`bash
|
|
729
|
+
${cmd} "YOUR_PROMPT_HERE"
|
|
730
|
+
\`\`\``;
|
|
731
|
+
})
|
|
732
|
+
.join("\n\n");
|
|
733
|
+
instructionParts.push(`## Available Coding Agents
|
|
734
|
+
|
|
735
|
+
You have access to the following AI coding agents. When the user mentions any of these by name (e.g. "use claude", "ask chatgpt"), you MUST run the exact command shown below via the Bash tool. Do NOT attempt to install or locate the CLI yourself — the command handles everything.
|
|
736
|
+
|
|
737
|
+
${agentList}
|
|
738
|
+
|
|
739
|
+
Replace "YOUR_PROMPT_HERE" with the user's request. These agents can read/write files, install packages, and run commands in the working directory.`);
|
|
740
|
+
}
|
|
741
|
+
instructionParts.push(`## Conversation History
|
|
742
|
+
|
|
743
|
+
You have access to GetChannelHistory to view previous messages in this thread.
|
|
744
|
+
Use it when the user references past discussions or you need context.`);
|
|
745
|
+
const gwParams = {
|
|
746
|
+
gatewayUrl,
|
|
747
|
+
workerToken,
|
|
748
|
+
channelId: this.config.channelId,
|
|
749
|
+
conversationId: this.config.conversationId,
|
|
750
|
+
platform: this.config.platform,
|
|
751
|
+
};
|
|
752
|
+
const customTools = (0, custom_tools_1.createOpenClawCustomTools)(gwParams);
|
|
753
|
+
// Register MCP tools as first-class callable tools (alongside virtual memory wrappers)
|
|
754
|
+
const mcpToolDefs = (0, custom_tools_1.createMcpToolDefinitions)(context.mcpTools, gwParams, context.mcpContext);
|
|
755
|
+
if (mcpToolDefs.length > 0) {
|
|
756
|
+
customTools.push(...mcpToolDefs);
|
|
757
|
+
logger.info(`Registered ${mcpToolDefs.length} MCP tool(s): ${mcpToolDefs.map((t) => t.name).join(", ")}`);
|
|
758
|
+
}
|
|
759
|
+
// Load OpenClaw plugins
|
|
760
|
+
const pluginsConfig = rawOptions.pluginsConfig;
|
|
761
|
+
const loadedPlugins = await (0, plugin_loader_1.loadPlugins)(pluginsConfig, workspaceDir);
|
|
762
|
+
const pluginTools = loadedPlugins.flatMap((p) => p.tools);
|
|
763
|
+
if (pluginTools.length > 0) {
|
|
764
|
+
customTools.push(...pluginTools);
|
|
765
|
+
logger.info(`Loaded ${pluginTools.length} tool(s) from ${loadedPlugins.length} plugin(s)`);
|
|
766
|
+
}
|
|
767
|
+
// Apply plugin provider registrations to ModelRegistry
|
|
768
|
+
const modelRegistry = new pi_coding_agent_1.ModelRegistry(authStorage);
|
|
769
|
+
const allProviders = loadedPlugins.flatMap((p) => p.providers);
|
|
770
|
+
for (const reg of allProviders) {
|
|
771
|
+
try {
|
|
772
|
+
modelRegistry.registerProvider(reg.name, reg.config);
|
|
773
|
+
logger.info(`Registered provider "${reg.name}" from plugin`);
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
logger.error(`Failed to register provider "${reg.name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
await (0, plugin_loader_1.startPluginServices)(loadedPlugins);
|
|
780
|
+
// Proactive owletto login: call owletto_login to check auth status.
|
|
781
|
+
// If not authenticated, inject the login link into instructions so the model
|
|
782
|
+
// can relay it to the user without needing to call tools itself.
|
|
783
|
+
{
|
|
784
|
+
const loginTool = pluginTools.find((t) => t.name === "owletto_login");
|
|
785
|
+
if (loginTool) {
|
|
786
|
+
logger.info("Checking Owletto auth status via proactive login");
|
|
787
|
+
try {
|
|
788
|
+
const loginResult = await loginTool.execute("proactive-login", {}, undefined, undefined, undefined);
|
|
789
|
+
const resultText = loginResult?.content
|
|
790
|
+
?.filter((c) => c.type === "text" && typeof c.text === "string")
|
|
791
|
+
.map((c) => c.text)
|
|
792
|
+
.join("\n") || "";
|
|
793
|
+
logger.info(`Owletto login result: ${resultText.slice(0, 200)}`);
|
|
794
|
+
const parsed = JSON.parse(resultText);
|
|
795
|
+
if (parsed.status === "login_started" && parsed.verification_url) {
|
|
796
|
+
// Send login link directly to the user — don't rely on the model
|
|
797
|
+
const loginMessage = `🔑 Memory requires login. Open this link to connect:\n${parsed.verification_url}${parsed.user_code ? `\nCode: ${parsed.user_code}` : ""}\n\n`;
|
|
798
|
+
await onProgress({
|
|
799
|
+
type: "output",
|
|
800
|
+
data: loginMessage,
|
|
801
|
+
timestamp: Date.now(),
|
|
802
|
+
});
|
|
803
|
+
logger.info("Proactive owletto login started — login link sent directly to user");
|
|
804
|
+
instructionParts.push(`\n\n## Owletto Login In Progress\nThe login link and code have already been sent directly to the user by the system. Do not repeat the verification URL or code unless the user explicitly asks. Do not call owletto_login again while authentication is pending. If the current request depends on Owletto, tell the user briefly to complete login and reply once done.`);
|
|
805
|
+
}
|
|
806
|
+
else if (parsed.status === "already_authenticated") {
|
|
807
|
+
logger.info("Owletto already authenticated");
|
|
808
|
+
}
|
|
809
|
+
else if (parsed.status === "error") {
|
|
810
|
+
logger.warn(`Owletto login returned error: ${parsed.message}`);
|
|
811
|
+
instructionParts.push(`\n\n## Owletto Memory Not Connected\nOwletto memory is not connected and login could not be started automatically. Tell the user they need to connect their Owletto memory. If owletto_login tool is available, call it to try again. Otherwise tell them an admin or an existing auth flow is required.`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
catch (err) {
|
|
815
|
+
logger.warn(`Proactive owletto login failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
816
|
+
instructionParts.push(`\n\n## Owletto Memory Not Connected\nOwletto memory is not connected. Tell the user they need to connect their memory via Owletto. If the owletto_login tool is available, call it to start the login flow.`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
// Rebuild final instructions after possible login link injection
|
|
821
|
+
const finalInstructionsUpdated = instructionParts
|
|
822
|
+
.filter(Boolean)
|
|
823
|
+
.join("\n\n");
|
|
824
|
+
logger.info(`Starting OpenClaw session: provider=${provider}, model=${modelId}, tools=${tools.length}, customTools=${customTools.length}`);
|
|
825
|
+
// Heartbeat timer to keep connection alive during long API calls
|
|
826
|
+
const HEARTBEAT_INTERVAL_MS = 20000;
|
|
827
|
+
let heartbeatTimer = null;
|
|
828
|
+
let deltaTimer = null;
|
|
829
|
+
let session = null;
|
|
830
|
+
const pluginHookContext = {
|
|
831
|
+
cwd: workspaceDir,
|
|
832
|
+
sessionKey: this.config.sessionKey,
|
|
833
|
+
messageProvider: this.config.platform,
|
|
834
|
+
};
|
|
835
|
+
try {
|
|
836
|
+
const createdSession = await (0, pi_coding_agent_1.createAgentSession)({
|
|
837
|
+
cwd: workspaceDir,
|
|
838
|
+
model,
|
|
839
|
+
tools,
|
|
840
|
+
customTools,
|
|
841
|
+
sessionManager,
|
|
842
|
+
settingsManager,
|
|
843
|
+
authStorage,
|
|
844
|
+
modelRegistry,
|
|
845
|
+
});
|
|
846
|
+
session = createdSession.session;
|
|
847
|
+
const basePrompt = session.systemPrompt;
|
|
848
|
+
session.agent.setSystemPrompt(`${basePrompt}\n\n${finalInstructionsUpdated}`);
|
|
849
|
+
let resolveTurnDone = null;
|
|
850
|
+
let turnNonce = 0;
|
|
851
|
+
let suppressProgressOutput = false;
|
|
852
|
+
// Wire events through progress processor with delta batching
|
|
853
|
+
let pendingDelta = "";
|
|
854
|
+
const DELTA_BATCH_INTERVAL_MS = 150;
|
|
855
|
+
const flushDelta = async () => {
|
|
856
|
+
if (pendingDelta) {
|
|
857
|
+
const toSend = pendingDelta;
|
|
858
|
+
pendingDelta = "";
|
|
859
|
+
await onProgress({
|
|
860
|
+
type: "output",
|
|
861
|
+
data: toSend,
|
|
862
|
+
timestamp: Date.now(),
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
if (deltaTimer) {
|
|
866
|
+
clearTimeout(deltaTimer);
|
|
867
|
+
deltaTimer = null;
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
const scheduleDeltaFlush = () => {
|
|
871
|
+
if (!deltaTimer) {
|
|
872
|
+
deltaTimer = setTimeout(() => {
|
|
873
|
+
flushDelta().catch((err) => {
|
|
874
|
+
logger.error("Failed to flush delta:", err);
|
|
875
|
+
});
|
|
876
|
+
}, DELTA_BATCH_INTERVAL_MS);
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
const runPromptTurn = async (promptText, options) => {
|
|
880
|
+
const currentSession = session;
|
|
881
|
+
if (!currentSession) {
|
|
882
|
+
throw new Error("OpenClaw session is not initialized");
|
|
883
|
+
}
|
|
884
|
+
turnNonce += 1;
|
|
885
|
+
const currentTurnNonce = turnNonce;
|
|
886
|
+
const turnDone = new Promise((resolve) => {
|
|
887
|
+
resolveTurnDone = () => {
|
|
888
|
+
if (currentTurnNonce !== turnNonce) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
resolveTurnDone = null;
|
|
892
|
+
resolve();
|
|
893
|
+
};
|
|
894
|
+
});
|
|
895
|
+
suppressProgressOutput = options?.silent === true;
|
|
896
|
+
try {
|
|
897
|
+
if (options?.images) {
|
|
898
|
+
await currentSession.prompt(promptText, { images: options.images });
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
await currentSession.prompt(promptText);
|
|
902
|
+
}
|
|
903
|
+
await turnDone;
|
|
904
|
+
}
|
|
905
|
+
finally {
|
|
906
|
+
suppressProgressOutput = false;
|
|
907
|
+
if (resolveTurnDone && currentTurnNonce === turnNonce) {
|
|
908
|
+
resolveTurnDone = null;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
session.subscribe((event) => {
|
|
913
|
+
if (suppressProgressOutput) {
|
|
914
|
+
if (event.type === "agent_end") {
|
|
915
|
+
resolveTurnDone?.();
|
|
916
|
+
}
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const hasUpdate = this.progressProcessor.processEvent(event);
|
|
920
|
+
if (hasUpdate) {
|
|
921
|
+
const delta = this.progressProcessor.getDelta();
|
|
922
|
+
if (delta) {
|
|
923
|
+
pendingDelta += delta;
|
|
924
|
+
scheduleDeltaFlush();
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (event.type === "agent_end") {
|
|
928
|
+
flushDelta()
|
|
929
|
+
.then(() => resolveTurnDone?.())
|
|
930
|
+
.catch((err) => {
|
|
931
|
+
logger.error("Failed to flush final delta:", err);
|
|
932
|
+
resolveTurnDone?.();
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
let elapsedTime = 0;
|
|
937
|
+
let lastHeartbeatTime = Date.now();
|
|
938
|
+
const MAX_CONSECUTIVE_HEARTBEAT_FAILURES = 5;
|
|
939
|
+
let consecutiveHeartbeatFailures = 0;
|
|
940
|
+
const sendHeartbeat = async () => {
|
|
941
|
+
const now = Date.now();
|
|
942
|
+
elapsedTime += now - lastHeartbeatTime;
|
|
943
|
+
lastHeartbeatTime = now;
|
|
944
|
+
const seconds = Math.floor(elapsedTime / 1000);
|
|
945
|
+
logger.warn(`⏳ Still running after ${seconds}s - no response from API yet`);
|
|
946
|
+
await onProgress({
|
|
947
|
+
type: "status_update",
|
|
948
|
+
data: {
|
|
949
|
+
elapsedSeconds: seconds,
|
|
950
|
+
state: "is running..",
|
|
951
|
+
},
|
|
952
|
+
timestamp: Date.now(),
|
|
953
|
+
});
|
|
954
|
+
};
|
|
955
|
+
heartbeatTimer = setInterval(() => {
|
|
956
|
+
sendHeartbeat()
|
|
957
|
+
.then(() => {
|
|
958
|
+
consecutiveHeartbeatFailures = 0;
|
|
959
|
+
})
|
|
960
|
+
.catch((err) => {
|
|
961
|
+
consecutiveHeartbeatFailures += 1;
|
|
962
|
+
logger.error(`Failed to send heartbeat (${consecutiveHeartbeatFailures}/${MAX_CONSECUTIVE_HEARTBEAT_FAILURES}):`, err);
|
|
963
|
+
if (consecutiveHeartbeatFailures >= MAX_CONSECUTIVE_HEARTBEAT_FAILURES) {
|
|
964
|
+
logger.error("Gateway unresponsive after consecutive heartbeat failures, aborting session");
|
|
965
|
+
if (heartbeatTimer) {
|
|
966
|
+
clearInterval(heartbeatTimer);
|
|
967
|
+
heartbeatTimer = null;
|
|
968
|
+
}
|
|
969
|
+
if (session) {
|
|
970
|
+
session.dispose();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
975
|
+
// Session reset: run unconditional memory flush, delete session file, and return early
|
|
976
|
+
if (this.config.platformMetadata?.sessionReset === true) {
|
|
977
|
+
logger.info("Session reset requested — running unconditional memory flush");
|
|
978
|
+
const flushPrompt = `${memoryFlushConfig.systemPrompt}\n\n${memoryFlushConfig.prompt}`;
|
|
979
|
+
try {
|
|
980
|
+
await runPromptTurn(flushPrompt, { silent: true });
|
|
981
|
+
logger.info("Memory flush completed for session reset");
|
|
982
|
+
}
|
|
983
|
+
catch (error) {
|
|
984
|
+
logger.warn(`Memory flush failed during session reset: ${error instanceof Error ? error.message : String(error)}`);
|
|
985
|
+
}
|
|
986
|
+
// Delete session file so next run starts with a clean history
|
|
987
|
+
try {
|
|
988
|
+
await fs.unlink(sessionFile);
|
|
989
|
+
logger.info("Deleted session file for session reset");
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
// File may not exist
|
|
993
|
+
}
|
|
994
|
+
// Send visible confirmation to user
|
|
995
|
+
await onProgress({
|
|
996
|
+
type: "output",
|
|
997
|
+
data: "Context saved. Starting fresh.",
|
|
998
|
+
timestamp: Date.now(),
|
|
999
|
+
});
|
|
1000
|
+
if (heartbeatTimer)
|
|
1001
|
+
clearInterval(heartbeatTimer);
|
|
1002
|
+
if (deltaTimer)
|
|
1003
|
+
clearTimeout(deltaTimer);
|
|
1004
|
+
await (0, plugin_loader_1.stopPluginServices)(loadedPlugins);
|
|
1005
|
+
return {
|
|
1006
|
+
success: true,
|
|
1007
|
+
exitCode: 0,
|
|
1008
|
+
output: "",
|
|
1009
|
+
sessionKey: this.config.sessionKey,
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
// Consume any pending config change notifications from SSE events
|
|
1013
|
+
const { consumePendingConfigNotifications } = await Promise.resolve().then(() => __importStar(require("../gateway/sse-client")));
|
|
1014
|
+
const configNotifications = consumePendingConfigNotifications();
|
|
1015
|
+
let configNotice = "";
|
|
1016
|
+
if (configNotifications.length > 0) {
|
|
1017
|
+
const lines = configNotifications.map((n) => {
|
|
1018
|
+
let line = `- ${n.summary}`;
|
|
1019
|
+
if (n.details?.length) {
|
|
1020
|
+
line += `: ${n.details.join("; ")}`;
|
|
1021
|
+
}
|
|
1022
|
+
return line;
|
|
1023
|
+
});
|
|
1024
|
+
configNotice = `[System notice: Your configuration was updated since the last message]\n${lines.join("\n")}\n\n`;
|
|
1025
|
+
}
|
|
1026
|
+
const beforeAgentStartResults = await (0, plugin_loader_1.runPluginHooks)({
|
|
1027
|
+
plugins: loadedPlugins,
|
|
1028
|
+
hook: "before_agent_start",
|
|
1029
|
+
event: {
|
|
1030
|
+
prompt: userPrompt,
|
|
1031
|
+
messages: session.messages,
|
|
1032
|
+
},
|
|
1033
|
+
ctx: pluginHookContext,
|
|
1034
|
+
});
|
|
1035
|
+
const prependContexts = beforeAgentStartResults
|
|
1036
|
+
.flatMap((result) => {
|
|
1037
|
+
if (!result || typeof result !== "object")
|
|
1038
|
+
return [];
|
|
1039
|
+
const prepend = result.prependContext;
|
|
1040
|
+
if (typeof prepend !== "string" || !prepend.trim())
|
|
1041
|
+
return [];
|
|
1042
|
+
return [prepend.trim()];
|
|
1043
|
+
})
|
|
1044
|
+
.join("\n\n");
|
|
1045
|
+
const effectivePromptText = `${configNotice}${sessionSummary ? `${sessionSummary}\n\n` : ""}${prependContexts ? `${prependContexts}\n\n` : ""}${userPrompt}`;
|
|
1046
|
+
// Load image attachments for vision-capable models
|
|
1047
|
+
const images = await this.loadImageAttachments();
|
|
1048
|
+
if (images.length > 0) {
|
|
1049
|
+
logger.info(`Including ${images.length} image(s) in prompt for vision`);
|
|
1050
|
+
}
|
|
1051
|
+
await this.maybeRunPreCompactionMemoryFlush({
|
|
1052
|
+
session,
|
|
1053
|
+
sessionManager,
|
|
1054
|
+
settingsManager,
|
|
1055
|
+
memoryFlushConfig,
|
|
1056
|
+
incomingPromptText: effectivePromptText,
|
|
1057
|
+
incomingImageCount: images.length,
|
|
1058
|
+
runSilentPrompt: async (prompt) => {
|
|
1059
|
+
await runPromptTurn(prompt, { silent: true });
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
await runPromptTurn(effectivePromptText, { images });
|
|
1063
|
+
const sessionError = this.progressProcessor.consumeFatalErrorMessage();
|
|
1064
|
+
if (sessionError) {
|
|
1065
|
+
await (0, plugin_loader_1.runPluginHooks)({
|
|
1066
|
+
plugins: loadedPlugins,
|
|
1067
|
+
hook: "agent_end",
|
|
1068
|
+
event: {
|
|
1069
|
+
success: false,
|
|
1070
|
+
error: sessionError,
|
|
1071
|
+
messages: session.messages,
|
|
1072
|
+
},
|
|
1073
|
+
ctx: pluginHookContext,
|
|
1074
|
+
});
|
|
1075
|
+
const errorWithHint = await this.maybeBuildAuthHintMessage(sessionError, provider, modelId, gatewayUrl, workerToken);
|
|
1076
|
+
return {
|
|
1077
|
+
success: false,
|
|
1078
|
+
exitCode: 1,
|
|
1079
|
+
output: "",
|
|
1080
|
+
error: errorWithHint,
|
|
1081
|
+
sessionKey: this.config.sessionKey,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
await (0, plugin_loader_1.runPluginHooks)({
|
|
1085
|
+
plugins: loadedPlugins,
|
|
1086
|
+
hook: "agent_end",
|
|
1087
|
+
event: {
|
|
1088
|
+
success: true,
|
|
1089
|
+
messages: session.messages,
|
|
1090
|
+
},
|
|
1091
|
+
ctx: pluginHookContext,
|
|
1092
|
+
});
|
|
1093
|
+
return {
|
|
1094
|
+
success: true,
|
|
1095
|
+
exitCode: 0,
|
|
1096
|
+
output: "",
|
|
1097
|
+
sessionKey: this.config.sessionKey,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1102
|
+
if (session) {
|
|
1103
|
+
await (0, plugin_loader_1.runPluginHooks)({
|
|
1104
|
+
plugins: loadedPlugins,
|
|
1105
|
+
hook: "agent_end",
|
|
1106
|
+
event: {
|
|
1107
|
+
success: false,
|
|
1108
|
+
error: errorMsg,
|
|
1109
|
+
messages: session.messages,
|
|
1110
|
+
},
|
|
1111
|
+
ctx: pluginHookContext,
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
const errorWithHint = await this.maybeBuildAuthHintMessage(errorMsg, provider, modelId, gatewayUrl, workerToken);
|
|
1115
|
+
return {
|
|
1116
|
+
success: false,
|
|
1117
|
+
exitCode: 1,
|
|
1118
|
+
output: "",
|
|
1119
|
+
error: errorWithHint,
|
|
1120
|
+
sessionKey: this.config.sessionKey,
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
finally {
|
|
1124
|
+
if (heartbeatTimer) {
|
|
1125
|
+
clearInterval(heartbeatTimer);
|
|
1126
|
+
heartbeatTimer = null;
|
|
1127
|
+
logger.debug("Heartbeat timer cleared");
|
|
1128
|
+
}
|
|
1129
|
+
if (deltaTimer) {
|
|
1130
|
+
clearTimeout(deltaTimer);
|
|
1131
|
+
deltaTimer = null;
|
|
1132
|
+
logger.debug("Delta batch timer cleared");
|
|
1133
|
+
}
|
|
1134
|
+
if (session) {
|
|
1135
|
+
session.dispose();
|
|
1136
|
+
session = null;
|
|
1137
|
+
}
|
|
1138
|
+
await (0, plugin_loader_1.stopPluginServices)(loadedPlugins);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
// ---------------------------------------------------------------------------
|
|
1142
|
+
// Helpers
|
|
1143
|
+
// ---------------------------------------------------------------------------
|
|
1144
|
+
async setupIODirectories() {
|
|
1145
|
+
const workspaceDir = this.workspaceManager.getCurrentWorkingDirectory();
|
|
1146
|
+
const inputDir = path.join(workspaceDir, "input");
|
|
1147
|
+
const outputDir = path.join(workspaceDir, "output");
|
|
1148
|
+
const tempDir = path.join(workspaceDir, "temp");
|
|
1149
|
+
await fs.mkdir(inputDir, { recursive: true });
|
|
1150
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
1151
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
1152
|
+
try {
|
|
1153
|
+
const files = await fs.readdir(outputDir);
|
|
1154
|
+
for (const file of files) {
|
|
1155
|
+
await fs.unlink(path.join(outputDir, file)).catch(() => {
|
|
1156
|
+
/* intentionally empty */
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
logger.debug("Could not clear output directory:", error);
|
|
1162
|
+
}
|
|
1163
|
+
logger.info("I/O directories setup completed");
|
|
1164
|
+
}
|
|
1165
|
+
async downloadInputFiles() {
|
|
1166
|
+
const files = this.uploadedFiles;
|
|
1167
|
+
if (files.length === 0) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
logger.info(`Downloading ${files.length} input files...`);
|
|
1171
|
+
const workspaceDir = this.workspaceManager.getCurrentWorkingDirectory();
|
|
1172
|
+
const inputDir = path.join(workspaceDir, "input");
|
|
1173
|
+
const dispatcherUrl = process.env.DISPATCHER_URL;
|
|
1174
|
+
const workerToken = process.env.WORKER_TOKEN;
|
|
1175
|
+
for (const file of files) {
|
|
1176
|
+
try {
|
|
1177
|
+
logger.info(`Downloading file: ${file.name} (${file.id})`);
|
|
1178
|
+
const response = await fetch(`${dispatcherUrl}/internal/files/download?fileId=${file.id}`, {
|
|
1179
|
+
headers: {
|
|
1180
|
+
Authorization: `Bearer ${workerToken}`,
|
|
1181
|
+
},
|
|
1182
|
+
signal: AbortSignal.timeout(60_000),
|
|
1183
|
+
});
|
|
1184
|
+
if (!response.ok) {
|
|
1185
|
+
logger.error(`Failed to download file ${file.name}: ${response.statusText}`);
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
// Sanitize file name to prevent path traversal
|
|
1189
|
+
const safeName = path.basename(file.name);
|
|
1190
|
+
if (!safeName || safeName === "." || safeName === "..") {
|
|
1191
|
+
logger.warn(`Skipping file with invalid name: ${file.name}`);
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
if (safeName !== file.name) {
|
|
1195
|
+
logger.warn(`Sanitized file name from "${file.name}" to "${safeName}"`);
|
|
1196
|
+
}
|
|
1197
|
+
if (!response.body) {
|
|
1198
|
+
logger.error(`Response body is null for file ${safeName}`);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
const destPath = path.join(inputDir, safeName);
|
|
1202
|
+
const fileStream = node_stream_1.Readable.fromWeb(response.body);
|
|
1203
|
+
const writeStream = (await Promise.resolve().then(() => __importStar(require("node:fs")))).createWriteStream(destPath);
|
|
1204
|
+
await (0, promises_1.pipeline)(fileStream, writeStream);
|
|
1205
|
+
logger.info(`Downloaded: ${safeName} to input directory`);
|
|
1206
|
+
}
|
|
1207
|
+
catch (error) {
|
|
1208
|
+
logger.error(`Error downloading file ${file.name}:`, error);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
get uploadedFiles() {
|
|
1213
|
+
return this.config.platformMetadata?.files || [];
|
|
1214
|
+
}
|
|
1215
|
+
static isImage(mimetype) {
|
|
1216
|
+
return !!mimetype?.startsWith("image/");
|
|
1217
|
+
}
|
|
1218
|
+
getFileIOInstructions() {
|
|
1219
|
+
const workspaceDir = this.workspaceManager.getCurrentWorkingDirectory();
|
|
1220
|
+
const files = this.uploadedFiles;
|
|
1221
|
+
if (files.length === 0) {
|
|
1222
|
+
return `
|
|
1223
|
+
|
|
1224
|
+
## File Generation & Output
|
|
1225
|
+
|
|
1226
|
+
**When to Create Files:**
|
|
1227
|
+
Create and show files for any output that helps answer the user's request by using \`UploadUserFile\` tool:
|
|
1228
|
+
- **Charts & visualizations**: pie charts, bar graphs, plots, diagrams via \`matplotlib\`
|
|
1229
|
+
- **Reports & documents**: analysis reports, summaries, PDFs
|
|
1230
|
+
- **Data files**: CSV exports, JSON data, spreadsheets
|
|
1231
|
+
- **Code files**: scripts, configurations, examples
|
|
1232
|
+
- **Images**: generated images, processed photos, screenshots.
|
|
1233
|
+
`;
|
|
1234
|
+
}
|
|
1235
|
+
const fileListing = files
|
|
1236
|
+
.map((f) => `- \`${workspaceDir}/input/${f.name}\` (${f.mimetype || "unknown type"})`)
|
|
1237
|
+
.join("\n");
|
|
1238
|
+
const hasImages = files.some((f) => OpenClawWorker.isImage(f.mimetype));
|
|
1239
|
+
const hasNonImages = files.some((f) => !OpenClawWorker.isImage(f.mimetype));
|
|
1240
|
+
let hints = "";
|
|
1241
|
+
if (hasImages) {
|
|
1242
|
+
hints +=
|
|
1243
|
+
"\nImage files have been included directly in this message for visual analysis.";
|
|
1244
|
+
}
|
|
1245
|
+
if (hasNonImages) {
|
|
1246
|
+
hints +=
|
|
1247
|
+
"\nYou can read non-image files with standard commands like `cat`, `less`, or `head`.";
|
|
1248
|
+
}
|
|
1249
|
+
return `
|
|
1250
|
+
|
|
1251
|
+
## File Generation & Output
|
|
1252
|
+
|
|
1253
|
+
**When to Create Files:**
|
|
1254
|
+
Create and show files for any output that helps answer the user's request by using \`UploadUserFile\` tool:
|
|
1255
|
+
- **Charts & visualizations**: pie charts, bar graphs, plots, diagrams via \`matplotlib\`
|
|
1256
|
+
- **Reports & documents**: analysis reports, summaries, PDFs
|
|
1257
|
+
- **Data files**: CSV exports, JSON data, spreadsheets
|
|
1258
|
+
- **Code files**: scripts, configurations, examples
|
|
1259
|
+
- **Images**: generated images, processed photos, screenshots.
|
|
1260
|
+
|
|
1261
|
+
### User-Uploaded Files
|
|
1262
|
+
The user has uploaded ${files.length} file(s) for you to analyze:
|
|
1263
|
+
${fileListing}
|
|
1264
|
+
|
|
1265
|
+
**Use these files to answer the user's request.**${hints}
|
|
1266
|
+
`;
|
|
1267
|
+
}
|
|
1268
|
+
/** Max image size to embed in prompt (20 MB). Larger files are skipped. */
|
|
1269
|
+
static MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
1270
|
+
async loadImageAttachments() {
|
|
1271
|
+
const imageFiles = this.uploadedFiles.filter((f) => OpenClawWorker.isImage(f.mimetype));
|
|
1272
|
+
if (imageFiles.length === 0)
|
|
1273
|
+
return [];
|
|
1274
|
+
const inputDir = path.join(this.workspaceManager.getCurrentWorkingDirectory(), "input");
|
|
1275
|
+
const results = [];
|
|
1276
|
+
for (const file of imageFiles) {
|
|
1277
|
+
try {
|
|
1278
|
+
// Sanitize file name to prevent path traversal
|
|
1279
|
+
const safeName = path.basename(file.name);
|
|
1280
|
+
if (!safeName || safeName === "." || safeName === "..") {
|
|
1281
|
+
logger.warn(`Skipping image with invalid name: ${file.name}`);
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (safeName !== file.name) {
|
|
1285
|
+
logger.warn(`Sanitized image file name from "${file.name}" to "${safeName}"`);
|
|
1286
|
+
}
|
|
1287
|
+
const data = await fs.readFile(path.join(inputDir, safeName));
|
|
1288
|
+
if (data.length > OpenClawWorker.MAX_IMAGE_BYTES) {
|
|
1289
|
+
logger.warn(`Skipping image ${file.name}: ${Math.round(data.length / 1024 / 1024)}MB exceeds limit`);
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
results.push({
|
|
1293
|
+
type: "image",
|
|
1294
|
+
data: data.toString("base64"),
|
|
1295
|
+
mimeType: file.mimetype,
|
|
1296
|
+
});
|
|
1297
|
+
logger.info(`Loaded image: ${file.name} (${file.mimetype}, ${Math.round(data.length / 1024)}KB)`);
|
|
1298
|
+
}
|
|
1299
|
+
catch (error) {
|
|
1300
|
+
logger.warn(`Failed to load image ${file.name}:`, error);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return results;
|
|
1304
|
+
}
|
|
1305
|
+
async maybeBuildAuthHintMessage(errorMessage, provider, modelId, gatewayUrl, workerToken) {
|
|
1306
|
+
void gatewayUrl;
|
|
1307
|
+
void workerToken;
|
|
1308
|
+
const authHint = (0, provider_auth_hints_1.getProviderAuthHintFromError)(errorMessage, provider);
|
|
1309
|
+
if (!authHint) {
|
|
1310
|
+
return errorMessage;
|
|
1311
|
+
}
|
|
1312
|
+
return `To use ${modelId}, an admin needs to connect ${authHint.providerName} on the base agent. Ask an admin to configure ${authHint.providerName} and then try again.`;
|
|
1313
|
+
}
|
|
1314
|
+
async maybeBuildAudioPermissionHintMessage(outputText, gatewayUrl, workerToken) {
|
|
1315
|
+
const lower = outputText.toLowerCase();
|
|
1316
|
+
if (!lower.includes("api.model.audio.request")) {
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
if (lower.includes("settings button has been sent") ||
|
|
1320
|
+
lower.includes("connect button has been sent") ||
|
|
1321
|
+
lower.includes("open settings") ||
|
|
1322
|
+
lower.includes("secure connect link")) {
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
try {
|
|
1326
|
+
const suggestions = await (0, audio_provider_suggestions_1.fetchAudioProviderSuggestions)({
|
|
1327
|
+
gatewayUrl,
|
|
1328
|
+
workerToken,
|
|
1329
|
+
});
|
|
1330
|
+
const providerList = suggestions.providerDisplayList || "an audio-capable provider";
|
|
1331
|
+
return `Voice generation needs an audio-capable provider (${providerList}) connected on the base agent. Ask an admin to connect one of these providers, then try again.`;
|
|
1332
|
+
}
|
|
1333
|
+
catch (error) {
|
|
1334
|
+
logger.error("Failed to fetch audio provider suggestions", error);
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
exports.OpenClawWorker = OpenClawWorker;
|
|
1340
|
+
//# sourceMappingURL=worker.js.map
|