diff --git a/learning/01-ARCHITECTURE_OVERVIEW.md b/learning/01-ARCHITECTURE_OVERVIEW.md index 9f5921b..1d6b82c 100644 --- a/learning/01-ARCHITECTURE_OVERVIEW.md +++ b/learning/01-ARCHITECTURE_OVERVIEW.md @@ -405,87 +405,295 @@ └── Manages ──► PromptTemplates ``` -## Data Flow +## Data Flow with Type Transformations + +### Complete User Input → Conversation History Flow ``` ┌─────────────────────────────────────────────────────────────────────────────┐ -│ Data Flow Between Layers │ +│ Level 1: User Input │ +└─────────────────────────────────────────────────────────────────────────────┘ + User Input + • String: "Hello, what's in the directory?" + • AgentMessage: UserMessage(...) + • Vector{AgentMessage}: [UserMessage(...), AssistantMessage(...)] + + │ + ▼ + ┌──────────────────────────────────────────────────────────────┐ + │ Agent.prompt() / normalizePromptInput() │ + │ │ + │ Type Dispatch: │ + │ • String → UserMessage("user", [TextContent(input)], ts) │ + │ • AgentMessage → [input] (wrap in array) │ + │ • Vector{AgentMessage} → input (pass-through) │ + │ │ + │ Output: Vector{AgentMessage} │ + └──────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────┐ + │ AgentState.messages (AgentMessage[]) │ + │ │ + │ AgentMessage Types: │ + │ • UserMessage (role: "user") │ + │ • AssistantMessage (role: "assistant") │ + │ • ToolResultMessage (role: "toolResult") │ + │ • BashExecutionMessage (custom) │ + │ • CompactionSummaryMessage (custom) │ + │ • BranchSummaryMessage (custom) │ + └──────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Level 2: AgentLoop Processing │ └─────────────────────────────────────────────────────────────────────────────┘ - User Input (String/Message) + ┌──────────────────────────────────────────────────────────────┐ + │ transform_context() (optional hook) │ + │ │ + │ Input: Vector{AgentMessage} │ + │ Output: Vector{AgentMessage} (transformed) │ + │ - Can truncate, filter, or modify messages │ + └──────────────────────────────────────────────────────────────┘ │ ▼ - ┌──────────────────────┐ - │ Agent.prompt() │ - │ - normalizeInput() │ - └──────────────────────┘ + ┌────────────────────────────────────────────────────────────────┐ + │ convertToLlm() - Type Transformation Pipeline │ + │ │ + │ Input: Vector{AgentMessage} │ + │ Output: Vector{Message} (for LLM API) │ + │ │ + │ Single Dispatch Mapping: │ + │ • UserMessage → UserMessage (pass-through) │ + │ • AssistantMessage → AssistantMessage (pass-through) │ + │ • ToolResultMessage → ToolResultMessage (pass-through) │ + │ │ + │ Custom Message Conversions: │ + │ • BashExecutionMessage → UserMessage (via bashExecutionToText)│ + │ • CompactionSummaryMessage → UserMessage (wrapped) │ + │ • BranchSummaryMessage → UserMessage (wrapped) │ + └────────────────────────────────────────────────────────────────┘ │ ▼ - ┌──────────────────────┐ - │ AgentState.messages │ ──► AgentMessage[] - └──────────────────────┘ + ┌──────────────────────────────────────────────────────────────┐ + │ Context for LLM API │ + │ - system_prompt: String │ + │ - messages: Vector{Message} │ + │ - tools: Vector{AgentTool} │ + └──────────────────────────────────────────────────────────────┘ │ ▼ - ┌──────────────────────┐ - │ AgentLoop │ - │ - transform_context │ - └──────────────────────┘ + ┌──────────────────────────────────────────────────────────────┐ + │ LLM API Call (stream_fn) │ + │ │ + │ Input: model, context, config │ + │ Output: Stream{AssistantMessageEvent} │ + │ • StartEvent: partial AssistantMessage │ + │ • TextStartEvent/TextDeltaEvent/TextEndEvent │ + │ • ToolCallStartEvent/ToolCallDeltaEvent/ToolCallEndEvent │ + │ • DoneEvent: final AssistantMessage with usage │ + └──────────────────────────────────────────────────────────────┘ │ ▼ - ┌──────────────────────┐ - │ convertToLlm() │ ──► Transforms AgentMessage[] to Message[] - └──────────────────────┘ + ┌──────────────────────────────────────────────────────────────┐ + │ AssistantMessage (returned from LLM) │ + │ │ + │ • role: "assistant" │ + │ • content: Vector{MessageContent} │ + │ └─ Contains: TextContent[] and/or ToolCall[] │ + │ • api, provider, model: String │ + │ • usage: Usage (input, output, cache_read, cache_write) │ + │ • stop_reason: String ("done", "length", "error", etc.) │ + │ • error_message: Union{String, Nothing} │ + │ • timestamp: Timestamp (Int64) │ + └──────────────────────────────────────────────────────────────┘ + │ + ├─► Append to AgentState.messages (AssistantMessage) │ ▼ - ┌──────────────────────┐ - │ LLM API (StreamFn) │ - │ - Context: Message[] │ - └──────────────────────┘ + ┌──────────────────────────────────────────────────────────────┐ + │ executeToolCalls() - Tool Processing │ + │ │ + │ Extract: filter(c -> c isa ToolCall, assistant.content) │ + │ Output: ExecutedToolCallBatch │ + │ • messages: Vector{ToolResultMessage} │ + │ • terminate: Bool │ + └──────────────────────────────────────────────────────────────┘ │ ▼ - ┌──────────────────────┐ - │ Response (Streaming) │ - │ - Text deltas │ - │ - Tool call deltas │ - └──────────────────────┘ + ┌──────────────────────────────────────────────────────────────┐ + │ ToolResultMessage (for each ToolCall) │ + │ │ + │ • role: "toolResult" │ + │ • tool_call_id: String (matches ToolCall.id) │ + │ • tool_name: String (matches ToolCall.name) │ + │ • content: Vector{MessageContent} │ + │ • details: Any (tool-specific) │ + │ • usage: Union{Usage, Nothing} │ + │ • added_tool_names: Union{Vector{String}, Nothing} │ + │ • is_error: Bool │ + │ • timestamp: Timestamp (Int64) │ + └──────────────────────────────────────────────────────────────┘ + │ + ├─► Append to AgentState.messages (ToolResultMessage) │ ▼ - ┌──────────────────────┐ - │ AssistantMessage │ - │ - content: Message[] │ - └──────────────────────┘ + ┌──────────────────────────────────────────────────────────────┐ + │ Updated AgentState.messages (AgentMessage[]) │ + │ │ + │ Conversation History: │ + │ [UserMessage, AssistantMessage, ToolResultMessage, ...] │ + └──────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Level 3: Session Storage (optional, for persistence) │ +└─────────────────────────────────────────────────────────────────────────────┘ + + AgentState.messages (Vector{AgentMessage}) │ ▼ - ┌──────────────────────┐ - │ AgentState.messages │ ──► Appended to conversation - └──────────────────────┘ + ┌──────────────────────────────────────────────────────────────┐ + │ Session Storage (JSONL) │ + │ │ + │ SessionTreeEntry Types: │ + │ • MessageEntry (agent_message) │ + │ • CompactionEntry (summary, tokens_before) │ + │ • BranchSummaryEntry (from_id, summary) │ + │ • ModelChangeEntry (provider, model_id) │ + │ • ThinkingLevelChangeEntry (thinking_level) │ + │ • ActiveToolsChangeEntry (active_tool_names) │ + └──────────────────────────────────────────────────────────────┘ │ ▼ - ┌──────────────────────┐ - │ Tool Execution │ - │ - Extract ToolCalls │ - │ - Execute tools │ - └──────────────────────┘ - │ - ▼ - ┌──────────────────────┐ - │ ToolResultMessage[] │ - └──────────────────────┘ - │ - ▼ - ┌──────────────────────┐ - │ AgentState.messages │ ──► Tool results appended - └──────────────────────┘ - │ - │ (Loop back to LLM or end) - ▼ - ┌──────────────────────┐ - │ Session Storage │ - │ - JSONL format │ - │ - Tree entries │ - └──────────────────────┘ + ┌──────────────────────────────────────────────────────────────┐ + │ Persisted Data (JSON format) │ + │ - Each entry has: id, parent_id, timestamp, type │ + │ - MessageEntry contains full AgentMessage │ + └──────────────────────────────────────────────────────────────┘ ``` +### Tool Call Execution Flow (Detailed) + +``` +ToolCall (from AssistantMessage.content) + │ + ├─ type: "tool" + ├─ id: "tc_abc123" + ├─ name: "bash" + ├─ arguments: Dict("command" => "ls -la") + └─ partial_json: nothing + │ + ▼ + ┌──────────────────────────────────────────────────────────────┐ + │ prepareToolCall() │ + │ │ + │ Input: tool_call::ToolCall │ + │ Output: Union{PreparedToolCall, ImmediateToolCallOutcome} │ + │ │ + │ Steps: │ + │ 1. Find tool by name in current_context.tools │ + │ 2. before_tool_call hook (optional) │ + │ Input: BeforeToolCallContext │ + │ Output: BeforeToolCallResult (block, reason) or nothing │ + │ 3. prepareToolCallArguments() (optional) │ + │ Input: tool_call.arguments::Dict │ + │ Output: prepared_arguments::Any │ + │ 4. validateToolArguments() (optional) │ + │ Input: prepared_tool_call.arguments │ + │ Output: validated_args::Any │ + │ 5. Return: PreparedToolCall(kind, tool_call, tool, args) │ + └──────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────┐ + │ executePreparedToolCall() (if prepared) │ + │ │ + │ Input: PreparedToolCall │ + │ Output: ExecutedToolCallOutcome │ + │ │ + │ tool.execute(tool_call.id, args, signal, on_update) │ + │ │ │ + │ └─ Returns: AgentToolResultMutable │ + │ • content::Vector{MessageContent} │ + │ • details::Any │ + │ • usage::Union{Usage, Nothing} │ + │ • added_tool_names::Union{Vector{String}, Nothing} │ + │ • terminate::Union{Bool, Nothing} │ + └──────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌───────────────────────────────────────────────────────────────┐ + │ finalizeExecutedToolCall() │ + │ │ + │ Input: ExecutedToolCallOutcome │ + │ Output: FinalizedToolCallOutcome │ + │ │ + │ Steps: │ + │ 1. after_tool_call hook (optional) │ + │ Input: AfterToolCallContext │ + │ Output: AfterToolCallResult (patches) │ + │ 2. Apply patches to result │ + │ 3. Return: FinalizedToolCallOutcome(tool_call, result, error)│ + └───────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────┐ + │ createToolResultMessage() │ + │ │ + │ Input: FinalizedToolCallOutcome │ + │ Output: ToolResultMessage │ + │ │ + │ Fields: │ + │ • role: "toolResult" │ + │ • tool_call_id: tool_call.id │ + │ • tool_name: tool_call.name │ + │ • content: result.content │ + │ • details: result.details │ + │ • usage: result.usage │ + │ • added_tool_names: result.added_tool_names │ + │ • is_error: is_error │ + │ • timestamp: Int64(Dates.now(Dates.UTC).datetime) │ + └──────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────────────────────┐ + │ Emit: ToolResultMessage to conversation │ + └──────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Summary of Type Transformations │ +└─────────────────────────────────────────────────────────────────────────────┘ + + User Input (String) + │ + ├─► normalizePromptInput() + │ └─► UserMessage (AgentMessage subtype) + │ + Vector{AgentMessage} + │ + ├─► transform_context() (optional) + │ └─► Vector{AgentMessage} (transformed) + │ + ├─► convertToLlm() + │ └─► Vector{Message} (LLM API format) + │ ├── UserMessage (pass-through) + │ ├── AssistantMessage (pass-through) + │ ├── ToolResultMessage (pass-through) + │ └── Custom messages → UserMessage + │ + AssistantMessage (from LLM) + │ + ├─► executeToolCalls() + │ └─► ToolResultMessage[] + │ + ToolResultMessage[] + │ + └─► Appended to AgentState.messages + └─► Vector{AgentMessage} (updated conversation history) +``` + +## Summary + ## Summary The AgentCore.jl architecture follows a clean separation of concerns: @@ -495,4 +703,48 @@ The AgentCore.jl architecture follows a clean separation of concerns: 3. **AgentLoop** - Core LLM interaction loop 4. **Session** - Conversation history management +### Data Transformation Summary + +``` +Input Type Flow: + User Input (String/Message) + │ + ├─ normalizePromptInput() + │ └─► Vector{AgentMessage} + │ + ├─ transform_context() (optional) + │ └─► Vector{AgentMessage} (transformed) + │ + ├─ convertToLlm() + │ └─► Vector{Message} (LLM API format) + │ + ├─ LLM API (stream_fn) + │ └─► AssistantMessage + │ + ├─ executeToolCalls() + │ └─► ToolResultMessage[] + │ + └─► Vector{AgentMessage} (final conversation) +``` + +### Key Data Flow Patterns + +1. **Message Transformation**: `AgentMessage[] → Message[]` via `convertToLlm()` + - UserMessage → UserMessage (pass-through) + - AssistantMessage → AssistantMessage (pass-through) + - ToolResultMessage → ToolResultMessage (pass-through) + - Custom messages (Bash, Compaction, Branch) → UserMessage + +2. **Tool Execution**: `ToolCall → ToolResultMessage` + - prepareToolCall() validates and prepares + - execute() runs the tool + - finalize() applies hooks and returns outcome + - createToolResultMessage() creates result entry + +3. **Event Streaming**: `Stream{Event}` with lifecycle events + - AgentStartEvent, TurnStartEvent + - MessageStartEvent, MessageUpdateEvent, MessageEndEvent + - ToolExecutionStartEvent, ToolExecutionEndEvent + - TurnEndEvent, AgentEndEvent + Each layer transforms data and passes it to the next layer, with clear interfaces and event hooks for customization. diff --git a/learning/02-AGENT_COMPONENT.md b/learning/02-AGENT_COMPONENT.md index a46a1b8..18cb3f3 100644 --- a/learning/02-AGENT_COMPONENT.md +++ b/learning/02-AGENT_COMPONENT.md @@ -243,16 +243,35 @@ followUp(agent, UserMessage(...)) ```julia # Transform messages before sending to LLM -function myConvertToLlm(messages::Vector{AgentMessage}) - return filter( - m -> m.role in ["user", "assistant", "toolResult"], - messages - ) +function myConvertToLlm(messages::Vector{AgentMessage})::Vector{Message} + result::Vector{Message} = Message[] + for m in messages + converted = convertToLlmMessage(m) + if !isnothing(converted) + push!(result, converted) + end + end + return result end agent = Agent(Dict(:convertToLlm => myConvertToLlm)) ``` +**Data Flow**: +``` +Vector{AgentMessage} + │ + │ convertToLlmMessage() dispatches on type: + │ • UserMessage → UserMessage (pass-through) + │ • AssistantMessage → AssistantMessage (pass-through) + │ • ToolResultMessage → ToolResultMessage (pass-through) + │ • BashExecutionMessage → UserMessage (bashExecutionToText) + │ • CompactionSummaryMessage → UserMessage (wrapped) + │ • BranchSummaryMessage → UserMessage (wrapped) + ▼ +Vector{Message} (for LLM API) +``` + #### transform_context ```julia diff --git a/learning/03-AGENTLOOP_COMPONENT.md b/learning/03-AGENTLOOP_COMPONENT.md index fa8751c..c4098e3 100644 --- a/learning/03-AGENTLOOP_COMPONENT.md +++ b/learning/03-AGENTLOOP_COMPONENT.md @@ -27,11 +27,16 @@ agentLoopContinue() └─ Returns: EventStream ┌─────────────────────────────────────────────────────────────────────────────┐ -│ Internal Flow │ +│ Data Flow with Type Transformations │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 1. runAgentLoop() ── Entry point for new conversation │ +│ Input: prompts::Vector{AgentMessage} │ +│ context::AgentContext (system_prompt, messages, tools) │ +│ config::AgentLoopConfig │ +│ Output: new_messages::Vector{AgentMessage} (appended prompts + turns) │ +│ │ │ - Creates copy of prompts │ │ - Appends prompts to context.messages │ │ - Emits AgentStartEvent │ @@ -43,12 +48,20 @@ agentLoopContinue() ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 2. runLoop() ── Main event loop │ +│ Input: current_context::AgentContext │ +│ new_messages::Vector{AgentMessage} │ +│ Output: N/A (writes to new_messages and context.messages) │ +│ │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ while true: │ │ │ │ 1. Get steering/follow-up messages (if any) │ │ │ │ 2. Emit messages as UserMessage │ │ │ │ 3. streamAssistantResponse() │ │ +│ │ - Input: context.messages::Vector{AgentMessage} │ │ +│ │ - Output: message::AssistantMessage │ │ │ │ 4. Execute tool calls (sequential or parallel) │ │ +│ │ - Input: AssistantMessage with ToolCall[] │ │ +│ │ - Output: tool_results::Vector{ToolResultMessage} │ │ │ │ 5. Emit TurnEndEvent │ │ │ │ 6. prepare_next_turn (optional) │ │ │ │ 7. should_stop_after_turn? (check termination) │ │ @@ -59,27 +72,89 @@ agentLoopContinue() ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 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 │ +│ Input: context::AgentContext │ +│ config::AgentLoopConfig │ +│ Output: message::AssistantMessage │ +│ │ +│ Data Transformations: │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ Step 1: transform_context (optional) │ │ +│ │ Input: context.messages::Vector{AgentMessage} │ │ +│ │ Output: messages::Vector{AgentMessage} (transformed) │ │ +│ │ │ │ +│ │ Step 2: convert_to_llm │ │ +│ │ Input: messages::Vector{AgentMessage} │ │ +│ │ Output: llm_messages::Vector{Message} │ │ +│ │ - UserMessage → UserMessage │ │ +│ │ - AssistantMessage → AssistantMessage │ │ +│ │ - ToolResultMessage → ToolResultMessage │ │ +│ │ - BashExecutionMessage → UserMessage │ │ +│ │ - CompactionSummaryMessage → UserMessage │ │ +│ │ - BranchSummaryMessage → UserMessage │ │ +│ │ │ │ +│ │ Step 3: Call stream_fn │ │ +│ │ Input: model, llm_context::Context, config │ │ +│ │ Output: response::Stream (events) │ │ +│ │ │ │ +│ │ Step 4: Stream events │ │ +│ │ Events: start, text_start/delta/end, toolcall_start/delta/end │ │ +│ │ Final: AssistantMessage (with ToolCall[] in content) │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 4. executeToolCalls() ── Tool execution │ +│ Input: assistant_message::AssistantMessage (contains ToolCall[]) │ +│ current_context::AgentContext │ +│ Output: ExecutedToolCallBatch (messages::ToolResultMessage[], terminate) │ +│ │ +│ For each ToolCall: │ │ ┌────────────────────────────────────────────────────────────────────┐ │ -│ │ if EXECUTION_SEQUENTIAL || has_sequential_tool: │ │ -│ │ executeToolCallsSequential() │ │ -│ │ else: │ │ -│ │ executeToolCallsParallel() │ │ +│ │ prepareToolCall() │ │ +│ │ Input: tool_call::ToolCall │ │ +│ │ Output: PreparedToolCall or ImmediateToolCallOutcome │ │ +│ │ - validates arguments │ │ +│ │ - runs before_tool_call hook (optional) │ │ +│ │ - runs prepare_arguments hook (optional) │ │ +│ │ │ │ +│ │ executePreparedToolCall() (if prepared) │ │ +│ │ Input: PreparedToolCall │ │ +│ │ Output: ExecutedToolCallOutcome │ │ +│ │ - calls tool.execute() │ │ +│ │ - returns AgentToolResultMutable │ │ +│ │ │ │ +│ │ finalizeExecutedToolCall() │ │ +│ │ Input: ExecutedToolCallOutcome │ │ +│ │ Output: FinalizedToolCallOutcome │ │ +│ │ - runs after_tool_call hook (optional) │ │ +│ │ - returns ToolCall + AgentToolResultMutable + is_error │ │ +│ │ │ │ +│ │ createToolResultMessage() │ │ +│ │ Input: FinalizedToolCallOutcome │ │ +│ │ Output: ToolResultMessage │ │ +│ │ - role: "toolResult" │ │ +│ │ - tool_call_id, tool_name, content, details │ │ +│ │ - usage, added_tool_names, is_error, timestamp │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ Sequential: executeToolCallsSequential() │ │ +│ │ - Executes tools one at a time, waits for each │ │ +│ │ - Returns batch of ToolResultMessage[] │ │ +│ │ │ │ +│ │ Parallel: executeToolCallsParallel() │ │ +│ │ - Creates closures for async execution │ │ +│ │ - Executes all closures, collects results │ │ +│ │ - Returns batch of ToolResultMessage[] │ │ │ └────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 5. AgentEndEvent ── Final event with all messages │ +│ Output: messages::Vector{AgentMessage} │ +│ Contains: [UserMessage, AssistantMessage, ToolResultMessage, ...] │ └─────────────────────────────────────────────────────────────────────────────┘ ``` @@ -301,17 +376,44 @@ function streamAssistantResponse( )::AssistantMessage ``` -**Flow**: -1. Get messages from context -2. Apply transform_context (optional) -3. Convert to LLM messages with convert_to_llm -4. Create Context object -5. Resolve API key -6. Call stream_fn with model, context, and config -7. Stream events: - - "start" → MessageStartEvent - - "text_start", "text_delta", "text_end" → MessageUpdateEvent - - "done", "error" → MessageEndEvent +**Data Flow**: + +``` +Input: context.messages::Vector{AgentMessage} + ↓ + [transform_context] (optional hook) + ↓ + messages::Vector{AgentMessage} + ↓ + [convert_to_llm] - Type transformation pipeline + ↓ + llm_messages::Vector{Message} + │ + │ AgentMessage → Message mapping: + │ • UserMessage → UserMessage (pass-through) + │ • AssistantMessage → AssistantMessage (pass-through) + │ • ToolResultMessage → ToolResultMessage (pass-through) + │ • BashExecutionMessage → UserMessage (text conversion) + │ • CompactionSummaryMessage → UserMessage (text wrapped) + │ • BranchSummaryMessage → UserMessage (text wrapped) + │ + ▼ + Context(system_prompt, llm_messages, tools) + ↓ + stream_fn(model, context, config) - LLM API call + ↓ + Stream of AssistantMessageEvent: + • StartEvent (partial AssistantMessage) + • TextStartEvent/TextDeltaEvent/TextEndEvent + • ToolCallStartEvent/ToolCallDeltaEvent/ToolCallEndEvent + • DoneEvent (final AssistantMessage with usage, stop_reason) + ↓ + Return: AssistantMessage + - content::Vector{MessageContent} + - usage::Usage + - stop_reason::String + - Contains ToolCall[] if tool calls requested +``` ### executeToolCalls() @@ -320,27 +422,56 @@ function executeToolCalls( current_context::AgentContext, assistant_message::AssistantMessage, config::AgentLoopConfig, - signal::Union{Nothing, AbortSignal}, + signal::Union{Nothing, AbortSignal>, emit::AgentEventSink, )::ExecutedToolCallBatch ``` -**Logic**: -```julia -tool_calls = filter(c -> c isa ToolCall, assistant_message.content) +**Data Flow**: -# Check if any tool requires sequential execution -has_sequential = any(tc -> begin - tool = findfirst(t -> t.name == tc.name, current_context.tools) - !isnothing(tool) && tool.execution_mode == EXECUTION_SEQUENTIAL -end, tool_calls) - -# Determine execution mode -if config.tool_execution == EXECUTION_SEQUENTIAL || has_sequential - executeToolCallsSequential(...) -else - executeToolCallsParallel(...) -end +``` +Input: assistant_message::AssistantMessage + - content::Vector{MessageContent} + └─ Contains ToolCall[] and/or TextContent[] + ↓ + filter(c -> c isa ToolCall, assistant_message.content) + ↓ + tool_calls::Vector{ToolCall} + - type: "tool" + - id::String + - name::String + - arguments::Dict{String, Any} + - partial_json::Union{String, Nothing} + ↓ + Check execution mode: + • config.tool_execution (sequential/parallel) + • Any tool.execution_mode == EXECUTION_SEQUENTIAL? + ↓ + ┌─────────────────────────────────────────────────────────────────┐ + │ Sequential Mode (or has_sequential_tool) │ + │ For each tool_call in tool_calls: │ + │ prepareToolCall() → PreparedToolCall │ + │ executePreparedToolCall() → ExecutedToolCallOutcome │ + │ finalizeExecutedToolCall() → FinalizedToolCallOutcome │ + │ createToolResultMessage() → ToolResultMessage │ + │ (wait for completion before next tool) │ + └─────────────────────────────────────────────────────────────────┘ + ↓ + ┌─────────────────────────────────────────────────────────────────┐ + │ Parallel Mode │ + │ For each tool_call in tool_calls: │ + │ prepareToolCall() → (PreparedToolCall | ImmediateOutcome) │ + │ If prepared: create closure │ + │ If immediate: execute and add to finalized_calls │ + │ │ + │ For each entry in finalized_calls: │ + │ If closure: execute closure │ + │ If finalized: use as-is │ + └─────────────────────────────────────────────────────────────────┘ + ↓ + ExecutedToolCallBatch + - messages::Vector{ToolResultMessage} + - terminate::Bool (true if all tools have terminate=true) ``` ### executeToolCallsSequential() @@ -400,29 +531,77 @@ function prepareToolCall( )::Union{PreparedToolCall, ImmediateToolCallOutcome} ``` -**Flow**: -1. Find tool by name -2. If not found → ImmediateToolCallOutcome (error) -3. before_tool_call hook (optional) -4. prepareToolCallArguments() (optional) -5. validateToolArguments() -6. Return PreparedToolCall +**Data Flow**: + +``` +Input: tool_call::ToolCall + - id::String + - name::String + - arguments::Dict{String, Any} + ↓ + findfirst(t -> t.name == tool_call.name, current_context.tools) + ↓ + If tool is nothing: + → ImmediateToolCallOutcome("immediate", error_result, is_error=true) + ↓ + If tool exists: + [before_tool_call hook] (optional) + Input: BeforeToolCallContext(assistant_message, tool_call, args, context) + Output: BeforeToolCallResult (block, reason) or nothing + If block=true → ImmediateToolCallOutcome(error) + ↓ + prepareToolCallArguments(tool, tool_call) + Input: tool_call.arguments::Dict{String, Any} + Output: prepared_arguments::Any + (Optional: transform arguments before validation) + ↓ + validateToolArguments(tool, prepared_tool_call) + Input: prepared_tool_call.arguments + Output: validated_args::Any + (Optional: JSON schema validation) + ↓ + Return: PreparedToolCall("prepared", tool_call, tool, validated_args) + - kind: "prepared" + - tool_call: ToolCall (original) + - tool: AgentTool + - args: validated arguments +``` ### executePreparedToolCall() ```julia function executePreparedToolCall( prepared::PreparedToolCall, - signal::Union{Nothing, AbortSignal}, + 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) +**Data Flow**: + +``` +Input: prepared::PreparedToolCall + - tool_call::ToolCall + - tool::AgentTool + - args::Any (validated) + ↓ + tool.execute(tool_call.id, args, signal, on_update) + Input: tool_call_id::String + args::Any + signal::Union{Any, Nothing} + on_update::Function (partial_result → void) + Output: AgentToolResultMutable + - content::Vector{MessageContent} + - details::Any + - usage::Union{Usage, Nothing} + - added_tool_names::Union{Vector{String}, Nothing} + - terminate::Union{Bool, Nothing} + ↓ + Collect update events from on_update callbacks + ↓ + Return: ExecutedToolCallOutcome(result, is_error=false) + - result::AgentToolResultMutable +``` ### finalizeExecutedToolCall() @@ -433,13 +612,43 @@ function finalizeExecutedToolCall( prepared::PreparedToolCall, executed::ExecutedToolCallOutcome, config::AgentLoopConfig, - signal::Union{Nothing, AbortSignal}, + signal::Union{Nothing, AbortSignal>, )::FinalizedToolCallOutcome ``` -**Flow**: -1. after_tool_call hook (optional) -2. Return FinalizedToolCallOutcome +**Data Flow**: + +``` +Input: executed::ExecutedToolCallOutcome + - result::AgentToolResultMutable + - is_error::Bool + ↓ + [after_tool_call hook] (optional) + Input: AfterToolCallContext( + assistant_message, + tool_call, + args, + result, + is_error, + context + ) + Output: AfterToolCallResult (optional patches) + - content::Union{Vector{MessageContent}, Nothing} + - details::Union{Any, Nothing} + - is_error::Union{Bool, Nothing} + - usage::Union{Usage, Nothing} + - terminate::Union{Bool, Nothing} + ↓ + Apply patches to result (if any) + result.content = result.content ∪ patches.content + result.details = result.details ∪ patches.details + is_error = is_error ∪ patches.is_error + ↓ + Return: FinalizedToolCallOutcome + - tool_call::ToolCall (original) + - result::AgentToolResultMutable (final) + - is_error::Bool +``` ### createToolResultMessage() @@ -449,19 +658,39 @@ function createToolResultMessage( )::ToolResultMessage ``` -**Creates**: -```julia -ToolResultMessage( - "toolResult", - finalized.tool_call.id, - finalized.tool_call.name, - finalized.result.content, - finalized.result.details, - finalized.result.usage, - finalized.result.added_tool_names, - finalized.is_error, - timestamp, -) +**Data Flow**: + +``` +Input: finalized::FinalizedToolCallOutcome + - tool_call::ToolCall + - result::AgentToolResultMutable + - content::Vector{MessageContent} + - details::Any + - usage::Union{Usage, Nothing} + - added_tool_names::Union{Vector{String}, Nothing} + - is_error::Bool + ↓ + Build ToolResultMessage: + • role: "toolResult" + • tool_call_id: tool_call.id + • tool_name: tool_call.name + • content: result.content + • details: result.details + • usage: result.usage + • added_tool_names: result.added_tool_names + • is_error: is_error + • timestamp: Int64(Dates.now(Dates.UTC).datetime) + ↓ + Output: ToolResultMessage + - role::String ("toolResult") + - tool_call_id::String + - tool_name::String + - content::Vector{MessageContent} + - details::Any + - usage::Union{Usage, Nothing} + - added_tool_names::Union{Vector{String}, Nothing} + - is_error::Bool + - timestamp::Timestamp (Int64) ``` ## Execution Modes @@ -650,35 +879,70 @@ AgentStartEvent ### 1. Message Transformation Pipeline ``` -AgentMessage[] (internal) +Vector{AgentMessage} (internal conversation history) │ - │ transform_context() - ▼ -AgentMessage[] (transformed) + ├─ transform_context() (optional hook) + │ Input: Vector{AgentMessage} + │ Output: Vector{AgentMessage} (transformed) │ - │ convert_to_llm() - ▼ -Message[] (LLM API) + └─ convert_to_llm() + │ + │ Type mapping (single dispatch): + │ • UserMessage → UserMessage (pass-through) + │ • AssistantMessage → AssistantMessage (pass-through) + │ • ToolResultMessage → ToolResultMessage (pass-through) + │ • BashExecutionMessage → UserMessage (text conversion) + │ • CompactionSummaryMessage → UserMessage (text wrapped) + │ • BranchSummaryMessage → UserMessage (text wrapped) + │ + ▼ + Vector{Message} (for LLM API) ``` -### 2. Tool Call Lifecycle +### 2. Tool Call Lifecycle (with Data Transformations) ``` -ToolCall (in assistant message) +ToolCall (in AssistantMessage.content) │ - ├─ before_tool_call (hook) + ├─ before_tool_call hook (optional) + │ Input: BeforeToolCallContext( + │ assistant_message::AssistantMessage, + │ tool_call::ToolCall, + │ args::Dict{String, Any}, + │ context::AgentContext + │ ) + │ Output: BeforeToolCallResult (block, reason) or nothing │ ├─ prepareToolCall() - │ ├─ validate arguments - │ └─ prepare arguments (optional) + │ Input: tool_call::ToolCall + │ Output: Union{PreparedToolCall, ImmediateToolCallOutcome} + │ • PreparedToolCall (kind, tool_call, tool, args) + │ • ImmediateToolCallOutcome (immediate, result, is_error) │ - ├─ execute() - │ ├─ Immediate: return result - │ └─ Prepared: async execution + ├─ executePreparedToolCall() (if prepared) + │ Input: PreparedToolCall + │ Output: ExecutedToolCallOutcome + │ tool.execute() returns AgentToolResultMutable + │ • content::Vector{MessageContent} + │ • details::Any + │ • usage::Union{Usage, Nothing} + │ • terminate::Union{Bool, Nothing} │ - ├─ after_tool_call (hook) + ├─ finalizeExecutedToolCall() + │ Input: ExecutedToolCallOutcome + │ Output: FinalizedToolCallOutcome + │ • tool_call::ToolCall + │ • result::AgentToolResultMutable + │ • is_error::Bool │ └─ createToolResultMessage() + Input: FinalizedToolCallOutcome + Output: ToolResultMessage + • role: "toolResult" + • tool_call_id, tool_name + • content::Vector{MessageContent} + • details, usage, added_tool_names + • is_error, timestamp ``` ### 3. Turn Termination diff --git a/learning/04-TYPES_MESSAGES.md b/learning/04-TYPES_MESSAGES.md index 69c0f3d..3fcdfd5 100644 --- a/learning/04-TYPES_MESSAGES.md +++ b/learning/04-TYPES_MESSAGES.md @@ -98,6 +98,52 @@ ## Message Types +### Type Hierarchy + +``` +Message (for LLM API) +├── UserMessage (role: "user") +│ ├── content::Vector{MessageContent} +│ │ ├── TextContent (text::String) +│ │ └── ImageContent (data::String, mime_type::String) +│ └── timestamp::Timestamp (Int64) +├── AssistantMessage (role: "assistant") +│ ├── content::Vector{MessageContent} +│ │ ├── TextContent +│ │ └── ToolCall (type, id, name, arguments::Dict{String, Any}) +│ ├── api::String +│ ├── provider::String +│ ├── model::String +│ ├── usage::Usage +│ │ ├── input, output, cache_read, cache_write, total_tokens::Int64 +│ │ └── cost::UsageCost (input, output, cache_read, cache_write, total::Float64) +│ ├── stop_reason::String +│ ├── error_message::Union{String, Nothing} +│ └── timestamp::Timestamp +└── ToolResultMessage (role: "toolResult") + ├── tool_call_id::String + ├── tool_name::String + ├── content::Vector{MessageContent} + ├── details::Any + ├── usage::Union{Usage, Nothing} + ├── added_tool_names::Union{Vector{String}, Nothing} + ├── is_error::Bool + └── timestamp::Timestamp + +AgentMessage (internal, extends Message) +├── UserMessage (same as above) +├── AssistantMessage (same as above) +├── ToolResultMessage (same as above) +├── BashExecutionMessage (custom, converted to UserMessage) +│ ├── role, command, output, exit_code +│ ├── cancelled, truncated, full_output_path +│ └── exclude_from_context::Bool +├── CompactionSummaryMessage (custom, converted to UserMessage) +│ ├── summary, tokens_before, timestamp +└── BranchSummaryMessage (custom, converted to UserMessage) + ├── summary, from_id, timestamp +``` + ### UserMessage ```julia @@ -108,6 +154,16 @@ struct UserMessage <: Message end ``` +**Usage**: +```julia +# Simple text message +UserMessage( + "user", + [TextContent("Hello, how are you?")], + Int64(Dates.now(Dates.UTC).datetime) +) +``` + **Usage**: ```julia # Simple text message @@ -496,9 +552,9 @@ end **Note**: AgentState is mutable and used internally by Agent -## Key Conversion Functions +## Message Transformation Pipeline -### convertToLlm() +### convertToLlm() - AgentMessage[] → Message[] ```julia function convertToLlm(messages::Vector{AgentMessage})::Vector{Message} @@ -515,26 +571,53 @@ function convertToLlm(messages::Vector{AgentMessage})::Vector{Message} end ``` -**Purpose**: Transform AgentMessage[] to Message[] for LLM API +**Data Flow**: + +``` +Vector{AgentMessage} (internal conversation history) + │ + │ Type dispatch on convertToLlmMessage(): + │ + │ • UserMessage → UserMessage (pass-through) + │ • AssistantMessage → AssistantMessage (pass-through) + │ • ToolResultMessage → ToolResultMessage (pass-through) + │ + │ Custom messages converted to UserMessage: + │ • BashExecutionMessage → UserMessage + │ (via bashExecutionToText() for display) + │ • CompactionSummaryMessage → UserMessage + │ (wrapped with COMPACTION_SUMMARY_PREFIX/SUFFIX) + │ • BranchSummaryMessage → UserMessage + │ (wrapped with BRANCH_SUMMARY_PREFIX/SUFFIX) + │ + ▼ +Vector{Message} (for LLM API) + - Excludes: BashExecutionMessage (if exclude_from_context) + - Includes: All standard messages + converted custom messages +``` **Example**: ```julia -# Input: AgentMessage[] +# Input: Vector{AgentMessage} [ - UserMessage(...), - AssistantMessage(...), - ToolResultMessage(...), - BashExecutionMessage(...), # Will be converted to UserMessage - CompactionSummaryMessage(...), # Will be converted to UserMessage + UserMessage("user", [TextContent("Hello")], 1234567890), + AssistantMessage("assistant", [ + TextContent("Hi there!"), + ToolCall("bash", "call_123", "bash", Dict("command" => "ls"), nothing) + ], "openai", "openai", "gpt-4", Usage(...), "done", nothing, 1234567891), + BashExecutionMessage("custom", "ls -la", "file1.md\nfile2.md\n", 0, false, false, nothing, 1234567892, false), + CompactionSummaryMessage("compactionSummary", "Previous conversation compacted", 1000, 1234567893), ] -# Output: Message[] +# Output: Vector{Message} [ - UserMessage(...), - AssistantMessage(...), - ToolResultMessage(...), - UserMessage(...), # Converted from BashExecutionMessage - UserMessage(...), # Converted from CompactionSummaryMessage + UserMessage("user", [TextContent("Hello")], 1234567890), + AssistantMessage("assistant", [ + TextContent("Hi there!"), + ToolCall(...) + ], "openai", "openai", "gpt-4", Usage(...), "done", nothing, 1234567891), + UserMessage("user", [TextContent("Ran `ls -la`\n```\nfile1.md\nfile2.md\n```\n")], 1234567892), + UserMessage("user", [TextContent("Previous conversation compacted")], 1234567893), ] ``` @@ -571,6 +654,182 @@ function convertToLlmMessage(m::ToolResultMessage) end ``` +## Complete Data Flow Examples + +### Example 1: User Prompt → Assistant Response + +``` +User Input: +"Hello, what's in the current directory?" + +↓ + +prompt(agent, "Hello, what's in the current directory?") + │ + └─► normalizePromptInput(String) + Input: "Hello, what's in the current directory?" + Output: [UserMessage("user", [TextContent("Hello, what's in the current directory?")], timestamp)] + +↓ + +AgentLoop execution: + │ + ├─► transform_context() (optional) + │ Input: [UserMessage(...)] + │ Output: [UserMessage(...)] + │ + ├─► convert_to_llm() + │ Input: [UserMessage(...)] + │ Output: [UserMessage(...)] + │ + ├─► stream_fn() - LLM API + │ Input: model, Context(...), config + │ Output: AssistantMessage with ToolCall[] + │ role: "assistant" + │ content: [ + │ TextContent("I'll check the directory for you."), + │ ToolCall("tool", "tc_123", "bash", Dict("command" => "ls -la"), nothing) + │ ] + │ usage: Usage(input=100, output=20, ...) + │ stop_reason: "done" + │ + ├─► executeToolCalls() + │ Input: AssistantMessage with ToolCall[] + │ Output: ToolResultMessage[] + │ role: "toolResult" + │ tool_call_id: "tc_123" + │ tool_name: "bash" + │ content: [TextContent("file1.md\nfile2.md\n")] + │ is_error: false + │ + └─► Append to context.messages + +↓ + +Final Conversation History: +[ + UserMessage("user", [TextContent("Hello, what's in the current directory?")], ...), + AssistantMessage("assistant", [ + TextContent("I'll check the directory for you."), + ToolCall("tool", "tc_123", "bash", Dict("command" => "ls -la"), nothing) + ], "openai", "openai", "gpt-4", Usage(...), "done", nothing, ...), + ToolResultMessage("toolResult", "tc_123", "bash", [TextContent("file1.md\nfile2.md\n")], ..., false, ...), +] +``` + +### Example 2: Tool Call Execution → Tool Result + +``` +ToolCall from AssistantMessage + │ + ├─ type: "tool" + ├─ id: "tc_123" + ├─ name: "bash" + ├─ arguments: Dict("command" => "ls -la") + └─ partial_json: nothing + ↓ + prepareToolCall(tool_call) + ↓ + Finds tool by name "bash" + ↓ + before_tool_call hook (optional) + Input: BeforeToolCallContext(...) + Output: BeforeToolCallResult(block=false) or nothing + ↓ + validateToolArguments(tool_call) + Input: Dict("command" => "ls -la") + Output: Dict("command" => "ls -la") + ↓ + Return: PreparedToolCall("prepared", tool_call, bash_tool, validated_args) + ↓ + executePreparedToolCall(prepared) + ↓ + tool.execute("tc_123", Dict("command" => "ls -la"), signal, on_update) + ↓ + Bash tool executes "ls -la" command + Returns: AgentToolResultMutable( + content: [TextContent("file1.md\nfile2.md\n")], + details: BashToolDetails(...), + usage: nothing, + added_tool_names: nothing, + terminate: nothing + ) + ↓ + finalizeExecutedToolCall(executed) + ↓ + after_tool_call hook (optional) + Input: AfterToolCallContext(...) + Output: AfterToolCallResult(...) or nothing + ↓ + Return: FinalizedToolCallOutcome( + tool_call: ToolCall(...), + result: AgentToolResultMutable(...), + is_error: false + ) + ↓ + createToolResultMessage(finalized) + ↓ + Return: ToolResultMessage( + role: "toolResult", + tool_call_id: "tc_123", + tool_name: "bash", + content: [TextContent("file1.md\nfile2.md\n")], + details: BashToolDetails(...), + usage: nothing, + added_tool_names: nothing, + is_error: false, + timestamp: Int64(...) + ) +``` + +### Example 3: Custom Message Conversion + +``` +BashExecutionMessage (custom, for logging) + │ + role: "custom" + command: "ls -la" + output: "file1.md\nfile2.md\n" + exit_code: 0 + cancelled: false + truncated: false + full_output_path: nothing + timestamp: 1234567890 + exclude_from_context: false + ↓ + convertToLlmMessage(BashExecutionMessage) + ↓ + bashExecutionToText(msg) + Output: "Ran `ls -la`\n```\nfile1.md\nfile2.md\n```\n" + ↓ + Return: UserMessage( + "user", + [TextContent("Ran `ls -la`\n```\nfile1.md\nfile2.md\n```\n")], + 1234567890 + ) + ↓ + (Excluded if exclude_from_context = true) + +──────────────────────────────────────────────────────────────────── + +CompactionSummaryMessage (custom, for history compression) + │ + role: "compactionSummary" + summary: "Previous 100 turns about Python programming" + tokens_before: 15000 + timestamp: 1234567890 + ↓ + convertToLlmMessage(CompactionSummaryMessage) + ↓ + Text = COMPACTION_SUMMARY_PREFIX + summary + COMPACTION_SUMMARY_SUFFIX + Result: "\nPrevious 100 turns about Python programming\n" + ↓ + Return: UserMessage( + "user", + [TextContent("...\nPrevious 100 turns...\n")], + 1234567890 + ) + ## Summary The type system in AgentCore.jl provides: diff --git a/learning/05-SESSION_MANAGEMENT.md b/learning/05-SESSION_MANAGEMENT.md index db74030..f0a95f1 100644 --- a/learning/05-SESSION_MANAGEMENT.md +++ b/learning/05-SESSION_MANAGEMENT.md @@ -1,6 +1,6 @@ # AgentCore.jl - Session Management Deep Dive -## Session Architecture +## Session Architecture with Data Flow ``` ┌─────────────────────────────────────────────────────────────────────────────┐ @@ -20,6 +20,12 @@ │ ▼ ▼ ▼ ▼ ▼ │ │ Message Message Compaction Message BranchSummary │ │ │ +│ Data Flow: │ +│ AgentMessage[] (AgentState.messages) │ +│ │ │ +│ └─► appendMessage() → MessageEntry │ +│ └─► storage.appendEntry() → JSONL file │ +│ │ │ To navigate to E2 (fork point): │ │ Session.moveTo(E2) │ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ @@ -33,6 +39,38 @@ └─────────────────────────────────────────────────────────────────────────────┘ ``` +## Data Flow: AgentMessage → SessionTreeEntry + +``` +AgentState.messages::Vector{AgentMessage} + │ + ├─► For each message in messages: + │ │ + │ ▼ + │ ┌──────────────────────────────────────────────────────────────┐ + │ │ appendMessage(session, AgentMessage) │ + │ │ Input: message::AgentMessage │ + │ │ Output: entry_id::String │ + │ │ │ + │ │ Steps: │ + │ │ 1. Create MessageEntry: │ + │ │ - type: "message" │ + │ │ - id: createEntryId(storage) │ + │ │ - parent_id: getLeafId(storage) │ + │ │ - timestamp: create_timestamp() │ + │ │ - message: copy(message) │ + │ │ 2. storage.appendEntry(entry) │ + │ │ - Write JSONL line to file │ + │ │ - Update leaf_id │ + │ │ 3. Return entry.id │ + │ └──────────────────────────────────────────────────────────────┘ + │ + └─► Entry stored in JSONL: + {"type":"message","id":"msg_1","parent_id":null,"timestamp":"...","message":{...}} +``` + +## Entry Types + ## Entry Types ```julia diff --git a/learning/06-TOOLS.md b/learning/06-TOOLS.md index dd875ae..1e1f8bc 100644 --- a/learning/06-TOOLS.md +++ b/learning/06-TOOLS.md @@ -1,6 +1,6 @@ # AgentCore.jl - Tools Deep Dive -## Tool Architecture +## Tool Architecture with Data Flow ``` ┌─────────────────────────────────────────────────────────────────────────────┐ @@ -12,7 +12,7 @@ │ - name: String (identifier) │ │ - label: String (display name) │ │ - description: String (what it does) │ -│ - parameters: JSON schema │ +│ - parameters::Any (JSON schema or type) │ │ - execute::Function (main logic) │ │ - prepare_arguments::Union{Function, Nothing} │ │ - execution_mode::Union{ToolExecutionMode, Nothing} │ @@ -21,77 +21,108 @@ ┌───────────────┼───────────────┐ │ │ │ ▼ ▼ ▼ - ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ - │ BashTool │ │ ReadTool │ │ WriteTool │ - │ - bash() │ │ - read() │ │ - write() │ - └─────────────┘ └─────────────┘ └─────────────┘ - ┌─────────────┐ - │ EditTool │ - │ - edit() │ - └─────────────┘ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ BashTool │ │ ReadTool │ │ WriteTool │ + │ - bash() │ │ - read() │ │ - write() │ + └─────────────┘ └─────────────┘ └─────────────┘ + ┌─────────────┐ + │ EditTool │ + │ - edit() │ + └─────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ -│ Tool Execution Flow │ +│ Tool Execution Data Flow │ └─────────────────────────────────────────────────────────────────────────────┘ - Assistant Message - ┌────────────────────────────────────────────────────────┐ - │ AssistantMessage: │ - │ content: [ │ - │ TextContent("I'll check the files..."), │ - │ ToolCall("bash", {command: "ls -la"}), │ - │ ToolCall("read", {path: "README.md"}) │ - │ ] │ - └────────────────────────────────────────────────────────┘ + Input: AssistantMessage (from LLM) + content::Vector{MessageContent} + └─ Contains: TextContent[] and ToolCall[] + + ▼ + ┌─────────────────────────────────────────────────────────────────────┐ + │ extract ToolCalls │ + │ filter(c -> c isa ToolCall, assistant_message.content) │ + └─────────────────────────────────────────────────────────────────────┘ │ ▼ - ┌────────────────────────────────────────────────────────┐ - │ AgentLoop.executeToolCalls() │ - │ - Extract ToolCalls from message content │ - │ - Determine execution mode (sequential/parallel) │ - └────────────────────────────────────────────────────────┘ - │ - ├─► executeToolCallsSequential() - │ (for tools that require order) - │ - └─► executeToolCallsParallel() - (for independent tools) - + ┌─────────────────────────────────────────────────────────────────────┐ + │ ToolCall Type │ + │ • type::String ("tool") │ + │ • id::String (unique identifier) │ + │ • name::String (tool name to execute) │ + │ • arguments::Dict{String, Any} (JSON-like arguments) │ + │ • partial_json::Union{String, Nothing} │ + └─────────────────────────────────────────────────────────────────────┘ │ ├─► prepareToolCall() - │ - before_tool_call hook (optional) - │ - validate arguments - │ - prepare arguments (optional) + │ Input: tool_call::ToolCall + │ Output: Union{PreparedToolCall, ImmediateToolCallOutcome} │ - ├─► execute() - │ - Tool-specific logic - │ - Return AgentToolResult + │ Steps: + │ 1. Find tool by name in context.tools + │ 2. before_tool_call hook (optional) + │ Input: BeforeToolCallContext + │ Output: BeforeToolCallResult (block, reason) + │ 3. prepareToolCallArguments() (optional) + │ Input: tool_call.arguments::Dict{String, Any} + │ Output: prepared_arguments::Any + │ 4. validateToolArguments() + │ Input: prepared_tool_call.arguments + │ Output: validated_args::Any + │ 5. Return: PreparedToolCall(kind, tool_call, tool, args) + │ + ├─► executePreparedToolCall() (if prepared) + │ Input: PreparedToolCall + │ Output: ExecutedToolCallOutcome + │ + │ tool.execute(tool_call.id, args, signal, on_update) + │ Input: tool_call_id::String + │ args::Any + │ signal::Union{Any, Nothing} + │ on_update::Function (streaming updates) + │ Output: AgentToolResultMutable + │ • content::Vector{MessageContent} + │ • details::Any + │ • usage::Union{Usage, Nothing} + │ • terminate::Union{Bool, Nothing} │ ├─► finalizeExecutedToolCall() - │ - after_tool_call hook (optional) + │ Input: ExecutedToolCallOutcome + │ Output: FinalizedToolCallOutcome + │ + │ Steps: + │ 1. after_tool_call hook (optional) + │ Input: AfterToolCallContext + │ Output: AfterToolCallResult (patches) + │ 2. Apply patches to result + │ 3. Return: FinalizedToolCallOutcome(tool_call, result, is_error) │ └─► createToolResultMessage() - - Emit ToolResultMessage - + Input: FinalizedToolCallOutcome + Output: ToolResultMessage + • role: "toolResult" + • tool_call_id::String (matches ToolCall.id) + • tool_name::String (matches ToolCall.name) + • content::Vector{MessageContent} + • details::Any + • usage::Union{Usage, Nothing} + • added_tool_names::Union{Vector{String}, Nothing} + • is_error::Bool + • timestamp::Timestamp (Int64) │ ▼ - ┌────────────────────────────────────────────────────────┐ - │ ToolResultMessage │ - │ - tool_call_id: "ref to original ToolCall" │ - │ - tool_name: "bash" │ - │ - content: [TextContent("file1.md\nfile2.md\n")] │ - │ - is_error: false │ - └────────────────────────────────────────────────────────┘ + ┌─────────────────────────────────────────────────────────────────────┐ + │ ToolResultMessage[] (one per ToolCall) │ + └─────────────────────────────────────────────────────────────────────┘ │ - ▼ - ┌────────────────────────────────────────────────────────┐ - │ AgentState.messages.append(tool_result) │ - │ - Next turn: LLM sees tool results │ - └────────────────────────────────────────────────────────┘ + ├─► Append to context.messages (AgentState.messages) + └─► Next turn: LLM sees tool results as input ``` ## Built-in Tools +## Built-in Tools + ### 1. BashTool ```julia diff --git a/learning/README.md b/learning/README.md index e27f844..252edb9 100644 --- a/learning/README.md +++ b/learning/README.md @@ -165,32 +165,206 @@ AgentStartEvent └─ AgentEndEvent ``` -## Data Flow +## Complete Data Flow with Type Transformations -### Message Transformation +This documentation shows how data is transformed through the agent lifecycle. + +### Message Type Hierarchy ``` -AgentMessage[] (internal) +Message (for LLM API) +├── UserMessage (role: "user") +│ └── content::Vector{MessageContent} +│ ├── TextContent (text::String) +│ └── ImageContent (data::String, mime_type::String) +├── AssistantMessage (role: "assistant") +│ ├── content::Vector{MessageContent} +│ │ ├── TextContent +│ │ └── ToolCall (id, name, arguments::Dict{String, Any}) +│ ├── usage::Usage +│ ├── stop_reason::String +│ └── timestamp::Timestamp +└── ToolResultMessage (role: "toolResult") + ├── tool_call_id::String + ├── tool_name::String + ├── content::Vector{MessageContent} + ├── details::Any + ├── usage::Union{Usage, Nothing} + ├── is_error::Bool + └── timestamp::Timestamp + +AgentMessage (internal, extends Message) +├── UserMessage (same as above) +├── AssistantMessage (same as above) +├── ToolResultMessage (same as above) +├── BashExecutionMessage (custom) +│ ├── role, command, output, exit_code +│ ├── cancelled, truncated, exclude_from_context +│ └── timestamp +├── CompactionSummaryMessage (custom) +│ ├── summary, tokens_before, timestamp +│ └── converted to UserMessage for LLM +└── BranchSummaryMessage (custom) + ├── summary, from_id, timestamp + └── converted to UserMessage for LLM +``` + +### Complete Conversation Flow + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Step 1: User Input (Vector{AgentMessage}) │ +└─────────────────────────────────────────────────────────────────────────────┘ +prompt(agent, "Hello!") │ - ├─ transform_context() (optional) - ▼ -AgentMessage[] (transformed) + └─► normalizePromptInput() + Input: "Hello!"::String + Output: [UserMessage("user", [TextContent("Hello!")], timestamp)] + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Step 2: AgentLoop Processing │ +└─────────────────────────────────────────────────────────────────────────────┘ +runAgentLoop() + │ + ├─► transform_context() (optional hook) + │ Input: [UserMessage(...)]::Vector{AgentMessage} + │ Output: [UserMessage(...)]::Vector{AgentMessage} + │ + ├─► convert_to_llm() + │ Input: [UserMessage(...)]::Vector{AgentMessage} + │ Output: [UserMessage(...)]::Vector{Message} + │ + ├─► stream_fn() - LLM API call + │ Input: model, Context(...), config + │ Output: AssistantMessage with ToolCall[] + │ + ├─► executeToolCalls() + │ Input: AssistantMessage (with ToolCall[]) + │ Output: ToolResultMessage[] + │ + └─► Emit events and append to context.messages + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Step 3: Final Conversation State │ +└─────────────────────────────────────────────────────────────────────────────┘ +context.messages::Vector{AgentMessage} +├─ UserMessage("user", [TextContent("Hello!")], ...) +├─ AssistantMessage("assistant", [ +│ TextContent("Hi there!"), +│ ToolCall("bash", {...}) +│ ], ...) +└─ ToolResultMessage("toolResult", "bash", [TextContent("...")], ...) + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Step 4: AgentEndEvent (final output) │ +└─────────────────────────────────────────────────────────────────────────────┘ +AgentEndEvent(messages::Vector{AgentMessage}) + └─ Contains full conversation history +User Input (String / AgentMessage / Vector{AgentMessage}) + │ + ├─► normalizePromptInput() + │ Input: input::Union{String, AgentMessage, Vector{AgentMessage}} + │ Output: Vector{AgentMessage} + │ • String → UserMessage("user", [TextContent(input)], timestamp) + │ • AgentMessage → [input] + │ • Vector{AgentMessage} → input (pass-through) + │ + ├─► prompt(agent, messages) + │ └─► runPromptMessages() │ - ├─ convert_to_llm() ▼ -Message[] (LLM API) +AgentLoop Execution: + │ + ├─► transform_context() (optional hook) + │ Input: context.messages::Vector{AgentMessage} + │ Output: messages::Vector{AgentMessage} (transformed) + │ + ├─► convert_to_llm() + │ Input: messages::Vector{AgentMessage} + │ Output: llm_messages::Vector{Message} + │ + │ AgentMessage → Message mapping: + │ • UserMessage → UserMessage (pass-through) + │ • AssistantMessage → AssistantMessage (pass-through) + │ • ToolResultMessage → ToolResultMessage (pass-through) + │ • BashExecutionMessage → UserMessage (text conversion) + │ • CompactionSummaryMessage → UserMessage (text wrapped) + │ • BranchSummaryMessage → UserMessage (text wrapped) + │ + ├─► LLM API Call (stream_fn) + │ Input: model, Context(system_prompt, llm_messages, tools), config + │ Output: Stream{AssistantMessageEvent} + │ + ├─► AssistantMessage (returned from LLM) + │ content::Vector{MessageContent} + │ └─ Contains: TextContent[] and/or ToolCall[] + │ + ├─► executeToolCalls() (if ToolCall[] in content) + │ │ + │ ├─► prepareToolCall() for each ToolCall + │ │ Input: tool_call::ToolCall + │ │ Output: PreparedToolCall or ImmediateToolCallOutcome + │ │ + │ ├─► executePreparedToolCall() (if prepared) + │ │ Input: PreparedToolCall + │ │ Output: ExecutedToolCallOutcome + │ │ tool.execute() returns AgentToolResultMutable + │ │ + │ ├─► finalizeExecutedToolCall() + │ │ Input: ExecutedToolCallOutcome + │ │ Output: FinalizedToolCallOutcome + │ │ + │ └─► createToolResultMessage() + │ Input: FinalizedToolCallOutcome + │ Output: ToolResultMessage + │ • role: "toolResult" + │ • tool_call_id, tool_name + │ • content::Vector{MessageContent} + │ • details, usage, added_tool_names + │ • is_error, timestamp + │ + └─► Append to context.messages and new_messages + │ + ▼ + Vector{AgentMessage} (final conversation history) + Contains: [UserMessage, AssistantMessage, ToolResultMessage, ...] ``` ### Tool Execution Flow ``` -ToolCall (in assistant message) +ToolCall (in AssistantMessage.content) + │ + ├─ before_tool_call hook (optional) + │ Input: BeforeToolCallContext + │ Output: BeforeToolCallResult (block, reason) or nothing │ - ├─ before_tool_call hook ├─ prepareToolCall() - ├─ execute() - ├─ after_tool_call hook + │ Input: tool_call::ToolCall + │ Output: Union{PreparedToolCall, ImmediateToolCallOutcome} + │ • Validates tool exists + │ • Runs before_tool_call hook + │ • Runs prepare_arguments hook (optional) + │ • Runs validateToolArguments (optional) + │ + ├─ executePreparedToolCall() (if prepared) + │ Input: PreparedToolCall + │ Output: ExecutedToolCallOutcome + │ tool.execute() returns AgentToolResultMutable + │ + ├─ finalizeExecutedToolCall() + │ Input: ExecutedToolCallOutcome + │ Output: FinalizedToolCallOutcome + │ Runs after_tool_call hook (optional) + │ └─ createToolResultMessage() + Input: FinalizedToolCallOutcome + Output: ToolResultMessage + • role: "toolResult" + • tool_call_id, tool_name + • content::Vector{MessageContent} + • details, usage, added_tool_names + • is_error, timestamp ``` ## Best Practices