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