morpheus-cli 0.7.7 → 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/index.js +1 -0
- package/dist/runtime/tools/morpheus-tools.js +122 -0
- package/dist/runtime/tools/smith-tool.js +147 -0
- 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,265 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { DisplayManager } from '../display.js';
|
|
3
|
+
import { ConfigManager } from '../../config/manager.js';
|
|
4
|
+
import { SmithConnection } from './connection.js';
|
|
5
|
+
/**
|
|
6
|
+
* SmithRegistry — singleton that manages all Smith connections.
|
|
7
|
+
* Pattern follows ChannelRegistry from src/channels/registry.ts.
|
|
8
|
+
*/
|
|
9
|
+
export class SmithRegistry extends EventEmitter {
|
|
10
|
+
static instance;
|
|
11
|
+
smiths = new Map();
|
|
12
|
+
connections = new Map();
|
|
13
|
+
display = DisplayManager.getInstance();
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
}
|
|
17
|
+
static getInstance() {
|
|
18
|
+
if (!SmithRegistry.instance) {
|
|
19
|
+
SmithRegistry.instance = new SmithRegistry();
|
|
20
|
+
}
|
|
21
|
+
return SmithRegistry.instance;
|
|
22
|
+
}
|
|
23
|
+
/** Reset singleton (for testing) */
|
|
24
|
+
static resetInstance() {
|
|
25
|
+
if (SmithRegistry.instance) {
|
|
26
|
+
SmithRegistry.instance.removeAllListeners();
|
|
27
|
+
SmithRegistry.instance.smiths.clear();
|
|
28
|
+
SmithRegistry.instance.connections.clear();
|
|
29
|
+
}
|
|
30
|
+
SmithRegistry.instance = undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Register a Smith from config entry.
|
|
34
|
+
* Does NOT initiate connection — call connectAll() for that.
|
|
35
|
+
*/
|
|
36
|
+
register(entry) {
|
|
37
|
+
if (this.smiths.has(entry.name)) {
|
|
38
|
+
this.display.log(`Smith '${entry.name}' already registered, skipping.`, {
|
|
39
|
+
source: 'SmithRegistry',
|
|
40
|
+
level: 'warning',
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const info = {
|
|
45
|
+
name: entry.name,
|
|
46
|
+
host: entry.host,
|
|
47
|
+
port: entry.port,
|
|
48
|
+
state: 'offline',
|
|
49
|
+
capabilities: [],
|
|
50
|
+
};
|
|
51
|
+
this.smiths.set(entry.name, info);
|
|
52
|
+
this.display.log(`Smith '${entry.name}' registered (${entry.host}:${entry.port})`, {
|
|
53
|
+
source: 'SmithRegistry',
|
|
54
|
+
level: 'info',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Register a Smith that self-announced via HTTP handshake.
|
|
59
|
+
*/
|
|
60
|
+
registerFromHandshake(name, host, port, capabilities) {
|
|
61
|
+
const existing = this.smiths.get(name);
|
|
62
|
+
if (existing) {
|
|
63
|
+
// Update existing entry with new info
|
|
64
|
+
existing.host = host;
|
|
65
|
+
existing.port = port;
|
|
66
|
+
existing.capabilities = capabilities;
|
|
67
|
+
existing.state = 'online';
|
|
68
|
+
existing.lastSeen = new Date();
|
|
69
|
+
this.emit('smith:updated', name);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const info = {
|
|
73
|
+
name,
|
|
74
|
+
host,
|
|
75
|
+
port,
|
|
76
|
+
state: 'online',
|
|
77
|
+
capabilities,
|
|
78
|
+
lastSeen: new Date(),
|
|
79
|
+
};
|
|
80
|
+
this.smiths.set(name, info);
|
|
81
|
+
this.emit('smith:connected', name);
|
|
82
|
+
this.display.log(`Smith '${name}' self-registered (${host}:${port})`, {
|
|
83
|
+
source: 'SmithRegistry',
|
|
84
|
+
level: 'info',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Remove a Smith by name */
|
|
88
|
+
unregister(name) {
|
|
89
|
+
const connection = this.connections.get(name);
|
|
90
|
+
if (connection) {
|
|
91
|
+
connection.disconnect().catch(() => { });
|
|
92
|
+
this.connections.delete(name);
|
|
93
|
+
}
|
|
94
|
+
const removed = this.smiths.delete(name);
|
|
95
|
+
if (removed) {
|
|
96
|
+
this.emit('smith:disconnected', name);
|
|
97
|
+
this.display.log(`Smith '${name}' unregistered.`, {
|
|
98
|
+
source: 'SmithRegistry',
|
|
99
|
+
level: 'info',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return removed;
|
|
103
|
+
}
|
|
104
|
+
/** Get a specific Smith's info */
|
|
105
|
+
get(name) {
|
|
106
|
+
return this.smiths.get(name);
|
|
107
|
+
}
|
|
108
|
+
/** List all registered Smiths */
|
|
109
|
+
list() {
|
|
110
|
+
return Array.from(this.smiths.values());
|
|
111
|
+
}
|
|
112
|
+
/** List only online Smiths */
|
|
113
|
+
getOnline() {
|
|
114
|
+
return this.list().filter(s => s.state === 'online');
|
|
115
|
+
}
|
|
116
|
+
/** Get a SmithConnection by name (for sending messages) */
|
|
117
|
+
getConnection(name) {
|
|
118
|
+
return this.connections.get(name);
|
|
119
|
+
}
|
|
120
|
+
/** Store config_report received from the Smith */
|
|
121
|
+
updateConfig(name, config) {
|
|
122
|
+
const smith = this.smiths.get(name);
|
|
123
|
+
if (!smith)
|
|
124
|
+
return;
|
|
125
|
+
smith.config = config;
|
|
126
|
+
}
|
|
127
|
+
/** Update Smith state (called by SmithConnection) */
|
|
128
|
+
updateState(name, state, stats) {
|
|
129
|
+
const smith = this.smiths.get(name);
|
|
130
|
+
if (!smith)
|
|
131
|
+
return;
|
|
132
|
+
const previousState = smith.state;
|
|
133
|
+
smith.state = state;
|
|
134
|
+
if (stats)
|
|
135
|
+
smith.stats = stats;
|
|
136
|
+
if (state === 'online')
|
|
137
|
+
smith.lastSeen = new Date();
|
|
138
|
+
if (previousState !== state) {
|
|
139
|
+
this.emit(`smith:${state}`, name);
|
|
140
|
+
this.display.log(`Smith '${name}' state: ${previousState} → ${state}`, {
|
|
141
|
+
source: 'SmithRegistry',
|
|
142
|
+
level: state === 'error' ? 'warning' : 'info',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Initialize WebSocket connections to all configured Smiths */
|
|
147
|
+
async connectAll() {
|
|
148
|
+
const config = ConfigManager.getInstance().getSmithsConfig();
|
|
149
|
+
if (!config.enabled) {
|
|
150
|
+
this.display.log('Smiths subsystem disabled.', { source: 'SmithRegistry', level: 'info' });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Register all entries from config
|
|
154
|
+
for (const entry of config.entries) {
|
|
155
|
+
this.register(entry);
|
|
156
|
+
}
|
|
157
|
+
// Initiate connections in the background — don't block startup
|
|
158
|
+
for (const entry of config.entries) {
|
|
159
|
+
const connection = new SmithConnection(entry, this);
|
|
160
|
+
this.connections.set(entry.name, connection);
|
|
161
|
+
connection.connect().catch(err => {
|
|
162
|
+
this.display.log(`Failed to connect to Smith '${entry.name}': ${err.message}`, {
|
|
163
|
+
source: 'SmithRegistry',
|
|
164
|
+
level: 'warning',
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const total = this.smiths.size;
|
|
169
|
+
this.display.log(`Smiths registered: ${total} (connecting in background)`, {
|
|
170
|
+
source: 'SmithRegistry',
|
|
171
|
+
level: 'info',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** Disconnect all Smiths gracefully */
|
|
175
|
+
async disconnectAll() {
|
|
176
|
+
const disconnectPromises = [];
|
|
177
|
+
for (const [name, connection] of this.connections) {
|
|
178
|
+
disconnectPromises.push(connection.disconnect().catch(err => {
|
|
179
|
+
this.display.log(`Error disconnecting Smith '${name}': ${err.message}`, {
|
|
180
|
+
source: 'SmithRegistry',
|
|
181
|
+
level: 'warning',
|
|
182
|
+
});
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
await Promise.allSettled(disconnectPromises);
|
|
186
|
+
this.connections.clear();
|
|
187
|
+
// Mark all as offline
|
|
188
|
+
for (const smith of this.smiths.values()) {
|
|
189
|
+
smith.state = 'offline';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Hot-reload Smiths from current config.
|
|
194
|
+
* - Connects new entries that aren't yet registered
|
|
195
|
+
* - Disconnects entries that were removed from config
|
|
196
|
+
* - Leaves existing connections untouched
|
|
197
|
+
*/
|
|
198
|
+
async reload() {
|
|
199
|
+
const config = ConfigManager.getInstance().getSmithsConfig();
|
|
200
|
+
const added = [];
|
|
201
|
+
const removed = [];
|
|
202
|
+
if (!config.enabled) {
|
|
203
|
+
// Disabled — disconnect everything
|
|
204
|
+
const allNames = [...this.smiths.keys()];
|
|
205
|
+
await this.disconnectAll();
|
|
206
|
+
this.smiths.clear();
|
|
207
|
+
return { added: [], removed: allNames };
|
|
208
|
+
}
|
|
209
|
+
const configNames = new Set(config.entries.map(e => e.name));
|
|
210
|
+
const currentNames = new Set(this.smiths.keys());
|
|
211
|
+
// Remove Smiths no longer in config
|
|
212
|
+
for (const name of currentNames) {
|
|
213
|
+
if (!configNames.has(name)) {
|
|
214
|
+
const conn = this.connections.get(name);
|
|
215
|
+
if (conn) {
|
|
216
|
+
await conn.disconnect().catch(() => { });
|
|
217
|
+
this.connections.delete(name);
|
|
218
|
+
}
|
|
219
|
+
this.smiths.delete(name);
|
|
220
|
+
removed.push(name);
|
|
221
|
+
this.display.log(`Smith '${name}' removed (hot-reload)`, {
|
|
222
|
+
source: 'SmithRegistry', level: 'info',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Add new Smiths from config
|
|
227
|
+
for (const entry of config.entries) {
|
|
228
|
+
if (!currentNames.has(entry.name)) {
|
|
229
|
+
this.register(entry);
|
|
230
|
+
const connection = new SmithConnection(entry, this);
|
|
231
|
+
this.connections.set(entry.name, connection);
|
|
232
|
+
connection.connect().catch(err => {
|
|
233
|
+
this.display.log(`Failed to connect to Smith '${entry.name}': ${err.message}`, {
|
|
234
|
+
source: 'SmithRegistry', level: 'warning',
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
added.push(entry.name);
|
|
238
|
+
this.display.log(`Smith '${entry.name}' added and connecting (hot-reload)`, {
|
|
239
|
+
source: 'SmithRegistry', level: 'info',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { added, removed };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Generate a system prompt section listing available Smiths.
|
|
247
|
+
* Injected into Oracle's system prompt.
|
|
248
|
+
*/
|
|
249
|
+
getSystemPromptSection() {
|
|
250
|
+
const online = this.getOnline();
|
|
251
|
+
if (online.length === 0)
|
|
252
|
+
return '';
|
|
253
|
+
const lines = online.map(s => {
|
|
254
|
+
const caps = s.capabilities.length > 0 ? ` (capabilities: ${s.capabilities.join(', ')})` : '';
|
|
255
|
+
const os = s.stats?.os ? ` [${s.stats.os}]` : '';
|
|
256
|
+
return `- **${s.name}**: ${s.host}:${s.port}${os}${caps}`;
|
|
257
|
+
});
|
|
258
|
+
return `\n## Available Smiths (Remote Agents)
|
|
259
|
+
The following remote Smiths are online and can execute DevKit tasks on external machines:
|
|
260
|
+
${lines.join('\n')}
|
|
261
|
+
|
|
262
|
+
Use "smith_delegate" tool to delegate a task to a specific Smith by name.
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -4,6 +4,7 @@ import { Apoc } from '../apoc.js';
|
|
|
4
4
|
import { Neo } from '../neo.js';
|
|
5
5
|
import { Trinity } from '../trinity.js';
|
|
6
6
|
import { executeKeymakerTask } from '../keymaker.js';
|
|
7
|
+
import { SmithDelegator } from '../smiths/delegator.js';
|
|
7
8
|
import { TaskRepository } from './repository.js';
|
|
8
9
|
export class TaskWorker {
|
|
9
10
|
workerId;
|
|
@@ -93,6 +94,23 @@ export class TaskWorker {
|
|
|
93
94
|
});
|
|
94
95
|
break;
|
|
95
96
|
}
|
|
97
|
+
case 'smith': {
|
|
98
|
+
// Parse smith name from context JSON
|
|
99
|
+
let smithName = 'unknown';
|
|
100
|
+
if (task.context) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(task.context);
|
|
103
|
+
smithName = parsed.smith_name || parsed.smith || 'unknown';
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
smithName = task.context;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const delegator = SmithDelegator.getInstance();
|
|
110
|
+
const result = await delegator.delegate(smithName, task.input, task.context ?? undefined);
|
|
111
|
+
output = typeof result === 'string' ? result : JSON.stringify(result);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
96
114
|
default: {
|
|
97
115
|
throw new Error(`Unknown task agent: ${task.agent}`);
|
|
98
116
|
}
|
|
@@ -809,6 +809,126 @@ export const TrinityDbManageTool = tool(async ({ action, name, id, type, host, p
|
|
|
809
809
|
allow_ddl: z.boolean().optional(),
|
|
810
810
|
}),
|
|
811
811
|
});
|
|
812
|
+
// ─── Smith Management ─────────────────────────────────────────────────────────
|
|
813
|
+
export const SmithListTool = tool(async () => {
|
|
814
|
+
try {
|
|
815
|
+
const { SmithRegistry } = await import("../smiths/registry.js");
|
|
816
|
+
const smiths = SmithRegistry.getInstance().list();
|
|
817
|
+
const result = smiths.map((s) => ({
|
|
818
|
+
name: s.name,
|
|
819
|
+
host: s.host,
|
|
820
|
+
port: s.port,
|
|
821
|
+
state: s.state,
|
|
822
|
+
capabilities: s.capabilities,
|
|
823
|
+
lastSeen: s.lastSeen?.toISOString() ?? null,
|
|
824
|
+
error: s.error ?? null,
|
|
825
|
+
}));
|
|
826
|
+
return JSON.stringify(result);
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
return JSON.stringify({ error: `Failed to list Smiths: ${error.message}` });
|
|
830
|
+
}
|
|
831
|
+
}, {
|
|
832
|
+
name: "smith_list",
|
|
833
|
+
description: "Lists all registered Smith remote agents with their name, host, port, connection state (online/connecting/offline), capabilities, and last seen time.",
|
|
834
|
+
schema: z.object({}),
|
|
835
|
+
});
|
|
836
|
+
export const SmithManageTool = tool(async ({ action, name, host, port, auth_token }) => {
|
|
837
|
+
try {
|
|
838
|
+
const { SmithRegistry } = await import("../smiths/registry.js");
|
|
839
|
+
const { SmithDelegator } = await import("../smiths/delegator.js");
|
|
840
|
+
const configManager = ConfigManager.getInstance();
|
|
841
|
+
const requireName = () => {
|
|
842
|
+
if (!name)
|
|
843
|
+
throw new Error(`"name" is required for action "${action}"`);
|
|
844
|
+
return name;
|
|
845
|
+
};
|
|
846
|
+
switch (action) {
|
|
847
|
+
case "add": {
|
|
848
|
+
const smithName = requireName();
|
|
849
|
+
if (!host)
|
|
850
|
+
return JSON.stringify({ error: "host is required for add action" });
|
|
851
|
+
if (!auth_token)
|
|
852
|
+
return JSON.stringify({ error: "auth_token is required for add action" });
|
|
853
|
+
const entry = { name: smithName, host, port: port ?? 7900, auth_token };
|
|
854
|
+
const currentConfig = configManager.get();
|
|
855
|
+
const smithsConfig = configManager.getSmithsConfig();
|
|
856
|
+
if (smithsConfig.entries.some((e) => e.name === smithName)) {
|
|
857
|
+
return JSON.stringify({ error: `Smith "${smithName}" already exists` });
|
|
858
|
+
}
|
|
859
|
+
await configManager.save({
|
|
860
|
+
...currentConfig,
|
|
861
|
+
smiths: {
|
|
862
|
+
...smithsConfig,
|
|
863
|
+
entries: [...smithsConfig.entries, entry],
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
SmithRegistry.getInstance().register(entry);
|
|
867
|
+
// Hot-reload: connect the new Smith in background
|
|
868
|
+
SmithRegistry.getInstance().reload().catch(() => { });
|
|
869
|
+
return JSON.stringify({ success: true, message: `Smith "${smithName}" added` });
|
|
870
|
+
}
|
|
871
|
+
case "remove": {
|
|
872
|
+
const smithName = requireName();
|
|
873
|
+
const currentConfig = configManager.get();
|
|
874
|
+
const smithsConfig = configManager.getSmithsConfig();
|
|
875
|
+
const filtered = smithsConfig.entries.filter((e) => e.name !== smithName);
|
|
876
|
+
if (filtered.length === smithsConfig.entries.length) {
|
|
877
|
+
return JSON.stringify({ error: `Smith "${smithName}" not found in config` });
|
|
878
|
+
}
|
|
879
|
+
await configManager.save({
|
|
880
|
+
...currentConfig,
|
|
881
|
+
smiths: {
|
|
882
|
+
...smithsConfig,
|
|
883
|
+
entries: filtered,
|
|
884
|
+
},
|
|
885
|
+
});
|
|
886
|
+
SmithRegistry.getInstance().unregister(smithName);
|
|
887
|
+
// Hot-reload: disconnect removed Smith
|
|
888
|
+
SmithRegistry.getInstance().reload().catch(() => { });
|
|
889
|
+
return JSON.stringify({ success: true, message: `Smith "${smithName}" removed` });
|
|
890
|
+
}
|
|
891
|
+
case "ping": {
|
|
892
|
+
const smithName = requireName();
|
|
893
|
+
const result = await SmithDelegator.getInstance().ping(smithName);
|
|
894
|
+
return JSON.stringify(result);
|
|
895
|
+
}
|
|
896
|
+
case "enable": {
|
|
897
|
+
const currentConfig = configManager.get();
|
|
898
|
+
const smithsConfig = configManager.getSmithsConfig();
|
|
899
|
+
await configManager.save({
|
|
900
|
+
...currentConfig,
|
|
901
|
+
smiths: { ...smithsConfig, enabled: true },
|
|
902
|
+
});
|
|
903
|
+
return JSON.stringify({ success: true, message: "Smiths enabled" });
|
|
904
|
+
}
|
|
905
|
+
case "disable": {
|
|
906
|
+
const currentConfig = configManager.get();
|
|
907
|
+
const smithsConfig = configManager.getSmithsConfig();
|
|
908
|
+
await configManager.save({
|
|
909
|
+
...currentConfig,
|
|
910
|
+
smiths: { ...smithsConfig, enabled: false },
|
|
911
|
+
});
|
|
912
|
+
return JSON.stringify({ success: true, message: "Smiths disabled" });
|
|
913
|
+
}
|
|
914
|
+
default:
|
|
915
|
+
return JSON.stringify({ error: `Unknown action: ${action}` });
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
return JSON.stringify({ error: `Smith manage failed: ${error.message}` });
|
|
920
|
+
}
|
|
921
|
+
}, {
|
|
922
|
+
name: "smith_manage",
|
|
923
|
+
description: "Manage Smith remote agents: add a new Smith entry, remove an existing one, ping to test connectivity, or enable/disable the Smiths subsystem.",
|
|
924
|
+
schema: z.object({
|
|
925
|
+
action: z.enum(["add", "remove", "ping", "enable", "disable"]),
|
|
926
|
+
name: z.string().optional().describe("Smith name (required for add, remove, ping)"),
|
|
927
|
+
host: z.string().optional().describe("Smith host address (required for add)"),
|
|
928
|
+
port: z.number().int().optional().describe("Smith port (default 7900)"),
|
|
929
|
+
auth_token: z.string().optional().describe("Authentication token (required for add)"),
|
|
930
|
+
}),
|
|
931
|
+
});
|
|
812
932
|
// ─── Unified export ───────────────────────────────────────────────────────────
|
|
813
933
|
export const morpheusTools = [
|
|
814
934
|
ConfigQueryTool,
|
|
@@ -824,4 +944,6 @@ export const morpheusTools = [
|
|
|
824
944
|
WebhookManageTool,
|
|
825
945
|
TrinityDbListTool,
|
|
826
946
|
TrinityDbManageTool,
|
|
947
|
+
SmithListTool,
|
|
948
|
+
SmithManageTool,
|
|
827
949
|
];
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { tool } from "@langchain/core/tools";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { TaskRepository } from "../tasks/repository.js";
|
|
4
|
+
import { TaskRequestContext } from "../tasks/context.js";
|
|
5
|
+
import { DisplayManager } from "../display.js";
|
|
6
|
+
import { ConfigManager } from "../../config/manager.js";
|
|
7
|
+
import { SmithDelegator } from "../smiths/delegator.js";
|
|
8
|
+
import { SmithRegistry } from "../smiths/registry.js";
|
|
9
|
+
import { ChannelRegistry } from "../../channels/registry.js";
|
|
10
|
+
/**
|
|
11
|
+
* Returns true when Smiths are configured in sync mode (inline execution).
|
|
12
|
+
*/
|
|
13
|
+
function isSmithSync() {
|
|
14
|
+
const config = ConfigManager.getInstance().getSmithsConfig();
|
|
15
|
+
return config.execution_mode === 'sync';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Tool that Oracle uses to delegate tasks to a remote Smith agent.
|
|
19
|
+
* Each Smith is a DevKit executor running on an external machine.
|
|
20
|
+
* Oracle should call this when the user explicitly requests operations
|
|
21
|
+
* on a remote machine / specific Smith by name.
|
|
22
|
+
*/
|
|
23
|
+
export const SmithDelegateTool = tool(async ({ smith, task, context }) => {
|
|
24
|
+
try {
|
|
25
|
+
const display = DisplayManager.getInstance();
|
|
26
|
+
const registry = SmithRegistry.getInstance();
|
|
27
|
+
const smithInfo = registry.get(smith);
|
|
28
|
+
if (!smithInfo) {
|
|
29
|
+
const available = registry.list().map(s => `${s.name} (${s.state})`).join(', ');
|
|
30
|
+
return `❌ Smith '${smith}' not found. Available Smiths: ${available || 'none configured'}`;
|
|
31
|
+
}
|
|
32
|
+
if (smithInfo.state !== 'online') {
|
|
33
|
+
return `❌ Smith '${smith}' is currently ${smithInfo.state}. Cannot delegate task.`;
|
|
34
|
+
}
|
|
35
|
+
// ── Sync mode: execute inline and return result ──
|
|
36
|
+
if (isSmithSync()) {
|
|
37
|
+
display.log(`Smith '${smith}' executing synchronously: ${task.slice(0, 80)}...`, {
|
|
38
|
+
source: "SmithDelegateTool",
|
|
39
|
+
level: "info",
|
|
40
|
+
});
|
|
41
|
+
const ctx = TaskRequestContext.get();
|
|
42
|
+
// Notify originating channel
|
|
43
|
+
if (ctx?.origin_channel && ctx.origin_user_id && ctx.origin_channel !== 'api' && ctx.origin_channel !== 'ui') {
|
|
44
|
+
ChannelRegistry.sendToUser(ctx.origin_channel, ctx.origin_user_id, `🤖 Smith '${smith}' is executing your request...`)
|
|
45
|
+
.catch(() => { });
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const delegator = SmithDelegator.getInstance();
|
|
49
|
+
const result = await delegator.delegate(smith, task, context);
|
|
50
|
+
TaskRequestContext.incrementSyncDelegation();
|
|
51
|
+
display.log(`Smith '${smith}' sync execution completed.`, {
|
|
52
|
+
source: "SmithDelegateTool",
|
|
53
|
+
level: "info",
|
|
54
|
+
});
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
catch (syncErr) {
|
|
58
|
+
TaskRequestContext.incrementSyncDelegation();
|
|
59
|
+
display.log(`Smith '${smith}' sync execution failed: ${syncErr.message}`, {
|
|
60
|
+
source: "SmithDelegateTool",
|
|
61
|
+
level: "error",
|
|
62
|
+
});
|
|
63
|
+
return `❌ Smith '${smith}' error: ${syncErr.message}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ── Async mode (default): create background task ──
|
|
67
|
+
const existingAck = TaskRequestContext.findDuplicateDelegation("smith", task);
|
|
68
|
+
if (existingAck) {
|
|
69
|
+
display.log(`Smith delegation deduplicated. Reusing task ${existingAck.task_id}.`, {
|
|
70
|
+
source: "SmithDelegateTool",
|
|
71
|
+
level: "info",
|
|
72
|
+
});
|
|
73
|
+
return `Task ${existingAck.task_id} already queued for Smith '${smith}' execution.`;
|
|
74
|
+
}
|
|
75
|
+
if (!TaskRequestContext.canEnqueueDelegation()) {
|
|
76
|
+
display.log(`Smith delegation blocked by per-turn limit.`, {
|
|
77
|
+
source: "SmithDelegateTool",
|
|
78
|
+
level: "warning",
|
|
79
|
+
});
|
|
80
|
+
return "Delegation limit reached for this user turn. Split the request or wait for current tasks.";
|
|
81
|
+
}
|
|
82
|
+
const ctx = TaskRequestContext.get();
|
|
83
|
+
const repository = TaskRepository.getInstance();
|
|
84
|
+
const created = repository.createTask({
|
|
85
|
+
agent: "smith",
|
|
86
|
+
input: task,
|
|
87
|
+
context: context ? JSON.stringify({ smith_name: smith, context }) : JSON.stringify({ smith_name: smith }),
|
|
88
|
+
origin_channel: ctx?.origin_channel ?? "api",
|
|
89
|
+
session_id: ctx?.session_id ?? "default",
|
|
90
|
+
origin_message_id: ctx?.origin_message_id ?? null,
|
|
91
|
+
origin_user_id: ctx?.origin_user_id ?? null,
|
|
92
|
+
max_attempts: 3,
|
|
93
|
+
});
|
|
94
|
+
TaskRequestContext.setDelegationAck({ task_id: created.id, agent: "smith", task });
|
|
95
|
+
display.log(`Smith task created: ${created.id} → ${smith}`, {
|
|
96
|
+
source: "SmithDelegateTool",
|
|
97
|
+
level: "info",
|
|
98
|
+
meta: {
|
|
99
|
+
agent: "smith",
|
|
100
|
+
smith_name: smith,
|
|
101
|
+
origin_channel: created.origin_channel,
|
|
102
|
+
session_id: created.session_id,
|
|
103
|
+
input: created.input,
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return `Task ${created.id} queued for Smith '${smith}' execution.`;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const display = DisplayManager.getInstance();
|
|
110
|
+
display.log(`SmithDelegateTool error: ${err.message}`, { source: "SmithDelegateTool", level: "error" });
|
|
111
|
+
return `Smith task enqueue failed: ${err.message}`;
|
|
112
|
+
}
|
|
113
|
+
}, {
|
|
114
|
+
name: "smith_delegate",
|
|
115
|
+
description: `Delegate a task to a remote Smith agent running on an external machine.
|
|
116
|
+
|
|
117
|
+
Smiths are remote DevKit executors deployed on external servers, VMs, or containers.
|
|
118
|
+
Each Smith can execute filesystem, shell, git, network, and system commands on its host machine.
|
|
119
|
+
|
|
120
|
+
## When to use
|
|
121
|
+
- User asks to run something on a remote machine or mentions a Smith by name
|
|
122
|
+
- A mission requires operations on a remote environment (deploy, build, test, inspect)
|
|
123
|
+
- You need to coordinate work across multiple remote machines
|
|
124
|
+
|
|
125
|
+
## How to handle complex missions
|
|
126
|
+
For multi-step missions (e.g. "deploy the project", "run the test suite and fix failures"):
|
|
127
|
+
1. **Decompose** the mission into sequential subtasks
|
|
128
|
+
2. **Delegate one subtask at a time** — call this tool once per logical step
|
|
129
|
+
3. **Read the result** before proceeding — verify success before the next step
|
|
130
|
+
4. **Use the context field** to carry forward relevant state (e.g. "previous step: git pull succeeded, branch=main")
|
|
131
|
+
5. **Iterate** until the mission is complete or an unrecoverable error occurs
|
|
132
|
+
6. **Report** a clear summary of all steps taken and their outcomes
|
|
133
|
+
|
|
134
|
+
Do NOT batch an entire multi-step mission into a single task description — break it down so you can react to each result.
|
|
135
|
+
|
|
136
|
+
## Parameters
|
|
137
|
+
- smith: Name of the target Smith (must match a registered Smith)
|
|
138
|
+
- task: Clear natural-language description of the single step to execute
|
|
139
|
+
- context: State from previous steps relevant to this step
|
|
140
|
+
|
|
141
|
+
Available Smiths are listed in the system prompt under "Available Smiths".`,
|
|
142
|
+
schema: z.object({
|
|
143
|
+
smith: z.string().describe("Name of the target Smith agent"),
|
|
144
|
+
task: z.string().describe("Clear description of the task to execute on the remote machine **in the user's language**"),
|
|
145
|
+
context: z.string().optional().describe("Optional context from the conversation"),
|
|
146
|
+
}),
|
|
147
|
+
});
|
package/dist/types/config.js
CHANGED
|
@@ -77,5 +77,13 @@ export const DEFAULT_CONFIG = {
|
|
|
77
77
|
personality: 'data_specialist',
|
|
78
78
|
execution_mode: 'async',
|
|
79
79
|
},
|
|
80
|
+
smiths: {
|
|
81
|
+
enabled: false,
|
|
82
|
+
execution_mode: 'async',
|
|
83
|
+
heartbeat_interval_ms: 30000,
|
|
84
|
+
connection_timeout_ms: 10000,
|
|
85
|
+
task_timeout_ms: 60000,
|
|
86
|
+
entries: [],
|
|
87
|
+
},
|
|
80
88
|
verbose_mode: true,
|
|
81
89
|
};
|