@compilr-dev/agents 0.2.1 → 0.3.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.
@@ -116,7 +116,7 @@ export const bashTool = defineTool({
116
116
  /**
117
117
  * Execute the bash tool
118
118
  */
119
- async function executeBash(input) {
119
+ async function executeBash(input, context) {
120
120
  const { command, cwd, timeout = DEFAULT_TIMEOUT, env, run_in_background } = input;
121
121
  // Detect FIFO usage and add warning
122
122
  const fifoCheck = detectFifoUsage(command);
@@ -124,6 +124,17 @@ async function executeBash(input) {
124
124
  if (run_in_background) {
125
125
  return executeInBackground(command, { cwd, env });
126
126
  }
127
+ // Use streaming execution if context.onOutput is provided
128
+ if (context?.onOutput) {
129
+ return executeWithStreaming(command, {
130
+ cwd,
131
+ timeout,
132
+ env,
133
+ onOutput: context.onOutput,
134
+ fifoCheck,
135
+ });
136
+ }
137
+ // Non-streaming execution (original behavior)
127
138
  try {
128
139
  const result = await execAsync(command, {
129
140
  cwd,
@@ -164,6 +175,154 @@ async function executeBash(input) {
164
175
  return createErrorResult(error instanceof Error ? error.message : String(error));
165
176
  }
166
177
  }
178
+ /**
179
+ * Execute command with streaming output to onOutput callback
180
+ */
181
+ async function executeWithStreaming(command, options) {
182
+ const { cwd, timeout = DEFAULT_TIMEOUT, env, onOutput, fifoCheck } = options;
183
+ return new Promise((resolve) => {
184
+ const child = spawn(command, [], {
185
+ cwd,
186
+ shell: '/bin/bash',
187
+ env: env ? { ...process.env, ...env } : undefined,
188
+ });
189
+ let stdoutBuffer = '';
190
+ let stderrBuffer = '';
191
+ let timedOut = false;
192
+ // Set up timeout
193
+ const timeoutId = timeout > 0
194
+ ? setTimeout(() => {
195
+ timedOut = true;
196
+ child.kill('SIGTERM');
197
+ // Force kill after 5 seconds if not dead
198
+ setTimeout(() => child.kill('SIGKILL'), 5000);
199
+ }, timeout)
200
+ : undefined;
201
+ // Stream stdout
202
+ child.stdout.on('data', (data) => {
203
+ const text = data.toString();
204
+ stdoutBuffer += text;
205
+ // Emit each line separately for better UI updates
206
+ const lines = text.split('\n');
207
+ for (const line of lines) {
208
+ if (line.length > 0) {
209
+ onOutput(line, 'stdout');
210
+ }
211
+ }
212
+ });
213
+ // Stream stderr
214
+ child.stderr.on('data', (data) => {
215
+ const text = data.toString();
216
+ stderrBuffer += text;
217
+ // Emit each line separately
218
+ const lines = text.split('\n');
219
+ for (const line of lines) {
220
+ if (line.length > 0) {
221
+ onOutput(line, 'stderr');
222
+ }
223
+ }
224
+ });
225
+ // Handle completion
226
+ child.on('close', (code) => {
227
+ if (timeoutId)
228
+ clearTimeout(timeoutId);
229
+ if (timedOut) {
230
+ resolve(createErrorResult(`Command timed out after ${String(timeout)}ms`));
231
+ return;
232
+ }
233
+ // Truncate for final result
234
+ const stdout = truncateOutput(stdoutBuffer, DEFAULT_MAX_OUTPUT_SIZE);
235
+ const stderr = truncateOutput(stderrBuffer, DEFAULT_MAX_OUTPUT_SIZE);
236
+ resolve(createSuccessResult({
237
+ stdout: stdout.content,
238
+ stderr: stderr.content,
239
+ exitCode: code ?? 0,
240
+ truncated: stdout.truncated || stderr.truncated,
241
+ ...(fifoCheck.detected && { fifoWarning: fifoCheck.warning }),
242
+ }));
243
+ });
244
+ // Handle spawn errors
245
+ child.on('error', (error) => {
246
+ if (timeoutId)
247
+ clearTimeout(timeoutId);
248
+ resolve(createErrorResult(error.message));
249
+ });
250
+ });
251
+ }
252
+ /**
253
+ * Execute command with streaming output (custom options version for createBashTool)
254
+ */
255
+ async function executeWithStreamingCustom(command, options) {
256
+ const { cwd, timeout = DEFAULT_TIMEOUT, env, onOutput, fifoCheck, maxOutputSize, shell, } = options;
257
+ return new Promise((resolve) => {
258
+ const child = spawn(command, [], {
259
+ cwd,
260
+ shell,
261
+ env: env ? { ...process.env, ...env } : undefined,
262
+ });
263
+ let stdoutBuffer = '';
264
+ let stderrBuffer = '';
265
+ let timedOut = false;
266
+ // Set up timeout
267
+ const timeoutId = timeout > 0
268
+ ? setTimeout(() => {
269
+ timedOut = true;
270
+ child.kill('SIGTERM');
271
+ // Force kill after 5 seconds if not dead
272
+ setTimeout(() => child.kill('SIGKILL'), 5000);
273
+ }, timeout)
274
+ : undefined;
275
+ // Stream stdout
276
+ child.stdout.on('data', (data) => {
277
+ const text = data.toString();
278
+ stdoutBuffer += text;
279
+ // Emit each line separately for better UI updates
280
+ const lines = text.split('\n');
281
+ for (const line of lines) {
282
+ if (line.length > 0) {
283
+ onOutput(line, 'stdout');
284
+ }
285
+ }
286
+ });
287
+ // Stream stderr
288
+ child.stderr.on('data', (data) => {
289
+ const text = data.toString();
290
+ stderrBuffer += text;
291
+ // Emit each line separately
292
+ const lines = text.split('\n');
293
+ for (const line of lines) {
294
+ if (line.length > 0) {
295
+ onOutput(line, 'stderr');
296
+ }
297
+ }
298
+ });
299
+ // Handle completion
300
+ child.on('close', (code) => {
301
+ if (timeoutId)
302
+ clearTimeout(timeoutId);
303
+ if (timedOut) {
304
+ resolve(createErrorResult(`Command timed out after ${String(timeout)}ms`));
305
+ return;
306
+ }
307
+ // Truncate for final result
308
+ const stdout = truncateOutput(stdoutBuffer, maxOutputSize);
309
+ const stderr = truncateOutput(stderrBuffer, maxOutputSize);
310
+ resolve(createSuccessResult({
311
+ stdout: stdout.content,
312
+ stderr: stderr.content,
313
+ exitCode: code ?? 0,
314
+ truncated: stdout.truncated || stderr.truncated,
315
+ ...(fifoCheck.detected && { fifoWarning: fifoCheck.warning }),
316
+ }));
317
+ });
318
+ // Handle spawn errors
319
+ child.on('error', (error) => {
320
+ if (timeoutId)
321
+ clearTimeout(timeoutId);
322
+ resolve(createErrorResult(error.message));
323
+ });
324
+ });
325
+ }
167
326
  /**
168
327
  * Execute command in background and return shell ID
169
328
  */
@@ -234,7 +393,7 @@ export function createBashTool(options) {
234
393
  },
235
394
  required: ['command'],
236
395
  },
237
- execute: async (input) => {
396
+ execute: async (input, context) => {
238
397
  const { cwd: defaultCwd, timeout: defaultTimeout = DEFAULT_TIMEOUT, maxOutputSize = DEFAULT_MAX_OUTPUT_SIZE, blockedCommands = [], restrictToAllowed = false, allowedCommands = [], shell = '/bin/bash', shellManager, fifoMode = 'warn', } = options ?? {};
239
398
  const command = input.command.trim();
240
399
  // Detect FIFO usage
@@ -274,19 +433,27 @@ export function createBashTool(options) {
274
433
  return createErrorResult(error instanceof Error ? error.message : String(error));
275
434
  }
276
435
  }
277
- // Execute with merged options
278
- const mergedInput = {
279
- ...input,
280
- cwd: input.cwd ?? defaultCwd,
281
- timeout: input.timeout ?? defaultTimeout,
282
- };
436
+ const timeout = input.timeout ?? defaultTimeout;
437
+ // Use streaming execution if context.onOutput is provided
438
+ if (context?.onOutput) {
439
+ return executeWithStreamingCustom(command, {
440
+ cwd: input.cwd ?? defaultCwd,
441
+ timeout,
442
+ env: input.env,
443
+ onOutput: context.onOutput,
444
+ fifoCheck,
445
+ maxOutputSize,
446
+ shell,
447
+ });
448
+ }
449
+ // Execute with merged options (non-streaming)
283
450
  try {
284
- const result = await execAsync(mergedInput.command, {
285
- cwd: mergedInput.cwd,
286
- timeout: mergedInput.timeout,
451
+ const result = await execAsync(command, {
452
+ cwd: input.cwd ?? defaultCwd,
453
+ timeout,
287
454
  maxBuffer: MAX_BUFFER_SIZE,
288
455
  shell,
289
- env: mergedInput.env ? { ...process.env, ...mergedInput.env } : undefined,
456
+ env: input.env ? { ...process.env, ...input.env } : undefined,
290
457
  });
291
458
  // Truncate output to prevent memory bloat
292
459
  const stdout = truncateOutput(result.stdout, maxOutputSize);
@@ -302,7 +469,7 @@ export function createBashTool(options) {
302
469
  catch (error) {
303
470
  if (isExecError(error)) {
304
471
  if (error.killed) {
305
- return createErrorResult(`Command timed out after ${String(mergedInput.timeout ?? defaultTimeout)}ms`);
472
+ return createErrorResult(`Command timed out after ${String(timeout)}ms`);
306
473
  }
307
474
  // Truncate output even on error
308
475
  const stdout = truncateOutput(error.stdout ?? '', maxOutputSize);
@@ -29,6 +29,12 @@ export { webFetchTool, createWebFetchTool } from './web-fetch.js';
29
29
  export type { WebFetchInput, WebFetchResult, WebFetchOptions } from './web-fetch.js';
30
30
  export { suggestTool, createSuggestTool } from './suggest.js';
31
31
  export type { SuggestInput, SuggestToolOptions } from './suggest.js';
32
+ export { askUserTool, createAskUserTool } from './ask-user.js';
33
+ export type { AskUserInput, AskUserResult, AskUserQuestion, AskUserOption, AskUserToolOptions, } from './ask-user.js';
34
+ export { askUserSimpleTool, createAskUserSimpleTool } from './ask-user-simple.js';
35
+ export type { AskUserSimpleInput, AskUserSimpleResult, AskUserSimpleToolOptions, } from './ask-user-simple.js';
36
+ export { backlogReadTool, backlogWriteTool, createBacklogTools } from './backlog.js';
37
+ export type { BacklogItem, BacklogStatus, BacklogItemType, BacklogPriority, BacklogReadInput, BacklogReadResult, BacklogWriteInput, BacklogWriteResult, BacklogToolOptions, } from './backlog.js';
32
38
  export declare const builtinTools: {
33
39
  readonly readFile: import("../types.js").Tool<import("./read-file.js").ReadFileInput>;
34
40
  readonly writeFile: import("../types.js").Tool<import("./write-file.js").WriteFileInput>;
@@ -42,8 +48,12 @@ export declare const builtinTools: {
42
48
  readonly todoRead: import("../types.js").Tool<import("./todo.js").TodoReadInput>;
43
49
  readonly webFetch: import("../types.js").Tool<import("./web-fetch.js").WebFetchInput>;
44
50
  readonly suggest: import("../types.js").Tool<import("./suggest.js").SuggestInput>;
51
+ readonly askUser: import("../types.js").Tool<import("./ask-user.js").AskUserInput>;
52
+ readonly askUserSimple: import("../types.js").Tool<import("./ask-user-simple.js").AskUserSimpleInput>;
53
+ readonly backlogRead: import("../types.js").Tool<import("./backlog.js").BacklogReadInput>;
54
+ readonly backlogWrite: import("../types.js").Tool<import("./backlog.js").BacklogWriteInput>;
45
55
  };
46
56
  /**
47
57
  * Array of all built-in tools
48
58
  */
49
- export declare const allBuiltinTools: readonly [import("../types.js").Tool<import("./read-file.js").ReadFileInput>, import("../types.js").Tool<import("./write-file.js").WriteFileInput>, import("../types.js").Tool<import("./bash.js").BashInput>, import("../types.js").Tool<import("./bash-output.js").BashOutputInput>, import("../types.js").Tool<import("./kill-shell.js").KillShellInput>, import("../types.js").Tool<import("./grep.js").GrepInput>, import("../types.js").Tool<import("./glob.js").GlobInput>, import("../types.js").Tool<import("./edit.js").EditInput>, import("../types.js").Tool<import("./todo.js").TodoWriteInput>, import("../types.js").Tool<import("./todo.js").TodoReadInput>, import("../types.js").Tool<import("./web-fetch.js").WebFetchInput>, import("../types.js").Tool<import("./suggest.js").SuggestInput>];
59
+ export declare const allBuiltinTools: readonly [import("../types.js").Tool<import("./read-file.js").ReadFileInput>, import("../types.js").Tool<import("./write-file.js").WriteFileInput>, import("../types.js").Tool<import("./bash.js").BashInput>, import("../types.js").Tool<import("./bash-output.js").BashOutputInput>, import("../types.js").Tool<import("./kill-shell.js").KillShellInput>, import("../types.js").Tool<import("./grep.js").GrepInput>, import("../types.js").Tool<import("./glob.js").GlobInput>, import("../types.js").Tool<import("./edit.js").EditInput>, import("../types.js").Tool<import("./todo.js").TodoWriteInput>, import("../types.js").Tool<import("./todo.js").TodoReadInput>, import("../types.js").Tool<import("./web-fetch.js").WebFetchInput>, import("../types.js").Tool<import("./suggest.js").SuggestInput>, import("../types.js").Tool<import("./ask-user.js").AskUserInput>, import("../types.js").Tool<import("./ask-user-simple.js").AskUserSimpleInput>, import("../types.js").Tool<import("./backlog.js").BacklogReadInput>, import("../types.js").Tool<import("./backlog.js").BacklogWriteInput>];
@@ -29,6 +29,11 @@ export { createTaskTool, defaultAgentTypes } from './task.js';
29
29
  export { webFetchTool, createWebFetchTool } from './web-fetch.js';
30
30
  // Suggest tool (next action suggestions)
31
31
  export { suggestTool, createSuggestTool } from './suggest.js';
32
+ // Ask user tools (user interaction)
33
+ export { askUserTool, createAskUserTool } from './ask-user.js';
34
+ export { askUserSimpleTool, createAskUserSimpleTool } from './ask-user-simple.js';
35
+ // Backlog tools (file-based project backlog)
36
+ export { backlogReadTool, backlogWriteTool, createBacklogTools } from './backlog.js';
32
37
  /**
33
38
  * Collection of all built-in tools for easy registration
34
39
  */
@@ -43,6 +48,9 @@ import { editTool } from './edit.js';
43
48
  import { todoWriteTool, todoReadTool } from './todo.js';
44
49
  import { webFetchTool } from './web-fetch.js';
45
50
  import { suggestTool } from './suggest.js';
51
+ import { askUserTool } from './ask-user.js';
52
+ import { askUserSimpleTool } from './ask-user-simple.js';
53
+ import { backlogReadTool, backlogWriteTool } from './backlog.js';
46
54
  export const builtinTools = {
47
55
  readFile: readFileTool,
48
56
  writeFile: writeFileTool,
@@ -56,6 +64,10 @@ export const builtinTools = {
56
64
  todoRead: todoReadTool,
57
65
  webFetch: webFetchTool,
58
66
  suggest: suggestTool,
67
+ askUser: askUserTool,
68
+ askUserSimple: askUserSimpleTool,
69
+ backlogRead: backlogReadTool,
70
+ backlogWrite: backlogWriteTool,
59
71
  };
60
72
  /**
61
73
  * Array of all built-in tools
@@ -73,4 +85,8 @@ export const allBuiltinTools = [
73
85
  todoReadTool,
74
86
  webFetchTool,
75
87
  suggestTool,
88
+ askUserTool,
89
+ askUserSimpleTool,
90
+ backlogReadTool,
91
+ backlogWriteTool,
76
92
  ];
@@ -143,6 +143,10 @@ export interface SubAgentEventInfo {
143
143
  * Type of the sub-agent
144
144
  */
145
145
  agentType: string;
146
+ /**
147
+ * Tool use ID for correlation with parent tool call
148
+ */
149
+ toolUseId?: string;
146
150
  /**
147
151
  * The original event from the sub-agent
148
152
  */
@@ -174,13 +178,19 @@ export interface TaskToolOptions {
174
178
  */
175
179
  defaultTimeout?: number;
176
180
  /**
177
- * Called when a sub-agent is spawned
181
+ * Called when a sub-agent is spawned.
182
+ * @param agentType - The type of agent being spawned
183
+ * @param description - Description of the task
184
+ * @param toolUseId - Tool use ID for correlation (enables parallel tracking)
178
185
  */
179
- onSpawn?: (agentType: string, description: string) => void;
186
+ onSpawn?: (agentType: string, description: string, toolUseId?: string) => void;
180
187
  /**
181
- * Called when a sub-agent completes
188
+ * Called when a sub-agent completes.
189
+ * @param agentType - The type of agent that completed
190
+ * @param result - The task result
191
+ * @param toolUseId - Tool use ID for correlation (enables parallel tracking)
182
192
  */
183
- onComplete?: (agentType: string, result: TaskResult) => void;
193
+ onComplete?: (agentType: string, result: TaskResult, toolUseId?: string) => void;
184
194
  /**
185
195
  * Called when a sub-agent emits an event (for real-time streaming)
186
196
  * Only called if enableEventStreaming is true
@@ -114,7 +114,8 @@ export function createTaskTool(options) {
114
114
  },
115
115
  required: ['description', 'prompt', 'subagent_type'],
116
116
  },
117
- execute: async (input) => {
117
+ execute: async (input, context) => {
118
+ const toolUseId = context?.toolUseId;
118
119
  const { description, prompt, subagent_type, model, context_mode, thoroughness = 'medium', } = input;
119
120
  // Validate agent type exists
120
121
  if (!Object.hasOwn(agentTypes, subagent_type)) {
@@ -124,16 +125,14 @@ export function createTaskTool(options) {
124
125
  const agentConfig = agentTypes[subagent_type];
125
126
  // Check concurrent limit
126
127
  if (activeCount >= maxConcurrent) {
127
- console.error(`[task-tool] BLOCKED: activeCount=${String(activeCount)}, maxConcurrent=${String(maxConcurrent)}`);
128
128
  return createErrorResult(`Maximum concurrent sub-agents (${String(maxConcurrent)}) reached. ` +
129
129
  `Wait for existing tasks to complete.`);
130
130
  }
131
- // Notify spawn
131
+ // Notify spawn (include toolUseId for parallel execution tracking)
132
132
  if (onSpawn) {
133
- onSpawn(subagent_type, description);
133
+ onSpawn(subagent_type, description, toolUseId);
134
134
  }
135
135
  activeCount++;
136
- console.error(`[task-tool] SPAWN: ${subagent_type}, activeCount=${String(activeCount)}`);
137
136
  try {
138
137
  // Note: Sub-agents currently use the parent's provider
139
138
  // Future enhancement: support model switching via providerFactory
@@ -178,6 +177,7 @@ export function createTaskTool(options) {
178
177
  onSubAgentEvent({
179
178
  agentName: subAgentName,
180
179
  agentType: subagent_type,
180
+ toolUseId,
181
181
  event,
182
182
  });
183
183
  };
@@ -194,21 +194,21 @@ export function createTaskTool(options) {
194
194
  iterations: result.iterations,
195
195
  toolCalls: result.toolCalls.length,
196
196
  };
197
- // Notify completion
197
+ // Notify completion (include toolUseId for parallel execution tracking)
198
198
  if (onComplete) {
199
- onComplete(subagent_type, taskResult);
199
+ onComplete(subagent_type, taskResult, toolUseId);
200
200
  }
201
201
  return createSuccessResult(taskResult);
202
202
  }
203
203
  catch (error) {
204
- console.error(`[task-tool] ERROR: ${subagent_type}, error=${error instanceof Error ? error.message : String(error)}`);
205
204
  return createErrorResult(`Sub-agent failed: ${error instanceof Error ? error.message : String(error)}`);
206
205
  }
207
206
  finally {
208
207
  activeCount--;
209
- console.error(`[task-tool] DONE: ${subagent_type}, activeCount=${String(activeCount)}`);
210
208
  }
211
209
  },
210
+ // Task tool can run in parallel since sub-agents are independent
211
+ parallel: true,
212
212
  });
213
213
  }
214
214
  /**
@@ -22,6 +22,13 @@ export interface DefineToolOptions<T extends object> {
22
22
  * Function that executes the tool
23
23
  */
24
24
  execute: (input: T) => Promise<ToolExecutionResult>;
25
+ /**
26
+ * If true, multiple calls to this tool can execute in parallel.
27
+ * When the LLM requests multiple parallel-safe tools in one response,
28
+ * they will be executed concurrently using Promise.all.
29
+ * Default: false (sequential execution)
30
+ */
31
+ parallel?: boolean;
25
32
  }
26
33
  /**
27
34
  * Define a tool with type-safe input handling
@@ -32,6 +32,7 @@ export function defineTool(options) {
32
32
  return {
33
33
  definition,
34
34
  execute: options.execute,
35
+ parallel: options.parallel,
35
36
  };
36
37
  }
37
38
  /**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ToolRegistry - Manages available tools for an agent
3
3
  */
4
- import type { Tool, ToolDefinition, ToolRegistry as IToolRegistry, ToolExecutionResult } from './types.js';
4
+ import type { Tool, ToolDefinition, ToolRegistry as IToolRegistry, ToolExecutionResult, ToolExecutionContext } from './types.js';
5
5
  /**
6
6
  * Options for creating a DefaultToolRegistry
7
7
  */
@@ -61,9 +61,10 @@ export declare class DefaultToolRegistry implements IToolRegistry {
61
61
  *
62
62
  * @param name - Tool name
63
63
  * @param input - Tool input parameters
64
+ * @param contextOrTimeout - Optional execution context or timeout override
64
65
  * @param timeoutMs - Optional timeout override (uses default if not provided)
65
66
  */
66
- execute(name: string, input: Record<string, unknown>, timeoutMs?: number): Promise<ToolExecutionResult>;
67
+ execute(name: string, input: Record<string, unknown>, contextOrTimeout?: ToolExecutionContext | number, timeoutMs?: number): Promise<ToolExecutionResult>;
67
68
  /**
68
69
  * Execute a tool with a timeout
69
70
  */
@@ -75,9 +75,10 @@ export class DefaultToolRegistry {
75
75
  *
76
76
  * @param name - Tool name
77
77
  * @param input - Tool input parameters
78
+ * @param contextOrTimeout - Optional execution context or timeout override
78
79
  * @param timeoutMs - Optional timeout override (uses default if not provided)
79
80
  */
80
- async execute(name, input, timeoutMs) {
81
+ async execute(name, input, contextOrTimeout, timeoutMs) {
81
82
  const tool = this.tools.get(name);
82
83
  if (!tool) {
83
84
  return {
@@ -85,15 +86,27 @@ export class DefaultToolRegistry {
85
86
  error: `Tool not found: ${name}`,
86
87
  };
87
88
  }
89
+ // Handle backwards compatibility: contextOrTimeout can be number (old API) or context (new API)
90
+ let context;
91
+ let explicitTimeout;
92
+ if (typeof contextOrTimeout === 'number') {
93
+ // Old API: execute(name, input, timeoutMs)
94
+ explicitTimeout = contextOrTimeout;
95
+ }
96
+ else {
97
+ // New API: execute(name, input, context?, timeoutMs?)
98
+ context = contextOrTimeout;
99
+ explicitTimeout = timeoutMs;
100
+ }
88
101
  // Determine timeout: explicit param > per-tool config > default
89
102
  const perToolTimeout = Object.hasOwn(this.toolTimeouts, name)
90
103
  ? this.toolTimeouts[name]
91
104
  : undefined;
92
- const timeout = timeoutMs ?? perToolTimeout ?? this.defaultTimeoutMs;
105
+ const timeout = explicitTimeout ?? perToolTimeout ?? this.defaultTimeoutMs;
93
106
  // If timeout is 0 or negative, execute without timeout
94
107
  if (timeout <= 0) {
95
108
  try {
96
- return await tool.execute(input);
109
+ return await tool.execute(input, context);
97
110
  }
98
111
  catch (error) {
99
112
  return {
@@ -104,7 +117,7 @@ export class DefaultToolRegistry {
104
117
  }
105
118
  // Execute with timeout
106
119
  try {
107
- return await this.executeWithTimeout(tool, name, input, timeout);
120
+ return await this.executeWithTimeout(tool, name, input, timeout, context);
108
121
  }
109
122
  catch (error) {
110
123
  if (error instanceof ToolTimeoutError) {
@@ -122,7 +135,7 @@ export class DefaultToolRegistry {
122
135
  /**
123
136
  * Execute a tool with a timeout
124
137
  */
125
- async executeWithTimeout(tool, name, input, timeoutMs) {
138
+ async executeWithTimeout(tool, name, input, timeoutMs, context) {
126
139
  // Create a promise that rejects after the timeout
127
140
  const timeoutPromise = new Promise((_, reject) => {
128
141
  setTimeout(() => {
@@ -130,7 +143,7 @@ export class DefaultToolRegistry {
130
143
  }, timeoutMs);
131
144
  });
132
145
  // Race the tool execution against the timeout
133
- return Promise.race([tool.execute(input), timeoutPromise]);
146
+ return Promise.race([tool.execute(input, context), timeoutPromise]);
134
147
  }
135
148
  /**
136
149
  * Clear all registered tools
@@ -25,16 +25,40 @@ export interface ToolExecutionResult {
25
25
  result?: unknown;
26
26
  error?: string;
27
27
  }
28
+ /**
29
+ * Context passed to tool execution for streaming output
30
+ */
31
+ export interface ToolExecutionContext {
32
+ /**
33
+ * Callback for streaming output (e.g., bash stdout/stderr)
34
+ * @param output - The output text
35
+ * @param stream - Which stream the output came from
36
+ */
37
+ onOutput?: (output: string, stream?: 'stdout' | 'stderr') => void;
38
+ /**
39
+ * Tool use ID for correlation with events
40
+ */
41
+ toolUseId?: string;
42
+ }
28
43
  /**
29
44
  * Tool handler function type
45
+ * @param input - The tool input parameters
46
+ * @param context - Optional execution context for streaming
30
47
  */
31
- export type ToolHandler<T = object> = (input: T) => Promise<ToolExecutionResult>;
48
+ export type ToolHandler<T = object> = (input: T, context?: ToolExecutionContext) => Promise<ToolExecutionResult>;
32
49
  /**
33
50
  * Tool implementation - combines definition with handler
34
51
  */
35
52
  export interface Tool<T = object> {
36
53
  definition: ToolDefinition;
37
54
  execute: ToolHandler<T>;
55
+ /**
56
+ * If true, multiple calls to this tool can execute in parallel.
57
+ * When the LLM requests multiple parallel-safe tools in one response,
58
+ * they will be executed concurrently using Promise.all.
59
+ * Default: false (sequential execution)
60
+ */
61
+ parallel?: boolean;
38
62
  }
39
63
  /**
40
64
  * Tool registry for managing available tools
@@ -54,6 +78,9 @@ export interface ToolRegistry {
54
78
  getDefinitions(): ToolDefinition[];
55
79
  /**
56
80
  * Execute a tool by name with given input
81
+ * @param name - Tool name
82
+ * @param input - Tool input parameters
83
+ * @param context - Optional execution context for streaming
57
84
  */
58
- execute(name: string, input: Record<string, unknown>): Promise<ToolExecutionResult>;
85
+ execute(name: string, input: Record<string, unknown>, context?: ToolExecutionContext): Promise<ToolExecutionResult>;
59
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/agents",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Lightweight multi-LLM agent library for building CLI AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",