Async (Non-Blocking) Tools
Async (Non-Blocking) Tools
Some tools take seconds or minutes to complete — web scraping, data analysis, external API calls, sub-agent orchestration. Normally, the agentic loop blocks on every tool call. With async tools (blocking: false), the tool executes in the background while the agent continues reasoning.
How It Works
The lifecycle of an async tool call:
- LLM requests tool call — The agent's agentic loop extracts tool calls from the LLM response as usual
- Placeholder returned — For
blocking: falsetools, a placeholdertool_resultis returned immediately: "Tool X is executing asynchronously. The result will be delivered in a follow-up message." - Background execution — The tool runs in the background (fire-and-forget promise with timeout)
- Loop continues — The LLM sees the placeholder, can call other (blocking) tools, reason about the situation, or produce text output
- Result arrives — When the async tool completes (or fails/times out), the result is queued
- Delivery as user message — Queued results are batched and injected as a structured user message:
[Async Tool Results] Tool "analyze_dataset" (call_abc123) completed: { "summary": "...", "score": 42 } Process these results and continue. - Continuation — If
autoContinue: true(default), the agent re-enters the agentic loop to process the results. Iffalse, the caller must invokeagent.continueWithAsyncResults().
Quick Start
import { Agent, type ToolFunction } from 'oneringai';
// 1. Define a tool with blocking: false
const analyzeData: ToolFunction = {
definition: {
type: 'function',
function: {
name: 'analyze_dataset',
description: 'Run statistical analysis on a large dataset (may take 30+ seconds)',
parameters: {
type: 'object',
properties: {
dataset: { type: 'string', description: 'Dataset name or path' },
metrics: { type: 'array', items: { type: 'string' }, description: 'Metrics to compute' },
},
required: ['dataset'],
},
},
blocking: false, // <-- Makes this tool async
},
execute: async (args) => {
const data = await loadDataset(args.dataset);
const results = await computeMetrics(data, args.metrics);
return { summary: results.summary, rowCount: data.length, metrics: results.values };
},
};
// 2. Create agent with async config
const agent = Agent.create({
connector: 'anthropic',
model: 'claude-sonnet-4-6',
asyncTools: {
autoContinue: true, // Default: auto re-enter loop on result
batchWindowMs: 1000, // Batch results within 1s window
asyncTimeout: 300000, // 5 min timeout per async tool
},
tools: [analyzeData, readFile, writeFile], // Mix blocking and async tools
});
// 3. Run — async tools are handled automatically
const response = await agent.run('Analyze the Q4 sales dataset and write a summary report');
// The agent will:
// - Call analyze_dataset (async) → gets placeholder
// - Maybe call read_file (blocking) → gets real result
// - Produce intermediate text while waiting
// - When analyze_dataset completes → auto-continue, process results
// - Write the report with writeFile
Auto-Continue vs Manual Mode
Auto-Continue (Default)
With autoContinue: true (default), the agent re-enters the agentic loop automatically when results arrive:
const agent = Agent.create({
connector: 'openai',
model: 'gpt-4.1',
asyncTools: { autoContinue: true }, // default
tools: [asyncTool],
});
// Just run — everything is handled
const response = await agent.run('Do the analysis');
// response.pendingAsyncTools may be non-empty if tools are still running
// when the initial loop ends (no more blocking work to do)
The initial run() returns when the agentic loop has no more blocking work. If async tools are still pending, response.pendingAsyncTools lists them. When they complete, the agent auto-continues in the background.
Manual Mode
With autoContinue: false, results are queued but the agent doesn't auto-continue. The caller decides when:
const agent = Agent.create({
connector: 'openai',
model: 'gpt-4.1',
asyncTools: { autoContinue: false },
tools: [asyncTool],
});
// Listen for completions
agent.on('async:tool:complete', (event) => {
console.log(`${event.toolName} finished in ${event.duration}ms`);
});
const response = await agent.run('Start the analysis');
// Check what's pending
if (agent.hasPendingAsyncTools()) {
console.log('Pending:', agent.getPendingAsyncTools().map(p => p.toolName));
// Wait for results to arrive (via events, polling, or your own logic)
// Then trigger continuation:
const continuation = await agent.continueWithAsyncResults();
console.log(continuation.output_text); // Agent processed the async results
}
Configuration
interface AsyncToolConfig {
/**
* Auto re-enter agentic loop when async results arrive.
* @default true
*/
autoContinue?: boolean;
/**
* Batch window in ms. If multiple async tools complete within this window,
* their results are delivered together in a single user message.
* @default 500
*/
batchWindowMs?: number;
/**
* Timeout per async tool execution in ms.
* @default 300000 (5 minutes)
*/
asyncTimeout?: number;
}
Set blocking: false on individual tool definitions:
const tool: ToolFunction = {
definition: {
type: 'function',
function: { name: 'slow_tool', description: '...', parameters: {} },
blocking: false, // <-- async
},
execute: async (args) => { /* ... */ },
};
Tools default to blocking: true if not specified.
Events
Six new events for monitoring async tool lifecycle (a sixth, async:results:injected — { executionId, iteration, results: [{toolCallId, toolName}], timestamp } — fires when queued results are injected mid-loop before the next LLM call, in addition to the five below):
| Event | Payload | When |
|---|---|---|
async:tool:started |
{ executionId, toolCallId, toolName, args, timestamp } |
Async tool execution begins |
async:tool:complete |
{ executionId, toolCallId, toolName, result, duration, timestamp } |
Tool completed successfully |
async:tool:error |
{ executionId, toolCallId, toolName, error, duration, timestamp } |
Tool threw an error |
async:tool:timeout |
{ executionId, toolCallId, toolName, timeout, timestamp } |
Tool exceeded asyncTimeout |
async:continuation:start |
{ executionId, results: [{toolCallId, toolName}], timestamp } |
Agent re-entering loop with results |
agent.on('async:tool:started', (e) => {
console.log(`[ASYNC] ${e.toolName} started (${e.toolCallId})`);
});
agent.on('async:tool:complete', (e) => {
console.log(`[ASYNC] ${e.toolName} completed in ${e.duration}ms`);
});
agent.on('async:tool:error', (e) => {
console.error(`[ASYNC] ${e.toolName} failed: ${e.error.message}`);
});
agent.on('async:tool:timeout', (e) => {
console.warn(`[ASYNC] ${e.toolName} timed out after ${e.timeout}ms`);
});
agent.on('async:continuation:start', (e) => {
console.log(`[ASYNC] Continuing with ${e.results.length} result(s)`);
});
Public API
// Check if any async tools are still running
agent.hasPendingAsyncTools(): boolean;
// Get details about pending async tools
agent.getPendingAsyncTools(): PendingAsyncTool[];
// PendingAsyncTool: { toolCallId, toolName, args, startTime, status, result?, error? }
// Cancel a specific async tool
agent.cancelAsyncTool(toolCallId: string): void;
// Cancel all pending async tools and clear the result queue
agent.cancelAllAsyncTools(): void;
// Manually trigger continuation with queued results
// (only needed when autoContinue: false)
agent.continueWithAsyncResults(results?: ToolResult[]): Promise<AgentResponse>;
// Response includes pending info
const response = await agent.run('...');
response.pendingAsyncTools; // Array<{ toolCallId, toolName, startTime, status }> | undefined
Mixed Blocking and Async Tools
Blocking and async tools work together naturally in the same iteration:
const agent = Agent.create({
connector: 'openai',
model: 'gpt-4.1',
tools: [
readFile, // blocking (default)
writeFile, // blocking
webFetch, // blocking
analyzeDataset, // blocking: false
generateReport, // blocking: false
],
});
// If the LLM calls both readFile and analyzeDataset in the same turn:
// - readFile executes synchronously, result returned immediately
// - analyzeDataset starts in background, placeholder returned
// - Both results go to context, loop continues
Edge Cases
| Scenario | Behavior |
|---|---|
| Result arrives during active loop | Injected as a [Async Tool Results] user message at the start of the next iteration of the same run — the LLM can see results before run() ends, not just via a later continuation. (This mid-loop message omits the (toolCallId) and the "Process these results and continue." line used by continueWithAsyncResults().) |
| All called tools are async | LLM sees only placeholders → likely produces text → loop ends → results trickle in → auto-continue |
| Async tool fails | Error result delivered same as success — LLM sees the error and can react |
| Async tool times out | Treated as error with timeout message. Configurable via asyncTimeout. |
| Agent destroyed with pending tools | destroy() cancels all pending async tools and clears timers |
| Multiple results arrive close together | Batched within batchWindowMs window, delivered as single message |
continueWithAsyncResults() called with no results |
Throws "No async results to deliver" |
| Concurrent continuations | Second call throws "A continuation is already in progress" |
Use Cases
- Long-running analysis — Data processing, ML inference, report generation
- Parallel API calls — Fetch from multiple external services simultaneously
- Sub-agent orchestration — Dispatch work to sub-agents without blocking the orchestrator
- Web scraping — Scrape multiple pages in parallel while the agent reasons about strategy
- File processing — Process large files while the agent works on other tasks