morpheus-cli 0.7.7 → 0.8.2

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.
@@ -0,0 +1,276 @@
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
+ * - Reconnects existing entries whose connection details changed (host/port/tls/auth_token)
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 or reconnect existing ones whose connection details changed
227
+ for (const entry of config.entries) {
228
+ const isNew = !currentNames.has(entry.name);
229
+ const existingConn = this.connections.get(entry.name);
230
+ const changed = !isNew && existingConn && existingConn.hasEntryChanged(entry);
231
+ if (isNew || changed) {
232
+ if (changed && existingConn) {
233
+ await existingConn.disconnect().catch(() => { });
234
+ this.connections.delete(entry.name);
235
+ this.smiths.delete(entry.name);
236
+ this.display.log(`Smith '${entry.name}' reconnecting with updated config (hot-reload)`, {
237
+ source: 'SmithRegistry', level: 'info',
238
+ });
239
+ }
240
+ this.register(entry);
241
+ const connection = new SmithConnection(entry, this);
242
+ this.connections.set(entry.name, connection);
243
+ connection.connect().catch(err => {
244
+ this.display.log(`Failed to connect to Smith '${entry.name}': ${err.message}`, {
245
+ source: 'SmithRegistry', level: 'warning',
246
+ });
247
+ });
248
+ added.push(entry.name);
249
+ this.display.log(`Smith '${entry.name}' ${isNew ? 'added and connecting' : 'reconnecting'} (hot-reload)`, {
250
+ source: 'SmithRegistry', level: 'info',
251
+ });
252
+ }
253
+ }
254
+ return { added, removed };
255
+ }
256
+ /**
257
+ * Generate a system prompt section listing available Smiths.
258
+ * Injected into Oracle's system prompt.
259
+ */
260
+ getSystemPromptSection() {
261
+ const online = this.getOnline();
262
+ if (online.length === 0)
263
+ return '';
264
+ const lines = online.map(s => {
265
+ const caps = s.capabilities.length > 0 ? ` (capabilities: ${s.capabilities.join(', ')})` : '';
266
+ const os = s.stats?.os ? ` [${s.stats.os}]` : '';
267
+ return `- **${s.name}**: ${s.host}:${s.port}${os}${caps}`;
268
+ });
269
+ return `\n## Available Smiths (Remote Agents)
270
+ The following remote Smiths are online and can execute DevKit tasks on external machines:
271
+ ${lines.join('\n')}
272
+
273
+ Use "smith_delegate" tool to delegate a task to a specific Smith by name.
274
+ `;
275
+ }
276
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Wire protocol types for Morpheus ↔ Smith communication.
3
+ * All messages are JSON-serialized over WebSocket.
4
+ */
5
+ export const SMITH_PROTOCOL_VERSION = 1;
6
+ export const SMITH_DEFAULT_PORT = 7900;
@@ -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
  }
@@ -2,5 +2,6 @@
2
2
  export * from './morpheus-tools.js';
3
3
  export * from './apoc-tool.js';
4
4
  export * from './neo-tool.js';
5
+ export * from './smith-tool.js';
5
6
  export * from './chronos-tools.js';
6
7
  export * from './time-verify-tools.js';
@@ -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
+ });
@@ -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
  };