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) │ │ 2. AGENT LOOP START (runAgentLoop) │
│ │ │ │
│ new_messages = copy(prompts) │ │ 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!) │ │ (They are NOT in the steering queue!) │
│ │ │ │
│ emit(AgentStartEvent) │ │ emit(AgentStartEvent) │
@@ -42,7 +42,7 @@
│ │ │ │
│ pending_messages = get_steering_messages() │ │ 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) │ │ These are for CONTINUING conversation (NOT new user prompts) │
│ │ │ │
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
@@ -51,17 +51,17 @@
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 4. PENDING MESSAGE HANDLING (steering messages only) │ │ │ │ │ │ 4. PENDING MESSAGE HANDLING (steering messages only) │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ pending_messages = get_steering() │ │ │ │ │ │ pending_messages = get_steering() │ │ │
│ │ │ if !isempty(pending_messages): │ │ │ │ │ │ if !isempty(pending_messages): │ │ │
│ │ │ for msg in pending_messages: │ │ │ │ │ │ for msg in pending_messages: │ │ │
│ │ │ emit(MessageStartEvent(msg)) │ │ │ │ │ │ emit(MessageStartEvent(msg)) │ │ │
│ │ │ emit(MessageEndEvent(msg)) │ │ │ │ │ │ emit(MessageEndEvent(msg)) │ │ │
│ │ │ push to current_context.messages ← Steering messages go HERE │ │ │ │ │ │ push to current_context.messages ← Steering messages go HERE │ │ │
│ │ │ push to new_messages │ │ │ │ │ │ push to new_messages │ │ │
│ │ │ pending_messages = [] │ │ │ │ │ │ pending_messages = [] │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ Note: User messages from Agent.prompt() are ALREADY in context.messages │ │ │ │ │ │ Note: User messages from Agent.prompt() are ALREADY in context.messages │ │ │
│ │ │ (They were added in runAgentLoop via vcat(), not via this queue) │ │ │ │ │ │ (They were added in runAgentLoop via vcat(), not via this queue) │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
@@ -168,7 +168,7 @@
│ new_messages = [UserMessage("What is Julia?")] │ │ new_messages = [UserMessage("What is Julia?")] │
│ current_context.messages = vcat([...existing...], [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(AgentStartEvent), emit(TurnStartEvent) │
│ emit(MessageStart/End) for user message │ │ emit(MessageStart/End) for user message │
│ │ │ │
@@ -207,11 +207,11 @@
│ │ follow_up_queue: [] │ │ │ │ follow_up_queue: [] │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ LLM SEES (convert_to_llm() filters): │ LLM SEES (convert_to_llm() filters):
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │ Messages passed to LLM API: │ │ │ │ Messages passed to LLM API: │
│ │ [UserMessage("What is Julia?"), AssistantMessage("Julia is...")] │ │ [UserMessage("What is Julia?"), AssistantMessage("Julia is...")]
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────┘
│ │ │ │
│ TURN #2: User asks "How does it work?" │ │ TURN #2: User asks "How does it work?" │
│ ───────────────────────────────────────── │ │ ───────────────────────────────────────── │
@@ -223,7 +223,7 @@
│ new_messages = [UserMessage("How does it work?")] │ │ new_messages = [UserMessage("How does it work?")] │
│ current_context.messages = vcat([...previous..., 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(AgentStartEvent), emit(TurnStartEvent) │
│ emit(MessageStart/End) for user message │ │ emit(MessageStart/End) for user message │
│ │ │ │
@@ -245,13 +245,13 @@
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ LLM SEES: │ │ LLM SEES: │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │ Messages passed to LLM API: │ │ │ │ Messages passed to LLM API: │
│ │ [UserMessage("What is Julia?"), │ │ [UserMessage("What is Julia?"),
│ │ AssistantMessage("Julia is..."), │ │ AssistantMessage("Julia is..."),
│ │ UserMessage("How does it work?"), │ │ UserMessage("How does it work?"),
│ │ AssistantMessage("It works by...")] │ │ AssistantMessage("It works by...")]
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────┘
│ │ │ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
@@ -261,83 +261,83 @@
│ │ │ │
│ What is a steering message? │ │ What is a steering message? │
│ • A message (any AgentMessage type) injected via: `agent.steer(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? │ │ How is it created? │
│ • User code calls: agent.steer(UserMessage("...")) │ │ • User code calls: agent.steer(UserMessage("..."))
│ • Or: agent.steer(AssistantMessage("...")) │ │ • Or: agent.steer(AssistantMessage("..."))
│ • Or any other AgentMessage subtype │ │ • Or any other AgentMessage subtype
│ │ │ │
│ When is it processed? │ │ When is it processed? │
│ • At the START of the next loop iteration (line 194-202 in agent_loop.jl) │ │ • At the START of the next loop iteration (line 194-202 in agent_loop.jl) │
│ • AFTER the previous assistant turn completes │ │ • AFTER the previous assistant turn completes │
│ • BEFORE the next assistant response is streamed │ │ • BEFORE the next assistant response is streamed
│ │ │ │
│ Why use steering? │ │ Why use steering? │
│ Use case 1: Tool execution result injection │ │ Use case 1: Tool execution result injection
│ - Agent calls a tool (e.g., read_file, bash) │ │ - Agent calls a tool (e.g., read_file, bash)
│ - Tool returns result │ │ - Tool returns result
│ - You want to inject a follow-up question based on the 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?")) │ │ - agent.steer(UserMessage("Based on the file, what should we do next?"))
│ │ │ │
│ Use case 2: Multi-turn conversation without user input │ │ Use case 2: Multi-turn conversation without user input
│ - Agent responds to user │ │ - Agent responds to user
│ - Before user types again, you want to inject a system message │ │ - Before user types again, you want to inject a system message
│ - agent.steer(BashExecutionMessage(...)) or custom message │ │ - agent.steer(BashExecutionMessage(...)) or custom message
│ - This continues the conversation automatically │ │ - This continues the conversation automatically │
│ │ │ │
│ Use case 3: Branch navigation recovery │ │ Use case 3: Branch navigation recovery
│ - User navigates between conversation branches │ │ - User navigates between conversation branches
│ - After switching branches, you want to inject a context message │ │ - After switching branches, you want to inject a context message
│ - agent.steer(BranchSummaryMessage(...)) │ │ - agent.steer(BranchSummaryMessage(...))
│ - The agent can then continue from the new branch context │ │ - The agent can then continue from the new branch context
│ │ │ │
│ Use case 4: Compaction summary injection │ │ Use case 4: Compaction summary injection
│ - Conversation history is compacted │ │ - Conversation history is compacted
│ - After compaction, inject summary message │ │ - After compaction, inject summary message
│ - agent.steer(CompactionSummaryMessage(...)) │ │ - agent.steer(CompactionSummaryMessage(...))
│ - Agent knows old history was summarized │ │ - Agent knows old history was summarized │
│ │ │ │
│ Example: │ │ Example: │
│ agent.steer(UserMessage("Follow-up question here")) │ │ agent.steer(UserMessage("Follow-up question here"))
│ # This will be processed in the next loop iteration, │ │ # This will be processed in the next loop iteration,
│ # appearing in context.messages before the next LLM call │ │ # appearing in context.messages before the next LLM call
│ │ │ │
│ The LLM sees: │ │ The LLM sees: │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │ All messages become Message[] via convert_to_llm(): │ │ All messages become Message[] via convert_to_llm():
│ │ [UserMessage(...), AssistantMessage(...), UserMessage(from_steer), ...] │ │ [UserMessage(...), AssistantMessage(...), UserMessage(from_steer), ...]
│ │ │ │ │ │ │
│ │ The LLM cannot tell which came from Agent.prompt() vs agent.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[]: │ │ convert_to_llm() transforms ALL AgentMessages to Message[]:
│ │ │ │ │ │ │
│ │ UserMessage("user") → UserMessage (for LLM) │ │ UserMessage("user") → UserMessage (for LLM)
│ │ Steering UserMessage("user") → UserMessage (for LLM) ← Same! │ │ Steering UserMessage("user") → UserMessage (for LLM) ← Same!
│ │ AssistantMessage("assistant") → AssistantMessage (for LLM) │ │ AssistantMessage("assistant") → AssistantMessage (for LLM)
│ │ ToolResultMessage("toolResult") → ToolResultMessage (for LLM) │ │ ToolResultMessage("toolResult") → ToolResultMessage (for LLM)
│ │ │ │ │ │ │
│ │ BranchSummaryMessage → UserMessage (wrapped in summary tags) │ │ BranchSummaryMessage → UserMessage (wrapped in summary tags)
│ │ CompactionSummaryMessage → UserMessage (wrapped in summary tags) │ │ CompactionSummaryMessage → UserMessage (wrapped in summary tags)
│ │ BashExecutionMessage → UserMessage (if not excluded) │ │ BashExecutionMessage → UserMessage (if not excluded)
│ │ CustomMessage → UserMessage │ │ CustomMessage → UserMessage
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────────────────────────────────────────────────┘
│ │ │ │
│ The difference is ONLY in HOW messages enter the system: │ │ The difference is ONLY in HOW messages enter the system:
│ • User messages: Agent.prompt() → vcat() → context.messages (direct) │ │ • User messages: Agent.prompt() → vcat() → context.messages (direct)
│ • Steering: agent.steer() → queue → loop → context.messages (indirect) │ │ • 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() │ │ 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 │ │ This allows continuing conversation without calling Agent.prompt() again │
│ │ │ │
│ 3. Context is preserved across turns - context.messages grows with each turn │ │ 3. Context is preserved across turns - context.messages grows with each turn │
│ LLM sees the full conversation history │ │ 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 │ │ The "steering" vs "user" distinction is just a control mechanism, not a message type │
│ │ │ │
│ 5. New turn is triggered by: │ │ 5. New turn is triggered by: │
+397 -145
View File
@@ -51,7 +51,7 @@
``` ```
┌─────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────┐
│ Agent Lifecycle │ Agent Lifecycle │
└─────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────┘
User Code User Code
@@ -83,20 +83,20 @@
┌──────────────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────┐
│ AgentLoop (runs in separate thread) │ │ AgentLoop (runs in separate thread) │
│ │
│ ┌────────────────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────────────────┐ │
│ │ 1. Emit AgentStartEvent │ │ │ │ 1. Emit AgentStartEvent │ │
│ │ 2. Emit TurnStartEvent │ │ │ │ 2. Emit TurnStartEvent │ │
│ │ 3. Process prompts (emit MessageStart/End) │ │ │ │ 3. Process prompts (emit MessageStart/End) │ │
│ │ 4. ┌──────────────────────────────────────────────┐ │ │ │ │ 4.┌────────────────────────────────────────────────┐ │
│ │ │ while true: │ │ │ │ │ │ while true: │ │
│ │ │ │ Process steering/follow-up messages │ │ │ │ │ │ │ Process steering/follow-up messages │ │
│ │ │ │ Stream assistant response (LLM call) │ │ │ │ │ │ │ Stream assistant response (LLM call) │ │
│ │ │ │ Execute tool calls (parallel/sequential) │ │ │ │ │ │ │ Execute tool calls (parallel/sequential) │ │
│ │ │ │ Emit TurnEndEvent │ │ │ │ │ │ │ Emit TurnEndEvent │ │
│ │ │ │ Check if should stop │ │ │ │ │ │ │ Check if should stop │ │
│ │ │ │ Get next steering messages │ │ │ │ │ │ │ Get next steering messages │ │
│ │ └───┴────────────────────────────────────────────┘ │ │ │ │ └───┴────────────────────────────────────────────┘ │
│ └────────────────────────────────────────────────────────┘ │ │ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────┘
@@ -125,18 +125,18 @@
│ ┌────────────────────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ agentLoop(prompts, context, config, signal, stream_fn) │ │ │ │ agentLoop(prompts, context, config, signal, stream_fn) │ │
│ └────────────────────────────────────────────────────────────┘ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ runAgentLoop(prompts, context, config, emit, signal) │ │ │ │ runAgentLoop(prompts, context, config, emit, signal) │ │
│ └────────────────────────────────────────────────────────────┘ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ runLoop() - Main Event Loop │ │ │ │ runLoop() - Main Event Loop │ │
│ └────────────────────────────────────────────────────────────┘ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
└──────────────────────────────┼─────────────────────────────────────── └──────────────────────────────┼─────────────────────────────────────┘
│ Loop Iteration │ Loop Iteration
@@ -150,8 +150,8 @@
│ │ │ (after assistant) │ │ (after stop) │ │ │ │ │ │ (after assistant) │ │ (after stop) │ │ │
│ │ └────────────────────┘ └──────────────────────┘ │ │ │ │ └────────────────────┘ └──────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ 2. Stream Assistant Response │ │ │ │ 2. Stream Assistant Response │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │
@@ -161,39 +161,39 @@
│ │ │ - Text deltas │ │ │ │ │ │ - Text deltas │ │ │
│ │ │ - Tool call deltas │ │ │ │ │ │ - Tool call deltas │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │ │ │ └────────────────────────────────────────────────────┘ │ │
│ │ │ │ │ │ │ │ │ │
│ │ ▼ │ │ │ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ Emit: MessageStartEvent, MessageUpdateEvent, │ │ │ │ │ │ Emit: MessageStartEvent, MessageUpdateEvent, │ │ │
│ │ │ MessageEndEvent │ │ │ │ │ │ MessageEndEvent │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │ │ │ └────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ 3. Execute Tool Calls │ │ │ │ 3. Execute Tool Calls │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ extract ToolCall from assistant content │ │ │ │ │ │ extract ToolCall from assistant content │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ if EXECUTION_SEQUENTIAL || has_sequential_tool: │ │ │ │ │ │ if EXECUTION_SEQUENTIAL || has_sequential_tool: │ │ │
│ │ │ executeToolCallsSequential() │ │ │ │ │ │ executeToolCallsSequential() │ │ │
│ │ │ else: │ │ │ │ │ │ else: │ │ │
│ │ │ executeToolCallsParallel() │ │ │ │ │ │ executeToolCallsParallel() │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │ │ │ └────────────────────────────────────────────────────┘ │ │
│ │ │ │ │ │ │ │ │ │
│ │ ▼ │ │ │ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ For each tool call: │ │ │ │ │ │ For each tool call: │ │ │
│ │ │ 1. before_tool_call hook │ │ │ │ │ │ 1. before_tool_call hook │ │ │
│ │ │ 2. prepareToolCall() │ │ │ │ │ │ 2. prepareToolCall() │ │ │
│ │ │ 3. execute() │ │ │ │ │ │ 3. execute() │ │ │
│ │ │ 4. after_tool_call hook │ │ │ │ │ │ 4. after_tool_call hook │ │ │
│ │ │ 5. Emit ToolExecutionStart/Update/EndEvent │ │ │ │ │ │ 5. Emit ToolExecutionStart/Update/EndEvent │ │ │
│ │ │ 6. Emit ToolResultMessage │ │ │ │ │ │ 6. Emit ToolResultMessage │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │ │ │ └────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ 4. Prepare Next Turn │ │ │ │ 4. Prepare Next Turn │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │
@@ -202,29 +202,29 @@
│ │ │ - Optional: Update context │ │ │ │ │ │ - Optional: Update context │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │ │ │ └────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────────────────────┐ │
│ │ 5. Check Termination Conditions │ │ │ │ 5. Check Termination Conditions │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ should_stop_after_turn(context) -> bool │ │ │ │ │ │ should_stop_after_turn(context) -> bool │ │ │
│ │ │ - Max turns reached? │ │ │ │ │ │ - Max turns reached? │ │ │
│ │ │ - Tool returned terminate=true? │ │ │ │ │ │ - Tool returned terminate=true? │ │ │
│ │ │ - Steering queue empty and follow-up empty? │ │ │ │ │ │ - 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 │ │ │ │ Loop continues until termination condition met │ │
│ └────────────────────────────────────────────────────────────┘ │ │ └────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
└──────────────────────────────┼─────────────────────────────────────── └──────────────────────────────┼─────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────────────┐
@@ -240,34 +240,34 @@
└─────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────┐
│ Assistant Message with Tool Calls │ Assistant Message with Tool Calls │
│ ┌─────────────────────────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────────────────┐ │
│ │ AssistantMessage: │ │ │ │ AssistantMessage: │ │
│ │ content: [ │ │ │ │ content: [ │ │
│ │ TextContent("I'll help you"), │ │ │ │ TextContent("I'll help you"), │ │
│ │ ToolCall(id="tc1", name="bash", args={...}), │ │ │ │ ToolCall(id="tc1", name="bash", args={...}), │ │
│ │ ToolCall(id="tc2", name="read", args={...}) │ │ │ │ ToolCall(id="tc2", name="read", args={...}) │ │
│ │ ] │ │ │ │ ] │ │
│ └─────────────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ │ ▼ │
└───────────────────────────────────────────────────────────────────┘ └───────────────────────────────────────────────────────────────────┘
│ executeToolCalls() │ executeToolCalls()
┌───────────────────────────────────────────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────┐
│ Determine Execution Mode │ Determine Execution Mode │
│ ┌─────────────────────────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────────────────┐ │
│ │ config.tool_execution == EXECUTION_SEQUENTIAL? │ │ │ │ config.tool_execution == EXECUTION_SEQUENTIAL? │ │
│ │ OR any tool has execution_mode == EXECUTION_SEQUENTIAL? │ │ │ │ OR any tool has execution_mode == EXECUTION_SEQUENTIAL? │ │
│ └─────────────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ┌───────────────┴───────────────┐ │ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │ ▼ ▼ │
│ ┌────────────────────────┐ ┌────────────────────────┐ │ │ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ executeSequential() │ │ executeParallel() │ │ │ │ executeSequential() │ │ executeParallel() │ │
│ └────────────────────────┘ └────────────────────────┘ │ │ └────────────────────────┘ └────────────────────────┘ │
│ │ │ │ │ │ │
└──────────────┼───────────────────────────────┼────────────────────┘ └──────────────┼───────────────────────────────┼────────────────────┘
│ │ │ │
│ │ │ │
@@ -285,7 +285,7 @@
┌───────────────────────────────────────────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────┐
│ For Each Tool Call │ For Each Tool Call │
│ ┌─────────────────────────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 1. before_tool_call hook (optional) │ │ │ │ 1. before_tool_call hook (optional) │ │
│ │ - Can block execution │ │ │ │ - Can block execution │ │
@@ -293,20 +293,20 @@
│ │ - validateToolArguments() │ │ │ │ - validateToolArguments() │ │
│ │ - prepareToolCallArguments() (optional) │ │ │ │ - prepareToolCallArguments() (optional) │ │
│ │ 3. Execute Tool: │ │ │ │ 3. Execute Tool: │ │
│ │ tool.execute(tool_call_id, args, signal, on_update) │ │ │ │ tool.execute(tool_call_id, args, signal, on_update) │ │
│ │ 4. after_tool_call hook (optional) │ │ │ │ 4. after_tool_call hook (optional) │ │
│ │ - Can modify result content │ │ │ │ - Can modify result content │ │
│ │ 5. Emit events: │ │ │ │ 5. Emit events: │ │
│ │ - ToolExecutionStartEvent │ │ │ │ - ToolExecutionStartEvent │ │
│ │ - ToolExecutionUpdateEvent (optional) │ │ │ │ - ToolExecutionUpdateEvent (optional) │ │
│ │ - ToolExecutionEndEvent │ │ │ │ - ToolExecutionEndEvent │ │
│ │ 6. Create ToolResultMessage │ │ │ │ 6. Create ToolResultMessage │ │
│ └─────────────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘ └───────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────┐
│ Tool Result Messages │ Tool Result Messages │
│ ┌─────────────────────────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ToolResultMessage: │ │ │ │ ToolResultMessage: │ │
│ │ role: "toolResult" │ │ │ │ role: "toolResult" │ │
@@ -329,47 +329,47 @@
┌───────────────────────────────────────────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────┐
│ Branch Navigation │ │ Branch Navigation │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ │ E1 │────▶│ E2 │────▶│ E3 │────▶│ E4 │────▶│ E5 │ (leaf) │ │ E1 │────▶│ E2 │────▶│ E3 │────▶│ E4 │────▶│ E5 │ (leaf) │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │ │ ▼ ▼ ▼ ▼ ▼ │
│ Message Message Compaction Message BranchSummary │ │ Message Message Compaction Message BranchSummary │
│ │
│ E3 is a Compaction Entry: │ │ E3 is a Compaction Entry: │
│ - Summary of E1, E2 │ │ - Summary of E1, E2 │
│ - first_kept_entry_id: reference to first retained message │ │ - first_kept_entry_id: reference to first retained message │
│ - tokens_before: context size before compaction │ │ - tokens_before: context size before compaction │
│ │
│ E5 is a BranchSummary Entry: │ │ E5 is a BranchSummary Entry: │
│ - Summary of branch from from_id │ │ - Summary of branch from from_id │
│ - Represents a fork point in conversation history │ │ - Represents a fork point in conversation history │
│ │
└───────────────────────────────────────────────────────────────────┘ └───────────────────────────────────────────────────────────────────┘
│ Session.moveTo() │ Session.moveTo()
┌───────────────────────────────────────────────────────────────────┐ ┌───────────────────────────────────────────────────────────────────┐
│ Forking & Branching │ │ Forking & Branching │
│ │
│ Current branch: │ │ Current branch: │
│ ┌─────┐ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ E1 │────▶│ E2 │────▶│ E3 │ │ │ │ E1 │────▶│ E2 │────▶│ E3 │ │
│ └─────┘ └─────┘ └─────┘ │ │ └─────┘ └─────┘ └─────┘ │
│ moveTo(E2) │ │ │ moveTo(E2)
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │ (new branch) │ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │ (new branch) │
│ └─────┘ └─────┘ └─────┘ └─────┘ │ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │ │ │ │ │
│ │ create BranchSummary │ │ │ create BranchSummary │
│ ▼ │ │ ▼ │
│ ┌─────┐ ┌─────┐ │
│ │ E5 │ (branch summary) │ E5 │ (branch summary) │
│ └─────┘ └─────┘ │
│ │
└───────────────────────────────────────────────────────────────────┘ └───────────────────────────────────────────────────────────────────┘
``` ```
@@ -377,7 +377,7 @@
``` ```
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ Component Relationships │ Component Relationships │
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
User Code User Code
@@ -405,87 +405,295 @@
└── Manages ──► PromptTemplates └── 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() convertToLlm() - Type Transformation Pipeline
- normalizeInput()
└──────────────────────┘ │ 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 LLM API Call (stream_fn)
- transform_context
└──────────────────────┘ │ 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) executeToolCalls() - Tool Processing
- Context: Message[]
└──────────────────────┘ │ Extract: filter(c -> c isa ToolCall, assistant.content) │
│ Output: ExecutedToolCallBatch │
│ • messages: Vector{ToolResultMessage} │
│ • terminate: Bool │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────┐ ┌──────────────────────────────────────────────────────────────
Response (Streaming) ToolResultMessage (for each ToolCall)
- Text deltas
- Tool call deltas • 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 Updated AgentState.messages (AgentMessage[])
- content: Message[]
└──────────────────────┘ │ 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 Persisted Data (JSON format)
│ - Extract ToolCalls - Each entry has: id, parent_id, timestamp, type
│ - Execute tools - MessageEntry contains full AgentMessage
└──────────────────────┘ └──────────────────────────────────────────────────────────────
┌──────────────────────┐
│ ToolResultMessage[] │
└──────────────────────┘
┌──────────────────────┐
│ AgentState.messages │ ──► Tool results appended
└──────────────────────┘
│ (Loop back to LLM or end)
┌──────────────────────┐
│ Session Storage │
│ - JSONL format │
│ - Tree entries │
└──────────────────────┘
``` ```
### 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 ## Summary
The AgentCore.jl architecture follows a clean separation of concerns: 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 3. **AgentLoop** - Core LLM interaction loop
4. **Session** - Conversation history management 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. 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 # Create agent with options
agent = Agent(Dict{Symbol, Any}( agent = Agent(Dict{Symbol, Any}(
:systemPrompt => "You are a helpful assistant", :systemPrompt => "You are a helpful assistant",
:model => Model(...), :model => Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
:thinkingLevel => THINKING_MEDIUM, :thinkingLevel => THINKING_OFF,
:tools => [bash_tool, read_tool], :tools => [bash_tool, read_tool],
:steeringMode => QUEUE_ONE_AT_A_TIME, :steeringMode => QUEUE_ONE_AT_A_TIME,
:followUpMode => QUEUE_ONE_AT_A_TIME, :followUpMode => QUEUE_ONE_AT_A_TIME,
@@ -243,16 +243,35 @@ followUp(agent, UserMessage(...))
```julia ```julia
# Transform messages before sending to LLM # Transform messages before sending to LLM
function myConvertToLlm(messages::Vector{AgentMessage}) function myConvertToLlm(messages::Vector{AgentMessage})::Vector{Message}
return filter( result::Vector{Message} = Message[]
m -> m.role in ["user", "assistant", "toolResult"], for m in messages
messages converted = convertToLlmMessage(m)
) if !isnothing(converted)
push!(result, converted)
end
end
return result
end end
agent = Agent(Dict(:convertToLlm => myConvertToLlm)) 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 #### transform_context
```julia ```julia
@@ -271,7 +290,7 @@ agent = Agent(Dict(:transformContext => myTransformContext))
# Hook before tool execution # Hook before tool execution
function myBeforeToolCall(context, signal) function myBeforeToolCall(context, signal)
println("About to execute: $(context.tool_call.name)") 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 end
agent = Agent(Dict(:beforeToolCall => myBeforeToolCall)) agent = Agent(Dict(:beforeToolCall => myBeforeToolCall))
@@ -284,8 +303,11 @@ agent = Agent(Dict(:beforeToolCall => myBeforeToolCall))
function myAfterToolCall(context, signal) function myAfterToolCall(context, signal)
# Can modify tool result # Can modify tool result
return AfterToolCallResult( return AfterToolCallResult(
content = context.result.content, context.result.content,
terminate = context.result.terminate context.result.details,
nothing,
nothing,
context.result.terminate
) )
end end
@@ -300,9 +322,9 @@ function myPrepareNextTurn(context, signal)
# context: PrepareNextTurnContext # context: PrepareNextTurnContext
# Returns AgentLoopTurnUpdate or nothing # Returns AgentLoopTurnUpdate or nothing
return AgentLoopTurnUpdate( return AgentLoopTurnUpdate(
context = context.context, context.context, # context
model = context.context.model, # Can change model context.context.model, # model - can change
thinking_level = THINKING_HIGH # Can change thinking level THINKING_HIGH # thinking_level - can change
) )
end end
@@ -315,11 +337,11 @@ agent = Agent(Dict(:prepareNextTurn => myPrepareNextTurn))
# Check if agent is busy # Check if agent is busy
if !isnothing(agent.active_run) if !isnothing(agent.active_run)
# Agent is processing # Agent is processing
abort(agent) # Abort current run abort(agent) # Abort current run (NOTE: implementation is a TODO stub)
end end
# Wait for completion # Wait for completion
wait_for_idle(agent) # Returns Promise waitForIdle(agent) # Returns Promise
``` ```
## Complete Example ## Complete Example
@@ -348,7 +370,7 @@ end
prompt(agent, "What's in the current directory?") prompt(agent, "What's in the current directory?")
# 4. Wait for completion # 4. Wait for completion
wait_for_idle(agent) waitForIdle(agent)
# 5. Check final state # 5. Check final state
state = get_state(agent) state = get_state(agent)
@@ -356,7 +378,7 @@ println("Total messages: $(length(state.messages))")
# 6. Continue with steering # 6. Continue with steering
steer(agent, UserMessage(...)) steer(agent, UserMessage(...))
wait_for_idle(agent) waitForIdle(agent)
# 7. Clean up # 7. Clean up
unsubscribe() # Stop listening unsubscribe() # Stop listening
@@ -403,9 +425,9 @@ Scenario: User sends message, agent responds with tool calls
┌────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────┐
│ Time 2: User queues steering message │ │ Time 2: User queues steering message │
│ ┌──────────────────┐ │ │ ┌──────────────────┐ │
│ │ steer(msg2) │ ──► steering_queue.push(msg2) │ │ │ steer(msg2) │ ──► steering_queue.push(msg2)
│ └──────────────────┘ │ │ └──────────────────┘ │
│ │
│ (msg2 not processed yet!) │ │ (msg2 not processed yet!) │
└────────────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────────┘
@@ -413,9 +435,9 @@ Scenario: User sends message, agent responds with tool calls
┌────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────┐
│ Time 3: Tool execution │ │ Time 3: Tool execution │
│ ┌──────────────────────────────────────────────────────┐ │ │ ┌──────────────────────────────────────────────────────┐ │
│ │ Execute bash tool... │ │ │ │ Execute bash tool... │ │
│ │ Execute read tool... │ │ │ │ Execute read tool... │ │
│ │ Emit ToolResultMessage[] │ │ │ │ Emit ToolResultMessage[] │ │
│ └──────────────────────────────────────────────────────┘ │ │ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────────┘
+559 -244
View File
@@ -27,59 +27,137 @@ agentLoopContinue()
└─ Returns: EventStream └─ Returns: EventStream
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
Internal Flow Data Flow with Type Transformations
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. runAgentLoop() ── Entry point for new conversation │ │ 1. runAgentLoop() ── Entry point for new conversation
- Creates copy of prompts Input: prompts::Vector{AgentMessage}
- Appends prompts to context.messages context::AgentContext (system_prompt, messages, tools)
- Emits AgentStartEvent config::AgentLoopConfig
- Emits TurnStartEvent Output: new_messages::Vector{AgentMessage} (appended prompts + turns)
- Emits MessageStart/End for each prompt
│ - Calls runLoop() │ - 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: │ │ │ │ while true (outer loop: follow-up messages) │ │
│ │ 1. Get steering/follow-up messages (if any) │ │ │ │ has_more_tool_calls = true │ │
│ │ 2. Emit messages as UserMessage │ │ │ │ while has_more_tool_calls || !isempty(pending_messages) │ │
│ │ 3. streamAssistantResponse() │ │ │ │ 1. Emit TurnStartEvent (on subsequent turns) │ │
│ │ 4. Execute tool calls (sequential or parallel) │ │ │ │ 2. If pending_messages: emit MessageStart/End, drain queue │ │
│ │ 5. Emit TurnEndEvent │ │ │ │ 3. streamAssistantResponse() │ │
│ │ 6. prepare_next_turn (optional) │ │ │ │ 4. If error/aborted: emit TurnEnd, AgentEnd, return │ │
│ │ 7. should_stop_after_turn? (check termination) │ │ │ │ 5. Execute tool calls (sequential or parallel) │ │
│ │ 8. Loop continues if not terminated │ │ │ │ 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 │ │ 3. streamAssistantResponse() ── LLM interaction
- transform_context (optional) Input: context::AgentContext
- convert_to_llm (transform to Message[]) config::AgentLoopConfig
- Call stream_fn (LLM API) Output: message::AssistantMessage
- Stream response deltas
- Emit MessageStart/Update/End events Data Transformations:
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. executeToolCalls() ── Tool execution │
│ ┌────────────────────────────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ if EXECUTION_SEQUENTIAL || has_sequential_tool: │ │ │ │ Step 1: transform_context (optional) │ │
│ │ executeToolCallsSequential() │ │ │ │ Input: context.messages::Vector{AgentMessage} │ │
│ │ else: │ │ │ │ Output: messages::Vector{AgentMessage} (transformed) │ │
│ │ executeToolCallsParallel() │ │ │ │ │ │
│ │ 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_api_key::Union{Function, Nothing}
get_steering_messages::Union{Function, Nothing} get_steering_messages::Union{Function, Nothing}
get_follow_up_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 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 ## Main Functions
### agentLoop() ### agentLoop()
@@ -161,11 +253,13 @@ function runAgentLoop(
**Purpose**: Execute agent loop with initial prompts **Purpose**: Execute agent loop with initial prompts
**Flow**: **Flow**:
1. Copy prompts to new_messages 1. Copy prompts to `new_messages`
2. Append prompts to context.messages 2. Create `current_context` with prompts appended to `context.messages`
3. Emit AgentStartEvent 3. Emit `AgentStartEvent`
4. For each prompt: emit MessageStartEvent, MessageEndEvent 4. Emit `TurnStartEvent`
5. Call runLoop() 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 ### runLoop() - The Heart of AgentLoop
@@ -180,109 +274,124 @@ function runLoop(
)::Nothing )::Nothing
``` ```
**Main Loop**: **Main Loop** (simplified — shows structure; actual code has type annotations):
```julia ```julia
current_context = initial_context current_context = initial_context
config = initial_config config = initial_config
first_turn = true first_turn = true
pending_messages = get_steering_messages() pending_messages = get_steering_messages(config)
while true while true
# Process steering/follow-up messages has_more_tool_calls = true
while !isempty(pending_messages)
# Inner loop: process pending messages AND/OR tool results
while has_more_tool_calls || !isempty(pending_messages)
if !first_turn if !first_turn
emit(TurnStartEvent()) emit(TurnStartEvent())
else else
first_turn = false first_turn = false
end end
# Emit pending messages # Emit pending messages (steering / follow-up)
for message in pending_messages if !isempty(pending_messages)
emit(MessageStartEvent(message)) for message in pending_messages
emit(MessageEndEvent(message)) emit(MessageStartEvent(message))
push!(current_context.messages, message) emit(MessageEndEvent(message))
push!(new_messages, message) push!(current_context.messages, message)
push!(new_messages, message)
end
pending_messages = AgentMessage[]
end end
pending_messages = [] # Stream assistant response
end message = streamAssistantResponse(
current_context, config, signal, emit, stream_function
# Stream assistant response )
message = streamAssistantResponse( push!(new_messages, message)
current_context,
config, # Early exit on error/abort
signal, if message.stop_reason in ("error", "aborted")
emit, emit(TurnEndEvent(message, ToolResultMessage[]))
stream_function, emit(AgentEndEvent(new_messages))
) return
push!(new_messages, message) end
# Check for errors # Execute tool calls (if any)
if message.stop_reason in ("error", "aborted") tool_calls = filter(c -> c isa ToolCall, message.content)
emit(TurnEndEvent(message, [])) tool_results = ToolResultMessage[]
emit(AgentEndEvent(new_messages)) has_more_tool_calls = false
return if !isempty(tool_calls)
end executed_batch = if message.stop_reason == "length"
failToolCallsFromTruncatedMessage(tool_calls, emit)
# Execute tool calls else
tool_calls = filter(c -> c isa ToolCall, message.content) executeToolCalls(
tool_results = [] current_context, message, config, signal, emit
has_more_tool_calls = false )
end
if !isempty(tool_calls) append!(tool_results, executed_batch.messages)
executed_batch = if message.stop_reason == "length" has_more_tool_calls = !executed_batch.terminate
failToolCallsFromTruncatedMessage(tool_calls, emit) for result in tool_results
else push!(current_context.messages, result)
executeToolCalls( push!(new_messages, result)
current_context, end
message, end
config,
signal, emit(TurnEndEvent(message, tool_results))
emit,
# 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 end
append!(tool_results, executed_batch.messages)
has_more_tool_calls = !executed_batch.terminate # Check termination
if should_stop_after_turn(config, next_turn_context)
for result in tool_results emit(AgentEndEvent(new_messages))
push!(current_context.messages, result) return
push!(new_messages, result)
end end
# Get next steering messages
pending_messages = get_steering_messages(config)
end end
emit(TurnEndEvent(message, tool_results)) # Check follow-up messages (processed only after all tool calls complete)
follow_up_messages = get_follow_up_messages(config)
# 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()
if !isempty(follow_up_messages) if !isempty(follow_up_messages)
pending_messages = follow_up_messages pending_messages = follow_up_messages
continue continue
end end
break break
end end
@@ -301,17 +410,44 @@ function streamAssistantResponse(
)::AssistantMessage )::AssistantMessage
``` ```
**Flow**: **Data Flow**:
1. Get messages from context
2. Apply transform_context (optional) ```
3. Convert to LLM messages with convert_to_llm Input: context.messages::Vector{AgentMessage}
4. Create Context object
5. Resolve API key [transform_context] (optional hook)
6. Call stream_fn with model, context, and config
7. Stream events: messages::Vector{AgentMessage}
- "start" → MessageStartEvent
- "text_start", "text_delta", "text_end" → MessageUpdateEvent [convert_to_llm] - Type transformation pipeline
- "done", "error" → MessageEndEvent
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() ### executeToolCalls()
@@ -325,22 +461,51 @@ function executeToolCalls(
)::ExecutedToolCallBatch )::ExecutedToolCallBatch
``` ```
**Logic**: **Data Flow**:
```julia
tool_calls = filter(c -> c isa ToolCall, assistant_message.content)
# Check if any tool requires sequential execution ```
has_sequential = any(tc -> begin Input: assistant_message::AssistantMessage
tool = findfirst(t -> t.name == tc.name, current_context.tools) - content::Vector{MessageContent}
!isnothing(tool) && tool.execution_mode == EXECUTION_SEQUENTIAL └─ Contains ToolCall[] and/or TextContent[]
end, tool_calls)
filter(c -> c isa ToolCall, assistant_message.content)
# Determine execution mode
if config.tool_execution == EXECUTION_SEQUENTIAL || has_sequential tool_calls::Vector{ToolCall}
executeToolCallsSequential(...) - type: "tool"
else - id::String
executeToolCallsParallel(...) - name::String
end - 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() ### executeToolCallsSequential()
@@ -400,13 +565,41 @@ function prepareToolCall(
)::Union{PreparedToolCall, ImmediateToolCallOutcome} )::Union{PreparedToolCall, ImmediateToolCallOutcome}
``` ```
**Flow**: **Data Flow**:
1. Find tool by name
2. If not found → ImmediateToolCallOutcome (error) ```
3. before_tool_call hook (optional) Input: tool_call::ToolCall
4. prepareToolCallArguments() (optional) - id::String
5. validateToolArguments() - name::String
6. Return PreparedToolCall - 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() ### executePreparedToolCall()
@@ -418,11 +611,35 @@ function executePreparedToolCall(
)::ExecutedToolCallOutcome )::ExecutedToolCallOutcome
``` ```
**Flow**: **Data Flow**:
1. Call tool.execute(id, args, signal, on_update)
2. Collect update events (if any) ```
3. Wait for all update events Input: prepared::PreparedToolCall
4. Return ExecutedToolCallOutcome(result) - 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() ### finalizeExecutedToolCall()
@@ -437,9 +654,44 @@ function finalizeExecutedToolCall(
)::FinalizedToolCallOutcome )::FinalizedToolCallOutcome
``` ```
**Flow**: **Data Flow**:
1. after_tool_call hook (optional)
2. Return FinalizedToolCallOutcome ```
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() ### createToolResultMessage()
@@ -449,19 +701,39 @@ function createToolResultMessage(
)::ToolResultMessage )::ToolResultMessage
``` ```
**Creates**: **Data Flow**:
```julia
ToolResultMessage( ```
"toolResult", Input: finalized::FinalizedToolCallOutcome
finalized.tool_call.id, - tool_call::ToolCall
finalized.tool_call.name, - result::AgentToolResultMutable
finalized.result.content, - content::Vector{MessageContent}
finalized.result.details, - details::Any
finalized.result.usage, - usage::Union{Usage, Nothing}
finalized.result.added_tool_names, - added_tool_names::Union{Vector{String}, Nothing}
finalized.is_error, - is_error::Bool
timestamp,
) 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 ## Execution Modes
@@ -470,45 +742,39 @@ ToolResultMessage(
``` ```
┌─────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────┐
│ Sequential Execution Flow │ Sequential Execution Flow (Strict Order)
└─────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────┘
┌──────┐ TC1 TC2 TC3
│ TC1 │ ──► prepareToolCall() │ │ │
└──────┘ │ ▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
┌──────────────┐ │ prepareToolCall()│───▶│ prepareToolCall()│───▶│ prepareToolCall()│
│ execute() │ ──► Wait for completion └──────────────────┘ └──────────────────┘ └──────────────────┘
└──────────────┘ │ │
├───────────── createToolResultMessage() ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
execute() execute() execute()
▼ ▼ │ (blocking) │ │ (blocking) │ │ (blocking) │
────────────── ────────── └──────────────────┘ └────────────────── └──────────────────
TC2 ──► │ Result1
└──────┘ └──────────┘ ▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ finalize() │ │ finalize() │ │ finalize() │
┌──────────────┐ │ + emit events │ │ + emit events │ │ + emit events │
│ execute() │ └──────────────────┘ └──────────────────┘ └──────────────────┘
└──────────────┘ │ │ │
▼ ▼ ▼
Result1 Result2 Result3
┌──────────────┐ │ │ │
│ TC3 │ ──► │ └───────────────────────┴───────────────────────┘
└──────┘
───── createToolResultMessage() ────────────────────────┐
│ ExecutedToolCallBatch
▼ ▼ │ (Result1, Result2, │
┌──────────────┐ ┌──────────┐ │ Result3, terminate) │
│ execute() │ │ │ Result2 │ └────────────────────────┘
└──────────────┘ └──────────┘
┌──────────┐
│ Result3 │
└──────────┘
``` ```
### Parallel Execution ### Parallel Execution
@@ -520,13 +786,13 @@ ToolResultMessage(
┌──────┐ ┌──────┐
│ TC1 │ ──► prepareToolCall() ──► create closure ──► ┐ │ TC1 │ ──► prepareToolCall() ──► create closure ──► ┐
└──────┘ └──────┘ │
┌──────┐ ┌──────┐ │
│ TC2 │ ──► prepareToolCall() ──► create closure ──► ├─► All closures queued │ TC2 │ ──► prepareToolCall() ──► create closure ──► ├─► All closures queued
└──────┘ └──────┘ │
┌──────┐ ┌──────┐ │
│ TC3 │ ──► prepareToolCall() ──► create closure ──► ┘ │ TC3 │ ──► prepareToolCall() ──► create closure ──► ┘
└──────┘ └──────┘
@@ -650,50 +916,85 @@ AgentStartEvent
### 1. Message Transformation Pipeline ### 1. Message Transformation Pipeline
``` ```
AgentMessage[] (internal) Vector{AgentMessage} (internal conversation history)
transform_context() ├─ transform_context() (optional hook)
│ Input: Vector{AgentMessage}
AgentMessage[] (transformed) │ Output: Vector{AgentMessage} (transformed)
convert_to_llm() └─ convert_to_llm()
Message[] (LLM API) │ 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() ├─ prepareToolCall()
├─ validate arguments Input: tool_call::ToolCall
└─ prepare arguments (optional) Output: Union{PreparedToolCall, ImmediateToolCallOutcome}
│ • PreparedToolCall (kind, tool_call, tool, args)
│ • ImmediateToolCallOutcome (immediate, result, is_error)
├─ execute() ├─ executePreparedToolCall() (if prepared)
├─ Immediate: return result Input: PreparedToolCall
└─ Prepared: async execution 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() └─ 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 ### 3. Turn Termination
```julia ```julia
# Turn ends when: # The outer while-true loop exits when:
# 1. No more pending messages # 1. No pending messages AND no tool results to reprocess (inner loop ends)
# 2. No more tool calls to execute # 2. No follow-up messages to queue
# 3. should_stop_after_turn() returns true # 3. should_stop_after_turn() returns true (checked after each tool-call batch)
# Reasons to stop: # Termination conditions:
# - Max turns reached # - message.stop_reason in ("error", "aborted") → immediate return
# - Tool returned terminate=true # - should_stop_after_turn() hook returns true → return AgentEndEvent
# - Error or abort # - tool result batch has terminate=true → has_more_tool_calls = false, exit inner loop
# - Steering/follow-up queues empty # - No pending messages, no follow-up messages → break outer loop
``` ```
## Best Practices ## Best Practices
@@ -713,6 +1014,12 @@ using AgentCore
config = AgentLoopConfig( config = AgentLoopConfig(
model = my_model, model = my_model,
reasoning = THINKING_MEDIUM, 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, tool_execution = EXECUTION_PARALLEL,
before_tool_call = myBeforeToolCallHook, before_tool_call = myBeforeToolCallHook,
after_tool_call = myAfterToolCallHook, after_tool_call = myAfterToolCallHook,
@@ -722,6 +1029,14 @@ config = AgentLoopConfig(
get_api_key = myGetApiKey, get_api_key = myGetApiKey,
get_steering_messages = myGetSteeringMessages, get_steering_messages = myGetSteeringMessages,
get_follow_up_messages = myGetFollowUpMessages, 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 # Start agent loop
+372 -65
View File
@@ -19,85 +19,131 @@
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ ToolExecutionMode (Enum) │ │ ToolExecutionMode (Enum)
│ - EXECUTION_SEQUENTIAL (Tools run one at a time) │ │ - EXECUTION_SEQUENTIAL (Tools run one at a time)
│ - EXECUTION_PARALLEL (Tools run concurrently) │ │ - EXECUTION_PARALLEL (Tools run concurrently)
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ QueueMode (Enum) │ │ QueueMode (Enum)
│ - QUEUE_ALL (Drain all messages at once) │ │ - QUEUE_ALL (Drain all messages at once)
│ - QUEUE_ONE_AT_A_TIME (Process one message at a time) │ │ - QUEUE_ONE_AT_A_TIME (Process one message at a time)
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ MessageContent (Abstract Type) │ │ MessageContent (Abstract Type)
│ ├── TextContent (String) │ │ ├── TextContent (String)
│ └── ImageContent (data::String, mime_type::String) │ │ └── ImageContent (data::String, mime_type::String)
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ Message (Abstract Type) │ │ Message (Abstract Type)
│ ├── UserMessage │ │ ├── UserMessage
│ │ └─ role: "user", content: Message[], timestamp: Int64 │ │ │ └─ role: "user", content: Message[], timestamp: Int64
│ ├── AssistantMessage │ │ ├── AssistantMessage
│ │ └─ role: "assistant", content: Message[], api, provider, model, │ │ │ └─ role: "assistant", content: Message[], api, provider, model,
│ │ usage: Usage, stop_reason, error_message, timestamp │ │ │ usage: Usage, stop_reason, error_message, timestamp
│ └── ToolResultMessage │ │ └── ToolResultMessage
│ └─ role: "toolResult", tool_call_id, tool_name, content, details, │ │ └─ role: "toolResult", tool_call_id, tool_name, content, details,
│ usage, added_tool_names, is_error, timestamp │ │ usage, added_tool_names, is_error, timestamp
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ AgentMessage (Abstract Type) │ │ AgentMessage (Abstract Type)
│ └─ Union of all message types above + custom types │ │ └─ Union of all message types above + custom types
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ AgentTool │ │ AgentTool
│ - name: String │ │ - name: String
│ - label: String │ │ - label: String
│ - description: String │ │ - description: String
│ - parameters: Any │ │ - parameters: Any
│ - execute: Function │ │ - execute: Function
│ - prepare_arguments: Union{Function, Nothing} │ │ - prepare_arguments: Union{Function, Nothing}
│ - execution_mode: Union{ToolExecutionMode, Nothing} │ │ - execution_mode: Union{ToolExecutionMode, Nothing}
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ AgentContext │ │ AgentContext
│ - system_prompt: String │ │ - system_prompt: String
│ - messages: Vector{AgentMessage} │ │ - messages: Vector{AgentMessage}
│ - tools: Union{Vector{AgentTool}, Nothing} │ │ - tools: Union{Vector{AgentTool}, Nothing}
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ AgentEvent (Abstract Type) │ │ AgentEvent (Abstract Type)
│ ├── AgentStartEvent / AgentEndEvent │ │ ├── AgentStartEvent / AgentEndEvent
│ ├── TurnStartEvent / TurnEndEvent │ │ ├── TurnStartEvent / TurnEndEvent
│ ├── MessageStartEvent / MessageEndEvent │ │ ├── MessageStartEvent / MessageEndEvent
│ ├── MessageUpdateEvent │ │ ├── MessageUpdateEvent
│ ├── ToolExecutionStartEvent / ToolExecutionEndEvent │ │ ├── ToolExecutionStartEvent / ToolExecutionEndEvent
│ └── ToolExecutionUpdateEvent │ │ └── ToolExecutionUpdateEvent
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ Usage & ModelCost │ │ Usage & ModelCost
│ Usage: input, output, cache_read, cache_write, total_tokens, cost │ │ Usage: input, output, cache_read, cache_write, total_tokens, cost
│ ModelCost: input, output, cache_read, cache_write (all Float64) │ │ ModelCost: input, output, cache_read, cache_write (all Float64)
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ Model │ │ Model
│ - id, name, api, provider, base_url, reasoning: Bool │ │ - id, name, api, provider, base_url, reasoning: Bool
│ - input: Vector{String} │ │ - input: Vector{String}
│ - cost: ModelCost │ │ - cost: ModelCost
│ - context_window, max_tokens: Int64 │ │ - context_window, max_tokens: Int64
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
``` ```
## Message Types ## 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 ### UserMessage
```julia ```julia
@@ -108,6 +154,16 @@ struct UserMessage <: Message
end end
``` ```
**Usage**:
```julia
# Simple text message
UserMessage(
"user",
[TextContent("Hello, how are you?")],
Int64(Dates.now(Dates.UTC).datetime)
)
```
**Usage**: **Usage**:
```julia ```julia
# Simple text message # Simple text message
@@ -192,6 +248,9 @@ struct ToolResultMessage <: Message
is_error::Bool # True if tool execution failed is_error::Bool # True if tool execution failed
timestamp::Timestamp timestamp::Timestamp
end end
# Note: AgentToolResult{T} (types.jl) - generic result type with type param T
# AgentToolResultMutable (agent_loop.jl) - mutable variant used internally
``` ```
**Usage**: **Usage**:
@@ -232,6 +291,8 @@ end
- `prepare_arguments`: Optional preprocessing - `prepare_arguments`: Optional preprocessing
- `execution_mode`: Sequential or parallel - `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 ### Tool Execution Function Signature
```julia ```julia
@@ -241,12 +302,12 @@ execute::Function(
signal::Union{Any, Nothing}, # Abort signal signal::Union{Any, Nothing}, # Abort signal
on_update::Function, # Callback for streaming updates on_update::Function, # Callback for streaming updates
context::Any, # Tool context context::Any, # Tool context
)::AgentToolResult )::AgentToolResult{T}
``` ```
**Returns**: **Returns** (`AgentToolResult{T}` from `types.jl`):
```julia ```julia
AgentToolResult( AgentToolResult{T}(
content::Vector{MessageContent}, # Result content content::Vector{MessageContent}, # Result content
details::T, # Tool-specific details details::T, # Tool-specific details
usage::Union{Usage, Nothing}, # Usage statistics 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 ## AgentContext
```julia ```julia
@@ -476,6 +541,32 @@ mutable struct BranchSummaryMessage
end 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 ## AgentState
```julia ```julia
@@ -496,9 +587,9 @@ end
**Note**: AgentState is mutable and used internally by Agent **Note**: AgentState is mutable and used internally by Agent
## Key Conversion Functions ## Message Transformation Pipeline
### convertToLlm() ### convertToLlm() - AgentMessage[] → Message[]
```julia ```julia
function convertToLlm(messages::Vector{AgentMessage})::Vector{Message} function convertToLlm(messages::Vector{AgentMessage})::Vector{Message}
@@ -515,26 +606,57 @@ function convertToLlm(messages::Vector{AgentMessage})::Vector{Message}
end 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**: **Example**:
```julia ```julia
# Input: AgentMessage[] # Input: Vector{AgentMessage}
[ [
UserMessage(...), UserMessage("user", [TextContent("Hello")], 1234567890),
AssistantMessage(...), AssistantMessage("assistant", [
ToolResultMessage(...), TextContent("Hi there!"),
BashExecutionMessage(...), # Will be converted to UserMessage ToolCall("bash", "call_123", "bash", Dict("command" => "ls"), nothing)
CompactionSummaryMessage(...), # Will be converted to UserMessage ], "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(...), UserMessage("user", [TextContent("Hello")], 1234567890),
AssistantMessage(...), AssistantMessage("assistant", [
ToolResultMessage(...), TextContent("Hi there!"),
UserMessage(...), # Converted from BashExecutionMessage ToolCall(...)
UserMessage(...), # Converted from CompactionSummaryMessage ], "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) return UserMessage("user", [TextContent(text)], m.timestamp)
end 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) function convertToLlmMessage(m::BranchSummaryMessage)
text = BRANCH_SUMMARY_PREFIX * m.summary * BRANCH_SUMMARY_SUFFIX text = BRANCH_SUMMARY_PREFIX * m.summary * BRANCH_SUMMARY_SUFFIX
return UserMessage("user", [TextContent(text)], m.timestamp) return UserMessage("user", [TextContent(text)], m.timestamp)
@@ -571,6 +702,182 @@ function convertToLlmMessage(m::ToolResultMessage)
end 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 ## Summary
The type system in AgentCore.jl provides: The type system in AgentCore.jl provides:
+347 -192
View File
@@ -1,6 +1,6 @@
# AgentCore.jl - Session Management Deep Dive # AgentCore.jl - Session Management Deep Dive
## Session Architecture ## Session Architecture with Data Flow
``` ```
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
@@ -9,45 +9,86 @@
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ Session = Tree of Entries │ │ Session = Tree of Entries │
│ │
│ Each entry represents a change in conversation state │ │ Each entry represents a change in conversation state │
│ │
│ Branch Navigation: │ │ Branch Navigation: │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ E1 │────▶│ E2 │────▶│ E3 │────▶│ E4 │────▶│ E5 │ (current leaf) │ │ E1 │────▶│ E2 │────▶│ E3 │────▶│ E4 │────▶│ E5 │ (current leaf) │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │ │ │ │ │ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │ │ ▼ ▼ ▼ ▼ ▼
│ Message Message Compaction Message BranchSummary │ │ Message Message Compaction Message BranchSummary
│ │
│ Data Flow: │
│ AgentMessage[] (AgentState.messages) │
│ │ │
│ └─► appendMessage() → MessageEntry │
│ └─► storage.appendEntry() → JSONL file │
│ │
│ To navigate to E2 (fork point): │ │ To navigate to E2 (fork point): │
Session.moveTo(E2) │ session.moveTo(E2) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │────▶│ E5' │ (new branch) │ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │────▶│ E5' │ (new branch) │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘
│ │ │ │ │ │ │
│ │ ▼ create BranchSummary │ │ │ ▼ 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 ## Entry Types
All entry types extend `abstract type SessionTreeEntry end` and embed a
`base::SessionTreeEntryBase` struct containing `type`, `id`, `parent_id`, and `timestamp`.
```julia ```julia
abstract type SessionTreeEntry end abstract type SessionTreeEntry end
struct SessionTreeEntryBase
type::String
id::String
parent_id::Union{String, Nothing}
timestamp::String
end
``` ```
### 1. MessageEntry ### 1. MessageEntry
```julia ```julia
struct MessageEntry <: SessionTreeEntry struct MessageEntry <: SessionTreeEntry
type::String # "message" base::SessionTreeEntryBase
id::String # Unique entry ID message::AgentMessage
parent_id::Union{String, Nothing}
timestamp::String # ISO 8601 timestamp
message::AgentMessage # The actual message
end end
``` ```
@@ -57,11 +98,8 @@ end
```julia ```julia
struct ThinkingLevelChangeEntry <: SessionTreeEntry struct ThinkingLevelChangeEntry <: SessionTreeEntry
type::String # "thinking_level_change" base::SessionTreeEntryBase
id::String thinking_level::String
parent_id::Union{String, Nothing}
timestamp::String
thinking_level::String # "off", "minimal", "low", "medium", etc.
end end
``` ```
@@ -71,12 +109,9 @@ end
```julia ```julia
struct ModelChangeEntry <: SessionTreeEntry struct ModelChangeEntry <: SessionTreeEntry
type::String # "model_change" base::SessionTreeEntryBase
id::String provider::String
parent_id::Union{String, Nothing} model_id::String
timestamp::String
provider::String # "openai", "anthropic", etc.
model_id::String # Model identifier
end end
``` ```
@@ -86,10 +121,7 @@ end
```julia ```julia
struct ActiveToolsChangeEntry <: SessionTreeEntry struct ActiveToolsChangeEntry <: SessionTreeEntry
type::String # "active_tools_change" base::SessionTreeEntryBase
id::String
parent_id::Union{String, Nothing}
timestamp::String
active_tool_names::Vector{String} active_tool_names::Vector{String}
end end
``` ```
@@ -99,18 +131,15 @@ end
### 5. CompactionEntry ### 5. CompactionEntry
```julia ```julia
struct CompactionEntry <: SessionTreeEntry struct CompactionEntry{T} <: SessionTreeEntry
type::String # "compaction" base::SessionTreeEntryBase
id::String summary::String
parent_id::Union{String, Nothing}
timestamp::String
summary::String # Summary of compacted history
first_kept_entry_id::Union{String, Nothing} first_kept_entry_id::Union{String, Nothing}
tokens_before::Int64 # Context size before compaction tokens_before::Int64
retained_tail::Union{Vector{AgentMessage}, Nothing} retained_tail::Union{Vector{AgentMessage}, Nothing}
details::Union{Any, Nothing} details::Union{T, Nothing}
usage::Union{Usage, Nothing} usage::Union{Usage, Nothing}
from_hook::Bool # Whether triggered by hook from_hook::Bool
end end
``` ```
@@ -125,14 +154,11 @@ end
### 6. BranchSummaryEntry ### 6. BranchSummaryEntry
```julia ```julia
struct BranchSummaryEntry <: SessionTreeEntry struct BranchSummaryEntry{T} <: SessionTreeEntry
type::String # "branch_summary" base::SessionTreeEntryBase
id::String from_id::String
parent_id::Union{String, Nothing} summary::String
timestamp::String details::Union{T, Nothing}
from_id::String # Branch point entry ID
summary::String # Summary of branch history
details::Union{Any, Nothing}
usage::Union{Usage, Nothing} usage::Union{Usage, Nothing}
from_hook::Bool from_hook::Bool
end end
@@ -143,13 +169,10 @@ end
### 7. CustomEntry ### 7. CustomEntry
```julia ```julia
struct CustomEntry <: SessionTreeEntry struct CustomEntry{T} <: SessionTreeEntry
type::String # Custom type base::SessionTreeEntryBase
id::String
parent_id::Union{String, Nothing}
timestamp::String
custom_type::String custom_type::String
data::Union{Any, Nothing} data::Union{T, Nothing}
end end
``` ```
@@ -158,14 +181,11 @@ end
### 8. CustomMessageEntry ### 8. CustomMessageEntry
```julia ```julia
struct CustomMessageEntry <: SessionTreeEntry struct CustomMessageEntry{T} <: SessionTreeEntry
type::String base::SessionTreeEntryBase
id::String
parent_id::Union{String, Nothing}
timestamp::String
custom_type::String custom_type::String
content::String content::String
details::Union{Any, Nothing} details::Union{T, Nothing}
display::Bool display::Bool
end end
``` ```
@@ -176,11 +196,8 @@ end
```julia ```julia
struct LabelEntry <: SessionTreeEntry struct LabelEntry <: SessionTreeEntry
type::String base::SessionTreeEntryBase
id::String target_id::String
parent_id::Union{String, Nothing}
timestamp::String
target_id::String # Entry being labeled
label::Union{String, Nothing} label::Union{String, Nothing}
end end
``` ```
@@ -191,10 +208,7 @@ end
```julia ```julia
struct SessionInfoEntry <: SessionTreeEntry struct SessionInfoEntry <: SessionTreeEntry
type::String base::SessionTreeEntryBase
id::String
parent_id::Union{String, Nothing}
timestamp::String
name::Union{String, Nothing} name::Union{String, Nothing}
end end
``` ```
@@ -205,10 +219,7 @@ end
```julia ```julia
struct LeafEntry <: SessionTreeEntry struct LeafEntry <: SessionTreeEntry
type::String base::SessionTreeEntryBase
id::String
parent_id::Union{String, Nothing}
timestamp::String
target_id::Union{String, Nothing} target_id::Union{String, Nothing}
end end
``` ```
@@ -221,86 +232,95 @@ end
abstract type SessionStorage{T<:SessionMetadata} end abstract type SessionStorage{T<:SessionMetadata} end
``` ```
### Storage Methods ### Storage Methods (actual implementation signatures)
```julia ```julia
# Metadata # Metadata
getMetadata(storage::SessionStorage)::Promise{T} getMetadata(storage::SessionStorage)::T
# Leaf management # Leaf management
getLeafId(storage::SessionStorage)::Promise{Union{String, Nothing}} getLeafId(storage::SessionStorage)::Union{String, Nothing}
setLeafId(storage::SessionStorage, leaf_id::String)::Promise{Nothing} setLeafId(storage::SessionStorage, leaf_id::Union{String, Nothing})::Nothing
# Entry management # Entry management
createEntryId(storage::SessionStorage)::Promise{String} createEntryId(storage::SessionStorage)::String
appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Promise{Nothing} appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Nothing
getEntry(storage::SessionStorage, id::String)::Promise{Union{SessionTreeEntry, Nothing}} getEntry(storage::SessionStorage, id::String)::Union{SessionTreeEntry, Nothing}
# Query # Query
findEntries(storage::SessionStorage, type::String)::Promise{Vector{SessionTreeEntry}} findEntries(storage::SessionStorage, type::String)::Vector{SessionTreeEntry}
getLabel(storage::SessionStorage, id::String)::Promise{Union{String, Nothing}} getLabel(storage::SessionStorage, id::String)::Union{String, Nothing}
getSessionName(storage::SessionStorage)::Promise{Union{String, Nothing}} getSessionName(storage::SessionStorage)::Union{String, Nothing}
# Branch navigation # Branch navigation
getPathToRootOrCompaction( getPathToRootOrCompaction(storage::SessionStorage, leaf_id::Union{String, Nothing})::Vector{SessionTreeEntry}
storage::SessionStorage, getEntries(storage::SessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry}
leaf_id::String,
)::Promise{Vector{SessionTreeEntry}}
getEntries(storage::SessionStorage, options::Dict{String, Any})::Promise{Vector{SessionTreeEntry}}
# Stats # Stats
getSessionStats(storage::SessionStorage)::Promise{SessionStats} getSessionStats(storage::SessionStorage)::SessionStats
``` ```
## JsonlSessionStorage ## 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 │ │ JSONL Storage Format │
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
File: session.jsonl File: session.jsonl (conceptual - not yet implemented)
Entry 1 (Metadata): Entry 1 (Metadata via SessionHeader):
{"type":"session","id":"meta_1","created_at":"2024-01-01T00:00:00Z","cwd":"/path","path":"/path/session.jsonl"} {"type":"session","version":3,"id":"meta_1","timestamp":"...","cwd":"/path","parent_session":null,"metadata":{}}
Entry 2 (Message): 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): 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): 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): 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): 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): 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): 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: 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) - parent_id references previous entry (linked list structure)
- Leaf entry points to current position in tree - Leaf entry points to current position in tree
- To fork, create new branch from any entry - To fork, create new branch from any entry
- In-memory mode uses Vector + Dict by_id for fast access
``` ```
## InMemorySessionStorage ## InMemorySessionStorage
```julia ```julia
mutable struct InMemorySessionStorage mutable struct InMemorySessionStorage{T<:SessionMetadata} <: SessionStorage{T}
metadata::SessionMetadata metadata::T
entries::Vector{SessionTreeEntry}
by_id::Dict{String, SessionTreeEntry}
labels_by_id::Dict{String, String}
leaf_id::Union{String, Nothing} leaf_id::Union{String, Nothing}
entries::Dict{String, SessionTreeEntry}
labels::Dict{String, String}
end end
``` ```
@@ -317,6 +337,19 @@ end
mutable struct Session{T<:SessionMetadata} mutable struct Session{T<:SessionMetadata}
storage::SessionStorage{T} storage::SessionStorage{T}
context_build_options::SessionContextBuildOptions 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 end
``` ```
@@ -326,14 +359,10 @@ end
```julia ```julia
function appendMessage(session::Session, message::AgentMessage)::String function appendMessage(session::Session, message::AgentMessage)::String
entry = MessageEntry( return appendTypedEntry(session, MessageEntry(
"message", SessionTreeEntryBase("message", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
createEntryId(session.storage),
getLeafId(session.storage),
create_timestamp(),
message, message,
) ))
return appendTypedEntry(session, entry)
end end
``` ```
@@ -354,18 +383,34 @@ tool_id = appendMessage(session, ToolResultMessage(...))
#### appendThinkingLevelChange() #### appendThinkingLevelChange()
```julia ```julia
function appendThinkingLevelChange( function appendThinkingLevelChange(session::Session, thinking_level::String)::String
session::Session, return appendTypedEntry(session, ThinkingLevelChangeEntry(
thinking_level::String, SessionTreeEntryBase("thinking_level_change", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
)::String
entry = ThinkingLevelChangeEntry(
"thinking_level_change",
createEntryId(session.storage),
getLeafId(session.storage),
create_timestamp(),
thinking_level, 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 end
``` ```
@@ -382,11 +427,8 @@ function appendCompaction(
usage::Union{Usage, Nothing}=nothing, usage::Union{Usage, Nothing}=nothing,
retained_tail::Union{Vector{AgentMessage}, Nothing}=nothing, retained_tail::Union{Vector{AgentMessage}, Nothing}=nothing,
)::String )::String
entry = CompactionEntry( return appendTypedEntry(session, CompactionEntry(
"compaction", SessionTreeEntryBase("compaction", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
createEntryId(session.storage),
getLeafId(session.storage),
create_timestamp(),
summary, summary,
first_kept_entry_id, first_kept_entry_id,
tokens_before, tokens_before,
@@ -394,8 +436,7 @@ function appendCompaction(
details, details,
usage, usage,
from_hook, from_hook,
) ))
return appendTypedEntry(session, entry)
end end
``` ```
@@ -407,25 +448,24 @@ function moveTo(
entry_id::Union{String, Nothing}, entry_id::Union{String, Nothing},
summary::Union{Dict{String, Any}, Nothing}=nothing, summary::Union{Dict{String, Any}, Nothing}=nothing,
)::Union{String, Nothing} )::Union{String, Nothing}
# Set new leaf # Validate entry exists
setLeafId(session.storage, entry_id) if !isnothing(entry_id) && isnothing(getEntry(session, entry_id))
throw(SessionError("not_found", "Entry $(entry_id) not found"))
# 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),
))
end end
# Set new leaf (creates a LeafEntry)
return nothing 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 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 ## Build Session Context
```julia ```julia
function buildSessionContext( function buildSessionContext(
path_entries::Vector{SessionTreeEntry}, path_entries::Vector{SessionTreeEntry},
options::SessionContextBuildOptions=SessionContextBuildOptions(), options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
)::SessionContext )::SessionContext
state = deriveSessionContextState(path_entries) state = deriveSessionContextState(path_entries)
context_entries = buildContextEntries(path_entries, options) context_entries = buildContextEntries(path_entries, options)
@@ -459,14 +505,36 @@ function buildSessionContext(
end end
return SessionContext(messages, state.thinking_level, state.model, state.active_tool_names) return SessionContext(messages, state.thinking_level, state.model, state.active_tool_names)
end 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 ### Context Entry Transform
```julia ```julia
function defaultContextEntryTransform( function defaultContextEntryTransform(path_entries::Vector{SessionTreeEntry})::Vector{SessionTreeEntry}
path_entries::Vector{SessionTreeEntry},
)::Vector{SessionTreeEntry}
compaction = nothing compaction = nothing
for entry in path_entries for entry in path_entries
if entry isa CompactionEntry if entry isa CompactionEntry
@@ -474,25 +542,26 @@ function defaultContextEntryTransform(
break break
end end
end end
if isnothing(compaction) if isnothing(compaction)
return copy(path_entries) return copy(path_entries)
end end
# Include compaction entry entries::Vector{SessionTreeEntry} = [compaction]
entries = [compaction] compaction_idx = findfirst(
(entry) -> entry isa CompactionEntry && entry.id == compaction.id,
# Include retained tail if present path_entries,
)
if !isnothing(compaction.retained_tail) if !isnothing(compaction.retained_tail)
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries) for i in compaction_idx+1:length(path_entries)
append!(entries, path_entries[compaction_idx+1:end]) push!(entries, path_entries[i])
end
return entries return entries
end end
# Otherwise include entries after first_kept_entry_id
if !isnothing(compaction.first_kept_entry_id) if !isnothing(compaction.first_kept_entry_id)
found_first_kept = false found_first_kept = false
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
for i in 1:compaction_idx-1 for i in 1:compaction_idx-1
entry = path_entries[i] entry = path_entries[i]
if entry.id == compaction.first_kept_entry_id if entry.id == compaction.first_kept_entry_id
@@ -503,11 +572,26 @@ function defaultContextEntryTransform(
end end
end end
end end
# Include entries after compaction for i in compaction_idx+1:length(path_entries)
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries) push!(entries, path_entries[i])
append!(entries, path_entries[compaction_idx+1:end]) 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 return entries
end end
``` ```
@@ -519,12 +603,12 @@ function sessionEntryToContextMessages(
entry::SessionTreeEntry, entry::SessionTreeEntry,
index::Int64, index::Int64,
entries::Vector{SessionTreeEntry}, entries::Vector{SessionTreeEntry},
options::SessionContextBuildOptions=SessionContextBuildOptions(), options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
)::Vector{AgentMessage} )::Vector{AgentMessage}
if entry isa MessageEntry if entry isa MessageEntry
return [entry.message] return [entry.message]
end end
if entry isa CustomMessageEntry if entry isa CustomMessageEntry
return [createCustomMessage( return [createCustomMessage(
entry.custom_type, entry.custom_type,
@@ -534,7 +618,7 @@ function sessionEntryToContextMessages(
entry.timestamp, entry.timestamp,
)] )]
end end
if entry isa CompactionEntry if entry isa CompactionEntry
messages = [createCompactionSummaryMessage( messages = [createCompactionSummaryMessage(
entry.summary, entry.summary,
@@ -546,7 +630,7 @@ function sessionEntryToContextMessages(
end end
return messages return messages
end end
if entry isa BranchSummaryEntry if entry isa BranchSummaryEntry
return [createBranchSummaryMessage( return [createBranchSummaryMessage(
entry.summary, entry.summary,
@@ -554,16 +638,15 @@ function sessionEntryToContextMessages(
entry.timestamp, entry.timestamp,
)] )]
end end
if entry isa CustomEntry if entry isa CustomEntry
# Custom projectors can transform custom entries
if !isnothing(options.entry_projectors) && haskey(options.entry_projectors, entry.custom_type) if !isnothing(options.entry_projectors) && haskey(options.entry_projectors, entry.custom_type)
projector = options.entry_projectors[entry.custom_type] projector = options.entry_projectors[entry.custom_type]
return projector(entry, index, entries) return projector(entry, index, entries)
end end
return AgentMessage[] return AgentMessage[]
end end
return AgentMessage[] return AgentMessage[]
end end
``` ```
@@ -610,6 +693,16 @@ Key Points:
- Each branch has independent tail - 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 ## Compaction Strategy
### Why Compaction? ### Why Compaction?
@@ -641,7 +734,7 @@ LLM context windows have limits:
# 4. Update storage # 4. Update storage
# - Append CompactionEntry # - Append CompactionEntry
# - Update leaf to CompactionEntry # - Leaf automatically points to CompactionEntry (leafIdAfterEntry)
``` ```
### Compaction Example ### Compaction Example
@@ -697,8 +790,10 @@ using AgentCore
# 1. Create storage # 1. Create storage
storage = JsonlSessionStorage( storage = JsonlSessionStorage(
SessionMetadata("session_1", "2024-01-01T00:00:00Z"),
"/path/to/session.jsonl", "/path/to/session.jsonl",
SessionHeader("session", 3, "session_1", created_at, "/path", nothing, nothing),
SessionTreeEntry[],
nothing,
) )
# 2. Create session # 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)) msg3_id = appendMessage(session, UserMessage("user", [TextContent("What can you do?")], timestamp))
msg4_id = appendMessage(session, AssistantMessage("assistant", [TextContent("I can...")], ...)) msg4_id = appendMessage(session, AssistantMessage("assistant", [TextContent("I can...")], ...))
# 7. Compact context (100K tokens 20K) # 7. Compact context (100K tokens -> 20K)
compact_id = appendCompaction( compact_id = appendCompaction(
session, session,
"User asked about capabilities and assistant explained", "User asked about capabilities and assistant explained",
@@ -733,31 +828,91 @@ compact_id = appendCompaction(
# 8. Fork and branch # 8. Fork and branch
session.moveTo(msg2_id) # Go back to msg2 session.moveTo(msg2_id) # Go back to msg2
# 9. Create new branch # 9. Continue on new branch (moveTo creates branch summary when summary is provided)
branch_id = appendBranchSummary( branch_id = moveTo(
session, session,
"User changed direction to focus on file operations",
msg2_id, msg2_id,
Dict("focus" => "files"), Dict("summary" => "User changed direction", "details" => Dict("focus" => "files")),
) )
# 10. Continue on new branch # 10. Continue on new branch
msg5_id = appendMessage(session, UserMessage("user", [TextContent("Let's work with files")], timestamp)) msg5_id = appendMessage(session, UserMessage("user", [TextContent("Let's work with files")], timestamp))
# 11. Query session context # 11. Query session context
context = buildSessionContext(session) context = buildContext(session)
# 12. Get stats # 12. Get stats
stats = getSessionStats(session) stats = getSessionStats(session)
println("Messages: $(stats.message_count)") println("Messages: $(stats.message_count)")
println("Total tokens: $(stats.total_tokens)") 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 ## Best Practices
1. **Use compaction** for long conversations to stay within context limits 1. **Use compaction** for long conversations to stay within context limits
2. **Create branch summaries** when forking to document divergent paths 2. **Create branch summaries** when forking to document divergent paths (via `moveTo()` with summary)
3. **Retain tail messages** after compaction for context 3. **Retain tail messages** after compaction for context (`retained_tail` field)
4. **Track token usage** to optimize compaction timing 4. **Track token usage** to optimize compaction timing
5. **Use InMemorySessionStorage** for testing 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 # 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
``` ```
┌─────────────────────────────────────────────────────────────────────────────┐ AssistantMessage (from LLM)
│ Tool Layer │ content::Vector{MessageContent}
└─────────────────────────────────────────────────────────────────────────────┘ └─ Contains: TextContent[] and ToolCall[]
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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"}) │
│ ] │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐ Agent.execute() (in agent.jl)
│ AgentLoop.executeToolCalls() │ └─ before_tool_call hook (Agent.before_tool_call, optional)
│ - Extract ToolCalls from message content │ Input: BeforeToolCallContext
│ - Determine execution mode (sequential/parallel) │ Output: BeforeToolCallResult (block, reason)
└────────────────────────────────────────────────────────┘
├─► 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
┌────────────────────────────────────────────────────────┐ For each ToolCall:
│ ToolResultMessage │ tool = find_tool(name)
│ - tool_call_id: "ref to original ToolCall" │ tool.execute(tool_call_id, args, signal, on_update, context)
│ - tool_name: "bash" │
│ - content: [TextContent("file1.md\nfile2.md\n")] │
│ - is_error: false │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐ AgentToolResult{T}(content, details, usage, added_tool_names, terminate)
│ AgentState.messages.append(tool_result) │
│ - Next turn: LLM sees tool results │ └─ 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 ## Built-in Tools
### 1. BashTool ### 1. BashTool (`tools/bash.jl`)
```julia ```julia
struct BashToolOptions{TContext} struct BashExecution
command_prefix::Union{String, Nothing} command::String
prepare::Union{BashPrepare{TContext}, Nothing} cwd::String
env::Dict{String, String}
inherit_env::Bool
end end
struct BashPrepare{TContext} mutable struct BashPrepare{TContext}
function::Function function::Function
context::TContext context::TContext
signal::Union{Any, Nothing} signal::Union{Any, Nothing}
end 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} truncation::Union{Any, Nothing}
full_output_path::Union{String, Nothing} full_output_path::Union{String, Nothing}
end 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 ```julia
function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing) mutable struct ReadToolDetails
return AgentTool( truncation::Union{Any, Nothing}
"bash", end
"bash",
"Execute a bash command in the current working directory.", mutable struct ReadToolOptions
Dict{String, Any}(), auto_resize_images::Bool
(tool_call_id, params, signal, on_update, context) -> begin image_processor::Union{Any, Nothing}
# Execute command end
result = executeBashCommand(params, signal, on_update)
function createReadTool{TContext}(options::Union{ReadToolOptions, Nothing}=nothing) where TContext
# Return result ```
return AgentToolResult(
[TextContent(result.output)], **Execute signature**: `(tool_call_id, params, signal, on_update, context) -> AgentToolResult`
BashToolDetails(result.truncation, result.full_path),
nothing, ### 3. WriteTool (`tools/write.jl`)
nothing,
result.terminate, ```julia
) function createWriteTool{TContext}() where TContext
end, ```
nothing, # prepare_arguments
nothing, # execution_mode (default: use config) **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 end
``` ```
**Parameters Schema**: Configured via `Agent(Dict(...))` options:
```json - `:beforeToolCall``Agent.before_tool_call`
{ - `:afterToolCall``Agent.after_tool_call`
"command": "string", - `:prepareNextTurn``Agent.prepare_next_turn`
"timeout": "number (optional)", - `:prepareNextTurnWithContext``Agent.prepare_next_turn_with_context`
"cwd": "string (optional)",
"env": "object (optional)"
}
```
**Example**: ### BeforeToolCallContext / BeforeToolCallResult (from types.jl)
```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
```julia ```julia
struct BeforeToolCallContext struct BeforeToolCallContext
@@ -444,32 +197,7 @@ struct BeforeToolCallResult
end end
``` ```
**Usage**: ### AfterToolCallContext / AfterToolCallResult (from types.jl)
```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
```julia ```julia
struct AfterToolCallContext struct AfterToolCallContext
@@ -490,37 +218,7 @@ struct AfterToolCallResult
end end
``` ```
**Usage**: ### PrepareNextTurnContext / AgentLoopTurnUpdate (from types.jl)
```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
```julia ```julia
struct PrepareNextTurnContext struct PrepareNextTurnContext
@@ -537,193 +235,73 @@ struct AgentLoopTurnUpdate
end 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 ## Tool Execution Modes
### Sequential Execution ### Sequential Execution
```julia ```julia
# Tools run one at a time, in order # Configure on Agent
# 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
agent = Agent(Dict( agent = Agent(Dict(
:toolExecution => EXECUTION_SEQUENTIAL, :toolExecution => EXECUTION_SEQUENTIAL,
)) ))
``` ```
**Example Scenario**: ### Parallel Execution (default)
```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
```julia ```julia
# Tools run concurrently
# Use case: Independent operations
# Default behavior
agent = Agent(Dict( agent = Agent(Dict(
:toolExecution => EXECUTION_PARALLEL, # Default :toolExecution => EXECUTION_PARALLEL,
)) ))
``` ```
**Example Scenario**: Tools can also specify their own mode:
```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
```julia ```julia
function createDatabaseTool() agent_tool = AgentTool(
return AgentTool( "name",
"database", "label",
"database", "description",
"Execute SQL queries against the database.", params_schema,
Dict{String, Any}( execute_fn,
"type" => "object", nothing,
"properties" => Dict( EXECUTION_SEQUENTIAL, # or EXECUTION_PARALLEL
"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]))
``` ```
### Example: HTTP Request Tool ## Tool Exports (from tools/index.jl)
```julia ```julia
function createHTTPTool() export
return AgentTool( createBashTool,
"http", createReadTool,
"http", createWriteTool,
"Make HTTP requests.", createEditTool,
Dict{String, Any}( BashExecution,
"type" => "object", BashPrepare,
"properties" => Dict( BashToolDetails,
"url" => Dict("type" => "string"), BashToolInput,
"method" => Dict("type" => "string", "enum" => ["GET", "POST", "PUT", "DELETE"]), BashToolOptions,
"body" => Dict("type" => "string"), EditToolDetails,
"headers" => Dict("type" => "object"), EditToolInput,
), ReadToolDetails,
"required" => ["url", "method"], ReadToolInput,
), ReadToolOptions,
(tool_call_id, params, signal, on_update, context) -> begin ReadImageProcessor,
# Make request ReadImageProcessorResult,
url = params["url"] WriteToolInput
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
``` ```
## Complete Example ## Example: Creating and Using Tools
```julia ```julia
using AgentCore using AgentCore
# 1. Create tools # Create tools
bash_tool = createBashTool() bash_tool = createBashTool()
read_tool = createReadTool() read_tool = createReadTool()
write_tool = createWriteTool() write_tool = createWriteTool()
# 2. Configure hooks # Configure hooks
before_hook = (context, signal) -> begin before_hook = (context, signal) -> begin
println("About to execute: $(context.tool_call.name)") println("About to execute: $(context.tool_call.name)")
return nothing return nothing
@@ -738,22 +316,17 @@ after_hook = (context, signal) -> begin
return nothing return nothing
end end
# 3. Create agent # Create agent with tools and hooks
agent = Agent(Dict( agent = Agent(Dict(
:systemPrompt => "You are a helpful assistant with file system access.", :systemPrompt => "You are a helpful assistant with file system access.",
:tools => [bash_tool, read_tool, write_tool], :tools => [bash_tool, read_tool, write_tool],
:beforeToolCall => before_hook, :beforeToolCall => before_hook,
:afterToolCall => after_hook, :afterToolCall => after_hook,
:toolExecution => EXECUTION_PARALLEL,
)) ))
# 4. Run conversation # Run prompt
prompt(agent, "List files in current directory and read the first one") 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 ## 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!") prompt(agent, "Hello!")
# Wait for completion # Wait for completion
wait_for_idle(agent) waitForIdle(agent)
``` ```
### Understanding the Flow ### Understanding the Flow
@@ -83,6 +83,12 @@ User Code
- `steer()` - Queue message for next turn - `steer()` - Queue message for next turn
- `followUp()` - Queue message after stop - `followUp()` - Queue message after stop
- `subscribe()` - Listen to events - `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 ### AgentLoop
@@ -113,9 +119,14 @@ User Code
**Key methods**: **Key methods**:
- `appendMessage()` - Add message - `appendMessage()` - Add message
- `appendCompaction()` - Compress history - `appendCompaction()` - Compress history with summary
- `moveTo()` - Navigate branches - `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 ### Tools
@@ -165,32 +176,209 @@ AgentStartEvent
└─ AgentEndEvent └─ 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) └─► normalizePromptInput()
Input: "Hello!"::String
AgentMessage[] (transformed) 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 ### 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() ├─ prepareToolCall()
├─ execute() │ Input: tool_call::ToolCall
├─ after_tool_call hook │ 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() └─ 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 ## Best Practices
@@ -253,7 +441,7 @@ appendMessage(session, user_message)
appendMessage(session, assistant_message) appendMessage(session, assistant_message)
# Build context from session # Build context from session
context = buildSessionContext(session) context = buildContext(session)
``` ```
### Pattern 2: Long Conversations ### Pattern 2: Long Conversations
@@ -277,7 +465,7 @@ end
session.moveTo(branch_point_id) session.moveTo(branch_point_id)
# Create new branch # Create new branch
appendBranchSummary(session, "Exploring alternative approach") moveTo(session, branch_point_id, summary=["summary" => "Exploring alternative approach"])
appendMessage(session, new_user_message) appendMessage(session, new_user_message)
``` ```
@@ -286,13 +474,13 @@ appendMessage(session, new_user_message)
```julia ```julia
# Create custom tool # Create custom tool
custom_tool = AgentTool( custom_tool = AgentTool(
"custom", "custom", # name
"custom", "Custom", # label
"Does custom thing", "Does custom thing", # description
..., parameters, # parameter schema
execute_function, execute_function, # execute
nothing, nothing, # prepare_arguments (optional)
EXECUTION_PARALLEL, EXECUTION_PARALLEL, # execution_mode
) )
# Add to agent # 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)
```