@astroanywhere/agent 0.2.4 → 0.2.5
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/README.md +19 -15
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +89 -14
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +4 -0
- package/dist/commands/start.js.map +1 -1
- package/dist/lib/display.d.ts +1 -1
- package/dist/lib/display.d.ts.map +1 -1
- package/dist/lib/display.js +5 -0
- package/dist/lib/display.js.map +1 -1
- package/dist/lib/openclaw-bridge.d.ts +39 -7
- package/dist/lib/openclaw-bridge.d.ts.map +1 -1
- package/dist/lib/openclaw-bridge.js +256 -40
- package/dist/lib/openclaw-bridge.js.map +1 -1
- package/dist/lib/openclaw-gateway.d.ts +52 -0
- package/dist/lib/openclaw-gateway.d.ts.map +1 -0
- package/dist/lib/openclaw-gateway.js +116 -0
- package/dist/lib/openclaw-gateway.js.map +1 -0
- package/dist/lib/providers.d.ts.map +1 -1
- package/dist/lib/providers.js +6 -37
- package/dist/lib/providers.js.map +1 -1
- package/dist/lib/ssh-discovery.d.ts +3 -1
- package/dist/lib/ssh-discovery.d.ts.map +1 -1
- package/dist/lib/ssh-discovery.js +34 -24
- package/dist/lib/ssh-discovery.js.map +1 -1
- package/dist/lib/ssh-installer.d.ts +26 -0
- package/dist/lib/ssh-installer.d.ts.map +1 -1
- package/dist/lib/ssh-installer.js +126 -4
- package/dist/lib/ssh-installer.js.map +1 -1
- package/dist/lib/task-executor.d.ts +7 -0
- package/dist/lib/task-executor.d.ts.map +1 -1
- package/dist/lib/task-executor.js +16 -2
- package/dist/lib/task-executor.js.map +1 -1
- package/dist/lib/websocket-client.d.ts +3 -0
- package/dist/lib/websocket-client.d.ts.map +1 -1
- package/dist/lib/websocket-client.js +6 -0
- package/dist/lib/websocket-client.js.map +1 -1
- package/dist/providers/index.d.ts +3 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +9 -3
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openclaw-adapter.d.ts +21 -29
- package/dist/providers/openclaw-adapter.d.ts.map +1 -1
- package/dist/providers/openclaw-adapter.js +147 -385
- package/dist/providers/openclaw-adapter.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,31 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenClaw provider adapter —
|
|
2
|
+
* OpenClaw provider adapter — Thin wrapper delegating to OpenClawBridge
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* When a bridge is injected via setBridge(), task execution goes through
|
|
5
|
+
* the bridge's shared WebSocket connection (session-multiplexed).
|
|
6
|
+
* Falls back to standalone connections when no bridge is available.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 2. Probe ws://127.0.0.1:{port} for connect.challenge
|
|
10
|
-
* 3. Handshake with client.id='gateway-client', mode='backend'
|
|
11
|
-
*
|
|
12
|
-
* Execution flow:
|
|
13
|
-
* chat.send({ sessionKey, message, idempotencyKey })
|
|
14
|
-
* → gateway streams `agent` + `chat` events over WebSocket
|
|
15
|
-
* → adapter translates to TaskOutputStream calls
|
|
16
|
-
* → returns TaskResult on session completion
|
|
8
|
+
* The HTTP llm-task path (runLlmTask) remains in the adapter since it
|
|
9
|
+
* uses a separate HTTP POST, not the WebSocket.
|
|
17
10
|
*/
|
|
18
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
19
|
-
import { join } from 'node:path';
|
|
20
|
-
import { homedir } from 'node:os';
|
|
21
11
|
import { randomUUID } from 'node:crypto';
|
|
22
|
-
import WebSocket from 'ws';
|
|
23
12
|
import { SUMMARY_PROMPT, SUMMARY_TIMEOUT_MS, parseSummaryResponse, createNoopStream } from './base-adapter.js';
|
|
13
|
+
import { readGatewayConfig, probeGateway, parseGatewayFrame, makeSessionKey, matchesSessionKey, PROTOCOL_VERSION, CONNECT_TIMEOUT_MS, } from '../lib/openclaw-gateway.js';
|
|
14
|
+
import WebSocket from 'ws';
|
|
24
15
|
// ---------------------------------------------------------------------------
|
|
25
16
|
// Constants
|
|
26
17
|
// ---------------------------------------------------------------------------
|
|
27
|
-
const PROTOCOL_VERSION = 3;
|
|
28
|
-
const CONNECT_TIMEOUT_MS = 10_000;
|
|
29
18
|
/** TTL for preserved sessions (10 minutes) */
|
|
30
19
|
const SESSION_TTL_MS = 10 * 60 * 1000;
|
|
31
20
|
export class OpenClawAdapter {
|
|
@@ -36,16 +25,31 @@ export class OpenClawAdapter {
|
|
|
36
25
|
lastError;
|
|
37
26
|
gatewayConfig = null;
|
|
38
27
|
lastAvailableCheck = null;
|
|
28
|
+
bridge = null;
|
|
39
29
|
/** Preserved sessions for multi-turn resume, keyed by taskId */
|
|
40
30
|
preservedSessions = new Map();
|
|
31
|
+
// ─── Bridge Injection ───────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Inject the shared bridge for task execution.
|
|
34
|
+
* Called by task-executor when the bridge becomes available.
|
|
35
|
+
*/
|
|
36
|
+
setBridge(bridge) {
|
|
37
|
+
this.bridge = bridge;
|
|
38
|
+
}
|
|
39
|
+
// ─── Availability ───────────────────────────────────────────────
|
|
41
40
|
async isAvailable() {
|
|
42
|
-
|
|
41
|
+
// If bridge is connected, we're available
|
|
42
|
+
if (this.bridge?.isConnected) {
|
|
43
|
+
this.lastAvailableCheck = { available: true, at: Date.now() };
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const config = readGatewayConfig();
|
|
43
47
|
if (!config)
|
|
44
48
|
return false;
|
|
45
49
|
this.gatewayConfig = config;
|
|
46
50
|
// Probe the gateway with a quick connect
|
|
47
51
|
try {
|
|
48
|
-
const ok = await
|
|
52
|
+
const ok = await probeGateway(config.url);
|
|
49
53
|
this.lastAvailableCheck = { available: ok, at: Date.now() };
|
|
50
54
|
return ok;
|
|
51
55
|
}
|
|
@@ -54,8 +58,10 @@ export class OpenClawAdapter {
|
|
|
54
58
|
return false;
|
|
55
59
|
}
|
|
56
60
|
}
|
|
61
|
+
// ─── Task Execution ─────────────────────────────────────────────
|
|
57
62
|
async execute(task, stream, signal) {
|
|
58
|
-
|
|
63
|
+
// Ensure we have config or bridge
|
|
64
|
+
if (!this.bridge?.isConnected && !this.gatewayConfig) {
|
|
59
65
|
const available = await this.isAvailable();
|
|
60
66
|
if (!available) {
|
|
61
67
|
return {
|
|
@@ -85,13 +91,33 @@ export class OpenClawAdapter {
|
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
93
|
catch (err) {
|
|
88
|
-
// Fall through to
|
|
94
|
+
// Fall through to agent mode
|
|
89
95
|
console.warn('[openclaw] llm-task failed, falling back to agent mode:', err);
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
98
|
stream.status('running', 0, 'Connecting to OpenClaw gateway');
|
|
93
|
-
const sessionKey =
|
|
94
|
-
|
|
99
|
+
const sessionKey = makeSessionKey(task.id);
|
|
100
|
+
// Build the prompt
|
|
101
|
+
let effectivePrompt = task.systemPrompt
|
|
102
|
+
? `${task.systemPrompt}\n\n---\n\n${task.prompt}`
|
|
103
|
+
: task.prompt;
|
|
104
|
+
// Prepend conversation history if available
|
|
105
|
+
if (task.messages && task.messages.length > 0) {
|
|
106
|
+
const conversationContext = task.messages
|
|
107
|
+
.map(m => `${m.role === 'user' ? 'Human' : 'Assistant'}: ${m.content}`)
|
|
108
|
+
.join('\n\n');
|
|
109
|
+
effectivePrompt = `${conversationContext}\n\nHuman: ${effectivePrompt}`;
|
|
110
|
+
}
|
|
111
|
+
let result;
|
|
112
|
+
if (this.bridge?.isConnected) {
|
|
113
|
+
// Bridge-backed execution (shared WebSocket)
|
|
114
|
+
stream.status('running', 5, 'Connected to gateway');
|
|
115
|
+
result = await this.bridge.executeTask(sessionKey, effectivePrompt, stream, signal, task.timeout);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Fallback: standalone connection
|
|
119
|
+
result = await this.runViaStandaloneConnection(sessionKey, effectivePrompt, stream, signal, task.timeout);
|
|
120
|
+
}
|
|
95
121
|
const isCancelled = signal.aborted || result.error === 'Task cancelled';
|
|
96
122
|
// Preserve session for multi-turn resume (unless cancelled/failed)
|
|
97
123
|
if (!isCancelled && !result.error) {
|
|
@@ -159,8 +185,7 @@ export class OpenClawAdapter {
|
|
|
159
185
|
}
|
|
160
186
|
}
|
|
161
187
|
async getStatus() {
|
|
162
|
-
// Use cached availability if checked within the last 30 seconds
|
|
163
|
-
// opening a new WebSocket probe on every status poll
|
|
188
|
+
// Use cached availability if checked within the last 30 seconds
|
|
164
189
|
let available;
|
|
165
190
|
if (this.lastAvailableCheck && Date.now() - this.lastAvailableCheck.at < 30_000) {
|
|
166
191
|
available = this.lastAvailableCheck.available;
|
|
@@ -179,30 +204,29 @@ export class OpenClawAdapter {
|
|
|
179
204
|
// ─── Multi-Turn Resume ─────────────────────────────────────────
|
|
180
205
|
/**
|
|
181
206
|
* Resume a completed session by sending another chat.send to the same sessionKey.
|
|
182
|
-
* The OpenClaw gateway preserves session history per sessionKey.
|
|
183
207
|
*/
|
|
184
208
|
async resumeTask(taskId, message, _workingDirectory, sessionId, stream, signal) {
|
|
185
|
-
|
|
186
|
-
// Attempt availability check as fallback (mirrors execute() pattern)
|
|
187
|
-
const available = await this.isAvailable();
|
|
188
|
-
if (!available || !this.gatewayConfig) {
|
|
189
|
-
return { success: false, output: '', error: 'OpenClaw gateway not available' };
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
// Use the preserved session key, or resolve from the provider sessionId.
|
|
193
|
-
// The sessionId may already be a valid session key (e.g. 'astro:task:...' or
|
|
194
|
-
// 'agent:main:astro:task:...'), so don't blindly wrap it in 'astro:task:'.
|
|
209
|
+
// Use the preserved session key, or resolve from the provider sessionId
|
|
195
210
|
const session = this.preservedSessions.get(taskId);
|
|
196
211
|
const sessionKey = session?.sessionKey || this.resolveSessionKey(sessionId);
|
|
197
212
|
this.activeTasks++;
|
|
198
|
-
let ws;
|
|
199
213
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
214
|
+
let result;
|
|
215
|
+
if (this.bridge?.isConnected) {
|
|
216
|
+
stream.status('running', 5, 'Resuming OpenClaw session');
|
|
217
|
+
result = await this.bridge.sendChatMessage(sessionKey, message, stream, signal);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// Fallback: standalone connection for resume
|
|
221
|
+
if (!this.gatewayConfig) {
|
|
222
|
+
const available = await this.isAvailable();
|
|
223
|
+
if (!available || !this.gatewayConfig) {
|
|
224
|
+
return { success: false, output: '', error: 'OpenClaw gateway not available' };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
stream.status('running', 5, 'Resuming OpenClaw session');
|
|
228
|
+
result = await this.runViaStandaloneConnection(sessionKey, message, stream, signal);
|
|
229
|
+
}
|
|
206
230
|
// Update preserved session timestamp
|
|
207
231
|
if (session) {
|
|
208
232
|
session.createdAt = Date.now();
|
|
@@ -214,7 +238,6 @@ export class OpenClawAdapter {
|
|
|
214
238
|
};
|
|
215
239
|
}
|
|
216
240
|
catch (error) {
|
|
217
|
-
ws?.close();
|
|
218
241
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
219
242
|
this.lastError = errorMsg;
|
|
220
243
|
return { success: false, output: '', error: errorMsg };
|
|
@@ -225,7 +248,6 @@ export class OpenClawAdapter {
|
|
|
225
248
|
}
|
|
226
249
|
/**
|
|
227
250
|
* Mid-execution message injection is not supported for OpenClaw gateway.
|
|
228
|
-
* The gateway processes one chat.send at a time per session.
|
|
229
251
|
*/
|
|
230
252
|
async injectMessage(_taskId, _content, _interrupt) {
|
|
231
253
|
return false;
|
|
@@ -256,7 +278,6 @@ export class OpenClawAdapter {
|
|
|
256
278
|
// ─── Summary Generation ──────────────────────────────────────
|
|
257
279
|
/**
|
|
258
280
|
* Generate a structured execution summary by resuming the completed OpenClaw session.
|
|
259
|
-
* Sends the summary prompt to the same session key via chat.send.
|
|
260
281
|
*/
|
|
261
282
|
async generateSummary(taskId, workingDirectory) {
|
|
262
283
|
const session = this.preservedSessions.get(taskId);
|
|
@@ -264,9 +285,9 @@ export class OpenClawAdapter {
|
|
|
264
285
|
console.log(`[openclaw] No session to resume for summary (task ${taskId})`);
|
|
265
286
|
return undefined;
|
|
266
287
|
}
|
|
267
|
-
if (!this.gatewayConfig) {
|
|
288
|
+
if (!this.bridge?.isConnected && !this.gatewayConfig) {
|
|
268
289
|
const available = await this.isAvailable();
|
|
269
|
-
if (!available
|
|
290
|
+
if (!available) {
|
|
270
291
|
console.warn(`[openclaw] Gateway not available for summary generation (task ${taskId})`);
|
|
271
292
|
return undefined;
|
|
272
293
|
}
|
|
@@ -287,10 +308,6 @@ export class OpenClawAdapter {
|
|
|
287
308
|
}
|
|
288
309
|
/**
|
|
289
310
|
* Resolve a provider session ID to a valid OpenClaw session key.
|
|
290
|
-
* The sessionId from the frontend may be:
|
|
291
|
-
* - 'astro:task:{taskId}' (direct)
|
|
292
|
-
* - 'agent:main:astro:task:{taskId}' (gateway-prefixed)
|
|
293
|
-
* Avoid double-wrapping by checking for existing prefixes.
|
|
294
311
|
*/
|
|
295
312
|
resolveSessionKey(sessionId) {
|
|
296
313
|
if (sessionId.startsWith('astro:task:'))
|
|
@@ -300,137 +317,19 @@ export class OpenClawAdapter {
|
|
|
300
317
|
return stripped;
|
|
301
318
|
return `astro:task:${sessionId}`;
|
|
302
319
|
}
|
|
303
|
-
// ───
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return null;
|
|
313
|
-
const token = raw?.gateway?.auth?.token || '';
|
|
314
|
-
const bind = raw?.gateway?.bind || '127.0.0.1';
|
|
315
|
-
const host = bind === 'loopback' || bind === '127.0.0.1' ? '127.0.0.1' : bind;
|
|
316
|
-
return {
|
|
317
|
-
port,
|
|
318
|
-
token,
|
|
319
|
-
url: `ws://${host}:${port}`,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
catch {
|
|
323
|
-
return null;
|
|
320
|
+
// ─── Standalone Connection (Fallback) ──────────────────────────
|
|
321
|
+
/**
|
|
322
|
+
* Execute via a standalone WebSocket connection when no bridge is available.
|
|
323
|
+
* This preserves backward compatibility for cases where the bridge isn't wired.
|
|
324
|
+
*/
|
|
325
|
+
async runViaStandaloneConnection(sessionKey, message, stream, signal, timeout) {
|
|
326
|
+
const config = this.gatewayConfig;
|
|
327
|
+
if (!config) {
|
|
328
|
+
throw new Error('Gateway config not available');
|
|
324
329
|
}
|
|
325
|
-
|
|
326
|
-
// ─── Gateway Probe ───────────────────────────────────────────────
|
|
327
|
-
probeGateway(config) {
|
|
328
|
-
return new Promise((resolve) => {
|
|
329
|
-
let resolved = false;
|
|
330
|
-
const done = (val) => { if (!resolved) {
|
|
331
|
-
resolved = true;
|
|
332
|
-
resolve(val);
|
|
333
|
-
} };
|
|
334
|
-
let ws;
|
|
335
|
-
const timeout = setTimeout(() => {
|
|
336
|
-
ws?.removeAllListeners();
|
|
337
|
-
ws?.close();
|
|
338
|
-
done(false);
|
|
339
|
-
}, 5000);
|
|
340
|
-
ws = new WebSocket(config.url);
|
|
341
|
-
ws.on('message', (data) => {
|
|
342
|
-
try {
|
|
343
|
-
const frame = JSON.parse(String(data));
|
|
344
|
-
if (frame.type === 'event' && frame.event === 'connect.challenge') {
|
|
345
|
-
clearTimeout(timeout);
|
|
346
|
-
ws.removeAllListeners();
|
|
347
|
-
ws.close();
|
|
348
|
-
done(true);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
catch {
|
|
352
|
-
// ignore
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
ws.on('error', () => {
|
|
356
|
-
clearTimeout(timeout);
|
|
357
|
-
ws?.removeAllListeners();
|
|
358
|
-
done(false);
|
|
359
|
-
});
|
|
360
|
-
ws.on('close', () => {
|
|
361
|
-
clearTimeout(timeout);
|
|
362
|
-
done(false);
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
// ─── Gateway Connection ──────────────────────────────────────────
|
|
367
|
-
connectToGateway(config) {
|
|
368
|
-
return new Promise((resolve, reject) => {
|
|
369
|
-
let ws;
|
|
370
|
-
const timeout = setTimeout(() => {
|
|
371
|
-
ws?.removeAllListeners();
|
|
372
|
-
ws?.close();
|
|
373
|
-
reject(new Error('Gateway connection timeout'));
|
|
374
|
-
}, CONNECT_TIMEOUT_MS);
|
|
375
|
-
ws = new WebSocket(config.url);
|
|
376
|
-
const handshakeHandler = (data) => {
|
|
377
|
-
try {
|
|
378
|
-
const frame = JSON.parse(String(data));
|
|
379
|
-
// Step 1: Receive challenge, send connect
|
|
380
|
-
if (frame.type === 'event' && frame.event === 'connect.challenge') {
|
|
381
|
-
ws.send(JSON.stringify({
|
|
382
|
-
type: 'req',
|
|
383
|
-
id: 'connect-1',
|
|
384
|
-
method: 'connect',
|
|
385
|
-
params: {
|
|
386
|
-
minProtocol: PROTOCOL_VERSION,
|
|
387
|
-
maxProtocol: PROTOCOL_VERSION,
|
|
388
|
-
client: {
|
|
389
|
-
id: 'gateway-client',
|
|
390
|
-
version: 'dev',
|
|
391
|
-
platform: process.platform,
|
|
392
|
-
mode: 'backend',
|
|
393
|
-
},
|
|
394
|
-
caps: ['tool-events'],
|
|
395
|
-
auth: { token: config.token },
|
|
396
|
-
role: 'operator',
|
|
397
|
-
scopes: ['operator.read', 'operator.write'],
|
|
398
|
-
},
|
|
399
|
-
}));
|
|
400
|
-
}
|
|
401
|
-
// Step 2: Receive connect response
|
|
402
|
-
if (frame.type === 'res' && frame.id === 'connect-1') {
|
|
403
|
-
clearTimeout(timeout);
|
|
404
|
-
ws.removeListener('message', handshakeHandler);
|
|
405
|
-
ws.removeListener('error', errorHandler);
|
|
406
|
-
if (frame.ok) {
|
|
407
|
-
resolve(ws);
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
ws.close();
|
|
411
|
-
reject(new Error(`Gateway handshake failed: ${frame.error?.message || 'unknown'}`));
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
catch {
|
|
416
|
-
// ignore parse errors during handshake
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
|
-
const errorHandler = (err) => {
|
|
420
|
-
clearTimeout(timeout);
|
|
421
|
-
ws?.removeListener('message', handshakeHandler);
|
|
422
|
-
reject(err);
|
|
423
|
-
};
|
|
424
|
-
ws.on('message', handshakeHandler);
|
|
425
|
-
ws.on('error', errorHandler);
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
// ─── Task Execution via Gateway ──────────────────────────────────
|
|
429
|
-
async runViaGateway(task, stream, signal) {
|
|
430
|
-
const ws = await this.connectToGateway(this.gatewayConfig);
|
|
330
|
+
const ws = await this.connectToGateway(config);
|
|
431
331
|
stream.status('running', 5, 'Connected to gateway');
|
|
432
332
|
return new Promise((resolve) => {
|
|
433
|
-
const sessionKey = `astro:task:${task.id}`;
|
|
434
333
|
const idempotencyKey = randomUUID();
|
|
435
334
|
const artifacts = [];
|
|
436
335
|
let outputText = '';
|
|
@@ -458,22 +357,17 @@ export class OpenClawAdapter {
|
|
|
458
357
|
metrics: lastMetrics,
|
|
459
358
|
});
|
|
460
359
|
};
|
|
461
|
-
/** Finish when both lifecycle.end and chat.final have been seen, or
|
|
462
|
-
* after a short grace period if only lifecycle.end arrived. */
|
|
463
360
|
const tryFinishAfterLifecycle = () => {
|
|
464
361
|
if (chatFinalReceived) {
|
|
465
362
|
finish();
|
|
466
363
|
}
|
|
467
364
|
else {
|
|
468
|
-
// Grace period: if chat.final doesn't arrive within 500ms, finish anyway
|
|
469
365
|
gracePeriodTimeout = setTimeout(() => { if (!finished)
|
|
470
366
|
finish(); }, 500);
|
|
471
367
|
}
|
|
472
368
|
};
|
|
473
|
-
// Handle abort
|
|
474
369
|
const abortHandler = () => {
|
|
475
370
|
if (runId) {
|
|
476
|
-
// Try to abort the chat
|
|
477
371
|
try {
|
|
478
372
|
ws.send(JSON.stringify({
|
|
479
373
|
type: 'req',
|
|
@@ -482,32 +376,21 @@ export class OpenClawAdapter {
|
|
|
482
376
|
params: { sessionKey },
|
|
483
377
|
}));
|
|
484
378
|
}
|
|
485
|
-
catch {
|
|
486
|
-
// ignore
|
|
487
|
-
}
|
|
379
|
+
catch { /* ignore */ }
|
|
488
380
|
}
|
|
489
381
|
finish('Task cancelled');
|
|
490
382
|
};
|
|
491
383
|
signal.addEventListener('abort', abortHandler);
|
|
492
|
-
// Handle timeout
|
|
493
384
|
let taskTimeout;
|
|
494
|
-
if (
|
|
495
|
-
taskTimeout = setTimeout(() => {
|
|
496
|
-
finish('Task timed out');
|
|
497
|
-
}, task.timeout);
|
|
385
|
+
if (timeout) {
|
|
386
|
+
taskTimeout = setTimeout(() => { finish('Task timed out'); }, timeout);
|
|
498
387
|
}
|
|
499
|
-
// Handle incoming events
|
|
500
388
|
ws.on('message', (data) => {
|
|
501
389
|
if (finished)
|
|
502
390
|
return;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
frame = JSON.parse(String(data));
|
|
506
|
-
}
|
|
507
|
-
catch {
|
|
391
|
+
const frame = parseGatewayFrame(data);
|
|
392
|
+
if (!frame)
|
|
508
393
|
return;
|
|
509
|
-
}
|
|
510
|
-
// Handle chat.send response
|
|
511
394
|
if (frame.type === 'res' && frame.id === 'chat-send-1') {
|
|
512
395
|
if (frame.ok) {
|
|
513
396
|
runId = frame.payload?.runId;
|
|
@@ -518,13 +401,10 @@ export class OpenClawAdapter {
|
|
|
518
401
|
}
|
|
519
402
|
return;
|
|
520
403
|
}
|
|
521
|
-
// Handle agent events
|
|
522
404
|
if (frame.type === 'event' && frame.event === 'agent') {
|
|
523
405
|
const p = frame.payload || {};
|
|
524
|
-
|
|
525
|
-
if (p.sessionKey !== `agent:main:${sessionKey}` && p.sessionKey !== sessionKey) {
|
|
406
|
+
if (!matchesSessionKey(p.sessionKey, sessionKey))
|
|
526
407
|
return;
|
|
527
|
-
}
|
|
528
408
|
const streamType = p.stream;
|
|
529
409
|
const eventData = p.data;
|
|
530
410
|
if (streamType === 'lifecycle') {
|
|
@@ -533,7 +413,6 @@ export class OpenClawAdapter {
|
|
|
533
413
|
stream.sessionInit(p.sessionKey || sessionKey, eventData?.model || undefined);
|
|
534
414
|
}
|
|
535
415
|
else if (phase === 'end') {
|
|
536
|
-
// Extract usage metrics from lifecycle.end if available
|
|
537
416
|
const usage = eventData?.usage;
|
|
538
417
|
const cost = (eventData?.total_cost_usd ?? eventData?.cost_usd);
|
|
539
418
|
const numTurns = eventData?.num_turns;
|
|
@@ -582,36 +461,27 @@ export class OpenClawAdapter {
|
|
|
582
461
|
}
|
|
583
462
|
return;
|
|
584
463
|
}
|
|
585
|
-
// Handle chat events (for final state + model info)
|
|
586
464
|
if (frame.type === 'event' && frame.event === 'chat') {
|
|
587
465
|
const p = frame.payload || {};
|
|
588
|
-
|
|
589
|
-
if (p.sessionKey !== `agent:main:${sessionKey}` && p.sessionKey !== sessionKey) {
|
|
466
|
+
if (!matchesSessionKey(p.sessionKey, sessionKey))
|
|
590
467
|
return;
|
|
591
|
-
|
|
592
|
-
const state = p.state;
|
|
593
|
-
if (state === 'final') {
|
|
468
|
+
if (p.state === 'final') {
|
|
594
469
|
chatFinalReceived = true;
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const content = message.content;
|
|
470
|
+
const chatMessage = p.message;
|
|
471
|
+
if (chatMessage) {
|
|
472
|
+
const content = chatMessage.content;
|
|
599
473
|
if (content) {
|
|
600
474
|
for (const block of content) {
|
|
601
|
-
if (block.type === 'text' && block.text) {
|
|
602
|
-
|
|
603
|
-
if (!outputText.includes(block.text)) {
|
|
604
|
-
outputText += block.text;
|
|
605
|
-
}
|
|
475
|
+
if (block.type === 'text' && block.text && !outputText.includes(block.text)) {
|
|
476
|
+
outputText += block.text;
|
|
606
477
|
}
|
|
607
478
|
}
|
|
608
479
|
}
|
|
609
480
|
}
|
|
610
|
-
// Extract model/usage from chat.final if not yet captured
|
|
611
481
|
if (!lastMetrics) {
|
|
612
482
|
const usage = p.usage;
|
|
613
483
|
const cost = (p.total_cost_usd ?? p.cost_usd);
|
|
614
|
-
const model = (p.model ?? message?.model);
|
|
484
|
+
const model = (p.model ?? p.message?.model);
|
|
615
485
|
if (usage || cost !== undefined || model) {
|
|
616
486
|
lastMetrics = {
|
|
617
487
|
inputTokens: usage?.input_tokens,
|
|
@@ -621,202 +491,94 @@ export class OpenClawAdapter {
|
|
|
621
491
|
};
|
|
622
492
|
}
|
|
623
493
|
}
|
|
624
|
-
// If lifecycle already ended, finish immediately
|
|
625
494
|
if (lifecycleEnded)
|
|
626
495
|
finish();
|
|
627
496
|
}
|
|
628
497
|
return;
|
|
629
498
|
}
|
|
630
|
-
// Handle tick/health/presence (ignore)
|
|
631
|
-
if (frame.type === 'event') {
|
|
632
|
-
const ignoredEvents = ['tick', 'health', 'presence', 'heartbeat'];
|
|
633
|
-
if (frame.event && ignoredEvents.includes(frame.event))
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
});
|
|
637
|
-
ws.on('close', () => {
|
|
638
|
-
if (!finished) {
|
|
639
|
-
finish('Gateway connection closed unexpectedly');
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
ws.on('error', (err) => {
|
|
643
|
-
if (!finished) {
|
|
644
|
-
finish(`Gateway WebSocket error: ${err.message}`);
|
|
645
|
-
}
|
|
646
499
|
});
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
:
|
|
651
|
-
// Prepend conversation history if available (fallback for multi-turn when
|
|
652
|
-
// session resume isn't used or preservedSessions lookup failed)
|
|
653
|
-
if (task.messages && task.messages.length > 0) {
|
|
654
|
-
const conversationContext = task.messages
|
|
655
|
-
.map(m => `${m.role === 'user' ? 'Human' : 'Assistant'}: ${m.content}`)
|
|
656
|
-
.join('\n\n');
|
|
657
|
-
effectivePrompt = `${conversationContext}\n\nHuman: ${effectivePrompt}`;
|
|
658
|
-
}
|
|
659
|
-
// Send chat.send
|
|
500
|
+
ws.on('close', () => { if (!finished)
|
|
501
|
+
finish('Gateway connection closed unexpectedly'); });
|
|
502
|
+
ws.on('error', (err) => { if (!finished)
|
|
503
|
+
finish(`Gateway WebSocket error: ${err.message}`); });
|
|
660
504
|
try {
|
|
661
505
|
ws.send(JSON.stringify({
|
|
662
506
|
type: 'req',
|
|
663
507
|
id: 'chat-send-1',
|
|
664
508
|
method: 'chat.send',
|
|
665
|
-
params: {
|
|
666
|
-
sessionKey,
|
|
667
|
-
message: effectivePrompt,
|
|
668
|
-
idempotencyKey,
|
|
669
|
-
},
|
|
509
|
+
params: { sessionKey, message, idempotencyKey },
|
|
670
510
|
}));
|
|
671
511
|
}
|
|
672
512
|
catch (err) {
|
|
673
513
|
finish(`Failed to send chat.send: ${err instanceof Error ? err.message : String(err)}`);
|
|
674
514
|
}
|
|
675
|
-
// Note: signal listener and timeout cleanup is handled in finish()
|
|
676
515
|
});
|
|
677
516
|
}
|
|
678
|
-
// ─── Reusable Chat Message Sender (for resume) ────────────────────
|
|
679
517
|
/**
|
|
680
|
-
*
|
|
681
|
-
* Used by resumeTask() to continue a conversation on the same sessionKey.
|
|
518
|
+
* Connect to gateway with full handshake (standalone fallback).
|
|
682
519
|
*/
|
|
683
|
-
|
|
684
|
-
return new Promise((resolve) => {
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
let finished = false;
|
|
688
|
-
let lifecycleEnded = false;
|
|
689
|
-
let chatFinalReceived = false;
|
|
690
|
-
let gracePeriodTimeout;
|
|
691
|
-
const finish = (error) => {
|
|
692
|
-
if (finished)
|
|
693
|
-
return;
|
|
694
|
-
finished = true;
|
|
695
|
-
signal.removeEventListener('abort', abortHandler);
|
|
696
|
-
if (gracePeriodTimeout)
|
|
697
|
-
clearTimeout(gracePeriodTimeout);
|
|
520
|
+
connectToGateway(config) {
|
|
521
|
+
return new Promise((resolve, reject) => {
|
|
522
|
+
const ws = new WebSocket(config.url);
|
|
523
|
+
const timeout = setTimeout(() => {
|
|
698
524
|
ws.removeAllListeners();
|
|
699
525
|
ws.close();
|
|
700
|
-
|
|
701
|
-
};
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
gracePeriodTimeout = setTimeout(() => { if (!finished)
|
|
708
|
-
finish(); }, 500);
|
|
709
|
-
}
|
|
710
|
-
};
|
|
711
|
-
const abortHandler = () => {
|
|
712
|
-
try {
|
|
526
|
+
reject(new Error('Gateway connection timeout'));
|
|
527
|
+
}, CONNECT_TIMEOUT_MS);
|
|
528
|
+
const handshakeHandler = (data) => {
|
|
529
|
+
const frame = parseGatewayFrame(data);
|
|
530
|
+
if (!frame)
|
|
531
|
+
return;
|
|
532
|
+
if (frame.type === 'event' && frame.event === 'connect.challenge') {
|
|
713
533
|
ws.send(JSON.stringify({
|
|
714
534
|
type: 'req',
|
|
715
|
-
id: '
|
|
716
|
-
method: '
|
|
717
|
-
params: {
|
|
535
|
+
id: 'connect-1',
|
|
536
|
+
method: 'connect',
|
|
537
|
+
params: {
|
|
538
|
+
minProtocol: PROTOCOL_VERSION,
|
|
539
|
+
maxProtocol: PROTOCOL_VERSION,
|
|
540
|
+
client: {
|
|
541
|
+
id: 'gateway-client',
|
|
542
|
+
version: 'dev',
|
|
543
|
+
platform: process.platform,
|
|
544
|
+
mode: 'backend',
|
|
545
|
+
},
|
|
546
|
+
caps: ['tool-events'],
|
|
547
|
+
auth: { token: config.token },
|
|
548
|
+
role: 'operator',
|
|
549
|
+
scopes: ['operator.read', 'operator.write'],
|
|
550
|
+
},
|
|
718
551
|
}));
|
|
719
552
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
return;
|
|
727
|
-
let frame;
|
|
728
|
-
try {
|
|
729
|
-
frame = JSON.parse(String(data));
|
|
730
|
-
}
|
|
731
|
-
catch {
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
if (frame.type === 'res' && frame.id === 'chat-resume-1') {
|
|
735
|
-
if (!frame.ok) {
|
|
736
|
-
finish(`Gateway rejected resume: ${frame.error?.message || 'unknown'}`);
|
|
737
|
-
}
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
if (frame.type === 'event' && frame.event === 'agent') {
|
|
741
|
-
const p = frame.payload || {};
|
|
742
|
-
if (p.sessionKey !== `agent:main:${sessionKey}` && p.sessionKey !== sessionKey)
|
|
743
|
-
return;
|
|
744
|
-
const streamType = p.stream;
|
|
745
|
-
const eventData = p.data;
|
|
746
|
-
if (streamType === 'lifecycle') {
|
|
747
|
-
const phase = eventData?.phase;
|
|
748
|
-
if (phase === 'end') {
|
|
749
|
-
lifecycleEnded = true;
|
|
750
|
-
tryFinishAfterLifecycle();
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
else if (streamType === 'assistant') {
|
|
754
|
-
const delta = eventData?.delta || eventData?.text;
|
|
755
|
-
if (delta) {
|
|
756
|
-
outputText += delta;
|
|
757
|
-
stream.text(delta);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
else if (streamType === 'tool_use') {
|
|
761
|
-
const toolName = eventData?.name || 'unknown';
|
|
762
|
-
stream.toolUse(toolName, eventData?.input || {});
|
|
763
|
-
}
|
|
764
|
-
else if (streamType === 'tool_result') {
|
|
765
|
-
const toolName = eventData?.name || 'unknown';
|
|
766
|
-
stream.toolResult(toolName, eventData?.result || '', eventData?.success !== false);
|
|
767
|
-
}
|
|
768
|
-
else if (streamType === 'file_change') {
|
|
769
|
-
const filePath = eventData?.path || eventData?.file;
|
|
770
|
-
if (filePath) {
|
|
771
|
-
const rawAction = eventData?.type || 'modified';
|
|
772
|
-
const action = (['created', 'modified', 'deleted'].includes(rawAction) ? rawAction : 'modified');
|
|
773
|
-
stream.fileChange(filePath, action);
|
|
774
|
-
}
|
|
553
|
+
if (frame.type === 'res' && frame.id === 'connect-1') {
|
|
554
|
+
clearTimeout(timeout);
|
|
555
|
+
ws.removeListener('message', handshakeHandler);
|
|
556
|
+
ws.removeListener('error', errorHandler);
|
|
557
|
+
if (frame.ok) {
|
|
558
|
+
resolve(ws);
|
|
775
559
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
const p = frame.payload || {};
|
|
780
|
-
if (p.sessionKey !== `agent:main:${sessionKey}` && p.sessionKey !== sessionKey)
|
|
781
|
-
return;
|
|
782
|
-
if (p.state === 'final') {
|
|
783
|
-
chatFinalReceived = true;
|
|
784
|
-
if (lifecycleEnded)
|
|
785
|
-
finish();
|
|
560
|
+
else {
|
|
561
|
+
ws.close();
|
|
562
|
+
reject(new Error(`Gateway handshake failed: ${frame.error?.message || 'unknown'}`));
|
|
786
563
|
}
|
|
787
|
-
return;
|
|
788
564
|
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
type: 'req',
|
|
798
|
-
id: 'chat-resume-1',
|
|
799
|
-
method: 'chat.send',
|
|
800
|
-
params: {
|
|
801
|
-
sessionKey,
|
|
802
|
-
message,
|
|
803
|
-
idempotencyKey,
|
|
804
|
-
},
|
|
805
|
-
}));
|
|
806
|
-
}
|
|
807
|
-
catch (err) {
|
|
808
|
-
finish(`Failed to send chat.send: ${err instanceof Error ? err.message : String(err)}`);
|
|
809
|
-
}
|
|
565
|
+
};
|
|
566
|
+
const errorHandler = (err) => {
|
|
567
|
+
clearTimeout(timeout);
|
|
568
|
+
ws?.removeListener('message', handshakeHandler);
|
|
569
|
+
reject(err);
|
|
570
|
+
};
|
|
571
|
+
ws.on('message', handshakeHandler);
|
|
572
|
+
ws.on('error', errorHandler);
|
|
810
573
|
});
|
|
811
574
|
}
|
|
812
575
|
// ─── LLM Task (Structured JSON via HTTP) ──────────────────────────
|
|
813
576
|
/**
|
|
814
577
|
* Use the Gateway's HTTP `POST /tools/invoke` endpoint for structured JSON
|
|
815
|
-
* plan generation.
|
|
816
|
-
* guaranteeing well-formed output without prompt engineering.
|
|
578
|
+
* plan generation.
|
|
817
579
|
*/
|
|
818
580
|
async runLlmTask(task, stream, signal) {
|
|
819
|
-
const config = this.gatewayConfig;
|
|
581
|
+
const config = this.gatewayConfig || readGatewayConfig();
|
|
820
582
|
if (!config) {
|
|
821
583
|
throw new Error('Gateway config not available');
|
|
822
584
|
}
|