@lobu/worker 6.1.1 → 7.1.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 +0 -4
- package/dist/core/error-handler.d.ts.map +1 -1
- package/dist/core/error-handler.js +4 -15
- package/dist/core/error-handler.js.map +1 -1
- package/dist/core/types.d.ts +1 -19
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +0 -4
- package/dist/core/types.js.map +1 -1
- package/dist/core/workspace.d.ts +2 -11
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +14 -36
- package/dist/core/workspace.js.map +1 -1
- package/dist/embedded/just-bash-bootstrap.d.ts.map +1 -1
- package/dist/embedded/just-bash-bootstrap.js +60 -6
- package/dist/embedded/just-bash-bootstrap.js.map +1 -1
- package/dist/embedded/mcp-cli-commands.d.ts.map +1 -1
- package/dist/embedded/mcp-cli-commands.js +3 -38
- package/dist/embedded/mcp-cli-commands.js.map +1 -1
- package/dist/gateway/gateway-integration.js +4 -4
- package/dist/gateway/gateway-integration.js.map +1 -1
- package/dist/gateway/message-batcher.d.ts.map +1 -1
- package/dist/gateway/message-batcher.js +3 -5
- package/dist/gateway/message-batcher.js.map +1 -1
- package/dist/gateway/sse-client.d.ts +1 -0
- package/dist/gateway/sse-client.d.ts.map +1 -1
- package/dist/gateway/sse-client.js +52 -8
- package/dist/gateway/sse-client.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -24
- package/dist/index.js.map +1 -1
- package/dist/instructions/builder.d.ts.map +1 -1
- package/dist/instructions/builder.js +2 -1
- package/dist/instructions/builder.js.map +1 -1
- package/dist/openclaw/plugin-loader.d.ts.map +1 -1
- package/dist/openclaw/plugin-loader.js +8 -19
- package/dist/openclaw/plugin-loader.js.map +1 -1
- package/dist/openclaw/processor.d.ts.map +1 -1
- package/dist/openclaw/processor.js +2 -0
- package/dist/openclaw/processor.js.map +1 -1
- package/dist/openclaw/sandbox-leak.d.ts.map +1 -1
- package/dist/openclaw/sandbox-leak.js +1 -6
- package/dist/openclaw/sandbox-leak.js.map +1 -1
- package/dist/openclaw/session-context.d.ts.map +1 -1
- package/dist/openclaw/session-context.js +3 -0
- package/dist/openclaw/session-context.js.map +1 -1
- package/dist/openclaw/tool-policy.d.ts.map +1 -1
- package/dist/openclaw/tool-policy.js +5 -11
- package/dist/openclaw/tool-policy.js.map +1 -1
- package/dist/openclaw/worker.d.ts +0 -1
- package/dist/openclaw/worker.d.ts.map +1 -1
- package/dist/openclaw/worker.js +19 -85
- package/dist/openclaw/worker.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -40
- package/dist/server.js.map +1 -1
- package/dist/shared/audio-provider-suggestions.d.ts.map +1 -1
- package/dist/shared/audio-provider-suggestions.js +4 -6
- package/dist/shared/audio-provider-suggestions.js.map +1 -1
- package/dist/shared/tool-implementations.d.ts.map +1 -1
- package/dist/shared/tool-implementations.js +99 -37
- package/dist/shared/tool-implementations.js.map +1 -1
- package/package.json +14 -4
- package/src/__tests__/audio-provider-suggestions.test.ts +199 -0
- package/src/__tests__/custom-tools.test.ts +92 -0
- package/src/__tests__/embedded-just-bash-bootstrap.test.ts +128 -0
- package/src/__tests__/embedded-mcp-cli-bash.test.ts +179 -0
- package/src/__tests__/embedded-tools.test.ts +744 -0
- package/src/__tests__/exec-sandbox-extra.test.ts +0 -0
- package/src/__tests__/exec-sandbox.test.ts +550 -0
- package/src/__tests__/generated-media.test.ts +142 -0
- package/src/__tests__/instructions.test.ts +60 -0
- package/src/__tests__/mcp-cli-commands-extra.test.ts +478 -0
- package/src/__tests__/mcp-cli-commands.test.ts +383 -0
- package/src/__tests__/mcp-tool-call.test.ts +423 -0
- package/src/__tests__/memory-flush-harden.test.ts +367 -0
- package/src/__tests__/memory-flush-runtime.test.ts +138 -0
- package/src/__tests__/memory-flush.test.ts +64 -0
- package/src/__tests__/message-batcher.test.ts +247 -0
- package/src/__tests__/model-resolver-harden.test.ts +197 -0
- package/src/__tests__/model-resolver.test.ts +156 -0
- package/src/__tests__/processor-harden.test.ts +259 -0
- package/src/__tests__/processor.test.ts +225 -0
- package/src/__tests__/replace-base-prompt-identity.test.ts +41 -0
- package/src/__tests__/sandbox-leak-harden.test.ts +200 -0
- package/src/__tests__/sandbox-leak.test.ts +167 -0
- package/src/__tests__/setup.ts +102 -0
- package/src/__tests__/sse-client-harden.test.ts +588 -0
- package/src/__tests__/sse-client.test.ts +90 -0
- package/src/__tests__/tool-implementations.test.ts +196 -0
- package/src/__tests__/tool-policy-edge-cases.test.ts +263 -0
- package/src/__tests__/tool-policy.test.ts +269 -0
- package/src/__tests__/worker.test.ts +89 -0
- package/src/core/error-handler.ts +47 -0
- package/src/core/project-scanner.ts +65 -0
- package/src/core/types.ts +94 -0
- package/src/core/workspace.ts +66 -0
- package/src/embedded/exec-sandbox.ts +372 -0
- package/src/embedded/just-bash-bootstrap.ts +575 -0
- package/src/embedded/mcp-cli-commands.ts +405 -0
- package/src/gateway/gateway-integration.ts +298 -0
- package/src/gateway/message-batcher.ts +123 -0
- package/src/gateway/sse-client.ts +988 -0
- package/src/gateway/types.ts +68 -0
- package/src/index.ts +123 -0
- package/src/instructions/builder.ts +44 -0
- package/src/instructions/providers.ts +27 -0
- package/src/modules/lifecycle.ts +92 -0
- package/src/openclaw/custom-tools.ts +315 -0
- package/src/openclaw/instructions.ts +36 -0
- package/src/openclaw/model-resolver.ts +150 -0
- package/src/openclaw/plugin-loader.ts +423 -0
- package/src/openclaw/processor.ts +199 -0
- package/src/openclaw/sandbox-leak.ts +100 -0
- package/src/openclaw/session-context.ts +323 -0
- package/src/openclaw/tool-policy.ts +241 -0
- package/src/openclaw/tools.ts +277 -0
- package/src/openclaw/worker.ts +1836 -0
- package/src/server.ts +330 -0
- package/src/shared/audio-provider-suggestions.ts +130 -0
- package/src/shared/processor-utils.ts +33 -0
- package/src/shared/provider-auth-hints.ts +68 -0
- package/src/shared/tool-display-config.ts +75 -0
- package/src/shared/tool-implementations.ts +981 -0
- package/src/shared/worker-env-keys.ts +8 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for gateway communication
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentOptions, ThreadResponsePayload } from "@lobu/core";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Platform-specific metadata (e.g., Slack team_id, channel, thread_ts)
|
|
9
|
+
*/
|
|
10
|
+
interface PlatformMetadata {
|
|
11
|
+
team_id?: string;
|
|
12
|
+
channel?: string;
|
|
13
|
+
ts?: string;
|
|
14
|
+
thread_ts?: string;
|
|
15
|
+
files?: unknown[];
|
|
16
|
+
traceId?: string; // Trace ID for end-to-end observability
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Job type for queue messages
|
|
22
|
+
* - message: Standard agent message execution
|
|
23
|
+
* - exec: Direct command execution in sandbox
|
|
24
|
+
*/
|
|
25
|
+
export type JobType = "message" | "exec";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Message payload for agent execution
|
|
29
|
+
*/
|
|
30
|
+
export interface MessagePayload {
|
|
31
|
+
botId: string;
|
|
32
|
+
userId: string;
|
|
33
|
+
agentId: string;
|
|
34
|
+
conversationId: string;
|
|
35
|
+
platform: string;
|
|
36
|
+
channelId: string;
|
|
37
|
+
messageId: string;
|
|
38
|
+
messageText: string;
|
|
39
|
+
platformMetadata: PlatformMetadata;
|
|
40
|
+
agentOptions: AgentOptions;
|
|
41
|
+
jobId?: string; // Optional job ID from gateway
|
|
42
|
+
teamId?: string; // Optional team ID (WhatsApp uses top-level, Slack uses platformMetadata)
|
|
43
|
+
|
|
44
|
+
// Job type (default: "message")
|
|
45
|
+
jobType?: JobType;
|
|
46
|
+
|
|
47
|
+
// Exec-specific fields (only used when jobType === "exec")
|
|
48
|
+
execId?: string; // Unique ID for exec job (for response routing)
|
|
49
|
+
execCommand?: string; // Command to execute
|
|
50
|
+
execCwd?: string; // Working directory for command
|
|
51
|
+
execEnv?: Record<string, string>; // Additional environment variables
|
|
52
|
+
execTimeout?: number; // Timeout in milliseconds
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Queued message with timestamp
|
|
57
|
+
*/
|
|
58
|
+
export interface QueuedMessage {
|
|
59
|
+
payload: MessagePayload;
|
|
60
|
+
timestamp: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Response data sent back to gateway
|
|
65
|
+
*/
|
|
66
|
+
export type ResponseData = ThreadResponsePayload & {
|
|
67
|
+
originalMessageId: string;
|
|
68
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createLogger,
|
|
5
|
+
initSentry,
|
|
6
|
+
initTracing,
|
|
7
|
+
moduleRegistry,
|
|
8
|
+
} from "@lobu/core";
|
|
9
|
+
|
|
10
|
+
const logger = createLogger("worker");
|
|
11
|
+
|
|
12
|
+
import { GatewayClient } from "./gateway/sse-client";
|
|
13
|
+
import { startWorkerHttpServer, stopWorkerHttpServer } from "./server";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Main entry point for gateway-based persistent worker
|
|
17
|
+
*/
|
|
18
|
+
async function main() {
|
|
19
|
+
process.on("unhandledRejection", (reason) => {
|
|
20
|
+
logger.error("Unhandled rejection:", reason);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
process.on("uncaughtException", (error) => {
|
|
25
|
+
logger.error("Uncaught exception:", error);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
logger.info("Starting worker...");
|
|
30
|
+
|
|
31
|
+
await initSentry();
|
|
32
|
+
|
|
33
|
+
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
34
|
+
if (otlpEndpoint) {
|
|
35
|
+
initTracing({
|
|
36
|
+
serviceName: "lobu-worker",
|
|
37
|
+
otlpEndpoint,
|
|
38
|
+
});
|
|
39
|
+
logger.info(`Tracing initialized: lobu-worker -> ${otlpEndpoint}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await moduleRegistry.registerAvailableModules();
|
|
43
|
+
await moduleRegistry.initAll();
|
|
44
|
+
logger.info("✅ Modules initialized");
|
|
45
|
+
|
|
46
|
+
logger.info("🔄 Starting in gateway mode (SSE/HTTP-based persistent worker)");
|
|
47
|
+
|
|
48
|
+
const userId = process.env.USER_ID;
|
|
49
|
+
|
|
50
|
+
if (!userId) {
|
|
51
|
+
logger.error(
|
|
52
|
+
"❌ USER_ID environment variable is required for gateway mode"
|
|
53
|
+
);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const deploymentName = process.env.DEPLOYMENT_NAME;
|
|
59
|
+
const dispatcherUrl = process.env.DISPATCHER_URL;
|
|
60
|
+
const workerToken = process.env.WORKER_TOKEN;
|
|
61
|
+
|
|
62
|
+
if (!deploymentName) {
|
|
63
|
+
logger.error("❌ DEPLOYMENT_NAME environment variable is required");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
if (!dispatcherUrl) {
|
|
67
|
+
logger.error("❌ DISPATCHER_URL environment variable is required");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
if (!workerToken) {
|
|
71
|
+
logger.error("❌ WORKER_TOKEN environment variable is required");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const httpPort = await startWorkerHttpServer();
|
|
76
|
+
logger.info(`Worker HTTP server started on port ${httpPort}`);
|
|
77
|
+
|
|
78
|
+
logger.info(`🚀 Starting Gateway-based Persistent Worker`);
|
|
79
|
+
logger.info(`- User ID: ${userId}`);
|
|
80
|
+
logger.info(`- Deployment: ${deploymentName}`);
|
|
81
|
+
logger.info(`- Dispatcher URL: ${dispatcherUrl}`);
|
|
82
|
+
|
|
83
|
+
const gatewayClient = new GatewayClient(
|
|
84
|
+
dispatcherUrl,
|
|
85
|
+
workerToken,
|
|
86
|
+
userId,
|
|
87
|
+
deploymentName,
|
|
88
|
+
httpPort
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Register signal handlers before async operations
|
|
92
|
+
let isShuttingDown = false;
|
|
93
|
+
const shutdown = async (signal: NodeJS.Signals) => {
|
|
94
|
+
if (isShuttingDown) return;
|
|
95
|
+
isShuttingDown = true;
|
|
96
|
+
logger.info(`Received ${signal}, shutting down gateway worker...`);
|
|
97
|
+
await gatewayClient.stop();
|
|
98
|
+
await stopWorkerHttpServer();
|
|
99
|
+
process.exit(0);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
103
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
104
|
+
|
|
105
|
+
logger.info("🔌 Connecting to dispatcher...");
|
|
106
|
+
await gatewayClient.start();
|
|
107
|
+
logger.info("✅ Gateway worker started successfully");
|
|
108
|
+
|
|
109
|
+
await new Promise<never>(() => {
|
|
110
|
+
// Intentionally never resolves — block process until signal.
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
logger.error("❌ Gateway worker failed:", error);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type { WorkerConfig } from "./core/types";
|
|
119
|
+
|
|
120
|
+
main().catch((error) => {
|
|
121
|
+
logger.error("Fatal error in main:", error);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLogger,
|
|
3
|
+
type InstructionContext,
|
|
4
|
+
type InstructionProvider,
|
|
5
|
+
} from "@lobu/core";
|
|
6
|
+
|
|
7
|
+
const logger = createLogger("instruction-generator");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate custom instructions using modular providers.
|
|
11
|
+
* Only generates worker-local instructions (core, projects) — platform and
|
|
12
|
+
* MCP instructions are provided by the gateway.
|
|
13
|
+
*
|
|
14
|
+
* Per-provider error handling lives on `BaseInstructionProvider` (a thrown
|
|
15
|
+
* provider returns `""`). The fallback below covers the unlikely case of a
|
|
16
|
+
* provider that bypasses the base class throwing during the loop itself.
|
|
17
|
+
*/
|
|
18
|
+
export async function generateCustomInstructions(
|
|
19
|
+
providers: InstructionProvider[],
|
|
20
|
+
context: InstructionContext
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
try {
|
|
23
|
+
const sorted = [...providers].sort((a, b) => a.priority - b.priority);
|
|
24
|
+
const sections: string[] = [];
|
|
25
|
+
for (const provider of sorted) {
|
|
26
|
+
const instructions = await provider.getInstructions(context);
|
|
27
|
+
if (instructions?.trim()) {
|
|
28
|
+
sections.push(instructions.trim());
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const instructions = sections.join("\n\n");
|
|
33
|
+
logger.info(
|
|
34
|
+
`[WORKER-INSTRUCTIONS] Generated ${instructions.length} characters from ${providers.length} local providers`
|
|
35
|
+
);
|
|
36
|
+
logger.debug(`[WORKER-INSTRUCTIONS] \n${instructions}`);
|
|
37
|
+
return instructions;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
logger.error("Failed to generate worker instructions:", error);
|
|
40
|
+
const fallback = `You are a helpful AI agent for user ${context.userId}.`;
|
|
41
|
+
logger.warn(`[WORKER-INSTRUCTIONS] Using fallback: ${fallback}`);
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instruction providers for worker
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { BaseInstructionProvider, type InstructionContext } from "@lobu/core";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Provides information about available projects in the workspace
|
|
9
|
+
*/
|
|
10
|
+
export class ProjectsInstructionProvider extends BaseInstructionProvider {
|
|
11
|
+
readonly name = "projects";
|
|
12
|
+
readonly priority = 30;
|
|
13
|
+
|
|
14
|
+
protected buildInstructions(context: InstructionContext): string {
|
|
15
|
+
if (!context.availableProjects || context.availableProjects.length === 0) {
|
|
16
|
+
return `**Available projects:**
|
|
17
|
+
- none`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const projectList = context.availableProjects
|
|
21
|
+
.map((project: string) => ` - ${project}`)
|
|
22
|
+
.join("\n");
|
|
23
|
+
|
|
24
|
+
return `**Available projects:**
|
|
25
|
+
${projectList}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLogger,
|
|
3
|
+
type ModuleSessionContext,
|
|
4
|
+
moduleRegistry,
|
|
5
|
+
type SessionContext,
|
|
6
|
+
type WorkerModule,
|
|
7
|
+
} from "@lobu/core";
|
|
8
|
+
|
|
9
|
+
const logger = createLogger("worker");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute an operation on all worker modules with consistent error handling.
|
|
13
|
+
* Errors in individual modules are logged but do not halt iteration.
|
|
14
|
+
*/
|
|
15
|
+
async function executeForAllModules<T>(
|
|
16
|
+
operation: (module: WorkerModule) => Promise<T>,
|
|
17
|
+
operationName: string
|
|
18
|
+
): Promise<T[]> {
|
|
19
|
+
const workerModules = moduleRegistry.getWorkerModules();
|
|
20
|
+
const results: T[] = [];
|
|
21
|
+
for (const module of workerModules) {
|
|
22
|
+
try {
|
|
23
|
+
results.push(await operation(module));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
logger.error(
|
|
26
|
+
`Failed to execute ${operationName} for module ${module.name}:`,
|
|
27
|
+
error
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function onSessionStart(
|
|
35
|
+
context: SessionContext
|
|
36
|
+
): Promise<SessionContext> {
|
|
37
|
+
// Convert to module session context
|
|
38
|
+
const moduleContext: ModuleSessionContext = {
|
|
39
|
+
userId: context.userId,
|
|
40
|
+
conversationId: context.conversationId || "",
|
|
41
|
+
systemPrompt: context.customInstructions || "",
|
|
42
|
+
workspace: undefined,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let updatedContext = moduleContext;
|
|
46
|
+
|
|
47
|
+
await executeForAllModules(async (module) => {
|
|
48
|
+
updatedContext = await module.onSessionStart(updatedContext);
|
|
49
|
+
}, "onSessionStart");
|
|
50
|
+
|
|
51
|
+
// Merge back into original context, mapping systemPrompt back to customInstructions
|
|
52
|
+
return {
|
|
53
|
+
...context,
|
|
54
|
+
customInstructions:
|
|
55
|
+
updatedContext.systemPrompt || context.customInstructions,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Configuration for module workspace initialization
|
|
61
|
+
*/
|
|
62
|
+
interface ModuleWorkspaceConfig {
|
|
63
|
+
workspaceDir: string;
|
|
64
|
+
username: string;
|
|
65
|
+
sessionKey: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function initModuleWorkspace(
|
|
69
|
+
config: ModuleWorkspaceConfig
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
await executeForAllModules(
|
|
72
|
+
(module) => module.initWorkspace(config),
|
|
73
|
+
"initWorkspace"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function collectModuleData(context: {
|
|
78
|
+
workspaceDir: string;
|
|
79
|
+
userId: string;
|
|
80
|
+
conversationId: string;
|
|
81
|
+
}): Promise<Record<string, unknown>> {
|
|
82
|
+
const moduleData: Record<string, unknown> = {};
|
|
83
|
+
|
|
84
|
+
await executeForAllModules(async (module) => {
|
|
85
|
+
const data = await module.onBeforeResponse(context);
|
|
86
|
+
if (data !== null) {
|
|
87
|
+
moduleData[module.name] = data;
|
|
88
|
+
}
|
|
89
|
+
}, "onBeforeResponse");
|
|
90
|
+
|
|
91
|
+
return moduleData;
|
|
92
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { getCustomToolDescription, type McpToolDef } from "@lobu/core";
|
|
2
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
3
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import type { Static } from "@sinclair/typebox";
|
|
5
|
+
import { type TSchema, Type } from "@sinclair/typebox";
|
|
6
|
+
import type { GatewayParams, TextResult } from "../shared/tool-implementations";
|
|
7
|
+
import {
|
|
8
|
+
askUserQuestion,
|
|
9
|
+
callMcpTool,
|
|
10
|
+
checkMcpLogin,
|
|
11
|
+
generateAudio,
|
|
12
|
+
generateImage,
|
|
13
|
+
getChannelHistory,
|
|
14
|
+
logoutMcp,
|
|
15
|
+
startMcpLogin,
|
|
16
|
+
uploadUserFile,
|
|
17
|
+
} from "../shared/tool-implementations";
|
|
18
|
+
|
|
19
|
+
type ToolResult = AgentToolResult<Record<string, unknown>>;
|
|
20
|
+
|
|
21
|
+
/** Adapt shared TextResult to OpenClaw's ToolResult (adds details field) */
|
|
22
|
+
function toToolResult(result: TextResult): ToolResult {
|
|
23
|
+
return { content: result.content, details: {} };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a ToolDefinition with proper type bridging between TypeBox schemas
|
|
28
|
+
* and the shared tool implementation functions. Eliminates per-tool `as` casts
|
|
29
|
+
* by casting once at the boundary.
|
|
30
|
+
*/
|
|
31
|
+
function defineTool<T extends TSchema>(config: {
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
parameters: T;
|
|
35
|
+
run: (args: Static<T>) => Promise<TextResult>;
|
|
36
|
+
}): ToolDefinition {
|
|
37
|
+
return {
|
|
38
|
+
name: config.name,
|
|
39
|
+
label: config.name,
|
|
40
|
+
description: config.description,
|
|
41
|
+
parameters: config.parameters,
|
|
42
|
+
execute: async (_toolCallId, args) =>
|
|
43
|
+
toToolResult(await config.run(args as Static<T>)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createOpenClawCustomTools(params: {
|
|
48
|
+
gatewayUrl: string;
|
|
49
|
+
workerToken: string;
|
|
50
|
+
channelId: string;
|
|
51
|
+
conversationId: string;
|
|
52
|
+
platform?: string;
|
|
53
|
+
/** Session workspace directory. Required — UploadUserFile resolves relative paths against it. */
|
|
54
|
+
workspaceDir: string;
|
|
55
|
+
onCustomEvent?: (
|
|
56
|
+
name: string,
|
|
57
|
+
data: Record<string, unknown>
|
|
58
|
+
) => Promise<void> | void;
|
|
59
|
+
}): ToolDefinition[] {
|
|
60
|
+
const gw: GatewayParams = {
|
|
61
|
+
gatewayUrl: params.gatewayUrl,
|
|
62
|
+
workerToken: params.workerToken,
|
|
63
|
+
channelId: params.channelId,
|
|
64
|
+
conversationId: params.conversationId,
|
|
65
|
+
platform: params.platform || "slack",
|
|
66
|
+
workspaceDir: params.workspaceDir,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const tools: ToolDefinition[] = [
|
|
70
|
+
defineTool({
|
|
71
|
+
name: "UploadUserFile",
|
|
72
|
+
description: getCustomToolDescription("UploadUserFile"),
|
|
73
|
+
parameters: Type.Object({
|
|
74
|
+
file_path: Type.String({
|
|
75
|
+
description:
|
|
76
|
+
"Path to the file to show (absolute or relative to workspace)",
|
|
77
|
+
}),
|
|
78
|
+
description: Type.Optional(
|
|
79
|
+
Type.String({
|
|
80
|
+
description:
|
|
81
|
+
"Optional description of what the file contains or shows",
|
|
82
|
+
})
|
|
83
|
+
),
|
|
84
|
+
}),
|
|
85
|
+
run: (args) =>
|
|
86
|
+
uploadUserFile(gw, args, {
|
|
87
|
+
onUploaded: (data) => params.onCustomEvent?.("file-uploaded", data),
|
|
88
|
+
}),
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
defineTool({
|
|
92
|
+
name: "GenerateImage",
|
|
93
|
+
description: getCustomToolDescription("GenerateImage"),
|
|
94
|
+
parameters: Type.Object({
|
|
95
|
+
prompt: Type.String({
|
|
96
|
+
description: "The image prompt to generate",
|
|
97
|
+
}),
|
|
98
|
+
size: Type.Optional(
|
|
99
|
+
Type.Union(
|
|
100
|
+
[
|
|
101
|
+
Type.Literal("1024x1024"),
|
|
102
|
+
Type.Literal("1024x1536"),
|
|
103
|
+
Type.Literal("1536x1024"),
|
|
104
|
+
Type.Literal("auto"),
|
|
105
|
+
],
|
|
106
|
+
{
|
|
107
|
+
description: "Output image size (default: 1024x1024)",
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
),
|
|
111
|
+
quality: Type.Optional(
|
|
112
|
+
Type.Union(
|
|
113
|
+
[
|
|
114
|
+
Type.Literal("low"),
|
|
115
|
+
Type.Literal("medium"),
|
|
116
|
+
Type.Literal("high"),
|
|
117
|
+
Type.Literal("auto"),
|
|
118
|
+
],
|
|
119
|
+
{
|
|
120
|
+
description: "Image quality (default: auto)",
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
),
|
|
124
|
+
background: Type.Optional(
|
|
125
|
+
Type.Union(
|
|
126
|
+
[
|
|
127
|
+
Type.Literal("transparent"),
|
|
128
|
+
Type.Literal("opaque"),
|
|
129
|
+
Type.Literal("auto"),
|
|
130
|
+
],
|
|
131
|
+
{
|
|
132
|
+
description: "Background style (default: auto)",
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
),
|
|
136
|
+
format: Type.Optional(
|
|
137
|
+
Type.Union(
|
|
138
|
+
[Type.Literal("png"), Type.Literal("jpeg"), Type.Literal("webp")],
|
|
139
|
+
{
|
|
140
|
+
description: "Output image format (default: png)",
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
),
|
|
144
|
+
}),
|
|
145
|
+
run: (args) => generateImage(gw, args),
|
|
146
|
+
}),
|
|
147
|
+
|
|
148
|
+
defineTool({
|
|
149
|
+
name: "GenerateAudio",
|
|
150
|
+
description: getCustomToolDescription("GenerateAudio"),
|
|
151
|
+
parameters: Type.Object({
|
|
152
|
+
text: Type.String({
|
|
153
|
+
description: "The text to convert to speech (max 4096 characters)",
|
|
154
|
+
}),
|
|
155
|
+
voice: Type.Optional(
|
|
156
|
+
Type.String({
|
|
157
|
+
description:
|
|
158
|
+
"Voice ID (provider-specific). OpenAI: alloy, echo, fable, onyx, nova, shimmer. Leave empty for default.",
|
|
159
|
+
})
|
|
160
|
+
),
|
|
161
|
+
speed: Type.Optional(
|
|
162
|
+
Type.Number({
|
|
163
|
+
description: "Speech speed (0.5-2.0, default 1.0).",
|
|
164
|
+
})
|
|
165
|
+
),
|
|
166
|
+
}),
|
|
167
|
+
run: (args) => generateAudio(gw, args),
|
|
168
|
+
}),
|
|
169
|
+
|
|
170
|
+
defineTool({
|
|
171
|
+
name: "GetChannelHistory",
|
|
172
|
+
description: getCustomToolDescription("GetChannelHistory"),
|
|
173
|
+
parameters: Type.Object({
|
|
174
|
+
limit: Type.Optional(
|
|
175
|
+
Type.Number({
|
|
176
|
+
description: "Number of messages to fetch (default 50, max 100)",
|
|
177
|
+
})
|
|
178
|
+
),
|
|
179
|
+
before: Type.Optional(
|
|
180
|
+
Type.String({
|
|
181
|
+
description:
|
|
182
|
+
"ISO timestamp cursor - fetch messages before this time (for pagination)",
|
|
183
|
+
})
|
|
184
|
+
),
|
|
185
|
+
}),
|
|
186
|
+
run: (args) => getChannelHistory(gw, args),
|
|
187
|
+
}),
|
|
188
|
+
|
|
189
|
+
defineTool({
|
|
190
|
+
name: "AskUserQuestion",
|
|
191
|
+
description: getCustomToolDescription("AskUserQuestion"),
|
|
192
|
+
parameters: Type.Object({
|
|
193
|
+
question: Type.String({
|
|
194
|
+
description: "The question to ask the user",
|
|
195
|
+
}),
|
|
196
|
+
options: Type.Array(Type.String(), {
|
|
197
|
+
description: "Array of button labels for the user to choose from",
|
|
198
|
+
}),
|
|
199
|
+
}),
|
|
200
|
+
run: (args) => askUserQuestion(gw, args),
|
|
201
|
+
}),
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
return tools;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Convert MCP tool definitions from session context into first-class
|
|
209
|
+
* OpenClaw ToolDefinition objects that call the MCP proxy directly.
|
|
210
|
+
* Tools are dynamically discovered from each MCP server (e.g. lobu).
|
|
211
|
+
*/
|
|
212
|
+
export function createMcpToolDefinitions(
|
|
213
|
+
mcpTools: Record<string, McpToolDef[]>,
|
|
214
|
+
gw: GatewayParams,
|
|
215
|
+
mcpContext?: Record<string, string>
|
|
216
|
+
): ToolDefinition[] {
|
|
217
|
+
const tools: ToolDefinition[] = [];
|
|
218
|
+
|
|
219
|
+
for (const [mcpId, defs] of Object.entries(mcpTools)) {
|
|
220
|
+
const contextPrefix = mcpContext?.[mcpId];
|
|
221
|
+
for (const def of defs) {
|
|
222
|
+
if (!def.name || typeof def.name !== "string" || !def.name.trim()) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const schema = def.inputSchema
|
|
226
|
+
? Type.Unsafe(def.inputSchema)
|
|
227
|
+
: Type.Object({});
|
|
228
|
+
|
|
229
|
+
const baseDescription = def.description || `MCP tool from ${mcpId}`;
|
|
230
|
+
const description = contextPrefix
|
|
231
|
+
? `[${contextPrefix}] ${baseDescription}`
|
|
232
|
+
: baseDescription;
|
|
233
|
+
|
|
234
|
+
tools.push({
|
|
235
|
+
name: def.name,
|
|
236
|
+
label: `${mcpId}/${def.name}`,
|
|
237
|
+
description,
|
|
238
|
+
parameters: schema,
|
|
239
|
+
execute: async (_toolCallId, args) =>
|
|
240
|
+
toToolResult(
|
|
241
|
+
await callMcpTool(
|
|
242
|
+
gw,
|
|
243
|
+
mcpId,
|
|
244
|
+
def.name,
|
|
245
|
+
(args || {}) as Record<string, unknown>
|
|
246
|
+
)
|
|
247
|
+
),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return tools;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function createMcpAuthToolDefinitions(
|
|
256
|
+
mcpStatus: Array<{
|
|
257
|
+
id: string;
|
|
258
|
+
name: string;
|
|
259
|
+
requiresAuth: boolean;
|
|
260
|
+
requiresInput?: boolean;
|
|
261
|
+
authenticated?: boolean;
|
|
262
|
+
configured?: boolean;
|
|
263
|
+
}>,
|
|
264
|
+
gw: GatewayParams,
|
|
265
|
+
existingToolNames: Set<string> = new Set()
|
|
266
|
+
): ToolDefinition[] {
|
|
267
|
+
const tools: ToolDefinition[] = [];
|
|
268
|
+
|
|
269
|
+
for (const mcp of mcpStatus) {
|
|
270
|
+
if (!mcp.requiresAuth) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const loginToolName = `${mcp.id}_login`;
|
|
275
|
+
if (!existingToolNames.has(loginToolName)) {
|
|
276
|
+
tools.push(
|
|
277
|
+
defineTool({
|
|
278
|
+
name: loginToolName,
|
|
279
|
+
description: `Start the authentication flow for the ${mcp.name} MCP. Use this when ${mcp.name} requires login before its tools can be used.`,
|
|
280
|
+
parameters: Type.Object({}),
|
|
281
|
+
run: () => startMcpLogin(gw, { mcpId: mcp.id }),
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
existingToolNames.add(loginToolName);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const checkToolName = `${mcp.id}_login_check`;
|
|
288
|
+
if (!existingToolNames.has(checkToolName)) {
|
|
289
|
+
tools.push(
|
|
290
|
+
defineTool({
|
|
291
|
+
name: checkToolName,
|
|
292
|
+
description: `Check whether authentication for the ${mcp.name} MCP has completed. Call this after the user finishes login.`,
|
|
293
|
+
parameters: Type.Object({}),
|
|
294
|
+
run: () => checkMcpLogin(gw, { mcpId: mcp.id }),
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
existingToolNames.add(checkToolName);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const logoutToolName = `${mcp.id}_logout`;
|
|
301
|
+
if (!existingToolNames.has(logoutToolName)) {
|
|
302
|
+
tools.push(
|
|
303
|
+
defineTool({
|
|
304
|
+
name: logoutToolName,
|
|
305
|
+
description: `Remove the stored authentication credential for the ${mcp.name} MCP.`,
|
|
306
|
+
parameters: Type.Object({}),
|
|
307
|
+
run: () => logoutMcp(gw, { mcpId: mcp.id }),
|
|
308
|
+
})
|
|
309
|
+
);
|
|
310
|
+
existingToolNames.add(logoutToolName);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return tools;
|
|
315
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseInstructionProvider,
|
|
3
|
+
type InstructionContext,
|
|
4
|
+
renderAlwaysOnToolPolicyRules,
|
|
5
|
+
renderBaselineAgentPolicy,
|
|
6
|
+
renderDetectedToolIntentRules,
|
|
7
|
+
} from "@lobu/core";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* OpenClaw core instructions
|
|
11
|
+
*/
|
|
12
|
+
export class OpenClawCoreInstructionProvider extends BaseInstructionProvider {
|
|
13
|
+
readonly name = "core";
|
|
14
|
+
readonly priority = 10;
|
|
15
|
+
|
|
16
|
+
protected buildInstructions(context: InstructionContext): string {
|
|
17
|
+
return [
|
|
18
|
+
`You are a Lobu agent for user ${context.userId}.`,
|
|
19
|
+
`Working directory: ${context.workingDirectory}`,
|
|
20
|
+
renderBaselineAgentPolicy(),
|
|
21
|
+
renderAlwaysOnToolPolicyRules(),
|
|
22
|
+
`## Image Analysis
|
|
23
|
+
|
|
24
|
+
If the user asks to analyze an uploaded image, use the image content already attached to the prompt and provide direct analysis.`,
|
|
25
|
+
].join("\n\n");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class OpenClawPromptIntentInstructionProvider extends BaseInstructionProvider {
|
|
30
|
+
readonly name = "prompt-intent";
|
|
31
|
+
readonly priority = 15;
|
|
32
|
+
|
|
33
|
+
protected buildInstructions(context: InstructionContext): string {
|
|
34
|
+
return renderDetectedToolIntentRules(context.userPrompt || "");
|
|
35
|
+
}
|
|
36
|
+
}
|