update
This commit is contained in:
+137
-28
@@ -5,12 +5,11 @@
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 1. INITIALIZATION │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Agent.prompt(user_input) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ normalizePrompt() ← Convert input (String/Message/Vector) to AgentMessage[] │
|
||||
│ normalizePrompt() ← Convert input to AgentMessage[] │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ runPromptMessages() │
|
||||
@@ -21,10 +20,12 @@
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 2. AGENT LOOP START (runAgentLoop) │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ new_messages = copy(prompts) ← User messages copied to new_messages │
|
||||
│ current_context.messages = vcat(context.messages, copy(prompts)) ← User messages added to context │
|
||||
│ new_messages = copy(prompts) │
|
||||
│ current_context.messages = vcat(context.messages, copy(prompts)) │
|
||||
│ │ │
|
||||
│ └─→ User messages are IMMEDIATELY added to context.messages │
|
||||
│ (They are NOT in the steering queue!) │
|
||||
│ │
|
||||
│ emit(AgentStartEvent) │
|
||||
│ emit(TurnStartEvent) │
|
||||
@@ -32,33 +33,35 @@
|
||||
│ for prompt in prompts: │
|
||||
│ emit(MessageStartEvent(prompt)) │
|
||||
│ emit(MessageEndEvent(prompt)) │
|
||||
│ │ │
|
||||
│ ├─→ push to current_context.messages (for LLM) │
|
||||
│ └─→ push to new_messages (track what we've added) │
|
||||
│ │
|
||||
└─────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 3. MAIN LOOP (runLoop - while true) │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ pending_messages = get_steering_messages() ← Check steering queue (empty on first turn) │
|
||||
│ pending_messages = get_steering_messages() │
|
||||
│ │ │
|
||||
│ └─→ Steering queue: messages from agent.steer() │
|
||||
│ These are for CONTINUING conversation (NOT new user prompts) │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ While has pending_messages OR has_tool_calls: │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ 4. PENDING MESSAGE HANDLING │ │ │
|
||||
│ │ │ (Handles steering messages queued via agent.steer() AFTER previous turn) │ │ │
|
||||
│ │ │ 4. PENDING MESSAGE HANDLING (steering messages only) │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ pending_messages = get_steering() │ │ │
|
||||
│ │ │ if !isempty(pending_messages): │ │ │
|
||||
│ │ │ for msg in pending_messages: │ │ │
|
||||
│ │ │ emit(MessageStartEvent(msg)) │ │ │
|
||||
│ │ │ emit(MessageEndEvent(msg)) │ │ │
|
||||
│ │ │ push to current_context.messages │ │ │
|
||||
│ │ │ push to new_messages │ │ │
|
||||
│ │ │ push to current_context.messages ← Steering messages go HERE │ │ │
|
||||
│ │ │ push to new_messages │ │ │
|
||||
│ │ │ pending_messages = [] │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ Note: User messages from Agent.prompt() are ALREADY in context.messages │ │ │
|
||||
│ │ │ (They were added in runAgentLoop via vcat(), not via this queue) │ │ │
|
||||
│ │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ │
|
||||
@@ -163,12 +166,14 @@
|
||||
│ │
|
||||
│ 2. runAgentLoop() │
|
||||
│ new_messages = [UserMessage("What is Julia?")] │
|
||||
│ current_context.messages = [...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!) │
|
||||
│ emit(AgentStartEvent), emit(TurnStartEvent) │
|
||||
│ emit(MessageStart/End) for user message │
|
||||
│ │
|
||||
│ 3. runLoop() │
|
||||
│ pending_messages = get_steering() = [] ← Steering queue is empty │
|
||||
│ pending_messages = get_steering() = [] ← Steering queue is empty (no agent.steer() yet) │
|
||||
│ │
|
||||
│ 4. streamAssistantResponse() │
|
||||
│ convert_to_llm([UserMessage]) → Message[] │
|
||||
@@ -202,6 +207,11 @@
|
||||
│ │ follow_up_queue: [] │ │
|
||||
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ LLM SEES (convert_to_llm() filters): │ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Messages passed to LLM API: │ │
|
||||
│ │ [UserMessage("What is Julia?"), AssistantMessage("Julia is...")] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ TURN #2: User asks "How does it work?" │
|
||||
│ ───────────────────────────────────────── │
|
||||
@@ -211,7 +221,9 @@
|
||||
│ │
|
||||
│ 2. runAgentLoop() │
|
||||
│ new_messages = [UserMessage("How does it work?")] │
|
||||
│ current_context.messages = [...previous..., UserMessage("How does it work?")] │
|
||||
│ current_context.messages = vcat([...previous..., UserMessage("How does it work?")]) │
|
||||
│ │ │
|
||||
│ └─→ User message added (context preserved from Turn #1) │
|
||||
│ emit(AgentStartEvent), emit(TurnStartEvent) │
|
||||
│ emit(MessageStart/End) for user message │
|
||||
│ │
|
||||
@@ -232,25 +244,122 @@
|
||||
│ │ [UserMsg1, AssistantMsg1, UserMsg2, AssistantMsg2] │ │
|
||||
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ LLM SEES: │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Messages passed to LLM API: │ │
|
||||
│ │ [UserMessage("What is Julia?"), │ │
|
||||
│ │ AssistantMessage("Julia is..."), │ │
|
||||
│ │ UserMessage("How does it work?"), │ │
|
||||
│ │ AssistantMessage("It works by...")] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ STEERING MESSAGES │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ What is a steering message? │
|
||||
│ • A message (any AgentMessage type) injected via: `agent.steer(message)` │
|
||||
│ • Goes into the steering queue, not immediately to context.messages │
|
||||
│ │
|
||||
│ How is it created? │
|
||||
│ • User code calls: agent.steer(UserMessage("...")) │
|
||||
│ • Or: agent.steer(AssistantMessage("...")) │
|
||||
│ • Or any other AgentMessage subtype │
|
||||
│ │
|
||||
│ When is it processed? │
|
||||
│ • At the START of the next loop iteration (line 194-202 in agent_loop.jl) │
|
||||
│ • AFTER the previous assistant turn completes │
|
||||
│ • BEFORE the next assistant response is streamed │
|
||||
│ │
|
||||
│ Why use steering? │
|
||||
│ Use case 1: Tool execution result injection │
|
||||
│ - Agent calls a tool (e.g., read_file, bash) │
|
||||
│ - Tool returns result │
|
||||
│ - You want to inject a follow-up question based on the result │
|
||||
│ - agent.steer(UserMessage("Based on the file, what should we do next?")) │
|
||||
│ │
|
||||
│ Use case 2: Multi-turn conversation without user input │
|
||||
│ - Agent responds to user │
|
||||
│ - Before user types again, you want to inject a system message │
|
||||
│ - agent.steer(BashExecutionMessage(...)) or custom message │
|
||||
│ - This continues the conversation automatically │
|
||||
│ │
|
||||
│ Use case 3: Branch navigation recovery │
|
||||
│ - User navigates between conversation branches │
|
||||
│ - After switching branches, you want to inject a context message │
|
||||
│ - agent.steer(BranchSummaryMessage(...)) │
|
||||
│ - The agent can then continue from the new branch context │
|
||||
│ │
|
||||
│ Use case 4: Compaction summary injection │
|
||||
│ - Conversation history is compacted │
|
||||
│ - After compaction, inject summary message │
|
||||
│ - agent.steer(CompactionSummaryMessage(...)) │
|
||||
│ - Agent knows old history was summarized │
|
||||
│ │
|
||||
│ Example: │
|
||||
│ agent.steer(UserMessage("Follow-up question here")) │
|
||||
│ # This will be processed in the next loop iteration, │
|
||||
│ # appearing in context.messages before the next LLM call │
|
||||
│ │
|
||||
│ The LLM sees: │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ All messages become Message[] via convert_to_llm(): │ │
|
||||
│ │ [UserMessage(...), AssistantMessage(...), UserMessage(from_steer), ...] │ │
|
||||
│ │ │ │
|
||||
│ │ The LLM cannot tell which came from Agent.prompt() vs agent.steer() │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ LLM PROCESSING: How LLM sees messages │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ The LLM NEVER sees "user message" vs "steering message" - it only sees Message types: │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ convert_to_llm() transforms ALL AgentMessages to Message[]: │ │
|
||||
│ │ │ │
|
||||
│ │ UserMessage("user") → UserMessage (for LLM) │ │
|
||||
│ │ Steering UserMessage("user") → UserMessage (for LLM) ← Same! │ │
|
||||
│ │ AssistantMessage("assistant") → AssistantMessage (for LLM) │ │
|
||||
│ │ ToolResultMessage("toolResult") → ToolResultMessage (for LLM) │ │
|
||||
│ │ │ │
|
||||
│ │ BranchSummaryMessage → UserMessage (wrapped in summary tags) │ │
|
||||
│ │ CompactionSummaryMessage → UserMessage (wrapped in summary tags) │ │
|
||||
│ │ BashExecutionMessage → UserMessage (if not excluded) │ │
|
||||
│ │ CustomMessage → UserMessage │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ The difference is ONLY in HOW messages enter the system: │
|
||||
│ • User messages: Agent.prompt() → vcat() → context.messages (direct) │
|
||||
│ • Steering: agent.steer() → queue → loop → context.messages (indirect) │
|
||||
│ │
|
||||
│ At LLM level: BOTH become UserMessage in the conversation! │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ KEY INSIGHTS │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. User prompts are NOT added to steering queue │
|
||||
│ They go directly into 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 AFTER a turn │
|
||||
│ Via agent.steer(message) - used for continuation without new prompt │
|
||||
│ 2. Steering queue is for messages injected via agent.steer() AFTER a turn finishes │
|
||||
│ This allows continuing conversation without calling Agent.prompt() again │
|
||||
│ │
|
||||
│ 3. Context is preserved across turns │
|
||||
│ Each turn appends to context.messages, so LLM sees full history │
|
||||
│ 3. Context is preserved across turns - context.messages grows with each turn │
|
||||
│ LLM sees the full conversation history │
|
||||
│ │
|
||||
│ 4. New turn = New prompt OR steering/follow-up messages │
|
||||
│ - New Agent.prompt() call starts new turn with new messages │
|
||||
│ - Steering messages continue from current state │
|
||||
│ - Follow-up messages run when agent would stop │
|
||||
│ 4. At LLM level, ALL messages become Message types (UserMessage/AssistantMessage/ToolResultMessage) │
|
||||
│ The "steering" vs "user" distinction is just a control mechanism, not a message type │
|
||||
│ │
|
||||
│ 5. New turn is triggered by: │
|
||||
│ - New Agent.prompt() call (adds user messages) │
|
||||
│ - Steering messages (adds steering messages) │
|
||||
│ - Follow-up messages (adds follow-up messages) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,10 +1,110 @@
|
||||
# check if this column has vector embedding. if there is one, seach vector version instead
|
||||
column_name_embedding = column_name * "_embedding"
|
||||
if occursin(column_name_embedding, tables_schema[column_name_embedding])
|
||||
vector_column = Dict(
|
||||
"table_name"=> table_name,
|
||||
"column_name"=> column_name_embedding,
|
||||
"operator"=> "vector_similarity",
|
||||
"value"=> column_obj["value"]
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
read codebase.
|
||||
I need to understand this agent concept deeply.
|
||||
Can you write related documents (.md files) that will help me understand the agent
|
||||
and save in "/home/ton/docker-apps/sommpanion/YiemAgent/learning" folder?
|
||||
I'm learning best in **Top-Down** style so I know how each component are synchonized.
|
||||
|
||||
P.S. use diagram to show how process flow and relationship
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
# AgentCore.jl - Architecture Overview
|
||||
|
||||
## Top-Down Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentCore.jl Layers │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Level 1: AgentHarness (Session Management & Persistence) │
|
||||
│ - Session persistence with JSONL storage │
|
||||
│ - Resource management (skills, prompt templates) │
|
||||
│ - Extension hooks system │
|
||||
│ - Branch navigation and compaction │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ orchestrates
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Level 2: Agent (State Management & Event Streaming) │
|
||||
│ - Conversation state (messages, tools, system prompt) │
|
||||
│ - Event streaming and lifecycle management │
|
||||
│ - Steering and follow-up message queues │
|
||||
│ - Abort handling │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ delegates to
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Level 3: AgentLoop (Core LLM Interaction Loop) │
|
||||
│ - Stateful LLM interactions │
|
||||
│ - Tool execution (parallel or sequential) │
|
||||
│ - Event emission lifecycle │
|
||||
│ - Steering/follow-up message handling │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ transforms to
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Level 4: Session (Conversation History Management) │
|
||||
│ - Tree-based conversation history │
|
||||
│ - Branch support with compaction │
|
||||
│ - Message and metadata persistence │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Process Flow
|
||||
|
||||
### 1. Agent Lifecycle
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Agent Lifecycle │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
User Code
|
||||
│
|
||||
│ 1. Create Agent
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ Agent() │ ──► Initialize state, queues, listeners
|
||||
└──────────────┘
|
||||
│
|
||||
│ 2. Subscribe to events
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ subscribe() │ ──► Register event handlers
|
||||
└──────────────────┘
|
||||
│
|
||||
│ 3. Run prompt
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ prompt() │ ──► Validate input, normalize messages
|
||||
└──────────────────┘
|
||||
│
|
||||
│ 4. Start AgentLoop
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ runPromptMessages│ ──► Create ActiveRun, spawn loop
|
||||
└──────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ AgentLoop (runs in separate thread) │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ 1. Emit AgentStartEvent │ │
|
||||
│ │ 2. Emit TurnStartEvent │ │
|
||||
│ │ 3. Process prompts (emit MessageStart/End) │ │
|
||||
│ │ 4. ┌──────────────────────────────────────────────┐ │ │
|
||||
│ │ │ while true: │ │ │
|
||||
│ │ │ │ Process steering/follow-up messages │ │ │
|
||||
│ │ │ │ Stream assistant response (LLM call) │ │ │
|
||||
│ │ │ │ Execute tool calls (parallel/sequential) │ │ │
|
||||
│ │ │ │ Emit TurnEndEvent │ │ │
|
||||
│ │ │ │ Check if should stop │ │ │
|
||||
│ │ │ │ Get next steering messages │ │ │
|
||||
│ │ └───┴────────────────────────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ 5. Event streaming
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Event Handlers │ ──► User-defined listeners receive events
|
||||
└──────────────────┘
|
||||
│
|
||||
│ 6. Wait for completion
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ waitForIdle() │ ──► Resolve when all events processed
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
### 2. AgentLoop Flow Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentLoop Process Flow │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentLoop Entrypoint │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ agentLoop(prompts, context, config, signal, stream_fn) │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ runAgentLoop(prompts, context, config, emit, signal) │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ runLoop() - Main Event Loop │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└──────────────────────────────┼───────────────────────────────────────┘
|
||||
│
|
||||
│ Loop Iteration
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ Main Processing Loop │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 1. Get Steering/Follow-up Messages │ │
|
||||
│ │ ┌────────────────────┐ ┌──────────────────────┐ │ │
|
||||
│ │ │ steering_queue │ │ follow_up_queue │ │ │
|
||||
│ │ │ (after assistant) │ │ (after stop) │ │ │
|
||||
│ │ └────────────────────┘ └──────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 2. Stream Assistant Response │ │
|
||||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ transform_context() │ │ │
|
||||
│ │ │ convert_to_llm(messages) -> Message[] │ │ │
|
||||
│ │ │ stream_fn(model, context, config) -> Response │ │ │
|
||||
│ │ │ - Text deltas │ │ │
|
||||
│ │ │ - Tool call deltas │ │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Emit: MessageStartEvent, MessageUpdateEvent, │ │ │
|
||||
│ │ │ MessageEndEvent │ │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 3. Execute Tool Calls │ │
|
||||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ extract ToolCall from assistant content │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ if EXECUTION_SEQUENTIAL || has_sequential_tool: │ │ │
|
||||
│ │ │ executeToolCallsSequential() │ │ │
|
||||
│ │ │ else: │ │ │
|
||||
│ │ │ executeToolCallsParallel() │ │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ For each tool call: │ │ │
|
||||
│ │ │ 1. before_tool_call hook │ │ │
|
||||
│ │ │ 2. prepareToolCall() │ │ │
|
||||
│ │ │ 3. execute() │ │ │
|
||||
│ │ │ 4. after_tool_call hook │ │ │
|
||||
│ │ │ 5. Emit ToolExecutionStart/Update/EndEvent │ │ │
|
||||
│ │ │ 6. Emit ToolResultMessage │ │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 4. Prepare Next Turn │ │
|
||||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ prepare_next_turn(context) -> next_turn_snapshot │ │ │
|
||||
│ │ │ - Optional: Update model/thinking_level │ │ │
|
||||
│ │ │ - Optional: Update context │ │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 5. Check Termination Conditions │ │
|
||||
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ should_stop_after_turn(context) -> bool │ │ │
|
||||
│ │ │ - Max turns reached? │ │ │
|
||||
│ │ │ - Tool returned terminate=true? │ │ │
|
||||
│ │ │ - Steering queue empty and follow-up empty? │ │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 6. Emit TurnEndEvent (message, tool_results) │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Loop continues until termination condition met │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└──────────────────────────────┼───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentEndEvent with final messages │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3. Tool Execution Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tool Execution Flow │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌───────────────────────────────────────────────────────────────────┐
|
||||
│ Assistant Message with Tool Calls │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ AssistantMessage: │ │
|
||||
│ │ content: [ │ │
|
||||
│ │ TextContent("I'll help you"), │ │
|
||||
│ │ ToolCall(id="tc1", name="bash", args={...}), │ │
|
||||
│ │ ToolCall(id="tc2", name="read", args={...}) │ │
|
||||
│ │ ] │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
└───────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ executeToolCalls()
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────┐
|
||||
│ Determine Execution Mode │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ config.tool_execution == EXECUTION_SEQUENTIAL? │ │
|
||||
│ │ OR any tool has execution_mode == EXECUTION_SEQUENTIAL? │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────┴───────────────┐ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌────────────────────────┐ ┌────────────────────────┐ │
|
||||
│ │ executeSequential() │ │ executeParallel() │ │
|
||||
│ └────────────────────────┘ └────────────────────────┘ │
|
||||
│ │ │ │
|
||||
└──────────────┼───────────────────────────────┼────────────────────┘
|
||||
│ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ Sequential Execution │ │ Parallel Execution │
|
||||
│ │ │ │
|
||||
│ for tool_call in: │ │ for tool_call in: │
|
||||
│ prepareToolCall() │ │ prepareToolCall() │
|
||||
│ execute() │ │ execute() (async) │
|
||||
│ finalize() │ │ │
|
||||
│ │ │ wait all results │
|
||||
│ │ └──────────────────────┘
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────┐
|
||||
│ For Each Tool Call │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 1. before_tool_call hook (optional) │ │
|
||||
│ │ - Can block execution │ │
|
||||
│ │ 2. prepareToolCall() │ │
|
||||
│ │ - validateToolArguments() │ │
|
||||
│ │ - prepareToolCallArguments() (optional) │ │
|
||||
│ │ 3. Execute Tool: │ │
|
||||
│ │ tool.execute(tool_call_id, args, signal, on_update) │ │
|
||||
│ │ 4. after_tool_call hook (optional) │ │
|
||||
│ │ - Can modify result content │ │
|
||||
│ │ 5. Emit events: │ │
|
||||
│ │ - ToolExecutionStartEvent │ │
|
||||
│ │ - ToolExecutionUpdateEvent (optional) │ │
|
||||
│ │ - ToolExecutionEndEvent │ │
|
||||
│ │ 6. Create ToolResultMessage │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────┐
|
||||
│ Tool Result Messages │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ToolResultMessage: │ │
|
||||
│ │ role: "toolResult" │ │
|
||||
│ │ tool_call_id: "tc1" │ │
|
||||
│ │ tool_name: "bash" │ │
|
||||
│ │ content: [TextContent("command output")] │ │
|
||||
│ │ is_error: false │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4. Session & Tree Structure
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Session Tree Structure │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Session = Linked List of Entries (tree structure)
|
||||
|
||||
┌───────────────────────────────────────────────────────────────────┐
|
||||
│ Branch Navigation │
|
||||
│ │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ E1 │────▶│ E2 │────▶│ E3 │────▶│ E4 │────▶│ E5 │ (leaf) │
|
||||
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
|
||||
│ │ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ ▼ │
|
||||
│ Message Message Compaction Message BranchSummary │
|
||||
│ │
|
||||
│ E3 is a Compaction Entry: │
|
||||
│ - Summary of E1, E2 │
|
||||
│ - first_kept_entry_id: reference to first retained message │
|
||||
│ - tokens_before: context size before compaction │
|
||||
│ │
|
||||
│ E5 is a BranchSummary Entry: │
|
||||
│ - Summary of branch from from_id │
|
||||
│ - Represents a fork point in conversation history │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ Session.moveTo()
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────┐
|
||||
│ Forking & Branching │
|
||||
│ │
|
||||
│ Current branch: │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ E1 │────▶│ E2 │────▶│ E3 │ │
|
||||
│ └─────┘ └─────┘ └─────┘ │
|
||||
│ │ │
|
||||
│ │ moveTo(E2) │
|
||||
│ ▼ │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │ (new branch) │
|
||||
│ └─────┘ └─────┘ └─────┘ └─────┘ │
|
||||
│ │ │
|
||||
│ │ create BranchSummary │
|
||||
│ ▼ │
|
||||
│ ┌─────┐ │
|
||||
│ │ E5 │ (branch summary) │
|
||||
│ └─────┘ │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Component Relationships
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Component Relationships │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
User Code
|
||||
│
|
||||
├── Creates ──► Agent
|
||||
│ │
|
||||
│ ├── Uses ──► AgentLoop
|
||||
│ │ │
|
||||
│ │ ├── Uses ──► StreamFn (LLM API)
|
||||
│ │ │
|
||||
│ │ └── Uses ──► Session
|
||||
│ │
|
||||
│ ├── Manages ──► AgentState
|
||||
│ │
|
||||
│ ├── Queues ──► SteeringQueue
|
||||
│ │
|
||||
│ └── Queues ──► FollowUpQueue
|
||||
│
|
||||
└── Interacts With ──► AgentHarness (optional, higher level)
|
||||
│
|
||||
├── Manages ──► SessionRepo
|
||||
│
|
||||
├── Manages ──► Skills
|
||||
│
|
||||
└── Manages ──► PromptTemplates
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Data Flow Between Layers │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
User Input (String/Message)
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Agent.prompt() │
|
||||
│ - normalizeInput() │
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AgentState.messages │ ──► AgentMessage[]
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AgentLoop │
|
||||
│ - transform_context │
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ convertToLlm() │ ──► Transforms AgentMessage[] to Message[]
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ LLM API (StreamFn) │
|
||||
│ - Context: Message[] │
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Response (Streaming) │
|
||||
│ - Text deltas │
|
||||
│ - Tool call deltas │
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AssistantMessage │
|
||||
│ - content: Message[] │
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AgentState.messages │ ──► Appended to conversation
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Tool Execution │
|
||||
│ - Extract ToolCalls │
|
||||
│ - Execute tools │
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ ToolResultMessage[] │
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AgentState.messages │ ──► Tool results appended
|
||||
└──────────────────────┘
|
||||
│
|
||||
│ (Loop back to LLM or end)
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Session Storage │
|
||||
│ - JSONL format │
|
||||
│ - Tree entries │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The AgentCore.jl architecture follows a clean separation of concerns:
|
||||
|
||||
1. **AgentHarness** - Highest level, handles persistence and resources
|
||||
2. **Agent** - State management and event streaming
|
||||
3. **AgentLoop** - Core LLM interaction loop
|
||||
4. **Session** - Conversation history management
|
||||
|
||||
Each layer transforms data and passes it to the next layer, with clear interfaces and event hooks for customization.
|
||||
@@ -0,0 +1,465 @@
|
||||
# AgentCore.jl - Agent Component Deep Dive
|
||||
|
||||
## Agent Structure
|
||||
|
||||
```julia
|
||||
mutable struct Agent
|
||||
_state::AgentState
|
||||
listeners::Set{Tuple{Function, Ref{Bool}}}
|
||||
steering_queue::PendingMessageQueue
|
||||
follow_up_queue::PendingMessageQueue
|
||||
|
||||
convert_to_llm::Function
|
||||
transform_context::Union{Function, Nothing}
|
||||
stream_function::StreamFn
|
||||
get_api_key::Union{Function, Nothing}
|
||||
on_payload::Union{Function, Nothing}
|
||||
on_response::Union{Function, Nothing}
|
||||
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}
|
||||
active_run::Union{ActiveRun, Nothing}
|
||||
session_id::Union{String, Nothing}
|
||||
thinking_budgets::Union{Dict{String, Int64}, Nothing}
|
||||
transport::String
|
||||
max_retry_delay_ms::Union{Int64, Nothing}
|
||||
tool_execution::ToolExecutionMode
|
||||
end
|
||||
```
|
||||
|
||||
## Agent Lifecycle
|
||||
|
||||
### 1. Initialization
|
||||
|
||||
```julia
|
||||
# Create agent with options
|
||||
agent = Agent(Dict{Symbol, Any}(
|
||||
:systemPrompt => "You are a helpful assistant",
|
||||
:model => Model(...),
|
||||
:thinkingLevel => THINKING_MEDIUM,
|
||||
:tools => [bash_tool, read_tool],
|
||||
:steeringMode => QUEUE_ONE_AT_A_TIME,
|
||||
:followUpMode => QUEUE_ONE_AT_A_TIME,
|
||||
:toolExecution => EXECUTION_PARALLEL,
|
||||
))
|
||||
|
||||
# Subscribe to events
|
||||
unsubscribe = subscribe(agent) do event, signal
|
||||
if event isa MessageEndEvent
|
||||
println("Message: $(event.message)")
|
||||
elseif event isa ToolExecutionEndEvent
|
||||
println("Tool completed: $(event.tool_name)")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 2. Message Queues
|
||||
|
||||
#### Steering Queue
|
||||
- Messages injected **after** the current assistant turn finishes
|
||||
- Used to correct or redirect the agent's behavior
|
||||
- Example: "Actually, let's do X instead"
|
||||
|
||||
#### Follow-Up Queue
|
||||
- Messages run **only after** the agent would otherwise stop
|
||||
- Used to continue conversation when agent thinks it's done
|
||||
- Example: "Wait, there's one more thing"
|
||||
|
||||
#### Queue Modes
|
||||
- `QUEUE_ALL` - Drain all messages at once
|
||||
- `QUEUE_ONE_AT_A_TIME` - Process one message at a time
|
||||
|
||||
```julia
|
||||
# Queue a steering message
|
||||
steer(agent, UserMessage(...))
|
||||
|
||||
# Queue a follow-up message
|
||||
followUp(agent, UserMessage(...))
|
||||
|
||||
# Check if queues have items
|
||||
hasQueuedMessages(agent) # Returns Bool
|
||||
|
||||
# Clear queues
|
||||
clearSteeringQueue(agent)
|
||||
clearFollowUpQueue(agent)
|
||||
clearAllQueues(agent)
|
||||
```
|
||||
|
||||
### 3. Event System
|
||||
|
||||
#### Agent Events
|
||||
|
||||
```julia
|
||||
abstract type AgentEvent end
|
||||
|
||||
# Lifecycle events
|
||||
struct AgentStartEvent <: AgentEvent end
|
||||
struct AgentEndEvent <: AgentEvent
|
||||
messages::Vector{AgentMessage}
|
||||
end
|
||||
|
||||
# Turn events
|
||||
struct TurnStartEvent <: AgentEvent end
|
||||
struct TurnEndEvent <: AgentEvent
|
||||
message::AgentMessage
|
||||
tool_results::Vector{ToolResultMessage}
|
||||
end
|
||||
|
||||
# Message events
|
||||
struct MessageStartEvent <: AgentEvent
|
||||
message::AgentMessage
|
||||
end
|
||||
struct MessageUpdateEvent <: AgentEvent
|
||||
message::AgentMessage
|
||||
assistant_message_event::Any
|
||||
end
|
||||
struct MessageEndEvent <: AgentEvent
|
||||
message::AgentMessage
|
||||
end
|
||||
|
||||
# Tool execution events
|
||||
struct ToolExecutionStartEvent <: AgentEvent
|
||||
tool_call_id::String
|
||||
tool_name::String
|
||||
args::Any
|
||||
end
|
||||
struct ToolExecutionUpdateEvent <: AgentEvent
|
||||
tool_call_id::String
|
||||
tool_name::String
|
||||
args::Any
|
||||
partial_result::Any
|
||||
end
|
||||
struct ToolExecutionEndEvent <: AgentEvent
|
||||
tool_call_id::String
|
||||
tool_name::String
|
||||
result::Any
|
||||
is_error::Bool
|
||||
end
|
||||
```
|
||||
|
||||
#### Event Flow Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Event Timeline │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
AgentStartEvent
|
||||
│
|
||||
├─ TurnStartEvent
|
||||
│ │
|
||||
│ ├─ MessageStartEvent (user prompt)
|
||||
│ ├─ MessageEndEvent (user prompt)
|
||||
│ │
|
||||
│ ├─ [Loop starts]
|
||||
│ │ │
|
||||
│ │ ├─ MessageStartEvent (assistant response)
|
||||
│ │ ├─ MessageUpdateEvent (text delta 1)
|
||||
│ │ ├─ MessageUpdateEvent (text delta 2)
|
||||
│ │ ├─ MessageUpdateEvent (tool call delta)
|
||||
│ │ ├─ MessageEndEvent (assistant complete)
|
||||
│ │ │
|
||||
│ │ ├─ ToolExecutionStartEvent (tc1)
|
||||
│ │ ├─ ToolExecutionUpdateEvent (partial result)
|
||||
│ │ ├─ ToolExecutionEndEvent (tc1 done)
|
||||
│ │ │
|
||||
│ │ ├─ ToolExecutionStartEvent (tc2)
|
||||
│ │ ├─ ToolExecutionEndEvent (tc2 done)
|
||||
│ │ │
|
||||
│ │ └─ TurnEndEvent (assistant + tools)
|
||||
│ │
|
||||
│ └─ [Next turn if needed]
|
||||
│
|
||||
└─ AgentEndEvent (final messages)
|
||||
```
|
||||
|
||||
### 4. State Management
|
||||
|
||||
```julia
|
||||
mutable struct AgentState
|
||||
system_prompt::String
|
||||
model::Model
|
||||
thinking_level::ThinkingLevel
|
||||
tools::Vector{AgentTool}
|
||||
messages::Vector{AgentMessage}
|
||||
is_streaming::Bool
|
||||
streaming_message::Union{AgentMessage, Nothing}
|
||||
pending_tool_calls::Set{String}
|
||||
error_message::Union{String, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
#### State Access
|
||||
|
||||
```julia
|
||||
# Get current state
|
||||
state = get_state(agent)
|
||||
|
||||
# Reset state
|
||||
reset!(agent) # Clears messages, queues, and runtime state
|
||||
```
|
||||
|
||||
### 5. Main Methods
|
||||
|
||||
#### prompt()
|
||||
|
||||
```julia
|
||||
# Start a new conversation
|
||||
prompt(agent, "Hello, how are you?")
|
||||
|
||||
# With multiple messages
|
||||
prompt(agent, [
|
||||
UserMessage(...),
|
||||
AssistantMessage(...),
|
||||
UserMessage(...)
|
||||
])
|
||||
|
||||
# With images
|
||||
prompt(agent, "Analyze this image", [ImageContent(data, "image/png")])
|
||||
```
|
||||
|
||||
#### continue!()
|
||||
|
||||
```julia
|
||||
# Continue from current transcript
|
||||
# Last message must be user or tool-result
|
||||
continue!(agent)
|
||||
```
|
||||
|
||||
#### steer() and followUp()
|
||||
|
||||
```julia
|
||||
# Steering: Redirect after next assistant turn
|
||||
steer(agent, UserMessage(...))
|
||||
|
||||
# Follow-up: Continue after agent would stop
|
||||
followUp(agent, UserMessage(...))
|
||||
```
|
||||
|
||||
### 6. Hooks
|
||||
|
||||
#### convert_to_llm
|
||||
|
||||
```julia
|
||||
# Transform messages before sending to LLM
|
||||
function myConvertToLlm(messages::Vector{AgentMessage})
|
||||
return filter(
|
||||
m -> m.role in ["user", "assistant", "toolResult"],
|
||||
messages
|
||||
)
|
||||
end
|
||||
|
||||
agent = Agent(Dict(:convertToLlm => myConvertToLlm))
|
||||
```
|
||||
|
||||
#### transform_context
|
||||
|
||||
```julia
|
||||
# Transform context before LLM call
|
||||
function myTransformContext(messages, signal)
|
||||
# Can truncate, filter, or modify messages
|
||||
return messages
|
||||
end
|
||||
|
||||
agent = Agent(Dict(:transformContext => myTransformContext))
|
||||
```
|
||||
|
||||
#### before_tool_call
|
||||
|
||||
```julia
|
||||
# Hook before tool execution
|
||||
function myBeforeToolCall(context, signal)
|
||||
println("About to execute: $(context.tool_call.name)")
|
||||
return nothing # Return block=true to prevent execution
|
||||
end
|
||||
|
||||
agent = Agent(Dict(:beforeToolCall => myBeforeToolCall))
|
||||
```
|
||||
|
||||
#### after_tool_call
|
||||
|
||||
```julia
|
||||
# Hook after tool execution
|
||||
function myAfterToolCall(context, signal)
|
||||
# Can modify tool result
|
||||
return AfterToolCallResult(
|
||||
content = context.result.content,
|
||||
terminate = context.result.terminate
|
||||
)
|
||||
end
|
||||
|
||||
agent = Agent(Dict(:afterToolCall => myAfterToolCall))
|
||||
```
|
||||
|
||||
#### prepare_next_turn
|
||||
|
||||
```julia
|
||||
# Modify context/model/thinking level between turns
|
||||
function myPrepareNextTurn(context, signal)
|
||||
# context: PrepareNextTurnContext
|
||||
# Returns AgentLoopTurnUpdate or nothing
|
||||
return AgentLoopTurnUpdate(
|
||||
context = context.context,
|
||||
model = context.context.model, # Can change model
|
||||
thinking_level = THINKING_HIGH # Can change thinking level
|
||||
)
|
||||
end
|
||||
|
||||
agent = Agent(Dict(:prepareNextTurn => myPrepareNextTurn))
|
||||
```
|
||||
|
||||
### 7. Active Run Management
|
||||
|
||||
```julia
|
||||
# Check if agent is busy
|
||||
if !isnothing(agent.active_run)
|
||||
# Agent is processing
|
||||
abort(agent) # Abort current run
|
||||
end
|
||||
|
||||
# Wait for completion
|
||||
wait_for_idle(agent) # Returns Promise
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```julia
|
||||
using AgentCore
|
||||
|
||||
# 1. Create agent
|
||||
agent = Agent(Dict(
|
||||
:systemPrompt => "You are a helpful assistant.",
|
||||
:model => Model(...),
|
||||
:tools => [bash_tool, read_tool],
|
||||
))
|
||||
|
||||
# 2. Subscribe to events
|
||||
events_received = []
|
||||
unsubscribe = subscribe(agent) do event, signal
|
||||
push!(events_received, event)
|
||||
|
||||
if event isa MessageEndEvent
|
||||
println("Message: $(event.message)")
|
||||
end
|
||||
end
|
||||
|
||||
# 3. Start conversation
|
||||
prompt(agent, "What's in the current directory?")
|
||||
|
||||
# 4. Wait for completion
|
||||
wait_for_idle(agent)
|
||||
|
||||
# 5. Check final state
|
||||
state = get_state(agent)
|
||||
println("Total messages: $(length(state.messages))")
|
||||
|
||||
# 6. Continue with steering
|
||||
steer(agent, UserMessage(...))
|
||||
wait_for_idle(agent)
|
||||
|
||||
# 7. Clean up
|
||||
unsubscribe() # Stop listening
|
||||
reset!(agent) # Clear state
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Message Queueing
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Message Queue Behavior │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Scenario: User sends message, agent responds with tool calls
|
||||
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Time 0: User sends message │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ prompt(msg) │ │
|
||||
│ └──────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ AgentLoop │ │
|
||||
│ │ processes │ │
|
||||
│ │ msg │ │
|
||||
│ └─────────────┘ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Time 1: Agent responds with tool calls │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ AssistantMessage: │ │
|
||||
│ │ content: [Text("I'll check..."), │ │
|
||||
│ │ ToolCall("bash", {...}), │ │
|
||||
│ │ ToolCall("read", {...})] │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Time 2: User queues steering message │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ steer(msg2) │ ──► steering_queue.push(msg2) │
|
||||
│ └──────────────────┘ │
|
||||
│ │
|
||||
│ (msg2 not processed yet!) │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Time 3: Tool execution │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ Execute bash tool... │ │
|
||||
│ │ Execute read tool... │ │
|
||||
│ │ Emit ToolResultMessage[] │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Time 4: Agent responds to tool results │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ AssistantMessage (2nd turn): │ │
|
||||
│ │ content: [Text("The results are...")] │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Time 5: Steering message processed │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ steering_queue.drain() → [msg2] │ │
|
||||
│ │ Emit msg2 as UserMessage │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Time 6: Next turn (agent responds to steering) │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ AssistantMessage (3rd turn): │ │
|
||||
│ │ content: [Text("Okay, I'll do X instead...")] │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Queue Behavior Summary
|
||||
|
||||
| Action | Queue | When Processed |
|
||||
|--------|-------|----------------|
|
||||
| `prompt()` | N/A | Immediate |
|
||||
| `steer()` | steering_queue | After assistant turn completes |
|
||||
| `followUp()` | follow_up_queue | After agent would normally stop |
|
||||
| `continue!()` | N/A | Immediately if last message is user/tool |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use steering for redirects**: When user wants to change direction mid-conversation
|
||||
2. **Use follow-up for continuation**: When agent thinks it's done but user wants more
|
||||
3. **Subscribe to events**: Monitor agent behavior and debug issues
|
||||
4. **Clear queues**: Use `clearAllQueues()` when resetting conversation
|
||||
5. **Check active run**: Don't call `prompt()` while agent is busy
|
||||
@@ -0,0 +1,758 @@
|
||||
# AgentCore.jl - AgentLoop Component Deep Dive
|
||||
|
||||
## AgentLoop Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentLoop Layer │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Public API │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
agentLoop()
|
||||
├─ prompts: Vector{AgentMessage}
|
||||
├─ context: AgentContext
|
||||
├─ config: AgentLoopConfig
|
||||
├─ signal: Union{Nothing, AbortSignal}
|
||||
└─ stream_fn: StreamFn
|
||||
└─ Returns: EventStream
|
||||
|
||||
agentLoopContinue()
|
||||
├─ context: AgentContext
|
||||
├─ config: AgentLoopConfig
|
||||
├─ signal: Union{Nothing, AbortSignal}
|
||||
└─ stream_fn: StreamFn
|
||||
└─ Returns: EventStream
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Internal Flow │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 1. runAgentLoop() ── Entry point for new conversation │
|
||||
│ - Creates copy of prompts │
|
||||
│ - Appends prompts to context.messages │
|
||||
│ - Emits AgentStartEvent │
|
||||
│ - Emits TurnStartEvent │
|
||||
│ - Emits MessageStart/End for each prompt │
|
||||
│ - Calls runLoop() │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 2. runLoop() ── Main event loop │
|
||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ while true: │ │
|
||||
│ │ 1. Get steering/follow-up messages (if any) │ │
|
||||
│ │ 2. Emit messages as UserMessage │ │
|
||||
│ │ 3. streamAssistantResponse() │ │
|
||||
│ │ 4. Execute tool calls (sequential or parallel) │ │
|
||||
│ │ 5. Emit TurnEndEvent │ │
|
||||
│ │ 6. prepare_next_turn (optional) │ │
|
||||
│ │ 7. should_stop_after_turn? (check termination) │ │
|
||||
│ │ 8. Loop continues if not terminated │ │
|
||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 3. streamAssistantResponse() ── LLM interaction │
|
||||
│ - transform_context (optional) │
|
||||
│ - convert_to_llm (transform to Message[]) │
|
||||
│ - Call stream_fn (LLM API) │
|
||||
│ - Stream response deltas │
|
||||
│ - Emit MessageStart/Update/End events │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 4. executeToolCalls() ── Tool execution │
|
||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ if EXECUTION_SEQUENTIAL || has_sequential_tool: │ │
|
||||
│ │ executeToolCallsSequential() │ │
|
||||
│ │ else: │ │
|
||||
│ │ executeToolCallsParallel() │ │
|
||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 5. AgentEndEvent ── Final event with all messages │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## AgentLoopConfig
|
||||
|
||||
```julia
|
||||
struct AgentLoopConfig
|
||||
model::Model
|
||||
reasoning::Union{ThinkingLevel, Nothing}
|
||||
session_id::Union{String, Nothing}
|
||||
on_payload::Union{Function, Nothing}
|
||||
on_response::Union{Function, Nothing}
|
||||
transport::String
|
||||
thinking_budgets::Union{Dict{String, Int64}, Nothing}
|
||||
max_retry_delay_ms::Union{Int64, Nothing}
|
||||
tool_execution::ToolExecutionMode
|
||||
before_tool_call::Union{Function, Nothing}
|
||||
after_tool_call::Union{Function, Nothing}
|
||||
prepare_next_turn::Union{Function, Nothing}
|
||||
convert_to_llm::Function
|
||||
transform_context::Union{Function, Nothing}
|
||||
get_api_key::Union{Function, Nothing}
|
||||
get_steering_messages::Union{Function, Nothing}
|
||||
get_follow_up_messages::Union{Function, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
## Main Functions
|
||||
|
||||
### agentLoop()
|
||||
|
||||
```julia
|
||||
function agentLoop(
|
||||
prompts::Vector{AgentMessage},
|
||||
context::AgentContext,
|
||||
config::AgentLoopConfig,
|
||||
signal::Union{Nothing, AbortSignal},
|
||||
stream_fn::StreamFn,
|
||||
)::EventStream
|
||||
```
|
||||
|
||||
**Purpose**: Start a new conversation with initial prompts
|
||||
|
||||
**Flow**:
|
||||
1. Create event stream
|
||||
2. Spawn thread to run agent loop
|
||||
3. Return stream for event consumption
|
||||
|
||||
```julia
|
||||
stream = agentLoop(
|
||||
[UserMessage("user", [TextContent("Hello")], timestamp)],
|
||||
AgentContext(system_prompt, messages, tools),
|
||||
config,
|
||||
nothing,
|
||||
stream_fn,
|
||||
)
|
||||
|
||||
# Consume events
|
||||
for event in stream
|
||||
if event isa MessageEndEvent
|
||||
println("Received: $(event.message)")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### runAgentLoop()
|
||||
|
||||
```julia
|
||||
function runAgentLoop(
|
||||
prompts::Vector{AgentMessage},
|
||||
context::AgentContext,
|
||||
config::AgentLoopConfig,
|
||||
emit::AgentEventSink,
|
||||
signal::Union{Nothing, AbortSignal},
|
||||
stream_fn::StreamFn,
|
||||
)::Vector{AgentMessage}
|
||||
```
|
||||
|
||||
**Purpose**: Execute agent loop with initial prompts
|
||||
|
||||
**Flow**:
|
||||
1. Copy prompts to new_messages
|
||||
2. Append prompts to context.messages
|
||||
3. Emit AgentStartEvent
|
||||
4. For each prompt: emit MessageStartEvent, MessageEndEvent
|
||||
5. Call runLoop()
|
||||
|
||||
### runLoop() - The Heart of AgentLoop
|
||||
|
||||
```julia
|
||||
function runLoop(
|
||||
initial_context::AgentContext,
|
||||
new_messages::Vector{AgentMessage},
|
||||
initial_config::AgentLoopConfig,
|
||||
signal::Union{Nothing, AbortSignal},
|
||||
emit::AgentEventSink,
|
||||
stream_function::StreamFn,
|
||||
)::Nothing
|
||||
```
|
||||
|
||||
**Main Loop**:
|
||||
```julia
|
||||
current_context = initial_context
|
||||
config = initial_config
|
||||
first_turn = true
|
||||
pending_messages = get_steering_messages()
|
||||
|
||||
while true
|
||||
# Process steering/follow-up messages
|
||||
while !isempty(pending_messages)
|
||||
if !first_turn
|
||||
emit(TurnStartEvent())
|
||||
else
|
||||
first_turn = false
|
||||
end
|
||||
|
||||
# Emit pending messages
|
||||
for message in pending_messages
|
||||
emit(MessageStartEvent(message))
|
||||
emit(MessageEndEvent(message))
|
||||
push!(current_context.messages, message)
|
||||
push!(new_messages, message)
|
||||
end
|
||||
|
||||
pending_messages = []
|
||||
end
|
||||
|
||||
# Stream assistant response
|
||||
message = streamAssistantResponse(
|
||||
current_context,
|
||||
config,
|
||||
signal,
|
||||
emit,
|
||||
stream_function,
|
||||
)
|
||||
push!(new_messages, message)
|
||||
|
||||
# Check for errors
|
||||
if message.stop_reason in ("error", "aborted")
|
||||
emit(TurnEndEvent(message, []))
|
||||
emit(AgentEndEvent(new_messages))
|
||||
return
|
||||
end
|
||||
|
||||
# Execute tool calls
|
||||
tool_calls = filter(c -> c isa ToolCall, message.content)
|
||||
tool_results = []
|
||||
has_more_tool_calls = false
|
||||
|
||||
if !isempty(tool_calls)
|
||||
executed_batch = if message.stop_reason == "length"
|
||||
failToolCallsFromTruncatedMessage(tool_calls, emit)
|
||||
else
|
||||
executeToolCalls(
|
||||
current_context,
|
||||
message,
|
||||
config,
|
||||
signal,
|
||||
emit,
|
||||
)
|
||||
end
|
||||
append!(tool_results, executed_batch.messages)
|
||||
has_more_tool_calls = !executed_batch.terminate
|
||||
|
||||
for result in tool_results
|
||||
push!(current_context.messages, result)
|
||||
push!(new_messages, result)
|
||||
end
|
||||
end
|
||||
|
||||
emit(TurnEndEvent(message, tool_results))
|
||||
|
||||
# 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)
|
||||
pending_messages = follow_up_messages
|
||||
continue
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
|
||||
emit(AgentEndEvent(new_messages))
|
||||
```
|
||||
|
||||
### streamAssistantResponse()
|
||||
|
||||
```julia
|
||||
function streamAssistantResponse(
|
||||
context::AgentContext,
|
||||
config::AgentLoopConfig,
|
||||
signal::Union{Nothing, AbortSignal},
|
||||
emit::AgentEventSink,
|
||||
stream_function::StreamFn,
|
||||
)::AssistantMessage
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. Get messages from context
|
||||
2. Apply transform_context (optional)
|
||||
3. Convert to LLM messages with convert_to_llm
|
||||
4. Create Context object
|
||||
5. Resolve API key
|
||||
6. Call stream_fn with model, context, and config
|
||||
7. Stream events:
|
||||
- "start" → MessageStartEvent
|
||||
- "text_start", "text_delta", "text_end" → MessageUpdateEvent
|
||||
- "done", "error" → MessageEndEvent
|
||||
|
||||
### executeToolCalls()
|
||||
|
||||
```julia
|
||||
function executeToolCalls(
|
||||
current_context::AgentContext,
|
||||
assistant_message::AssistantMessage,
|
||||
config::AgentLoopConfig,
|
||||
signal::Union{Nothing, AbortSignal},
|
||||
emit::AgentEventSink,
|
||||
)::ExecutedToolCallBatch
|
||||
```
|
||||
|
||||
**Logic**:
|
||||
```julia
|
||||
tool_calls = filter(c -> c isa ToolCall, assistant_message.content)
|
||||
|
||||
# Check if any tool requires sequential execution
|
||||
has_sequential = any(tc -> begin
|
||||
tool = findfirst(t -> t.name == tc.name, current_context.tools)
|
||||
!isnothing(tool) && tool.execution_mode == EXECUTION_SEQUENTIAL
|
||||
end, tool_calls)
|
||||
|
||||
# Determine execution mode
|
||||
if config.tool_execution == EXECUTION_SEQUENTIAL || has_sequential
|
||||
executeToolCallsSequential(...)
|
||||
else
|
||||
executeToolCallsParallel(...)
|
||||
end
|
||||
```
|
||||
|
||||
### executeToolCallsSequential()
|
||||
|
||||
```julia
|
||||
function executeToolCallsSequential(
|
||||
current_context::AgentContext,
|
||||
assistant_message::AssistantMessage,
|
||||
tool_calls::Vector{ToolCall},
|
||||
config::AgentLoopConfig,
|
||||
signal::Union{Nothing, AbortSignal},
|
||||
emit::AgentEventSink,
|
||||
)::ExecutedToolCallBatch
|
||||
```
|
||||
|
||||
**Flow** (for each tool call):
|
||||
1. Emit ToolExecutionStartEvent
|
||||
2. prepareToolCall() → PreparedToolCall or ImmediateToolCallOutcome
|
||||
3. If prepared: executePreparedToolCall()
|
||||
4. finalizeExecutedToolCall()
|
||||
5. Emit ToolExecutionEndEvent
|
||||
6. Emit ToolResultMessage
|
||||
7. Check if signal.aborted → break
|
||||
|
||||
### executeToolCallsParallel()
|
||||
|
||||
```julia
|
||||
function executeToolCallsParallel(
|
||||
current_context::AgentContext,
|
||||
assistant_message::AssistantMessage,
|
||||
tool_calls::Vector{ToolCall},
|
||||
config::AgentLoopConfig,
|
||||
signal::Union{Nothing, AbortSignal},
|
||||
emit::AgentEventSink,
|
||||
)::ExecutedToolCallBatch
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. For each tool call:
|
||||
- If immediate: execute and add to finalized_calls
|
||||
- If prepared: create closure, add to finalized_calls
|
||||
2. For each entry in finalized_calls:
|
||||
- If closure: execute closure
|
||||
- If finalized: use as-is
|
||||
3. Collect all tool results
|
||||
4. Return batch
|
||||
|
||||
### prepareToolCall()
|
||||
|
||||
```julia
|
||||
function prepareToolCall(
|
||||
current_context::AgentContext,
|
||||
assistant_message::AssistantMessage,
|
||||
tool_call::ToolCall,
|
||||
config::AgentLoopConfig,
|
||||
signal::Union{Nothing, AbortSignal},
|
||||
)::Union{PreparedToolCall, ImmediateToolCallOutcome}
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. Find tool by name
|
||||
2. If not found → ImmediateToolCallOutcome (error)
|
||||
3. before_tool_call hook (optional)
|
||||
4. prepareToolCallArguments() (optional)
|
||||
5. validateToolArguments()
|
||||
6. Return PreparedToolCall
|
||||
|
||||
### executePreparedToolCall()
|
||||
|
||||
```julia
|
||||
function executePreparedToolCall(
|
||||
prepared::PreparedToolCall,
|
||||
signal::Union{Nothing, AbortSignal},
|
||||
emit::AgentEventSink,
|
||||
)::ExecutedToolCallOutcome
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. Call tool.execute(id, args, signal, on_update)
|
||||
2. Collect update events (if any)
|
||||
3. Wait for all update events
|
||||
4. Return ExecutedToolCallOutcome(result)
|
||||
|
||||
### finalizeExecutedToolCall()
|
||||
|
||||
```julia
|
||||
function finalizeExecutedToolCall(
|
||||
current_context::AgentContext,
|
||||
assistant_message::AssistantMessage,
|
||||
prepared::PreparedToolCall,
|
||||
executed::ExecutedToolCallOutcome,
|
||||
config::AgentLoopConfig,
|
||||
signal::Union{Nothing, AbortSignal},
|
||||
)::FinalizedToolCallOutcome
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. after_tool_call hook (optional)
|
||||
2. Return FinalizedToolCallOutcome
|
||||
|
||||
### createToolResultMessage()
|
||||
|
||||
```julia
|
||||
function createToolResultMessage(
|
||||
finalized::FinalizedToolCallOutcome,
|
||||
)::ToolResultMessage
|
||||
```
|
||||
|
||||
**Creates**:
|
||||
```julia
|
||||
ToolResultMessage(
|
||||
"toolResult",
|
||||
finalized.tool_call.id,
|
||||
finalized.tool_call.name,
|
||||
finalized.result.content,
|
||||
finalized.result.details,
|
||||
finalized.result.usage,
|
||||
finalized.result.added_tool_names,
|
||||
finalized.is_error,
|
||||
timestamp,
|
||||
)
|
||||
```
|
||||
|
||||
## Execution Modes
|
||||
|
||||
### Sequential Execution
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Sequential Execution Flow │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────┐
|
||||
│ TC1 │ ──► prepareToolCall()
|
||||
└──────┘ │
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ execute() │ ──► Wait for completion
|
||||
└──────────────┘ │
|
||||
│ ▼
|
||||
├───────────── createToolResultMessage()
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────┐
|
||||
│ TC2 │ ──► │ │ Result1 │
|
||||
└──────┘ └──────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ execute() │
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ TC3 │ ──► │
|
||||
└──────┘ │
|
||||
│ ▼
|
||||
├───── createToolResultMessage()
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────┐
|
||||
│ execute() │ │ │ Result2 │
|
||||
└──────────────┘ └──────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ Result3 │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Parallel Execution Flow │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────┐
|
||||
│ TC1 │ ──► prepareToolCall() ──► create closure ──► ┐
|
||||
└──────┘ │
|
||||
│
|
||||
┌──────┐ │
|
||||
│ TC2 │ ──► prepareToolCall() ──► create closure ──► ├─► All closures queued
|
||||
└──────┘ │
|
||||
│
|
||||
┌──────┐ │
|
||||
│ TC3 │ ──► prepareToolCall() ──► create closure ──► ┘
|
||||
└──────┘
|
||||
|
||||
│
|
||||
▼
|
||||
┌───────────────────────┐
|
||||
│ for closure in closures│
|
||||
│ execute_closure() │
|
||||
└───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────┐
|
||||
│ Collect all results │
|
||||
└───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────┐
|
||||
│ createToolResult() │
|
||||
└───────────────────────┘
|
||||
```
|
||||
|
||||
## Helper Types
|
||||
|
||||
### ExecutedToolCallBatch
|
||||
|
||||
```julia
|
||||
struct ExecutedToolCallBatch
|
||||
messages::Vector{ToolResultMessage}
|
||||
terminate::Bool
|
||||
end
|
||||
```
|
||||
|
||||
- `messages`: All tool result messages
|
||||
- `terminate`: If true, stop agent after this batch
|
||||
|
||||
### PrepareNextTurnContext
|
||||
|
||||
```julia
|
||||
struct PrepareNextTurnContext
|
||||
message::AssistantMessage
|
||||
tool_results::Vector{ToolResultMessage}
|
||||
context::AgentContext
|
||||
new_messages::Vector{AgentMessage}
|
||||
end
|
||||
```
|
||||
|
||||
Used by prepare_next_turn hook to decide next steps.
|
||||
|
||||
### Before/After Tool Call Contexts
|
||||
|
||||
```julia
|
||||
struct BeforeToolCallContext
|
||||
assistant_message::AssistantMessage
|
||||
tool_call::ToolCall
|
||||
args::Any
|
||||
context::AgentContext
|
||||
end
|
||||
|
||||
struct BeforeToolCallResult
|
||||
block::Union{Bool, Nothing}
|
||||
reason::Union{String, Nothing}
|
||||
end
|
||||
|
||||
struct AfterToolCallContext
|
||||
assistant_message::AssistantMessage
|
||||
tool_call::ToolCall
|
||||
args::Any
|
||||
result::AgentToolResult
|
||||
is_error::Bool
|
||||
context::AgentContext
|
||||
end
|
||||
|
||||
struct AfterToolCallResult
|
||||
content::Union{Vector{MessageContent}, Nothing}
|
||||
details::Union{Any, Nothing}
|
||||
is_error::Union{Bool, Nothing}
|
||||
usage::Union{Usage, Nothing}
|
||||
terminate::Union{Bool, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
## Event Emission Timeline
|
||||
|
||||
```
|
||||
AgentStartEvent
|
||||
│
|
||||
├─ TurnStartEvent (turn 1)
|
||||
│ │
|
||||
│ ├─ MessageStartEvent (user prompt)
|
||||
│ ├─ MessageEndEvent (user prompt)
|
||||
│ │
|
||||
│ ├─ MessageStartEvent (assistant)
|
||||
│ ├─ MessageUpdateEvent (text delta)
|
||||
│ ├─ MessageUpdateEvent (tool call delta)
|
||||
│ ├─ MessageEndEvent (assistant)
|
||||
│ │
|
||||
│ ├─ ToolExecutionStartEvent (tc1)
|
||||
│ ├─ ToolExecutionEndEvent (tc1)
|
||||
│ │
|
||||
│ ├─ ToolExecutionStartEvent (tc2)
|
||||
│ ├─ ToolExecutionEndEvent (tc2)
|
||||
│ │
|
||||
│ └─ TurnEndEvent (assistant, tool_results)
|
||||
│
|
||||
├─ TurnStartEvent (turn 2 - if needed)
|
||||
│ │
|
||||
│ ├─ MessageStartEvent (steering/follow-up)
|
||||
│ ├─ MessageEndEvent (steering/follow-up)
|
||||
│ │
|
||||
│ ├─ MessageStartEvent (assistant)
|
||||
│ ├─ MessageUpdateEvent (text)
|
||||
│ ├─ MessageEndEvent (assistant)
|
||||
│ │
|
||||
│ └─ TurnEndEvent (assistant, [])
|
||||
│
|
||||
└─ AgentEndEvent (final messages)
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### 1. Message Transformation Pipeline
|
||||
|
||||
```
|
||||
AgentMessage[] (internal)
|
||||
│
|
||||
│ transform_context()
|
||||
▼
|
||||
AgentMessage[] (transformed)
|
||||
│
|
||||
│ convert_to_llm()
|
||||
▼
|
||||
Message[] (LLM API)
|
||||
```
|
||||
|
||||
### 2. Tool Call Lifecycle
|
||||
|
||||
```
|
||||
ToolCall (in assistant message)
|
||||
│
|
||||
├─ before_tool_call (hook)
|
||||
│
|
||||
├─ prepareToolCall()
|
||||
│ ├─ validate arguments
|
||||
│ └─ prepare arguments (optional)
|
||||
│
|
||||
├─ execute()
|
||||
│ ├─ Immediate: return result
|
||||
│ └─ Prepared: async execution
|
||||
│
|
||||
├─ after_tool_call (hook)
|
||||
│
|
||||
└─ createToolResultMessage()
|
||||
```
|
||||
|
||||
### 3. Turn Termination
|
||||
|
||||
```julia
|
||||
# Turn ends when:
|
||||
# 1. No more pending messages
|
||||
# 2. No more tool calls to execute
|
||||
# 3. should_stop_after_turn() returns true
|
||||
|
||||
# Reasons to stop:
|
||||
# - Max turns reached
|
||||
# - Tool returned terminate=true
|
||||
# - Error or abort
|
||||
# - Steering/follow-up queues empty
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use sequential execution** for tools that modify shared state
|
||||
2. **Use parallel execution** for independent tool calls (better performance)
|
||||
3. **Implement prepare_next_turn** for dynamic model/thinking level changes
|
||||
4. **Use before_tool_call** for logging or blocking sensitive operations
|
||||
5. **Use after_tool_call** for modifying results or collecting metrics
|
||||
|
||||
## Complete Example
|
||||
|
||||
```julia
|
||||
using AgentCore
|
||||
|
||||
# Create config
|
||||
config = AgentLoopConfig(
|
||||
model = my_model,
|
||||
reasoning = THINKING_MEDIUM,
|
||||
tool_execution = EXECUTION_PARALLEL,
|
||||
before_tool_call = myBeforeToolCallHook,
|
||||
after_tool_call = myAfterToolCallHook,
|
||||
prepare_next_turn = myPrepareNextTurnHook,
|
||||
convert_to_llm = myConvertToLlm,
|
||||
transform_context = myTransformContext,
|
||||
get_api_key = myGetApiKey,
|
||||
get_steering_messages = myGetSteeringMessages,
|
||||
get_follow_up_messages = myGetFollowUpMessages,
|
||||
)
|
||||
|
||||
# Start agent loop
|
||||
stream = agentLoop(
|
||||
[UserMessage("user", [TextContent("Hello")], timestamp)],
|
||||
AgentContext(system_prompt, messages, tools),
|
||||
config,
|
||||
nothing,
|
||||
stream_fn,
|
||||
)
|
||||
|
||||
# Consume events
|
||||
final_messages = []
|
||||
for event in stream
|
||||
if event isa MessageEndEvent
|
||||
push!(final_messages, event.message)
|
||||
end
|
||||
end
|
||||
|
||||
# Or use event sink
|
||||
messages = []
|
||||
emit(event) = push!(messages, event)
|
||||
|
||||
messages = runAgentLoop(
|
||||
[UserMessage(...)],
|
||||
context,
|
||||
config,
|
||||
emit,
|
||||
nothing,
|
||||
stream_fn,
|
||||
)
|
||||
```
|
||||
|
||||
This documentation provides a comprehensive understanding of the AgentLoop component, including its architecture, main functions, execution modes, and best practices for building AI agents with AgentCore.jl.
|
||||
@@ -0,0 +1,589 @@
|
||||
# AgentCore.jl - Types and Messages Deep Dive
|
||||
|
||||
## Core Type Hierarchy
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Type Hierarchy │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ThinkingLevel (Enum) │
|
||||
│ - THINKING_OFF │
|
||||
│ - THINKING_MINIMAL │
|
||||
│ - THINKING_LOW │
|
||||
│ - THINKING_MEDIUM │
|
||||
│ - THINKING_HIGH │
|
||||
│ - THINKING_XHIGH │
|
||||
│ - THINKING_MAX │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ToolExecutionMode (Enum) │
|
||||
│ - EXECUTION_SEQUENTIAL (Tools run one at a time) │
|
||||
│ - EXECUTION_PARALLEL (Tools run concurrently) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ QueueMode (Enum) │
|
||||
│ - QUEUE_ALL (Drain all messages at once) │
|
||||
│ - QUEUE_ONE_AT_A_TIME (Process one message at a time) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ MessageContent (Abstract Type) │
|
||||
│ ├── TextContent (String) │
|
||||
│ └── ImageContent (data::String, mime_type::String) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Message (Abstract Type) │
|
||||
│ ├── UserMessage │
|
||||
│ │ └─ role: "user", content: Message[], timestamp: Int64 │
|
||||
│ ├── AssistantMessage │
|
||||
│ │ └─ role: "assistant", content: Message[], api, provider, model, │
|
||||
│ │ usage: Usage, stop_reason, error_message, timestamp │
|
||||
│ └── ToolResultMessage │
|
||||
│ └─ role: "toolResult", tool_call_id, tool_name, content, details, │
|
||||
│ usage, added_tool_names, is_error, timestamp │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentMessage (Abstract Type) │
|
||||
│ └─ Union of all message types above + custom types │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentTool │
|
||||
│ - name: String │
|
||||
│ - label: String │
|
||||
│ - description: String │
|
||||
│ - parameters: Any │
|
||||
│ - execute: Function │
|
||||
│ - prepare_arguments: Union{Function, Nothing} │
|
||||
│ - execution_mode: Union{ToolExecutionMode, Nothing} │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentContext │
|
||||
│ - system_prompt: String │
|
||||
│ - messages: Vector{AgentMessage} │
|
||||
│ - tools: Union{Vector{AgentTool}, Nothing} │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentEvent (Abstract Type) │
|
||||
│ ├── AgentStartEvent / AgentEndEvent │
|
||||
│ ├── TurnStartEvent / TurnEndEvent │
|
||||
│ ├── MessageStartEvent / MessageEndEvent │
|
||||
│ ├── MessageUpdateEvent │
|
||||
│ ├── ToolExecutionStartEvent / ToolExecutionEndEvent │
|
||||
│ └── ToolExecutionUpdateEvent │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Usage & ModelCost │
|
||||
│ Usage: input, output, cache_read, cache_write, total_tokens, cost │
|
||||
│ ModelCost: input, output, cache_read, cache_write (all Float64) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Model │
|
||||
│ - id, name, api, provider, base_url, reasoning: Bool │
|
||||
│ - input: Vector{String} │
|
||||
│ - cost: ModelCost │
|
||||
│ - context_window, max_tokens: Int64 │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Message Types
|
||||
|
||||
### UserMessage
|
||||
|
||||
```julia
|
||||
struct UserMessage <: Message
|
||||
role::String # "user"
|
||||
content::Vector{MessageContent}
|
||||
timestamp::Timestamp # Int64 (Unix timestamp)
|
||||
end
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
# Simple text message
|
||||
UserMessage(
|
||||
"user",
|
||||
[TextContent("Hello, how are you?")],
|
||||
Int64(Dates.now(Dates.UTC).datetime)
|
||||
)
|
||||
|
||||
# With multiple content types
|
||||
UserMessage(
|
||||
"user",
|
||||
[
|
||||
TextContent("Analyze this image"),
|
||||
ImageContent(data_base64, "image/png")
|
||||
],
|
||||
timestamp
|
||||
)
|
||||
```
|
||||
|
||||
### AssistantMessage
|
||||
|
||||
```julia
|
||||
struct AssistantMessage <: Message
|
||||
role::String # "assistant"
|
||||
content::Vector{MessageContent}
|
||||
api::String # API identifier
|
||||
provider::String # Provider name
|
||||
model::String # Model ID
|
||||
usage::Usage
|
||||
stop_reason::String # "done", "error", "aborted", "length", etc.
|
||||
error_message::Union{String, Nothing}
|
||||
timestamp::Timestamp
|
||||
end
|
||||
```
|
||||
|
||||
**Content can include**:
|
||||
- TextContent
|
||||
- ToolCall
|
||||
|
||||
```julia
|
||||
AssistantMessage(
|
||||
"assistant",
|
||||
[
|
||||
TextContent("I'll check the directory for you."),
|
||||
ToolCall(
|
||||
"tool",
|
||||
"tc_123",
|
||||
"bash",
|
||||
Dict("command" => "ls -la"),
|
||||
nothing
|
||||
),
|
||||
ToolCall(
|
||||
"tool",
|
||||
"tc_456",
|
||||
"read",
|
||||
Dict("path" => "README.md"),
|
||||
nothing
|
||||
)
|
||||
],
|
||||
"openai",
|
||||
"openai",
|
||||
"gpt-4",
|
||||
Usage(100, 50, 0, 0, 150, UsageCost(0.001, 0.002, 0.0, 0.0, 0.003)),
|
||||
"done",
|
||||
nothing,
|
||||
timestamp
|
||||
)
|
||||
```
|
||||
|
||||
### ToolResultMessage
|
||||
|
||||
```julia
|
||||
struct ToolResultMessage <: Message
|
||||
role::String # "toolResult"
|
||||
tool_call_id::String # Reference to original ToolCall
|
||||
tool_name::String # Name of tool that executed
|
||||
content::Vector{MessageContent}
|
||||
details::Any # Additional tool-specific details
|
||||
usage::Union{Usage, Nothing}
|
||||
added_tool_names::Union{Vector{String}, Nothing}
|
||||
is_error::Bool # True if tool execution failed
|
||||
timestamp::Timestamp
|
||||
end
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
ToolResultMessage(
|
||||
"toolResult",
|
||||
"tc_123",
|
||||
"bash",
|
||||
[TextContent("file1.md\nfile2.md\n")],
|
||||
BashToolDetails(...),
|
||||
nothing,
|
||||
nothing,
|
||||
false,
|
||||
timestamp
|
||||
)
|
||||
```
|
||||
|
||||
## AgentTool Structure
|
||||
|
||||
```julia
|
||||
struct AgentTool{TParameters, TDetails}
|
||||
name::String
|
||||
label::String
|
||||
description::String
|
||||
parameters::TParameters
|
||||
execute::Function
|
||||
prepare_arguments::Union{Function, Nothing}
|
||||
execution_mode::Union{ToolExecutionMode, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `name`: Unique identifier for the tool
|
||||
- `label`: Display name
|
||||
- `description`: What the tool does
|
||||
- `parameters`: JSON schema for tool arguments
|
||||
- `execute`: Main execution function
|
||||
- `prepare_arguments`: Optional preprocessing
|
||||
- `execution_mode`: Sequential or parallel
|
||||
|
||||
### Tool Execution Function Signature
|
||||
|
||||
```julia
|
||||
execute::Function(
|
||||
tool_call_id::String,
|
||||
params::Dict{String, Any},
|
||||
signal::Union{Any, Nothing}, # Abort signal
|
||||
on_update::Function, # Callback for streaming updates
|
||||
context::Any, # Tool context
|
||||
)::AgentToolResult
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
```julia
|
||||
AgentToolResult(
|
||||
content::Vector{MessageContent}, # Result content
|
||||
details::T, # Tool-specific details
|
||||
usage::Union{Usage, Nothing}, # Usage statistics
|
||||
added_tool_names::Union{Vector{String}, Nothing},
|
||||
terminate::Union{Bool, Nothing}, # If true, stop agent after this
|
||||
)
|
||||
```
|
||||
|
||||
## AgentContext
|
||||
|
||||
```julia
|
||||
struct AgentContext
|
||||
system_prompt::String
|
||||
messages::Vector{AgentMessage}
|
||||
tools::Union{Vector{AgentTool}, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Purpose**: Read-only snapshot of agent state for LLM calls
|
||||
|
||||
**Usage in AgentLoop**:
|
||||
```julia
|
||||
function streamAssistantResponse(
|
||||
context::AgentContext, # Contains messages, tools, system prompt
|
||||
config::AgentLoopConfig,
|
||||
...
|
||||
)::AssistantMessage
|
||||
# Convert to LLM format
|
||||
llm_messages = config.convert_to_llm(context.messages)
|
||||
|
||||
# Create context for API
|
||||
llm_context = Context(
|
||||
context.system_prompt,
|
||||
llm_messages,
|
||||
context.tools,
|
||||
)
|
||||
|
||||
# Call LLM
|
||||
return stream_function(context.model, llm_context, config)
|
||||
end
|
||||
```
|
||||
|
||||
## Event Types
|
||||
|
||||
### Agent Lifecycle Events
|
||||
|
||||
```julia
|
||||
struct AgentStartEvent <: AgentEvent end
|
||||
struct AgentEndEvent <: AgentEvent
|
||||
messages::Vector{AgentMessage}
|
||||
end
|
||||
```
|
||||
|
||||
### Turn Events
|
||||
|
||||
```julia
|
||||
struct TurnStartEvent <: AgentEvent end
|
||||
struct TurnEndEvent <: AgentEvent
|
||||
message::AgentMessage
|
||||
tool_results::Vector{ToolResultMessage}
|
||||
end
|
||||
```
|
||||
|
||||
### Message Events
|
||||
|
||||
```julia
|
||||
struct MessageStartEvent <: AgentEvent
|
||||
message::AgentMessage
|
||||
end
|
||||
struct MessageUpdateEvent <: AgentEvent
|
||||
message::AgentMessage
|
||||
assistant_message_event::Any # Partial message event
|
||||
end
|
||||
struct MessageEndEvent <: AgentEvent
|
||||
message::AgentMessage
|
||||
end
|
||||
```
|
||||
|
||||
### Tool Execution Events
|
||||
|
||||
```julia
|
||||
struct ToolExecutionStartEvent <: AgentEvent
|
||||
tool_call_id::String
|
||||
tool_name::String
|
||||
args::Any
|
||||
end
|
||||
struct ToolExecutionUpdateEvent <: AgentEvent
|
||||
tool_call_id::String
|
||||
tool_name::String
|
||||
args::Any
|
||||
partial_result::Any
|
||||
end
|
||||
struct ToolExecutionEndEvent <: AgentEvent
|
||||
tool_call_id::String
|
||||
tool_name::String
|
||||
result::Any
|
||||
is_error::Bool
|
||||
end
|
||||
```
|
||||
|
||||
## Usage Statistics
|
||||
|
||||
```julia
|
||||
struct Usage
|
||||
input::Int64 # Input tokens
|
||||
output::Int64 # Output tokens
|
||||
cache_read::Int64 # Cache read tokens
|
||||
cache_write::Int64 # Cache write tokens
|
||||
total_tokens::Int64 # Total tokens
|
||||
cost::UsageCost
|
||||
end
|
||||
|
||||
struct UsageCost
|
||||
input::Float64
|
||||
output::Float64
|
||||
cache_read::Float64
|
||||
cache_write::Float64
|
||||
total::Float64
|
||||
end
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```julia
|
||||
Usage(
|
||||
1000, # input tokens
|
||||
200, # output tokens
|
||||
500, # cache read tokens
|
||||
0, # cache write tokens
|
||||
1700, # total tokens
|
||||
UsageCost(
|
||||
0.0005, # input cost ($0.50 per 1M tokens)
|
||||
0.0015, # output cost ($1.50 per 1M tokens)
|
||||
0.00025, # cache read cost
|
||||
0.0, # cache write cost
|
||||
0.0035 # total cost
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Model Type
|
||||
|
||||
```julia
|
||||
struct Model{Api}
|
||||
id::String # Model identifier (e.g., "gpt-4")
|
||||
name::String # Model name (e.g., "GPT-4")
|
||||
api::Api # API type (String, Symbol, or custom type)
|
||||
provider::String # Provider name (e.g., "openai")
|
||||
base_url::String # API base URL
|
||||
reasoning::Bool # Whether model supports reasoning
|
||||
input::Vector{String} # Input modes (e.g., ["text", "image"])
|
||||
cost::ModelCost
|
||||
context_window::Int64 # Max context window (e.g., 128000)
|
||||
max_tokens::Int64 # Max output tokens
|
||||
end
|
||||
|
||||
struct ModelCost
|
||||
input::Float64
|
||||
output::Float64
|
||||
cache_read::Float64
|
||||
cache_write::Float64
|
||||
end
|
||||
```
|
||||
|
||||
## ToolCall Type
|
||||
|
||||
```julia
|
||||
struct ToolCall
|
||||
type::String # "tool"
|
||||
id::String # Unique ID for this tool call
|
||||
name::String # Tool name to call
|
||||
arguments::Dict{String, Any} # Tool arguments as JSON-like Dict
|
||||
partial_json::Union{String, Nothing} # Partial JSON string
|
||||
end
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```julia
|
||||
ToolCall(
|
||||
"tool",
|
||||
"call_abc123",
|
||||
"bash",
|
||||
Dict(
|
||||
"command" => "ls -la",
|
||||
"timeout" => 30
|
||||
),
|
||||
nothing
|
||||
)
|
||||
```
|
||||
|
||||
## Custom Message Types
|
||||
|
||||
### BashExecutionMessage
|
||||
|
||||
```julia
|
||||
mutable struct BashExecutionMessage
|
||||
role::String # "custom"
|
||||
command::String
|
||||
output::String
|
||||
exit_code::Union{Int64, Nothing}
|
||||
cancelled::Bool
|
||||
truncated::Bool
|
||||
full_output_path::Union{String, Nothing}
|
||||
timestamp::Timestamp
|
||||
exclude_from_context::Bool
|
||||
end
|
||||
```
|
||||
|
||||
### CompactionSummaryMessage
|
||||
|
||||
```julia
|
||||
mutable struct CompactionSummaryMessage
|
||||
role::String # "compactionSummary"
|
||||
summary::String # Summary of compacted history
|
||||
tokens_before::Int64 # Context size before compaction
|
||||
timestamp::Timestamp
|
||||
end
|
||||
```
|
||||
|
||||
### BranchSummaryMessage
|
||||
|
||||
```julia
|
||||
mutable struct BranchSummaryMessage
|
||||
role::String # "branchSummary"
|
||||
summary::String # Summary of branch history
|
||||
from_id::String # Branch point ID
|
||||
timestamp::Timestamp
|
||||
end
|
||||
```
|
||||
|
||||
## AgentState
|
||||
|
||||
```julia
|
||||
mutable struct AgentState
|
||||
system_prompt::String
|
||||
model::Model
|
||||
thinking_level::ThinkingLevel
|
||||
tools::Vector{AgentTool}
|
||||
messages::Vector{AgentMessage}
|
||||
is_streaming::Bool
|
||||
streaming_message::Union{AgentMessage, Nothing}
|
||||
pending_tool_calls::Set{String}
|
||||
error_message::Union{String, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Purpose**: Runtime state of the Agent
|
||||
|
||||
**Note**: AgentState is mutable and used internally by Agent
|
||||
|
||||
## Key Conversion Functions
|
||||
|
||||
### convertToLlm()
|
||||
|
||||
```julia
|
||||
function convertToLlm(messages::Vector{AgentMessage})::Vector{Message}
|
||||
result::Vector{Message} = Message[]
|
||||
|
||||
for m in messages
|
||||
converted = convertToLlmMessage(m)
|
||||
if !isnothing(converted)
|
||||
push!(result, converted)
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
```
|
||||
|
||||
**Purpose**: Transform AgentMessage[] to Message[] for LLM API
|
||||
|
||||
**Example**:
|
||||
```julia
|
||||
# Input: AgentMessage[]
|
||||
[
|
||||
UserMessage(...),
|
||||
AssistantMessage(...),
|
||||
ToolResultMessage(...),
|
||||
BashExecutionMessage(...), # Will be converted to UserMessage
|
||||
CompactionSummaryMessage(...), # Will be converted to UserMessage
|
||||
]
|
||||
|
||||
# Output: Message[]
|
||||
[
|
||||
UserMessage(...),
|
||||
AssistantMessage(...),
|
||||
ToolResultMessage(...),
|
||||
UserMessage(...), # Converted from BashExecutionMessage
|
||||
UserMessage(...), # Converted from CompactionSummaryMessage
|
||||
]
|
||||
```
|
||||
|
||||
### Default convertToLlmMessage Implementations
|
||||
|
||||
```julia
|
||||
function convertToLlmMessage(m::BashExecutionMessage)
|
||||
if m.exclude_from_context
|
||||
return nothing
|
||||
end
|
||||
return UserMessage("user", [TextContent(bashExecutionToText(m))], m.timestamp)
|
||||
end
|
||||
|
||||
function convertToLlmMessage(m::CompactionSummaryMessage)
|
||||
text = COMPACTION_SUMMARY_PREFIX * m.summary * COMPACTION_SUMMARY_SUFFIX
|
||||
return UserMessage("user", [TextContent(text)], m.timestamp)
|
||||
end
|
||||
|
||||
function convertToLlmMessage(m::BranchSummaryMessage)
|
||||
text = BRANCH_SUMMARY_PREFIX * m.summary * BRANCH_SUMMARY_SUFFIX
|
||||
return UserMessage("user", [TextContent(text)], m.timestamp)
|
||||
end
|
||||
|
||||
function convertToLlmMessage(m::UserMessage)
|
||||
return m # Pass through
|
||||
end
|
||||
|
||||
function convertToLlmMessage(m::AssistantMessage)
|
||||
return m # Pass through
|
||||
end
|
||||
|
||||
function convertToLlmMessage(m::ToolResultMessage)
|
||||
return m # Pass through
|
||||
end
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The type system in AgentCore.jl provides:
|
||||
|
||||
1. **Strong typing** for different message types
|
||||
2. **Extensibility** through abstract types and multiple dispatch
|
||||
3. **Clear separation** between internal (AgentMessage) and external (Message) formats
|
||||
4. **Rich metadata** in Usage and Model types for cost tracking
|
||||
5. **Event-driven architecture** through Event types
|
||||
6. **Tool execution flexibility** through Tool types with hooks
|
||||
|
||||
All types are designed for:
|
||||
- **Interoperability** with LLM APIs
|
||||
- **Extensibility** for custom message types
|
||||
- **Performance** with immutable structs where possible
|
||||
- **Debuggability** through rich event system
|
||||
@@ -0,0 +1,763 @@
|
||||
# AgentCore.jl - Session Management Deep Dive
|
||||
|
||||
## Session Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Session Layer │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Session = Tree of Entries │
|
||||
│ │
|
||||
│ Each entry represents a change in conversation state │
|
||||
│ │
|
||||
│ Branch Navigation: │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ E1 │────▶│ E2 │────▶│ E3 │────▶│ E4 │────▶│ E5 │ (current leaf) │
|
||||
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
|
||||
│ │ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ ▼ │
|
||||
│ Message Message Compaction Message BranchSummary │
|
||||
│ │
|
||||
│ To navigate to E2 (fork point): │
|
||||
│ Session.moveTo(E2) │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │────▶│ E5' │ (new branch) │
|
||||
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
|
||||
│ │ │ │
|
||||
│ │ ▼ create BranchSummary │
|
||||
│ │ ┌─────┐ │
|
||||
│ └──────│ E6 │ (branch summary) │
|
||||
│ └─────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Entry Types
|
||||
|
||||
```julia
|
||||
abstract type SessionTreeEntry end
|
||||
```
|
||||
|
||||
### 1. MessageEntry
|
||||
|
||||
```julia
|
||||
struct MessageEntry <: SessionTreeEntry
|
||||
type::String # "message"
|
||||
id::String # Unique entry ID
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String # ISO 8601 timestamp
|
||||
message::AgentMessage # The actual message
|
||||
end
|
||||
```
|
||||
|
||||
**Represents**: A user, assistant, or tool message
|
||||
|
||||
### 2. ThinkingLevelChangeEntry
|
||||
|
||||
```julia
|
||||
struct ThinkingLevelChangeEntry <: SessionTreeEntry
|
||||
type::String # "thinking_level_change"
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
thinking_level::String # "off", "minimal", "low", "medium", etc.
|
||||
end
|
||||
```
|
||||
|
||||
**Represents**: Change in model thinking level
|
||||
|
||||
### 3. ModelChangeEntry
|
||||
|
||||
```julia
|
||||
struct ModelChangeEntry <: SessionTreeEntry
|
||||
type::String # "model_change"
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
provider::String # "openai", "anthropic", etc.
|
||||
model_id::String # Model identifier
|
||||
end
|
||||
```
|
||||
|
||||
**Represents**: Change in model
|
||||
|
||||
### 4. ActiveToolsChangeEntry
|
||||
|
||||
```julia
|
||||
struct ActiveToolsChangeEntry <: SessionTreeEntry
|
||||
type::String # "active_tools_change"
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
active_tool_names::Vector{String}
|
||||
end
|
||||
```
|
||||
|
||||
**Represents**: Change in active tools
|
||||
|
||||
### 5. CompactionEntry
|
||||
|
||||
```julia
|
||||
struct CompactionEntry <: SessionTreeEntry
|
||||
type::String # "compaction"
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
summary::String # Summary of compacted history
|
||||
first_kept_entry_id::Union{String, Nothing}
|
||||
tokens_before::Int64 # Context size before compaction
|
||||
retained_tail::Union{Vector{AgentMessage}, Nothing}
|
||||
details::Union{Any, Nothing}
|
||||
usage::Union{Usage, Nothing}
|
||||
from_hook::Bool # Whether triggered by hook
|
||||
end
|
||||
```
|
||||
|
||||
**Represents**: Context window compression
|
||||
|
||||
**Key fields**:
|
||||
- `summary`: Summary of removed messages
|
||||
- `first_kept_entry_id`: First entry that was kept
|
||||
- `tokens_before`: Context size before compaction
|
||||
- `retained_tail`: Messages kept after compaction point
|
||||
|
||||
### 6. BranchSummaryEntry
|
||||
|
||||
```julia
|
||||
struct BranchSummaryEntry <: SessionTreeEntry
|
||||
type::String # "branch_summary"
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
from_id::String # Branch point entry ID
|
||||
summary::String # Summary of branch history
|
||||
details::Union{Any, Nothing}
|
||||
usage::Union{Usage, Nothing}
|
||||
from_hook::Bool
|
||||
end
|
||||
```
|
||||
|
||||
**Represents**: Branch point with summary
|
||||
|
||||
### 7. CustomEntry
|
||||
|
||||
```julia
|
||||
struct CustomEntry <: SessionTreeEntry
|
||||
type::String # Custom type
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
custom_type::String
|
||||
data::Union{Any, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Represents**: Custom application-specific data
|
||||
|
||||
### 8. CustomMessageEntry
|
||||
|
||||
```julia
|
||||
struct CustomMessageEntry <: SessionTreeEntry
|
||||
type::String
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
custom_type::String
|
||||
content::String
|
||||
details::Union{Any, Nothing}
|
||||
display::Bool
|
||||
end
|
||||
```
|
||||
|
||||
**Represents**: Custom message to display to user
|
||||
|
||||
### 9. LabelEntry
|
||||
|
||||
```julia
|
||||
struct LabelEntry <: SessionTreeEntry
|
||||
type::String
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
target_id::String # Entry being labeled
|
||||
label::Union{String, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Represents**: Label/note on an entry
|
||||
|
||||
### 10. SessionInfoEntry
|
||||
|
||||
```julia
|
||||
struct SessionInfoEntry <: SessionTreeEntry
|
||||
type::String
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
name::Union{String, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Represents**: Session metadata (name, etc.)
|
||||
|
||||
### 11. LeafEntry
|
||||
|
||||
```julia
|
||||
struct LeafEntry <: SessionTreeEntry
|
||||
type::String
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
target_id::Union{String, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Represents**: Change in current leaf (branch pointer)
|
||||
|
||||
## Session Storage Interface
|
||||
|
||||
```julia
|
||||
abstract type SessionStorage{T<:SessionMetadata} end
|
||||
```
|
||||
|
||||
### Storage Methods
|
||||
|
||||
```julia
|
||||
# Metadata
|
||||
getMetadata(storage::SessionStorage)::Promise{T}
|
||||
|
||||
# Leaf management
|
||||
getLeafId(storage::SessionStorage)::Promise{Union{String, Nothing}}
|
||||
setLeafId(storage::SessionStorage, leaf_id::String)::Promise{Nothing}
|
||||
|
||||
# Entry management
|
||||
createEntryId(storage::SessionStorage)::Promise{String}
|
||||
appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Promise{Nothing}
|
||||
getEntry(storage::SessionStorage, id::String)::Promise{Union{SessionTreeEntry, Nothing}}
|
||||
|
||||
# Query
|
||||
findEntries(storage::SessionStorage, type::String)::Promise{Vector{SessionTreeEntry}}
|
||||
getLabel(storage::SessionStorage, id::String)::Promise{Union{String, Nothing}}
|
||||
getSessionName(storage::SessionStorage)::Promise{Union{String, Nothing}}
|
||||
|
||||
# Branch navigation
|
||||
getPathToRootOrCompaction(
|
||||
storage::SessionStorage,
|
||||
leaf_id::String,
|
||||
)::Promise{Vector{SessionTreeEntry}}
|
||||
|
||||
getEntries(storage::SessionStorage, options::Dict{String, Any})::Promise{Vector{SessionTreeEntry}}
|
||||
|
||||
# Stats
|
||||
getSessionStats(storage::SessionStorage)::Promise{SessionStats}
|
||||
```
|
||||
|
||||
## JsonlSessionStorage
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ JSONL Storage Format │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
File: session.jsonl
|
||||
|
||||
Entry 1 (Metadata):
|
||||
{"type":"session","id":"meta_1","created_at":"2024-01-01T00:00:00Z","cwd":"/path","path":"/path/session.jsonl"}
|
||||
|
||||
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"}]}}
|
||||
|
||||
Entry 3 (Thinking Level):
|
||||
{"type":"thinking_level_change","id":"tl_1","parent_id":"msg_1","timestamp":"2024-01-01T00:00:02Z","thinking_level":"medium"}
|
||||
|
||||
Entry 4 (Model Change):
|
||||
{"type":"model_change","id":"mc_1","parent_id":"tl_1","timestamp":"2024-01-01T00:00:03Z","provider":"openai","model_id":"gpt-4"}
|
||||
|
||||
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}
|
||||
|
||||
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"}
|
||||
|
||||
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"]}
|
||||
|
||||
Entry 8 (Leaf):
|
||||
{"type":"leaf","id":"leaf_1","parent_id":"tools_1","timestamp":"2024-01-01T00:00:07Z","target_id":"msg_5"}
|
||||
|
||||
Notes:
|
||||
- Each line is a JSON object (JSONL format)
|
||||
- parent_id references previous entry (linked list structure)
|
||||
- Leaf entry points to current position in tree
|
||||
- To fork, create new branch from any entry
|
||||
```
|
||||
|
||||
## InMemorySessionStorage
|
||||
|
||||
```julia
|
||||
mutable struct InMemorySessionStorage
|
||||
metadata::SessionMetadata
|
||||
leaf_id::Union{String, Nothing}
|
||||
entries::Dict{String, SessionTreeEntry}
|
||||
labels::Dict{String, String}
|
||||
end
|
||||
```
|
||||
|
||||
**Purpose**: Testing and temporary sessions
|
||||
|
||||
**Advantages**:
|
||||
- Fast (no I/O)
|
||||
- Easy to inspect
|
||||
- Perfect for tests
|
||||
|
||||
## Session Class
|
||||
|
||||
```julia
|
||||
mutable struct Session{T<:SessionMetadata}
|
||||
storage::SessionStorage{T}
|
||||
context_build_options::SessionContextBuildOptions
|
||||
end
|
||||
```
|
||||
|
||||
### Session Methods
|
||||
|
||||
#### appendMessage()
|
||||
|
||||
```julia
|
||||
function appendMessage(session::Session, message::AgentMessage)::String
|
||||
entry = MessageEntry(
|
||||
"message",
|
||||
createEntryId(session.storage),
|
||||
getLeafId(session.storage),
|
||||
create_timestamp(),
|
||||
message,
|
||||
)
|
||||
return appendTypedEntry(session, entry)
|
||||
end
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
session = Session(storage)
|
||||
|
||||
# Add user message
|
||||
user_id = appendMessage(session, UserMessage("user", [TextContent("Hello")], timestamp))
|
||||
|
||||
# Add assistant message
|
||||
assistant_id = appendMessage(session, AssistantMessage(...))
|
||||
|
||||
# Add tool result
|
||||
tool_id = appendMessage(session, ToolResultMessage(...))
|
||||
```
|
||||
|
||||
#### appendThinkingLevelChange()
|
||||
|
||||
```julia
|
||||
function appendThinkingLevelChange(
|
||||
session::Session,
|
||||
thinking_level::String,
|
||||
)::String
|
||||
entry = ThinkingLevelChangeEntry(
|
||||
"thinking_level_change",
|
||||
createEntryId(session.storage),
|
||||
getLeafId(session.storage),
|
||||
create_timestamp(),
|
||||
thinking_level,
|
||||
)
|
||||
return appendTypedEntry(session, entry)
|
||||
end
|
||||
```
|
||||
|
||||
#### appendCompaction()
|
||||
|
||||
```julia
|
||||
function appendCompaction(
|
||||
session::Session,
|
||||
summary::String,
|
||||
first_kept_entry_id::Union{String, Nothing},
|
||||
tokens_before::Int64,
|
||||
details::Union{Any, Nothing}=nothing,
|
||||
from_hook::Bool=false,
|
||||
usage::Union{Usage, Nothing}=nothing,
|
||||
retained_tail::Union{Vector{AgentMessage}, Nothing}=nothing,
|
||||
)::String
|
||||
entry = CompactionEntry(
|
||||
"compaction",
|
||||
createEntryId(session.storage),
|
||||
getLeafId(session.storage),
|
||||
create_timestamp(),
|
||||
summary,
|
||||
first_kept_entry_id,
|
||||
tokens_before,
|
||||
retained_tail,
|
||||
details,
|
||||
usage,
|
||||
from_hook,
|
||||
)
|
||||
return appendTypedEntry(session, entry)
|
||||
end
|
||||
```
|
||||
|
||||
#### moveTo()
|
||||
|
||||
```julia
|
||||
function moveTo(
|
||||
session::Session,
|
||||
entry_id::Union{String, Nothing},
|
||||
summary::Union{Dict{String, Any}, Nothing}=nothing,
|
||||
)::Union{String, Nothing}
|
||||
# Set new leaf
|
||||
setLeafId(session.storage, entry_id)
|
||||
|
||||
# Optionally create branch summary
|
||||
if !isnothing(summary)
|
||||
return appendTypedEntry(session, BranchSummaryEntry(
|
||||
"branch_summary",
|
||||
createEntryId(session.storage),
|
||||
entry_id,
|
||||
create_timestamp(),
|
||||
entry_id,
|
||||
summary["summary"],
|
||||
get(summary, "details", nothing),
|
||||
get(summary, "usage", nothing),
|
||||
get(summary, "from_hook", false),
|
||||
))
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
# Fork from a specific point
|
||||
session.moveTo(msg_3_id)
|
||||
|
||||
# Branch with summary
|
||||
session.moveTo(
|
||||
msg_3_id,
|
||||
Dict(
|
||||
"summary" => "User wanted to focus on file operations",
|
||||
"details" => Dict("focus" => "files"),
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Build Session Context
|
||||
|
||||
```julia
|
||||
function buildSessionContext(
|
||||
path_entries::Vector{SessionTreeEntry},
|
||||
options::SessionContextBuildOptions=SessionContextBuildOptions(),
|
||||
)::SessionContext
|
||||
state = deriveSessionContextState(path_entries)
|
||||
context_entries = buildContextEntries(path_entries, options)
|
||||
messages = SessionTreeEntry[]
|
||||
for (i, entry) in enumerate(context_entries)
|
||||
append!(messages, sessionEntryToContextMessages(entry, i, context_entries, options))
|
||||
end
|
||||
return SessionContext(messages, state.thinking_level, state.model, state.active_tool_names)
|
||||
end
|
||||
```
|
||||
|
||||
### Context Entry Transform
|
||||
|
||||
```julia
|
||||
function defaultContextEntryTransform(
|
||||
path_entries::Vector{SessionTreeEntry},
|
||||
)::Vector{SessionTreeEntry}
|
||||
compaction = nothing
|
||||
for entry in path_entries
|
||||
if entry isa CompactionEntry
|
||||
compaction = entry
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if isnothing(compaction)
|
||||
return copy(path_entries)
|
||||
end
|
||||
|
||||
# Include compaction entry
|
||||
entries = [compaction]
|
||||
|
||||
# Include retained tail if present
|
||||
if !isnothing(compaction.retained_tail)
|
||||
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
|
||||
append!(entries, path_entries[compaction_idx+1:end])
|
||||
return entries
|
||||
end
|
||||
|
||||
# Otherwise include entries after first_kept_entry_id
|
||||
if !isnothing(compaction.first_kept_entry_id)
|
||||
found_first_kept = false
|
||||
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
|
||||
for i in 1:compaction_idx-1
|
||||
entry = path_entries[i]
|
||||
if entry.id == compaction.first_kept_entry_id
|
||||
found_first_kept = true
|
||||
end
|
||||
if found_first_kept
|
||||
push!(entries, entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Include entries after compaction
|
||||
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
|
||||
append!(entries, path_entries[compaction_idx+1:end])
|
||||
|
||||
return entries
|
||||
end
|
||||
```
|
||||
|
||||
### Session Entry to Context Messages
|
||||
|
||||
```julia
|
||||
function sessionEntryToContextMessages(
|
||||
entry::SessionTreeEntry,
|
||||
index::Int64,
|
||||
entries::Vector{SessionTreeEntry},
|
||||
options::SessionContextBuildOptions=SessionContextBuildOptions(),
|
||||
)::Vector{AgentMessage}
|
||||
if entry isa MessageEntry
|
||||
return [entry.message]
|
||||
end
|
||||
|
||||
if entry isa CustomMessageEntry
|
||||
return [createCustomMessage(
|
||||
entry.custom_type,
|
||||
entry.content,
|
||||
entry.display,
|
||||
entry.details,
|
||||
entry.timestamp,
|
||||
)]
|
||||
end
|
||||
|
||||
if entry isa CompactionEntry
|
||||
messages = [createCompactionSummaryMessage(
|
||||
entry.summary,
|
||||
entry.tokens_before,
|
||||
entry.timestamp,
|
||||
)]
|
||||
if !isnothing(entry.retained_tail)
|
||||
append!(messages, entry.retained_tail)
|
||||
end
|
||||
return messages
|
||||
end
|
||||
|
||||
if entry isa BranchSummaryEntry
|
||||
return [createBranchSummaryMessage(
|
||||
entry.summary,
|
||||
entry.from_id,
|
||||
entry.timestamp,
|
||||
)]
|
||||
end
|
||||
|
||||
if entry isa CustomEntry
|
||||
# Custom projectors can transform custom entries
|
||||
if !isnothing(options.entry_projectors) && haskey(options.entry_projectors, entry.custom_type)
|
||||
projector = options.entry_projectors[entry.custom_type]
|
||||
return projector(entry, index, entries)
|
||||
end
|
||||
return AgentMessage[]
|
||||
end
|
||||
|
||||
return AgentMessage[]
|
||||
end
|
||||
```
|
||||
|
||||
## Branch Navigation
|
||||
|
||||
```
|
||||
Scenario: User wants to explore a different path
|
||||
|
||||
Initial Branch (current path):
|
||||
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
|
||||
│ E1 │────▶│ E2 │────▶│ E3 │────▶│ E4 │ (leaf)
|
||||
└─────┘ └─────┘ └─────┘ └─────┘
|
||||
│ │ │ │
|
||||
Message Message Compaction Message
|
||||
|
||||
Step 1: Fork from E2
|
||||
┌─────┐ ┌─────┐ ┌─────┐
|
||||
│ E1 │────▶│ E2 │─────────────────┐
|
||||
└─────┘ └─────┘ │
|
||||
│ │ │
|
||||
│ ▼ create BranchSummary│
|
||||
│ ┌─────┐ │
|
||||
│ │ E5 │ (branch summary) │
|
||||
│ └─────┘ │
|
||||
└──────────────────────────────────┘
|
||||
(new branch from E2)
|
||||
|
||||
Step 2: Continue on new branch
|
||||
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
|
||||
│ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │────▶│ E5' │ (new leaf)
|
||||
└─────┘ └─────┘ └─────┘ └─────┘ └─────┘
|
||||
|
||||
Current branch now is:
|
||||
[ E1, E2, E3', E4', E5' ]
|
||||
|
||||
Original branch is:
|
||||
[ E1, E2, E5 ] (E3, E4 are now separate branch)
|
||||
|
||||
Key Points:
|
||||
- Shared entries: E1, E2
|
||||
- Branch point: E2
|
||||
- Branch summary: E5 (points to E2)
|
||||
- Each branch has independent tail
|
||||
```
|
||||
|
||||
## Compaction Strategy
|
||||
|
||||
### Why Compaction?
|
||||
|
||||
LLM context windows have limits:
|
||||
- GPT-4: 128K tokens
|
||||
- Claude 2: 100K tokens
|
||||
- Llama 2: 4K tokens
|
||||
|
||||
**Problem**: Conversations grow unbounded
|
||||
**Solution**: Compaction - summarize old messages
|
||||
|
||||
### Compaction Process
|
||||
|
||||
```julia
|
||||
# 1. Identify messages to compact
|
||||
# - Keep recent N messages (e.g., last 2 turns)
|
||||
# - Summarize everything before
|
||||
|
||||
# 2. Generate summary
|
||||
# - Use LLM to summarize
|
||||
# - Include key facts, decisions, user preferences
|
||||
|
||||
# 3. Create CompactionEntry
|
||||
# - summary: The summary text
|
||||
# - first_kept_entry_id: First entry that was NOT compacted
|
||||
# - tokens_before: Context size before compaction
|
||||
# - retained_tail: Messages kept after compaction point
|
||||
|
||||
# 4. Update storage
|
||||
# - Append CompactionEntry
|
||||
# - Update leaf to CompactionEntry
|
||||
```
|
||||
|
||||
### Compaction Example
|
||||
|
||||
```julia
|
||||
# Before compaction (100K tokens):
|
||||
[
|
||||
msg_1, # User: "I need to set up a project"
|
||||
msg_2, # Assistant: "Sure, what language?"
|
||||
msg_3, # User: "Python"
|
||||
msg_4, # Assistant: "I'll create a Python project"
|
||||
msg_5, # User: "With FastAPI"
|
||||
msg_6, # Assistant: "Creating FastAPI project..."
|
||||
msg_7, # Tool: bash("mkdir myapp")
|
||||
msg_8, # Tool: write("myapp/main.py", ...)
|
||||
msg_9, # Assistant: "Project created!"
|
||||
msg_10, # User: "Can you add auth?"
|
||||
msg_11, # Assistant: "Adding auth..."
|
||||
msg_12, # User: "Use JWT"
|
||||
msg_13, # Assistant: "Implementing JWT..."
|
||||
msg_14, # Tool: bash("pip install jwt")
|
||||
msg_15, # Tool: write("myapp/auth.py", ...)
|
||||
msg_16, # Assistant: "Auth implemented!"
|
||||
]
|
||||
|
||||
# After compaction (20K tokens):
|
||||
[
|
||||
compaction_entry, # Summary of msg_1 to msg_10
|
||||
msg_11, # Keep recent messages
|
||||
msg_12,
|
||||
msg_13,
|
||||
msg_14,
|
||||
msg_15,
|
||||
msg_16,
|
||||
]
|
||||
|
||||
# Compaction summary:
|
||||
"""
|
||||
Previous conversation summary:
|
||||
- User wanted to create a Python project
|
||||
- Chose FastAPI framework
|
||||
- Assistant created project structure in myapp/
|
||||
- User requested authentication
|
||||
- Chose JWT for auth
|
||||
- Assistant implemented JWT auth in myapp/auth.py
|
||||
"""
|
||||
```
|
||||
|
||||
## Complete Session Example
|
||||
|
||||
```julia
|
||||
using AgentCore
|
||||
|
||||
# 1. Create storage
|
||||
storage = JsonlSessionStorage(
|
||||
SessionMetadata("session_1", "2024-01-01T00:00:00Z"),
|
||||
"/path/to/session.jsonl",
|
||||
)
|
||||
|
||||
# 2. Create session
|
||||
session = Session(storage)
|
||||
|
||||
# 3. Add messages
|
||||
msg1_id = appendMessage(session, UserMessage("user", [TextContent("Hello")], timestamp))
|
||||
msg2_id = appendMessage(session, AssistantMessage("assistant", [TextContent("Hi!")], ...))
|
||||
|
||||
# 4. Change thinking level
|
||||
tl_id = appendThinkingLevelChange(session, "medium")
|
||||
|
||||
# 5. Change model
|
||||
mc_id = appendModelChange(session, "openai", "gpt-4")
|
||||
|
||||
# 6. Add more messages
|
||||
msg3_id = appendMessage(session, UserMessage("user", [TextContent("What can you do?")], timestamp))
|
||||
msg4_id = appendMessage(session, AssistantMessage("assistant", [TextContent("I can...")], ...))
|
||||
|
||||
# 7. Compact context (100K tokens → 20K)
|
||||
compact_id = appendCompaction(
|
||||
session,
|
||||
"User asked about capabilities and assistant explained",
|
||||
msg2_id,
|
||||
100000,
|
||||
Dict("summary_length" => 50),
|
||||
false,
|
||||
usage,
|
||||
[msg3, msg4], # Retained tail
|
||||
)
|
||||
|
||||
# 8. Fork and branch
|
||||
session.moveTo(msg2_id) # Go back to msg2
|
||||
|
||||
# 9. Create new branch
|
||||
branch_id = appendBranchSummary(
|
||||
session,
|
||||
"User changed direction to focus on file operations",
|
||||
msg2_id,
|
||||
Dict("focus" => "files"),
|
||||
)
|
||||
|
||||
# 10. Continue on new branch
|
||||
msg5_id = appendMessage(session, UserMessage("user", [TextContent("Let's work with files")], timestamp))
|
||||
|
||||
# 11. Query session context
|
||||
context = buildSessionContext(session)
|
||||
|
||||
# 12. Get stats
|
||||
stats = getSessionStats(session)
|
||||
println("Messages: $(stats.message_count)")
|
||||
println("Total tokens: $(stats.total_tokens)")
|
||||
println("Cost: $$(stats.cost_total)")
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use compaction** for long conversations to stay within context limits
|
||||
2. **Create branch summaries** when forking to document divergent paths
|
||||
3. **Retain tail messages** after compaction for context
|
||||
4. **Track token usage** to optimize compaction timing
|
||||
5. **Use InMemorySessionStorage** for testing
|
||||
@@ -0,0 +1,767 @@
|
||||
# AgentCore.jl - Tools Deep Dive
|
||||
|
||||
## Tool Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tool Layer │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentTool │
|
||||
│ - name: String (identifier) │
|
||||
│ - label: String (display name) │
|
||||
│ - description: String (what it does) │
|
||||
│ - parameters: JSON schema │
|
||||
│ - execute::Function (main logic) │
|
||||
│ - prepare_arguments::Union{Function, Nothing} │
|
||||
│ - execution_mode::Union{ToolExecutionMode, Nothing} │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ BashTool │ │ ReadTool │ │ WriteTool │
|
||||
│ - bash() │ │ - read() │ │ - write() │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
┌─────────────┐
|
||||
│ EditTool │
|
||||
│ - edit() │
|
||||
└─────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tool Execution Flow │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Assistant Message
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ AssistantMessage: │
|
||||
│ content: [ │
|
||||
│ TextContent("I'll check the files..."), │
|
||||
│ ToolCall("bash", {command: "ls -la"}), │
|
||||
│ ToolCall("read", {path: "README.md"}) │
|
||||
│ ] │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ AgentLoop.executeToolCalls() │
|
||||
│ - Extract ToolCalls from message content │
|
||||
│ - Determine execution mode (sequential/parallel) │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
│
|
||||
├─► executeToolCallsSequential()
|
||||
│ (for tools that require order)
|
||||
│
|
||||
└─► executeToolCallsParallel()
|
||||
(for independent tools)
|
||||
|
||||
│
|
||||
├─► prepareToolCall()
|
||||
│ - before_tool_call hook (optional)
|
||||
│ - validate arguments
|
||||
│ - prepare arguments (optional)
|
||||
│
|
||||
├─► execute()
|
||||
│ - Tool-specific logic
|
||||
│ - Return AgentToolResult
|
||||
│
|
||||
├─► finalizeExecutedToolCall()
|
||||
│ - after_tool_call hook (optional)
|
||||
│
|
||||
└─► createToolResultMessage()
|
||||
- Emit ToolResultMessage
|
||||
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ ToolResultMessage │
|
||||
│ - tool_call_id: "ref to original ToolCall" │
|
||||
│ - tool_name: "bash" │
|
||||
│ - content: [TextContent("file1.md\nfile2.md\n")] │
|
||||
│ - is_error: false │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ AgentState.messages.append(tool_result) │
|
||||
│ - Next turn: LLM sees tool results │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Built-in Tools
|
||||
|
||||
### 1. BashTool
|
||||
|
||||
```julia
|
||||
struct BashToolOptions{TContext}
|
||||
command_prefix::Union{String, Nothing}
|
||||
prepare::Union{BashPrepare{TContext}, Nothing}
|
||||
end
|
||||
|
||||
struct BashPrepare{TContext}
|
||||
function::Function
|
||||
context::TContext
|
||||
signal::Union{Any, Nothing}
|
||||
end
|
||||
|
||||
struct BashToolDetails
|
||||
truncation::Union{Any, Nothing}
|
||||
full_output_path::Union{String, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
#### createBashTool()
|
||||
|
||||
```julia
|
||||
function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing)
|
||||
return AgentTool(
|
||||
"bash",
|
||||
"bash",
|
||||
"Execute a bash command in the current working directory.",
|
||||
Dict{String, Any}(),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Execute command
|
||||
result = executeBashCommand(params, signal, on_update)
|
||||
|
||||
# Return result
|
||||
return AgentToolResult(
|
||||
[TextContent(result.output)],
|
||||
BashToolDetails(result.truncation, result.full_path),
|
||||
nothing,
|
||||
nothing,
|
||||
result.terminate,
|
||||
)
|
||||
end,
|
||||
nothing, # prepare_arguments
|
||||
nothing, # execution_mode (default: use config)
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
**Parameters Schema**:
|
||||
```json
|
||||
{
|
||||
"command": "string",
|
||||
"timeout": "number (optional)",
|
||||
"cwd": "string (optional)",
|
||||
"env": "object (optional)"
|
||||
}
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```julia
|
||||
# Create tool
|
||||
bash_tool = createBashTool()
|
||||
|
||||
# Agent receives command
|
||||
tool_call = ToolCall("tool", "tc1", "bash", Dict(
|
||||
"command" => "ls -la",
|
||||
"timeout" => 30
|
||||
), nothing)
|
||||
|
||||
# Execute
|
||||
result = bash_tool.execute(
|
||||
"tc1",
|
||||
Dict("command" => "ls -la", "timeout" => 30),
|
||||
nothing,
|
||||
on_update, # Callback for streaming output
|
||||
nothing,
|
||||
)
|
||||
|
||||
# Result
|
||||
AgentToolResult(
|
||||
[TextContent("total 12\n-rw-r--r-- 1 user user 100 Jan 1 file1.md\n-rw-r--r-- 1 user user 200 Jan 2 file2.md\n")],
|
||||
BashToolDetails(truncation_info, nothing),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
```
|
||||
|
||||
### 2. ReadTool
|
||||
|
||||
```julia
|
||||
struct ReadToolOptions{TContext}
|
||||
max_size::Union{Int64, Nothing}
|
||||
max_lines::Union{Int64, Nothing}
|
||||
image_processor::Union{ReadImageProcessor, Nothing}
|
||||
prepare::Union{ReadPrepare{TContext}, Nothing}
|
||||
end
|
||||
|
||||
struct ReadImageProcessor
|
||||
function::Function
|
||||
context::Any
|
||||
end
|
||||
|
||||
struct ReadImageProcessorResult
|
||||
content::Vector{MessageContent}
|
||||
usage::Union{Usage, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
#### createReadTool()
|
||||
|
||||
```julia
|
||||
function createReadTool{TContext}(options::Union{ReadToolOptions{TContext}, Nothing}=nothing)
|
||||
return AgentTool(
|
||||
"read",
|
||||
"read",
|
||||
"Read a file from the file system.",
|
||||
Dict{String, Any}(),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Read file
|
||||
result = readFileSystem(params, signal, options)
|
||||
|
||||
# Process content
|
||||
content = if isImage(params.path)
|
||||
# Image processing
|
||||
image_result = options.image_processor.function(result.path, context)
|
||||
image_result.content
|
||||
else
|
||||
# Text content
|
||||
[TextContent(result.content)]
|
||||
end
|
||||
|
||||
return AgentToolResult(
|
||||
content,
|
||||
ReadToolDetails(result.size, result.truncated, result.full_path),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
**Parameters Schema**:
|
||||
```json
|
||||
{
|
||||
"path": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```julia
|
||||
# Create tool
|
||||
read_tool = createReadTool()
|
||||
|
||||
# Agent requests to read file
|
||||
tool_call = ToolCall("tool", "tc2", "read", Dict(
|
||||
"path" => "src/main.jl"
|
||||
), nothing)
|
||||
|
||||
# Execute
|
||||
result = read_tool.execute("tc2", Dict("path" => "src/main.jl"), nothing, nothing, nothing)
|
||||
|
||||
# Result
|
||||
AgentToolResult(
|
||||
[TextContent("module Main\nfunction main()\n println(\"Hello\")\nend\nend\n")],
|
||||
ReadToolDetails(1234, false, "/path/to/src/main.jl"),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
```
|
||||
|
||||
### 3. WriteTool
|
||||
|
||||
```julia
|
||||
struct WriteToolInput
|
||||
path::String
|
||||
content::String
|
||||
end
|
||||
```
|
||||
|
||||
#### createWriteTool()
|
||||
|
||||
```julia
|
||||
function createWriteTool{TContext}(options::Union{WriteToolOptions{TContext}, Nothing}=nothing)
|
||||
return AgentTool(
|
||||
"write",
|
||||
"write",
|
||||
"Write content to a file.",
|
||||
Dict{String, Any}(),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Write file
|
||||
result = writeToFile(params, signal)
|
||||
|
||||
return AgentToolResult(
|
||||
[TextContent(result.message)],
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
**Parameters Schema**:
|
||||
```json
|
||||
{
|
||||
"path": "string",
|
||||
"content": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```julia
|
||||
# Create tool
|
||||
write_tool = createWriteTool()
|
||||
|
||||
# Agent wants to write file
|
||||
tool_call = ToolCall("tool", "tc3", "write", Dict(
|
||||
"path" => "output.txt",
|
||||
"content" => "Hello World"
|
||||
), nothing)
|
||||
|
||||
# Execute
|
||||
result = write_tool.execute("tc3", Dict(
|
||||
"path" => "output.txt",
|
||||
"content" => "Hello World"
|
||||
), nothing, nothing, nothing)
|
||||
|
||||
# Result
|
||||
AgentToolResult(
|
||||
[TextContent("File written: output.txt (11 bytes)")],
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
```
|
||||
|
||||
### 4. EditTool
|
||||
|
||||
```julia
|
||||
struct EditToolInput
|
||||
path::String
|
||||
find::String
|
||||
replacement::String
|
||||
end
|
||||
|
||||
struct EditToolDetails
|
||||
edits::Vector{Edit}
|
||||
before_content::String
|
||||
after_content::String
|
||||
end
|
||||
```
|
||||
|
||||
#### createEditTool()
|
||||
|
||||
```julia
|
||||
function createEditTool{TContext}(options::Union{EditToolOptions{TContext}, Nothing}=nothing)
|
||||
return AgentTool(
|
||||
"edit",
|
||||
"edit",
|
||||
"Edit a file by finding and replacing text.",
|
||||
Dict{String, Any}(),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Read file
|
||||
before_content = read(params.path)
|
||||
|
||||
# Apply edit
|
||||
after_content = replace(before_content, params.find => params.replacement)
|
||||
|
||||
# Write file
|
||||
write(params.path, after_content)
|
||||
|
||||
return AgentToolResult(
|
||||
[TextContent("Edit applied successfully")],
|
||||
EditToolDetails([Edit(params.find, params.replacement)], before_content, after_content),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
**Parameters Schema**:
|
||||
```json
|
||||
{
|
||||
"path": "string",
|
||||
"find": "string",
|
||||
"replacement": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```julia
|
||||
# Create tool
|
||||
edit_tool = createEditTool()
|
||||
|
||||
# Agent wants to replace text
|
||||
tool_call = ToolCall("tool", "tc4", "edit", Dict(
|
||||
"path" => "README.md",
|
||||
"find" => "v1.0.0",
|
||||
"replacement" => "v2.0.0"
|
||||
), nothing)
|
||||
|
||||
# Execute
|
||||
result = edit_tool.execute("tc4", Dict(
|
||||
"path" => "README.md",
|
||||
"find" => "v1.0.0",
|
||||
"replacement" => "v2.0.0"
|
||||
), nothing, nothing, nothing)
|
||||
|
||||
# Result
|
||||
AgentToolResult(
|
||||
[TextContent("Edit applied: README.md")],
|
||||
EditToolDetails([Edit("v1.0.0", "v2.0.0")], "Version 1.0.0", "Version 2.0.0"),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
```
|
||||
|
||||
## Tool Execution Hooks
|
||||
|
||||
### before_tool_call
|
||||
|
||||
```julia
|
||||
struct BeforeToolCallContext
|
||||
assistant_message::AssistantMessage
|
||||
tool_call::ToolCall
|
||||
args::Any
|
||||
context::AgentContext
|
||||
end
|
||||
|
||||
struct BeforeToolCallResult
|
||||
block::Union{Bool, Nothing}
|
||||
reason::Union{String, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
function myBeforeToolCall(context, signal)
|
||||
tool_name = context.tool_call.name
|
||||
|
||||
# Block dangerous commands
|
||||
if tool_name == "bash" && contains(context.args["command"], "rm -rf /")
|
||||
return BeforeToolCallResult(
|
||||
true,
|
||||
"Blocking dangerous command: rm -rf /"
|
||||
)
|
||||
end
|
||||
|
||||
# Log tool execution
|
||||
println("Executing tool: $tool_name")
|
||||
|
||||
return nothing # Allow execution
|
||||
end
|
||||
|
||||
# Configure agent
|
||||
agent = Agent(Dict(
|
||||
:beforeToolCall => myBeforeToolCall,
|
||||
))
|
||||
```
|
||||
|
||||
### after_tool_call
|
||||
|
||||
```julia
|
||||
struct AfterToolCallContext
|
||||
assistant_message::AssistantMessage
|
||||
tool_call::ToolCall
|
||||
args::Any
|
||||
result::AgentToolResult
|
||||
is_error::Bool
|
||||
context::AgentContext
|
||||
end
|
||||
|
||||
struct AfterToolCallResult
|
||||
content::Union{Vector{MessageContent}, Nothing}
|
||||
details::Union{Any, Nothing}
|
||||
is_error::Union{Bool, Nothing}
|
||||
usage::Union{Usage, Nothing}
|
||||
terminate::Union{Bool, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
function myAfterToolCall(context, signal)
|
||||
tool_name = context.tool_call.name
|
||||
|
||||
# Modify bash output
|
||||
if tool_name == "bash"
|
||||
# Add timestamp to output
|
||||
new_content = [
|
||||
TextContent("[Executed at $(Dates.now())]\n"),
|
||||
context.result.content[1],
|
||||
]
|
||||
return AfterToolCallResult(
|
||||
content = new_content,
|
||||
details = context.result.details,
|
||||
is_error = context.is_error,
|
||||
usage = context.result.usage,
|
||||
terminate = context.result.terminate,
|
||||
)
|
||||
end
|
||||
|
||||
return nothing # Use original result
|
||||
end
|
||||
|
||||
# Configure agent
|
||||
agent = Agent(Dict(
|
||||
:afterToolCall => myAfterToolCall,
|
||||
))
|
||||
```
|
||||
|
||||
### prepare_next_turn
|
||||
|
||||
```julia
|
||||
struct PrepareNextTurnContext
|
||||
message::AssistantMessage
|
||||
tool_results::Vector{ToolResultMessage}
|
||||
context::AgentContext
|
||||
new_messages::Vector{AgentMessage}
|
||||
end
|
||||
|
||||
struct AgentLoopTurnUpdate
|
||||
context::Union{AgentContext, Nothing}
|
||||
model::Union{Model, Nothing}
|
||||
thinking_level::Union{ThinkingLevel, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
function myPrepareNextTurn(context, signal)
|
||||
# Check if we should use a different model
|
||||
last_message = context.message
|
||||
tool_results = context.tool_results
|
||||
|
||||
# If tool execution had errors, use more capable model
|
||||
has_errors = any(r -> r.is_error, tool_results)
|
||||
if has_errors
|
||||
return AgentLoopTurnUpdate(
|
||||
context = context.context,
|
||||
model = Model("gpt-4", "GPT-4", "openai", "openai", "", ...),
|
||||
thinking_level = THINKING_HIGH,
|
||||
)
|
||||
end
|
||||
|
||||
return nothing # Keep current settings
|
||||
end
|
||||
|
||||
# Configure agent
|
||||
agent = Agent(Dict(
|
||||
:prepareNextTurn => myPrepareNextTurn,
|
||||
))
|
||||
```
|
||||
|
||||
## Tool Execution Modes
|
||||
|
||||
### Sequential Execution
|
||||
|
||||
```julia
|
||||
# Tools run one at a time, in order
|
||||
# Use case: Tools that modify shared state
|
||||
|
||||
# Configure tool
|
||||
bash_tool = AgentTool(
|
||||
"bash",
|
||||
"bash",
|
||||
"Execute bash command",
|
||||
...,
|
||||
execute,
|
||||
nothing,
|
||||
EXECUTION_SEQUENTIAL, # Force sequential
|
||||
)
|
||||
|
||||
# Or configure globally
|
||||
agent = Agent(Dict(
|
||||
:toolExecution => EXECUTION_SEQUENTIAL,
|
||||
))
|
||||
```
|
||||
|
||||
**Example Scenario**:
|
||||
```julia
|
||||
# Sequential execution (correct order)
|
||||
|
||||
1. Tool 1: create_directory("build/")
|
||||
└─ Creates build/ directory
|
||||
|
||||
2. Tool 2: write("build/app.js", "...")
|
||||
└─ Writes file to build/
|
||||
|
||||
(If parallel: might fail because build/ doesn't exist yet)
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```julia
|
||||
# Tools run concurrently
|
||||
# Use case: Independent operations
|
||||
|
||||
# Default behavior
|
||||
agent = Agent(Dict(
|
||||
:toolExecution => EXECUTION_PARALLEL, # Default
|
||||
))
|
||||
```
|
||||
|
||||
**Example Scenario**:
|
||||
```julia
|
||||
# Parallel execution (independent operations)
|
||||
|
||||
1. Tool 1: read("README.md") ─────┐
|
||||
2. Tool 2: read("CHANGELOG.md") ─┼─► Run simultaneously
|
||||
3. Tool 3: read("LICENSE") ──────┘
|
||||
|
||||
(Parallel: All three read operations can happen at once)
|
||||
(Sequential: Would wait for each read to complete)
|
||||
```
|
||||
|
||||
## Custom Tools
|
||||
|
||||
### Example: Database Tool
|
||||
|
||||
```julia
|
||||
function createDatabaseTool()
|
||||
return AgentTool(
|
||||
"database",
|
||||
"database",
|
||||
"Execute SQL queries against the database.",
|
||||
Dict{String, Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"query" => Dict("type" => "string"),
|
||||
"params" => Dict("type" => "array", "items" => Dict("type" => "string")),
|
||||
),
|
||||
"required" => ["query"],
|
||||
),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Execute query
|
||||
query = params["query"]
|
||||
result = executeQuery(query)
|
||||
|
||||
# Format output
|
||||
output = formatQueryResult(result)
|
||||
|
||||
return AgentToolResult(
|
||||
[TextContent(output)],
|
||||
Dict("rows_affected" => result.rows_affected),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
EXECUTION_SEQUENTIAL,
|
||||
)
|
||||
end
|
||||
|
||||
# Usage
|
||||
db_tool = createDatabaseTool()
|
||||
agent = Agent(Dict(:tools => [db_tool]))
|
||||
```
|
||||
|
||||
### Example: HTTP Request Tool
|
||||
|
||||
```julia
|
||||
function createHTTPTool()
|
||||
return AgentTool(
|
||||
"http",
|
||||
"http",
|
||||
"Make HTTP requests.",
|
||||
Dict{String, Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"url" => Dict("type" => "string"),
|
||||
"method" => Dict("type" => "string", "enum" => ["GET", "POST", "PUT", "DELETE"]),
|
||||
"body" => Dict("type" => "string"),
|
||||
"headers" => Dict("type" => "object"),
|
||||
),
|
||||
"required" => ["url", "method"],
|
||||
),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Make request
|
||||
url = params["url"]
|
||||
method = params["method"]
|
||||
body = get(params, "body", nothing)
|
||||
headers = get(params, "headers", Dict())
|
||||
|
||||
response = makeHTTPRequest(method, url, body, headers)
|
||||
|
||||
return AgentToolResult(
|
||||
[TextContent(response.body)],
|
||||
Dict(
|
||||
"status_code" => response.status_code,
|
||||
"headers" => response.headers,
|
||||
),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
EXECUTION_PARALLEL,
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```julia
|
||||
using AgentCore
|
||||
|
||||
# 1. Create tools
|
||||
bash_tool = createBashTool()
|
||||
read_tool = createReadTool()
|
||||
write_tool = createWriteTool()
|
||||
|
||||
# 2. Configure hooks
|
||||
before_hook = (context, signal) -> begin
|
||||
println("About to execute: $(context.tool_call.name)")
|
||||
return nothing
|
||||
end
|
||||
|
||||
after_hook = (context, signal) -> begin
|
||||
if context.is_error
|
||||
println("Tool failed: $(context.tool_call.name)")
|
||||
else
|
||||
println("Tool completed: $(context.tool_call.name)")
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
# 3. Create agent
|
||||
agent = Agent(Dict(
|
||||
:systemPrompt => "You are a helpful assistant with file system access.",
|
||||
:tools => [bash_tool, read_tool, write_tool],
|
||||
:beforeToolCall => before_hook,
|
||||
:afterToolCall => after_hook,
|
||||
))
|
||||
|
||||
# 4. Run conversation
|
||||
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
|
||||
|
||||
1. **Use sequential execution** for tools that depend on shared state
|
||||
2. **Use parallel execution** for independent operations
|
||||
3. **Implement before_tool_call hook** for logging and validation
|
||||
4. **Implement after_tool_call hook** for result modification
|
||||
5. **Use prepare_next_turn hook** for dynamic model/thinking level changes
|
||||
6. **Return terminate=true** from tool when agent should stop
|
||||
7. **Include usage statistics** in tool results when possible
|
||||
@@ -0,0 +1,754 @@
|
||||
# AgentCore.jl - AgentHarness Deep Dive
|
||||
|
||||
## AgentHarness Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentHarness Layer │
|
||||
└─────────────────────────────────────────────────────────────────────────────┐
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentHarness = Agent + Session + Resources │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ AgentHarness │ │
|
||||
│ │ - Manages Agent instances │ │
|
||||
│ │ - Provides Session persistence │ │
|
||||
│ │ - Manages resources (skills, prompt templates) │ │
|
||||
│ │ - Handles extension hooks │ │
|
||||
│ │ - Coordinates tool execution with context │ │
|
||||
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────┼─────────────────────┐ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Agent │ │ SessionRepo │ │ Resources │ │
|
||||
│ │ (state, │ │ (create, │ │ (skills, │ │
|
||||
│ │ events) │ │ open, │ │ templates) │ │
|
||||
│ └──────────────┘ │ list) │ └──────────────┘ │
|
||||
│ └──────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ Session │ │
|
||||
│ │ (history, │ │
|
||||
│ │ branching) │ │
|
||||
│ └──────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentHarnessEvent System │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
AgentEvent (from Agent)
|
||||
├─ AgentHarnessOwnEvent
|
||||
│ ├─ BeforeAgentStartEvent
|
||||
│ ├─ ContextEvent
|
||||
│ ├─ BeforeProviderRequestEvent
|
||||
│ ├─ BeforeProviderPayloadEvent
|
||||
│ ├─ AfterProviderResponseEvent
|
||||
│ ├─ ToolCallEvent
|
||||
│ ├─ ToolResultEvent
|
||||
│ ├─ SessionBeforeCompactEvent
|
||||
│ ├─ SessionCompactEvent
|
||||
│ ├─ SessionBeforeTreeEvent
|
||||
│ ├─ SessionTreeEvent
|
||||
│ ├─ ModelUpdateEvent
|
||||
│ ├─ ThinkingLevelUpdateEvent
|
||||
│ ├─ ToolsUpdateEvent
|
||||
│ ├─ ResourcesUpdateEvent
|
||||
│ └─ ... (other session events)
|
||||
|
||||
└─ AgentEvent (from AgentLoop)
|
||||
├─ AgentStartEvent / AgentEndEvent
|
||||
├─ TurnStartEvent / TurnEndEvent
|
||||
├─ MessageStartEvent / MessageEndEvent
|
||||
└─ ToolExecutionStartEvent / ToolExecutionEndEvent
|
||||
```
|
||||
|
||||
## AgentHarness Components
|
||||
|
||||
### 1. AgentHarnessOptions
|
||||
|
||||
```julia
|
||||
mutable struct AgentHarnessOptions{
|
||||
TC, TSkill<:Skill, TPromptTemplate<:PromptTemplate, TTool<:AgentHarnessTool
|
||||
}
|
||||
session::Session
|
||||
models::Any
|
||||
tools::Union{Vector{TTool}, Nothing}
|
||||
resources::Union{AgentHarnessResources{TSkill, TPromptTemplate}, Nothing}
|
||||
system_prompt::Union{AgentHarnessSystemPrompt{TC, TSkill, TPromptTemplate, TTool}, Nothing}
|
||||
stream_options::Union{AgentHarnessStreamOptions, Nothing}
|
||||
retry::Union{Any, Nothing}
|
||||
model::Model
|
||||
thinking_level::Union{ThinkingLevel, Nothing}
|
||||
active_tool_names::Union{Vector{String}, Nothing}
|
||||
steering_mode::Union{QueueMode, Nothing}
|
||||
follow_up_mode::Union{QueueMode, Nothing}
|
||||
tool_context::Union{AgentHarnessToolContextSource{TC}, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Purpose**: Configure AgentHarness with all necessary options
|
||||
|
||||
**Key fields**:
|
||||
- `session`: Session instance for persistence
|
||||
- `models`: Available models
|
||||
- `tools`: Agent tools
|
||||
- `resources`: Skills and prompt templates
|
||||
- `system_prompt`: System prompt (string or function)
|
||||
- `stream_options`: LLM streaming options
|
||||
- `model`: Default model
|
||||
- `thinking_level`: Default thinking level
|
||||
- `active_tool_names`: Active tools
|
||||
- `tool_context`: Context source for tools
|
||||
|
||||
### 2. AgentHarnessResources
|
||||
|
||||
```julia
|
||||
mutable struct AgentHarnessResources{TSkill<:Skill, TPromptTemplate<:PromptTemplate}
|
||||
promptTemplates::Union{Vector{TPromptTemplate}, Nothing}
|
||||
skills::Union{Vector{TSkill}, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Purpose**: Load and manage skills and prompt templates
|
||||
|
||||
### 3. Skill
|
||||
|
||||
```julia
|
||||
mutable struct Skill
|
||||
name::String
|
||||
description::String
|
||||
content::String
|
||||
filePath::String
|
||||
disableModelInvocation::Bool
|
||||
end
|
||||
```
|
||||
|
||||
**Purpose**: Define specialized instructions for specific tasks
|
||||
|
||||
**Format**:
|
||||
```markdown
|
||||
<!-- SKILL.md -->
|
||||
{
|
||||
"name": "File Operations",
|
||||
"description": "Handle file system operations",
|
||||
"disable-model-invocation": false
|
||||
}
|
||||
---
|
||||
|
||||
# File Operations Skill
|
||||
|
||||
This skill provides instructions for working with files...
|
||||
```
|
||||
|
||||
### 4. PromptTemplate
|
||||
|
||||
```julia
|
||||
mutable struct PromptTemplate
|
||||
name::String
|
||||
description::Union{String, Nothing}
|
||||
content::String
|
||||
end
|
||||
```
|
||||
|
||||
**Purpose**: Reusable prompt snippets with arguments
|
||||
|
||||
**Format**:
|
||||
```markdown
|
||||
<!-- template.md -->
|
||||
{
|
||||
"description": "Generate commit message"
|
||||
}
|
||||
---
|
||||
|
||||
Generate a git commit message for:
|
||||
$1
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
### 5. AgentHarnessStreamOptions
|
||||
|
||||
```julia
|
||||
mutable struct AgentHarnessStreamOptions
|
||||
transport::Union{String, Nothing}
|
||||
timeout_ms::Union{Int64, Nothing}
|
||||
max_retries::Union{Int64, Nothing}
|
||||
max_retry_delay_ms::Union{Int64, Nothing}
|
||||
headers::Union{Dict{String, String}, Nothing}
|
||||
metadata::Union{Dict{String, Any}, Nothing}
|
||||
cache_retention::Union{String, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
**Purpose**: Configure LLM API call options
|
||||
|
||||
## SessionRepo Interface
|
||||
|
||||
```julia
|
||||
abstract type SessionRepo<
|
||||
TMetadata<:SessionMetadata,
|
||||
TCreateOptions,
|
||||
TListOptions
|
||||
> end
|
||||
```
|
||||
|
||||
### Repo Methods
|
||||
|
||||
```julia
|
||||
# Create new session
|
||||
create(repo::SessionRepo, options::TCreateOptions)::Promise{Session}
|
||||
|
||||
# Open existing session
|
||||
open(repo::SessionRepo, metadata::TMetadata)::Promise{Session}
|
||||
|
||||
# List sessions
|
||||
list(repo::SessionRepo, options::TListOptions)::Promise{Vector{TMetadata}}
|
||||
|
||||
# Delete session
|
||||
delete(repo::SessionRepo, metadata::TMetadata)::Promise{Nothing}
|
||||
|
||||
# Fork session (create branch)
|
||||
fork(repo::SessionRepo, source::TMetadata, options::Dict{String, Any})::Promise{Session}
|
||||
```
|
||||
|
||||
### JsonlSessionRepo
|
||||
|
||||
```julia
|
||||
# JSONL-based session repository
|
||||
# - Sessions stored as JSONL files
|
||||
# - Supports create, open, list, delete, fork
|
||||
# - Branch navigation via session tree
|
||||
```
|
||||
|
||||
## Extension Hooks
|
||||
|
||||
### Hook Types
|
||||
|
||||
```julia
|
||||
# Before agent starts
|
||||
BeforeAgentStartEvent
|
||||
├─ prompt: String
|
||||
├─ images: Union{Vector{ImageContent}, Nothing}
|
||||
├─ system_prompt: String
|
||||
└─ resources: AgentHarnessResources
|
||||
|
||||
BeforeAgentStartResult
|
||||
├─ messages: Union{Vector{AgentMessage}, Nothing}
|
||||
└─ system_prompt: Union{String, Nothing}
|
||||
|
||||
# Context event
|
||||
ContextEvent
|
||||
└─ messages: Vector{AgentMessage}
|
||||
|
||||
ContextResult
|
||||
└─ messages: Vector{AgentMessage}
|
||||
|
||||
# Before LLM request
|
||||
BeforeProviderRequestEvent
|
||||
├─ model: Model
|
||||
├─ session_id: String
|
||||
└─ stream_options: AgentHarnessStreamOptions
|
||||
|
||||
BeforeProviderRequestResult
|
||||
└─ stream_options: Union{AgentHarnessStreamOptionsPatch, Nothing}
|
||||
|
||||
# Before LLM payload
|
||||
BeforeProviderPayloadEvent
|
||||
├─ model: Model
|
||||
└─ payload: Any
|
||||
|
||||
BeforeProviderPayloadResult
|
||||
└─ payload: Any
|
||||
|
||||
# After LLM response
|
||||
AfterProviderResponseEvent
|
||||
├─ status: Int64
|
||||
└─ headers: Dict{String, String}
|
||||
|
||||
# Tool call
|
||||
ToolCallEvent
|
||||
├─ tool_call_id: String
|
||||
├─ tool_name: String
|
||||
└─ input: Dict{String, Any}
|
||||
|
||||
ToolCallResult
|
||||
├─ block: Union{Bool, Nothing}
|
||||
└─ reason: Union{String, Nothing}
|
||||
|
||||
# Tool result
|
||||
ToolResultEvent
|
||||
├─ tool_call_id: String
|
||||
├─ tool_name: String
|
||||
├─ input: Dict{String, Any}
|
||||
├─ content: Vector{MessageContent}
|
||||
├─ details: Any
|
||||
├─ is_error: Bool
|
||||
└─ usage: Union{Usage, Nothing}
|
||||
|
||||
ToolResultPatch
|
||||
├─ content: Union{Vector{MessageContent}, Nothing}
|
||||
├─ details: Union{Any, Nothing}
|
||||
├─ is_error: Union{Bool, Nothing}
|
||||
├─ usage: Union{Usage, Nothing}
|
||||
└─ terminate: Union{Bool, Nothing}
|
||||
|
||||
# Session compaction
|
||||
SessionBeforeCompactEvent
|
||||
├─ preparation: Any
|
||||
├─ branch_entries: Vector{SessionTreeEntry}
|
||||
├─ custom_instructions: Union{String, Nothing}
|
||||
└─ signal: Any
|
||||
|
||||
SessionBeforeCompactResult
|
||||
├─ cancel: Union{Bool, Nothing}
|
||||
└─ compaction: Union{CompactResult, Nothing}
|
||||
|
||||
SessionCompactEvent
|
||||
├─ compaction_entry: CompactionEntry
|
||||
└─ from_hook: Bool
|
||||
|
||||
# Session tree (branching)
|
||||
SessionBeforeTreeEvent
|
||||
├─ preparation: Any
|
||||
└─ signal: Any
|
||||
|
||||
SessionBeforeTreeResult
|
||||
├─ cancel: Union{Bool, Nothing}
|
||||
├─ summary: Union{Dict{String, Any}, Nothing}
|
||||
├─ custom_instructions: Union{String, Nothing}
|
||||
├─ replace_instructions: Union{Bool, Nothing}
|
||||
└─ label: Union{String, Nothing}
|
||||
|
||||
SessionTreeEvent
|
||||
├─ new_leaf_id: Union{String, Nothing}
|
||||
├─ old_leaf_id: Union{String, Nothing}
|
||||
├─ summary_entry: Union{BranchSummaryEntry, Nothing}
|
||||
└─ from_hook: Union{Bool, Nothing}
|
||||
```
|
||||
|
||||
### Hook Usage Examples
|
||||
|
||||
#### BeforeAgentStartHook
|
||||
|
||||
```julia
|
||||
function beforeAgentStart(event, signal)
|
||||
# Modify system prompt based on context
|
||||
new_system_prompt = "$(event.system_prompt)\n\nUser prefers concise responses."
|
||||
|
||||
# Prepend initial messages
|
||||
initial_messages = [
|
||||
UserMessage("user", [TextContent("Context: $(event.prompt)")], timestamp),
|
||||
]
|
||||
|
||||
return BeforeAgentStartResult(
|
||||
initial_messages,
|
||||
new_system_prompt,
|
||||
)
|
||||
end
|
||||
|
||||
# Configure harness
|
||||
harness = AgentHarness(Dict(
|
||||
:beforeAgentStart => beforeAgentStart,
|
||||
))
|
||||
```
|
||||
|
||||
#### BeforeProviderPayloadHook
|
||||
|
||||
```julia
|
||||
function beforeProviderPayload(event, signal)
|
||||
# Modify LLM payload before sending
|
||||
payload = event.payload
|
||||
|
||||
# Add custom metadata
|
||||
payload.metadata = merge(payload.metadata, Dict(
|
||||
"session_id" => event.session_id,
|
||||
"timestamp" => Dates.now(),
|
||||
))
|
||||
|
||||
return BeforeProviderPayloadResult(payload)
|
||||
end
|
||||
```
|
||||
|
||||
#### ToolCallHook
|
||||
|
||||
```julia
|
||||
function toolCall(event, signal)
|
||||
# Block dangerous tool calls
|
||||
if event.tool_name == "bash" && contains(event.input["command"], "rm -rf /")
|
||||
return ToolCallResult(true, "Blocking dangerous command")
|
||||
end
|
||||
|
||||
# Log tool execution
|
||||
println("Tool call: $(event.tool_name)")
|
||||
|
||||
return nothing # Allow execution
|
||||
end
|
||||
```
|
||||
|
||||
#### BeforeCompactHook
|
||||
|
||||
```julia
|
||||
function beforeCompact(event, signal)
|
||||
# Add custom instructions for compaction
|
||||
custom_instructions = """
|
||||
Focus on retaining user preferences and key decisions.
|
||||
Omit verbose tool outputs that don't add value.
|
||||
"""
|
||||
|
||||
return SessionBeforeCompactResult(
|
||||
false, # Don't cancel
|
||||
Dict(
|
||||
"summary" => "Custom compaction with focus on user intent",
|
||||
"custom_instructions" => custom_instructions,
|
||||
),
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
## Tool Context
|
||||
|
||||
### AgentHarnessToolContextSource
|
||||
|
||||
```julia
|
||||
mutable struct AgentHarnessToolContextSource{TContext}
|
||||
context::Union{TContext, Function}
|
||||
end
|
||||
```
|
||||
|
||||
**Purpose**: Provide context to tools during execution
|
||||
|
||||
### Tool Execution Context
|
||||
|
||||
```julia
|
||||
# Tools receive context from AgentHarness
|
||||
tool.execute(
|
||||
tool_call_id,
|
||||
params,
|
||||
signal,
|
||||
on_update,
|
||||
context, # From AgentHarnessToolContextSource
|
||||
)
|
||||
|
||||
# Context can be:
|
||||
# - Static value
|
||||
# - Function that returns value
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```julia
|
||||
using AgentCore
|
||||
|
||||
# 1. Create skills
|
||||
skills, skill_diagnostics = loadSkills(
|
||||
execution_env,
|
||||
"/path/to/skills",
|
||||
)
|
||||
|
||||
# 2. Create prompt templates
|
||||
templates, template_diagnostics = loadPromptTemplates(
|
||||
execution_env,
|
||||
"/path/to/templates",
|
||||
)
|
||||
|
||||
# 3. Create resources
|
||||
resources = AgentHarnessResources(
|
||||
templates,
|
||||
skills,
|
||||
)
|
||||
|
||||
# 4. Create session repo
|
||||
repo = JsonlSessionRepo(
|
||||
"/path/to/sessions",
|
||||
)
|
||||
|
||||
# 5. Create session
|
||||
session = create(repo, Dict(
|
||||
"cwd" => "/path/to/project",
|
||||
"metadata" => Dict("project" => "my-project"),
|
||||
))
|
||||
|
||||
# 6. Configure tools
|
||||
bash_tool = createBashTool()
|
||||
read_tool = createReadTool()
|
||||
|
||||
tools = [bash_tool, read_tool]
|
||||
|
||||
# 7. Configure hooks
|
||||
hooks = Dict(
|
||||
:beforeAgentStart => beforeAgentStartHook,
|
||||
:beforeProviderPayload => beforePayloadHook,
|
||||
:toolCall => toolCallHook,
|
||||
)
|
||||
|
||||
# 8. Create harness
|
||||
harness = AgentHarness(Dict(
|
||||
:session => session,
|
||||
:models => models,
|
||||
:tools => tools,
|
||||
:resources => resources,
|
||||
:system_prompt => "You are a helpful assistant.",
|
||||
:model => Model(...),
|
||||
:thinking_level => THINKING_MEDIUM,
|
||||
:active_tool_names => ["bash", "read"],
|
||||
:steering_mode => QUEUE_ONE_AT_A_TIME,
|
||||
:follow_up_mode => QUEUE_ONE_AT_A_TIME,
|
||||
:tool_context => AgentHarnessToolContextSource(context),
|
||||
:stream_options => AgentHarnessStreamOptions(
|
||||
transport = "auto",
|
||||
timeout_ms = 30000,
|
||||
max_retries = 3,
|
||||
),
|
||||
))
|
||||
|
||||
# 9. Subscribe to events
|
||||
subscribe(harness) do event, signal
|
||||
if event isa BeforeAgentStartEvent
|
||||
println("Agent starting...")
|
||||
elseif event isa MessageEndEvent
|
||||
println("Message: $(event.message)")
|
||||
end
|
||||
end
|
||||
|
||||
# 10. Run conversation
|
||||
harness.prompt("What files are in the current directory?")
|
||||
|
||||
# 11. Wait for completion
|
||||
wait_for_idle(harness)
|
||||
|
||||
# 12. Manage branches
|
||||
session.moveTo(some_entry_id) # Fork from entry
|
||||
```
|
||||
|
||||
## Hook Execution Flow
|
||||
|
||||
```
|
||||
User Code
|
||||
│
|
||||
├─► AgentHarness.prompt()
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ BeforeAgentStartEvent │
|
||||
│ ├─ User prompt │
|
||||
│ ├─ System prompt │
|
||||
│ └─ Resources │
|
||||
│ │ │
|
||||
│ └─► beforeAgentStart hook (optional) │
|
||||
│ └─► BeforeAgentStartResult (optional modifications) │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ Agent.createLoopConfig() │
|
||||
│ └─► Merge options with hooks │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ Agent.prompt() │
|
||||
│ └─► Start AgentLoop │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentLoop.agentLoop() │
|
||||
│ │ │
|
||||
│ ├─► transform_context hook (optional) │
|
||||
│ └─► convert_to_llm() │
|
||||
│ └─► Message[] for LLM API │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ BeforeProviderRequestEvent │
|
||||
│ ├─ Model │
|
||||
│ ├─ Session ID │
|
||||
│ └─ Stream Options │
|
||||
│ │ │
|
||||
│ └─► beforeProviderRequest hook (optional) │
|
||||
│ └─► BeforeProviderRequestResult (optional modifications) │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ StreamFn (LLM API call) │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ AfterProviderResponseEvent │
|
||||
│ ├─ Status code │
|
||||
│ └─ Response headers │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ BeforeProviderPayloadEvent │
|
||||
│ ├─ Model │
|
||||
│ └─ Payload (before sending) │
|
||||
│ │ │
|
||||
│ └─► beforeProviderPayload hook (optional) │
|
||||
│ └─► BeforeProviderPayloadResult (optional modifications) │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ LLM API Request │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ Assistant Message (streaming) │
|
||||
│ │ │
|
||||
│ ├─► Text deltas │
|
||||
│ └─► Tool calls │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tool Execution (for each tool call) │
|
||||
│ │ │
|
||||
│ ├─► before_tool_call hook (Agent) │
|
||||
│ ├─► toolCall hook (Harness - optional) │
|
||||
│ │ └─► ToolCallResult (can block execution) │
|
||||
│ ├─► prepareToolCall() │
|
||||
│ ├─► execute() │
|
||||
│ │ └─► Tool execution with context │
|
||||
│ ├─► after_tool_call hook (Agent) │
|
||||
│ └─► toolResult hook (Harness - optional) │
|
||||
│ └─► ToolResultPatch (can modify result) │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentLoop continues with tool results │
|
||||
│ │ │
|
||||
│ ├─► Next LLM call with tool results │
|
||||
│ └─► Or end of conversation │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentEndEvent │
|
||||
│ └─► Final messages in session │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Session Management with Harness
|
||||
|
||||
```julia
|
||||
# Create harness with session repo
|
||||
repo = JsonlSessionRepo("/path/to/sessions")
|
||||
|
||||
# Create session
|
||||
session = create(repo, Dict(
|
||||
"cwd" => "/path/to/project",
|
||||
"metadata" => Dict("name" => "my-session"),
|
||||
))
|
||||
|
||||
# Or open existing session
|
||||
metadata = JsonlSessionMetadata(...)
|
||||
session = open(repo, metadata)
|
||||
|
||||
# List sessions
|
||||
sessions = list(repo, Dict())
|
||||
for meta in sessions
|
||||
println("Session: $(meta.id)")
|
||||
end
|
||||
|
||||
# Delete session
|
||||
delete(repo, metadata)
|
||||
|
||||
# Fork session (branch)
|
||||
forked_session = fork(repo, source_metadata, Dict(
|
||||
"summary" => "Branch for feature X",
|
||||
))
|
||||
```
|
||||
|
||||
## Resources Management
|
||||
|
||||
```julia
|
||||
# Load skills from directory
|
||||
skills, diagnostics = loadSkills(
|
||||
execution_env,
|
||||
"/path/to/skills",
|
||||
)
|
||||
|
||||
# Load prompt templates from directory
|
||||
templates, diagnostics = loadPromptTemplates(
|
||||
execution_env,
|
||||
"/path/to/templates",
|
||||
)
|
||||
|
||||
# Create resources
|
||||
resources = AgentHarnessResources(
|
||||
templates,
|
||||
skills,
|
||||
)
|
||||
|
||||
# Use in harness
|
||||
harness = AgentHarness(Dict(
|
||||
:resources => resources,
|
||||
))
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use hooks for logging and validation**
|
||||
- `beforeAgentStart` for initialization
|
||||
- `beforeProviderPayload` for custom metadata
|
||||
- `toolCall` for blocking dangerous operations
|
||||
|
||||
2. **Organize skills by domain**
|
||||
- File operations
|
||||
- Database queries
|
||||
- HTTP requests
|
||||
- Git operations
|
||||
|
||||
3. **Use templates for common patterns**
|
||||
- Commit message generation
|
||||
- Code review instructions
|
||||
- Testing prompts
|
||||
|
||||
4. **Manage sessions carefully**
|
||||
- Compact periodically
|
||||
- Use branches for exploration
|
||||
- Clean up old sessions
|
||||
|
||||
5. **Monitor resource usage**
|
||||
- Track token counts
|
||||
- Watch API costs
|
||||
- Optimize tool execution
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hook not being called
|
||||
|
||||
```julia
|
||||
# Check hook is registered
|
||||
if isnothing(harness.beforeAgentStart)
|
||||
println("Hook not registered")
|
||||
end
|
||||
```
|
||||
|
||||
### Session not persisting
|
||||
|
||||
```julia
|
||||
# Check repo is configured
|
||||
if isnothing(harness.repo)
|
||||
println("No repo configured")
|
||||
end
|
||||
```
|
||||
|
||||
### Resources not loading
|
||||
|
||||
```julia
|
||||
# Check diagnostics
|
||||
for diag in skill_diagnostics
|
||||
println("Skill warning: $(diag.message)")
|
||||
end
|
||||
```
|
||||
@@ -0,0 +1,893 @@
|
||||
# AgentCore.jl - Examples and Patterns
|
||||
|
||||
## Quick Start Examples
|
||||
|
||||
### Example 1: Basic Conversation
|
||||
|
||||
```julia
|
||||
using AgentCore
|
||||
|
||||
# Create model
|
||||
model = Model(
|
||||
"gpt-4",
|
||||
"GPT-4",
|
||||
"openai",
|
||||
"openai",
|
||||
"https://api.openai.com/v1",
|
||||
true,
|
||||
["text"],
|
||||
ModelCost(0.00003, 0.00006, 0.0, 0.0),
|
||||
128000,
|
||||
4096,
|
||||
)
|
||||
|
||||
# Create tools
|
||||
bash_tool = createBashTool()
|
||||
|
||||
# Create agent
|
||||
agent = Agent(Dict(
|
||||
:systemPrompt => "You are a helpful assistant.",
|
||||
:model => model,
|
||||
:tools => [bash_tool],
|
||||
:thinkingLevel => THINKING_MEDIUM,
|
||||
:toolExecution => EXECUTION_PARALLEL,
|
||||
))
|
||||
|
||||
# Subscribe to events
|
||||
subscribe(agent) do event, signal
|
||||
if event isa MessageEndEvent
|
||||
println("Agent: $(event.message)")
|
||||
end
|
||||
end
|
||||
|
||||
# Start conversation
|
||||
prompt(agent, "What's in the current directory?")
|
||||
|
||||
# Wait for completion
|
||||
wait_for_idle(agent)
|
||||
|
||||
# Get final state
|
||||
state = get_state(agent)
|
||||
println("Total messages: $(length(state.messages))")
|
||||
```
|
||||
|
||||
### Example 2: Conversation with Memory
|
||||
|
||||
```julia
|
||||
# Create session storage
|
||||
storage = JsonlSessionStorage(
|
||||
JsonlSessionMetadata(
|
||||
"session_1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
"/path/to/project",
|
||||
"/path/to/session.jsonl",
|
||||
nothing,
|
||||
Dict("project" => "my-project"),
|
||||
),
|
||||
"/path/to/session.jsonl",
|
||||
)
|
||||
|
||||
# Create session
|
||||
session = Session(storage)
|
||||
|
||||
# Create agent with session
|
||||
agent = Agent(Dict(
|
||||
:systemPrompt => "You are a helpful assistant.",
|
||||
:model => model,
|
||||
:tools => [bash_tool],
|
||||
:sessionId => session.getMetadata().id,
|
||||
))
|
||||
|
||||
# Add messages to session
|
||||
function addToSession(session, message)
|
||||
appendMessage(session, message)
|
||||
end
|
||||
|
||||
# Start conversation
|
||||
prompt(agent, "Hello, my name is Alice.")
|
||||
|
||||
# Continue conversation (messages persist in session)
|
||||
prompt(agent, "What's the weather like today?")
|
||||
|
||||
# Check session stats
|
||||
stats = getSessionStats(session)
|
||||
println("Messages: $(stats.message_count)")
|
||||
println("Total tokens: $(stats.total_tokens)")
|
||||
```
|
||||
|
||||
### Example 3: Steering and Follow-Up
|
||||
|
||||
```julia
|
||||
# Start conversation
|
||||
prompt(agent, "Create a Python project.")
|
||||
|
||||
# User wants to redirect
|
||||
steer(agent, UserMessage("user", [TextContent("Actually, let's use Node.js instead")], timestamp))
|
||||
|
||||
# Wait for redirection
|
||||
wait_for_idle(agent)
|
||||
|
||||
# Agent would normally stop, but user has more
|
||||
prompt(agent, "Wait, there's one more thing...")
|
||||
followUp(agent, UserMessage("user", [TextContent("Can you add tests?")], timestamp))
|
||||
|
||||
# Continue until completion
|
||||
while hasQueuedMessages(agent)
|
||||
wait_for_idle(agent)
|
||||
end
|
||||
```
|
||||
|
||||
### Example 4: Branching Conversations
|
||||
|
||||
```julia
|
||||
# Initial conversation
|
||||
prompt(agent, "I want to build a web app.")
|
||||
|
||||
# User decides to explore a different path
|
||||
session.moveTo(msg_3_id) # Go back to message 3
|
||||
|
||||
# Create branch
|
||||
appendBranchSummary(
|
||||
session,
|
||||
"User decided to explore mobile app instead",
|
||||
msg_3_id,
|
||||
Dict("focus" => "mobile"),
|
||||
)
|
||||
|
||||
# Continue on new branch
|
||||
prompt(agent, "Let's build a mobile app instead.")
|
||||
|
||||
# Check branches
|
||||
branch = getBranch(session)
|
||||
println("Current branch has $(length(branch)) entries")
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Pattern 1: Long-Running Agent with Compaction
|
||||
|
||||
```julia
|
||||
# Configure compaction settings
|
||||
MAX_TOKENS = 120000 # Stay under 128K limit
|
||||
COMPACTION_THRESHOLD = 100000
|
||||
|
||||
# Agent loop with compaction
|
||||
function runAgentWithCompaction(agent, session)
|
||||
while true
|
||||
# Get current token count
|
||||
stats = getSessionStats(session)
|
||||
|
||||
if stats.total_tokens > COMPACTION_THRESHOLD
|
||||
# Compact session
|
||||
compactSession(session)
|
||||
end
|
||||
|
||||
# Check if agent is idle
|
||||
if !hasQueuedMessages(agent) && !isnothing(agent.active_run)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function compactSession(session)
|
||||
# Get current branch
|
||||
branch = getBranch(session)
|
||||
|
||||
# Calculate tokens to compact
|
||||
total_tokens = 0
|
||||
for entry in branch
|
||||
if entry isa MessageEntry
|
||||
total_tokens += estimateTokens(entry.message)
|
||||
end
|
||||
end
|
||||
|
||||
if total_tokens < COMPACTION_THRESHOLD
|
||||
return
|
||||
end
|
||||
|
||||
# Identify messages to compact
|
||||
messages_to_compact = []
|
||||
tokens_to_keep = 50000 # Keep recent 50K tokens
|
||||
|
||||
for entry in branch
|
||||
if entry isa MessageEntry
|
||||
msg_tokens = estimateTokens(entry.message)
|
||||
if tokens_to_keep > 0
|
||||
tokens_to_keep -= msg_tokens
|
||||
else
|
||||
push!(messages_to_compact, entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Generate summary
|
||||
summary = generateSummary(messages_to_compact)
|
||||
|
||||
# Create compaction entry
|
||||
appendCompaction(
|
||||
session,
|
||||
summary,
|
||||
messages_to_compact[end].id,
|
||||
total_tokens,
|
||||
)
|
||||
|
||||
println("Compacted $(length(messages_to_compact)) messages")
|
||||
end
|
||||
|
||||
function estimateTokens(message::AgentMessage)::Int64
|
||||
# Simple estimation: ~4 chars per token
|
||||
content = if message isa UserMessage
|
||||
join([c.text for c in message.content if c isa TextContent])
|
||||
elseif message isa AssistantMessage
|
||||
join([c.text for c in message.content if c isa TextContent])
|
||||
elseif message isa ToolResultMessage
|
||||
join([c.text for c in message.content if c isa TextContent])
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
return ceil(Int, length(content) / 4)
|
||||
end
|
||||
|
||||
function generateSummary(messages::Vector{MessageEntry})::String
|
||||
# Use LLM to generate summary
|
||||
summary = "Conversation summary:"
|
||||
for msg in messages
|
||||
summary *= "\n- $(msg.message)"
|
||||
end
|
||||
return summary
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 2: Custom Tool with Context
|
||||
|
||||
```julia
|
||||
# Define context type
|
||||
struct DatabaseContext
|
||||
connection::Any
|
||||
user::String
|
||||
end
|
||||
|
||||
# Create tool with context
|
||||
function createDatabaseTool()
|
||||
return AgentTool(
|
||||
"database",
|
||||
"database",
|
||||
"Execute SQL queries",
|
||||
Dict{String, Any}(),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
if !isa(context, DatabaseContext)
|
||||
return AgentToolResult(
|
||||
[TextContent("Error: Database context not provided")],
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
true, # terminate
|
||||
)
|
||||
end
|
||||
|
||||
# Execute query
|
||||
query = params["query"]
|
||||
result = executeQuery(context.connection, query)
|
||||
|
||||
return AgentToolResult(
|
||||
[TextContent(formatResult(result))],
|
||||
Dict("user" => context.user),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
EXECUTION_SEQUENTIAL,
|
||||
)
|
||||
end
|
||||
|
||||
# Use tool with context
|
||||
db_context = DatabaseContext(connection, "alice")
|
||||
|
||||
harness = AgentHarness(Dict(
|
||||
:tools => [createDatabaseTool()],
|
||||
:tool_context => AgentHarnessToolContextSource(db_context),
|
||||
))
|
||||
```
|
||||
|
||||
### Pattern 3: Dynamic Model Selection
|
||||
|
||||
```julia
|
||||
# Hook to change model based on task
|
||||
function dynamicModelSelection(context, signal)
|
||||
# Check message content
|
||||
last_message = context.message
|
||||
|
||||
# If complex task, use more capable model
|
||||
if contains(join(last_message.content), "analyze")
|
||||
return AgentLoopTurnUpdate(
|
||||
context = context.context,
|
||||
model = Model("gpt-4", "GPT-4", "openai", ...),
|
||||
thinking_level = THINKING_HIGH,
|
||||
)
|
||||
end
|
||||
|
||||
# Otherwise use cheaper model
|
||||
return AgentLoopTurnUpdate(
|
||||
context = context.context,
|
||||
model = Model("gpt-3.5", "GPT-3.5", "openai", ...),
|
||||
thinking_level = THINKING_MEDIUM,
|
||||
)
|
||||
end
|
||||
|
||||
# Configure agent
|
||||
agent = Agent(Dict(
|
||||
:prepareNextTurn => dynamicModelSelection,
|
||||
))
|
||||
```
|
||||
|
||||
### Pattern 4: Rate Limiting
|
||||
|
||||
```julia
|
||||
# Rate limiter
|
||||
struct RateLimiter
|
||||
calls_per_minute::Int
|
||||
last_calls::Vector{DateTime}
|
||||
end
|
||||
|
||||
function RateLimiter(calls_per_minute::Int)
|
||||
return RateLimiter(calls_per_minute, DateTime[])
|
||||
end
|
||||
|
||||
function rateLimit(limiter::RateLimiter)
|
||||
now = Dates.now()
|
||||
|
||||
# Remove old calls
|
||||
limiter.last_calls = filter(
|
||||
c -> Dates.value(now - c) / 1000 < 60,
|
||||
limiter.last_calls,
|
||||
)
|
||||
|
||||
# Check limit
|
||||
if length(limiter.last_calls) >= limiter.calls_per_minute
|
||||
return false
|
||||
end
|
||||
|
||||
# Record call
|
||||
push!(limiter.last_calls, now)
|
||||
return true
|
||||
end
|
||||
|
||||
# Use in hook
|
||||
limiter = RateLimiter(60) # 60 calls per minute
|
||||
|
||||
function rateLimitHook(event, signal)
|
||||
if !rateLimit(limiter)
|
||||
return BeforeProviderPayloadResult(event.payload) # Still send, but track
|
||||
end
|
||||
|
||||
return BeforeProviderPayloadResult(event.payload)
|
||||
end
|
||||
|
||||
# Configure
|
||||
agent = Agent(Dict(
|
||||
:beforeProviderPayload => rateLimitHook,
|
||||
))
|
||||
```
|
||||
|
||||
### Pattern 5: Multi-Step Tool Execution
|
||||
|
||||
```julia
|
||||
# Tool that requires multiple steps
|
||||
function createMultiStepTool()
|
||||
return AgentTool(
|
||||
"multistep",
|
||||
"multistep",
|
||||
"Multi-step task",
|
||||
Dict{String, Any}(),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Step 1: Prepare
|
||||
on_update("Preparing...")
|
||||
prepare_result = prepareStep(params)
|
||||
|
||||
# Step 2: Execute
|
||||
on_update("Executing...")
|
||||
execute_result = executeStep(prepare_result, params)
|
||||
|
||||
# Step 3: Finalize
|
||||
on_update("Finalizing...")
|
||||
finalize_result = finalizeStep(execute_result)
|
||||
|
||||
return AgentToolResult(
|
||||
[TextContent(finalize_result)],
|
||||
Dict("steps" => 3),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
EXECUTION_SEQUENTIAL,
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 6: Image Processing
|
||||
|
||||
```julia
|
||||
# Create read tool with image support
|
||||
image_processor = ReadImageProcessor(
|
||||
(path, context) -> begin
|
||||
# Load image
|
||||
image_data = readImage(path)
|
||||
|
||||
# Process with vision model
|
||||
result = processImageWithVision(image_data)
|
||||
|
||||
return ReadImageProcessorResult(
|
||||
[TextContent(result.description)],
|
||||
result.usage,
|
||||
)
|
||||
end,
|
||||
context,
|
||||
)
|
||||
|
||||
read_tool = createReadTool(Dict(
|
||||
"image_processor" => image_processor,
|
||||
))
|
||||
```
|
||||
|
||||
### Pattern 7: Session Navigation
|
||||
|
||||
```julia
|
||||
# Navigate to specific point
|
||||
session.moveTo(entry_id)
|
||||
|
||||
# Get branch from specific point
|
||||
branch = getBranch(session, entry_id)
|
||||
|
||||
# Create label for easy navigation
|
||||
appendLabel(session, entry_id, "important-decision")
|
||||
|
||||
# Find labeled entry
|
||||
label = getLabel(session, "important-decision")
|
||||
|
||||
# Build context from branch
|
||||
context = buildSessionContext(session)
|
||||
|
||||
# Get specific messages
|
||||
messages = sessionEntryToContextMessages(entry, index, entries)
|
||||
```
|
||||
|
||||
### Pattern 8: Batch Processing
|
||||
|
||||
```julia
|
||||
# Process multiple prompts in batch
|
||||
prompts = [
|
||||
"What is Julia?",
|
||||
"What is JavaScript?",
|
||||
"What is Python?",
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
for prompt_text in prompts
|
||||
# Create fresh agent for each prompt
|
||||
agent = Agent(Dict(
|
||||
:systemPrompt => "You are a helpful assistant.",
|
||||
:model => model,
|
||||
:tools => [bash_tool],
|
||||
))
|
||||
|
||||
# Run prompt
|
||||
prompt(agent, prompt_text)
|
||||
wait_for_idle(agent)
|
||||
|
||||
# Get result
|
||||
state = get_state(agent)
|
||||
last_message = state.messages[end]
|
||||
|
||||
push!(results, last_message)
|
||||
|
||||
# Clean up
|
||||
reset!(agent)
|
||||
end
|
||||
|
||||
# Process results
|
||||
for result in results
|
||||
println("Result: $(result)")
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 9: Custom Event Handling
|
||||
|
||||
```julia
|
||||
# Custom event types
|
||||
struct CustomEvent <: AgentEvent
|
||||
data::Any
|
||||
end
|
||||
|
||||
# Custom event handler
|
||||
function customEventHandler(event, signal)
|
||||
if event isa CustomEvent
|
||||
println("Custom event: $(event.data)")
|
||||
end
|
||||
end
|
||||
|
||||
# Subscribe to custom events
|
||||
subscribe(agent) do event, signal
|
||||
customEventHandler(event, signal)
|
||||
end
|
||||
|
||||
# Emit custom event
|
||||
emit(CustomEvent("custom data"))
|
||||
```
|
||||
|
||||
### Pattern 10: Error Handling
|
||||
|
||||
```julia
|
||||
# Hook for error handling
|
||||
function errorHook(context, signal)
|
||||
if context isa PrepareNextTurnContext
|
||||
last_message = context.message
|
||||
|
||||
if last_message.stop_reason == "error"
|
||||
println("Error in conversation: $(last_message.error_message)")
|
||||
|
||||
return AgentLoopTurnUpdate(
|
||||
context = context.context,
|
||||
model = context.context.model,
|
||||
thinking_level = THINKING_HIGH, # Use more capable model
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
# Use in agent
|
||||
agent = Agent(Dict(
|
||||
:prepareNextTurn => errorHook,
|
||||
))
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Unit Testing
|
||||
|
||||
```julia
|
||||
# Test tool execution
|
||||
@testset "Bash tool" begin
|
||||
tool = createBashTool()
|
||||
|
||||
# Test successful execution
|
||||
result = tool.execute("tc1", Dict("command" => "echo hello"), nothing, nothing, nothing)
|
||||
@test result.content[1].text == "hello\n"
|
||||
@test result.details === nothing
|
||||
|
||||
# Test error handling
|
||||
result = tool.execute("tc2", Dict("command" => "exit 1"), nothing, nothing, nothing)
|
||||
@test result.terminate === true
|
||||
end
|
||||
|
||||
# Test agent with mock LLM
|
||||
@testset "Agent with mock" begin
|
||||
# Mock stream function
|
||||
function mockStreamFn(model, context, options)
|
||||
# Return mock response
|
||||
return MockResponse([TextContent("Hello!")])
|
||||
end
|
||||
|
||||
agent = Agent(Dict(
|
||||
:stream_fn => mockStreamFn,
|
||||
:systemPrompt => "You are a helpful assistant.",
|
||||
:model => model,
|
||||
))
|
||||
|
||||
# Test prompt
|
||||
prompt(agent, "Hello")
|
||||
wait_for_idle(agent)
|
||||
|
||||
# Verify result
|
||||
state = get_state(agent)
|
||||
@test length(state.messages) == 2 # User + Assistant
|
||||
end
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
```julia
|
||||
# Test full conversation flow
|
||||
@testset "Full conversation" begin
|
||||
# Create session storage
|
||||
storage = InMemorySessionStorage(...)
|
||||
session = Session(storage)
|
||||
|
||||
# Create agent
|
||||
agent = Agent(Dict(
|
||||
:systemPrompt => "You are a helpful assistant.",
|
||||
:model => model,
|
||||
:tools => [bash_tool],
|
||||
:sessionId => session.getMetadata().id,
|
||||
))
|
||||
|
||||
# Run conversation
|
||||
prompt(agent, "What's in the directory?")
|
||||
wait_for_idle(agent)
|
||||
|
||||
# Verify session
|
||||
context = buildSessionContext(session)
|
||||
@test length(context.messages) == 2
|
||||
|
||||
# Continue conversation
|
||||
prompt(agent, "What's the weather?")
|
||||
wait_for_idle(agent)
|
||||
|
||||
# Verify growth
|
||||
context = buildSessionContext(session)
|
||||
@test length(context.messages) == 4
|
||||
end
|
||||
```
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
### Pattern 1: Caching
|
||||
|
||||
```julia
|
||||
# Simple caching for LLM calls
|
||||
struct LLMCache
|
||||
cache::Dict{String, AssistantMessage}
|
||||
end
|
||||
|
||||
function LLMCache()
|
||||
return LLMCache(Dict{String, AssistantMessage}())
|
||||
end
|
||||
|
||||
function getCached(cache::LLMCache, key::String)
|
||||
return get(cache.cache, key, nothing)
|
||||
end
|
||||
|
||||
function setCached(cache::LLMCache, key::String, value::AssistantMessage)
|
||||
cache.cache[key] = value
|
||||
end
|
||||
|
||||
# Use in stream function
|
||||
function cachedStreamFn(model, context, options)
|
||||
key = generateCacheKey(context)
|
||||
|
||||
cached = getCached(cache, key)
|
||||
if !isnothing(cached)
|
||||
return MockResponse(cached)
|
||||
end
|
||||
|
||||
result = actualStreamFn(model, context, options)
|
||||
setCached(cache, key, result)
|
||||
return result
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 2: Batch LLM Calls
|
||||
|
||||
```julia
|
||||
# Batch multiple LLM calls
|
||||
function batchLLMCalls(calls::Vector{Dict})
|
||||
results = []
|
||||
|
||||
for call in calls
|
||||
result = streamFunction(
|
||||
call[:model],
|
||||
call[:context],
|
||||
call[:options],
|
||||
)
|
||||
push!(results, result)
|
||||
end
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
# Use with parallel execution
|
||||
tool.execute = (id, params, signal, on_update, context) -> begin
|
||||
# Batch multiple LLM calls
|
||||
llm_calls = [
|
||||
Dict(:model => model, :context => context1, :options => options1),
|
||||
Dict(:model => model, :context => context2, :options => options2),
|
||||
]
|
||||
|
||||
results = batchLLMCalls(llm_calls)
|
||||
|
||||
return AgentToolResult(
|
||||
[TextContent(join([r.text for r in results], "\n"))],
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 3: Lazy Loading
|
||||
|
||||
```julia
|
||||
# Lazy load skills
|
||||
struct LazySkills
|
||||
dir::String
|
||||
skills::Union{Vector{Skill}, Nothing}
|
||||
end
|
||||
|
||||
function LazySkills(dir)
|
||||
return LazySkills(dir, nothing)
|
||||
end
|
||||
|
||||
function getSkills(lazy::LazySkills)
|
||||
if isnothing(lazy.skills)
|
||||
lazy.skills, _ = loadSkills(lazy.dir)
|
||||
end
|
||||
return lazy.skills
|
||||
end
|
||||
|
||||
# Use in harness
|
||||
harness = AgentHarness(Dict(
|
||||
:resources => AgentHarnessResources(
|
||||
templates,
|
||||
LazySkills("/path/to/skills"),
|
||||
),
|
||||
))
|
||||
```
|
||||
|
||||
## Production Patterns
|
||||
|
||||
### Pattern 1: Observability
|
||||
|
||||
```julia
|
||||
# Logging hook
|
||||
function loggingHook(event, signal)
|
||||
if event isa BeforeProviderRequestEvent
|
||||
println("[Request] $(event.model.id)")
|
||||
elseif event isa AfterProviderResponseEvent
|
||||
println("[Response] Status: $(event.status)")
|
||||
elseif event isa ToolExecutionEndEvent
|
||||
println("[Tool] $(event.tool_name): $(event.is_error ? "error" : "success")")
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
# Metrics hook
|
||||
function metricsHook(event, signal)
|
||||
if event isa AgentStartEvent
|
||||
metrics.start_time = Dates.now()
|
||||
elseif event isa AgentEndEvent
|
||||
duration = Dates.value(Dates.now() - metrics.start_time) / 1000
|
||||
println("[Metrics] Duration: $(duration)s")
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 2: Retry Logic
|
||||
|
||||
```julia
|
||||
# Retry hook
|
||||
function retryHook(event, signal)
|
||||
if event isa AfterProviderResponseEvent && event.status >= 500
|
||||
# Server error, retry
|
||||
return BeforeProviderRequestResult(Dict(
|
||||
"retry" => true,
|
||||
"max_retries" => 3,
|
||||
))
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
# Use in stream options
|
||||
harness = AgentHarness(Dict(
|
||||
:stream_options => AgentHarnessStreamOptions(
|
||||
max_retries = 3,
|
||||
max_retry_delay_ms = 5000,
|
||||
),
|
||||
:retry => retryHook,
|
||||
))
|
||||
```
|
||||
|
||||
### Pattern 3: Security
|
||||
|
||||
```julia
|
||||
# Security hook
|
||||
function securityHook(event, signal)
|
||||
if event isa ToolCallEvent
|
||||
# Validate tool call
|
||||
if event.tool_name == "bash"
|
||||
command = event.input["command"]
|
||||
|
||||
# Block dangerous commands
|
||||
dangerous_patterns = ["rm -rf /", "sudo", "curl | sh"]
|
||||
for pattern in dangerous_patterns
|
||||
if contains(command, pattern)
|
||||
return ToolCallResult(true, "Blocked dangerous command")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nothing
|
||||
end
|
||||
```
|
||||
|
||||
## Debugging Patterns
|
||||
|
||||
### Pattern 1: Conversation Trace
|
||||
|
||||
```julia
|
||||
# Trace conversation
|
||||
trace = []
|
||||
|
||||
subscribe(agent) do event, signal
|
||||
if event isa MessageEndEvent
|
||||
push!(trace, Dict(
|
||||
"role" => event.message.role,
|
||||
"content" => event.message.content,
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
# Run conversation
|
||||
prompt(agent, "Hello")
|
||||
wait_for_idle(agent)
|
||||
|
||||
# Print trace
|
||||
for entry in trace
|
||||
println("$(entry["role"]): $(entry["content"])")
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 2: Tool Call Trace
|
||||
|
||||
```julia
|
||||
tool_trace = []
|
||||
|
||||
subscribe(agent) do event, signal
|
||||
if event isa ToolExecutionStartEvent
|
||||
push!(tool_trace, Dict(
|
||||
"type" => "start",
|
||||
"tool" => event.tool_name,
|
||||
"args" => event.args,
|
||||
))
|
||||
elseif event isa ToolExecutionEndEvent
|
||||
push!(tool_trace, Dict(
|
||||
"type" => "end",
|
||||
"tool" => event.tool_name,
|
||||
"error" => event.is_error,
|
||||
))
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 3: State Dump
|
||||
|
||||
```julia
|
||||
function dumpState(agent)
|
||||
state = get_state(agent)
|
||||
|
||||
println("=== Agent State ===")
|
||||
println("System prompt: $(state.system_prompt)")
|
||||
println("Model: $(state.model.name)")
|
||||
println("Thinking level: $(state.thinking_level)")
|
||||
println("Messages: $(length(state.messages))")
|
||||
println("Tools: $(length(state.tools))")
|
||||
println("==================")
|
||||
end
|
||||
|
||||
# Use after conversation
|
||||
prompt(agent, "Hello")
|
||||
wait_for_idle(agent)
|
||||
dumpState(agent)
|
||||
```
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Start simple**, add complexity gradually
|
||||
2. **Use hooks for customization**, not core logic
|
||||
3. **Test with mock LLM** first
|
||||
4. **Monitor token usage** for long conversations
|
||||
5. **Use branches** for exploration
|
||||
6. **Compact periodically** to stay within limits
|
||||
7. **Handle errors gracefully**
|
||||
8. **Log important events**
|
||||
9. **Test edge cases**
|
||||
10. **Profile performance**
|
||||
@@ -0,0 +1,386 @@
|
||||
# AgentCore.jl - Learning Guide
|
||||
|
||||
## How to Use This Documentation
|
||||
|
||||
### Top-Down Learning Approach
|
||||
|
||||
This documentation is organized in a **top-down** order, starting from high-level concepts and drilling down into implementation details. Follow this sequence:
|
||||
|
||||
1. **Architecture Overview** - Understand the big picture
|
||||
2. **Agent Component** - Learn about state management and event streaming
|
||||
3. **AgentLoop Component** - Understand the core LLM interaction loop
|
||||
4. **Types & Messages** - Learn the data structures
|
||||
5. **Session Management** - Understand conversation history
|
||||
6. **Tools** - Learn about tool execution
|
||||
|
||||
### Learning Style
|
||||
|
||||
- **Visual learners**: Study the ASCII diagrams
|
||||
- **Hands-on learners**: Code examples provided for each section
|
||||
- **Conceptual learners**: Read summaries and overviews first
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Minimal Example
|
||||
|
||||
```julia
|
||||
using AgentCore
|
||||
|
||||
# Create agent
|
||||
agent = Agent(Dict(
|
||||
:systemPrompt => "You are a helpful assistant.",
|
||||
:model => Model(...),
|
||||
:tools => [bash_tool],
|
||||
))
|
||||
|
||||
# Run conversation
|
||||
prompt(agent, "Hello!")
|
||||
|
||||
# Wait for completion
|
||||
wait_for_idle(agent)
|
||||
```
|
||||
|
||||
### Understanding the Flow
|
||||
|
||||
```
|
||||
User Code
|
||||
│
|
||||
├─► Create Agent
|
||||
│ ├─ Initialize state
|
||||
│ ├─ Set up queues
|
||||
│ └─ Register hooks
|
||||
│
|
||||
├─► prompt("Hello")
|
||||
│ ├─ Validate input
|
||||
│ └─ Start AgentLoop
|
||||
│
|
||||
├─► AgentLoop (runs in thread)
|
||||
│ ├─ Stream LLM response
|
||||
│ ├─ Execute tools
|
||||
│ └─ Emit events
|
||||
│
|
||||
└─► Event handlers receive events
|
||||
├─ MessageEndEvent
|
||||
├─ ToolExecutionEndEvent
|
||||
└─ AgentEndEvent
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Agent
|
||||
|
||||
**What it is**: High-level interface for LLM interactions
|
||||
|
||||
**What it does**:
|
||||
- Manages conversation state
|
||||
- Handles event streaming
|
||||
- Queues steering/follow-up messages
|
||||
- Provides hooks for customization
|
||||
|
||||
**Key methods**:
|
||||
- `prompt()` - Start new conversation
|
||||
- `continue!()` - Continue existing conversation
|
||||
- `steer()` - Queue message for next turn
|
||||
- `followUp()` - Queue message after stop
|
||||
- `subscribe()` - Listen to events
|
||||
|
||||
### AgentLoop
|
||||
|
||||
**What it is**: Core LLM interaction loop
|
||||
|
||||
**What it does**:
|
||||
- Calls LLM API with streaming
|
||||
- Executes tool calls (parallel or sequential)
|
||||
- Emits lifecycle events
|
||||
- Handles steering/follow-up messages
|
||||
|
||||
**Key functions**:
|
||||
- `agentLoop()` - Start new conversation
|
||||
- `agentLoopContinue()` - Continue conversation
|
||||
- `runAgentLoop()` - Internal loop execution
|
||||
- `streamAssistantResponse()` - LLM API call
|
||||
- `executeToolCalls()` - Tool execution
|
||||
|
||||
### Session
|
||||
|
||||
**What it is**: Conversation history management
|
||||
|
||||
**What it does**:
|
||||
- Persists messages to storage
|
||||
- Supports branching
|
||||
- Implements compaction
|
||||
- Manages conversation tree
|
||||
|
||||
**Key methods**:
|
||||
- `appendMessage()` - Add message
|
||||
- `appendCompaction()` - Compress history
|
||||
- `moveTo()` - Navigate branches
|
||||
- `buildSessionContext()` - Build context for LLM
|
||||
|
||||
### Tools
|
||||
|
||||
**What it is**: Functions agents can call
|
||||
|
||||
**What they do**:
|
||||
- Execute external operations
|
||||
- Return results to agent
|
||||
- Support streaming updates
|
||||
- Implement hooks
|
||||
|
||||
**Built-in tools**:
|
||||
- `bash` - Execute shell commands
|
||||
- `read` - Read files
|
||||
- `write` - Write files
|
||||
- `edit` - Edit files
|
||||
|
||||
## Event System
|
||||
|
||||
### Event Types
|
||||
|
||||
```
|
||||
AgentEvent
|
||||
├─ AgentStartEvent / AgentEndEvent
|
||||
├─ TurnStartEvent / TurnEndEvent
|
||||
├─ MessageStartEvent / MessageEndEvent
|
||||
├─ MessageUpdateEvent
|
||||
├─ ToolExecutionStartEvent / ToolExecutionEndEvent
|
||||
└─ ToolExecutionUpdateEvent
|
||||
```
|
||||
|
||||
### Event Flow
|
||||
|
||||
```
|
||||
AgentStartEvent
|
||||
│
|
||||
├─ TurnStartEvent
|
||||
│ ├─ MessageStartEvent (user)
|
||||
│ ├─ MessageEndEvent (user)
|
||||
│ ├─ MessageStartEvent (assistant)
|
||||
│ ├─ MessageUpdateEvent (streaming)
|
||||
│ ├─ MessageEndEvent (assistant)
|
||||
│ ├─ ToolExecutionStartEvent
|
||||
│ ├─ ToolExecutionEndEvent
|
||||
│ └─ TurnEndEvent
|
||||
│
|
||||
└─ AgentEndEvent
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Message Transformation
|
||||
|
||||
```
|
||||
AgentMessage[] (internal)
|
||||
│
|
||||
├─ transform_context() (optional)
|
||||
▼
|
||||
AgentMessage[] (transformed)
|
||||
│
|
||||
├─ convert_to_llm()
|
||||
▼
|
||||
Message[] (LLM API)
|
||||
```
|
||||
|
||||
### Tool Execution Flow
|
||||
|
||||
```
|
||||
ToolCall (in assistant message)
|
||||
│
|
||||
├─ before_tool_call hook
|
||||
├─ prepareToolCall()
|
||||
├─ execute()
|
||||
├─ after_tool_call hook
|
||||
└─ createToolResultMessage()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Hooks for Customization
|
||||
|
||||
```julia
|
||||
# Before tool call
|
||||
before_hook = (context, signal) -> begin
|
||||
println("Executing: $(context.tool_call.name)")
|
||||
return nothing
|
||||
end
|
||||
|
||||
# After tool call
|
||||
after_hook = (context, signal) -> begin
|
||||
if context.is_error
|
||||
println("Tool failed: $(context.tool_call.name)")
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
```
|
||||
|
||||
### 2. Monitor Events
|
||||
|
||||
```julia
|
||||
subscribe(agent) do event, signal
|
||||
if event isa MessageEndEvent
|
||||
println("Message: $(event.message)")
|
||||
elseif event isa ToolExecutionEndEvent
|
||||
println("Tool completed: $(event.tool_name)")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 3. Use Steering for Redirection
|
||||
|
||||
```julia
|
||||
# Agent is going wrong direction
|
||||
steer(agent, UserMessage("Actually, let's do X instead"))
|
||||
```
|
||||
|
||||
### 4. Use Follow-Up for Continuation
|
||||
|
||||
```julia
|
||||
# Agent thinks it's done, but user wants more
|
||||
followUp(agent, UserMessage("Wait, there's one more thing"))
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Conversation with Memory
|
||||
|
||||
```julia
|
||||
# Use Session to persist conversation
|
||||
storage = JsonlSessionStorage(...)
|
||||
session = Session(storage)
|
||||
|
||||
# Add messages to session
|
||||
appendMessage(session, user_message)
|
||||
appendMessage(session, assistant_message)
|
||||
|
||||
# Build context from session
|
||||
context = buildSessionContext(session)
|
||||
```
|
||||
|
||||
### Pattern 2: Long Conversations
|
||||
|
||||
```julia
|
||||
# Compact periodically to stay within context limits
|
||||
if token_count > MAX_TOKENS * 0.8
|
||||
compact_id = appendCompaction(
|
||||
session,
|
||||
summary,
|
||||
first_kept_id,
|
||||
token_count,
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### Pattern 3: Branching Conversations
|
||||
|
||||
```julia
|
||||
# User wants to explore alternative
|
||||
session.moveTo(branch_point_id)
|
||||
|
||||
# Create new branch
|
||||
appendBranchSummary(session, "Exploring alternative approach")
|
||||
appendMessage(session, new_user_message)
|
||||
```
|
||||
|
||||
### Pattern 4: Custom Tools
|
||||
|
||||
```julia
|
||||
# Create custom tool
|
||||
custom_tool = AgentTool(
|
||||
"custom",
|
||||
"custom",
|
||||
"Does custom thing",
|
||||
...,
|
||||
execute_function,
|
||||
nothing,
|
||||
EXECUTION_PARALLEL,
|
||||
)
|
||||
|
||||
# Add to agent
|
||||
agent = Agent(Dict(:tools => [custom_tool]))
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Check Active Run
|
||||
|
||||
```julia
|
||||
if !isnothing(agent.active_run)
|
||||
println("Agent is busy")
|
||||
else
|
||||
println("Agent is idle")
|
||||
end
|
||||
```
|
||||
|
||||
### Clear Queues
|
||||
|
||||
```julia
|
||||
clearAllQueues(agent)
|
||||
```
|
||||
|
||||
### Reset State
|
||||
|
||||
```julia
|
||||
reset!(agent)
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Use parallel execution** for independent tools
|
||||
2. **Compact periodically** for long conversations
|
||||
3. **Use thinking_level wisely** (higher = slower but better)
|
||||
4. **Batch tool calls** when possible
|
||||
5. **Cache LLM responses** when appropriate
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Agent stuck in loop
|
||||
|
||||
```julia
|
||||
# Check if agent is still processing
|
||||
if hasQueuedMessages(agent)
|
||||
# Clear queues
|
||||
clearAllQueues(agent)
|
||||
end
|
||||
```
|
||||
|
||||
### Too many tokens
|
||||
|
||||
```julia
|
||||
# Compact session
|
||||
compact_id = appendCompaction(
|
||||
session,
|
||||
summary,
|
||||
first_kept_id,
|
||||
token_count,
|
||||
)
|
||||
```
|
||||
|
||||
### Tool execution failed
|
||||
|
||||
```julia
|
||||
# Check tool result
|
||||
if result.is_error
|
||||
println("Tool failed: $(result.error)")
|
||||
end
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Read **Architecture Overview** for deep understanding
|
||||
2. Explore **Agent Component** for state management
|
||||
3. Study **AgentLoop** for core logic
|
||||
4. Learn **Types & Messages** for data structures
|
||||
5. Master **Session Management** for persistence
|
||||
6. Build **Tools** for custom functionality
|
||||
|
||||
## Resources
|
||||
|
||||
- Original TypeScript implementation: `@earendil-works/pi-agent-core`
|
||||
- AgentCore.jl source code: `src/`
|
||||
- Examples: `examples/`
|
||||
|
||||
## Community
|
||||
|
||||
For questions and discussions:
|
||||
- GitHub Issues: `/issues`
|
||||
- Documentation: `docs/`
|
||||
Reference in New Issue
Block a user