morpheus-cli 0.7.6 → 0.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/README.md +58 -5
- package/dist/cli/commands/smiths.js +110 -0
- package/dist/cli/commands/start.js +20 -0
- package/dist/cli/index.js +2 -0
- package/dist/config/manager.js +22 -0
- package/dist/config/schemas.js +16 -0
- package/dist/http/api.js +3 -0
- package/dist/http/routers/smiths.js +188 -0
- package/dist/runtime/display.js +3 -0
- package/dist/runtime/oracle.js +16 -3
- package/dist/runtime/smiths/connection.js +295 -0
- package/dist/runtime/smiths/delegator.js +214 -0
- package/dist/runtime/smiths/index.js +4 -0
- package/dist/runtime/smiths/registry.js +265 -0
- package/dist/runtime/smiths/types.js +6 -0
- package/dist/runtime/tasks/worker.js +18 -0
- package/dist/runtime/tools/apoc-tool.js +19 -8
- package/dist/runtime/tools/index.js +1 -0
- package/dist/runtime/tools/morpheus-tools.js +122 -0
- package/dist/runtime/tools/neo-tool.js +24 -13
- package/dist/runtime/tools/smith-tool.js +147 -0
- package/dist/runtime/tools/trinity-tool.js +19 -8
- package/dist/types/config.js +8 -0
- package/dist/ui/assets/index-Clx8mDZ2.js +117 -0
- package/dist/ui/assets/index-KRT9p6jS.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +3 -1
- package/dist/ui/assets/index-B6deYCij.css +0 -1
- package/dist/ui/assets/index-BTQ0jjvm.js +0 -117
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { DisplayManager } from '../display.js';
|
|
3
|
+
import { ConfigManager } from '../../config/manager.js';
|
|
4
|
+
import { SMITH_PROTOCOL_VERSION } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* SmithConnection — WebSocket client wrapper for a single Morpheus → Smith connection.
|
|
7
|
+
* Handles connection lifecycle, reconnection with exponential backoff, auth, and heartbeat.
|
|
8
|
+
*/
|
|
9
|
+
export class SmithConnection {
|
|
10
|
+
ws = null;
|
|
11
|
+
entry;
|
|
12
|
+
registry;
|
|
13
|
+
display = DisplayManager.getInstance();
|
|
14
|
+
messageHandlers = [];
|
|
15
|
+
pendingTasks = new Map();
|
|
16
|
+
reconnectTimer = null;
|
|
17
|
+
heartbeatTimer = null;
|
|
18
|
+
reconnectAttempt = 0;
|
|
19
|
+
maxReconnectDelay = 30000;
|
|
20
|
+
intentionalClose = false;
|
|
21
|
+
_connected = false;
|
|
22
|
+
_authFailed = false;
|
|
23
|
+
constructor(entry, registry) {
|
|
24
|
+
this.entry = entry;
|
|
25
|
+
this.registry = registry;
|
|
26
|
+
}
|
|
27
|
+
get connected() {
|
|
28
|
+
return this._connected;
|
|
29
|
+
}
|
|
30
|
+
/** Connect to the Smith WebSocket server */
|
|
31
|
+
async connect() {
|
|
32
|
+
this.intentionalClose = false;
|
|
33
|
+
this.registry.updateState(this.entry.name, 'connecting');
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const config = ConfigManager.getInstance().getSmithsConfig();
|
|
36
|
+
const scheme = this.entry.tls ? 'wss' : 'ws';
|
|
37
|
+
const url = `${scheme}://${this.entry.host}:${this.entry.port}`;
|
|
38
|
+
try {
|
|
39
|
+
this.ws = new WebSocket(url, {
|
|
40
|
+
handshakeTimeout: config.connection_timeout_ms,
|
|
41
|
+
headers: {
|
|
42
|
+
'x-smith-auth': this.entry.auth_token,
|
|
43
|
+
'x-smith-protocol-version': String(SMITH_PROTOCOL_VERSION),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
this.registry.updateState(this.entry.name, 'error');
|
|
49
|
+
reject(err);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const connectionTimeout = setTimeout(() => {
|
|
53
|
+
if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
|
|
54
|
+
this.ws.terminate();
|
|
55
|
+
const err = new Error(`Connection timeout to Smith '${this.entry.name}'`);
|
|
56
|
+
this.registry.updateState(this.entry.name, 'error');
|
|
57
|
+
reject(err);
|
|
58
|
+
}
|
|
59
|
+
}, config.connection_timeout_ms);
|
|
60
|
+
this.ws.on('open', () => {
|
|
61
|
+
clearTimeout(connectionTimeout);
|
|
62
|
+
this._connected = true;
|
|
63
|
+
this.reconnectAttempt = 0;
|
|
64
|
+
this.registry.updateState(this.entry.name, 'online');
|
|
65
|
+
this.startHeartbeat();
|
|
66
|
+
this.display.log(`Connected to Smith '${this.entry.name}' at ${url}`, {
|
|
67
|
+
source: 'SmithConnection',
|
|
68
|
+
level: 'info',
|
|
69
|
+
});
|
|
70
|
+
resolve();
|
|
71
|
+
});
|
|
72
|
+
this.ws.on('message', (data) => {
|
|
73
|
+
try {
|
|
74
|
+
const message = JSON.parse(data.toString());
|
|
75
|
+
this.handleMessage(message);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
this.display.log(`Invalid message from Smith '${this.entry.name}': ${err.message}`, {
|
|
79
|
+
source: 'SmithConnection',
|
|
80
|
+
level: 'warning',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
this.ws.on('close', (code, reason) => {
|
|
85
|
+
clearTimeout(connectionTimeout);
|
|
86
|
+
this._connected = false;
|
|
87
|
+
this.stopHeartbeat();
|
|
88
|
+
this.registry.updateState(this.entry.name, 'offline');
|
|
89
|
+
// Reject all pending tasks
|
|
90
|
+
for (const [id, pending] of this.pendingTasks) {
|
|
91
|
+
clearTimeout(pending.timer);
|
|
92
|
+
pending.reject(new Error(`Connection to Smith '${this.entry.name}' closed`));
|
|
93
|
+
}
|
|
94
|
+
this.pendingTasks.clear();
|
|
95
|
+
if (!this.intentionalClose) {
|
|
96
|
+
this.display.log(`Smith '${this.entry.name}' disconnected (code: ${code}). Reconnecting...`, { source: 'SmithConnection', level: 'warning' });
|
|
97
|
+
this.scheduleReconnect();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
this.ws.on('error', (err) => {
|
|
101
|
+
clearTimeout(connectionTimeout);
|
|
102
|
+
// Detect 401 auth failures — no point retrying
|
|
103
|
+
if (err.message?.includes('401')) {
|
|
104
|
+
this._authFailed = true;
|
|
105
|
+
}
|
|
106
|
+
this.display.log(`WebSocket error with Smith '${this.entry.name}': ${err.message}`, {
|
|
107
|
+
source: 'SmithConnection',
|
|
108
|
+
level: 'error',
|
|
109
|
+
});
|
|
110
|
+
this.registry.updateState(this.entry.name, 'error');
|
|
111
|
+
// 'close' event will fire after 'error', which handles reconnection
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Send a message to the Smith */
|
|
116
|
+
send(message) {
|
|
117
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
118
|
+
throw new Error(`Smith '${this.entry.name}' is not connected`);
|
|
119
|
+
}
|
|
120
|
+
this.ws.send(JSON.stringify(message));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Send a task to the Smith and wait for the result.
|
|
124
|
+
* Returns a promise that resolves when the Smith sends a task_result.
|
|
125
|
+
*/
|
|
126
|
+
sendTask(taskId, tool, args) {
|
|
127
|
+
const config = ConfigManager.getInstance().getSmithsConfig();
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
this.pendingTasks.delete(taskId);
|
|
131
|
+
reject(new Error(`Task ${taskId} timed out on Smith '${this.entry.name}'`));
|
|
132
|
+
}, config.task_timeout_ms);
|
|
133
|
+
this.pendingTasks.set(taskId, { resolve, reject, timer });
|
|
134
|
+
try {
|
|
135
|
+
this.send({
|
|
136
|
+
type: 'task',
|
|
137
|
+
id: taskId,
|
|
138
|
+
payload: { tool, args },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
this.pendingTasks.delete(taskId);
|
|
144
|
+
reject(err);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** Register a handler for incoming messages */
|
|
149
|
+
onMessage(handler) {
|
|
150
|
+
this.messageHandlers.push(handler);
|
|
151
|
+
}
|
|
152
|
+
/** Remove a previously registered message handler */
|
|
153
|
+
offMessage(handler) {
|
|
154
|
+
const idx = this.messageHandlers.indexOf(handler);
|
|
155
|
+
if (idx !== -1)
|
|
156
|
+
this.messageHandlers.splice(idx, 1);
|
|
157
|
+
}
|
|
158
|
+
/** Disconnect gracefully */
|
|
159
|
+
async disconnect() {
|
|
160
|
+
this.intentionalClose = true;
|
|
161
|
+
this.stopHeartbeat();
|
|
162
|
+
if (this.reconnectTimer) {
|
|
163
|
+
clearTimeout(this.reconnectTimer);
|
|
164
|
+
this.reconnectTimer = null;
|
|
165
|
+
}
|
|
166
|
+
// Reject pending tasks
|
|
167
|
+
for (const [id, pending] of this.pendingTasks) {
|
|
168
|
+
clearTimeout(pending.timer);
|
|
169
|
+
pending.reject(new Error('Connection intentionally closed'));
|
|
170
|
+
}
|
|
171
|
+
this.pendingTasks.clear();
|
|
172
|
+
if (this.ws) {
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
175
|
+
this.ws.once('close', () => {
|
|
176
|
+
this._connected = false;
|
|
177
|
+
resolve();
|
|
178
|
+
});
|
|
179
|
+
this.ws.close(1000, 'Morpheus shutting down');
|
|
180
|
+
// Force close after 3s if server doesn't respond
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
if (this.ws?.readyState !== WebSocket.CLOSED) {
|
|
183
|
+
this.ws?.terminate();
|
|
184
|
+
}
|
|
185
|
+
this._connected = false;
|
|
186
|
+
resolve();
|
|
187
|
+
}, 3000);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
this.ws?.terminate();
|
|
191
|
+
this._connected = false;
|
|
192
|
+
resolve();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// ─── Private ───
|
|
198
|
+
handleMessage(message) {
|
|
199
|
+
switch (message.type) {
|
|
200
|
+
case 'task_result':
|
|
201
|
+
this.handleTaskResult(message);
|
|
202
|
+
break;
|
|
203
|
+
case 'pong':
|
|
204
|
+
this.handlePong(message);
|
|
205
|
+
break;
|
|
206
|
+
case 'register':
|
|
207
|
+
this.handleRegister(message);
|
|
208
|
+
break;
|
|
209
|
+
case 'config_report':
|
|
210
|
+
this.handleConfigReport(message);
|
|
211
|
+
break;
|
|
212
|
+
case 'task_progress':
|
|
213
|
+
// Forward to all handlers for real-time progress
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
// Forward to all registered handlers
|
|
217
|
+
for (const handler of this.messageHandlers) {
|
|
218
|
+
try {
|
|
219
|
+
handler(message);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
this.display.log(`Message handler error: ${err.message}`, {
|
|
223
|
+
source: 'SmithConnection',
|
|
224
|
+
level: 'warning',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
handleTaskResult(message) {
|
|
230
|
+
const pending = this.pendingTasks.get(message.id);
|
|
231
|
+
if (pending) {
|
|
232
|
+
clearTimeout(pending.timer);
|
|
233
|
+
this.pendingTasks.delete(message.id);
|
|
234
|
+
pending.resolve(message.result);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
handlePong(message) {
|
|
238
|
+
this.registry.updateState(this.entry.name, 'online', message.stats);
|
|
239
|
+
}
|
|
240
|
+
handleRegister(message) {
|
|
241
|
+
if (message.protocol_version !== SMITH_PROTOCOL_VERSION) {
|
|
242
|
+
this.display.log(`Smith '${this.entry.name}' protocol version mismatch: expected ${SMITH_PROTOCOL_VERSION}, got ${message.protocol_version}`, { source: 'SmithConnection', level: 'warning' });
|
|
243
|
+
}
|
|
244
|
+
const smith = this.registry.get(this.entry.name);
|
|
245
|
+
if (smith) {
|
|
246
|
+
smith.capabilities = message.capabilities;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
handleConfigReport(message) {
|
|
250
|
+
this.registry.updateConfig(this.entry.name, message.devkit);
|
|
251
|
+
}
|
|
252
|
+
startHeartbeat() {
|
|
253
|
+
const config = ConfigManager.getInstance().getSmithsConfig();
|
|
254
|
+
this.heartbeatTimer = setInterval(() => {
|
|
255
|
+
try {
|
|
256
|
+
this.send({ type: 'ping', timestamp: Date.now() });
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Connection may be broken — close event will handle reconnect
|
|
260
|
+
}
|
|
261
|
+
}, config.heartbeat_interval_ms);
|
|
262
|
+
}
|
|
263
|
+
stopHeartbeat() {
|
|
264
|
+
if (this.heartbeatTimer) {
|
|
265
|
+
clearInterval(this.heartbeatTimer);
|
|
266
|
+
this.heartbeatTimer = null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
scheduleReconnect() {
|
|
270
|
+
if (this.intentionalClose)
|
|
271
|
+
return;
|
|
272
|
+
// Auth failures won't self-resolve — don't retry
|
|
273
|
+
if (this._authFailed) {
|
|
274
|
+
this.display.log(`Smith '${this.entry.name}' — authentication failed (401). Check auth_token in config. Not retrying.`, { source: 'SmithConnection', level: 'error' });
|
|
275
|
+
this.registry.updateState(this.entry.name, 'offline');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// Exponential backoff: 1s → 2s → 4s → … → 30s (cap), then keeps retrying every 30s indefinitely.
|
|
279
|
+
// We never give up — when the Smith comes back online, Morpheus reconnects automatically.
|
|
280
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempt), this.maxReconnectDelay);
|
|
281
|
+
// Cap the exponent so the delay stays at maxReconnectDelay once reached
|
|
282
|
+
if (delay < this.maxReconnectDelay) {
|
|
283
|
+
this.reconnectAttempt++;
|
|
284
|
+
}
|
|
285
|
+
this.display.log(`Reconnecting to Smith '${this.entry.name}' in ${Math.round(delay / 1000)}s (attempt ${this.reconnectAttempt + 1})`, { source: 'SmithConnection', level: 'info' });
|
|
286
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
287
|
+
try {
|
|
288
|
+
await this.connect();
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// connect() failure will trigger 'close' → scheduleReconnect again
|
|
292
|
+
}
|
|
293
|
+
}, delay);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { tool } from '@langchain/core/tools';
|
|
3
|
+
import { HumanMessage, SystemMessage, AIMessage } from '@langchain/core/messages';
|
|
4
|
+
import { DisplayManager } from '../display.js';
|
|
5
|
+
import { SmithRegistry } from './registry.js';
|
|
6
|
+
import { ConfigManager } from '../../config/manager.js';
|
|
7
|
+
import { ProviderFactory } from '../providers/factory.js';
|
|
8
|
+
import { buildDevKit } from '../../devkit/index.js';
|
|
9
|
+
import { SQLiteChatMessageHistory } from '../memory/sqlite.js';
|
|
10
|
+
/**
|
|
11
|
+
* SmithDelegator — delegates natural-language tasks to a specific Smith.
|
|
12
|
+
*
|
|
13
|
+
* Works like Apoc: creates a LangChain ReactAgent with proxy tools
|
|
14
|
+
* that forward execution to the remote Smith via WebSocket.
|
|
15
|
+
* The LLM plans which DevKit tools to call, and each tool invocation
|
|
16
|
+
* is sent to the Smith for actual execution.
|
|
17
|
+
*/
|
|
18
|
+
export class SmithDelegator {
|
|
19
|
+
static instance;
|
|
20
|
+
display = DisplayManager.getInstance();
|
|
21
|
+
registry = SmithRegistry.getInstance();
|
|
22
|
+
constructor() { }
|
|
23
|
+
static getInstance() {
|
|
24
|
+
if (!SmithDelegator.instance) {
|
|
25
|
+
SmithDelegator.instance = new SmithDelegator();
|
|
26
|
+
}
|
|
27
|
+
return SmithDelegator.instance;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build proxy tools that forward calls to a Smith via WebSocket.
|
|
31
|
+
* Uses local DevKit schemas for tool definitions, filtered by Smith capabilities.
|
|
32
|
+
*/
|
|
33
|
+
buildProxyTools(smithName) {
|
|
34
|
+
const smith = this.registry.get(smithName);
|
|
35
|
+
if (!smith)
|
|
36
|
+
return [];
|
|
37
|
+
const connection = this.registry.getConnection(smithName);
|
|
38
|
+
if (!connection)
|
|
39
|
+
return [];
|
|
40
|
+
const capabilities = new Set(smith.capabilities);
|
|
41
|
+
// Build local DevKit tools for schema extraction only
|
|
42
|
+
const localTools = buildDevKit({
|
|
43
|
+
working_dir: process.cwd(),
|
|
44
|
+
allowed_commands: [],
|
|
45
|
+
timeout_ms: 30000,
|
|
46
|
+
sandbox_dir: process.cwd(),
|
|
47
|
+
readonly_mode: false,
|
|
48
|
+
enable_filesystem: true,
|
|
49
|
+
enable_shell: true,
|
|
50
|
+
enable_git: true,
|
|
51
|
+
enable_network: true,
|
|
52
|
+
});
|
|
53
|
+
// Create proxy tools — same schema, but execution forwards to Smith
|
|
54
|
+
return localTools
|
|
55
|
+
.filter(t => capabilities.has(t.name))
|
|
56
|
+
.map(localTool => tool(async (args) => {
|
|
57
|
+
const taskId = uuidv4();
|
|
58
|
+
this.display.log(`Smith '${smithName}' → ${localTool.name}`, {
|
|
59
|
+
source: 'SmithDelegator',
|
|
60
|
+
level: 'info',
|
|
61
|
+
});
|
|
62
|
+
const progressHandler = (msg) => {
|
|
63
|
+
if (msg.type === 'task_progress' && msg.id === taskId) {
|
|
64
|
+
this.display.log(`Smith '${smithName}' → ${msg.progress.message}`, {
|
|
65
|
+
source: 'SmithDelegator',
|
|
66
|
+
level: 'info',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
connection.onMessage(progressHandler);
|
|
71
|
+
try {
|
|
72
|
+
const result = await connection.sendTask(taskId, localTool.name, args);
|
|
73
|
+
if (result.success) {
|
|
74
|
+
return typeof result.data === 'string'
|
|
75
|
+
? result.data
|
|
76
|
+
: JSON.stringify(result.data, null, 2);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
return `Error: ${result.error}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
return `Error executing ${localTool.name} on Smith '${smithName}': ${err.message}`;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
connection.offMessage(progressHandler);
|
|
87
|
+
}
|
|
88
|
+
}, {
|
|
89
|
+
name: localTool.name,
|
|
90
|
+
description: localTool.description,
|
|
91
|
+
schema: localTool.schema,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Delegate a task to a specific Smith by name.
|
|
96
|
+
* Creates an LLM agent with proxy tools, plans tool calls, and executes on the Smith.
|
|
97
|
+
*/
|
|
98
|
+
async delegate(smithName, task, context) {
|
|
99
|
+
const smith = this.registry.get(smithName);
|
|
100
|
+
if (!smith) {
|
|
101
|
+
return `❌ Smith '${smithName}' not found. Available: ${this.registry.list().map(s => s.name).join(', ') || 'none'}`;
|
|
102
|
+
}
|
|
103
|
+
if (smith.state !== 'online') {
|
|
104
|
+
return `❌ Smith '${smithName}' is ${smith.state}. Cannot delegate.`;
|
|
105
|
+
}
|
|
106
|
+
const connection = this.registry.getConnection(smithName);
|
|
107
|
+
if (!connection || !connection.connected) {
|
|
108
|
+
return `❌ No active connection to Smith '${smithName}'.`;
|
|
109
|
+
}
|
|
110
|
+
this.display.log(`Delegating to Smith '${smithName}': ${task.slice(0, 100)}...`, {
|
|
111
|
+
source: 'SmithDelegator',
|
|
112
|
+
level: 'info',
|
|
113
|
+
meta: { smith: smithName },
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
// Build proxy tools for this Smith's capabilities
|
|
117
|
+
const proxyTools = this.buildProxyTools(smithName);
|
|
118
|
+
if (proxyTools.length === 0) {
|
|
119
|
+
return `❌ Smith '${smithName}' has no available tools.`;
|
|
120
|
+
}
|
|
121
|
+
// Create a fresh ReactAgent with proxy tools
|
|
122
|
+
const config = ConfigManager.getInstance().get();
|
|
123
|
+
const llmConfig = config.apoc || config.llm;
|
|
124
|
+
const agent = await ProviderFactory.createBare(llmConfig, proxyTools);
|
|
125
|
+
const osInfo = smith.stats?.os ? ` running ${smith.stats.os}` : '';
|
|
126
|
+
const hostname = smith.stats?.hostname ? ` (hostname: ${smith.stats.hostname})` : '';
|
|
127
|
+
const systemMessage = new SystemMessage(`You are a remote task executor for Smith '${smithName}'.
|
|
128
|
+
Your tools execute on a remote machine at ${smith.host}:${smith.port}${osInfo}${hostname}.
|
|
129
|
+
|
|
130
|
+
Execute the requested task using the available tools. Be direct and efficient.
|
|
131
|
+
If a task fails, report the error clearly.
|
|
132
|
+
Respond in the same language as the task.`);
|
|
133
|
+
const userContent = context
|
|
134
|
+
? `Context: ${context}\n\nTask: ${task}`
|
|
135
|
+
: task;
|
|
136
|
+
const messages = [systemMessage, new HumanMessage(userContent)];
|
|
137
|
+
const response = await agent.invoke({ messages });
|
|
138
|
+
// Extract final response
|
|
139
|
+
const lastMessage = response.messages[response.messages.length - 1];
|
|
140
|
+
const content = typeof lastMessage.content === 'string'
|
|
141
|
+
? lastMessage.content
|
|
142
|
+
: JSON.stringify(lastMessage.content);
|
|
143
|
+
// Persist token usage to session history
|
|
144
|
+
try {
|
|
145
|
+
const history = new SQLiteChatMessageHistory({ sessionId: 'smith' });
|
|
146
|
+
try {
|
|
147
|
+
const persisted = new AIMessage(content);
|
|
148
|
+
persisted.usage_metadata = lastMessage.usage_metadata
|
|
149
|
+
?? lastMessage.response_metadata?.usage
|
|
150
|
+
?? lastMessage.response_metadata?.tokenUsage
|
|
151
|
+
?? lastMessage.usage;
|
|
152
|
+
persisted.provider_metadata = {
|
|
153
|
+
provider: llmConfig.provider,
|
|
154
|
+
model: llmConfig.model,
|
|
155
|
+
};
|
|
156
|
+
await history.addMessage(persisted);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
history.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Non-critical — don't fail the delegation over token tracking
|
|
164
|
+
}
|
|
165
|
+
this.display.log(`Smith '${smithName}' delegation completed.`, {
|
|
166
|
+
source: 'SmithDelegator',
|
|
167
|
+
level: 'info',
|
|
168
|
+
});
|
|
169
|
+
return content;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
this.display.log(`Smith delegation error: ${err.message}`, {
|
|
173
|
+
source: 'SmithDelegator',
|
|
174
|
+
level: 'error',
|
|
175
|
+
});
|
|
176
|
+
return `❌ Smith '${smithName}' delegation failed: ${err.message}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Ping a specific Smith and return latency info.
|
|
181
|
+
*/
|
|
182
|
+
async ping(smithName) {
|
|
183
|
+
const smith = this.registry.get(smithName);
|
|
184
|
+
if (!smith) {
|
|
185
|
+
return { online: false, error: `Smith '${smithName}' not found` };
|
|
186
|
+
}
|
|
187
|
+
const connection = this.registry.getConnection(smithName);
|
|
188
|
+
if (!connection || !connection.connected) {
|
|
189
|
+
return { online: false, error: `No active connection to Smith '${smithName}'` };
|
|
190
|
+
}
|
|
191
|
+
const start = Date.now();
|
|
192
|
+
try {
|
|
193
|
+
connection.send({ type: 'ping', timestamp: start });
|
|
194
|
+
// Wait for pong (up to 5s)
|
|
195
|
+
return new Promise((resolve) => {
|
|
196
|
+
const handler = (msg) => {
|
|
197
|
+
if (msg.type === 'pong') {
|
|
198
|
+
clearTimeout(timeout);
|
|
199
|
+
connection.offMessage(handler);
|
|
200
|
+
resolve({ online: true, latencyMs: Date.now() - start });
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const timeout = setTimeout(() => {
|
|
204
|
+
connection.offMessage(handler);
|
|
205
|
+
resolve({ online: false, error: 'Ping timeout (5s)' });
|
|
206
|
+
}, 5000);
|
|
207
|
+
connection.onMessage(handler);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
return { online: false, error: err.message };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|