scalix 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,12 +3,47 @@ import { z } from 'zod';
3
3
  import fs from 'node:fs/promises';
4
4
  import { sandboxPath } from './sandbox.js';
5
5
  import { backgroundProcesses } from './tools.js';
6
- import { spawnSubAgent } from './sub-agent.js';
6
+ import { spawnSubAgent, spawnParallelAgents } from './sub-agent.js';
7
7
  import { logger } from './logger.js';
8
8
  import { TOOL_DEFAULTS } from './constants.js';
9
9
  import { DEFAULT_LIMITS } from './settings.js';
10
+ import { AgentRegistry } from './agent-registry.js';
11
+ import { TaskBoard } from './task-board.js';
12
+ import { getAgentDefinitions, isBuiltInAgentType } from './agent-definitions.js';
10
13
  let _limits = DEFAULT_LIMITS;
11
14
  export function setAdvancedToolLimits(limits) { _limits = limits; }
15
+ const BUILT_IN_INSTRUCTIONS = {
16
+ plan: 'You are a planning assistant. Analyze the codebase and design an implementation approach. Be thorough and specific.',
17
+ explore: 'You are a code exploration assistant. Search and read files to answer questions. Be concise and factual.',
18
+ general: 'You are a focused assistant with full capabilities. Complete the task efficiently. You can spawn sub-agents if needed.',
19
+ };
20
+ async function resolveAgentType(subagentType) {
21
+ const agentType = subagentType || 'general';
22
+ if (isBuiltInAgentType(agentType)) {
23
+ const isReadOnly = agentType === 'explore' || agentType === 'plan';
24
+ return {
25
+ toolLevel: isReadOnly ? 'read-only' : 'all',
26
+ isReadOnly,
27
+ instructions: BUILT_IN_INSTRUCTIONS[agentType] || BUILT_IN_INSTRUCTIONS.general,
28
+ maxSteps: agentType === 'explore' ? _limits.exploreMaxSteps : _limits.subAgentMaxSteps,
29
+ allowSpawning: true,
30
+ };
31
+ }
32
+ const definitions = await getAgentDefinitions(process.cwd());
33
+ const def = definitions.get(agentType);
34
+ if (!def) {
35
+ throw new Error(`Unknown agent type "${agentType}". Built-in types: general, explore, plan. Check .scalix/agents/ for custom definitions.`);
36
+ }
37
+ return {
38
+ toolLevel: def.tools,
39
+ isReadOnly: def.tools === 'read-only',
40
+ instructions: def.instructions,
41
+ maxSteps: def.maxSteps ?? _limits.subAgentMaxSteps,
42
+ model: def.model,
43
+ allowSpawning: def.allowSpawning ?? true,
44
+ description: def.description,
45
+ };
46
+ }
12
47
  let currentTodos = [];
13
48
  export function getTodos() {
14
49
  return [...currentTodos];
@@ -64,6 +99,10 @@ export const notebookEditTool = tool({
64
99
  execute: async ({ notebook_path, command, cell_type, cell_number, new_source, position }) => {
65
100
  try {
66
101
  const safePath = sandboxPath(notebook_path);
102
+ const stat = await fs.stat(safePath);
103
+ if (stat.size > TOOL_DEFAULTS.readFileMaxSize) {
104
+ return { error: `Notebook too large (${Math.round(stat.size / 1024 / 1024)}MB). Max: ${Math.round(TOOL_DEFAULTS.readFileMaxSize / 1024 / 1024)}MB.` };
105
+ }
67
106
  const raw = await fs.readFile(safePath, 'utf-8');
68
107
  const notebook = JSON.parse(raw);
69
108
  if (notebook.nbformat !== 4) {
@@ -115,7 +154,6 @@ export const notebookEditTool = tool({
115
154
  return { error: `Invalid cell_number: ${cell_number}. Notebook has ${cells.length} cells.` };
116
155
  }
117
156
  const [cell] = cells.splice(cell_number, 1);
118
- // After splice, validate position against the reduced array
119
157
  const maxPos = cells.length;
120
158
  const targetPos = position !== undefined ? Math.min(position, maxPos) : maxPos;
121
159
  cells.splice(targetPos, 0, cell);
@@ -168,65 +206,282 @@ let parentAgentConfig = null;
168
206
  export function setAgentToolConfig(config) {
169
207
  parentAgentConfig = config;
170
208
  }
209
+ function resolveAgentConfig(context) {
210
+ if (context?.agentConfig)
211
+ return context.agentConfig;
212
+ return parentAgentConfig;
213
+ }
171
214
  export const agentTool = tool({
172
215
  name: 'agent',
173
- description: 'Spawn a sub-agent to handle a complex task in parallel or independently. The sub-agent has access to read/write tools but cannot spawn further agents. Returns the sub-agent\'s final response.',
216
+ description: `Spawn agents for complex tasks. Supports multiple actions:
217
+ - spawn (default): Create a sub-agent to handle a task independently. Sub-agents have full tool access including spawning further agents.
218
+ - message: Send a message to another active agent by ID.
219
+ - spawn_parallel: Spawn multiple agents in parallel. Returns results from all agents.
220
+ - list_agents: List all active and completed agents with their status and resource usage.`,
174
221
  inputSchema: z.object({
175
- prompt: z.string().min(1).max(10000).describe('The task for the sub-agent to perform'),
176
- description: z.string().min(1).max(100).describe('Short description of the task (3-5 words)'),
177
- subagent_type: z.enum(['general', 'explore', 'plan']).optional().describe('Agent type: general (read+write), explore (read-only), plan (read-only analysis)'),
222
+ prompt: z.string().min(1).max(10000).optional().describe('The task for the sub-agent (required for spawn)'),
223
+ description: z.string().min(1).max(100).optional().describe('Short description of the task (3-5 words)'),
224
+ subagent_type: z.string().max(50).optional().describe('Agent type: general (full access), explore (read-only), plan (read-only analysis), or a custom type from .scalix/agents/'),
178
225
  model: z.string().max(100).optional().describe('Model override for the sub-agent'),
226
+ action: z.enum(['spawn', 'message', 'spawn_parallel', 'list_agents']).optional().describe('Action to perform (default: spawn)'),
227
+ target_agent_id: z.string().optional().describe('Target agent ID for message action'),
228
+ message_content: z.string().max(5000).optional().describe('Message content for message action'),
229
+ parallel_tasks: z.array(z.object({
230
+ prompt: z.string().min(1).max(10000),
231
+ description: z.string().min(1).max(100),
232
+ subagent_type: z.string().max(50).optional(),
233
+ model: z.string().max(100).optional(),
234
+ })).optional().describe('Array of tasks for spawn_parallel action'),
179
235
  }),
180
- execute: async ({ prompt, description, subagent_type, model }) => {
181
- if (!parentAgentConfig) {
182
- return { error: 'Agent tool not configured. Parent agent config not set.' };
183
- }
184
- if (model && !parentAgentConfig.models.includes(model)) {
185
- return { error: `Unknown model "${model}". Available: ${parentAgentConfig.models.join(', ')}` };
186
- }
187
- const isReadOnly = subagent_type === 'explore' || subagent_type === 'plan';
188
- const toolLevel = isReadOnly ? 'read-only' : 'standard';
189
- // General sub-agents inherit parent's permission mode (capped at 'auto' — never yolo)
190
- // Read-only sub-agents always use 'plan' mode
191
- let subPermissionMode = 'plan';
192
- let subConfirmToolCall;
193
- if (!isReadOnly && parentAgentConfig.permissionMode) {
194
- const parentMode = parentAgentConfig.permissionMode;
195
- subPermissionMode = parentMode === 'yolo' ? 'auto' : parentMode;
196
- subConfirmToolCall = parentAgentConfig.confirmToolCall;
197
- }
198
- logger.info('Spawning sub-agent', { description, type: subagent_type || 'general', permissionMode: subPermissionMode });
199
- try {
200
- const result = await spawnSubAgent(parentAgentConfig, prompt, {
201
- model,
202
- tools: toolLevel,
203
- permissionMode: subPermissionMode,
204
- confirmToolCall: subConfirmToolCall,
205
- maxSteps: subagent_type === 'explore' ? _limits.exploreMaxSteps : _limits.subAgentMaxSteps,
206
- instructions: subagent_type === 'plan'
207
- ? 'You are a planning assistant. Analyze the codebase and design an implementation approach. Be thorough and specific.'
208
- : subagent_type === 'explore'
209
- ? 'You are a code exploration assistant. Search and read files to answer questions. Be concise and factual.'
210
- : 'You are a focused assistant. Complete the task efficiently. Be concise.',
211
- timeout: _limits.agentToolTimeoutMs,
212
- });
213
- return {
214
- description,
215
- response: result.response,
216
- model: result.model,
217
- steps: result.steps,
218
- tokenEstimate: result.tokenEstimate,
219
- };
220
- }
221
- catch (error) {
222
- return { error: `Sub-agent failed: ${error.message}` };
236
+ execute: async (args, context) => {
237
+ const config = resolveAgentConfig(context);
238
+ const agentId = context?.agentId;
239
+ const action = args.action || 'spawn';
240
+ switch (action) {
241
+ case 'list_agents': {
242
+ const registry = AgentRegistry.getInstance();
243
+ const active = registry.listActive();
244
+ const summary = registry.getUsageSummary();
245
+ return {
246
+ activeAgents: active.length,
247
+ totalTokens: summary.totalTokens,
248
+ tree: registry.getAgentTree(),
249
+ agents: summary.byAgent,
250
+ };
251
+ }
252
+ case 'message': {
253
+ if (!args.target_agent_id || !args.message_content) {
254
+ return { error: 'target_agent_id and message_content are required for message action' };
255
+ }
256
+ const registry = AgentRegistry.getInstance();
257
+ const result = registry.sendMessage(agentId || 'unknown', args.target_agent_id, args.message_content);
258
+ if ('error' in result)
259
+ return result;
260
+ return { success: true, messageId: result.id, to: args.target_agent_id };
261
+ }
262
+ case 'spawn_parallel': {
263
+ if (!config) {
264
+ return { error: 'Agent tool not configured. Parent agent config not set.' };
265
+ }
266
+ if (!args.parallel_tasks || args.parallel_tasks.length === 0) {
267
+ return { error: 'parallel_tasks array is required for spawn_parallel action' };
268
+ }
269
+ const tasks = await Promise.all(args.parallel_tasks.map(async (task) => {
270
+ let resolved;
271
+ try {
272
+ resolved = await resolveAgentType(task.subagent_type);
273
+ }
274
+ catch (err) {
275
+ return { error: `Invalid agent type "${task.subagent_type}": ${err.message}` };
276
+ }
277
+ const effectiveModel = task.model || resolved.model;
278
+ const subPermissionMode = resolved.isReadOnly ? 'plan' : (config.permissionMode ?? 'default');
279
+ const subConfirmToolCall = resolved.isReadOnly ? undefined : config.confirmToolCall;
280
+ return {
281
+ prompt: task.prompt,
282
+ description: task.description,
283
+ options: {
284
+ model: effectiveModel,
285
+ tools: resolved.toolLevel,
286
+ permissionMode: subPermissionMode,
287
+ confirmToolCall: subConfirmToolCall,
288
+ allowAgentTool: resolved.allowSpawning,
289
+ parentAgentId: agentId,
290
+ agentDescription: task.description,
291
+ maxSteps: resolved.maxSteps,
292
+ instructions: resolved.instructions,
293
+ timeout: _limits.agentToolTimeoutMs,
294
+ },
295
+ };
296
+ }));
297
+ const validTasks = tasks.filter((t) => !t.error);
298
+ const errorTasks = tasks.filter((t) => t.error);
299
+ if (validTasks.length === 0) {
300
+ return { error: 'All parallel tasks failed validation', details: errorTasks };
301
+ }
302
+ logger.info('Spawning parallel agents', { count: validTasks.length, skipped: errorTasks.length });
303
+ const results = await spawnParallelAgents(config, validTasks);
304
+ const allResults = [
305
+ ...errorTasks.map((t, i) => ({
306
+ index: i,
307
+ description: t.error,
308
+ status: 'failed',
309
+ error: t.error,
310
+ })),
311
+ ...results.map((r, i) => {
312
+ if (r.status === 'fulfilled') {
313
+ return {
314
+ index: errorTasks.length + i,
315
+ description: validTasks[i].description,
316
+ status: 'completed',
317
+ response: r.value.response,
318
+ model: r.value.model,
319
+ steps: r.value.steps,
320
+ tokens: r.value.tokenEstimate,
321
+ agentId: r.value.agentId,
322
+ };
323
+ }
324
+ return {
325
+ index: errorTasks.length + i,
326
+ description: validTasks[i].description,
327
+ status: 'failed',
328
+ error: r.reason?.message || String(r.reason),
329
+ };
330
+ }),
331
+ ];
332
+ return {
333
+ total: allResults.length,
334
+ results: allResults,
335
+ };
336
+ }
337
+ case 'spawn':
338
+ default: {
339
+ if (!config) {
340
+ return { error: 'Agent tool not configured. Parent agent config not set.' };
341
+ }
342
+ if (!args.prompt) {
343
+ return { error: 'prompt is required for spawn action' };
344
+ }
345
+ if (!args.description) {
346
+ return { error: 'description is required for spawn action' };
347
+ }
348
+ let resolved;
349
+ try {
350
+ resolved = await resolveAgentType(args.subagent_type);
351
+ }
352
+ catch (err) {
353
+ return { error: err.message };
354
+ }
355
+ const effectiveModel = args.model || resolved.model;
356
+ if (effectiveModel && !config.models.includes(effectiveModel)) {
357
+ return { error: `Unknown model "${effectiveModel}". Available: ${config.models.join(', ')}` };
358
+ }
359
+ let subPermissionMode = resolved.isReadOnly ? 'plan' : (config.permissionMode ?? 'default');
360
+ let subConfirmToolCall;
361
+ if (!resolved.isReadOnly) {
362
+ subConfirmToolCall = config.confirmToolCall;
363
+ }
364
+ logger.info('Spawning sub-agent', {
365
+ description: args.description,
366
+ type: args.subagent_type || 'general',
367
+ permissionMode: subPermissionMode,
368
+ });
369
+ try {
370
+ const result = await spawnSubAgent(config, args.prompt, {
371
+ model: effectiveModel,
372
+ tools: resolved.toolLevel,
373
+ permissionMode: subPermissionMode,
374
+ confirmToolCall: subConfirmToolCall,
375
+ allowAgentTool: resolved.allowSpawning,
376
+ parentAgentId: agentId,
377
+ agentDescription: args.description,
378
+ maxSteps: resolved.maxSteps,
379
+ instructions: resolved.instructions,
380
+ timeout: _limits.agentToolTimeoutMs,
381
+ });
382
+ return {
383
+ description: args.description,
384
+ response: result.response,
385
+ model: result.model,
386
+ steps: result.steps,
387
+ tokenEstimate: result.tokenEstimate,
388
+ agentId: result.agentId,
389
+ };
390
+ }
391
+ catch (error) {
392
+ return { error: `Sub-agent failed: ${error.message}` };
393
+ }
394
+ }
223
395
  }
224
396
  },
225
397
  });
398
+ // ─── Send Message Tool ───
399
+ export const sendMessageTool = tool({
400
+ name: 'send_message',
401
+ description: 'Send a message to another active agent by ID. Use this for inter-agent communication and coordination.',
402
+ inputSchema: z.object({
403
+ to: z.string().min(1).describe('Target agent ID'),
404
+ content: z.string().min(1).max(5000).describe('Message content'),
405
+ }),
406
+ execute: async ({ to, content }, context) => {
407
+ const from = context?.agentId || 'unknown';
408
+ const registry = AgentRegistry.getInstance();
409
+ const result = registry.sendMessage(from, to, content);
410
+ if ('error' in result)
411
+ return result;
412
+ return { success: true, messageId: result.id, to, from };
413
+ },
414
+ });
415
+ // ─── Task Board Tools ───
416
+ export const taskCreateTool = tool({
417
+ name: 'task_create',
418
+ description: 'Create a new task on the shared task board. Tasks can be claimed and worked on by any agent.',
419
+ inputSchema: z.object({
420
+ title: z.string().min(1).max(200).describe('Task title'),
421
+ description: z.string().max(2000).optional().describe('Detailed task description'),
422
+ }),
423
+ execute: async ({ title, description }, context) => {
424
+ const agentId = context?.agentId || 'unknown';
425
+ const board = TaskBoard.getInstance();
426
+ const task = board.create(agentId, title, description);
427
+ return { success: true, task };
428
+ },
429
+ });
430
+ export const taskClaimTool = tool({
431
+ name: 'task_claim',
432
+ description: 'Claim an open task from the shared task board. Only open tasks can be claimed.',
433
+ inputSchema: z.object({
434
+ taskId: z.string().min(1).describe('Task ID to claim'),
435
+ }),
436
+ execute: async ({ taskId }, context) => {
437
+ const agentId = context?.agentId || 'unknown';
438
+ const board = TaskBoard.getInstance();
439
+ const result = board.claim(taskId, agentId);
440
+ if ('error' in result)
441
+ return result;
442
+ return { success: true, task: result };
443
+ },
444
+ });
445
+ export const taskUpdateTool = tool({
446
+ name: 'task_update',
447
+ description: 'Update a task\'s status or result on the shared task board. Use to mark tasks as in_progress, completed, or failed.',
448
+ inputSchema: z.object({
449
+ taskId: z.string().min(1).describe('Task ID to update'),
450
+ status: z.enum(['in_progress', 'completed', 'failed']).optional().describe('New task status'),
451
+ result: z.string().max(5000).optional().describe('Task result or output'),
452
+ }),
453
+ execute: async ({ taskId, status, result }, context) => {
454
+ const agentId = context?.agentId || 'unknown';
455
+ const board = TaskBoard.getInstance();
456
+ const updated = board.update(taskId, agentId, { status, result });
457
+ if ('error' in updated)
458
+ return updated;
459
+ return { success: true, task: updated };
460
+ },
461
+ });
462
+ export const taskListTool = tool({
463
+ name: 'task_list',
464
+ description: 'List tasks from the shared task board. Optionally filter by status or assignee.',
465
+ inputSchema: z.object({
466
+ status: z.enum(['open', 'claimed', 'in_progress', 'completed', 'failed']).optional().describe('Filter by status'),
467
+ assignee: z.string().optional().describe('Filter by assignee agent ID'),
468
+ }),
469
+ execute: async ({ status, assignee }) => {
470
+ const board = TaskBoard.getInstance();
471
+ const tasks = board.list({ status, assignee });
472
+ const stats = board.getStats();
473
+ return { tasks, stats };
474
+ },
475
+ });
226
476
  // ─── Export ───
227
477
  export const advancedTools = [
228
478
  todoWriteTool,
229
479
  notebookEditTool,
230
480
  monitorTool,
231
481
  agentTool,
482
+ sendMessageTool,
483
+ taskCreateTool,
484
+ taskClaimTool,
485
+ taskUpdateTool,
486
+ taskListTool,
232
487
  ];