Agent Conversation Loop
The AgentLoop is the central orchestrator of Mistral Vibe. This document details how conversations are managed and executed.
AgentLoop Class Overview
File: vibe/core/agent_loop.py
Class: AgentLoop at line 105
Constructor
# agent_loop.py:106-114
def __init__(
self,
config: VibeConfig,
agent_name: str = BuiltinAgentName.DEFAULT,
message_observer: Callable[[LLMMessage], None] | None = None,
max_turns: int | None = None,
max_price: float | None = None,
backend: BackendLike | None = None,
enable_streaming: bool = False,
) -> None:Initialization Sequence
AgentLoop.__init__() [agent_loop.py:105]
│
├─1─► Store base config [agent_loop.py:116]
│
├─2─► Create AgentManager [agent_loop.py]
│ AgentManager(config_getter, initial_agent=agent_name)
│ Discovers built-in + custom agents
│
├─3─► Create ToolManager [agent_loop.py]
│ ToolManager(config)
│ Discovers and registers all available tools
│
├─4─► Create SkillManager [agent_loop.py]
│ SkillManager(config_getter)
│ Discovers SKILL.md files from search paths
│
├─5─► Create APIToolFormatHandler
│ Handles tool call serialization/deserialization
│
├─6─► Select/create backend
│ backend_factory = lambda: backend or self._select_backend()
│
├─7─► Setup middleware pipeline
│ _setup_middleware(max_turns, max_price)
│ Now includes PlanAgentMiddleware
│
├─8─► Build system prompt
│ get_universal_system_prompt(tool_manager, config, skill_manager, agent_manager)
│
├─9─► Initialize messages with system prompt
│ messages = [LLMMessage(role=Role.system, content=system_prompt)]
│
├─10► Initialize stats
│ AgentStats with pricing from active model
│
├─11► Create SessionLogger
│ SessionLogger(config.session_logging, session_id)
│
└─12► Spawn session migration thread
Thread(target=migrate_sessions_entrypoint, daemon=True)
Key Properties
| Property | Purpose |
|---|---|
agent_profile |
Delegates to agent_manager.active_profile |
config |
Delegates to agent_manager.config (merged base + profile overrides) |
auto_approve |
Delegates to agent_manager.config.auto_approve |
Main Entry Point: act()
Method: act() at agent_loop.py
async def act(self, msg: str) -> AsyncGenerator[BaseEvent]:
self._clean_message_history()
async for event in self._conversation_loop(msg):
yield eventThis is an async generator that:
- Cleans any malformed message history
- Delegates to
_conversation_loop() - Yields events as they're produced
The Conversation Loop
Method: _conversation_loop() at agent_loop.py
This is the heart of the agent — a multi-turn conversation loop that handles LLM calls, tool execution, and middleware.
Flow Diagram
_conversation_loop(user_msg)
│
├─1─► Append user message
│ messages.append(LLMMessage(role=Role.user, content=user_msg))
│
├─2─► Yield UserMessageEvent
│
├─3─► Increment steps
│ stats.steps += 1
│
└─4─► Enter main loop
│
while not should_break_loop:
│
├─a─► Run middleware before turn
│ result = await middleware_pipeline.run_before_turn(context)
│ │
│ └── Handle result
│ - STOP: yield event, return
│ - COMPACT: trigger compaction
│ - INJECT_MESSAGE: append to last message
│ - CONTINUE: proceed
│
├─b─► Increment steps
│
├─c─► Perform LLM turn
│ async for event in _perform_llm_turn():
│ if is_user_cancellation_event(event):
│ user_cancelled = True
│ yield event
│
├─d─► Check break condition
│ should_break = (
│ last_message.role != Role.tool AND
│ finish_reason is not None
│ )
│
├─e─► Flush messages to observer
│
├─f─► Save interaction via session_logger
│
├─g─► Handle user cancellation
│ If cancelled, flush and return
│
├─h─► Run middleware after turn
│ Similar handling to before_turn
│
└─i─► Final flush and save
Loop Termination Conditions
The loop terminates when any of these occur:
- Normal completion: Last message is NOT a tool response and LLM returned a finish_reason
- Middleware STOP: Turn limit reached or price limit exceeded
- User cancellation: User interrupted the operation
LLM Turn Execution
Method: _perform_llm_turn()
_perform_llm_turn()
│
├─1─► Call LLM (streaming or non-streaming)
│ │
│ ├── If streaming: _stream_assistant_events()
│ │ Yields AssistantEvents and ReasoningEvents incrementally
│ │
│ └── Else: _get_assistant_event()
│ Returns single AssistantEvent
│
├─2─► Get last message and chunk
│
├─3─► Parse and resolve tool calls
│ parsed = format_handler.parse_message(last_message)
│ resolved = format_handler.resolve_tool_calls(parsed, tool_manager)
│
├─4─► Calculate tokens per second
│
└─5─► Handle tool calls if any
if resolved.tool_calls or resolved.failed_calls:
async for event in _handle_tool_calls(resolved):
yield event
Streaming with Reasoning Content
When streaming, the agent now handles ReasoningEvent for models that produce reasoning/thinking tokens:
# During streaming:
if chunk.message.reasoning_content:
yield ReasoningEvent(content=reasoning_content, message_id=message_id)
if chunk.message.content:
yield AssistantEvent(content=content, message_id=message_id)Tool Call Handling
Method: _handle_tool_calls()
_handle_tool_calls(resolved)
│
├─1─► Handle failed tool calls
│ For each failed call:
│ - Yield ToolResultEvent with error
│ - Increment stats.tool_calls_failed
│ - Append error message to history
│
└─2─► Process valid tool calls
│
for tool_call in resolved.tool_calls:
│
├─a─► Yield ToolCallEvent
│
├─b─► Get tool instance
│ tool_instance = tool_manager.get(tool_call.tool_name)
│
├─c─► Check execution permission
│ decision = await _should_execute_tool(tool, args, tool_call_id)
│
├─d─► If SKIP:
│ - Increment tool_calls_rejected
│ - Yield ToolResultEvent with skipped=True
│ - Append skip message to history
│ - continue
│
└─e─► Execute tool:
│
├── Create InvokeContext
│ InvokeContext(
│ tool_call_id=call_id,
│ approval_callback=self.approval_callback,
│ agent_manager=self.agent_manager,
│ user_input_callback=self.user_input_callback,
│ )
│
├── tool_instance.run(args, ctx=ctx)
│ Returns AsyncGenerator[ToolStreamEvent | ToolResult, None]
│
├── Yield ToolStreamEvent items (streaming output)
│
├── Yield ToolResultEvent with final result
│
└── Handle errors (CancelledError, ToolError, etc.)
Permission Checking
Method: _should_execute_tool()
_should_execute_tool(tool, args, tool_call_id)
│
├─1─► Auto-approve check
│ If auto_approve: return EXECUTE
│
├─2─► Validate args
│
├─3─► Check allowlist/denylist patterns
│ result = tool.check_allowlist_denylist(validated_args)
│ │
│ ├── ALWAYS: return EXECUTE
│ └── NEVER: return SKIP with denylist feedback
│
├─4─► Check tool permission config
│ perm = tool_manager.get_tool_config(tool_name).permission
│ │
│ ├── ALWAYS: return EXECUTE
│ └── NEVER: return SKIP
│
└─5─► Ask user for approval
return await _ask_approval(tool_name, args, tool_call_id)
Agent Switching
Method: switch_agent()
Switches to a different agent profile, rebuilds system prompt, and optionally resets history:
def switch_agent(self, name: str) -> None:
self.agent_manager.switch_profile(name)
# Rebuild system prompt with new agent's config
# Reset tool manager with new configContext Management
Auto-Compaction
Method: compact()
Triggered by AutoCompactMiddleware when context exceeds threshold:
compact()
│
├─1─► Clean message history
│
├─2─► Save current interaction via session_logger
│ session_logger.save_interaction(..., agent_profile=self.agent_profile)
│
├─3─► Find last user message
│
├─4─► Request summary from LLM
│ summary_request = UtilityPrompt.COMPACT.read()
│ summary_result = await _chat()
│
├─5─► Replace history with summary
│ messages = [system_message, summary_message]
│
├─6─► Recalculate context tokens
│
└─7─► Reset session and middleware
History Clearing
Method: clear_history()
clear_history()
│
├─1─► Save current interaction
├─2─► Keep only system message: messages = messages[:1]
├─3─► Reset stats
├─4─► Reset middleware pipeline
├─5─► Reset tool manager state
└─6─► Reset session logger with new session ID
Event Types
Events yielded by the agent (defined in types.py):
| Event | Purpose |
|---|---|
UserMessageEvent |
Echo of user message with message_id |
AssistantEvent |
LLM response content |
ReasoningEvent |
Reasoning/thinking content from LLM |
ToolCallEvent |
Tool invocation |
ToolResultEvent |
Tool execution result |
ToolStreamEvent |
Streaming tool output (e.g., from Task subagent) |
CompactStartEvent |
Compaction beginning |
CompactEndEvent |
Compaction complete |
AssistantEvent Structure
class AssistantEvent(BaseEvent):
content: str
stopped_by_middleware: bool = False
message_id: str | None = NoneReasoningEvent Structure
class ReasoningEvent(BaseEvent):
content: str
message_id: str | None = NoneToolCallEvent Structure
class ToolCallEvent(BaseEvent):
tool_name: str
tool_class: type[BaseTool]
args: BaseModel
tool_call_id: strToolResultEvent Structure
class ToolResultEvent(BaseEvent):
tool_name: str
tool_class: type[BaseTool] | None
result: BaseModel | None = None
error: str | None = None
skipped: bool = False
skip_reason: str | None = None
duration: float | None = None
tool_call_id: strToolStreamEvent Structure
class ToolStreamEvent(BaseEvent):
tool_name: str
message: str
tool_call_id: strState Management
AgentStats
Class: AgentStats at types.py:26
Tracks conversation statistics:
class AgentStats(BaseModel):
steps: int = 0 # LLM turn count
session_prompt_tokens: int = 0 # Total input tokens
session_completion_tokens: int = 0 # Total output tokens
tool_calls_agreed: int = 0 # Approved tool calls
tool_calls_rejected: int = 0 # Rejected tool calls
tool_calls_failed: int = 0 # Failed tool calls
tool_calls_succeeded: int = 0 # Successful tool calls
context_tokens: int = 0 # Current context size
last_turn_prompt_tokens: int = 0
last_turn_completion_tokens: int = 0
last_turn_duration: float = 0.0
tokens_per_second: float = 0.0
input_price_per_million: float = 0.0
output_price_per_million: float = 0.0
@computed_field
@property
def session_cost(self) -> float: # Calculated cost
...Source File References
| File | Key Lines | Description |
|---|---|---|
agent_loop.py:105 |
AgentLoop |
Class definition |
agent_loop.py:106-114 |
__init__() |
Constructor with agent_name parameter |
agent_loop.py |
act() |
Main entry point |
agent_loop.py |
_conversation_loop() |
Main conversation loop |
agent_loop.py |
_perform_llm_turn() |
Single LLM turn |
agent_loop.py |
_stream_assistant_events() |
Streaming with reasoning support |
agent_loop.py |
_handle_tool_calls() |
Tool execution with InvokeContext |
agent_loop.py |
_should_execute_tool() |
Permission checking |
agent_loop.py |
clear_history() |
History management |
agent_loop.py |
compact() |
Context compaction |
agent_loop.py |
switch_agent() |
Agent profile switching |
types.py:26-110 |
AgentStats |
Statistics tracking |
types.py:299-301 |
UserMessageEvent |
User message event |
types.py:304-315 |
AssistantEvent |
Assistant response event |
types.py:318-320 |
ReasoningEvent |
Reasoning content event |
types.py:323-327 |
ToolCallEvent |
Tool call event |
types.py:330-338 |
ToolResultEvent |
Tool result event |
types.py:341-344 |
ToolStreamEvent |
Tool streaming event |