Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7876ff21eb | |||
| c9a7661e93 | |||
| 8d5c661562 | |||
| 244cfc4b96 | |||
| a541905b72 | |||
| a0787c2316 | |||
| 8fd72f8d37 |
+74
-74
@@ -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: │
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
+500
-603
File diff suppressed because it is too large
Load Diff
+344
-513
File diff suppressed because it is too large
Load Diff
+212
-24
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user