This document defines the Agent's run loop. Explicitly separate implemented behavior from future enhancements.
Implemented(packages/core/src/agent/agent.ts):
export type AgentOptions = {
llm: BaseChatModel;
tools: Tool[];
systemPrompt?: string;
maxIterations?: number; // default: 200
toolChoice?: ToolChoice; // default: undefined
// context management
compaction?: CompactionConfig | null; // default: enabled
toolOutputCache?: ToolOutputCacheConfig | null; // default: enabled
// DI
services?: AgentServices;
modelRegistry?: ModelRegistry;
// usage
enableUsageTracking?: boolean; // default: true
// done tool mode
requireDoneTool?: boolean; // default: false
// LLM retry options (declared; retry loop behavior is not implemented yet)
llmMaxRetries?: number; // default: 5
llmRetryBaseDelayMs?: number; // default: 1000
llmRetryMaxDelayMs?: number; // default: 60000
llmRetryableStatusCodes?: number[]; // default: [429,500,502,503,504]
// tool permission hook
canExecuteTool?: ToolPermissionHook;
};Partially implemented:
llmMaxRetries/llmRetryBaseDelayMs/llmRetryMaxDelayMs/llmRetryableStatusCodesare declared inAgentOptions, but retry behavior is not implemented in the run loop.
Planned (not implemented):
dependencyOverrides
Implemented:
history: HistoryAdapter(actual history. Maintain order withcommitModelResponse(response.messages))tools: Tool[]usageService: TokenUsageServicecompactionService?: CompactionService | nulltoolOutputCacheService?: ToolOutputCacheService | null
Implemented:
- If
systemPromptis specified, callhistory.enqueueSystem()at the start ofrunStream() - Keep "system only once" on
MessageHistoryAdapterside
run(message, { signal, session }) {
enqueueSystemIfAny()
enqueueUserMessage(message)
while (iterations < maxIterations) {
throwIfAborted(signal)
trimToolOutputs()
const input = history.prepareInvokeInput({ tools, toolChoice })
recordLlmRequest(session, input)
const response = await llm.ainvoke({ ...input, signal })
recordLlmResponse(session, response)
usageService.updateUsageSummary(response.usage)
history.commitModelResponse(response) // Add response.messages as is
const { reasoningTexts, assistantTexts, toolCalls } = collectModelOutput(response.messages)
emitReasoningEvents(reasoningTexts)
const hasToolCalls = toolCalls.length > 0
if (!hasToolCalls) {
if (!requireDoneTool) {
// terminal no-tool response
yield* checkAndCompact()
emitFinal(assistantTexts.join("\n").trim())
return
}
// requireDoneTool=true means "no tool call" is not terminal
yield* checkAndCompact()
continue
}
for (toolCall of toolCalls) {
emitStepStart/toolCall
const execution = await executeToolCall(toolCall)
enqueueToolResult(execution.message)
emitToolResult/stepComplete
if (execution.done) {
emitFinal(execution.finalMessage ?? assistantTexts.join("\n").trim())
return
}
}
yield* checkAndCompact()
}
emitFinal(await generateFinalResponse())
}Implemented:
- Add
response.messagesto the history without reconfiguring it on the Agent side. - At the end without tool call, omit
textand emit onlyfinal requireDoneTool=truekeeps looping when there are no tool calls.- LLM calls within loop currently have no retries (one call)
Planned:
- Implementation of incomplete work hook (prompt for incomplete tasks)
Implemented:
run()/runStream()receiveoptions.signal- Check abort before loop, each iteration, and before tool execution
- Pass
signaltollm.ainvoke()andToolContext - When canceling,
runStreamcan exit without issuingfinal
Implemented:
ReasoningEvent(if there is a reasoning message)TextEvent(other than terminal no-tool)- For each tool call:
StepStartEventToolCallEventToolResultEventStepCompleteEvent
FinalResponseEventon exit
Implemented:
- Do not issue
TextEventat the end where there is no tool call
Implemented:
run()consumesrunStream()and returns the firstfinal
Implemented:
- generate
ToolMessage(is_error=true, content="Error: Unknown tool '...'") - loop continues
Implemented:
- If parse fails, emit
ToolCallEventasargs = { _raw: <raw arguments> } - tool execution itself continues (pass raw arguments string to
executeRaw)
Implemented:
- tool execution exceptions are converted to
ToolMessage(is_error=true, content="Error: ...") - emit
ToolResultEvent(is_error=true)andStepCompleteEvent(status="error")
Implemented:
- When a
TaskCompleteexception is received from the tool layer, it becomesexecution.done=trueand ends withfinal. - Leave the done side tool message in the history
Planned:
- Support for
DoneSignalreturn value method
Implemented:
- Retry within Agent loop is not implemented at this time.
Planned:
- Introduced exponential backoff for 429/5xx etc.
- Judgment based on provider error normalization (
ModelRateLimitError/ModelProviderError)
Implemented:
- Temporarily assemble the input with "summary user message" added using
generateFinalResponse()and call LLM - Summary calls are
tools: null,toolChoice: "none" - Do not change history directly, process with temporary array of
[...history, summaryMessage] - Return fixed fallback statement on failure
Planned:
- hook equivalent to
getIncompleteWorkPrompt - Currently only TODO comments, no execution logic implemented