Compare commits

...

7 Commits

Author SHA1 Message Date
ton 7876ff21eb update 2026-07-31 11:41:53 +07:00
ton c9a7661e93 update 2026-07-31 08:08:03 +07:00
ton 8d5c661562 add tracing 2026-07-30 16:23:08 +07:00
ton 244cfc4b96 update 2026-07-30 09:28:19 +07:00
ton a541905b72 update 2026-07-29 14:00:13 +07:00
ton a0787c2316 update 2026-07-29 13:52:02 +07:00
ton 8fd72f8d37 ีupdate 2026-07-29 13:50:00 +07:00
11 changed files with 3602 additions and 2502 deletions
+74 -74
View File
@@ -22,9 +22,9 @@
│ 2. AGENT LOOP START (runAgentLoop) │
│ │
│ new_messages = copy(prompts) │
│ current_context.messages = vcat(context.messages, copy(prompts)) │
│ current_context.messages = vcat(context.messages, copy(prompts))
│ │ │
│ └─→ User messages are IMMEDIATELY added to context.messages │
│ └─→ User messages are IMMEDIATELY added to context.messages
│ (They are NOT in the steering queue!) │
│ │
│ emit(AgentStartEvent) │
@@ -42,7 +42,7 @@
│ │
│ pending_messages = get_steering_messages() │
│ │ │
│ └─→ Steering queue: messages from agent.steer() │
│ └─→ Steering queue: messages from agent.steer()
│ These are for CONTINUING conversation (NOT new user prompts) │
│ │
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
@@ -51,17 +51,17 @@
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 4. PENDING MESSAGE HANDLING (steering messages only) │ │ │
│ │ │ │ │ │
│ │ │ pending_messages = get_steering() │ │ │
│ │ │ pending_messages = get_steering() │ │ │
│ │ │ if !isempty(pending_messages): │ │ │
│ │ │ for msg in pending_messages: │ │ │
│ │ │ emit(MessageStartEvent(msg)) │ │ │
│ │ │ emit(MessageEndEvent(msg)) │ │ │
│ │ │ push to current_context.messages ← Steering messages go HERE │ │ │
│ │ │ push to new_messages │ │ │
│ │ │ push to new_messages │ │ │
│ │ │ pending_messages = [] │ │ │
│ │ │ │ │ │
│ │ │ Note: User messages from Agent.prompt() are ALREADY in context.messages │ │ │
│ │ │ (They were added in runAgentLoop via vcat(), not via this queue) │ │ │
│ │ │ Note: User messages from Agent.prompt() are ALREADY in context.messages │ │ │
│ │ │ (They were added in runAgentLoop via vcat(), not via this queue) │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
@@ -168,7 +168,7 @@
│ new_messages = [UserMessage("What is Julia?")] │
│ current_context.messages = vcat([...existing...], [UserMessage("What is Julia?")]) │
│ │ │
│ └─→ User message IMMEDIATELY added to context.messages (NOT via steering queue!) │
│ └─→ User message IMMEDIATELY added to context.messages (NOT via steering queue!)
│ emit(AgentStartEvent), emit(TurnStartEvent) │
│ emit(MessageStart/End) for user message │
│ │
@@ -207,11 +207,11 @@
│ │ follow_up_queue: [] │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ LLM SEES (convert_to_llm() filters):
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Messages passed to LLM API: │ │
│ │ [UserMessage("What is Julia?"), AssistantMessage("Julia is...")]
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ LLM SEES (convert_to_llm() filters):
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │ Messages passed to LLM API: │
│ │ [UserMessage("What is Julia?"), AssistantMessage("Julia is...")]
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘
│ │
│ TURN #2: User asks "How does it work?" │
│ ───────────────────────────────────────── │
@@ -223,7 +223,7 @@
│ new_messages = [UserMessage("How does it work?")] │
│ current_context.messages = vcat([...previous..., UserMessage("How does it work?")]) │
│ │ │
│ └─→ User message added (context preserved from Turn #1) │
│ └─→ User message added (context preserved from Turn #1)
│ emit(AgentStartEvent), emit(TurnStartEvent) │
│ emit(MessageStart/End) for user message │
│ │
@@ -245,13 +245,13 @@
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ LLM SEES: │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Messages passed to LLM API: │ │
│ │ [UserMessage("What is Julia?"),
│ │ AssistantMessage("Julia is..."),
│ │ UserMessage("How does it work?"),
│ │ AssistantMessage("It works by...")]
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │ Messages passed to LLM API: │
│ │ [UserMessage("What is Julia?"),
│ │ AssistantMessage("Julia is..."),
│ │ UserMessage("How does it work?"),
│ │ AssistantMessage("It works by...")]
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
@@ -261,83 +261,83 @@
│ │
│ What is a steering message? │
│ • A message (any AgentMessage type) injected via: `agent.steer(message)` │
│ • Goes into the steering queue, not immediately to context.messages │
│ • Goes into the steering queue, not immediately to context.messages
│ │
│ How is it created? │
│ • User code calls: agent.steer(UserMessage("...")) │
│ • Or: agent.steer(AssistantMessage("...")) │
│ • Or any other AgentMessage subtype │
│ • User code calls: agent.steer(UserMessage("..."))
│ • Or: agent.steer(AssistantMessage("..."))
│ • Or any other AgentMessage subtype
│ │
│ When is it processed? │
│ • At the START of the next loop iteration (line 194-202 in agent_loop.jl) │
│ • AFTER the previous assistant turn completes │
│ • BEFORE the next assistant response is streamed │
│ • BEFORE the next assistant response is streamed
│ │
│ Why use steering? │
│ Use case 1: Tool execution result injection │
│ - Agent calls a tool (e.g., read_file, bash) │
│ - Tool returns result │
│ - You want to inject a follow-up question based on the result │
│ - agent.steer(UserMessage("Based on the file, what should we do next?")) │
│ Use case 1: Tool execution result injection
│ - Agent calls a tool (e.g., read_file, bash)
│ - Tool returns result
│ - You want to inject a follow-up question based on the result
│ - agent.steer(UserMessage("Based on the file, what should we do next?"))
│ │
│ Use case 2: Multi-turn conversation without user input │
│ - Agent responds to user │
│ - Before user types again, you want to inject a system message │
│ - agent.steer(BashExecutionMessage(...)) or custom message │
│ Use case 2: Multi-turn conversation without user input
│ - Agent responds to user
│ - Before user types again, you want to inject a system message
│ - agent.steer(BashExecutionMessage(...)) or custom message
│ - This continues the conversation automatically │
│ │
│ Use case 3: Branch navigation recovery │
│ - User navigates between conversation branches │
│ - After switching branches, you want to inject a context message │
│ - agent.steer(BranchSummaryMessage(...)) │
│ - The agent can then continue from the new branch context │
│ Use case 3: Branch navigation recovery
│ - User navigates between conversation branches
│ - After switching branches, you want to inject a context message
│ - agent.steer(BranchSummaryMessage(...))
│ - The agent can then continue from the new branch context
│ │
│ Use case 4: Compaction summary injection │
│ - Conversation history is compacted │
│ - After compaction, inject summary message │
│ - agent.steer(CompactionSummaryMessage(...)) │
│ Use case 4: Compaction summary injection
│ - Conversation history is compacted
│ - After compaction, inject summary message
│ - agent.steer(CompactionSummaryMessage(...))
│ - Agent knows old history was summarized │
│ │
│ Example: │
│ agent.steer(UserMessage("Follow-up question here")) │
│ # This will be processed in the next loop iteration, │
│ # appearing in context.messages before the next LLM call │
│ agent.steer(UserMessage("Follow-up question here"))
│ # This will be processed in the next loop iteration,
│ # appearing in context.messages before the next LLM call
│ │
│ The LLM sees: │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ All messages become Message[] via convert_to_llm():
│ │ [UserMessage(...), AssistantMessage(...), UserMessage(from_steer), ...]
│ │ │ │
│ │ The LLM cannot tell which came from Agent.prompt() vs agent.steer()
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │ All messages become Message[] via convert_to_llm():
│ │ [UserMessage(...), AssistantMessage(...), UserMessage(from_steer), ...]
│ │ │
│ │ The LLM cannot tell which came from Agent.prompt() vs agent.steer()
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ LLM PROCESSING: How LLM sees messages │
│ LLM PROCESSING: How LLM sees messages
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ The LLM NEVER sees "user message" vs "steering message" - it only sees Message types: │
│ The LLM NEVER sees "user message" vs "steering message" - it only sees Message types:
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ convert_to_llm() transforms ALL AgentMessages to Message[]:
│ │ │ │
│ │ UserMessage("user") → UserMessage (for LLM)
│ │ Steering UserMessage("user") → UserMessage (for LLM) ← Same!
│ │ AssistantMessage("assistant") → AssistantMessage (for LLM)
│ │ ToolResultMessage("toolResult") → ToolResultMessage (for LLM)
│ │ │ │
│ │ BranchSummaryMessage → UserMessage (wrapped in summary tags)
│ │ CompactionSummaryMessage → UserMessage (wrapped in summary tags)
│ │ BashExecutionMessage → UserMessage (if not excluded)
│ │ CustomMessage → UserMessage
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │ convert_to_llm() transforms ALL AgentMessages to Message[]:
│ │ │
│ │ UserMessage("user") → UserMessage (for LLM)
│ │ Steering UserMessage("user") → UserMessage (for LLM) ← Same!
│ │ AssistantMessage("assistant") → AssistantMessage (for LLM)
│ │ ToolResultMessage("toolResult") → ToolResultMessage (for LLM)
│ │ │
│ │ BranchSummaryMessage → UserMessage (wrapped in summary tags)
│ │ CompactionSummaryMessage → UserMessage (wrapped in summary tags)
│ │ BashExecutionMessage → UserMessage (if not excluded)
│ │ CustomMessage → UserMessage
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘
│ │
│ The difference is ONLY in HOW messages enter the system: │
│ • User messages: Agent.prompt() → vcat() → context.messages (direct) │
│ • Steering: agent.steer() → queue → loop → context.messages (indirect) │
│ The difference is ONLY in HOW messages enter the system:
│ • User messages: Agent.prompt() → vcat() → context.messages (direct)
│ • Steering: agent.steer() → queue → loop → context.messages (indirect)
│ │
│ At LLM level: BOTH become UserMessage in the conversation! │
│ At LLM level: BOTH become UserMessage in the conversation!
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
@@ -347,13 +347,13 @@
│ │
│ 1. User prompts go DIRECTLY to context.messages via vcat() in runAgentLoop() │
│ │
│ 2. Steering queue is for messages injected via agent.steer() AFTER a turn finishes
│ 2. Steering queue is for messages injected via agent.steer() AFTER a turn finishes │
│ This allows continuing conversation without calling Agent.prompt() again │
│ │
│ 3. Context is preserved across turns - context.messages grows with each turn │
│ LLM sees the full conversation history │
│ │
│ 4. At LLM level, ALL messages become Message types (UserMessage/AssistantMessage/ToolResultMessage) │
│ 4. At LLM level, ALL messages become Message types (UserMessage/AssistantMessage/ToolResultMessage)
│ The "steering" vs "user" distinction is just a control mechanism, not a message type │
│ │
│ 5. New turn is triggered by: │
+397 -145
View File
@@ -51,7 +51,7 @@
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Agent Lifecycle
│ Agent Lifecycle │
└─────────────────────────────────────────────────────────────────────────┘
User Code
@@ -83,20 +83,20 @@
┌──────────────────────────────────────────────────────────────┐
│ AgentLoop (runs in separate thread) │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 1. Emit AgentStartEvent │ │
│ │ 2. Emit TurnStartEvent │ │
│ │ 3. Process prompts (emit MessageStart/End) │ │
│ │ 4. ┌──────────────────────────────────────────────┐ │ │
│ │ │ while true: │ │ │
│ │ │ │ Process steering/follow-up messages │ │ │
│ │ │ │ Stream assistant response (LLM call) │ │ │
│ │ │ │ Execute tool calls (parallel/sequential) │ │ │
│ │ │ │ Emit TurnEndEvent │ │ │
│ │ │ │ Check if should stop │ │ │
│ │ │ │ Get next steering messages │ │ │
│ │ └───┴────────────────────────────────────────────┘ │ │
│ │ 4.┌────────────────────────────────────────────────┐ │
│ │ │ while true: │ │
│ │ │ │ Process steering/follow-up messages │ │
│ │ │ │ Stream assistant response (LLM call) │ │
│ │ │ │ Execute tool calls (parallel/sequential) │ │
│ │ │ │ Emit TurnEndEvent │ │
│ │ │ │ Check if should stop │ │
│ │ │ │ Get next steering messages │ │
│ │ └───┴────────────────────────────────────────────┘ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
@@ -125,18 +125,18 @@
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ agentLoop(prompts, context, config, signal, stream_fn) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ▼
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ runAgentLoop(prompts, context, config, emit, signal) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ▼
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ runLoop() - Main Event Loop │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────┼───────────────────────────────────────
│ │ │
└──────────────────────────────┼─────────────────────────────────────┘
│ Loop Iteration
@@ -150,8 +150,8 @@
│ │ │ (after assistant) │ │ (after stop) │ │ │
│ │ └────────────────────┘ └──────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ▼
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 2. Stream Assistant Response │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
@@ -161,39 +161,39 @@
│ │ │ - Text deltas │ │ │
│ │ │ - Tool call deltas │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ Emit: MessageStartEvent, MessageUpdateEvent, │ │ │
│ │ │ MessageEndEvent │ │ │
│ │ │ MessageEndEvent │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ▼
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 3. Execute Tool Calls │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ extract ToolCall from assistant content │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ if EXECUTION_SEQUENTIAL || has_sequential_tool: │ │ │
│ │ │ executeToolCallsSequential() │ │ │
│ │ │ else: │ │ │
│ │ │ else: │ │ │
│ │ │ executeToolCallsParallel() │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ For each tool call: │ │ │
│ │ │ 1. before_tool_call hook │ │ │
│ │ │ 2. prepareToolCall() │ │ │
│ │ │ 3. execute() │ │ │
│ │ │ 4. after_tool_call hook │ │ │
│ │ │ 5. Emit ToolExecutionStart/Update/EndEvent │ │ │
│ │ │ 6. Emit ToolResultMessage │ │ │
│ │ │ For each tool call: │ │ │
│ │ │ 1. before_tool_call hook │ │ │
│ │ │ 2. prepareToolCall() │ │ │
│ │ │ 3. execute() │ │ │
│ │ │ 4. after_tool_call hook │ │ │
│ │ │ 5. Emit ToolExecutionStart/Update/EndEvent │ │ │
│ │ │ 6. Emit ToolResultMessage │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ▼
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 4. Prepare Next Turn │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
@@ -202,29 +202,29 @@
│ │ │ - Optional: Update context │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ▼
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 5. Check Termination Conditions │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ should_stop_after_turn(context) -> bool │ │ │
│ │ │ - Max turns reached? │ │ │
│ │ │ - Tool returned terminate=true? │ │ │
│ │ │ - Steering queue empty and follow-up empty? │ │ │
│ │ │ should_stop_after_turn(context) -> bool │ │ │
│ │ │ - Max turns reached? │ │ │
│ │ │ - Tool returned terminate=true? │ │ │
│ │ │ - Steering queue empty and follow-up empty? │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ▼
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 6. Emit TurnEndEvent (message, tool_results) │ │
│ │ 6. Emit TurnEndEvent (message, tool_results) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ▼
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Loop continues until termination condition met │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────┼───────────────────────────────────────
│ │ │
└──────────────────────────────┼─────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐
@@ -240,34 +240,34 @@
└─────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────┐
│ Assistant Message with Tool Calls
│ Assistant Message with Tool Calls │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ AssistantMessage: │ │
│ │ AssistantMessage: │ │
│ │ content: [ │ │
│ │ TextContent("I'll help you"), │ │
│ │ ToolCall(id="tc1", name="bash", args={...}), │ │
│ │ ToolCall(id="tc2", name="read", args={...}) │ │
│ │ ToolCall(id="tc1", name="bash", args={...}), │ │
│ │ ToolCall(id="tc2", name="read", args={...}) │ │
│ │ ] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ▼
│ │ │
│ ▼ │
└───────────────────────────────────────────────────────────────────┘
│ executeToolCalls()
┌───────────────────────────────────────────────────────────────────┐
│ Determine Execution Mode
│ Determine Execution Mode │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ config.tool_execution == EXECUTION_SEQUENTIAL? │ │
│ │ OR any tool has execution_mode == EXECUTION_SEQUENTIAL? │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────┴───────────────┐
│ ▼ ▼
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ executeSequential() │ │ executeParallel() │ │
│ └────────────────────────┘ └────────────────────────┘ │
│ │ │
│ │ │ │
└──────────────┼───────────────────────────────┼────────────────────┘
│ │
│ │
@@ -285,7 +285,7 @@
┌───────────────────────────────────────────────────────────────────┐
│ For Each Tool Call
│ For Each Tool Call │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 1. before_tool_call hook (optional) │ │
│ │ - Can block execution │ │
@@ -293,20 +293,20 @@
│ │ - validateToolArguments() │ │
│ │ - prepareToolCallArguments() (optional) │ │
│ │ 3. Execute Tool: │ │
│ │ tool.execute(tool_call_id, args, signal, on_update) │ │
│ │ 4. after_tool_call hook (optional) │ │
│ │ tool.execute(tool_call_id, args, signal, on_update) │ │
│ │ 4. after_tool_call hook (optional) │ │
│ │ - Can modify result content │ │
│ │ 5. Emit events: │ │
│ │ - ToolExecutionStartEvent │ │
│ │ - ToolExecutionUpdateEvent (optional) │ │
│ │ - ToolExecutionEndEvent │ │
│ │ - ToolExecutionStartEvent │ │
│ │ - ToolExecutionUpdateEvent (optional) │ │
│ │ - ToolExecutionEndEvent │ │
│ │ 6. Create ToolResultMessage │ │
│ └─────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────┐
│ Tool Result Messages
│ Tool Result Messages │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ToolResultMessage: │ │
│ │ role: "toolResult" │ │
@@ -329,47 +329,47 @@
┌───────────────────────────────────────────────────────────────────┐
│ Branch Navigation │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ E1 │────▶│ E2 │────▶│ E3 │────▶│ E4 │────▶│ E5 │ (leaf)
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ │ E1 │────▶│ E2 │────▶│ E3 │────▶│ E4 │────▶│ E5 │ (leaf) │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ Message Message Compaction Message BranchSummary │
│ │
│ E3 is a Compaction Entry: │
│ - Summary of E1, E2 │
│ - first_kept_entry_id: reference to first retained message │
│ - tokens_before: context size before compaction │
│ │
│ E5 is a BranchSummary Entry: │
│ - Summary of branch from from_id │
│ - Represents a fork point in conversation history │
│ │
└───────────────────────────────────────────────────────────────────┘
│ Session.moveTo()
┌───────────────────────────────────────────────────────────────────┐
│ Forking & Branching │
│ │
│ Current branch: │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ E1 │────▶│ E2 │────▶│ E3 │ │
│ └─────┘ └─────┘ └─────┘ │
│ moveTo(E2) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │ (new branch)
│ └─────┘ └─────┘ └─────┘ └─────┘
│ └─────┘ └─────┘ └─────┘ │
│ │ moveTo(E2)
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │ (new branch) │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │ │
│ │ create BranchSummary │
│ ▼ │
│ ┌─────┐
│ │ E5 │ (branch summary)
│ └─────┘
┌─────┐ │
│ E5 │ (branch summary) │
└─────┘ │
│ │
└───────────────────────────────────────────────────────────────────┘
```
@@ -377,7 +377,7 @@
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Component Relationships
│ Component Relationships │
└─────────────────────────────────────────────────────────────────────────────┘
User Code
@@ -405,87 +405,295 @@
└── Manages ──► PromptTemplates
```
## Data Flow
## Data Flow with Type Transformations
### Complete User Input → Conversation History Flow
```
┌─────────────────────────────────────────────────────────────────────────────┐
Data Flow Between Layers
Level 1: User Input
└─────────────────────────────────────────────────────────────────────────────┘
User Input
• String: "Hello, what's in the directory?"
• AgentMessage: UserMessage(...)
• Vector{AgentMessage}: [UserMessage(...), AssistantMessage(...)]
┌──────────────────────────────────────────────────────────────┐
│ Agent.prompt() / normalizePromptInput() │
│ │
│ Type Dispatch: │
│ • String → UserMessage("user", [TextContent(input)], ts) │
│ • AgentMessage → [input] (wrap in array) │
│ • Vector{AgentMessage} → input (pass-through) │
│ │
│ Output: Vector{AgentMessage} │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ AgentState.messages (AgentMessage[]) │
│ │
│ AgentMessage Types: │
│ • UserMessage (role: "user") │
│ • AssistantMessage (role: "assistant") │
│ • ToolResultMessage (role: "toolResult") │
│ • BashExecutionMessage (custom) │
│ • CompactionSummaryMessage (custom) │
│ • BranchSummaryMessage (custom) │
└──────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Level 2: AgentLoop Processing │
└─────────────────────────────────────────────────────────────────────────────┘
User Input (String/Message)
┌──────────────────────────────────────────────────────────────┐
│ transform_context() (optional hook) │
│ │
│ Input: Vector{AgentMessage} │
│ Output: Vector{AgentMessage} (transformed) │
│ - Can truncate, filter, or modify messages │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────┐
Agent.prompt()
- normalizeInput()
└──────────────────────┘
┌────────────────────────────────────────────────────────────────
convertToLlm() - Type Transformation Pipeline
│ Input: Vector{AgentMessage} │
│ Output: Vector{Message} (for LLM API) │
│ │
│ Single Dispatch Mapping: │
│ • UserMessage → UserMessage (pass-through) │
│ • AssistantMessage → AssistantMessage (pass-through) │
│ • ToolResultMessage → ToolResultMessage (pass-through) │
│ │
│ Custom Message Conversions: │
│ • BashExecutionMessage → UserMessage (via bashExecutionToText)│
│ • CompactionSummaryMessage → UserMessage (wrapped) │
│ • BranchSummaryMessage → UserMessage (wrapped) │
└────────────────────────────────────────────────────────────────┘
┌──────────────────────┐
AgentState.messages │ ──► AgentMessage[]
└──────────────────────┘
┌──────────────────────────────────────────────────────────────
Context for LLM API │
│ - system_prompt: String │
│ - messages: Vector{Message} │
│ - tools: Vector{AgentTool} │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────┐
AgentLoop
- transform_context
└──────────────────────┘
┌──────────────────────────────────────────────────────────────
LLM API Call (stream_fn)
│ Input: model, context, config │
│ Output: Stream{AssistantMessageEvent} │
│ • StartEvent: partial AssistantMessage │
│ • TextStartEvent/TextDeltaEvent/TextEndEvent │
│ • ToolCallStartEvent/ToolCallDeltaEvent/ToolCallEndEvent │
│ • DoneEvent: final AssistantMessage with usage │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────┐
convertToLlm() ──► Transforms AgentMessage[] to Message[]
└──────────────────────┘
┌──────────────────────────────────────────────────────────────
AssistantMessage (returned from LLM)
│ │
│ • role: "assistant" │
│ • content: Vector{MessageContent} │
│ └─ Contains: TextContent[] and/or ToolCall[] │
│ • api, provider, model: String │
│ • usage: Usage (input, output, cache_read, cache_write) │
│ • stop_reason: String ("done", "length", "error", etc.) │
│ • error_message: Union{String, Nothing} │
│ • timestamp: Timestamp (Int64) │
└──────────────────────────────────────────────────────────────┘
├─► Append to AgentState.messages (AssistantMessage)
┌──────────────────────┐
LLM API (StreamFn)
- Context: Message[]
└──────────────────────┘
┌──────────────────────────────────────────────────────────────
executeToolCalls() - Tool Processing
│ Extract: filter(c -> c isa ToolCall, assistant.content) │
│ Output: ExecutedToolCallBatch │
│ • messages: Vector{ToolResultMessage} │
│ • terminate: Bool │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────┐
Response (Streaming)
- Text deltas
- Tool call deltas
└──────────────────────┘
┌──────────────────────────────────────────────────────────────
ToolResultMessage (for each ToolCall)
• role: "toolResult"
│ • tool_call_id: String (matches ToolCall.id) │
│ • tool_name: String (matches ToolCall.name) │
│ • content: Vector{MessageContent} │
│ • details: Any (tool-specific) │
│ • usage: Union{Usage, Nothing} │
│ • added_tool_names: Union{Vector{String}, Nothing} │
│ • is_error: Bool │
│ • timestamp: Timestamp (Int64) │
└──────────────────────────────────────────────────────────────┘
├─► Append to AgentState.messages (ToolResultMessage)
┌──────────────────────┐
AssistantMessage
- content: Message[]
└──────────────────────┘
┌──────────────────────────────────────────────────────────────
Updated AgentState.messages (AgentMessage[])
│ Conversation History: │
│ [UserMessage, AssistantMessage, ToolResultMessage, ...] │
└──────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Level 3: Session Storage (optional, for persistence) │
└─────────────────────────────────────────────────────────────────────────────┘
AgentState.messages (Vector{AgentMessage})
┌──────────────────────┐
AgentState.messages │ ──► Appended to conversation
└──────────────────────┘
┌──────────────────────────────────────────────────────────────
Session Storage (JSONL) │
│ │
│ SessionTreeEntry Types: │
│ • MessageEntry (agent_message) │
│ • CompactionEntry (summary, tokens_before) │
│ • BranchSummaryEntry (from_id, summary) │
│ • ModelChangeEntry (provider, model_id) │
│ • ThinkingLevelChangeEntry (thinking_level) │
│ • ActiveToolsChangeEntry (active_tool_names) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────┐
Tool Execution
│ - Extract ToolCalls
│ - Execute tools
└──────────────────────┘
┌──────────────────────┐
│ ToolResultMessage[] │
└──────────────────────┘
┌──────────────────────┐
│ AgentState.messages │ ──► Tool results appended
└──────────────────────┘
│ (Loop back to LLM or end)
┌──────────────────────┐
│ Session Storage │
│ - JSONL format │
│ - Tree entries │
└──────────────────────┘
┌──────────────────────────────────────────────────────────────
Persisted Data (JSON format)
- Each entry has: id, parent_id, timestamp, type
- MessageEntry contains full AgentMessage
└──────────────────────────────────────────────────────────────
```
### Tool Call Execution Flow (Detailed)
```
ToolCall (from AssistantMessage.content)
├─ type: "tool"
├─ id: "tc_abc123"
├─ name: "bash"
├─ arguments: Dict("command" => "ls -la")
└─ partial_json: nothing
┌──────────────────────────────────────────────────────────────┐
│ prepareToolCall() │
│ │
│ Input: tool_call::ToolCall │
│ Output: Union{PreparedToolCall, ImmediateToolCallOutcome} │
│ │
│ Steps: │
│ 1. Find tool by name in current_context.tools │
│ 2. before_tool_call hook (optional) │
│ Input: BeforeToolCallContext │
│ Output: BeforeToolCallResult (block, reason) or nothing │
│ 3. prepareToolCallArguments() (optional) │
│ Input: tool_call.arguments::Dict │
│ Output: prepared_arguments::Any │
│ 4. validateToolArguments() (optional) │
│ Input: prepared_tool_call.arguments │
│ Output: validated_args::Any │
│ 5. Return: PreparedToolCall(kind, tool_call, tool, args) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ executePreparedToolCall() (if prepared) │
│ │
│ Input: PreparedToolCall │
│ Output: ExecutedToolCallOutcome │
│ │
│ tool.execute(tool_call.id, args, signal, on_update) │
│ │ │
│ └─ Returns: AgentToolResultMutable │
│ • content::Vector{MessageContent} │
│ • details::Any │
│ • usage::Union{Usage, Nothing} │
│ • added_tool_names::Union{Vector{String}, Nothing} │
│ • terminate::Union{Bool, Nothing} │
└──────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ finalizeExecutedToolCall() │
│ │
│ Input: ExecutedToolCallOutcome │
│ Output: FinalizedToolCallOutcome │
│ │
│ Steps: │
│ 1. after_tool_call hook (optional) │
│ Input: AfterToolCallContext │
│ Output: AfterToolCallResult (patches) │
│ 2. Apply patches to result │
│ 3. Return: FinalizedToolCallOutcome(tool_call, result, error)│
└───────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ createToolResultMessage() │
│ │
│ Input: FinalizedToolCallOutcome │
│ Output: ToolResultMessage │
│ │
│ Fields: │
│ • role: "toolResult" │
│ • tool_call_id: tool_call.id │
│ • tool_name: tool_call.name │
│ • content: result.content │
│ • details: result.details │
│ • usage: result.usage │
│ • added_tool_names: result.added_tool_names │
│ • is_error: is_error │
│ • timestamp: Int64(Dates.now(Dates.UTC).datetime) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Emit: ToolResultMessage to conversation │
└──────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Summary of Type Transformations │
└─────────────────────────────────────────────────────────────────────────────┘
User Input (String)
├─► normalizePromptInput()
│ └─► UserMessage (AgentMessage subtype)
Vector{AgentMessage}
├─► transform_context() (optional)
│ └─► Vector{AgentMessage} (transformed)
├─► convertToLlm()
│ └─► Vector{Message} (LLM API format)
│ ├── UserMessage (pass-through)
│ ├── AssistantMessage (pass-through)
│ ├── ToolResultMessage (pass-through)
│ └── Custom messages → UserMessage
AssistantMessage (from LLM)
├─► executeToolCalls()
│ └─► ToolResultMessage[]
ToolResultMessage[]
└─► Appended to AgentState.messages
└─► Vector{AgentMessage} (updated conversation history)
```
## Summary
## Summary
The AgentCore.jl architecture follows a clean separation of concerns:
@@ -495,4 +703,48 @@ The AgentCore.jl architecture follows a clean separation of concerns:
3. **AgentLoop** - Core LLM interaction loop
4. **Session** - Conversation history management
### Data Transformation Summary
```
Input Type Flow:
User Input (String/Message)
├─ normalizePromptInput()
│ └─► Vector{AgentMessage}
├─ transform_context() (optional)
│ └─► Vector{AgentMessage} (transformed)
├─ convertToLlm()
│ └─► Vector{Message} (LLM API format)
├─ LLM API (stream_fn)
│ └─► AssistantMessage
├─ executeToolCalls()
│ └─► ToolResultMessage[]
└─► Vector{AgentMessage} (final conversation)
```
### Key Data Flow Patterns
1. **Message Transformation**: `AgentMessage[] → Message[]` via `convertToLlm()`
- UserMessage → UserMessage (pass-through)
- AssistantMessage → AssistantMessage (pass-through)
- ToolResultMessage → ToolResultMessage (pass-through)
- Custom messages (Bash, Compaction, Branch) → UserMessage
2. **Tool Execution**: `ToolCall → ToolResultMessage`
- prepareToolCall() validates and prepares
- execute() runs the tool
- finalize() applies hooks and returns outcome
- createToolResultMessage() creates result entry
3. **Event Streaming**: `Stream{Event}` with lifecycle events
- AgentStartEvent, TurnStartEvent
- MessageStartEvent, MessageUpdateEvent, MessageEndEvent
- ToolExecutionStartEvent, ToolExecutionEndEvent
- TurnEndEvent, AgentEndEvent
Each layer transforms data and passes it to the next layer, with clear interfaces and event hooks for customization.
+44 -22
View File
@@ -36,8 +36,8 @@ end
# Create agent with options
agent = Agent(Dict{Symbol, Any}(
:systemPrompt => "You are a helpful assistant",
:model => Model(...),
:thinkingLevel => THINKING_MEDIUM,
:model => Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
:thinkingLevel => THINKING_OFF,
:tools => [bash_tool, read_tool],
:steeringMode => QUEUE_ONE_AT_A_TIME,
:followUpMode => QUEUE_ONE_AT_A_TIME,
@@ -243,16 +243,35 @@ followUp(agent, UserMessage(...))
```julia
# Transform messages before sending to LLM
function myConvertToLlm(messages::Vector{AgentMessage})
return filter(
m -> m.role in ["user", "assistant", "toolResult"],
messages
)
function myConvertToLlm(messages::Vector{AgentMessage})::Vector{Message}
result::Vector{Message} = Message[]
for m in messages
converted = convertToLlmMessage(m)
if !isnothing(converted)
push!(result, converted)
end
end
return result
end
agent = Agent(Dict(:convertToLlm => myConvertToLlm))
```
**Data Flow**:
```
Vector{AgentMessage}
│ convertToLlmMessage() dispatches on type:
│ • UserMessage → UserMessage (pass-through)
│ • AssistantMessage → AssistantMessage (pass-through)
│ • ToolResultMessage → ToolResultMessage (pass-through)
│ • BashExecutionMessage → UserMessage (bashExecutionToText)
│ • CompactionSummaryMessage → UserMessage (wrapped)
│ • BranchSummaryMessage → UserMessage (wrapped)
Vector{Message} (for LLM API)
```
#### transform_context
```julia
@@ -271,7 +290,7 @@ agent = Agent(Dict(:transformContext => myTransformContext))
# Hook before tool execution
function myBeforeToolCall(context, signal)
println("About to execute: $(context.tool_call.name)")
return nothing # Return block=true to prevent execution
return BeforeToolCallResult(nothing, nothing) # Return BeforeToolCallResult(true, "reason") to block
end
agent = Agent(Dict(:beforeToolCall => myBeforeToolCall))
@@ -284,8 +303,11 @@ agent = Agent(Dict(:beforeToolCall => myBeforeToolCall))
function myAfterToolCall(context, signal)
# Can modify tool result
return AfterToolCallResult(
content = context.result.content,
terminate = context.result.terminate
context.result.content,
context.result.details,
nothing,
nothing,
context.result.terminate
)
end
@@ -300,9 +322,9 @@ function myPrepareNextTurn(context, signal)
# context: PrepareNextTurnContext
# Returns AgentLoopTurnUpdate or nothing
return AgentLoopTurnUpdate(
context = context.context,
model = context.context.model, # Can change model
thinking_level = THINKING_HIGH # Can change thinking level
context.context, # context
context.context.model, # model - can change
THINKING_HIGH # thinking_level - can change
)
end
@@ -315,11 +337,11 @@ agent = Agent(Dict(:prepareNextTurn => myPrepareNextTurn))
# Check if agent is busy
if !isnothing(agent.active_run)
# Agent is processing
abort(agent) # Abort current run
abort(agent) # Abort current run (NOTE: implementation is a TODO stub)
end
# Wait for completion
wait_for_idle(agent) # Returns Promise
waitForIdle(agent) # Returns Promise
```
## Complete Example
@@ -348,7 +370,7 @@ end
prompt(agent, "What's in the current directory?")
# 4. Wait for completion
wait_for_idle(agent)
waitForIdle(agent)
# 5. Check final state
state = get_state(agent)
@@ -356,7 +378,7 @@ println("Total messages: $(length(state.messages))")
# 6. Continue with steering
steer(agent, UserMessage(...))
wait_for_idle(agent)
waitForIdle(agent)
# 7. Clean up
unsubscribe() # Stop listening
@@ -403,9 +425,9 @@ Scenario: User sends message, agent responds with tool calls
┌────────────────────────────────────────────────────────────┐
│ Time 2: User queues steering message │
│ ┌──────────────────┐ │
│ │ steer(msg2) │ ──► steering_queue.push(msg2) │
│ │ steer(msg2) │ ──► steering_queue.push(msg2)
│ └──────────────────┘ │
│ │
│ (msg2 not processed yet!) │
└────────────────────────────────────────────────────────────┘
@@ -413,9 +435,9 @@ Scenario: User sends message, agent responds with tool calls
┌────────────────────────────────────────────────────────────┐
│ Time 3: Tool execution │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Execute bash tool... │ │
│ │ Execute read tool... │ │
│ │ Emit ToolResultMessage[] │ │
│ │ Execute bash tool... │ │
│ │ Execute read tool... │ │
│ │ Emit ToolResultMessage[] │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
+559 -244
View File
@@ -27,59 +27,137 @@ agentLoopContinue()
└─ Returns: EventStream
┌─────────────────────────────────────────────────────────────────────────────┐
Internal Flow
Data Flow with Type Transformations
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. runAgentLoop() ── Entry point for new conversation │
- Creates copy of prompts
- Appends prompts to context.messages
- Emits AgentStartEvent
- Emits TurnStartEvent
- Emits MessageStart/End for each prompt
│ - Calls runLoop()
│ 1. runAgentLoop() ── Entry point for new conversation
Input: prompts::Vector{AgentMessage}
context::AgentContext (system_prompt, messages, tools)
config::AgentLoopConfig
Output: new_messages::Vector{AgentMessage} (appended prompts + turns)
│ - Creates copy of prompts
│ - Appends prompts to context.messages │
│ - Emits AgentStartEvent │
│ - Emits TurnStartEvent │
│ - Emits MessageStart/End for each prompt │
│ - Calls runLoop() │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. runLoop() ── Main event loop │
│ 2. runLoop() ── Main event loop
│ Input: current_context::AgentContext │
│ new_messages::Vector{AgentMessage} │
│ Output: N/A (writes to new_messages and context.messages) │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ while true: │ │
│ │ 1. Get steering/follow-up messages (if any) │ │
│ │ 2. Emit messages as UserMessage │ │
│ │ 3. streamAssistantResponse() │ │
│ │ 4. Execute tool calls (sequential or parallel) │ │
│ │ 5. Emit TurnEndEvent │ │
│ │ 6. prepare_next_turn (optional) │ │
│ │ 7. should_stop_after_turn? (check termination) │ │
│ │ 8. Loop continues if not terminated │ │
│ │ while true (outer loop: follow-up messages) │ │
│ │ has_more_tool_calls = true │ │
│ │ while has_more_tool_calls || !isempty(pending_messages) │ │
│ │ 1. Emit TurnStartEvent (on subsequent turns) │ │
│ │ 2. If pending_messages: emit MessageStart/End, drain queue │ │
│ │ 3. streamAssistantResponse() │ │
│ │ 4. If error/aborted: emit TurnEnd, AgentEnd, return │ │
│ │ 5. Execute tool calls (sequential or parallel) │ │
│ │ 6. has_more_tool_calls = !batch.terminate │ │
│ │ 7. Emit TurnEndEvent │ │
│ │ 8. prepare_next_turn (optional config update) │ │
│ │ 9. should_stop_after_turn? (early return) │ │
│ │ 10. pending_messages = get_steering_messages() │ │
│ │ if !isempty(get_follow_up_messages()) → continue outer loop │ │
│ │ break │ │
│ │ emit AgentEndEvent │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. streamAssistantResponse() ── LLM interaction │
- transform_context (optional)
- convert_to_llm (transform to Message[])
- Call stream_fn (LLM API)
- Stream response deltas
- Emit MessageStart/Update/End events
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. executeToolCalls() ── Tool execution │
│ 3. streamAssistantResponse() ── LLM interaction
Input: context::AgentContext
config::AgentLoopConfig
Output: message::AssistantMessage
Data Transformations:
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ if EXECUTION_SEQUENTIAL || has_sequential_tool: │ │
│ │ executeToolCallsSequential() │ │
│ │ else: │ │
│ │ executeToolCallsParallel() │ │
│ │ Step 1: transform_context (optional) │ │
│ │ Input: context.messages::Vector{AgentMessage} │ │
│ │ Output: messages::Vector{AgentMessage} (transformed) │ │
│ │ │ │
│ │ Step 2: convert_to_llm │ │
│ │ Input: messages::Vector{AgentMessage} │ │
│ │ Output: llm_messages::Vector{Message} │ │
│ │ - UserMessage → UserMessage │ │
│ │ - AssistantMessage → AssistantMessage │ │
│ │ - ToolResultMessage → ToolResultMessage │ │
│ │ - BashExecutionMessage → UserMessage │ │
│ │ - CompactionSummaryMessage → UserMessage │ │
│ │ - BranchSummaryMessage → UserMessage │ │
│ │ │ │
│ │ Step 3: Call stream_fn │ │
│ │ Input: model, llm_context::Context, config │ │
│ │ Output: response::Stream (events) │ │
│ │ │ │
│ │ Step 4: Stream events │ │
│ │ Events: start, text_start/delta/end, toolcall_start/delta/end │ │
│ │ Final: AssistantMessage (with ToolCall[] in content) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
5. AgentEndEvent ── Final event with all messages
4. executeToolCalls() ── Tool execution
│ Input: assistant_message::AssistantMessage (contains ToolCall[]) │
│ current_context::AgentContext │
│ Output: ExecutedToolCallBatch (messages::ToolResultMessage[], terminate) │
│ │
│ For each ToolCall: │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ prepareToolCall() │ │
│ │ Input: tool_call::ToolCall │ │
│ │ Output: PreparedToolCall or ImmediateToolCallOutcome │ │
│ │ - validates arguments │ │
│ │ - runs before_tool_call hook (optional) │ │
│ │ - runs prepare_arguments hook (optional) │ │
│ │ │ │
│ │ executePreparedToolCall() (if prepared) │ │
│ │ Input: PreparedToolCall │ │
│ │ Output: ExecutedToolCallOutcome │ │
│ │ - calls tool.execute() │ │
│ │ - returns AgentToolResultMutable │ │
│ │ │ │
│ │ finalizeExecutedToolCall() │ │
│ │ Input: ExecutedToolCallOutcome │ │
│ │ Output: FinalizedToolCallOutcome │ │
│ │ - runs after_tool_call hook (optional) │ │
│ │ - returns ToolCall + AgentToolResultMutable + is_error │ │
│ │ │ │
│ │ createToolResultMessage() │ │
│ │ Input: FinalizedToolCallOutcome │ │
│ │ Output: ToolResultMessage │ │
│ │ - role: "toolResult" │ │
│ │ - tool_call_id, tool_name, content, details │ │
│ │ - usage, added_tool_names, is_error, timestamp │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ Sequential: executeToolCallsSequential() │ │
│ │ - Executes tools one at a time, waits for each │ │
│ │ - Returns batch of ToolResultMessage[] │ │
│ │ │ │
│ │ Parallel: executeToolCallsParallel() │ │
│ │ - Creates closures for async execution │ │
│ │ - Executes all closures, collects results │ │
│ │ - Returns batch of ToolResultMessage[] │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. AgentEndEvent ── Final event with all messages │
│ Output: messages::Vector{AgentMessage} │
│ Contains: [UserMessage, AssistantMessage, ToolResultMessage, ...] │
└─────────────────────────────────────────────────────────────────────────────┘
```
@@ -104,9 +182,23 @@ struct AgentLoopConfig
get_api_key::Union{Function, Nothing}
get_steering_messages::Union{Function, Nothing}
get_follow_up_messages::Union{Function, Nothing}
should_stop_after_turn::Union{Function, Nothing}
max_tokens::Union{Int64, Nothing}
temperature::Union{Float64, Nothing}
cache_retention::Union{String, Nothing}
headers::Union{Dict{String, String}, Nothing}
metadata::Union{Dict{String, Any}, Nothing}
signal::Union{Any, Nothing}
api_key::Union{String, Nothing}
end
```
**Notes:**
- `should_stop_after_turn(context::PrepareNextTurnContext)::Bool` — Default returns `false`. Use to implement custom termination logic (e.g., max turns, tool-specific termination).
- `max_tokens`, `temperature`, `cache_retention` — Passed through to the LLM API provider.
- `signal`, `api_key` — Per-request overrides for abort handling and authentication.
- `headers`, `metadata` — Passed through to the LLM API provider.
## Main Functions
### agentLoop()
@@ -161,11 +253,13 @@ function runAgentLoop(
**Purpose**: Execute agent loop with initial prompts
**Flow**:
1. Copy prompts to new_messages
2. Append prompts to context.messages
3. Emit AgentStartEvent
4. For each prompt: emit MessageStartEvent, MessageEndEvent
5. Call runLoop()
1. Copy prompts to `new_messages`
2. Create `current_context` with prompts appended to `context.messages`
3. Emit `AgentStartEvent`
4. Emit `TurnStartEvent`
5. For each prompt: emit `MessageStartEvent`, `MessageEndEvent`
6. Call `runLoop()` — handles the main loop, tool execution, and termination
7. Return `new_messages`
### runLoop() - The Heart of AgentLoop
@@ -180,109 +274,124 @@ function runLoop(
)::Nothing
```
**Main Loop**:
**Main Loop** (simplified — shows structure; actual code has type annotations):
```julia
current_context = initial_context
config = initial_config
first_turn = true
pending_messages = get_steering_messages()
pending_messages = get_steering_messages(config)
while true
# Process steering/follow-up messages
while !isempty(pending_messages)
has_more_tool_calls = true
# Inner loop: process pending messages AND/OR tool results
while has_more_tool_calls || !isempty(pending_messages)
if !first_turn
emit(TurnStartEvent())
else
first_turn = false
end
# Emit pending messages
for message in pending_messages
emit(MessageStartEvent(message))
emit(MessageEndEvent(message))
push!(current_context.messages, message)
push!(new_messages, message)
# Emit pending messages (steering / follow-up)
if !isempty(pending_messages)
for message in pending_messages
emit(MessageStartEvent(message))
emit(MessageEndEvent(message))
push!(current_context.messages, message)
push!(new_messages, message)
end
pending_messages = AgentMessage[]
end
pending_messages = []
end
# Stream assistant response
message = streamAssistantResponse(
current_context,
config,
signal,
emit,
stream_function,
)
push!(new_messages, message)
# Check for errors
if message.stop_reason in ("error", "aborted")
emit(TurnEndEvent(message, []))
emit(AgentEndEvent(new_messages))
return
end
# Execute tool calls
tool_calls = filter(c -> c isa ToolCall, message.content)
tool_results = []
has_more_tool_calls = false
if !isempty(tool_calls)
executed_batch = if message.stop_reason == "length"
failToolCallsFromTruncatedMessage(tool_calls, emit)
else
executeToolCalls(
current_context,
message,
config,
signal,
emit,
# Stream assistant response
message = streamAssistantResponse(
current_context, config, signal, emit, stream_function
)
push!(new_messages, message)
# Early exit on error/abort
if message.stop_reason in ("error", "aborted")
emit(TurnEndEvent(message, ToolResultMessage[]))
emit(AgentEndEvent(new_messages))
return
end
# Execute tool calls (if any)
tool_calls = filter(c -> c isa ToolCall, message.content)
tool_results = ToolResultMessage[]
has_more_tool_calls = false
if !isempty(tool_calls)
executed_batch = if message.stop_reason == "length"
failToolCallsFromTruncatedMessage(tool_calls, emit)
else
executeToolCalls(
current_context, message, config, signal, emit
)
end
append!(tool_results, executed_batch.messages)
has_more_tool_calls = !executed_batch.terminate
for result in tool_results
push!(current_context.messages, result)
push!(new_messages, result)
end
end
emit(TurnEndEvent(message, tool_results))
# Optional: prepare next turn (model/thinking/context changes)
next_turn_context = PrepareNextTurnContext(
message, tool_results, current_context, new_messages
)
next_turn_snapshot = prepare_next_turn(config, next_turn_context)
if !isnothing(next_turn_snapshot)
current_context = next_turn_snapshot.context
# Rebuild config with updated model/thinking + preserved fields
config = AgentLoopConfig(
model = next_turn_snapshot.model,
reasoning = next_turn_snapshot.thinking_level,
convert_to_llm = config.convert_to_llm,
transform_context = config.transform_context,
get_api_key = config.get_api_key,
should_stop_after_turn = config.should_stop_after_turn,
prepare_next_turn = config.prepare_next_turn,
get_steering_messages = config.get_steering_messages,
get_follow_up_messages = config.get_follow_up_messages,
tool_execution = config.tool_execution,
before_tool_call = config.before_tool_call,
after_tool_call = config.after_tool_call,
max_tokens = config.max_tokens,
temperature = config.temperature,
reasoning = config.reasoning,
cache_retention = config.cache_retention,
session_id = config.session_id,
headers = config.headers,
metadata = config.metadata,
transport = config.transport,
signal = signal,
api_key = config.api_key,
on_payload = config.on_payload,
on_response = config.on_response,
max_retry_delay_ms = config.max_retry_delay_ms,
)
end
append!(tool_results, executed_batch.messages)
has_more_tool_calls = !executed_batch.terminate
for result in tool_results
push!(current_context.messages, result)
push!(new_messages, result)
# Check termination
if should_stop_after_turn(config, next_turn_context)
emit(AgentEndEvent(new_messages))
return
end
# Get next steering messages
pending_messages = get_steering_messages(config)
end
emit(TurnEndEvent(message, tool_results))
# Prepare next turn (optional)
next_turn_context = PrepareNextTurnContext(
message, tool_results, current_context, new_messages
)
next_turn_snapshot = prepare_next_turn(config, next_turn_context)
if !isnothing(next_turn_snapshot)
current_context = next_turn_snapshot.context
config = AgentLoopConfig(
model = next_turn_snapshot.model,
reasoning = next_turn_snapshot.thinking_level,
# ... other config fields
)
end
# Check if should stop
if should_stop_after_turn(config, next_turn_context)
emit(AgentEndEvent(new_messages))
return
end
# Get next pending messages
pending_messages = get_steering_messages()
# Check follow-up messages
follow_up_messages = get_follow_up_messages()
# Check follow-up messages (processed only after all tool calls complete)
follow_up_messages = get_follow_up_messages(config)
if !isempty(follow_up_messages)
pending_messages = follow_up_messages
continue
end
break
end
@@ -301,17 +410,44 @@ function streamAssistantResponse(
)::AssistantMessage
```
**Flow**:
1. Get messages from context
2. Apply transform_context (optional)
3. Convert to LLM messages with convert_to_llm
4. Create Context object
5. Resolve API key
6. Call stream_fn with model, context, and config
7. Stream events:
- "start" → MessageStartEvent
- "text_start", "text_delta", "text_end" → MessageUpdateEvent
- "done", "error" → MessageEndEvent
**Data Flow**:
```
Input: context.messages::Vector{AgentMessage}
[transform_context] (optional hook)
messages::Vector{AgentMessage}
[convert_to_llm] - Type transformation pipeline
llm_messages::Vector{Message}
│ AgentMessage → Message mapping:
│ • UserMessage → UserMessage (pass-through)
│ • AssistantMessage → AssistantMessage (pass-through)
│ • ToolResultMessage → ToolResultMessage (pass-through)
│ • BashExecutionMessage → UserMessage (text conversion)
│ • CompactionSummaryMessage → UserMessage (text wrapped)
│ • BranchSummaryMessage → UserMessage (text wrapped)
Context(system_prompt, llm_messages, tools)
stream_fn(model, context, config) - LLM API call
Stream of AssistantMessageEvent:
• StartEvent (partial AssistantMessage)
• TextStartEvent/TextDeltaEvent/TextEndEvent
• ToolCallStartEvent/ToolCallDeltaEvent/ToolCallEndEvent
• DoneEvent (final AssistantMessage with usage, stop_reason)
Return: AssistantMessage
- content::Vector{MessageContent}
- usage::Usage
- stop_reason::String
- Contains ToolCall[] if tool calls requested
```
### executeToolCalls()
@@ -325,22 +461,51 @@ function executeToolCalls(
)::ExecutedToolCallBatch
```
**Logic**:
```julia
tool_calls = filter(c -> c isa ToolCall, assistant_message.content)
**Data Flow**:
# Check if any tool requires sequential execution
has_sequential = any(tc -> begin
tool = findfirst(t -> t.name == tc.name, current_context.tools)
!isnothing(tool) && tool.execution_mode == EXECUTION_SEQUENTIAL
end, tool_calls)
# Determine execution mode
if config.tool_execution == EXECUTION_SEQUENTIAL || has_sequential
executeToolCallsSequential(...)
else
executeToolCallsParallel(...)
end
```
Input: assistant_message::AssistantMessage
- content::Vector{MessageContent}
└─ Contains ToolCall[] and/or TextContent[]
filter(c -> c isa ToolCall, assistant_message.content)
tool_calls::Vector{ToolCall}
- type: "tool"
- id::String
- name::String
- arguments::Dict{String, Any}
- partial_json::Union{String, Nothing}
Check execution mode:
• config.tool_execution (sequential/parallel)
• Any tool.execution_mode == EXECUTION_SEQUENTIAL?
┌─────────────────────────────────────────────────────────────────┐
│ Sequential Mode (or has_sequential_tool) │
│ For each tool_call in tool_calls: │
│ prepareToolCall() → PreparedToolCall │
│ executePreparedToolCall() → ExecutedToolCallOutcome │
│ finalizeExecutedToolCall() → FinalizedToolCallOutcome │
│ createToolResultMessage() → ToolResultMessage │
│ (wait for completion before next tool) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Parallel Mode │
│ For each tool_call in tool_calls: │
│ prepareToolCall() → (PreparedToolCall | ImmediateOutcome) │
│ If prepared: create closure │
│ If immediate: execute and add to finalized_calls │
│ │
│ For each entry in finalized_calls: │
│ If closure: execute closure │
│ If finalized: use as-is │
└─────────────────────────────────────────────────────────────────┘
ExecutedToolCallBatch
- messages::Vector{ToolResultMessage}
- terminate::Bool (true if all tools have terminate=true)
```
### executeToolCallsSequential()
@@ -400,13 +565,41 @@ function prepareToolCall(
)::Union{PreparedToolCall, ImmediateToolCallOutcome}
```
**Flow**:
1. Find tool by name
2. If not found → ImmediateToolCallOutcome (error)
3. before_tool_call hook (optional)
4. prepareToolCallArguments() (optional)
5. validateToolArguments()
6. Return PreparedToolCall
**Data Flow**:
```
Input: tool_call::ToolCall
- id::String
- name::String
- arguments::Dict{String, Any}
findfirst(t -> t.name == tool_call.name, current_context.tools)
If tool is nothing:
→ ImmediateToolCallOutcome("immediate", error_result, is_error=true)
If tool exists:
[before_tool_call hook] (optional)
Input: BeforeToolCallContext(assistant_message, tool_call, args, context)
Output: BeforeToolCallResult (block, reason) or nothing
If block=true → ImmediateToolCallOutcome(error)
prepareToolCallArguments(tool, tool_call)
Input: tool_call.arguments::Dict{String, Any}
Output: prepared_arguments::Any
(Optional: transform arguments before validation)
validateToolArguments(tool, prepared_tool_call)
Input: prepared_tool_call.arguments
Output: validated_args::Any
(Optional: JSON schema validation)
Return: PreparedToolCall("prepared", tool_call, tool, validated_args)
- kind: "prepared"
- tool_call: ToolCall (original)
- tool: AgentTool
- args: validated arguments
```
### executePreparedToolCall()
@@ -418,11 +611,35 @@ function executePreparedToolCall(
)::ExecutedToolCallOutcome
```
**Flow**:
1. Call tool.execute(id, args, signal, on_update)
2. Collect update events (if any)
3. Wait for all update events
4. Return ExecutedToolCallOutcome(result)
**Data Flow**:
```
Input: prepared::PreparedToolCall
- tool_call::ToolCall
- tool::AgentTool
- args::Any (validated)
tool.execute(tool_call.id, args, signal, on_update)
Input: tool_call_id::String
args::Any
signal::Union{Any, Nothing}
on_update::Function (partial_result → void)
Output: AgentToolResultMutable (defined in agent_loop.jl)
- content::Vector{MessageContent}
- details::Any
- usage::Union{Usage, Nothing}
- added_tool_names::Union{Vector{String}, Nothing}
- terminate::Union{Bool, Nothing}
Note: tool.execute signature is
(tool_call_id, args, signal, on_update, context)
where context is the tool's captured context closure parameter
Collect update events from on_update callbacks
Return: ExecutedToolCallOutcome(result, is_error=false)
- result::AgentToolResultMutable
```
### finalizeExecutedToolCall()
@@ -437,9 +654,44 @@ function finalizeExecutedToolCall(
)::FinalizedToolCallOutcome
```
**Flow**:
1. after_tool_call hook (optional)
2. Return FinalizedToolCallOutcome
**Data Flow**:
```
Input: executed::ExecutedToolCallOutcome
- result::AgentToolResultMutable
- is_error::Bool
[after_tool_call hook] (optional)
Input: AfterToolCallContext(
assistant_message,
tool_call,
args,
result,
is_error,
context
)
Output: AfterToolCallResult (optional patches)
- content::Union{Vector{MessageContent}, Nothing}
- details::Union{Any, Nothing}
- is_error::Union{Bool, Nothing}
- usage::Union{Usage, Nothing}
- terminate::Union{Bool, Nothing}
Apply patches to result (if patches not nothing, replace non-nothing fields)
result = AgentToolResultMutable(
patches.content != nothing ? patches.content : result.content,
patches.details != nothing ? patches.details : result.details,
patches.usage != nothing ? patches.usage : result.usage,
result.added_tool_names, # not patched
patches.terminate != nothing ? patches.terminate : result.terminate,
)
is_error = patches.is_error != nothing ? patches.is_error : is_error
Return: FinalizedToolCallOutcome
- tool_call::ToolCall (original)
- result::AgentToolResultMutable (final)
- is_error::Bool
```
### createToolResultMessage()
@@ -449,19 +701,39 @@ function createToolResultMessage(
)::ToolResultMessage
```
**Creates**:
```julia
ToolResultMessage(
"toolResult",
finalized.tool_call.id,
finalized.tool_call.name,
finalized.result.content,
finalized.result.details,
finalized.result.usage,
finalized.result.added_tool_names,
finalized.is_error,
timestamp,
)
**Data Flow**:
```
Input: finalized::FinalizedToolCallOutcome
- tool_call::ToolCall
- result::AgentToolResultMutable
- content::Vector{MessageContent}
- details::Any
- usage::Union{Usage, Nothing}
- added_tool_names::Union{Vector{String}, Nothing}
- is_error::Bool
Build ToolResultMessage:
• role: "toolResult"
• tool_call_id: tool_call.id
• tool_name: tool_call.name
• content: result.content
• details: result.details
• usage: result.usage
• added_tool_names: result.added_tool_names
• is_error: is_error
• timestamp: Int64(Dates.now(Dates.UTC).datetime)
Output: ToolResultMessage
- role::String ("toolResult")
- tool_call_id::String
- tool_name::String
- content::Vector{MessageContent}
- details::Any
- usage::Union{Usage, Nothing}
- added_tool_names::Union{Vector{String}, Nothing}
- is_error::Bool
- timestamp::Timestamp (Int64)
```
## Execution Modes
@@ -470,45 +742,39 @@ ToolResultMessage(
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Sequential Execution Flow
│ Sequential Execution Flow (Strict Order)
└─────────────────────────────────────────────────────────────────────────┘
┌──────┐
│ TC1 │ ──► prepareToolCall()
└──────┘ │
┌──────────────┐
│ execute() │ ──► Wait for completion
└──────────────┘
├───────────── createToolResultMessage()
▼ ▼
────────────── ──────────
TC2 ──► │ Result1
└──────┘ └──────────┘
┌──────────────┐
│ execute() │
└──────────────┘
┌──────────────┐
│ TC3 │ ──► │
└──────┘
───── createToolResultMessage()
▼ ▼
┌──────────────┐ ┌──────────┐
│ execute() │ │ │ Result2 │
└──────────────┘ └──────────┘
┌──────────┐
│ Result3 │
└──────────┘
TC1 TC2 TC3
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ prepareToolCall()│───▶│ prepareToolCall()│───▶│ prepareToolCall()│
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
execute() execute() execute()
│ (blocking) │ │ (blocking) │ │ (blocking) │
└──────────────────┘ └────────────────── └──────────────────
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ finalize() │ │ finalize() │ │ finalize() │
│ + emit events │ │ + emit events │ │ + emit events │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
▼ ▼ ▼
Result1 Result2 Result3
│ │ │
└───────────────────────┴───────────────────────┘
────────────────────────┐
│ ExecutedToolCallBatch
│ (Result1, Result2, │
│ Result3, terminate) │
└────────────────────────┘
```
### Parallel Execution
@@ -520,13 +786,13 @@ ToolResultMessage(
┌──────┐
│ TC1 │ ──► prepareToolCall() ──► create closure ──► ┐
└──────┘
┌──────┐
└──────┘ │
┌──────┐ │
│ TC2 │ ──► prepareToolCall() ──► create closure ──► ├─► All closures queued
└──────┘
┌──────┐
└──────┘ │
┌──────┐ │
│ TC3 │ ──► prepareToolCall() ──► create closure ──► ┘
└──────┘
@@ -650,50 +916,85 @@ AgentStartEvent
### 1. Message Transformation Pipeline
```
AgentMessage[] (internal)
Vector{AgentMessage} (internal conversation history)
transform_context()
AgentMessage[] (transformed)
├─ transform_context() (optional hook)
│ Input: Vector{AgentMessage}
│ Output: Vector{AgentMessage} (transformed)
convert_to_llm()
Message[] (LLM API)
└─ convert_to_llm()
│ Type mapping (single dispatch):
│ • UserMessage → UserMessage (pass-through)
│ • AssistantMessage → AssistantMessage (pass-through)
│ • ToolResultMessage → ToolResultMessage (pass-through)
│ • BashExecutionMessage → UserMessage (text conversion)
│ • CompactionSummaryMessage → UserMessage (text wrapped)
│ • BranchSummaryMessage → UserMessage (text wrapped)
Vector{Message} (for LLM API)
```
### 2. Tool Call Lifecycle
### 2. Tool Call Lifecycle (with Data Transformations)
```
ToolCall (in assistant message)
ToolCall (in AssistantMessage.content)
├─ before_tool_call (hook)
├─ before_tool_call hook (optional)
│ Input: BeforeToolCallContext(
│ assistant_message::AssistantMessage,
│ tool_call::ToolCall,
│ args::Dict{String, Any},
│ context::AgentContext
│ )
│ Output: BeforeToolCallResult (block, reason) or nothing
├─ prepareToolCall()
├─ validate arguments
└─ prepare arguments (optional)
Input: tool_call::ToolCall
Output: Union{PreparedToolCall, ImmediateToolCallOutcome}
│ • PreparedToolCall (kind, tool_call, tool, args)
│ • ImmediateToolCallOutcome (immediate, result, is_error)
├─ execute()
├─ Immediate: return result
└─ Prepared: async execution
├─ executePreparedToolCall() (if prepared)
Input: PreparedToolCall
Output: ExecutedToolCallOutcome
│ tool.execute() returns AgentToolResultMutable
│ • content::Vector{MessageContent}
│ • details::Any
│ • usage::Union{Usage, Nothing}
│ • terminate::Union{Bool, Nothing}
├─ after_tool_call (hook)
├─ finalizeExecutedToolCall()
│ Input: ExecutedToolCallOutcome
│ Output: FinalizedToolCallOutcome
│ • tool_call::ToolCall
│ • result::AgentToolResultMutable
│ • is_error::Bool
└─ createToolResultMessage()
Input: FinalizedToolCallOutcome
Output: ToolResultMessage
• role: "toolResult"
• tool_call_id, tool_name
• content::Vector{MessageContent}
• details, usage, added_tool_names
• is_error, timestamp
```
### 3. Turn Termination
```julia
# Turn ends when:
# 1. No more pending messages
# 2. No more tool calls to execute
# 3. should_stop_after_turn() returns true
# The outer while-true loop exits when:
# 1. No pending messages AND no tool results to reprocess (inner loop ends)
# 2. No follow-up messages to queue
# 3. should_stop_after_turn() returns true (checked after each tool-call batch)
# Reasons to stop:
# - Max turns reached
# - Tool returned terminate=true
# - Error or abort
# - Steering/follow-up queues empty
# Termination conditions:
# - message.stop_reason in ("error", "aborted") → immediate return
# - should_stop_after_turn() hook returns true → return AgentEndEvent
# - tool result batch has terminate=true → has_more_tool_calls = false, exit inner loop
# - No pending messages, no follow-up messages → break outer loop
```
## Best Practices
@@ -713,6 +1014,12 @@ using AgentCore
config = AgentLoopConfig(
model = my_model,
reasoning = THINKING_MEDIUM,
session_id = nothing,
on_payload = nothing,
on_response = nothing,
transport = "auto",
thinking_budgets = nothing,
max_retry_delay_ms = nothing,
tool_execution = EXECUTION_PARALLEL,
before_tool_call = myBeforeToolCallHook,
after_tool_call = myAfterToolCallHook,
@@ -722,6 +1029,14 @@ config = AgentLoopConfig(
get_api_key = myGetApiKey,
get_steering_messages = myGetSteeringMessages,
get_follow_up_messages = myGetFollowUpMessages,
should_stop_after_turn = myShouldStopHook, # Default: always return false
max_tokens = nothing,
temperature = nothing,
cache_retention = nothing,
headers = nothing,
metadata = nothing,
signal = nothing,
api_key = nothing,
)
# Start agent loop
+372 -65
View File
@@ -19,85 +19,131 @@
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ToolExecutionMode (Enum) │
│ - EXECUTION_SEQUENTIAL (Tools run one at a time) │
│ - EXECUTION_PARALLEL (Tools run concurrently) │
│ ToolExecutionMode (Enum)
│ - EXECUTION_SEQUENTIAL (Tools run one at a time)
│ - EXECUTION_PARALLEL (Tools run concurrently)
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ QueueMode (Enum) │
│ - QUEUE_ALL (Drain all messages at once) │
│ - QUEUE_ONE_AT_A_TIME (Process one message at a time) │
│ QueueMode (Enum)
│ - QUEUE_ALL (Drain all messages at once)
│ - QUEUE_ONE_AT_A_TIME (Process one message at a time)
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ MessageContent (Abstract Type) │
│ ├── TextContent (String) │
│ └── ImageContent (data::String, mime_type::String) │
│ MessageContent (Abstract Type)
│ ├── TextContent (String)
│ └── ImageContent (data::String, mime_type::String)
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Message (Abstract Type) │
│ ├── UserMessage │
│ │ └─ role: "user", content: Message[], timestamp: Int64 │
│ ├── AssistantMessage │
│ │ └─ role: "assistant", content: Message[], api, provider, model, │
│ │ usage: Usage, stop_reason, error_message, timestamp │
│ └── ToolResultMessage │
│ └─ role: "toolResult", tool_call_id, tool_name, content, details, │
│ usage, added_tool_names, is_error, timestamp │
│ Message (Abstract Type)
│ ├── UserMessage
│ │ └─ role: "user", content: Message[], timestamp: Int64
│ ├── AssistantMessage
│ │ └─ role: "assistant", content: Message[], api, provider, model,
│ │ usage: Usage, stop_reason, error_message, timestamp
│ └── ToolResultMessage
│ └─ role: "toolResult", tool_call_id, tool_name, content, details,
│ usage, added_tool_names, is_error, timestamp
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ AgentMessage (Abstract Type) │
│ └─ Union of all message types above + custom types │
│ AgentMessage (Abstract Type)
│ └─ Union of all message types above + custom types
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ AgentTool │
│ - name: String │
│ - label: String │
│ - description: String │
│ - parameters: Any │
│ - execute: Function │
│ - prepare_arguments: Union{Function, Nothing} │
│ - execution_mode: Union{ToolExecutionMode, Nothing} │
│ AgentTool
│ - name: String
│ - label: String
│ - description: String
│ - parameters: Any
│ - execute: Function
│ - prepare_arguments: Union{Function, Nothing}
│ - execution_mode: Union{ToolExecutionMode, Nothing}
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ AgentContext │
│ - system_prompt: String │
│ - messages: Vector{AgentMessage} │
│ - tools: Union{Vector{AgentTool}, Nothing} │
│ AgentContext
│ - system_prompt: String
│ - messages: Vector{AgentMessage}
│ - tools: Union{Vector{AgentTool}, Nothing}
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ AgentEvent (Abstract Type) │
│ ├── AgentStartEvent / AgentEndEvent │
│ ├── TurnStartEvent / TurnEndEvent │
│ ├── MessageStartEvent / MessageEndEvent │
│ ├── MessageUpdateEvent │
│ ├── ToolExecutionStartEvent / ToolExecutionEndEvent │
│ └── ToolExecutionUpdateEvent │
│ AgentEvent (Abstract Type)
│ ├── AgentStartEvent / AgentEndEvent
│ ├── TurnStartEvent / TurnEndEvent
│ ├── MessageStartEvent / MessageEndEvent
│ ├── MessageUpdateEvent
│ ├── ToolExecutionStartEvent / ToolExecutionEndEvent
│ └── ToolExecutionUpdateEvent
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Usage & ModelCost │
│ Usage: input, output, cache_read, cache_write, total_tokens, cost │
│ ModelCost: input, output, cache_read, cache_write (all Float64) │
│ Usage & ModelCost
│ Usage: input, output, cache_read, cache_write, total_tokens, cost
│ ModelCost: input, output, cache_read, cache_write (all Float64)
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Model │
│ - id, name, api, provider, base_url, reasoning: Bool │
│ - input: Vector{String} │
│ - cost: ModelCost │
│ - context_window, max_tokens: Int64 │
│ Model
│ - id, name, api, provider, base_url, reasoning: Bool
│ - input: Vector{String}
│ - cost: ModelCost
│ - context_window, max_tokens: Int64
└─────────────────────────────────────────────────────────────────────────────┘
```
## Message Types
### Type Hierarchy
```
Message (for LLM API)
├── UserMessage (role: "user")
│ ├── content::Vector{MessageContent}
│ │ ├── TextContent (text::String)
│ │ └── ImageContent (data::String, mime_type::String)
│ └── timestamp::Timestamp (Int64)
├── AssistantMessage (role: "assistant")
│ ├── content::Vector{MessageContent}
│ │ ├── TextContent
│ │ └── ToolCall (type, id, name, arguments::Dict{String, Any})
│ ├── api::String
│ ├── provider::String
│ ├── model::String
│ ├── usage::Usage
│ │ ├── input, output, cache_read, cache_write, total_tokens::Int64
│ │ └── cost::UsageCost (input, output, cache_read, cache_write, total::Float64)
│ ├── stop_reason::String
│ ├── error_message::Union{String, Nothing}
│ └── timestamp::Timestamp
└── ToolResultMessage (role: "toolResult")
├── tool_call_id::String
├── tool_name::String
├── content::Vector{MessageContent}
├── details::Any
├── usage::Union{Usage, Nothing}
├── added_tool_names::Union{Vector{String}, Nothing}
├── is_error::Bool
└── timestamp::Timestamp
AgentMessage (internal, extends Message)
├── UserMessage (same as above)
├── AssistantMessage (same as above)
├── ToolResultMessage (same as above)
├── BashExecutionMessage (custom, converted to UserMessage)
│ ├── role, command, output, exit_code
│ ├── cancelled, truncated, full_output_path
│ └── exclude_from_context::Bool
├── CompactionSummaryMessage (custom, converted to UserMessage)
│ ├── summary, tokens_before, timestamp
└── BranchSummaryMessage (custom, converted to UserMessage)
├── summary, from_id, timestamp
```
### UserMessage
```julia
@@ -108,6 +154,16 @@ struct UserMessage <: Message
end
```
**Usage**:
```julia
# Simple text message
UserMessage(
"user",
[TextContent("Hello, how are you?")],
Int64(Dates.now(Dates.UTC).datetime)
)
```
**Usage**:
```julia
# Simple text message
@@ -192,6 +248,9 @@ struct ToolResultMessage <: Message
is_error::Bool # True if tool execution failed
timestamp::Timestamp
end
# Note: AgentToolResult{T} (types.jl) - generic result type with type param T
# AgentToolResultMutable (agent_loop.jl) - mutable variant used internally
```
**Usage**:
@@ -232,6 +291,8 @@ end
- `prepare_arguments`: Optional preprocessing
- `execution_mode`: Sequential or parallel
**Note:** `AgentHarnessTool` (`harness_types.jl:91`) is a harness-specific variant with the same structure but uses camelCase field names (`prepareArguments`, `executionMode`) and includes additional type parameters `{TContext, TParameters, TDetails}`.
### Tool Execution Function Signature
```julia
@@ -241,12 +302,12 @@ execute::Function(
signal::Union{Any, Nothing}, # Abort signal
on_update::Function, # Callback for streaming updates
context::Any, # Tool context
)::AgentToolResult
)::AgentToolResult{T}
```
**Returns**:
**Returns** (`AgentToolResult{T}` from `types.jl`):
```julia
AgentToolResult(
AgentToolResult{T}(
content::Vector{MessageContent}, # Result content
details::T, # Tool-specific details
usage::Union{Usage, Nothing}, # Usage statistics
@@ -255,6 +316,10 @@ AgentToolResult(
)
```
**Note:** `AgentToolResultMutable` (in `agent_loop.jl`) is a mutable variant used internally for intermediate results.
**Note:** External types used throughout the codebase: `Context`, `AbortSignal`, `EventStream`, `Promise` are defined in external modules (not in the source files covered by this document).
## AgentContext
```julia
@@ -476,6 +541,32 @@ mutable struct BranchSummaryMessage
end
```
### CustomMessage
**Note:** There are two `CustomMessage` types in the codebase:
1. **Types.CustomMessage** (`types.jl:155`) - A simple wrapper that holds another `AgentMessage` with a custom type label:
```julia
struct CustomMessage <: AgentMessage
message::AgentMessage
custom_type::String
end
```
2. **Messages.CustomMessage{T}** (`messages.jl:42`) - A standalone mutable message with content, display flag, and details:
```julia
mutable struct CustomMessage{T}
role::String
custom_type::String
content::Union{String, Vector{MessageContent}}
display::Bool
details::Union{T, Nothing}
timestamp::Timestamp
end
```
Only `Messages.CustomMessage{T}` is converted by `convertToLlmMessage()` to a `UserMessage`.
## AgentState
```julia
@@ -496,9 +587,9 @@ end
**Note**: AgentState is mutable and used internally by Agent
## Key Conversion Functions
## Message Transformation Pipeline
### convertToLlm()
### convertToLlm() - AgentMessage[] → Message[]
```julia
function convertToLlm(messages::Vector{AgentMessage})::Vector{Message}
@@ -515,26 +606,57 @@ function convertToLlm(messages::Vector{AgentMessage})::Vector{Message}
end
```
**Purpose**: Transform AgentMessage[] to Message[] for LLM API
**Data Flow**:
```
Vector{AgentMessage} (internal conversation history)
│ Type dispatch on convertToLlmMessage():
│ • UserMessage → UserMessage (pass-through)
│ • AssistantMessage → AssistantMessage (pass-through)
│ • ToolResultMessage → ToolResultMessage (pass-through)
│ Custom messages converted to UserMessage:
│ • BashExecutionMessage → UserMessage
│ (via bashExecutionToText() for display)
│ • CompactionSummaryMessage → UserMessage
│ (wrapped with COMPACTION_SUMMARY_PREFIX/SUFFIX)
│ • BranchSummaryMessage → UserMessage
│ (wrapped with BRANCH_SUMMARY_PREFIX/SUFFIX)
│ • CustomMessage → UserMessage
│ (content field used directly, string→TextContent)
Vector{Message} (for LLM API)
- Excludes: BashExecutionMessage (if exclude_from_context)
- Includes: All standard messages + converted custom messages
```
**Example**:
```julia
# Input: AgentMessage[]
# Input: Vector{AgentMessage}
[
UserMessage(...),
AssistantMessage(...),
ToolResultMessage(...),
BashExecutionMessage(...), # Will be converted to UserMessage
CompactionSummaryMessage(...), # Will be converted to UserMessage
UserMessage("user", [TextContent("Hello")], 1234567890),
AssistantMessage("assistant", [
TextContent("Hi there!"),
ToolCall("bash", "call_123", "bash", Dict("command" => "ls"), nothing)
], "openai", "openai", "gpt-4", Usage(...), "done", nothing, 1234567891),
BashExecutionMessage("custom", "ls -la", "file1.md\nfile2.md\n", 0, false, false, nothing, 1234567892, false),
CompactionSummaryMessage("compactionSummary", "Previous conversation compacted", 1000, 1234567893),
CustomMessage("custom", "someCustomType", "Some custom content", true, nothing, 1234567894),
]
# Output: Message[]
# Output: Vector{Message}
[
UserMessage(...),
AssistantMessage(...),
ToolResultMessage(...),
UserMessage(...), # Converted from BashExecutionMessage
UserMessage(...), # Converted from CompactionSummaryMessage
UserMessage("user", [TextContent("Hello")], 1234567890),
AssistantMessage("assistant", [
TextContent("Hi there!"),
ToolCall(...)
], "openai", "openai", "gpt-4", Usage(...), "done", nothing, 1234567891),
UserMessage("user", [TextContent("Ran `ls -la`\n```\nfile1.md\nfile2.md\n```\n")], 1234567892),
UserMessage("user", [TextContent("<summary>Previous conversation compacted</summary>")], 1234567893),
UserMessage("user", [TextContent("Some custom content")], 1234567894),
]
```
@@ -553,6 +675,15 @@ function convertToLlmMessage(m::CompactionSummaryMessage)
return UserMessage("user", [TextContent(text)], m.timestamp)
end
function convertToLlmMessage(m::CustomMessage)::Union{UserMessage, Nothing}
content = if m.content isa String
[TextContent(m.content)]
else
m.content
end
return UserMessage("user", content, m.timestamp)
end
function convertToLlmMessage(m::BranchSummaryMessage)
text = BRANCH_SUMMARY_PREFIX * m.summary * BRANCH_SUMMARY_SUFFIX
return UserMessage("user", [TextContent(text)], m.timestamp)
@@ -571,6 +702,182 @@ function convertToLlmMessage(m::ToolResultMessage)
end
```
## Complete Data Flow Examples
### Example 1: User Prompt → Assistant Response
```
User Input:
"Hello, what's in the current directory?"
prompt(agent, "Hello, what's in the current directory?")
└─► normalizePromptInput(String)
Input: "Hello, what's in the current directory?"
Output: [UserMessage("user", [TextContent("Hello, what's in the current directory?")], timestamp)]
AgentLoop execution:
├─► transform_context() (optional)
│ Input: [UserMessage(...)]
│ Output: [UserMessage(...)]
├─► convert_to_llm()
│ Input: [UserMessage(...)]
│ Output: [UserMessage(...)]
├─► stream_fn() - LLM API
│ Input: model, Context(...), config
│ Output: AssistantMessage with ToolCall[]
│ role: "assistant"
│ content: [
│ TextContent("I'll check the directory for you."),
│ ToolCall("tool", "tc_123", "bash", Dict("command" => "ls -la"), nothing)
│ ]
│ usage: Usage(input=100, output=20, ...)
│ stop_reason: "done"
├─► executeToolCalls()
│ Input: AssistantMessage with ToolCall[]
│ Output: ToolResultMessage[]
│ role: "toolResult"
│ tool_call_id: "tc_123"
│ tool_name: "bash"
│ content: [TextContent("file1.md\nfile2.md\n")]
│ is_error: false
└─► Append to context.messages
Final Conversation History:
[
UserMessage("user", [TextContent("Hello, what's in the current directory?")], ...),
AssistantMessage("assistant", [
TextContent("I'll check the directory for you."),
ToolCall("tool", "tc_123", "bash", Dict("command" => "ls -la"), nothing)
], "openai", "openai", "gpt-4", Usage(...), "done", nothing, ...),
ToolResultMessage("toolResult", "tc_123", "bash", [TextContent("file1.md\nfile2.md\n")], ..., false, ...),
]
```
### Example 2: Tool Call Execution → Tool Result
```
ToolCall from AssistantMessage
├─ type: "tool"
├─ id: "tc_123"
├─ name: "bash"
├─ arguments: Dict("command" => "ls -la")
└─ partial_json: nothing
prepareToolCall(tool_call)
Finds tool by name "bash"
before_tool_call hook (optional)
Input: BeforeToolCallContext(...)
Output: BeforeToolCallResult(block=false) or nothing
validateToolArguments(tool_call)
Input: Dict("command" => "ls -la")
Output: Dict("command" => "ls -la")
Return: PreparedToolCall("prepared", tool_call, bash_tool, validated_args)
executePreparedToolCall(prepared)
tool.execute("tc_123", Dict("command" => "ls -la"), signal, on_update)
Bash tool executes "ls -la" command
Returns: AgentToolResultMutable(
content: [TextContent("file1.md\nfile2.md\n")],
details: BashToolDetails(...),
usage: nothing,
added_tool_names: nothing,
terminate: nothing
)
finalizeExecutedToolCall(executed)
after_tool_call hook (optional)
Input: AfterToolCallContext(...)
Output: AfterToolCallResult(...) or nothing
Return: FinalizedToolCallOutcome(
tool_call: ToolCall(...),
result: AgentToolResultMutable(...),
is_error: false
)
createToolResultMessage(finalized)
Return: ToolResultMessage(
role: "toolResult",
tool_call_id: "tc_123",
tool_name: "bash",
content: [TextContent("file1.md\nfile2.md\n")],
details: BashToolDetails(...),
usage: nothing,
added_tool_names: nothing,
is_error: false,
timestamp: Int64(...)
)
```
### Example 3: Custom Message Conversion
```
BashExecutionMessage (custom, for logging)
role: "custom"
command: "ls -la"
output: "file1.md\nfile2.md\n"
exit_code: 0
cancelled: false
truncated: false
full_output_path: nothing
timestamp: 1234567890
exclude_from_context: false
convertToLlmMessage(BashExecutionMessage)
bashExecutionToText(msg)
Output: "Ran `ls -la`\n```\nfile1.md\nfile2.md\n```\n"
Return: UserMessage(
"user",
[TextContent("Ran `ls -la`\n```\nfile1.md\nfile2.md\n```\n")],
1234567890
)
(Excluded if exclude_from_context = true)
────────────────────────────────────────────────────────────────────
CompactionSummaryMessage (custom, for history compression)
role: "compactionSummary"
summary: "Previous 100 turns about Python programming"
tokens_before: 15000
timestamp: 1234567890
convertToLlmMessage(CompactionSummaryMessage)
Text = COMPACTION_SUMMARY_PREFIX + summary + COMPACTION_SUMMARY_SUFFIX
Result: "<summary>\nPrevious 100 turns about Python programming\n</summary>"
Return: UserMessage(
"user",
[TextContent("<summary>...\nPrevious 100 turns...\n</summary>")],
1234567890
)
## Summary
The type system in AgentCore.jl provides:
+347 -192
View File
@@ -1,6 +1,6 @@
# AgentCore.jl - Session Management Deep Dive
## Session Architecture
## Session Architecture with Data Flow
```
┌─────────────────────────────────────────────────────────────────────────────┐
@@ -9,45 +9,86 @@
┌─────────────────────────────────────────────────────────────────────────────┐
│ Session = Tree of Entries │
│ │
│ Each entry represents a change in conversation state │
│ │
│ Branch Navigation: │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ │ E1 │────▶│ E2 │────▶│ E3 │────▶│ E4 │────▶│ E5 │ (current leaf)
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ Message Message Compaction Message BranchSummary │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ E1 │────▶│ E2 │────▶│ E3 │────▶│ E4 │────▶│ E5 │ (current leaf) │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼
│ Message Message Compaction Message BranchSummary
│ │
│ Data Flow: │
│ AgentMessage[] (AgentState.messages) │
│ │ │
│ └─► appendMessage() → MessageEntry │
│ └─► storage.appendEntry() → JSONL file │
│ │
│ To navigate to E2 (fork point): │
Session.moveTo(E2) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │────▶│ E5' │ (new branch)
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │ │
session.moveTo(E2) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │────▶│ E5' │ (new branch) │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘
│ │ │ │
│ │ ▼ create BranchSummary │
│ │ ┌─────┐ │
└──────│ E6 │ (branch summary) │
└─────┘ │
│ E6 │ (branch summary) │
└─────┘ │
│ └───────────────────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────────────────────┘
```
## Data Flow: AgentMessage → SessionTreeEntry
```
AgentState.messages::Vector{AgentMessage}
├─► For each message in messages:
│ │
│ ▼
│ ┌──────────────────────────────────────────────────────────────┐
│ │ appendMessage(session, AgentMessage) │
│ │ Input: session::Session, message::AgentMessage │
│ │ Output: entry_id::String │
│ │ │
│ │ Steps: │
│ │ 1. Create MessageEntry: │
│ │ - base: SessionTreeEntryBase(type, id, leaf_id, time) │
│ │ - message: the AgentMessage │
│ │ 2. storage.appendEntry(entry) │
│ │ - In-memory: push to entries vector, update by_id dict │
│ │ - JSONL: would append to file (TODO) │
│ │ 3. Return entry.id │
│ └──────────────────────────────────────────────────────────────┘
└─► Entry stored in JSONL (conceptual):
{"type":"message","id":"msg_1","parent_id":null,"timestamp":"...","message":{...}}
```
## Entry Types
All entry types extend `abstract type SessionTreeEntry end` and embed a
`base::SessionTreeEntryBase` struct containing `type`, `id`, `parent_id`, and `timestamp`.
```julia
abstract type SessionTreeEntry end
struct SessionTreeEntryBase
type::String
id::String
parent_id::Union{String, Nothing}
timestamp::String
end
```
### 1. MessageEntry
```julia
struct MessageEntry <: SessionTreeEntry
type::String # "message"
id::String # Unique entry ID
parent_id::Union{String, Nothing}
timestamp::String # ISO 8601 timestamp
message::AgentMessage # The actual message
base::SessionTreeEntryBase
message::AgentMessage
end
```
@@ -57,11 +98,8 @@ end
```julia
struct ThinkingLevelChangeEntry <: SessionTreeEntry
type::String # "thinking_level_change"
id::String
parent_id::Union{String, Nothing}
timestamp::String
thinking_level::String # "off", "minimal", "low", "medium", etc.
base::SessionTreeEntryBase
thinking_level::String
end
```
@@ -71,12 +109,9 @@ end
```julia
struct ModelChangeEntry <: SessionTreeEntry
type::String # "model_change"
id::String
parent_id::Union{String, Nothing}
timestamp::String
provider::String # "openai", "anthropic", etc.
model_id::String # Model identifier
base::SessionTreeEntryBase
provider::String
model_id::String
end
```
@@ -86,10 +121,7 @@ end
```julia
struct ActiveToolsChangeEntry <: SessionTreeEntry
type::String # "active_tools_change"
id::String
parent_id::Union{String, Nothing}
timestamp::String
base::SessionTreeEntryBase
active_tool_names::Vector{String}
end
```
@@ -99,18 +131,15 @@ end
### 5. CompactionEntry
```julia
struct CompactionEntry <: SessionTreeEntry
type::String # "compaction"
id::String
parent_id::Union{String, Nothing}
timestamp::String
summary::String # Summary of compacted history
struct CompactionEntry{T} <: SessionTreeEntry
base::SessionTreeEntryBase
summary::String
first_kept_entry_id::Union{String, Nothing}
tokens_before::Int64 # Context size before compaction
tokens_before::Int64
retained_tail::Union{Vector{AgentMessage}, Nothing}
details::Union{Any, Nothing}
details::Union{T, Nothing}
usage::Union{Usage, Nothing}
from_hook::Bool # Whether triggered by hook
from_hook::Bool
end
```
@@ -125,14 +154,11 @@ end
### 6. BranchSummaryEntry
```julia
struct BranchSummaryEntry <: SessionTreeEntry
type::String # "branch_summary"
id::String
parent_id::Union{String, Nothing}
timestamp::String
from_id::String # Branch point entry ID
summary::String # Summary of branch history
details::Union{Any, Nothing}
struct BranchSummaryEntry{T} <: SessionTreeEntry
base::SessionTreeEntryBase
from_id::String
summary::String
details::Union{T, Nothing}
usage::Union{Usage, Nothing}
from_hook::Bool
end
@@ -143,13 +169,10 @@ end
### 7. CustomEntry
```julia
struct CustomEntry <: SessionTreeEntry
type::String # Custom type
id::String
parent_id::Union{String, Nothing}
timestamp::String
struct CustomEntry{T} <: SessionTreeEntry
base::SessionTreeEntryBase
custom_type::String
data::Union{Any, Nothing}
data::Union{T, Nothing}
end
```
@@ -158,14 +181,11 @@ end
### 8. CustomMessageEntry
```julia
struct CustomMessageEntry <: SessionTreeEntry
type::String
id::String
parent_id::Union{String, Nothing}
timestamp::String
struct CustomMessageEntry{T} <: SessionTreeEntry
base::SessionTreeEntryBase
custom_type::String
content::String
details::Union{Any, Nothing}
details::Union{T, Nothing}
display::Bool
end
```
@@ -176,11 +196,8 @@ end
```julia
struct LabelEntry <: SessionTreeEntry
type::String
id::String
parent_id::Union{String, Nothing}
timestamp::String
target_id::String # Entry being labeled
base::SessionTreeEntryBase
target_id::String
label::Union{String, Nothing}
end
```
@@ -191,10 +208,7 @@ end
```julia
struct SessionInfoEntry <: SessionTreeEntry
type::String
id::String
parent_id::Union{String, Nothing}
timestamp::String
base::SessionTreeEntryBase
name::Union{String, Nothing}
end
```
@@ -205,10 +219,7 @@ end
```julia
struct LeafEntry <: SessionTreeEntry
type::String
id::String
parent_id::Union{String, Nothing}
timestamp::String
base::SessionTreeEntryBase
target_id::Union{String, Nothing}
end
```
@@ -221,86 +232,95 @@ end
abstract type SessionStorage{T<:SessionMetadata} end
```
### Storage Methods
### Storage Methods (actual implementation signatures)
```julia
# Metadata
getMetadata(storage::SessionStorage)::Promise{T}
getMetadata(storage::SessionStorage)::T
# Leaf management
getLeafId(storage::SessionStorage)::Promise{Union{String, Nothing}}
setLeafId(storage::SessionStorage, leaf_id::String)::Promise{Nothing}
getLeafId(storage::SessionStorage)::Union{String, Nothing}
setLeafId(storage::SessionStorage, leaf_id::Union{String, Nothing})::Nothing
# Entry management
createEntryId(storage::SessionStorage)::Promise{String}
appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Promise{Nothing}
getEntry(storage::SessionStorage, id::String)::Promise{Union{SessionTreeEntry, Nothing}}
createEntryId(storage::SessionStorage)::String
appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Nothing
getEntry(storage::SessionStorage, id::String)::Union{SessionTreeEntry, Nothing}
# Query
findEntries(storage::SessionStorage, type::String)::Promise{Vector{SessionTreeEntry}}
getLabel(storage::SessionStorage, id::String)::Promise{Union{String, Nothing}}
getSessionName(storage::SessionStorage)::Promise{Union{String, Nothing}}
findEntries(storage::SessionStorage, type::String)::Vector{SessionTreeEntry}
getLabel(storage::SessionStorage, id::String)::Union{String, Nothing}
getSessionName(storage::SessionStorage)::Union{String, Nothing}
# Branch navigation
getPathToRootOrCompaction(
storage::SessionStorage,
leaf_id::String,
)::Promise{Vector{SessionTreeEntry}}
getEntries(storage::SessionStorage, options::Dict{String, Any})::Promise{Vector{SessionTreeEntry}}
getPathToRootOrCompaction(storage::SessionStorage, leaf_id::Union{String, Nothing})::Vector{SessionTreeEntry}
getEntries(storage::SessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry}
# Stats
getSessionStats(storage::SessionStorage)::Promise{SessionStats}
getSessionStats(storage::SessionStorage)::SessionStats
```
## JsonlSessionStorage
```
mutable struct JsonlSessionStorage{T<:SessionMetadata} <: SessionStorage{T}
file_path::String
metadata::T
entries::Vector{SessionTreeEntry} # ordered list
by_id::Dict{String, SessionTreeEntry} # fast lookup by id
labels_by_id::Dict{String, String} # label cache
current_leaf_id::Union{String, Nothing} # current branch tip
end
```
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ JSONL Storage Format │
└─────────────────────────────────────────────────────────────────────────────┘
File: session.jsonl
File: session.jsonl (conceptual - not yet implemented)
Entry 1 (Metadata):
{"type":"session","id":"meta_1","created_at":"2024-01-01T00:00:00Z","cwd":"/path","path":"/path/session.jsonl"}
Entry 1 (Metadata via SessionHeader):
{"type":"session","version":3,"id":"meta_1","timestamp":"...","cwd":"/path","parent_session":null,"metadata":{}}
Entry 2 (Message):
{"type":"message","id":"msg_1","parent_id":null,"timestamp":"2024-01-01T00:00:01Z","message":{"role":"user","content":[{"type":"text","text":"Hello"}]}}
{"type":"message","id":"msg_1","parent_id":null,"timestamp":"...","message":{"role":"user",...}}
Entry 3 (Thinking Level):
{"type":"thinking_level_change","id":"tl_1","parent_id":"msg_1","timestamp":"2024-01-01T00:00:02Z","thinking_level":"medium"}
{"type":"thinking_level_change","id":"tl_1","parent_id":"msg_1","timestamp":"...","thinking_level":"medium"}
Entry 4 (Model Change):
{"type":"model_change","id":"mc_1","parent_id":"tl_1","timestamp":"2024-01-01T00:00:03Z","provider":"openai","model_id":"gpt-4"}
{"type":"model_change","id":"mc_1","parent_id":"tl_1","timestamp":"...","provider":"openai","model_id":"gpt-4"}
Entry 5 (Compaction):
{"type":"compaction","id":"comp_1","parent_id":"mc_1","timestamp":"2024-01-01T00:00:04Z","summary":"Previous messages summarized...","first_kept_entry_id":"msg_3","tokens_before":100000,"tokens_after":50000}
{"type":"compaction","id":"comp_1","parent_id":"mc_1","timestamp":"...","summary":"...","first_kept_entry_id":"msg_3","tokens_before":100000}
Entry 6 (Branch Summary):
{"type":"branch_summary","id":"branch_1","parent_id":"comp_1","timestamp":"2024-01-01T00:00:05Z","from_id":"msg_3","summary":"Branch from message 3"}
{"type":"branch_summary","id":"branch_1","parent_id":"comp_1","timestamp":"...","from_id":"msg_3","summary":"..."}
Entry 7 (Active Tools):
{"type":"active_tools_change","id":"tools_1","parent_id":"branch_1","timestamp":"2024-01-01T00:00:06Z","active_tool_names":["bash","read"]}
{"type":"active_tools_change","id":"tools_1","parent_id":"branch_1","timestamp":"...","active_tool_names":["bash","read"]}
Entry 8 (Leaf):
{"type":"leaf","id":"leaf_1","parent_id":"tools_1","timestamp":"2024-01-01T00:00:07Z","target_id":"msg_5"}
{"type":"leaf","id":"leaf_1","parent_id":"tools_1","timestamp":"...","target_id":"msg_5"}
Notes:
- Each line is a JSON object (JSONL format)
- Each line is a JSON object (JSONL format) - TODO: file I/O not yet implemented
- parent_id references previous entry (linked list structure)
- Leaf entry points to current position in tree
- To fork, create new branch from any entry
- In-memory mode uses Vector + Dict by_id for fast access
```
## InMemorySessionStorage
```julia
mutable struct InMemorySessionStorage
metadata::SessionMetadata
mutable struct InMemorySessionStorage{T<:SessionMetadata} <: SessionStorage{T}
metadata::T
entries::Vector{SessionTreeEntry}
by_id::Dict{String, SessionTreeEntry}
labels_by_id::Dict{String, String}
leaf_id::Union{String, Nothing}
entries::Dict{String, SessionTreeEntry}
labels::Dict{String, String}
end
```
@@ -317,6 +337,19 @@ end
mutable struct Session{T<:SessionMetadata}
storage::SessionStorage{T}
context_build_options::SessionContextBuildOptions
function Session(storage::SessionStorage, context_build_options=SessionContextBuildOptions(nothing, nothing))
new{typeof(storage.metadata)}(storage, context_build_options)
end
end
```
### SessionContextBuildOptions
```julia
mutable struct SessionContextBuildOptions
entry_transforms::Union{Vector{Function}, Nothing}
entry_projectors::Union{Dict{String, Function}, Nothing}
end
```
@@ -326,14 +359,10 @@ end
```julia
function appendMessage(session::Session, message::AgentMessage)::String
entry = MessageEntry(
"message",
createEntryId(session.storage),
getLeafId(session.storage),
create_timestamp(),
return appendTypedEntry(session, MessageEntry(
SessionTreeEntryBase("message", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
message,
)
return appendTypedEntry(session, entry)
))
end
```
@@ -354,18 +383,34 @@ tool_id = appendMessage(session, ToolResultMessage(...))
#### appendThinkingLevelChange()
```julia
function appendThinkingLevelChange(
session::Session,
thinking_level::String,
)::String
entry = ThinkingLevelChangeEntry(
"thinking_level_change",
createEntryId(session.storage),
getLeafId(session.storage),
create_timestamp(),
function appendThinkingLevelChange(session::Session, thinking_level::String)::String
return appendTypedEntry(session, ThinkingLevelChangeEntry(
SessionTreeEntryBase("thinking_level_change", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
thinking_level,
)
return appendTypedEntry(session, entry)
))
end
```
#### appendModelChange()
```julia
function appendModelChange(session::Session, provider::String, model_id::String)::String
return appendTypedEntry(session, ModelChangeEntry(
SessionTreeEntryBase("model_change", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
provider,
model_id,
))
end
```
#### appendActiveToolsChange()
```julia
function appendActiveToolsChange(session::Session, active_tool_names::Vector{String})::String
return appendTypedEntry(session, ActiveToolsChangeEntry(
SessionTreeEntryBase("active_tools_change", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
active_tool_names,
))
end
```
@@ -382,11 +427,8 @@ function appendCompaction(
usage::Union{Usage, Nothing}=nothing,
retained_tail::Union{Vector{AgentMessage}, Nothing}=nothing,
)::String
entry = CompactionEntry(
"compaction",
createEntryId(session.storage),
getLeafId(session.storage),
create_timestamp(),
return appendTypedEntry(session, CompactionEntry(
SessionTreeEntryBase("compaction", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
summary,
first_kept_entry_id,
tokens_before,
@@ -394,8 +436,7 @@ function appendCompaction(
details,
usage,
from_hook,
)
return appendTypedEntry(session, entry)
))
end
```
@@ -407,25 +448,24 @@ function moveTo(
entry_id::Union{String, Nothing},
summary::Union{Dict{String, Any}, Nothing}=nothing,
)::Union{String, Nothing}
# Set new leaf
setLeafId(session.storage, entry_id)
# Optionally create branch summary
if !isnothing(summary)
return appendTypedEntry(session, BranchSummaryEntry(
"branch_summary",
createEntryId(session.storage),
entry_id,
create_timestamp(),
entry_id,
summary["summary"],
get(summary, "details", nothing),
get(summary, "usage", nothing),
get(summary, "from_hook", false),
))
# Validate entry exists
if !isnothing(entry_id) && isnothing(getEntry(session, entry_id))
throw(SessionError("not_found", "Entry $(entry_id) not found"))
end
return nothing
# Set new leaf (creates a LeafEntry)
setLeafId(session.storage, entry_id)
# Optionally create branch summary
if isnothing(summary)
return nothing
end
return appendTypedEntry(session, BranchSummaryEntry(
SessionTreeEntryBase("branch_summary", createEntryId(session.storage), entry_id, create_timestamp()),
entry_id,
summary["summary"],
get(summary, "details", nothing),
get(summary, "usage", nothing),
get(summary, "from_hook", false),
))
end
```
@@ -444,12 +484,18 @@ session.moveTo(
)
```
**How it works**:
1. Validates the target entry exists
2. Calls `setLeafId()` which creates a `LeafEntry` with `target_id = entry_id`
3. If `summary` is provided, creates a `BranchSummaryEntry` as a child of the target entry
4. The new leaf now points to `entry_id`, making it the root of a new branch
## Build Session Context
```julia
function buildSessionContext(
path_entries::Vector{SessionTreeEntry},
options::SessionContextBuildOptions=SessionContextBuildOptions(),
options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
)::SessionContext
state = deriveSessionContextState(path_entries)
context_entries = buildContextEntries(path_entries, options)
@@ -459,14 +505,36 @@ function buildSessionContext(
end
return SessionContext(messages, state.thinking_level, state.model, state.active_tool_names)
end
function deriveSessionContextState(path_entries::Vector{SessionTreeEntry})::Dict{String, Any}
thinking_level = "off"
model = nothing
active_tool_names = nothing
for entry in path_entries
if entry isa ThinkingLevelChangeEntry
thinking_level = entry.thinking_level
elseif entry isa ModelChangeEntry
model = Dict("provider" => entry.provider, "modelId" => entry.model_id)
elseif entry isa MessageEntry && entry.message.role == "assistant"
model = Dict("provider" => entry.message.provider, "modelId" => entry.message.model)
elseif entry isa ActiveToolsChangeEntry
active_tool_names = copy(entry.active_tool_names)
end
end
return Dict(
"thinking_level" => thinking_level,
"model" => model,
"active_tool_names" => active_tool_names,
)
end
```
### Context Entry Transform
```julia
function defaultContextEntryTransform(
path_entries::Vector{SessionTreeEntry},
)::Vector{SessionTreeEntry}
function defaultContextEntryTransform(path_entries::Vector{SessionTreeEntry})::Vector{SessionTreeEntry}
compaction = nothing
for entry in path_entries
if entry isa CompactionEntry
@@ -474,25 +542,26 @@ function defaultContextEntryTransform(
break
end
end
if isnothing(compaction)
return copy(path_entries)
end
# Include compaction entry
entries = [compaction]
# Include retained tail if present
entries::Vector{SessionTreeEntry} = [compaction]
compaction_idx = findfirst(
(entry) -> entry isa CompactionEntry && entry.id == compaction.id,
path_entries,
)
if !isnothing(compaction.retained_tail)
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
append!(entries, path_entries[compaction_idx+1:end])
for i in compaction_idx+1:length(path_entries)
push!(entries, path_entries[i])
end
return entries
end
# Otherwise include entries after first_kept_entry_id
if !isnothing(compaction.first_kept_entry_id)
found_first_kept = false
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
for i in 1:compaction_idx-1
entry = path_entries[i]
if entry.id == compaction.first_kept_entry_id
@@ -503,11 +572,26 @@ function defaultContextEntryTransform(
end
end
end
# Include entries after compaction
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
append!(entries, path_entries[compaction_idx+1:end])
for i in compaction_idx+1:length(path_entries)
push!(entries, path_entries[i])
end
return entries
end
function buildContextEntries(
path_entries::Vector{SessionTreeEntry},
options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
)::Vector{SessionTreeEntry}
entries = defaultContextEntryTransform(path_entries)
if !isnothing(options.entry_transforms)
for transform in options.entry_transforms
entries = transform(entries)
end
end
return entries
end
```
@@ -519,12 +603,12 @@ function sessionEntryToContextMessages(
entry::SessionTreeEntry,
index::Int64,
entries::Vector{SessionTreeEntry},
options::SessionContextBuildOptions=SessionContextBuildOptions(),
options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
)::Vector{AgentMessage}
if entry isa MessageEntry
return [entry.message]
end
if entry isa CustomMessageEntry
return [createCustomMessage(
entry.custom_type,
@@ -534,7 +618,7 @@ function sessionEntryToContextMessages(
entry.timestamp,
)]
end
if entry isa CompactionEntry
messages = [createCompactionSummaryMessage(
entry.summary,
@@ -546,7 +630,7 @@ function sessionEntryToContextMessages(
end
return messages
end
if entry isa BranchSummaryEntry
return [createBranchSummaryMessage(
entry.summary,
@@ -554,16 +638,15 @@ function sessionEntryToContextMessages(
entry.timestamp,
)]
end
if entry isa CustomEntry
# Custom projectors can transform custom entries
if !isnothing(options.entry_projectors) && haskey(options.entry_projectors, entry.custom_type)
projector = options.entry_projectors[entry.custom_type]
return projector(entry, index, entries)
end
return AgentMessage[]
end
return AgentMessage[]
end
```
@@ -610,6 +693,16 @@ Key Points:
- Each branch has independent tail
```
### getPathToRootOrCompaction
Walks from a leaf back to the root, handling compaction entries:
```julia
# When encountering a CompactionEntry:
# - If retained_tail is set: stop (compaction covers the tail)
# - Otherwise: skip to first_kept_entry_id and continue walking
```
## Compaction Strategy
### Why Compaction?
@@ -641,7 +734,7 @@ LLM context windows have limits:
# 4. Update storage
# - Append CompactionEntry
# - Update leaf to CompactionEntry
# - Leaf automatically points to CompactionEntry (leafIdAfterEntry)
```
### Compaction Example
@@ -697,8 +790,10 @@ using AgentCore
# 1. Create storage
storage = JsonlSessionStorage(
SessionMetadata("session_1", "2024-01-01T00:00:00Z"),
"/path/to/session.jsonl",
SessionHeader("session", 3, "session_1", created_at, "/path", nothing, nothing),
SessionTreeEntry[],
nothing,
)
# 2. Create session
@@ -718,7 +813,7 @@ mc_id = appendModelChange(session, "openai", "gpt-4")
msg3_id = appendMessage(session, UserMessage("user", [TextContent("What can you do?")], timestamp))
msg4_id = appendMessage(session, AssistantMessage("assistant", [TextContent("I can...")], ...))
# 7. Compact context (100K tokens 20K)
# 7. Compact context (100K tokens -> 20K)
compact_id = appendCompaction(
session,
"User asked about capabilities and assistant explained",
@@ -733,31 +828,91 @@ compact_id = appendCompaction(
# 8. Fork and branch
session.moveTo(msg2_id) # Go back to msg2
# 9. Create new branch
branch_id = appendBranchSummary(
# 9. Continue on new branch (moveTo creates branch summary when summary is provided)
branch_id = moveTo(
session,
"User changed direction to focus on file operations",
msg2_id,
Dict("focus" => "files"),
Dict("summary" => "User changed direction", "details" => Dict("focus" => "files")),
)
# 10. Continue on new branch
msg5_id = appendMessage(session, UserMessage("user", [TextContent("Let's work with files")], timestamp))
# 11. Query session context
context = buildSessionContext(session)
context = buildContext(session)
# 12. Get stats
stats = getSessionStats(session)
println("Messages: $(stats.message_count)")
println("Total tokens: $(stats.total_tokens)")
println("Cost: $$(stats.cost_total)")
println("Cost: \$(stats.cost_total)")
```
## Session Repo Interface
### Session Repository Methods
```julia
# Create a new session
create(repo::SessionRepo, options::TCreateOptions)::Session
# Open an existing session
open(repo::SessionRepo, metadata::TMetadata)::Session
# List sessions
list(repo::SessionRepo, options::TListOptions)::Vector{TMetadata}
# Delete a session
delete(repo::SessionRepo, metadata::TMetadata)::Nothing
# Fork a session (copy branch from entry)
fork(repo::SessionRepo, source::TMetadata, options::Dict{String, Any})::Session
```
### JSONL vs In-Memory Repos
| Feature | JsonlSessionRepo | InMemorySessionRepo |
|---------|------------------|---------------------|
| Persistence | File-based (TODO) | In-memory only |
| Use case | Production | Testing |
| Fork | Not implemented | Uses getEntriesToFork |
| Metadata | JsonlSessionMetadata | SessionMetadata |
### Fork Behavior (`getEntriesToFork`)
```julia
function getEntriesToFork(storage, options)::Vector{SessionTreeEntry}
# If no entryId specified, fork from current leaf (full copy)
if !haskey(options, :entryId) || isnothing(options[:entryId])
return getEntries(storage, Dict{String, Any}())
end
target = getEntry(storage, options[:entryId])
position = get(options, "position", "before")
if position == "at"
# Fork includes the target entry
effective_leaf_id = target.id
else
# Fork before the target (parent)
# Target must be a user message
if target isa MessageEntry && target.message.role != "user"
throw(SessionError("invalid_fork_target", "Not a user message"))
end
effective_leaf_id = target.parent_id
end
return getPathToRootOrCompaction(storage, effective_leaf_id)
end
```
## Best Practices
1. **Use compaction** for long conversations to stay within context limits
2. **Create branch summaries** when forking to document divergent paths
3. **Retain tail messages** after compaction for context
2. **Create branch summaries** when forking to document divergent paths (via `moveTo()` with summary)
3. **Retain tail messages** after compaction for context (`retained_tail` field)
4. **Track token usage** to optimize compaction timing
5. **Use InMemorySessionStorage** for testing
6. **Use `getBranch(session)`** to get the current path from leaf to root/compaction
7. **Use `buildContext(session)`** as the convenient Session method for building context
8. **Use `mergeContextBuildOptions(session, options)`** to combine session-level and call-level transforms/projectors
+193 -620
View File
@@ -1,434 +1,187 @@
# AgentCore.jl - Tools Deep Dive
## Tool Architecture
## Tool Types (from types.jl)
### AgentTool (struct)
```julia
struct AgentTool{TParameters, TDetails}
name::String # tool identifier
label::String # display name
description::String # what it does
parameters::TParameters # JSON schema or type
execute::Function # (tool_call_id, params, signal, on_update, context) -> AgentToolResult
prepare_arguments::Union{Function, Nothing}
execution_mode::Union{ToolExecutionMode, Nothing}
end
```
### AgentToolResult (struct)
```julia
struct AgentToolResult{T}
content::Vector{MessageContent}
details::T
usage::Union{Usage, Nothing}
added_tool_names::Union{Vector{String}, Nothing}
terminate::Union{Bool, Nothing}
end
```
### ToolCall (struct)
```julia
struct ToolCall
type::String # always "tool"
id::String # unique identifier
name::String # tool name to execute
arguments::Dict{String, Any} # JSON-like arguments
partial_json::Union{String, Nothing}
end
```
### ToolExecutionMode (enum)
```julia
@enum ToolExecutionMode begin
EXECUTION_SEQUENTIAL = "sequential"
EXECUTION_PARALLEL = "parallel"
end
```
## Tool Execution Flow
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Tool Layer │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ AgentTool │
│ - name: String (identifier) │
│ - label: String (display name) │
│ - description: String (what it does) │
│ - parameters: JSON schema │
│ - execute::Function (main logic) │
│ - prepare_arguments::Union{Function, Nothing} │
│ - execution_mode::Union{ToolExecutionMode, Nothing} │
└─────────────────────────────────────────────────────────────────────────────┘
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ BashTool │ │ ReadTool │ │ WriteTool │
│ - bash() │ │ - read() │ │ - write() │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐
│ EditTool │
│ - edit() │
└─────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Tool Execution Flow │
└─────────────────────────────────────────────────────────────────────────────┘
Assistant Message
┌────────────────────────────────────────────────────────┐
│ AssistantMessage: │
│ content: [ │
│ TextContent("I'll check the files..."), │
│ ToolCall("bash", {command: "ls -la"}), │
│ ToolCall("read", {path: "README.md"}) │
│ ] │
└────────────────────────────────────────────────────────┘
AssistantMessage (from LLM)
content::Vector{MessageContent}
└─ Contains: TextContent[] and ToolCall[]
┌────────────────────────────────────────────────────────┐
│ AgentLoop.executeToolCalls() │
│ - Extract ToolCalls from message content │
│ - Determine execution mode (sequential/parallel) │
└────────────────────────────────────────────────────────┘
├─► executeToolCallsSequential()
│ (for tools that require order)
└─► executeToolCallsParallel()
(for independent tools)
├─► prepareToolCall()
│ - before_tool_call hook (optional)
│ - validate arguments
│ - prepare arguments (optional)
├─► execute()
│ - Tool-specific logic
│ - Return AgentToolResult
├─► finalizeExecutedToolCall()
│ - after_tool_call hook (optional)
└─► createToolResultMessage()
- Emit ToolResultMessage
Agent.execute() (in agent.jl)
└─ before_tool_call hook (Agent.before_tool_call, optional)
Input: BeforeToolCallContext
Output: BeforeToolCallResult (block, reason)
┌────────────────────────────────────────────────────────┐
│ ToolResultMessage │
│ - tool_call_id: "ref to original ToolCall" │
│ - tool_name: "bash" │
│ - content: [TextContent("file1.md\nfile2.md\n")] │
│ - is_error: false │
└────────────────────────────────────────────────────────┘
For each ToolCall:
tool = find_tool(name)
tool.execute(tool_call_id, args, signal, on_update, context)
┌────────────────────────────────────────────────────────┐
│ AgentState.messages.append(tool_result) │
│ - Next turn: LLM sees tool results │
└────────────────────────────────────────────────────────┘
AgentToolResult{T}(content, details, usage, added_tool_names, terminate)
└─ after_tool_call hook (Agent.after_tool_call, optional)
Input: AfterToolCallContext
Output: AfterToolCallResult (patches: content, details, is_error, usage, terminate)
ToolResultMessage (one per ToolCall)
role: "toolResult"
tool_call_id::String
tool_name::String
content::Vector{MessageContent}
details::Any
usage::Union{Usage, Nothing}
added_tool_names::Union{Vector{String}, Nothing}
is_error::Bool
timestamp::Timestamp
Append to AgentState.messages
└─ Next turn: LLM sees tool results as input
```
## Built-in Tools
### 1. BashTool
### 1. BashTool (`tools/bash.jl`)
```julia
struct BashToolOptions{TContext}
command_prefix::Union{String, Nothing}
prepare::Union{BashPrepare{TContext}, Nothing}
struct BashExecution
command::String
cwd::String
env::Dict{String, String}
inherit_env::Bool
end
struct BashPrepare{TContext}
mutable struct BashPrepare{TContext}
function::Function
context::TContext
signal::Union{Any, Nothing}
end
struct BashToolDetails
mutable struct BashToolOptions{TContext}
command_prefix::Union{String, Nothing}
prepare::Union{BashPrepare{TContext}, Nothing}
end
mutable struct BashToolDetails
truncation::Union{Any, Nothing}
full_output_path::Union{String, Nothing}
end
function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing) where TContext
```
#### createBashTool()
**Execute signature**: `(tool_call_id, params, signal, on_update, context) -> AgentToolResult`
**Note**: The actual bash execution is a TODO stub in the current source.
### 2. ReadTool (`tools/read.jl`)
```julia
function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing)
return AgentTool(
"bash",
"bash",
"Execute a bash command in the current working directory.",
Dict{String, Any}(),
(tool_call_id, params, signal, on_update, context) -> begin
# Execute command
result = executeBashCommand(params, signal, on_update)
# Return result
return AgentToolResult(
[TextContent(result.output)],
BashToolDetails(result.truncation, result.full_path),
nothing,
nothing,
result.terminate,
)
end,
nothing, # prepare_arguments
nothing, # execution_mode (default: use config)
)
mutable struct ReadToolDetails
truncation::Union{Any, Nothing}
end
mutable struct ReadToolOptions
auto_resize_images::Bool
image_processor::Union{Any, Nothing}
end
function createReadTool{TContext}(options::Union{ReadToolOptions, Nothing}=nothing) where TContext
```
**Execute signature**: `(tool_call_id, params, signal, on_update, context) -> AgentToolResult`
### 3. WriteTool (`tools/write.jl`)
```julia
function createWriteTool{TContext}() where TContext
```
**Execute signature**: `(tool_call_id, params, signal, on_update, context) -> AgentToolResult`
### 4. EditTool (`tools/edit.jl`)
```julia
mutable struct EditToolDetails
diff::String
patch::String
first_changed_line::Union{Int64, Nothing}
end
function createEditTool{TContext}() where TContext
```
**Execute signature**: `(tool_call_id, params, signal, on_update, context) -> AgentToolResult`
## Tool Hooks (on Agent struct)
The `Agent` struct in `agent.jl` has these hook fields:
```julia
mutable struct Agent
...
before_tool_call::Union{Function, Nothing}
after_tool_call::Union{Function, Nothing}
prepare_next_turn::Union{Function, Nothing}
prepare_next_turn_with_context::Union{Function, Nothing}
...
end
```
**Parameters Schema**:
```json
{
"command": "string",
"timeout": "number (optional)",
"cwd": "string (optional)",
"env": "object (optional)"
}
```
Configured via `Agent(Dict(...))` options:
- `:beforeToolCall``Agent.before_tool_call`
- `:afterToolCall``Agent.after_tool_call`
- `:prepareNextTurn``Agent.prepare_next_turn`
- `:prepareNextTurnWithContext``Agent.prepare_next_turn_with_context`
**Example**:
```julia
# Create tool
bash_tool = createBashTool()
# Agent receives command
tool_call = ToolCall("tool", "tc1", "bash", Dict(
"command" => "ls -la",
"timeout" => 30
), nothing)
# Execute
result = bash_tool.execute(
"tc1",
Dict("command" => "ls -la", "timeout" => 30),
nothing,
on_update, # Callback for streaming output
nothing,
)
# Result
AgentToolResult(
[TextContent("total 12\n-rw-r--r-- 1 user user 100 Jan 1 file1.md\n-rw-r--r-- 1 user user 200 Jan 2 file2.md\n")],
BashToolDetails(truncation_info, nothing),
nothing,
nothing,
nothing,
)
```
### 2. ReadTool
```julia
struct ReadToolOptions{TContext}
max_size::Union{Int64, Nothing}
max_lines::Union{Int64, Nothing}
image_processor::Union{ReadImageProcessor, Nothing}
prepare::Union{ReadPrepare{TContext}, Nothing}
end
struct ReadImageProcessor
function::Function
context::Any
end
struct ReadImageProcessorResult
content::Vector{MessageContent}
usage::Union{Usage, Nothing}
end
```
#### createReadTool()
```julia
function createReadTool{TContext}(options::Union{ReadToolOptions{TContext}, Nothing}=nothing)
return AgentTool(
"read",
"read",
"Read a file from the file system.",
Dict{String, Any}(),
(tool_call_id, params, signal, on_update, context) -> begin
# Read file
result = readFileSystem(params, signal, options)
# Process content
content = if isImage(params.path)
# Image processing
image_result = options.image_processor.function(result.path, context)
image_result.content
else
# Text content
[TextContent(result.content)]
end
return AgentToolResult(
content,
ReadToolDetails(result.size, result.truncated, result.full_path),
nothing,
nothing,
nothing,
)
end,
nothing,
nothing,
)
end
```
**Parameters Schema**:
```json
{
"path": "string"
}
```
**Example**:
```julia
# Create tool
read_tool = createReadTool()
# Agent requests to read file
tool_call = ToolCall("tool", "tc2", "read", Dict(
"path" => "src/main.jl"
), nothing)
# Execute
result = read_tool.execute("tc2", Dict("path" => "src/main.jl"), nothing, nothing, nothing)
# Result
AgentToolResult(
[TextContent("module Main\nfunction main()\n println(\"Hello\")\nend\nend\n")],
ReadToolDetails(1234, false, "/path/to/src/main.jl"),
nothing,
nothing,
nothing,
)
```
### 3. WriteTool
```julia
struct WriteToolInput
path::String
content::String
end
```
#### createWriteTool()
```julia
function createWriteTool{TContext}(options::Union{WriteToolOptions{TContext}, Nothing}=nothing)
return AgentTool(
"write",
"write",
"Write content to a file.",
Dict{String, Any}(),
(tool_call_id, params, signal, on_update, context) -> begin
# Write file
result = writeToFile(params, signal)
return AgentToolResult(
[TextContent(result.message)],
nothing,
nothing,
nothing,
nothing,
)
end,
nothing,
nothing,
)
end
```
**Parameters Schema**:
```json
{
"path": "string",
"content": "string"
}
```
**Example**:
```julia
# Create tool
write_tool = createWriteTool()
# Agent wants to write file
tool_call = ToolCall("tool", "tc3", "write", Dict(
"path" => "output.txt",
"content" => "Hello World"
), nothing)
# Execute
result = write_tool.execute("tc3", Dict(
"path" => "output.txt",
"content" => "Hello World"
), nothing, nothing, nothing)
# Result
AgentToolResult(
[TextContent("File written: output.txt (11 bytes)")],
nothing,
nothing,
nothing,
nothing,
)
```
### 4. EditTool
```julia
struct EditToolInput
path::String
find::String
replacement::String
end
struct EditToolDetails
edits::Vector{Edit}
before_content::String
after_content::String
end
```
#### createEditTool()
```julia
function createEditTool{TContext}(options::Union{EditToolOptions{TContext}, Nothing}=nothing)
return AgentTool(
"edit",
"edit",
"Edit a file by finding and replacing text.",
Dict{String, Any}(),
(tool_call_id, params, signal, on_update, context) -> begin
# Read file
before_content = read(params.path)
# Apply edit
after_content = replace(before_content, params.find => params.replacement)
# Write file
write(params.path, after_content)
return AgentToolResult(
[TextContent("Edit applied successfully")],
EditToolDetails([Edit(params.find, params.replacement)], before_content, after_content),
nothing,
nothing,
nothing,
)
end,
nothing,
nothing,
)
end
```
**Parameters Schema**:
```json
{
"path": "string",
"find": "string",
"replacement": "string"
}
```
**Example**:
```julia
# Create tool
edit_tool = createEditTool()
# Agent wants to replace text
tool_call = ToolCall("tool", "tc4", "edit", Dict(
"path" => "README.md",
"find" => "v1.0.0",
"replacement" => "v2.0.0"
), nothing)
# Execute
result = edit_tool.execute("tc4", Dict(
"path" => "README.md",
"find" => "v1.0.0",
"replacement" => "v2.0.0"
), nothing, nothing, nothing)
# Result
AgentToolResult(
[TextContent("Edit applied: README.md")],
EditToolDetails([Edit("v1.0.0", "v2.0.0")], "Version 1.0.0", "Version 2.0.0"),
nothing,
nothing,
nothing,
)
```
## Tool Execution Hooks
### before_tool_call
### BeforeToolCallContext / BeforeToolCallResult (from types.jl)
```julia
struct BeforeToolCallContext
@@ -444,32 +197,7 @@ struct BeforeToolCallResult
end
```
**Usage**:
```julia
function myBeforeToolCall(context, signal)
tool_name = context.tool_call.name
# Block dangerous commands
if tool_name == "bash" && contains(context.args["command"], "rm -rf /")
return BeforeToolCallResult(
true,
"Blocking dangerous command: rm -rf /"
)
end
# Log tool execution
println("Executing tool: $tool_name")
return nothing # Allow execution
end
# Configure agent
agent = Agent(Dict(
:beforeToolCall => myBeforeToolCall,
))
```
### after_tool_call
### AfterToolCallContext / AfterToolCallResult (from types.jl)
```julia
struct AfterToolCallContext
@@ -490,37 +218,7 @@ struct AfterToolCallResult
end
```
**Usage**:
```julia
function myAfterToolCall(context, signal)
tool_name = context.tool_call.name
# Modify bash output
if tool_name == "bash"
# Add timestamp to output
new_content = [
TextContent("[Executed at $(Dates.now())]\n"),
context.result.content[1],
]
return AfterToolCallResult(
content = new_content,
details = context.result.details,
is_error = context.is_error,
usage = context.result.usage,
terminate = context.result.terminate,
)
end
return nothing # Use original result
end
# Configure agent
agent = Agent(Dict(
:afterToolCall => myAfterToolCall,
))
```
### prepare_next_turn
### PrepareNextTurnContext / AgentLoopTurnUpdate (from types.jl)
```julia
struct PrepareNextTurnContext
@@ -537,193 +235,73 @@ struct AgentLoopTurnUpdate
end
```
**Usage**:
```julia
function myPrepareNextTurn(context, signal)
# Check if we should use a different model
last_message = context.message
tool_results = context.tool_results
# If tool execution had errors, use more capable model
has_errors = any(r -> r.is_error, tool_results)
if has_errors
return AgentLoopTurnUpdate(
context = context.context,
model = Model("gpt-4", "GPT-4", "openai", "openai", "", ...),
thinking_level = THINKING_HIGH,
)
end
return nothing # Keep current settings
end
# Configure agent
agent = Agent(Dict(
:prepareNextTurn => myPrepareNextTurn,
))
```
## Tool Execution Modes
### Sequential Execution
```julia
# Tools run one at a time, in order
# Use case: Tools that modify shared state
# Configure tool
bash_tool = AgentTool(
"bash",
"bash",
"Execute bash command",
...,
execute,
nothing,
EXECUTION_SEQUENTIAL, # Force sequential
)
# Or configure globally
# Configure on Agent
agent = Agent(Dict(
:toolExecution => EXECUTION_SEQUENTIAL,
))
```
**Example Scenario**:
```julia
# Sequential execution (correct order)
1. Tool 1: create_directory("build/")
└─ Creates build/ directory
2. Tool 2: write("build/app.js", "...")
└─ Writes file to build/
(If parallel: might fail because build/ doesn't exist yet)
```
### Parallel Execution
### Parallel Execution (default)
```julia
# Tools run concurrently
# Use case: Independent operations
# Default behavior
agent = Agent(Dict(
:toolExecution => EXECUTION_PARALLEL, # Default
:toolExecution => EXECUTION_PARALLEL,
))
```
**Example Scenario**:
```julia
# Parallel execution (independent operations)
1. Tool 1: read("README.md") ─────┐
2. Tool 2: read("CHANGELOG.md") ─┼─► Run simultaneously
3. Tool 3: read("LICENSE") ──────┘
(Parallel: All three read operations can happen at once)
(Sequential: Would wait for each read to complete)
```
## Custom Tools
### Example: Database Tool
Tools can also specify their own mode:
```julia
function createDatabaseTool()
return AgentTool(
"database",
"database",
"Execute SQL queries against the database.",
Dict{String, Any}(
"type" => "object",
"properties" => Dict(
"query" => Dict("type" => "string"),
"params" => Dict("type" => "array", "items" => Dict("type" => "string")),
),
"required" => ["query"],
),
(tool_call_id, params, signal, on_update, context) -> begin
# Execute query
query = params["query"]
result = executeQuery(query)
# Format output
output = formatQueryResult(result)
return AgentToolResult(
[TextContent(output)],
Dict("rows_affected" => result.rows_affected),
nothing,
nothing,
nothing,
)
end,
nothing,
EXECUTION_SEQUENTIAL,
)
end
# Usage
db_tool = createDatabaseTool()
agent = Agent(Dict(:tools => [db_tool]))
agent_tool = AgentTool(
"name",
"label",
"description",
params_schema,
execute_fn,
nothing,
EXECUTION_SEQUENTIAL, # or EXECUTION_PARALLEL
)
```
### Example: HTTP Request Tool
## Tool Exports (from tools/index.jl)
```julia
function createHTTPTool()
return AgentTool(
"http",
"http",
"Make HTTP requests.",
Dict{String, Any}(
"type" => "object",
"properties" => Dict(
"url" => Dict("type" => "string"),
"method" => Dict("type" => "string", "enum" => ["GET", "POST", "PUT", "DELETE"]),
"body" => Dict("type" => "string"),
"headers" => Dict("type" => "object"),
),
"required" => ["url", "method"],
),
(tool_call_id, params, signal, on_update, context) -> begin
# Make request
url = params["url"]
method = params["method"]
body = get(params, "body", nothing)
headers = get(params, "headers", Dict())
response = makeHTTPRequest(method, url, body, headers)
return AgentToolResult(
[TextContent(response.body)],
Dict(
"status_code" => response.status_code,
"headers" => response.headers,
),
nothing,
nothing,
nothing,
)
end,
nothing,
EXECUTION_PARALLEL,
)
end
export
createBashTool,
createReadTool,
createWriteTool,
createEditTool,
BashExecution,
BashPrepare,
BashToolDetails,
BashToolInput,
BashToolOptions,
EditToolDetails,
EditToolInput,
ReadToolDetails,
ReadToolInput,
ReadToolOptions,
ReadImageProcessor,
ReadImageProcessorResult,
WriteToolInput
```
## Complete Example
## Example: Creating and Using Tools
```julia
using AgentCore
# 1. Create tools
# Create tools
bash_tool = createBashTool()
read_tool = createReadTool()
write_tool = createWriteTool()
# 2. Configure hooks
# Configure hooks
before_hook = (context, signal) -> begin
println("About to execute: $(context.tool_call.name)")
return nothing
@@ -738,22 +316,17 @@ after_hook = (context, signal) -> begin
return nothing
end
# 3. Create agent
# Create agent with tools and hooks
agent = Agent(Dict(
:systemPrompt => "You are a helpful assistant with file system access.",
:tools => [bash_tool, read_tool, write_tool],
:beforeToolCall => before_hook,
:afterToolCall => after_hook,
:toolExecution => EXECUTION_PARALLEL,
))
# 4. Run conversation
# Run prompt
prompt(agent, "List files in current directory and read the first one")
# 5. Agent will:
# - Execute bash("ls -la") tool
# - Parse output to find first file
# - Execute read("path/to/file") tool
# - Return content to user
```
## Best Practices
File diff suppressed because it is too large Load Diff
+344 -513
View File
File diff suppressed because it is too large Load Diff
+212 -24
View File
@@ -37,7 +37,7 @@ agent = Agent(Dict(
prompt(agent, "Hello!")
# Wait for completion
wait_for_idle(agent)
waitForIdle(agent)
```
### Understanding the Flow
@@ -83,6 +83,12 @@ User Code
- `steer()` - Queue message for next turn
- `followUp()` - Queue message after stop
- `subscribe()` - Listen to events
- `waitForIdle()` - Wait for agent to finish processing
- `reset!()` - Clear transcript state and queued messages
- `clearAllQueues()` - Remove all queued steering and follow-up messages
- `hasQueuedMessages()` - Check if queues have pending messages
- `abort()` - Abort the current run
- `get_state()` - Get the current agent state
### AgentLoop
@@ -113,9 +119,14 @@ User Code
**Key methods**:
- `appendMessage()` - Add message
- `appendCompaction()` - Compress history
- `appendCompaction()` - Compress history with summary
- `moveTo()` - Navigate branches
- `buildSessionContext()` - Build context for LLM
- `buildContext()` - Build context for LLM
- `getBranch()` - Get branch entries
- `getSessionStats()` - Get session statistics
- `appendThinkingLevelChange()` - Record thinking level change
- `appendModelChange()` - Record model change
- `appendActiveToolsChange()` - Record active tools change
### Tools
@@ -165,32 +176,209 @@ AgentStartEvent
└─ AgentEndEvent
```
## Data Flow
## Complete Data Flow with Type Transformations
### Message Transformation
This documentation shows how data is transformed through the agent lifecycle.
### Message Type Hierarchy
```
AgentMessage[] (internal)
Message (for LLM API)
├── UserMessage (role: "user")
│ └── content::Vector{MessageContent}
│ ├── TextContent (text::String)
│ └── ImageContent (data::String, mime_type::String)
├── AssistantMessage (role: "assistant")
│ ├── content::Vector{MessageContent}
│ │ ├── TextContent
│ │ └── ToolCall (id, name, arguments::Dict{String, Any})
│ ├── usage::Usage
│ ├── stop_reason::String
│ └── timestamp::Timestamp
└── ToolResultMessage (role: "toolResult")
├── tool_call_id::String
├── tool_name::String
├── content::Vector{MessageContent}
├── details::Any
├── usage::Union{Usage, Nothing}
├── is_error::Bool
└── timestamp::Timestamp
AgentMessage (internal, abstract type)
├── UserMessage (same as above)
├── AssistantMessage (same as above)
├── ToolResultMessage (same as above, plus: role, added_tool_names)
├── BashExecutionMessage (custom)
│ ├── role, command, output, exit_code
│ ├── cancelled, truncated, full_output_path, timestamp
│ └── exclude_from_context
├── CompactionSummaryMessage (custom)
│ ├── role, summary, tokens_before, timestamp
│ └── converted to UserMessage for LLM
├── BranchSummaryMessage (custom)
│ ├── role, summary, from_id, timestamp
│ └── converted to UserMessage for LLM
└── CustomMessage (custom, extends AgentMessage)
├── message::AgentMessage
└── custom_type::String
```
### Complete Conversation Flow
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 1: User Input (Vector{AgentMessage}) │
└─────────────────────────────────────────────────────────────────────────────┘
prompt(agent, "Hello!")
├─ transform_context() (optional)
AgentMessage[] (transformed)
└─► normalizePromptInput()
Input: "Hello!"::String
Output: [UserMessage("user", [TextContent("Hello!")], timestamp)]
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 2: AgentLoop Processing │
└─────────────────────────────────────────────────────────────────────────────┘
runAgentLoop()
├─► transform_context() (optional hook)
│ Input: [UserMessage(...)]::Vector{AgentMessage}
│ Output: [UserMessage(...)]::Vector{AgentMessage}
├─► convert_to_llm()
│ Input: [UserMessage(...)]::Vector{AgentMessage}
│ Output: [UserMessage(...)]::Vector{Message}
├─► stream_fn() - LLM API call
│ Input: model, Context(...), config
│ Output: AssistantMessage with ToolCall[]
├─► executeToolCalls()
│ Input: AssistantMessage (with ToolCall[])
│ Output: ToolResultMessage[]
└─► Emit events and append to context.messages
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 3: Final Conversation State │
└─────────────────────────────────────────────────────────────────────────────┘
context.messages::Vector{AgentMessage}
├─ UserMessage("user", [TextContent("Hello!")], ...)
├─ AssistantMessage("assistant", [
│ TextContent("Hi there!"),
│ ToolCall("bash", {...})
│ ], ...)
└─ ToolResultMessage("toolResult", "bash", [TextContent("...")], ...)
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 4: AgentEndEvent (final output) │
└─────────────────────────────────────────────────────────────────────────────┘
AgentEndEvent(messages::Vector{AgentMessage})
└─ Contains full conversation history
User Input (String / AgentMessage / Vector{AgentMessage})
├─► normalizePromptInput()
│ Input: input::Union{String, AgentMessage, Vector{AgentMessage}}
│ Output: Vector{AgentMessage}
│ • String → UserMessage("user", [TextContent(input)], timestamp)
│ • AgentMessage → [input]
│ • Vector{AgentMessage} → input (pass-through)
├─► prompt(agent, messages)
│ └─► runPromptMessages()
├─ convert_to_llm()
Message[] (LLM API)
AgentLoop Execution:
├─► transform_context() (optional hook)
│ Input: context.messages::Vector{AgentMessage}
│ Output: messages::Vector{AgentMessage} (transformed)
├─► convert_to_llm()
│ Input: messages::Vector{AgentMessage}
│ Output: llm_messages::Vector{Message}
│ AgentMessage → Message mapping:
│ • UserMessage → UserMessage (pass-through)
│ • AssistantMessage → AssistantMessage (pass-through)
│ • ToolResultMessage → ToolResultMessage (pass-through)
│ • BashExecutionMessage → UserMessage (text conversion)
│ • CompactionSummaryMessage → UserMessage (text wrapped)
│ • BranchSummaryMessage → UserMessage (text wrapped)
├─► LLM API Call (stream_fn)
│ Input: model, Context(system_prompt, llm_messages, tools), config
│ Output: Stream{AssistantMessageEvent}
├─► AssistantMessage (returned from LLM)
│ content::Vector{MessageContent}
│ └─ Contains: TextContent[] and/or ToolCall[]
├─► executeToolCalls() (if ToolCall[] in content)
│ │
│ ├─► prepareToolCall() for each ToolCall
│ │ Input: tool_call::ToolCall
│ │ Output: PreparedToolCall or ImmediateToolCallOutcome
│ │
│ ├─► executePreparedToolCall() (if prepared)
│ │ Input: PreparedToolCall
│ │ Output: ExecutedToolCallOutcome
│ │ tool.execute() returns AgentToolResultMutable
│ │
│ ├─► finalizeExecutedToolCall()
│ │ Input: ExecutedToolCallOutcome
│ │ Output: FinalizedToolCallOutcome
│ │
│ └─► createToolResultMessage()
│ Input: FinalizedToolCallOutcome
│ Output: ToolResultMessage
│ • role: "toolResult"
│ • tool_call_id, tool_name
│ • content::Vector{MessageContent}
│ • details, usage, added_tool_names
│ • is_error, timestamp
└─► Append to context.messages and new_messages
Vector{AgentMessage} (final conversation history)
Contains: [UserMessage, AssistantMessage, ToolResultMessage, ...]
```
### Tool Execution Flow
```
ToolCall (in assistant message)
ToolCall (in AssistantMessage.content)
├─ before_tool_call hook (optional)
│ Input: BeforeToolCallContext
│ Output: BeforeToolCallResult (block, reason) or nothing
├─ before_tool_call hook
├─ prepareToolCall()
├─ execute()
├─ after_tool_call hook
│ Input: tool_call::ToolCall
│ Output: Union{PreparedToolCall, ImmediateToolCallOutcome}
│ • Validates tool exists
│ • Runs before_tool_call hook
│ • Runs prepare_arguments hook (optional)
│ • Runs validateToolArguments (optional)
├─ executePreparedToolCall() (if prepared)
│ Input: PreparedToolCall
│ Output: ExecutedToolCallOutcome
│ tool.execute() returns AgentToolResultMutable
├─ finalizeExecutedToolCall()
│ Input: ExecutedToolCallOutcome
│ Output: FinalizedToolCallOutcome
│ Runs after_tool_call hook (optional)
└─ createToolResultMessage()
Input: FinalizedToolCallOutcome
Output: ToolResultMessage
• role: "toolResult"
• tool_call_id, tool_name
• content::Vector{MessageContent}
• details, usage, added_tool_names
• is_error, timestamp
```
## Best Practices
@@ -253,7 +441,7 @@ appendMessage(session, user_message)
appendMessage(session, assistant_message)
# Build context from session
context = buildSessionContext(session)
context = buildContext(session)
```
### Pattern 2: Long Conversations
@@ -277,7 +465,7 @@ end
session.moveTo(branch_point_id)
# Create new branch
appendBranchSummary(session, "Exploring alternative approach")
moveTo(session, branch_point_id, summary=["summary" => "Exploring alternative approach"])
appendMessage(session, new_user_message)
```
@@ -286,13 +474,13 @@ appendMessage(session, new_user_message)
```julia
# Create custom tool
custom_tool = AgentTool(
"custom",
"custom",
"Does custom thing",
...,
execute_function,
nothing,
EXECUTION_PARALLEL,
"custom", # name
"Custom", # label
"Does custom thing", # description
parameters, # parameter schema
execute_function, # execute
nothing, # prepare_arguments (optional)
EXECUTION_PARALLEL, # execution_mode
)
# Add to agent
+560
View File
@@ -0,0 +1,560 @@
# Agent Loop Tracing
This document traces the agent loop through two example interactions.
## Architecture Overview
```
Agent (src/agent.jl)
|
v
AgentLoop (src/agent_loop.jl) -- runLoop() is the core while(true) loop
|
v
StreamFn (src/stream_fn.jl) -- LLM streaming function (user-provided)
|
v
Tools (src/tools/*.jl) -- bash, read, write, edit
```
Key types:
- `Agent` (agent.jl:85) -- high-level wrapper with state, queues, listeners
- `agentLoop()` (agent_loop.jl:23) -- entry point, spawns thread, returns `EventStream`
- `runLoop()` (agent_loop.jl:169) -- the core `while(true)` loop
- `streamAssistantResponse()` (agent_loop.jl:361) -- calls LLM, streams events, returns `AssistantMessage`
- `executeToolCalls()` (agent_loop.jl:476) -- runs tool calls (sequential or parallel)
- `AgentContext` (types.jl:186) -- system_prompt + messages + tools
- `AgentLoopConfig` -- model, thinking_level, callbacks for steering/follow-up/tool execution
---
## Scenario 1: User asks "what is the content of text.txt file", agent responds
### Step 1: User invokes `prompt(agent, "what is the content of text.txt file")`
**File: agent.jl:284-292**
```julia
prompt(agent, "what is the content of text.txt file")
-> normalizePromptInput(agent, "what is the content of text.txt file", [])
-> [UserMessage("user", [TextContent("what is the content of text.txt file")], timestamp)]
-> runPromptMessages(agent, messages)
```
The string is normalized into a single `UserMessage`.
### Step 2: `runPromptMessages` calls `agentLoop()`
**File: agent.jl:310-313** (TODO stub, but conceptually):
```julia
runPromptMessages(agent, messages)
-> AgentLoop.agentLoop(
prompts = [UserMessage(...)],
context = createContextSnapshot(agent), # AgentContext with system_prompt, messages, tools
config = createLoopConfig(agent),
signal = nothing,
stream_fn = agent.stream_function,
)
```
### Step 3: `agentLoop()` spawns thread and calls `runAgentLoop()`
**File: agent_loop.jl:23-45**
```julia
agentLoop(prompts, context, config, signal, stream_fn)
-> createAgentStream() # creates EventStream
-> Threads.@spawn begin
runAgentLoop(prompts, context, config, emit, signal, stream_fn)
end(stream, messages)
end
-> return stream
```
### Step 4: `runAgentLoop()` initializes and enters `runLoop()`
**File: agent_loop.jl:85-116**
```julia
runAgentLoop(prompts, context, config, emit, signal, stream_fn)
-> new_messages = copy(prompts) # [UserMessage(...)]
-> current_context = AgentContext(context.system_prompt, vcat(context.messages, copy(prompts)), context.tools)
-> emit(AgentStartEvent())
-> emit(TurnStartEvent())
-> for prompt in prompts: emit(MessageStartEvent(prompt)); emit(MessageEndEvent(prompt)) end
-> runLoop(current_context, new_messages, config, signal, emit, stream_fn)
```
Events emitted so far:
1. `AgentStartEvent`
2. `TurnStartEvent`
3. `MessageStartEvent(UserMessage)`
4. `MessageEndEvent(UserMessage)`
### Step 5: `runLoop()` -- first iteration
**File: agent_loop.jl:169-310**
```julia
runLoop(initial_context, new_messages, initial_config, signal, emit, stream_function)
-> current_context = initial_context
-> first_turn = true
-> pending_messages = getSteeringMessages(config) # may be empty [] by default (agent_loop.jl:180-182)
-> while true:
has_more_tool_calls = true # reset each outer iteration
# Inner loop: has_more_tool_calls || !isempty(pending_messages)
while has_more_tool_calls || !isempty(pending_messages)
first_turn = false # TurnStartEvent NOT emitted (already done)
# no pending_messages
# === STEP 5a: Call LLM ===
message = streamAssistantResponse(current_context, config, signal, emit, stream_function)
```
### Step 5a: `streamAssistantResponse()` -- LLM call
**File: agent_loop.jl:361-435**
```julia
streamAssistantResponse(context, config, signal, emit, stream_function)
-> messages = context.messages # [UserMessage(...)]
-> llm_messages = config.convert_to_llm(messages) # filter to user/assistant/toolResult roles
-> llm_context = Context(context.system_prompt, llm_messages, context.tools)
-> response = stream_function(config.model, llm_context, merged_config)
```
The `stream_function` (user-provided via StreamFn) calls the LLM API. It yields events:
```
StartEvent(partial=AssistantMessage(role="assistant", content=[]))
-> push!(context.messages, partial_message)
-> emit(MessageStartEvent(partial_message))
TextDeltaEvent(partial=AssistantMessage with ToolCall for "read")
-> context.messages[end] = partial_message
-> emit(MessageUpdateEvent(partial_message, event))
TextDeltaEvent(...) -- streaming continues
toolcall_start/toolcall_delta/toolcall_end -- tool call detected: read(file="text.txt") (agent_loop.jl:405)
DoneEvent(reason="tool_calls", ...)
-> final_message = AssistantMessage(role="assistant", content=[ToolCall(...)])
-> context.messages[end] = final_message
-> emit(MessageEndEvent(final_message))
-> return final_message
```
Back in `runLoop`:
- `message` = `AssistantMessage` with `stop_reason = "tool_calls"`
- `push!(new_messages, message)`
### Step 5b: Tool call detection
**File: agent_loop.jl:219-244**
```julia
tool_calls = filter(c -> c isa ToolCall, message.content)
# tool_calls = [ToolCall(type="tool_call", id="call_1", name="read", arguments={file="text.txt"}, ...)]
tool_results = []
has_more_tool_calls = false # set to true only if tool calls execute and don't terminate (agent_loop.jl:225)
if !isempty(tool_calls)
executed_tool_batch = executeToolCalls(
current_context, message, config, signal, emit,
)
append!(tool_results, executed_tool_batch.messages)
has_more_tool_calls = !executed_tool_batch.terminate
```
### Step 5c: `executeToolCalls()` -- sequential or parallel
**File: agent_loop.jl:476-514**
Since there's only one tool call and no sequential mode forced, it uses `executeToolCallsParallel()` (or sequential -- both paths converge for a single tool call).
```julia
executeToolCalls(context, assistant_message, config, signal, emit)
-> tool_calls extracted from assistant_message.content (agent_loop.jl:483-486)
-> tool = findfirst(t -> t.name == "read", context.tools)
-> preparation = prepareToolCall(...)
-> validated_args = {file="text.txt"}
-> return PreparedToolCall("prepared", tool_call, tool, validated_args)
executed = executePreparedToolCall(preparation, signal, emit)
-> result = prepared.tool.execute("call_1", {file="text.txt"}, signal, on_update, context)
# This invokes the read tool's execute function (src/tools/read.jl:26)
# TODO: in the current code, it returns a placeholder
-> return ExecutedToolCallOutcome(result, false)
finalized = finalizeExecutedToolCall(...)
# Creates FinalizedToolCallOutcome
emitToolExecutionEnd(finalized, emit)
# emits ToolExecutionEndEvent
tool_result_message = createToolResultMessage(finalized)
# creates ToolResultMessage(role="toolResult", tool_call_id="call_1", tool_name="read", content=[TextContent(...)])
emitToolResultMessage(tool_result_message, emit)
# emits MessageStartEvent(tool_result_message), MessageEndEvent(tool_result_message)
```
Events emitted during tool execution:
5. `MessageStartEvent(assistant_message)` (from LLM)
6. `MessageEndEvent(assistant_message)` (from LLM done)
7. `ToolExecutionStartEvent`
8. `ToolExecutionEndEvent`
9. `MessageStartEvent(tool_result_message)`
10. `MessageEndEvent(tool_result_message)`
### Step 5d: Back in inner loop
**File: agent_loop.jl:240-294**
```julia
push!(current_context.messages, tool_result_message)
push!(new_messages, tool_result_message)
emit(TurnEndEvent(message, tool_results))
next_turn_snapshot = prepare_next_turn(config, PrepareNextTurnContext(...))
# Returns nothing by default (no custom prepare_next_turn)
if !isnothing(next_turn_snapshot) ... end # skipped
if should_stop_after_turn(config, ...) ... end # returns false by default
pending_messages = get_steering_messages(config) # returns []
# inner while continues: has_more_tool_calls = true, pending_messages = []
# === SECOND LLM CALL ===
message = streamAssistantResponse(current_context, config, signal, emit, stream_function)
# context.messages now = [UserMessage(...), AssistantMessage(read tool call), ToolResultMessage(file contents)]
```
### Step 5e: Second LLM call -- agent responds
**File: agent_loop.jl:361-435**
```julia
streamAssistantResponse(context, config, signal, emit, stream_function)
-> llm_messages = [UserMessage(...), AssistantMessage(...), ToolResultMessage(...)]
-> response = stream_function(model, Context(system_prompt, llm_messages, tools), config)
```
The LLM receives the user's question + its own tool call + the file contents as a tool result. It generates a text response.
Events:
```
StartEvent -> MessageStartEvent
TextDeltaEvent -> MessageUpdateEvent (text streaming)
...
DoneEvent(reason="end_turn") -> MessageEndEvent
```
### Step 5f: No more tool calls -- loop exits
**File: agent_loop.jl:219-244**
```julia
tool_calls = filter(c -> c isa ToolCall, message.content)
# tool_calls = [] (no tool calls in the final response)
has_more_tool_calls = false # stays false
emit(TurnEndEvent(message, ToolResultMessage[]))
next_turn_snapshot = prepare_next_turn(...) # nothing
should_stop_after_turn(...) # false
pending_messages = get_steering_messages(...) # []
# inner while: has_more_tool_calls=false, pending_messages=[] -> exits inner loop
follow_up_messages = get_follow_up_messages(...) # []
# exits outer while
emit(AgentEndEvent(new_messages))
```
Events emitted at end:
11. `MessageStartEvent(assistant_response)`
12. `MessageUpdateEvent(...)` (text deltas)
13. `MessageEndEvent(assistant_response)`
14. `TurnEndEvent(response, [])`
15. `AgentEndEvent([UserMessage, AssistantMessage, ToolResultMessage, AssistantResponse])`
### Summary of Scenario 1 event sequence:
| # | Event | Source |
|---|-------|--------|
| 1 | `AgentStartEvent` | runAgentLoop() |
| 2 | `TurnStartEvent` | runAgentLoop() |
| 3 | `MessageStartEvent(UserMessage)` | runAgentLoop() |
| 4 | `MessageEndEvent(UserMessage)` | runAgentLoop() |
| 5 | `MessageStartEvent(AssistantMessage)` | streamAssistantResponse() |
| 6 | `MessageUpdateEvent(AssistantMessage)` | streamAssistantResponse() (streaming) |
| 7 | `MessageEndEvent(AssistantMessage)` | streamAssistantResponse() |
| 8 | `ToolExecutionStartEvent` | executeToolCalls() |
| 9 | `ToolExecutionEndEvent` | executeToolCalls() |
| 10 | `MessageStartEvent(ToolResultMessage)` | emitToolResultMessage() |
| 11 | `MessageEndEvent(ToolResultMessage)` | emitToolResultMessage() |
| 12 | `TurnEndEvent(AssistantMessage, [tool_results])` | runLoop() |
| 13 | `MessageStartEvent(AssistantMessage)` | streamAssistantResponse() (2nd call) |
| 14 | `MessageUpdateEvent(AssistantMessage)` | streamAssistantResponse() (text streaming) |
| 15 | `MessageEndEvent(AssistantMessage)` | streamAssistantResponse() |
| 16 | `TurnEndEvent(AssistantResponse, [])` | runLoop() |
| 17 | `AgentEndEvent([all messages])` | runLoop() |
---
## Scenario 2: User asks "copy text.txt to text.md", agent responds
### Step 1-4: Same as Scenario 1
User invokes `prompt(agent, "copy text.txt to text.md")`, which flows through `agentLoop()` -> `runAgentLoop()` -> `runLoop()`.
Events 1-4 are identical (AgentStart, TurnStart, UserMessage start/end).
### Step 5: First LLM call -- agent decides to use tools
The LLM receives:
```
System: <system_prompt>
User: "copy text.txt to text.md"
```
The LLM decides it needs to:
1. Read text.txt (to get its contents), then
2. Write those contents to text.md
The LLM may emit a single `AssistantMessage` with **two** `ToolCall` objects:
```
AssistantMessage(content=[
ToolCall(id="call_1", name="read", arguments={file="text.txt"}),
ToolCall(id="call_2", name="write", arguments={file="text.md", content="...contents of text.txt..."}),
])
```
Or it may emit one tool call at a time (sequential), which is also supported.
### Step 5b: Tool execution
**File: agent_loop.jl:219-244**
```julia
tool_calls = filter(c -> c isa ToolCall, message.content)
# tool_calls = [ToolCall(read), ToolCall(write)]
executed_tool_batch = executeToolCalls(context, message, config, signal, emit)
```
If `tool_execution == EXECUTION_PARALLEL` (default) and no tool forces sequential mode:
**File: agent_loop.jl:568-633 (executeToolCallsParallel)**
```julia
executeToolCallsParallel(...)
-> for tool_call in tool_calls:
# call_1: read
emit(ToolExecutionStartEvent("call_1", "read", {file="text.txt"}))
preparation = prepareToolCall(...) # validated
push!(finalized_calls, () -> executed_read()) # closure for deferred execution
# call_2: write
emit(ToolExecutionStartEvent("call_2", "write", {file="text.md", content="..."}))
preparation = prepareToolCall(...)
push!(finalized_calls, () -> executed_write()) # closure
# Execute in order
ordered_finalized_calls = map(entry -> entry(), finalized_calls)
for finalized in ordered_finalized_calls:
tool_result_message = createToolResultMessage(finalized)
emitToolResultMessage(tool_result_message, emit)
```
Events for parallel execution:
```
ToolExecutionStartEvent(call_1, "read", ...)
ToolExecutionEndEvent(call_1, "read", ...)
ToolExecutionStartEvent(call_2, "write", ...)
ToolExecutionEndEvent(call_2, "write", ...)
MessageStartEvent(ToolResultMessage[read result])
MessageEndEvent(ToolResultMessage[read result])
MessageStartEvent(ToolResultMessage[write result])
MessageEndEvent(ToolResultMessage[write result])
```
If `tool_execution == EXECUTION_SEQUENTIAL` or any tool is marked sequential:
**File: agent_loop.jl:520-562 (executeToolCallsSequential)**
```julia
for tool_call in tool_calls:
emit(ToolExecutionStartEvent(...))
# execute, finalize, emit result
# THEN proceed to next
```
Events for sequential execution:
```
ToolExecutionStartEvent(call_1, "read", ...)
ToolExecutionEndEvent(call_1, "read", ...)
MessageStartEvent(ToolResultMessage[read result])
MessageEndEvent(ToolResultMessage[read result])
ToolExecutionStartEvent(call_2, "write", ...)
ToolExecutionEndEvent(call_2, "write", ...)
MessageStartEvent(ToolResultMessage[write result])
MessageEndEvent(ToolResultMessage[write result])
```
### Step 5d: Second LLM call
```julia
has_more_tool_calls = !executed_tool_batch.terminate # false (unless terminate=true)
# inner loop continues since pending_messages is still empty
# Actually: has_more_tool_calls = false, pending_messages = []
# -> exits inner loop
# follow_up_messages = []
# -> exits outer loop
emit(TurnEndEvent(message, tool_results))
```
Wait -- this depends on whether the LLM's first response included only tool calls (no text answer). If the LLM only returned tool calls and the tool results were processed, the agent may need a **third** LLM call to generate the final user-facing response.
**Revised flow for two tool calls:**
After tool results are added to context:
```
context.messages = [
UserMessage("copy text.txt to text.md"),
AssistantMessage([ToolCall(read), ToolCall(write)]),
ToolResultMessage(read result),
ToolResultMessage(write result),
]
```
The agent needs another LLM call to generate a response. Let's trace it:
### Step 5e: Second LLM call -- final response
```julia
message = streamAssistantResponse(current_context, ...)
```
LLM receives:
```
System: <system_prompt>
User: "copy text.txt to text.md"
Assistant: [ToolCall(read), ToolCall(write)]
ToolResult: (contents of text.txt)
ToolResult: (write confirmation)
```
LLM generates: "I've copied text.txt to text.md."
Events:
```
MessageStartEvent(AssistantMessage)
MessageUpdateEvent(... text deltas ...)
MessageEndEvent(AssistantMessage)
```
### Step 5f: No tool calls, loop exits
```julia
tool_calls = [] # no ToolCalls in response
has_more_tool_calls = false
emit(TurnEndEvent(message, []))
pending_messages = []
follow_up_messages = []
emit(AgentEndEvent(new_messages))
```
### Summary of Scenario 2 event sequence (parallel tool execution):
| # | Event | Source |
|---|-------|--------|
| 1 | `AgentStartEvent` | runAgentLoop() |
| 2 | `TurnStartEvent` | runAgentLoop() |
| 3 | `MessageStartEvent(UserMessage)` | runAgentLoop() |
| 4 | `MessageEndEvent(UserMessage)` | runAgentLoop() |
| 5 | `MessageStartEvent(AssistantMessage)` | streamAssistantResponse() (1st LLM call) |
| 6 | `MessageEndEvent(AssistantMessage)` | streamAssistantResponse() |
| 7 | `ToolExecutionStartEvent(call_1, "read")` | executeToolCallsParallel() |
| 8 | `ToolExecutionEndEvent(call_1, "read")` | executeToolCallsParallel() |
| 9 | `ToolExecutionStartEvent(call_2, "write")` | executeToolCallsParallel() |
| 10 | `ToolExecutionEndEvent(call_2, "write")` | executeToolCallsParallel() |
| 11 | `MessageStartEvent(ToolResultMessage[read])` | emitToolResultMessage() |
| 12 | `MessageEndEvent(ToolResultMessage[read])` | emitToolResultMessage() |
| 13 | `MessageStartEvent(ToolResultMessage[write])` | emitToolResultMessage() |
| 14 | `MessageEndEvent(ToolResultMessage[write])` | emitToolResultMessage() |
| 15 | `TurnEndEvent(AssistantToolCalls, [read_result, write_result])` | runLoop() |
| 16 | `MessageStartEvent(AssistantMessage)` | streamAssistantResponse() (2nd LLM call) |
| 17 | `MessageUpdateEvent(AssistantMessage)` | streamAssistantResponse() (text streaming) |
| 18 | `MessageEndEvent(AssistantMessage)` | streamAssistantResponse() |
| 19 | `TurnEndEvent(AssistantResponse, [])` | runLoop() |
| 20 | `AgentEndEvent([all messages])` | runLoop() |
---
## Key Design Patterns
### 1. Event Stream Architecture
Events flow through `emit::AgentEventSink` (a function) into an `EventStream`. Consumers subscribe to the stream and receive events as they occur. The stream terminates when `AgentEndEvent` is emitted.
### 2. Context Accumulation
`AgentContext.messages` grows across turns:
```
[UserMessage, AssistantMessage, ToolResultMessage, AssistantMessage, ToolResultMessage, ...]
```
### 3. LLM Conversion
Before each LLM call, `config.convert_to_llm()` filters the agent messages to only include user/assistant/toolResult roles (src/agent.jl:18-23):
```julia
filter(m -> m.role in ("user", "assistant", "toolResult"), messages)
```
### 4. Tool Execution Modes
- `EXECUTION_PARALLEL` (default): tool calls are prepared as closures and executed in sequence after all are prepared
- `EXECUTION_SEQUENTIAL`: each tool is prepared, executed, and finalized before the next begins
### 5. Turn Continuation
The inner `while has_more_tool_calls` loop handles:
- Multiple tool calls from a single assistant response
- Pending steering/follow-up messages injected between turns
The outer `while true` loop handles:
- Full turns (LLM call + tool execution)
- Switching between tool-result turns and response turns
### 6. Message Types
| Type | Role | Created By |
|------|------|------------|
| `UserMessage` | "user" | User via `prompt()` |
| `AssistantMessage` | "assistant" | LLM via `streamAssistantResponse()` |
| `ToolResultMessage` | "toolResult" | `createToolResultMessage()` after tool execution |
| `BashExecutionMessage` | "user" | Bash tool (excluded from context by default) |
| `CompactionSummaryMessage` | "user" | Compaction process |
| `BranchSummaryMessage` | "user" | Branch summarization |
### 7. Tool Call Lifecycle
```
ToolCall (from LLM)
-> prepareToolCall() (validate args, before_tool_call hook)
-> executePreparedToolCall() (invoke tool.execute)
-> finalizeExecutedToolCall() (after_tool_call hook)
-> createToolResultMessage() (wrap result in ToolResultMessage)
-> emitToolResultMessage() (emit MessageStart/MessageEnd)
```