Agent Loop and Tools
The loop is implemented in src/facade/bot/agent-loop.ts. It turns messages into model calls, executes tool calls, and emits AgentEvent values through an EventStream<AgentEvent, AgentMessage[]>.
Throughout the loop the conversation is carried as AgentMessage[]. It is flattened into the provider's Message[] only at the last moment, inside streamAssistantResponse, via convertToLlm.
Table of Contents
- Entry Points
- The Run Cycle
- Streaming a Model Response
- Tool Execution
- Steering and Follow-ups
- Error and Abort Handling
- Customization Hooks
- Built-in Tools
Entry Points
agentLoop(prompts, context, config, signal?, streamFn?)— appendspromptsto the context, emitsagent_startandturn_start, announces each prompt with pairedmessage_start/message_end, then runs the cycle.agentLoopContinue(context, config, signal?, streamFn?)— resumes from the current context. Throws if the context is empty or its last message is an assistant turn.
Both return EventStream<AgentEvent, AgentMessage[]>, which resolves with the accumulated AgentMessage[] once agent_end is pushed.
The Run Cycle
runLoop has an outer cycle (one pass per batch of follow-up work) and an inner cycle (keeps taking turns until the model stops requesting tools and no steering remains). Each turn:
- Emit
turn_start(suppressed on the very first turn, which reuses the caller'sturn_start). - Ingest any pending steering/follow-up messages into the context, emitting
message_start/message_endfor each. - Call the model via
streamAssistantResponseand append the resulting assistant message. - If the assistant
stopReasoniserrororaborted, emitturn_endand finish the run immediately. - Filter the assistant content for
toolCallblocks. If present, run them viaexecuteToolCallsand append everyToolResultMessage. - Emit
turn_endwith the assistant message and its tool results. - Decide the next
pendingbatch: prefer steering captured mid-tool-batch, otherwise pollgetSteeringMessagesagain.
When the inner cycle drains, getFollowUpMessages is consulted. Returned messages start one more outer pass; otherwise the run ends with agent_end.
Streaming a Model Response
streamAssistantResponse:
- Runs
transformContext(messages, signal)if provided, otherwise uses the context messages as-is. - Builds the provider
ContextwithsystemPrompt,convertToLlm(sourceMessages), andtools. - Resolves the API key from
getApiKey(model.provider)(falling back toconfig.apiKey). - Invokes
streamFn ?? streamSimplewith the model, context, and options. - On the
startevent, splices a live placeholder into the context and emitsmessage_start. On in-flight deltas it updates the placeholder and emitsmessage_update(carryingassistantMessageEvent). Ondone/errorit swaps in the resolved message and emitsmessage_end.
Tool Execution
executeToolCalls dispatches the assistant's toolCall blocks sequentially. For each call, executeToolCall:
- Finds the matching
AgentToolbyname; a missing tool throwsNo registered tool named "<name>". - Emits
tool_execution_startwithtoolCallId,toolName, andargs. - Validates arguments with
validateToolArgumentsfromindusagi/ai. - Calls
tool.execute(id, validatedArgs, signal, onUpdate); eachonUpdateemitstool_execution_updatewithpartialResult. - On any thrown error, builds an error result via the internal
AgentErrorHandlerand marksisError. - Emits
tool_execution_endwith the result andisError, then builds aToolResultMessageand emitsmessage_start/message_endfor it.
Steering and Follow-ups
After each tool call finishes, executeToolCalls polls getSteeringMessages. If steering input is present, it stops dispatching the remaining calls, synthesizes skipped results for them, and bubbles the steering upward. Skipped calls still produce a ToolResultMessage with isError: true and the text Not run: a newer user message took priority. — this keeps the call/result pairing well-formed.
The Agent class wires these hooks to its queue policy: steer() enqueues steering, followUp() enqueues follow-ups, and steeringMode/followUpMode ("all" vs "one-at-a-time") decide how many queued messages are released per turn.
Error and Abort Handling
- An assistant turn that ends with
stopReasonerrororabortedends the whole run. - A tool that throws becomes a
ToolResultMessagewithisError: true. - In
Agent._runLoop, a thrown error is turned into a placeholder assistant message viabuildErrorMessage(zeroed usage,stopReasonof"aborted"or"error"), appended to history, and surfaced throughagent_end.
Customization Hooks
Configure behavior through AgentLoopConfig (or the matching AgentOptions):
convertToLlm— mapAgentMessage[]to the model-readyMessage[].transformContext— reshape the context while it is stillAgentMessage[](trimming, augmentation).getApiKey— supply a fresh per-call API key (e.g. short-lived OAuth tokens).getSteeringMessages— yield mid-run interrupts.getFollowUpMessages— yield work to run once the agent is otherwise idle.
import { Agent } from "indusagi/agent";
const agent = new Agent({
transformContext: async (messages) => messages.slice(-50),
getApiKey: async (provider) => process.env[`${provider.toUpperCase()}_API_KEY`],
});
Built-in Tools
The built-in tools/actions live in src/facade/bot/actions/ and are exported from indusagi/agent. Every tool implements AgentTool and supports:
- Pluggable operations — override filesystem/shell operations for remote systems.
- Streaming updates — tools may emit partial results via the
onUpdatecallback. - Cancellation — execution respects the passed
AbortSignal.
Tool Collections
import {
codingTools, // default full-access tools (uses process.cwd())
readOnlyTools, // default read-only tools (uses process.cwd())
allTools, // Record of every tool, keyed by name
createCodingTools, // factory: full-access tools for a cwd
createReadOnlyTools, // factory: read-only tools for a cwd
createAllTools, // factory: every tool as a Record for a cwd
createToolRegistry, // factory: a populated ToolRegistry
} from "indusagi/agent";
createCodingTools(cwd, options?) returns: read, bash, edit, write, process, todoread, todowrite, websearch, webfetch, and the six composio_* tools.
createReadOnlyTools(cwd, options?) returns: read, grep, find, ls, todoread, websearch, webfetch, composio_toolkits, composio_tools, composio_connect, composio_accounts.
createAllTools(cwd, options?) returns a Record<ToolName, AgentTool> containing every tool, including grep, find, ls, and composio_execute/composio_enable.
Tool Summary
| Tool name | Label | Category | Purpose |
|---|---|---|---|
read |
read | filesystem | Read file contents (text and images) |
bash |
bash | core | Execute a bash command |
edit |
edit | filesystem | Replace one exact span of text in a file |
write |
write | filesystem | Create or overwrite a file |
grep |
grep | search | Search file contents for a pattern |
find |
find | search | Find files by glob |
ls |
ls | filesystem | List directory contents |
process |
process | management | Manage background processes |
todoread |
todoread | management | Read the todo list |
todowrite |
todowrite | management | Update the todo list |
websearch |
websearch | web | Search the web |
webfetch |
webfetch | web | Fetch and convert a URL |
composio_toolkits / composio_tools / composio_execute / composio_enable / composio_connect / composio_accounts |
(same) | management | Composio SaaS integration family |
Full parameter shapes, factory functions, and details types are in Tools Reference. The background process tool has its own page: Background Process Tool.
Example
import { Agent, createCodingTools } from "indusagi/agent";
const agent = new Agent({
initialState: {
systemPrompt: "You are a helpful coding assistant.",
tools: createCodingTools(process.cwd()),
},
});
await agent.prompt("Analyze this codebase and suggest improvements");
Pluggable Operations
Most filesystem and shell tools accept an operations override for remote execution (SSH, SFTP, etc.):
import { createReadTool, createBashTool } from "indusagi/agent";
const agent = new Agent({
initialState: {
tools: [
createReadTool(process.cwd(), {
operations: {
readFile: async (absolutePath) => readFileViaSsh(absolutePath),
access: async (absolutePath) => checkAccessViaSsh(absolutePath),
},
}),
createBashTool(process.cwd(), {
operations: {
exec: async (command, cwd, { onData, signal, timeout, env }) =>
execViaSsh(command, cwd, onData, signal),
},
}),
],
},
});
Related Documentation
- Tools Reference — exact parameters and details types.
- Background Process Tool — background process management.
- Agent API Reference — full Agent API reference.
- Agent Module — module overview.
