scalix 0.0.1 → 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.
Files changed (57) hide show
  1. package/.env.example +29 -0
  2. package/CHANGELOG.md +122 -0
  3. package/LICENSE +100 -0
  4. package/README.md +300 -2
  5. package/bin/scalix.js +19 -0
  6. package/dist/agent-definitions.js +100 -0
  7. package/dist/agent-registry.js +226 -0
  8. package/dist/agent-replay.js +166 -0
  9. package/dist/agent.js +1130 -0
  10. package/dist/api-client.js +77 -0
  11. package/dist/auto-verify.js +136 -0
  12. package/dist/change-tracker.js +53 -0
  13. package/dist/cli.js +2454 -0
  14. package/dist/commands.js +71 -0
  15. package/dist/config.js +16 -0
  16. package/dist/constants.js +358 -0
  17. package/dist/context-builder.js +158 -0
  18. package/dist/context-manager.js +148 -0
  19. package/dist/context-retriever.js +257 -0
  20. package/dist/cost-tracker.js +85 -0
  21. package/dist/diff.js +140 -0
  22. package/dist/git-context.js +183 -0
  23. package/dist/git-undo.js +67 -0
  24. package/dist/grounding-checker.js +154 -0
  25. package/dist/headless.js +743 -0
  26. package/dist/hooks.js +100 -0
  27. package/dist/injection-detector.js +86 -0
  28. package/dist/ipc-server.js +232 -0
  29. package/dist/logger.js +45 -0
  30. package/dist/markdown.js +41 -0
  31. package/dist/mcp.js +49 -0
  32. package/dist/model-router.js +68 -0
  33. package/dist/plugins.js +132 -0
  34. package/dist/project-index.js +376 -0
  35. package/dist/project-memory.js +165 -0
  36. package/dist/safe-math.js +194 -0
  37. package/dist/sandbox.js +403 -0
  38. package/dist/scoped-rules.js +82 -0
  39. package/dist/sessions.js +106 -0
  40. package/dist/settings.js +260 -0
  41. package/dist/sub-agent.js +154 -0
  42. package/dist/task-board.js +105 -0
  43. package/dist/token-counter.js +47 -0
  44. package/dist/tool-helper.js +97 -0
  45. package/dist/tool-selector.js +77 -0
  46. package/dist/tools-advanced.js +487 -0
  47. package/dist/tools-browser.js +534 -0
  48. package/dist/tools-extra.js +390 -0
  49. package/dist/tools-vision.js +116 -0
  50. package/dist/tools-voice.js +147 -0
  51. package/dist/tools-web.js +346 -0
  52. package/dist/tools.js +380 -0
  53. package/dist/types.js +1 -0
  54. package/dist/update-checker.js +111 -0
  55. package/dist/worktree.js +172 -0
  56. package/docs/DISTRIBUTION.md +301 -0
  57. package/package.json +71 -6
@@ -0,0 +1,97 @@
1
+ export function tool(def) {
2
+ return def;
3
+ }
4
+ export function zodToJsonSchema(schema) {
5
+ return convertZod(schema);
6
+ }
7
+ function convertZod(schema) {
8
+ if (!schema || !schema._def)
9
+ return { type: 'object' };
10
+ const def = schema._def;
11
+ const typeName = def.typeName;
12
+ switch (typeName) {
13
+ case 'ZodObject': {
14
+ const shape = def.shape?.() ?? def.shape ?? {};
15
+ const properties = {};
16
+ const required = [];
17
+ for (const [key, value] of Object.entries(shape)) {
18
+ const prop = convertZod(value);
19
+ const isOptional = value?._def?.typeName === 'ZodOptional';
20
+ if (!isOptional)
21
+ required.push(key);
22
+ properties[key] = prop;
23
+ }
24
+ const result = { type: 'object', properties };
25
+ if (required.length > 0)
26
+ result.required = required;
27
+ return result;
28
+ }
29
+ case 'ZodString': {
30
+ const result = { type: 'string' };
31
+ if (def.description)
32
+ result.description = def.description;
33
+ for (const check of def.checks ?? []) {
34
+ if (check.kind === 'min')
35
+ result.minLength = check.value;
36
+ if (check.kind === 'max')
37
+ result.maxLength = check.value;
38
+ if (check.kind === 'url')
39
+ result.format = 'uri';
40
+ }
41
+ return result;
42
+ }
43
+ case 'ZodNumber': {
44
+ const result = { type: 'number' };
45
+ if (def.description)
46
+ result.description = def.description;
47
+ for (const check of def.checks ?? []) {
48
+ if (check.kind === 'min')
49
+ result.minimum = check.value;
50
+ if (check.kind === 'max')
51
+ result.maximum = check.value;
52
+ }
53
+ return result;
54
+ }
55
+ case 'ZodBoolean':
56
+ return { type: 'boolean', ...(def.description ? { description: def.description } : {}) };
57
+ case 'ZodArray': {
58
+ const result = { type: 'array', items: convertZod(def.type) };
59
+ if (def.description)
60
+ result.description = def.description;
61
+ return result;
62
+ }
63
+ case 'ZodOptional': {
64
+ const inner = convertZod(def.innerType);
65
+ if (def.description)
66
+ inner.description = def.description;
67
+ return inner;
68
+ }
69
+ case 'ZodDefault':
70
+ return convertZod(def.innerType);
71
+ case 'ZodEnum':
72
+ return { type: 'string', enum: def.values };
73
+ case 'ZodRecord': {
74
+ const result = { type: 'object', additionalProperties: convertZod(def.valueType) };
75
+ return result;
76
+ }
77
+ case 'ZodUnion':
78
+ case 'ZodDiscriminatedUnion': {
79
+ const options = def.options ?? [];
80
+ const schemas = options.map((o) => convertZod(o));
81
+ return schemas.length === 1 ? schemas[0] : { anyOf: schemas };
82
+ }
83
+ case 'ZodLiteral': {
84
+ const v = def.value;
85
+ return { type: typeof v === 'number' ? 'number' : typeof v === 'boolean' ? 'boolean' : 'string', const: v };
86
+ }
87
+ case 'ZodNullable': {
88
+ const inner = convertZod(def.innerType);
89
+ return { ...inner, nullable: true };
90
+ }
91
+ case 'ZodEffects':
92
+ case 'ZodPipeline':
93
+ return convertZod(def.schema ?? def.innerType);
94
+ default:
95
+ return { type: 'string' };
96
+ }
97
+ }
@@ -0,0 +1,77 @@
1
+ const CORE_TOOL_NAMES = new Set([
2
+ 'read_file',
3
+ 'write_file',
4
+ 'run_command',
5
+ 'list_files',
6
+ 'edit_file',
7
+ 'search_files',
8
+ 'find_files',
9
+ 'todo_write',
10
+ 'monitor',
11
+ ]);
12
+ const CONTEXT_PATTERNS = [
13
+ [/\b(search|find|grep|look\s+for|where)\b/i, /search|find|grep/],
14
+ [/\b(git|commit|diff|branch|push|pull|merge|rebase)\b/i, /git/],
15
+ [/\b(web|url|http|fetch|browse|search\s+online|internet)\b/i, /web|fetch/],
16
+ [/\b(file|read|write|edit|create|open|save)\b/i, /file|read|write|edit/],
17
+ [/\b(tree|structure|directory|folder|layout|project)\b/i, /tree|list|find/],
18
+ [/\b(math|calc|compute|formula|equation)\b/i, /calc/],
19
+ [/\b(time|date|now|today|clock)\b/i, /time/],
20
+ [/\b(line|lines|range|portion|section)\b/i, /lines|read/],
21
+ [/\b(notebook|ipynb)\b|\.ipynb/i, /notebook/],
22
+ [/\b(sub[\s_-]?agent|delegate|parallel|spawn)\b/i, /agent/],
23
+ [/\b(task|todo|plan|track|progress)\b/i, /todo/],
24
+ [/\b(background|process|pid|monitor)\b/i, /monitor/],
25
+ [/\b(run|exec|shell|command|terminal|bash|script)\b/i, /run_command/],
26
+ [/\b(mcp|server|connect|sse|integration)\b/i, /mcp/],
27
+ ];
28
+ function extractKeywords(text) {
29
+ const words = text.toLowerCase()
30
+ .replace(/[^a-z0-9_\s]/g, ' ')
31
+ .split(/\s+/)
32
+ .filter((w) => w.length > 2);
33
+ return new Set(words);
34
+ }
35
+ export class ToolSelector {
36
+ toolIndex = [];
37
+ index(tools) {
38
+ this.toolIndex = tools.map((t) => {
39
+ const name = t.function?.name ?? t.name ?? '';
40
+ const description = t.function?.description ?? t.description ?? '';
41
+ const keywords = extractKeywords(`${name} ${description}`);
42
+ return { tool: t, name, keywords, isCore: CORE_TOOL_NAMES.has(name) };
43
+ });
44
+ }
45
+ select(query, recentToolNames = [], maxTools = 64) {
46
+ if (this.toolIndex.length <= maxTools) {
47
+ return this.toolIndex.map((t) => t.tool);
48
+ }
49
+ const queryKeywords = extractKeywords(query);
50
+ const recentSet = new Set(recentToolNames);
51
+ const scored = this.toolIndex.map((info) => {
52
+ let score = 0;
53
+ if (info.isCore)
54
+ score += 10;
55
+ if (recentSet.has(info.name))
56
+ score += 5;
57
+ for (const kw of queryKeywords) {
58
+ if (info.keywords.has(kw))
59
+ score += 2;
60
+ for (const tk of info.keywords) {
61
+ if (tk.includes(kw) || kw.includes(tk))
62
+ score += 1;
63
+ }
64
+ }
65
+ for (const [queryPattern, namePattern] of CONTEXT_PATTERNS) {
66
+ if (queryPattern.test(query) && namePattern.test(info.name))
67
+ score += 3;
68
+ }
69
+ return { info, score };
70
+ });
71
+ scored.sort((a, b) => b.score - a.score);
72
+ return scored.slice(0, maxTools).map((s) => s.info.tool);
73
+ }
74
+ getToolNames() {
75
+ return this.toolIndex.map((t) => t.name);
76
+ }
77
+ }
@@ -0,0 +1,487 @@
1
+ import { tool } from './tool-helper.js';
2
+ import { z } from 'zod';
3
+ import fs from 'node:fs/promises';
4
+ import { sandboxPath } from './sandbox.js';
5
+ import { backgroundProcesses } from './tools.js';
6
+ import { spawnSubAgent, spawnParallelAgents } from './sub-agent.js';
7
+ import { logger } from './logger.js';
8
+ import { TOOL_DEFAULTS } from './constants.js';
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';
13
+ let _limits = DEFAULT_LIMITS;
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
+ }
47
+ let currentTodos = [];
48
+ export function getTodos() {
49
+ return [...currentTodos];
50
+ }
51
+ export function clearTodos() {
52
+ currentTodos = [];
53
+ }
54
+ // ─── Todo Write Tool ───
55
+ export const todoWriteTool = tool({
56
+ name: 'todo_write',
57
+ description: 'Create or update a task list for tracking multi-step work. Each call replaces the entire list. Use to plan, track progress, and show completed work.',
58
+ inputSchema: z.object({
59
+ todos: z.array(z.object({
60
+ id: z.string().min(1).max(50).describe('Unique task ID'),
61
+ task: z.string().min(1).max(500).describe('Task description'),
62
+ status: z.enum(['pending', 'in_progress', 'completed']).describe('Task status'),
63
+ })).describe('The complete task list (replaces previous)'),
64
+ }),
65
+ execute: async ({ todos }) => {
66
+ currentTodos = todos.map((t) => ({ id: t.id, task: t.task, status: t.status }));
67
+ const pending = currentTodos.filter(t => t.status === 'pending').length;
68
+ const inProgress = currentTodos.filter(t => t.status === 'in_progress').length;
69
+ const completed = currentTodos.filter(t => t.status === 'completed').length;
70
+ const activeTask = currentTodos.find(t => t.status === 'in_progress');
71
+ return {
72
+ success: true,
73
+ total: currentTodos.length,
74
+ pending,
75
+ inProgress,
76
+ completed,
77
+ activeForm: activeTask?.id ?? null,
78
+ todos: currentTodos,
79
+ };
80
+ },
81
+ });
82
+ function textToNotebookSource(text) {
83
+ if (!text)
84
+ return [];
85
+ const lines = text.split('\n');
86
+ return lines.map((l, i) => i < lines.length - 1 ? l + '\n' : l);
87
+ }
88
+ export const notebookEditTool = tool({
89
+ name: 'notebook_edit',
90
+ description: 'Edit Jupyter notebooks (.ipynb). Add, edit, delete, or move cells.',
91
+ inputSchema: z.object({
92
+ notebook_path: z.string().min(1).max(500).describe('Path to the .ipynb file'),
93
+ command: z.enum(['add_cell', 'edit_cell', 'delete_cell', 'move_cell']).describe('Operation to perform'),
94
+ cell_type: z.enum(['code', 'markdown']).optional().describe('Cell type (for add_cell)'),
95
+ cell_number: z.number().min(0).optional().describe('Cell index (0-based)'),
96
+ new_source: z.string().max(TOOL_DEFAULTS.maxNotebookSource).optional().describe('New cell content'),
97
+ position: z.number().min(0).optional().describe('Target position (for move_cell or add_cell insertion point)'),
98
+ }),
99
+ execute: async ({ notebook_path, command, cell_type, cell_number, new_source, position }) => {
100
+ try {
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
+ }
106
+ const raw = await fs.readFile(safePath, 'utf-8');
107
+ const notebook = JSON.parse(raw);
108
+ if (notebook.nbformat !== 4) {
109
+ return { error: `Unsupported notebook format: nbformat ${notebook.nbformat}` };
110
+ }
111
+ if (!Array.isArray(notebook.cells)) {
112
+ return { error: 'Invalid notebook: cells is not an array' };
113
+ }
114
+ const cells = notebook.cells;
115
+ switch (command) {
116
+ case 'add_cell': {
117
+ const resolvedType = cell_type || 'code';
118
+ if (resolvedType !== 'code' && resolvedType !== 'markdown' && resolvedType !== 'raw') {
119
+ return { error: `Invalid cell_type: "${resolvedType}". Must be "code", "markdown", or "raw".` };
120
+ }
121
+ const newCell = {
122
+ cell_type: resolvedType,
123
+ source: textToNotebookSource(new_source || ''),
124
+ metadata: {},
125
+ ...(resolvedType === 'code' ? { outputs: [], execution_count: null } : {}),
126
+ };
127
+ const insertAt = position !== undefined ? Math.min(position, cells.length) : cells.length;
128
+ cells.splice(insertAt, 0, newCell);
129
+ break;
130
+ }
131
+ case 'edit_cell': {
132
+ if (cell_number === undefined || cell_number < 0 || cell_number >= cells.length) {
133
+ return { error: `Invalid cell_number: ${cell_number}. Notebook has ${cells.length} cells.` };
134
+ }
135
+ if (new_source === undefined) {
136
+ return { error: 'new_source is required for edit_cell' };
137
+ }
138
+ cells[cell_number].source = textToNotebookSource(new_source);
139
+ if (cells[cell_number].cell_type === 'code') {
140
+ cells[cell_number].outputs = [];
141
+ cells[cell_number].execution_count = null;
142
+ }
143
+ break;
144
+ }
145
+ case 'delete_cell': {
146
+ if (cell_number === undefined || cell_number < 0 || cell_number >= cells.length) {
147
+ return { error: `Invalid cell_number: ${cell_number}. Notebook has ${cells.length} cells.` };
148
+ }
149
+ cells.splice(cell_number, 1);
150
+ break;
151
+ }
152
+ case 'move_cell': {
153
+ if (cell_number === undefined || cell_number < 0 || cell_number >= cells.length) {
154
+ return { error: `Invalid cell_number: ${cell_number}. Notebook has ${cells.length} cells.` };
155
+ }
156
+ const [cell] = cells.splice(cell_number, 1);
157
+ const maxPos = cells.length;
158
+ const targetPos = position !== undefined ? Math.min(position, maxPos) : maxPos;
159
+ cells.splice(targetPos, 0, cell);
160
+ break;
161
+ }
162
+ }
163
+ notebook.cells = cells;
164
+ await fs.writeFile(safePath, JSON.stringify(notebook, null, 1) + '\n', 'utf-8');
165
+ return { success: true, command, totalCells: cells.length, path: notebook_path };
166
+ }
167
+ catch (error) {
168
+ return { error: error.message };
169
+ }
170
+ },
171
+ });
172
+ // ─── Monitor Tool ───
173
+ export const monitorTool = tool({
174
+ name: 'monitor',
175
+ description: 'Check output and status of a background process started by run_command with run_in_background. Returns buffered stdout/stderr and process status.',
176
+ inputSchema: z.object({
177
+ pid: z.number().describe('Process ID returned by run_command'),
178
+ }),
179
+ execute: async ({ pid }) => {
180
+ const proc = backgroundProcesses.get(pid);
181
+ if (!proc) {
182
+ return { error: `No background process found with PID ${pid}. It may have already been cleaned up.` };
183
+ }
184
+ const isRunning = proc.exitCode === null;
185
+ const elapsed = Math.round((Date.now() - proc.startedAt) / 1000);
186
+ const stdout = proc.stdout.slice(proc.lastRead);
187
+ const stderr = proc.stderr.slice(proc.lastStderrRead ?? 0);
188
+ proc.lastRead = proc.stdout.length;
189
+ proc.lastStderrRead = proc.stderr.length;
190
+ const result = {
191
+ pid,
192
+ command: proc.command,
193
+ status: isRunning ? 'running' : 'exited',
194
+ exitCode: proc.exitCode,
195
+ elapsed: `${elapsed}s`,
196
+ stdout: stdout || '(no new output)',
197
+ stderr,
198
+ };
199
+ if (!isRunning) {
200
+ backgroundProcesses.delete(pid);
201
+ }
202
+ return result;
203
+ },
204
+ });
205
+ let parentAgentConfig = null;
206
+ export function setAgentToolConfig(config) {
207
+ parentAgentConfig = config;
208
+ }
209
+ function resolveAgentConfig(context) {
210
+ if (context?.agentConfig)
211
+ return context.agentConfig;
212
+ return parentAgentConfig;
213
+ }
214
+ export const agentTool = tool({
215
+ name: 'agent',
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.`,
221
+ inputSchema: z.object({
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/'),
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'),
235
+ }),
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
+ }
395
+ }
396
+ },
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
+ });
476
+ // ─── Export ───
477
+ export const advancedTools = [
478
+ todoWriteTool,
479
+ notebookEditTool,
480
+ monitorTool,
481
+ agentTool,
482
+ sendMessageTool,
483
+ taskCreateTool,
484
+ taskClaimTool,
485
+ taskUpdateTool,
486
+ taskListTool,
487
+ ];