Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7876ff21eb | |||
| c9a7661e93 | |||
| 8d5c661562 | |||
| 244cfc4b96 | |||
| a541905b72 |
@@ -88,15 +88,15 @@
|
||||
│ │ 1. Emit AgentStartEvent │ │
|
||||
│ │ 2. Emit TurnStartEvent │ │
|
||||
│ │ 3. Process prompts (emit MessageStart/End) │ │
|
||||
│ │ 4.┌────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ while true: │ │ │
|
||||
│ │ │ │ Process steering/follow-up messages │ │ │
|
||||
│ │ │ │ Stream assistant response (LLM call) │ │ │
|
||||
│ │ │ │ Execute tool calls (parallel/sequential) │ │ │
|
||||
│ │ │ │ Emit TurnEndEvent │ │ │
|
||||
│ │ │ │ Check if should stop │ │ │
|
||||
│ │ │ │ Get next steering messages │ │ │
|
||||
│ │ └───┴────────────────────────────────────────────┘ │ │
|
||||
│ │ 4.┌────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ while true: │ │ │
|
||||
│ │ │ │ Process steering/follow-up messages │ │ │
|
||||
│ │ │ │ Stream assistant response (LLM call) │ │ │
|
||||
│ │ │ │ Execute tool calls (parallel/sequential) │ │ │
|
||||
│ │ │ │ Emit TurnEndEvent │ │ │
|
||||
│ │ │ │ Check if should stop │ │ │
|
||||
│ │ │ │ Get next steering messages │ │ │
|
||||
│ │ └───┴────────────────────────────────────────────┘ │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
@@ -405,87 +405,295 @@
|
||||
└── Manages ──► PromptTemplates
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
## Data Flow with Type Transformations
|
||||
|
||||
### Complete User Input → Conversation History Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Data Flow Between Layers │
|
||||
│ Level 1: User Input │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
User Input
|
||||
• String: "Hello, what's in the directory?"
|
||||
• AgentMessage: UserMessage(...)
|
||||
• Vector{AgentMessage}: [UserMessage(...), AssistantMessage(...)]
|
||||
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Agent.prompt() / normalizePromptInput() │
|
||||
│ │
|
||||
│ Type Dispatch: │
|
||||
│ • String → UserMessage("user", [TextContent(input)], ts) │
|
||||
│ • AgentMessage → [input] (wrap in array) │
|
||||
│ • Vector{AgentMessage} → input (pass-through) │
|
||||
│ │
|
||||
│ Output: Vector{AgentMessage} │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ AgentState.messages (AgentMessage[]) │
|
||||
│ │
|
||||
│ AgentMessage Types: │
|
||||
│ • UserMessage (role: "user") │
|
||||
│ • AssistantMessage (role: "assistant") │
|
||||
│ • ToolResultMessage (role: "toolResult") │
|
||||
│ • BashExecutionMessage (custom) │
|
||||
│ • CompactionSummaryMessage (custom) │
|
||||
│ • BranchSummaryMessage (custom) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Level 2: AgentLoop Processing │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
User Input (String/Message)
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ transform_context() (optional hook) │
|
||||
│ │
|
||||
│ Input: Vector{AgentMessage} │
|
||||
│ Output: Vector{AgentMessage} (transformed) │
|
||||
│ - Can truncate, filter, or modify messages │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Agent.prompt() │
|
||||
│ - normalizeInput() │
|
||||
└──────────────────────┘
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ convertToLlm() - Type Transformation Pipeline │
|
||||
│ │
|
||||
│ Input: Vector{AgentMessage} │
|
||||
│ Output: Vector{Message} (for LLM API) │
|
||||
│ │
|
||||
│ Single Dispatch Mapping: │
|
||||
│ • UserMessage → UserMessage (pass-through) │
|
||||
│ • AssistantMessage → AssistantMessage (pass-through) │
|
||||
│ • ToolResultMessage → ToolResultMessage (pass-through) │
|
||||
│ │
|
||||
│ Custom Message Conversions: │
|
||||
│ • BashExecutionMessage → UserMessage (via bashExecutionToText)│
|
||||
│ • CompactionSummaryMessage → UserMessage (wrapped) │
|
||||
│ • BranchSummaryMessage → UserMessage (wrapped) │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AgentState.messages │ ──► AgentMessage[]
|
||||
└──────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Context for LLM API │
|
||||
│ - system_prompt: String │
|
||||
│ - messages: Vector{Message} │
|
||||
│ - tools: Vector{AgentTool} │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AgentLoop │
|
||||
│ - transform_context │
|
||||
└──────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ LLM API Call (stream_fn) │
|
||||
│ │
|
||||
│ Input: model, context, config │
|
||||
│ Output: Stream{AssistantMessageEvent} │
|
||||
│ • StartEvent: partial AssistantMessage │
|
||||
│ • TextStartEvent/TextDeltaEvent/TextEndEvent │
|
||||
│ • ToolCallStartEvent/ToolCallDeltaEvent/ToolCallEndEvent │
|
||||
│ • DoneEvent: final AssistantMessage with usage │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ convertToLlm() │ ──► Transforms AgentMessage[] to Message[]
|
||||
└──────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ AssistantMessage (returned from LLM) │
|
||||
│ │
|
||||
│ • role: "assistant" │
|
||||
│ • content: Vector{MessageContent} │
|
||||
│ └─ Contains: TextContent[] and/or ToolCall[] │
|
||||
│ • api, provider, model: String │
|
||||
│ • usage: Usage (input, output, cache_read, cache_write) │
|
||||
│ • stop_reason: String ("done", "length", "error", etc.) │
|
||||
│ • error_message: Union{String, Nothing} │
|
||||
│ • timestamp: Timestamp (Int64) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
├─► Append to AgentState.messages (AssistantMessage)
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ LLM API (StreamFn) │
|
||||
│ - Context: Message[] │
|
||||
└──────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ executeToolCalls() - Tool Processing │
|
||||
│ │
|
||||
│ Extract: filter(c -> c isa ToolCall, assistant.content) │
|
||||
│ Output: ExecutedToolCallBatch │
|
||||
│ • messages: Vector{ToolResultMessage} │
|
||||
│ • terminate: Bool │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Response (Streaming) │
|
||||
│ - Text deltas │
|
||||
│ - Tool call deltas │
|
||||
└──────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ ToolResultMessage (for each ToolCall) │
|
||||
│ │
|
||||
│ • role: "toolResult" │
|
||||
│ • tool_call_id: String (matches ToolCall.id) │
|
||||
│ • tool_name: String (matches ToolCall.name) │
|
||||
│ • content: Vector{MessageContent} │
|
||||
│ • details: Any (tool-specific) │
|
||||
│ • usage: Union{Usage, Nothing} │
|
||||
│ • added_tool_names: Union{Vector{String}, Nothing} │
|
||||
│ • is_error: Bool │
|
||||
│ • timestamp: Timestamp (Int64) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
├─► Append to AgentState.messages (ToolResultMessage)
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AssistantMessage │
|
||||
│ - content: Message[] │
|
||||
└──────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Updated AgentState.messages (AgentMessage[]) │
|
||||
│ │
|
||||
│ Conversation History: │
|
||||
│ [UserMessage, AssistantMessage, ToolResultMessage, ...] │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Level 3: Session Storage (optional, for persistence) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
AgentState.messages (Vector{AgentMessage})
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AgentState.messages │ ──► Appended to conversation
|
||||
└──────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Session Storage (JSONL) │
|
||||
│ │
|
||||
│ SessionTreeEntry Types: │
|
||||
│ • MessageEntry (agent_message) │
|
||||
│ • CompactionEntry (summary, tokens_before) │
|
||||
│ • BranchSummaryEntry (from_id, summary) │
|
||||
│ • ModelChangeEntry (provider, model_id) │
|
||||
│ • ThinkingLevelChangeEntry (thinking_level) │
|
||||
│ • ActiveToolsChangeEntry (active_tool_names) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Tool Execution │
|
||||
│ - Extract ToolCalls │
|
||||
│ - Execute tools │
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ ToolResultMessage[] │
|
||||
└──────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AgentState.messages │ ──► Tool results appended
|
||||
└──────────────────────┘
|
||||
│
|
||||
│ (Loop back to LLM or end)
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Session Storage │
|
||||
│ - JSONL format │
|
||||
│ - Tree entries │
|
||||
└──────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Persisted Data (JSON format) │
|
||||
│ - Each entry has: id, parent_id, timestamp, type │
|
||||
│ - MessageEntry contains full AgentMessage │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Tool Call Execution Flow (Detailed)
|
||||
|
||||
```
|
||||
ToolCall (from AssistantMessage.content)
|
||||
│
|
||||
├─ type: "tool"
|
||||
├─ id: "tc_abc123"
|
||||
├─ name: "bash"
|
||||
├─ arguments: Dict("command" => "ls -la")
|
||||
└─ partial_json: nothing
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ prepareToolCall() │
|
||||
│ │
|
||||
│ Input: tool_call::ToolCall │
|
||||
│ Output: Union{PreparedToolCall, ImmediateToolCallOutcome} │
|
||||
│ │
|
||||
│ Steps: │
|
||||
│ 1. Find tool by name in current_context.tools │
|
||||
│ 2. before_tool_call hook (optional) │
|
||||
│ Input: BeforeToolCallContext │
|
||||
│ Output: BeforeToolCallResult (block, reason) or nothing │
|
||||
│ 3. prepareToolCallArguments() (optional) │
|
||||
│ Input: tool_call.arguments::Dict │
|
||||
│ Output: prepared_arguments::Any │
|
||||
│ 4. validateToolArguments() (optional) │
|
||||
│ Input: prepared_tool_call.arguments │
|
||||
│ Output: validated_args::Any │
|
||||
│ 5. Return: PreparedToolCall(kind, tool_call, tool, args) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ executePreparedToolCall() (if prepared) │
|
||||
│ │
|
||||
│ Input: PreparedToolCall │
|
||||
│ Output: ExecutedToolCallOutcome │
|
||||
│ │
|
||||
│ tool.execute(tool_call.id, args, signal, on_update) │
|
||||
│ │ │
|
||||
│ └─ Returns: AgentToolResultMutable │
|
||||
│ • content::Vector{MessageContent} │
|
||||
│ • details::Any │
|
||||
│ • usage::Union{Usage, Nothing} │
|
||||
│ • added_tool_names::Union{Vector{String}, Nothing} │
|
||||
│ • terminate::Union{Bool, Nothing} │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ finalizeExecutedToolCall() │
|
||||
│ │
|
||||
│ Input: ExecutedToolCallOutcome │
|
||||
│ Output: FinalizedToolCallOutcome │
|
||||
│ │
|
||||
│ Steps: │
|
||||
│ 1. after_tool_call hook (optional) │
|
||||
│ Input: AfterToolCallContext │
|
||||
│ Output: AfterToolCallResult (patches) │
|
||||
│ 2. Apply patches to result │
|
||||
│ 3. Return: FinalizedToolCallOutcome(tool_call, result, error)│
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ createToolResultMessage() │
|
||||
│ │
|
||||
│ Input: FinalizedToolCallOutcome │
|
||||
│ Output: ToolResultMessage │
|
||||
│ │
|
||||
│ Fields: │
|
||||
│ • role: "toolResult" │
|
||||
│ • tool_call_id: tool_call.id │
|
||||
│ • tool_name: tool_call.name │
|
||||
│ • content: result.content │
|
||||
│ • details: result.details │
|
||||
│ • usage: result.usage │
|
||||
│ • added_tool_names: result.added_tool_names │
|
||||
│ • is_error: is_error │
|
||||
│ • timestamp: Int64(Dates.now(Dates.UTC).datetime) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Emit: ToolResultMessage to conversation │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Summary of Type Transformations │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
User Input (String)
|
||||
│
|
||||
├─► normalizePromptInput()
|
||||
│ └─► UserMessage (AgentMessage subtype)
|
||||
│
|
||||
Vector{AgentMessage}
|
||||
│
|
||||
├─► transform_context() (optional)
|
||||
│ └─► Vector{AgentMessage} (transformed)
|
||||
│
|
||||
├─► convertToLlm()
|
||||
│ └─► Vector{Message} (LLM API format)
|
||||
│ ├── UserMessage (pass-through)
|
||||
│ ├── AssistantMessage (pass-through)
|
||||
│ ├── ToolResultMessage (pass-through)
|
||||
│ └── Custom messages → UserMessage
|
||||
│
|
||||
AssistantMessage (from LLM)
|
||||
│
|
||||
├─► executeToolCalls()
|
||||
│ └─► ToolResultMessage[]
|
||||
│
|
||||
ToolResultMessage[]
|
||||
│
|
||||
└─► Appended to AgentState.messages
|
||||
└─► Vector{AgentMessage} (updated conversation history)
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
## Summary
|
||||
|
||||
The AgentCore.jl architecture follows a clean separation of concerns:
|
||||
@@ -495,4 +703,48 @@ The AgentCore.jl architecture follows a clean separation of concerns:
|
||||
3. **AgentLoop** - Core LLM interaction loop
|
||||
4. **Session** - Conversation history management
|
||||
|
||||
### Data Transformation Summary
|
||||
|
||||
```
|
||||
Input Type Flow:
|
||||
User Input (String/Message)
|
||||
│
|
||||
├─ normalizePromptInput()
|
||||
│ └─► Vector{AgentMessage}
|
||||
│
|
||||
├─ transform_context() (optional)
|
||||
│ └─► Vector{AgentMessage} (transformed)
|
||||
│
|
||||
├─ convertToLlm()
|
||||
│ └─► Vector{Message} (LLM API format)
|
||||
│
|
||||
├─ LLM API (stream_fn)
|
||||
│ └─► AssistantMessage
|
||||
│
|
||||
├─ executeToolCalls()
|
||||
│ └─► ToolResultMessage[]
|
||||
│
|
||||
└─► Vector{AgentMessage} (final conversation)
|
||||
```
|
||||
|
||||
### Key Data Flow Patterns
|
||||
|
||||
1. **Message Transformation**: `AgentMessage[] → Message[]` via `convertToLlm()`
|
||||
- UserMessage → UserMessage (pass-through)
|
||||
- AssistantMessage → AssistantMessage (pass-through)
|
||||
- ToolResultMessage → ToolResultMessage (pass-through)
|
||||
- Custom messages (Bash, Compaction, Branch) → UserMessage
|
||||
|
||||
2. **Tool Execution**: `ToolCall → ToolResultMessage`
|
||||
- prepareToolCall() validates and prepares
|
||||
- execute() runs the tool
|
||||
- finalize() applies hooks and returns outcome
|
||||
- createToolResultMessage() creates result entry
|
||||
|
||||
3. **Event Streaming**: `Stream{Event}` with lifecycle events
|
||||
- AgentStartEvent, TurnStartEvent
|
||||
- MessageStartEvent, MessageUpdateEvent, MessageEndEvent
|
||||
- ToolExecutionStartEvent, ToolExecutionEndEvent
|
||||
- TurnEndEvent, AgentEndEvent
|
||||
|
||||
Each layer transforms data and passes it to the next layer, with clear interfaces and event hooks for customization.
|
||||
|
||||
@@ -36,8 +36,8 @@ end
|
||||
# Create agent with options
|
||||
agent = Agent(Dict{Symbol, Any}(
|
||||
:systemPrompt => "You are a helpful assistant",
|
||||
:model => Model(...),
|
||||
:thinkingLevel => THINKING_MEDIUM,
|
||||
:model => Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
|
||||
:thinkingLevel => THINKING_OFF,
|
||||
:tools => [bash_tool, read_tool],
|
||||
:steeringMode => QUEUE_ONE_AT_A_TIME,
|
||||
:followUpMode => QUEUE_ONE_AT_A_TIME,
|
||||
@@ -243,16 +243,35 @@ followUp(agent, UserMessage(...))
|
||||
|
||||
```julia
|
||||
# Transform messages before sending to LLM
|
||||
function myConvertToLlm(messages::Vector{AgentMessage})
|
||||
return filter(
|
||||
m -> m.role in ["user", "assistant", "toolResult"],
|
||||
messages
|
||||
)
|
||||
function myConvertToLlm(messages::Vector{AgentMessage})::Vector{Message}
|
||||
result::Vector{Message} = Message[]
|
||||
for m in messages
|
||||
converted = convertToLlmMessage(m)
|
||||
if !isnothing(converted)
|
||||
push!(result, converted)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
agent = Agent(Dict(:convertToLlm => myConvertToLlm))
|
||||
```
|
||||
|
||||
**Data Flow**:
|
||||
```
|
||||
Vector{AgentMessage}
|
||||
│
|
||||
│ convertToLlmMessage() dispatches on type:
|
||||
│ • UserMessage → UserMessage (pass-through)
|
||||
│ • AssistantMessage → AssistantMessage (pass-through)
|
||||
│ • ToolResultMessage → ToolResultMessage (pass-through)
|
||||
│ • BashExecutionMessage → UserMessage (bashExecutionToText)
|
||||
│ • CompactionSummaryMessage → UserMessage (wrapped)
|
||||
│ • BranchSummaryMessage → UserMessage (wrapped)
|
||||
▼
|
||||
Vector{Message} (for LLM API)
|
||||
```
|
||||
|
||||
#### transform_context
|
||||
|
||||
```julia
|
||||
@@ -271,7 +290,7 @@ agent = Agent(Dict(:transformContext => myTransformContext))
|
||||
# Hook before tool execution
|
||||
function myBeforeToolCall(context, signal)
|
||||
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
|
||||
|
||||
agent = Agent(Dict(:beforeToolCall => myBeforeToolCall))
|
||||
@@ -284,8 +303,11 @@ agent = Agent(Dict(:beforeToolCall => myBeforeToolCall))
|
||||
function myAfterToolCall(context, signal)
|
||||
# Can modify tool result
|
||||
return AfterToolCallResult(
|
||||
content = context.result.content,
|
||||
terminate = context.result.terminate
|
||||
context.result.content,
|
||||
context.result.details,
|
||||
nothing,
|
||||
nothing,
|
||||
context.result.terminate
|
||||
)
|
||||
end
|
||||
|
||||
@@ -300,9 +322,9 @@ function myPrepareNextTurn(context, signal)
|
||||
# context: PrepareNextTurnContext
|
||||
# Returns AgentLoopTurnUpdate or nothing
|
||||
return AgentLoopTurnUpdate(
|
||||
context = context.context,
|
||||
model = context.context.model, # Can change model
|
||||
thinking_level = THINKING_HIGH # Can change thinking level
|
||||
context.context, # context
|
||||
context.context.model, # model - can change
|
||||
THINKING_HIGH # thinking_level - can change
|
||||
)
|
||||
end
|
||||
|
||||
@@ -315,11 +337,11 @@ agent = Agent(Dict(:prepareNextTurn => myPrepareNextTurn))
|
||||
# Check if agent is busy
|
||||
if !isnothing(agent.active_run)
|
||||
# Agent is processing
|
||||
abort(agent) # Abort current run
|
||||
abort(agent) # Abort current run (NOTE: implementation is a TODO stub)
|
||||
end
|
||||
|
||||
# Wait for completion
|
||||
wait_for_idle(agent) # Returns Promise
|
||||
waitForIdle(agent) # Returns Promise
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
@@ -348,7 +370,7 @@ end
|
||||
prompt(agent, "What's in the current directory?")
|
||||
|
||||
# 4. Wait for completion
|
||||
wait_for_idle(agent)
|
||||
waitForIdle(agent)
|
||||
|
||||
# 5. Check final state
|
||||
state = get_state(agent)
|
||||
@@ -356,7 +378,7 @@ println("Total messages: $(length(state.messages))")
|
||||
|
||||
# 6. Continue with steering
|
||||
steer(agent, UserMessage(...))
|
||||
wait_for_idle(agent)
|
||||
waitForIdle(agent)
|
||||
|
||||
# 7. Clean up
|
||||
unsubscribe() # Stop listening
|
||||
|
||||
+538
-223
@@ -27,11 +27,16 @@ agentLoopContinue()
|
||||
└─ Returns: EventStream
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Internal Flow │
|
||||
│ Data Flow with Type Transformations │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 1. runAgentLoop() ── Entry point for new conversation │
|
||||
│ Input: prompts::Vector{AgentMessage} │
|
||||
│ context::AgentContext (system_prompt, messages, tools) │
|
||||
│ config::AgentLoopConfig │
|
||||
│ Output: new_messages::Vector{AgentMessage} (appended prompts + turns) │
|
||||
│ │
|
||||
│ - Creates copy of prompts │
|
||||
│ - Appends prompts to context.messages │
|
||||
│ - Emits AgentStartEvent │
|
||||
@@ -43,43 +48,116 @@ agentLoopContinue()
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 2. runLoop() ── Main event loop │
|
||||
│ Input: current_context::AgentContext │
|
||||
│ new_messages::Vector{AgentMessage} │
|
||||
│ Output: N/A (writes to new_messages and context.messages) │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ while true: │ │
|
||||
│ │ 1. Get steering/follow-up messages (if any) │ │
|
||||
│ │ 2. Emit messages as UserMessage │ │
|
||||
│ │ 3. streamAssistantResponse() │ │
|
||||
│ │ 4. Execute tool calls (sequential or parallel) │ │
|
||||
│ │ 5. Emit TurnEndEvent │ │
|
||||
│ │ 6. prepare_next_turn (optional) │ │
|
||||
│ │ 7. should_stop_after_turn? (check termination) │ │
|
||||
│ │ 8. Loop continues if not terminated │ │
|
||||
│ │ while true (outer loop: follow-up messages) │ │
|
||||
│ │ has_more_tool_calls = true │ │
|
||||
│ │ 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() │ │
|
||||
│ │ 4. If error/aborted: emit TurnEnd, AgentEnd, return │ │
|
||||
│ │ 5. Execute tool calls (sequential or parallel) │ │
|
||||
│ │ 6. has_more_tool_calls = !batch.terminate │ │
|
||||
│ │ 7. Emit TurnEndEvent │ │
|
||||
│ │ 8. prepare_next_turn (optional config update) │ │
|
||||
│ │ 9. should_stop_after_turn? (early return) │ │
|
||||
│ │ 10. pending_messages = get_steering_messages() │ │
|
||||
│ │ if !isempty(get_follow_up_messages()) → continue outer loop │ │
|
||||
│ │ break │ │
|
||||
│ │ emit AgentEndEvent │ │
|
||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 3. streamAssistantResponse() ── LLM interaction │
|
||||
│ - transform_context (optional) │
|
||||
│ - convert_to_llm (transform to Message[]) │
|
||||
│ - Call stream_fn (LLM API) │
|
||||
│ - Stream response deltas │
|
||||
│ - Emit MessageStart/Update/End events │
|
||||
│ Input: context::AgentContext │
|
||||
│ config::AgentLoopConfig │
|
||||
│ Output: message::AssistantMessage │
|
||||
│ │
|
||||
│ Data Transformations: │
|
||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Step 1: transform_context (optional) │ │
|
||||
│ │ Input: context.messages::Vector{AgentMessage} │ │
|
||||
│ │ Output: messages::Vector{AgentMessage} (transformed) │ │
|
||||
│ │ │ │
|
||||
│ │ Step 2: convert_to_llm │ │
|
||||
│ │ Input: messages::Vector{AgentMessage} │ │
|
||||
│ │ Output: llm_messages::Vector{Message} │ │
|
||||
│ │ - UserMessage → UserMessage │ │
|
||||
│ │ - AssistantMessage → AssistantMessage │ │
|
||||
│ │ - ToolResultMessage → ToolResultMessage │ │
|
||||
│ │ - BashExecutionMessage → UserMessage │ │
|
||||
│ │ - CompactionSummaryMessage → UserMessage │ │
|
||||
│ │ - BranchSummaryMessage → UserMessage │ │
|
||||
│ │ │ │
|
||||
│ │ Step 3: Call stream_fn │ │
|
||||
│ │ Input: model, llm_context::Context, config │ │
|
||||
│ │ Output: response::Stream (events) │ │
|
||||
│ │ │ │
|
||||
│ │ Step 4: Stream events │ │
|
||||
│ │ Events: start, text_start/delta/end, toolcall_start/delta/end │ │
|
||||
│ │ Final: AssistantMessage (with ToolCall[] in content) │ │
|
||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 4. executeToolCalls() ── Tool execution │
|
||||
│ Input: assistant_message::AssistantMessage (contains ToolCall[]) │
|
||||
│ current_context::AgentContext │
|
||||
│ Output: ExecutedToolCallBatch (messages::ToolResultMessage[], terminate) │
|
||||
│ │
|
||||
│ For each ToolCall: │
|
||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ if EXECUTION_SEQUENTIAL || has_sequential_tool: │ │
|
||||
│ │ executeToolCallsSequential() │ │
|
||||
│ │ else: │ │
|
||||
│ │ executeToolCallsParallel() │ │
|
||||
│ │ prepareToolCall() │ │
|
||||
│ │ Input: tool_call::ToolCall │ │
|
||||
│ │ Output: PreparedToolCall or ImmediateToolCallOutcome │ │
|
||||
│ │ - validates arguments │ │
|
||||
│ │ - runs before_tool_call hook (optional) │ │
|
||||
│ │ - runs prepare_arguments hook (optional) │ │
|
||||
│ │ │ │
|
||||
│ │ executePreparedToolCall() (if prepared) │ │
|
||||
│ │ Input: PreparedToolCall │ │
|
||||
│ │ Output: ExecutedToolCallOutcome │ │
|
||||
│ │ - calls tool.execute() │ │
|
||||
│ │ - returns AgentToolResultMutable │ │
|
||||
│ │ │ │
|
||||
│ │ finalizeExecutedToolCall() │ │
|
||||
│ │ Input: ExecutedToolCallOutcome │ │
|
||||
│ │ Output: FinalizedToolCallOutcome │ │
|
||||
│ │ - runs after_tool_call hook (optional) │ │
|
||||
│ │ - returns ToolCall + AgentToolResultMutable + is_error │ │
|
||||
│ │ │ │
|
||||
│ │ createToolResultMessage() │ │
|
||||
│ │ Input: FinalizedToolCallOutcome │ │
|
||||
│ │ Output: ToolResultMessage │ │
|
||||
│ │ - role: "toolResult" │ │
|
||||
│ │ - tool_call_id, tool_name, content, details │ │
|
||||
│ │ - usage, added_tool_names, is_error, timestamp │ │
|
||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Sequential: executeToolCallsSequential() │ │
|
||||
│ │ - Executes tools one at a time, waits for each │ │
|
||||
│ │ - Returns batch of ToolResultMessage[] │ │
|
||||
│ │ │ │
|
||||
│ │ Parallel: executeToolCallsParallel() │ │
|
||||
│ │ - Creates closures for async execution │ │
|
||||
│ │ - Executes all closures, collects results │ │
|
||||
│ │ - Returns batch of ToolResultMessage[] │ │
|
||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 5. AgentEndEvent ── Final event with all messages │
|
||||
│ Output: messages::Vector{AgentMessage} │
|
||||
│ Contains: [UserMessage, AssistantMessage, ToolResultMessage, ...] │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -104,9 +182,23 @@ struct AgentLoopConfig
|
||||
get_api_key::Union{Function, Nothing}
|
||||
get_steering_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
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
### agentLoop()
|
||||
@@ -161,11 +253,13 @@ function runAgentLoop(
|
||||
**Purpose**: Execute agent loop with initial prompts
|
||||
|
||||
**Flow**:
|
||||
1. Copy prompts to new_messages
|
||||
2. Append prompts to context.messages
|
||||
3. Emit AgentStartEvent
|
||||
4. For each prompt: emit MessageStartEvent, MessageEndEvent
|
||||
5. Call runLoop()
|
||||
1. Copy prompts to `new_messages`
|
||||
2. Create `current_context` with prompts appended to `context.messages`
|
||||
3. Emit `AgentStartEvent`
|
||||
4. Emit `TurnStartEvent`
|
||||
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
|
||||
|
||||
@@ -180,109 +274,124 @@ function runLoop(
|
||||
)::Nothing
|
||||
```
|
||||
|
||||
**Main Loop**:
|
||||
**Main Loop** (simplified — shows structure; actual code has type annotations):
|
||||
```julia
|
||||
current_context = initial_context
|
||||
config = initial_config
|
||||
first_turn = true
|
||||
pending_messages = get_steering_messages()
|
||||
pending_messages = get_steering_messages(config)
|
||||
|
||||
while true
|
||||
# Process steering/follow-up messages
|
||||
while !isempty(pending_messages)
|
||||
has_more_tool_calls = true
|
||||
|
||||
# Inner loop: process pending messages AND/OR tool results
|
||||
while has_more_tool_calls || !isempty(pending_messages)
|
||||
if !first_turn
|
||||
emit(TurnStartEvent())
|
||||
else
|
||||
first_turn = false
|
||||
end
|
||||
|
||||
# Emit pending messages
|
||||
for message in pending_messages
|
||||
emit(MessageStartEvent(message))
|
||||
emit(MessageEndEvent(message))
|
||||
push!(current_context.messages, message)
|
||||
push!(new_messages, message)
|
||||
|
||||
# Emit pending messages (steering / follow-up)
|
||||
if !isempty(pending_messages)
|
||||
for message in pending_messages
|
||||
emit(MessageStartEvent(message))
|
||||
emit(MessageEndEvent(message))
|
||||
push!(current_context.messages, message)
|
||||
push!(new_messages, message)
|
||||
end
|
||||
pending_messages = AgentMessage[]
|
||||
end
|
||||
|
||||
pending_messages = []
|
||||
end
|
||||
|
||||
# Stream assistant response
|
||||
message = streamAssistantResponse(
|
||||
current_context,
|
||||
config,
|
||||
signal,
|
||||
emit,
|
||||
stream_function,
|
||||
)
|
||||
push!(new_messages, message)
|
||||
|
||||
# Check for errors
|
||||
if message.stop_reason in ("error", "aborted")
|
||||
emit(TurnEndEvent(message, []))
|
||||
emit(AgentEndEvent(new_messages))
|
||||
return
|
||||
end
|
||||
|
||||
# Execute tool calls
|
||||
tool_calls = filter(c -> c isa ToolCall, message.content)
|
||||
tool_results = []
|
||||
has_more_tool_calls = false
|
||||
|
||||
if !isempty(tool_calls)
|
||||
executed_batch = if message.stop_reason == "length"
|
||||
failToolCallsFromTruncatedMessage(tool_calls, emit)
|
||||
else
|
||||
executeToolCalls(
|
||||
current_context,
|
||||
message,
|
||||
config,
|
||||
signal,
|
||||
emit,
|
||||
|
||||
# Stream assistant response
|
||||
message = streamAssistantResponse(
|
||||
current_context, config, signal, emit, stream_function
|
||||
)
|
||||
push!(new_messages, message)
|
||||
|
||||
# Early exit on error/abort
|
||||
if message.stop_reason in ("error", "aborted")
|
||||
emit(TurnEndEvent(message, ToolResultMessage[]))
|
||||
emit(AgentEndEvent(new_messages))
|
||||
return
|
||||
end
|
||||
|
||||
# Execute tool calls (if any)
|
||||
tool_calls = filter(c -> c isa ToolCall, message.content)
|
||||
tool_results = ToolResultMessage[]
|
||||
has_more_tool_calls = false
|
||||
if !isempty(tool_calls)
|
||||
executed_batch = if message.stop_reason == "length"
|
||||
failToolCallsFromTruncatedMessage(tool_calls, emit)
|
||||
else
|
||||
executeToolCalls(
|
||||
current_context, message, config, signal, emit
|
||||
)
|
||||
end
|
||||
append!(tool_results, executed_batch.messages)
|
||||
has_more_tool_calls = !executed_batch.terminate
|
||||
for result in tool_results
|
||||
push!(current_context.messages, result)
|
||||
push!(new_messages, result)
|
||||
end
|
||||
end
|
||||
|
||||
emit(TurnEndEvent(message, tool_results))
|
||||
|
||||
# Optional: prepare next turn (model/thinking/context changes)
|
||||
next_turn_context = PrepareNextTurnContext(
|
||||
message, tool_results, current_context, new_messages
|
||||
)
|
||||
next_turn_snapshot = prepare_next_turn(config, next_turn_context)
|
||||
if !isnothing(next_turn_snapshot)
|
||||
current_context = next_turn_snapshot.context
|
||||
# Rebuild config with updated model/thinking + preserved fields
|
||||
config = AgentLoopConfig(
|
||||
model = next_turn_snapshot.model,
|
||||
reasoning = next_turn_snapshot.thinking_level,
|
||||
convert_to_llm = config.convert_to_llm,
|
||||
transform_context = config.transform_context,
|
||||
get_api_key = config.get_api_key,
|
||||
should_stop_after_turn = config.should_stop_after_turn,
|
||||
prepare_next_turn = config.prepare_next_turn,
|
||||
get_steering_messages = config.get_steering_messages,
|
||||
get_follow_up_messages = config.get_follow_up_messages,
|
||||
tool_execution = config.tool_execution,
|
||||
before_tool_call = config.before_tool_call,
|
||||
after_tool_call = config.after_tool_call,
|
||||
max_tokens = config.max_tokens,
|
||||
temperature = config.temperature,
|
||||
reasoning = config.reasoning,
|
||||
cache_retention = config.cache_retention,
|
||||
session_id = config.session_id,
|
||||
headers = config.headers,
|
||||
metadata = config.metadata,
|
||||
transport = config.transport,
|
||||
signal = signal,
|
||||
api_key = config.api_key,
|
||||
on_payload = config.on_payload,
|
||||
on_response = config.on_response,
|
||||
max_retry_delay_ms = config.max_retry_delay_ms,
|
||||
)
|
||||
end
|
||||
append!(tool_results, executed_batch.messages)
|
||||
has_more_tool_calls = !executed_batch.terminate
|
||||
|
||||
for result in tool_results
|
||||
push!(current_context.messages, result)
|
||||
push!(new_messages, result)
|
||||
|
||||
# Check termination
|
||||
if should_stop_after_turn(config, next_turn_context)
|
||||
emit(AgentEndEvent(new_messages))
|
||||
return
|
||||
end
|
||||
|
||||
# Get next steering messages
|
||||
pending_messages = get_steering_messages(config)
|
||||
end
|
||||
|
||||
emit(TurnEndEvent(message, tool_results))
|
||||
|
||||
# Prepare next turn (optional)
|
||||
next_turn_context = PrepareNextTurnContext(
|
||||
message, tool_results, current_context, new_messages
|
||||
)
|
||||
next_turn_snapshot = prepare_next_turn(config, next_turn_context)
|
||||
|
||||
if !isnothing(next_turn_snapshot)
|
||||
current_context = next_turn_snapshot.context
|
||||
config = AgentLoopConfig(
|
||||
model = next_turn_snapshot.model,
|
||||
reasoning = next_turn_snapshot.thinking_level,
|
||||
# ... other config fields
|
||||
)
|
||||
end
|
||||
|
||||
# Check if should stop
|
||||
if should_stop_after_turn(config, next_turn_context)
|
||||
emit(AgentEndEvent(new_messages))
|
||||
return
|
||||
end
|
||||
|
||||
# Get next pending messages
|
||||
pending_messages = get_steering_messages()
|
||||
|
||||
# Check follow-up messages
|
||||
follow_up_messages = get_follow_up_messages()
|
||||
|
||||
# Check follow-up messages (processed only after all tool calls complete)
|
||||
follow_up_messages = get_follow_up_messages(config)
|
||||
if !isempty(follow_up_messages)
|
||||
pending_messages = follow_up_messages
|
||||
continue
|
||||
end
|
||||
|
||||
|
||||
break
|
||||
end
|
||||
|
||||
@@ -301,17 +410,44 @@ function streamAssistantResponse(
|
||||
)::AssistantMessage
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. Get messages from context
|
||||
2. Apply transform_context (optional)
|
||||
3. Convert to LLM messages with convert_to_llm
|
||||
4. Create Context object
|
||||
5. Resolve API key
|
||||
6. Call stream_fn with model, context, and config
|
||||
7. Stream events:
|
||||
- "start" → MessageStartEvent
|
||||
- "text_start", "text_delta", "text_end" → MessageUpdateEvent
|
||||
- "done", "error" → MessageEndEvent
|
||||
**Data Flow**:
|
||||
|
||||
```
|
||||
Input: context.messages::Vector{AgentMessage}
|
||||
↓
|
||||
[transform_context] (optional hook)
|
||||
↓
|
||||
messages::Vector{AgentMessage}
|
||||
↓
|
||||
[convert_to_llm] - Type transformation pipeline
|
||||
↓
|
||||
llm_messages::Vector{Message}
|
||||
│
|
||||
│ AgentMessage → Message mapping:
|
||||
│ • UserMessage → UserMessage (pass-through)
|
||||
│ • AssistantMessage → AssistantMessage (pass-through)
|
||||
│ • ToolResultMessage → ToolResultMessage (pass-through)
|
||||
│ • BashExecutionMessage → UserMessage (text conversion)
|
||||
│ • CompactionSummaryMessage → UserMessage (text wrapped)
|
||||
│ • BranchSummaryMessage → UserMessage (text wrapped)
|
||||
│
|
||||
▼
|
||||
Context(system_prompt, llm_messages, tools)
|
||||
↓
|
||||
stream_fn(model, context, config) - LLM API call
|
||||
↓
|
||||
Stream of AssistantMessageEvent:
|
||||
• StartEvent (partial AssistantMessage)
|
||||
• TextStartEvent/TextDeltaEvent/TextEndEvent
|
||||
• ToolCallStartEvent/ToolCallDeltaEvent/ToolCallEndEvent
|
||||
• DoneEvent (final AssistantMessage with usage, stop_reason)
|
||||
↓
|
||||
Return: AssistantMessage
|
||||
- content::Vector{MessageContent}
|
||||
- usage::Usage
|
||||
- stop_reason::String
|
||||
- Contains ToolCall[] if tool calls requested
|
||||
```
|
||||
|
||||
### executeToolCalls()
|
||||
|
||||
@@ -325,22 +461,51 @@ function executeToolCalls(
|
||||
)::ExecutedToolCallBatch
|
||||
```
|
||||
|
||||
**Logic**:
|
||||
```julia
|
||||
tool_calls = filter(c -> c isa ToolCall, assistant_message.content)
|
||||
**Data Flow**:
|
||||
|
||||
# Check if any tool requires sequential execution
|
||||
has_sequential = any(tc -> begin
|
||||
tool = findfirst(t -> t.name == tc.name, current_context.tools)
|
||||
!isnothing(tool) && tool.execution_mode == EXECUTION_SEQUENTIAL
|
||||
end, tool_calls)
|
||||
|
||||
# Determine execution mode
|
||||
if config.tool_execution == EXECUTION_SEQUENTIAL || has_sequential
|
||||
executeToolCallsSequential(...)
|
||||
else
|
||||
executeToolCallsParallel(...)
|
||||
end
|
||||
```
|
||||
Input: assistant_message::AssistantMessage
|
||||
- content::Vector{MessageContent}
|
||||
└─ Contains ToolCall[] and/or TextContent[]
|
||||
↓
|
||||
filter(c -> c isa ToolCall, assistant_message.content)
|
||||
↓
|
||||
tool_calls::Vector{ToolCall}
|
||||
- type: "tool"
|
||||
- id::String
|
||||
- name::String
|
||||
- arguments::Dict{String, Any}
|
||||
- partial_json::Union{String, Nothing}
|
||||
↓
|
||||
Check execution mode:
|
||||
• config.tool_execution (sequential/parallel)
|
||||
• Any tool.execution_mode == EXECUTION_SEQUENTIAL?
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Sequential Mode (or has_sequential_tool) │
|
||||
│ For each tool_call in tool_calls: │
|
||||
│ prepareToolCall() → PreparedToolCall │
|
||||
│ executePreparedToolCall() → ExecutedToolCallOutcome │
|
||||
│ finalizeExecutedToolCall() → FinalizedToolCallOutcome │
|
||||
│ createToolResultMessage() → ToolResultMessage │
|
||||
│ (wait for completion before next tool) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Parallel Mode │
|
||||
│ For each tool_call in tool_calls: │
|
||||
│ prepareToolCall() → (PreparedToolCall | ImmediateOutcome) │
|
||||
│ If prepared: create closure │
|
||||
│ If immediate: execute and add to finalized_calls │
|
||||
│ │
|
||||
│ For each entry in finalized_calls: │
|
||||
│ If closure: execute closure │
|
||||
│ If finalized: use as-is │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
ExecutedToolCallBatch
|
||||
- messages::Vector{ToolResultMessage}
|
||||
- terminate::Bool (true if all tools have terminate=true)
|
||||
```
|
||||
|
||||
### executeToolCallsSequential()
|
||||
@@ -400,13 +565,41 @@ function prepareToolCall(
|
||||
)::Union{PreparedToolCall, ImmediateToolCallOutcome}
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. Find tool by name
|
||||
2. If not found → ImmediateToolCallOutcome (error)
|
||||
3. before_tool_call hook (optional)
|
||||
4. prepareToolCallArguments() (optional)
|
||||
5. validateToolArguments()
|
||||
6. Return PreparedToolCall
|
||||
**Data Flow**:
|
||||
|
||||
```
|
||||
Input: tool_call::ToolCall
|
||||
- id::String
|
||||
- name::String
|
||||
- arguments::Dict{String, Any}
|
||||
↓
|
||||
findfirst(t -> t.name == tool_call.name, current_context.tools)
|
||||
↓
|
||||
If tool is nothing:
|
||||
→ ImmediateToolCallOutcome("immediate", error_result, is_error=true)
|
||||
↓
|
||||
If tool exists:
|
||||
[before_tool_call hook] (optional)
|
||||
Input: BeforeToolCallContext(assistant_message, tool_call, args, context)
|
||||
Output: BeforeToolCallResult (block, reason) or nothing
|
||||
If block=true → ImmediateToolCallOutcome(error)
|
||||
↓
|
||||
prepareToolCallArguments(tool, tool_call)
|
||||
Input: tool_call.arguments::Dict{String, Any}
|
||||
Output: prepared_arguments::Any
|
||||
(Optional: transform arguments before validation)
|
||||
↓
|
||||
validateToolArguments(tool, prepared_tool_call)
|
||||
Input: prepared_tool_call.arguments
|
||||
Output: validated_args::Any
|
||||
(Optional: JSON schema validation)
|
||||
↓
|
||||
Return: PreparedToolCall("prepared", tool_call, tool, validated_args)
|
||||
- kind: "prepared"
|
||||
- tool_call: ToolCall (original)
|
||||
- tool: AgentTool
|
||||
- args: validated arguments
|
||||
```
|
||||
|
||||
### executePreparedToolCall()
|
||||
|
||||
@@ -418,11 +611,35 @@ function executePreparedToolCall(
|
||||
)::ExecutedToolCallOutcome
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. Call tool.execute(id, args, signal, on_update)
|
||||
2. Collect update events (if any)
|
||||
3. Wait for all update events
|
||||
4. Return ExecutedToolCallOutcome(result)
|
||||
**Data Flow**:
|
||||
|
||||
```
|
||||
Input: prepared::PreparedToolCall
|
||||
- tool_call::ToolCall
|
||||
- tool::AgentTool
|
||||
- args::Any (validated)
|
||||
↓
|
||||
tool.execute(tool_call.id, args, signal, on_update)
|
||||
Input: tool_call_id::String
|
||||
args::Any
|
||||
signal::Union{Any, Nothing}
|
||||
on_update::Function (partial_result → void)
|
||||
Output: AgentToolResultMutable (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()
|
||||
|
||||
@@ -437,9 +654,44 @@ function finalizeExecutedToolCall(
|
||||
)::FinalizedToolCallOutcome
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. after_tool_call hook (optional)
|
||||
2. Return FinalizedToolCallOutcome
|
||||
**Data Flow**:
|
||||
|
||||
```
|
||||
Input: executed::ExecutedToolCallOutcome
|
||||
- result::AgentToolResultMutable
|
||||
- is_error::Bool
|
||||
↓
|
||||
[after_tool_call hook] (optional)
|
||||
Input: AfterToolCallContext(
|
||||
assistant_message,
|
||||
tool_call,
|
||||
args,
|
||||
result,
|
||||
is_error,
|
||||
context
|
||||
)
|
||||
Output: AfterToolCallResult (optional patches)
|
||||
- content::Union{Vector{MessageContent}, Nothing}
|
||||
- details::Union{Any, Nothing}
|
||||
- is_error::Union{Bool, Nothing}
|
||||
- usage::Union{Usage, Nothing}
|
||||
- terminate::Union{Bool, Nothing}
|
||||
↓
|
||||
Apply patches to result (if 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()
|
||||
|
||||
@@ -449,19 +701,39 @@ function createToolResultMessage(
|
||||
)::ToolResultMessage
|
||||
```
|
||||
|
||||
**Creates**:
|
||||
```julia
|
||||
ToolResultMessage(
|
||||
"toolResult",
|
||||
finalized.tool_call.id,
|
||||
finalized.tool_call.name,
|
||||
finalized.result.content,
|
||||
finalized.result.details,
|
||||
finalized.result.usage,
|
||||
finalized.result.added_tool_names,
|
||||
finalized.is_error,
|
||||
timestamp,
|
||||
)
|
||||
**Data Flow**:
|
||||
|
||||
```
|
||||
Input: finalized::FinalizedToolCallOutcome
|
||||
- tool_call::ToolCall
|
||||
- result::AgentToolResultMutable
|
||||
- content::Vector{MessageContent}
|
||||
- details::Any
|
||||
- usage::Union{Usage, Nothing}
|
||||
- added_tool_names::Union{Vector{String}, Nothing}
|
||||
- is_error::Bool
|
||||
↓
|
||||
Build ToolResultMessage:
|
||||
• role: "toolResult"
|
||||
• tool_call_id: tool_call.id
|
||||
• tool_name: tool_call.name
|
||||
• content: result.content
|
||||
• details: result.details
|
||||
• usage: result.usage
|
||||
• added_tool_names: result.added_tool_names
|
||||
• is_error: is_error
|
||||
• timestamp: Int64(Dates.now(Dates.UTC).datetime)
|
||||
↓
|
||||
Output: ToolResultMessage
|
||||
- role::String ("toolResult")
|
||||
- tool_call_id::String
|
||||
- tool_name::String
|
||||
- content::Vector{MessageContent}
|
||||
- details::Any
|
||||
- usage::Union{Usage, Nothing}
|
||||
- added_tool_names::Union{Vector{String}, Nothing}
|
||||
- is_error::Bool
|
||||
- timestamp::Timestamp (Int64)
|
||||
```
|
||||
|
||||
## Execution Modes
|
||||
@@ -470,45 +742,39 @@ ToolResultMessage(
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Sequential Execution Flow │
|
||||
│ Sequential Execution Flow (Strict Order) │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────┐
|
||||
│ TC1 │ ──► prepareToolCall()
|
||||
└──────┘ │
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ execute() │ ──► Wait for completion
|
||||
└──────────────┘ │
|
||||
│ ▼
|
||||
├───────────── createToolResultMessage()
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────┐
|
||||
│ TC2 │ ──► │ │ Result1 │
|
||||
└──────┘ └──────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ execute() │
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ TC3 │ ──► │
|
||||
└──────┘ │
|
||||
│ ▼
|
||||
├───── createToolResultMessage()
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────┐
|
||||
│ execute() │ │ │ Result2 │
|
||||
└──────────────┘ └──────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ Result3 │
|
||||
└──────────┘
|
||||
TC1 TC2 TC3
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ prepareToolCall()│───▶│ prepareToolCall()│───▶│ prepareToolCall()│
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ execute() │ │ execute() │ │ execute() │
|
||||
│ (blocking) │ │ (blocking) │ │ (blocking) │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ finalize() │ │ finalize() │ │ finalize() │
|
||||
│ + emit events │ │ + emit events │ │ + emit events │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Result1 Result2 Result3
|
||||
│ │ │
|
||||
└───────────────────────┴───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ ExecutedToolCallBatch │
|
||||
│ (Result1, Result2, │
|
||||
│ Result3, terminate) │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
@@ -650,50 +916,85 @@ AgentStartEvent
|
||||
### 1. Message Transformation Pipeline
|
||||
|
||||
```
|
||||
AgentMessage[] (internal)
|
||||
Vector{AgentMessage} (internal conversation history)
|
||||
│
|
||||
│ transform_context()
|
||||
▼
|
||||
AgentMessage[] (transformed)
|
||||
├─ transform_context() (optional hook)
|
||||
│ Input: Vector{AgentMessage}
|
||||
│ Output: Vector{AgentMessage} (transformed)
|
||||
│
|
||||
│ convert_to_llm()
|
||||
▼
|
||||
Message[] (LLM API)
|
||||
└─ convert_to_llm()
|
||||
│
|
||||
│ Type mapping (single dispatch):
|
||||
│ • UserMessage → UserMessage (pass-through)
|
||||
│ • AssistantMessage → AssistantMessage (pass-through)
|
||||
│ • ToolResultMessage → ToolResultMessage (pass-through)
|
||||
│ • BashExecutionMessage → UserMessage (text conversion)
|
||||
│ • CompactionSummaryMessage → UserMessage (text wrapped)
|
||||
│ • BranchSummaryMessage → UserMessage (text wrapped)
|
||||
│
|
||||
▼
|
||||
Vector{Message} (for LLM API)
|
||||
```
|
||||
|
||||
### 2. Tool Call Lifecycle
|
||||
### 2. Tool Call Lifecycle (with Data Transformations)
|
||||
|
||||
```
|
||||
ToolCall (in assistant message)
|
||||
ToolCall (in AssistantMessage.content)
|
||||
│
|
||||
├─ before_tool_call (hook)
|
||||
├─ before_tool_call hook (optional)
|
||||
│ Input: BeforeToolCallContext(
|
||||
│ assistant_message::AssistantMessage,
|
||||
│ tool_call::ToolCall,
|
||||
│ args::Dict{String, Any},
|
||||
│ context::AgentContext
|
||||
│ )
|
||||
│ Output: BeforeToolCallResult (block, reason) or nothing
|
||||
│
|
||||
├─ prepareToolCall()
|
||||
│ ├─ validate arguments
|
||||
│ └─ prepare arguments (optional)
|
||||
│ Input: tool_call::ToolCall
|
||||
│ Output: Union{PreparedToolCall, ImmediateToolCallOutcome}
|
||||
│ • PreparedToolCall (kind, tool_call, tool, args)
|
||||
│ • ImmediateToolCallOutcome (immediate, result, is_error)
|
||||
│
|
||||
├─ execute()
|
||||
│ ├─ Immediate: return result
|
||||
│ └─ Prepared: async execution
|
||||
├─ executePreparedToolCall() (if prepared)
|
||||
│ Input: PreparedToolCall
|
||||
│ Output: ExecutedToolCallOutcome
|
||||
│ tool.execute() returns AgentToolResultMutable
|
||||
│ • content::Vector{MessageContent}
|
||||
│ • details::Any
|
||||
│ • usage::Union{Usage, Nothing}
|
||||
│ • terminate::Union{Bool, Nothing}
|
||||
│
|
||||
├─ after_tool_call (hook)
|
||||
├─ finalizeExecutedToolCall()
|
||||
│ Input: ExecutedToolCallOutcome
|
||||
│ Output: FinalizedToolCallOutcome
|
||||
│ • tool_call::ToolCall
|
||||
│ • result::AgentToolResultMutable
|
||||
│ • is_error::Bool
|
||||
│
|
||||
└─ createToolResultMessage()
|
||||
Input: FinalizedToolCallOutcome
|
||||
Output: ToolResultMessage
|
||||
• role: "toolResult"
|
||||
• tool_call_id, tool_name
|
||||
• content::Vector{MessageContent}
|
||||
• details, usage, added_tool_names
|
||||
• is_error, timestamp
|
||||
```
|
||||
|
||||
### 3. Turn Termination
|
||||
|
||||
```julia
|
||||
# Turn ends when:
|
||||
# 1. No more pending messages
|
||||
# 2. No more tool calls to execute
|
||||
# 3. should_stop_after_turn() returns true
|
||||
# The outer while-true loop exits when:
|
||||
# 1. No pending messages AND no tool results to reprocess (inner loop ends)
|
||||
# 2. No follow-up messages to queue
|
||||
# 3. should_stop_after_turn() returns true (checked after each tool-call batch)
|
||||
|
||||
# Reasons to stop:
|
||||
# - Max turns reached
|
||||
# - Tool returned terminate=true
|
||||
# - Error or abort
|
||||
# - Steering/follow-up queues empty
|
||||
# Termination conditions:
|
||||
# - message.stop_reason in ("error", "aborted") → immediate return
|
||||
# - should_stop_after_turn() hook returns true → return AgentEndEvent
|
||||
# - tool result batch has terminate=true → has_more_tool_calls = false, exit inner loop
|
||||
# - No pending messages, no follow-up messages → break outer loop
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
@@ -713,6 +1014,12 @@ using AgentCore
|
||||
config = AgentLoopConfig(
|
||||
model = my_model,
|
||||
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,
|
||||
before_tool_call = myBeforeToolCallHook,
|
||||
after_tool_call = myAfterToolCallHook,
|
||||
@@ -722,6 +1029,14 @@ config = AgentLoopConfig(
|
||||
get_api_key = myGetApiKey,
|
||||
get_steering_messages = myGetSteeringMessages,
|
||||
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
|
||||
|
||||
+325
-18
@@ -98,6 +98,52 @@
|
||||
|
||||
## Message Types
|
||||
|
||||
### Type Hierarchy
|
||||
|
||||
```
|
||||
Message (for LLM API)
|
||||
├── UserMessage (role: "user")
|
||||
│ ├── content::Vector{MessageContent}
|
||||
│ │ ├── TextContent (text::String)
|
||||
│ │ └── ImageContent (data::String, mime_type::String)
|
||||
│ └── timestamp::Timestamp (Int64)
|
||||
├── AssistantMessage (role: "assistant")
|
||||
│ ├── content::Vector{MessageContent}
|
||||
│ │ ├── TextContent
|
||||
│ │ └── ToolCall (type, id, name, arguments::Dict{String, Any})
|
||||
│ ├── api::String
|
||||
│ ├── provider::String
|
||||
│ ├── model::String
|
||||
│ ├── usage::Usage
|
||||
│ │ ├── input, output, cache_read, cache_write, total_tokens::Int64
|
||||
│ │ └── cost::UsageCost (input, output, cache_read, cache_write, total::Float64)
|
||||
│ ├── stop_reason::String
|
||||
│ ├── error_message::Union{String, Nothing}
|
||||
│ └── timestamp::Timestamp
|
||||
└── ToolResultMessage (role: "toolResult")
|
||||
├── tool_call_id::String
|
||||
├── tool_name::String
|
||||
├── content::Vector{MessageContent}
|
||||
├── details::Any
|
||||
├── usage::Union{Usage, Nothing}
|
||||
├── added_tool_names::Union{Vector{String}, Nothing}
|
||||
├── is_error::Bool
|
||||
└── timestamp::Timestamp
|
||||
|
||||
AgentMessage (internal, extends Message)
|
||||
├── UserMessage (same as above)
|
||||
├── AssistantMessage (same as above)
|
||||
├── ToolResultMessage (same as above)
|
||||
├── BashExecutionMessage (custom, converted to UserMessage)
|
||||
│ ├── role, command, output, exit_code
|
||||
│ ├── cancelled, truncated, full_output_path
|
||||
│ └── exclude_from_context::Bool
|
||||
├── CompactionSummaryMessage (custom, converted to UserMessage)
|
||||
│ ├── summary, tokens_before, timestamp
|
||||
└── BranchSummaryMessage (custom, converted to UserMessage)
|
||||
├── summary, from_id, timestamp
|
||||
```
|
||||
|
||||
### UserMessage
|
||||
|
||||
```julia
|
||||
@@ -108,6 +154,16 @@ struct UserMessage <: Message
|
||||
end
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
# Simple text message
|
||||
UserMessage(
|
||||
"user",
|
||||
[TextContent("Hello, how are you?")],
|
||||
Int64(Dates.now(Dates.UTC).datetime)
|
||||
)
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
# Simple text message
|
||||
@@ -192,6 +248,9 @@ struct ToolResultMessage <: Message
|
||||
is_error::Bool # True if tool execution failed
|
||||
timestamp::Timestamp
|
||||
end
|
||||
|
||||
# Note: AgentToolResult{T} (types.jl) - generic result type with type param T
|
||||
# AgentToolResultMutable (agent_loop.jl) - mutable variant used internally
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
@@ -232,6 +291,8 @@ end
|
||||
- `prepare_arguments`: Optional preprocessing
|
||||
- `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
|
||||
|
||||
```julia
|
||||
@@ -241,12 +302,12 @@ execute::Function(
|
||||
signal::Union{Any, Nothing}, # Abort signal
|
||||
on_update::Function, # Callback for streaming updates
|
||||
context::Any, # Tool context
|
||||
)::AgentToolResult
|
||||
)::AgentToolResult{T}
|
||||
```
|
||||
|
||||
**Returns**:
|
||||
**Returns** (`AgentToolResult{T}` from `types.jl`):
|
||||
```julia
|
||||
AgentToolResult(
|
||||
AgentToolResult{T}(
|
||||
content::Vector{MessageContent}, # Result content
|
||||
details::T, # Tool-specific details
|
||||
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
|
||||
|
||||
```julia
|
||||
@@ -476,6 +541,32 @@ mutable struct BranchSummaryMessage
|
||||
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
|
||||
|
||||
```julia
|
||||
@@ -496,9 +587,9 @@ end
|
||||
|
||||
**Note**: AgentState is mutable and used internally by Agent
|
||||
|
||||
## Key Conversion Functions
|
||||
## Message Transformation Pipeline
|
||||
|
||||
### convertToLlm()
|
||||
### convertToLlm() - AgentMessage[] → Message[]
|
||||
|
||||
```julia
|
||||
function convertToLlm(messages::Vector{AgentMessage})::Vector{Message}
|
||||
@@ -515,26 +606,57 @@ function convertToLlm(messages::Vector{AgentMessage})::Vector{Message}
|
||||
end
|
||||
```
|
||||
|
||||
**Purpose**: Transform AgentMessage[] to Message[] for LLM API
|
||||
**Data Flow**:
|
||||
|
||||
```
|
||||
Vector{AgentMessage} (internal conversation history)
|
||||
│
|
||||
│ Type dispatch on convertToLlmMessage():
|
||||
│
|
||||
│ • UserMessage → UserMessage (pass-through)
|
||||
│ • AssistantMessage → AssistantMessage (pass-through)
|
||||
│ • ToolResultMessage → ToolResultMessage (pass-through)
|
||||
│
|
||||
│ Custom messages converted to UserMessage:
|
||||
│ • BashExecutionMessage → UserMessage
|
||||
│ (via bashExecutionToText() for display)
|
||||
│ • CompactionSummaryMessage → UserMessage
|
||||
│ (wrapped with COMPACTION_SUMMARY_PREFIX/SUFFIX)
|
||||
│ • BranchSummaryMessage → UserMessage
|
||||
│ (wrapped with BRANCH_SUMMARY_PREFIX/SUFFIX)
|
||||
│ • 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**:
|
||||
```julia
|
||||
# Input: AgentMessage[]
|
||||
# Input: Vector{AgentMessage}
|
||||
[
|
||||
UserMessage(...),
|
||||
AssistantMessage(...),
|
||||
ToolResultMessage(...),
|
||||
BashExecutionMessage(...), # Will be converted to UserMessage
|
||||
CompactionSummaryMessage(...), # Will be converted to UserMessage
|
||||
UserMessage("user", [TextContent("Hello")], 1234567890),
|
||||
AssistantMessage("assistant", [
|
||||
TextContent("Hi there!"),
|
||||
ToolCall("bash", "call_123", "bash", Dict("command" => "ls"), nothing)
|
||||
], "openai", "openai", "gpt-4", Usage(...), "done", nothing, 1234567891),
|
||||
BashExecutionMessage("custom", "ls -la", "file1.md\nfile2.md\n", 0, false, false, nothing, 1234567892, false),
|
||||
CompactionSummaryMessage("compactionSummary", "Previous conversation compacted", 1000, 1234567893),
|
||||
CustomMessage("custom", "someCustomType", "Some custom content", true, nothing, 1234567894),
|
||||
]
|
||||
|
||||
# Output: Message[]
|
||||
# Output: Vector{Message}
|
||||
[
|
||||
UserMessage(...),
|
||||
AssistantMessage(...),
|
||||
ToolResultMessage(...),
|
||||
UserMessage(...), # Converted from BashExecutionMessage
|
||||
UserMessage(...), # Converted from CompactionSummaryMessage
|
||||
UserMessage("user", [TextContent("Hello")], 1234567890),
|
||||
AssistantMessage("assistant", [
|
||||
TextContent("Hi there!"),
|
||||
ToolCall(...)
|
||||
], "openai", "openai", "gpt-4", Usage(...), "done", nothing, 1234567891),
|
||||
UserMessage("user", [TextContent("Ran `ls -la`\n```\nfile1.md\nfile2.md\n```\n")], 1234567892),
|
||||
UserMessage("user", [TextContent("<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)
|
||||
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)
|
||||
text = BRANCH_SUMMARY_PREFIX * m.summary * BRANCH_SUMMARY_SUFFIX
|
||||
return UserMessage("user", [TextContent(text)], m.timestamp)
|
||||
@@ -571,6 +702,182 @@ function convertToLlmMessage(m::ToolResultMessage)
|
||||
end
|
||||
```
|
||||
|
||||
## Complete Data Flow Examples
|
||||
|
||||
### Example 1: User Prompt → Assistant Response
|
||||
|
||||
```
|
||||
User Input:
|
||||
"Hello, what's in the current directory?"
|
||||
|
||||
↓
|
||||
|
||||
prompt(agent, "Hello, what's in the current directory?")
|
||||
│
|
||||
└─► normalizePromptInput(String)
|
||||
Input: "Hello, what's in the current directory?"
|
||||
Output: [UserMessage("user", [TextContent("Hello, what's in the current directory?")], timestamp)]
|
||||
|
||||
↓
|
||||
|
||||
AgentLoop execution:
|
||||
│
|
||||
├─► transform_context() (optional)
|
||||
│ Input: [UserMessage(...)]
|
||||
│ Output: [UserMessage(...)]
|
||||
│
|
||||
├─► convert_to_llm()
|
||||
│ Input: [UserMessage(...)]
|
||||
│ Output: [UserMessage(...)]
|
||||
│
|
||||
├─► stream_fn() - LLM API
|
||||
│ Input: model, Context(...), config
|
||||
│ Output: AssistantMessage with ToolCall[]
|
||||
│ role: "assistant"
|
||||
│ content: [
|
||||
│ TextContent("I'll check the directory for you."),
|
||||
│ ToolCall("tool", "tc_123", "bash", Dict("command" => "ls -la"), nothing)
|
||||
│ ]
|
||||
│ usage: Usage(input=100, output=20, ...)
|
||||
│ stop_reason: "done"
|
||||
│
|
||||
├─► executeToolCalls()
|
||||
│ Input: AssistantMessage with ToolCall[]
|
||||
│ Output: ToolResultMessage[]
|
||||
│ role: "toolResult"
|
||||
│ tool_call_id: "tc_123"
|
||||
│ tool_name: "bash"
|
||||
│ content: [TextContent("file1.md\nfile2.md\n")]
|
||||
│ is_error: false
|
||||
│
|
||||
└─► Append to context.messages
|
||||
|
||||
↓
|
||||
|
||||
Final Conversation History:
|
||||
[
|
||||
UserMessage("user", [TextContent("Hello, what's in the current directory?")], ...),
|
||||
AssistantMessage("assistant", [
|
||||
TextContent("I'll check the directory for you."),
|
||||
ToolCall("tool", "tc_123", "bash", Dict("command" => "ls -la"), nothing)
|
||||
], "openai", "openai", "gpt-4", Usage(...), "done", nothing, ...),
|
||||
ToolResultMessage("toolResult", "tc_123", "bash", [TextContent("file1.md\nfile2.md\n")], ..., false, ...),
|
||||
]
|
||||
```
|
||||
|
||||
### Example 2: Tool Call Execution → Tool Result
|
||||
|
||||
```
|
||||
ToolCall from AssistantMessage
|
||||
│
|
||||
├─ type: "tool"
|
||||
├─ id: "tc_123"
|
||||
├─ name: "bash"
|
||||
├─ arguments: Dict("command" => "ls -la")
|
||||
└─ partial_json: nothing
|
||||
↓
|
||||
prepareToolCall(tool_call)
|
||||
↓
|
||||
Finds tool by name "bash"
|
||||
↓
|
||||
before_tool_call hook (optional)
|
||||
Input: BeforeToolCallContext(...)
|
||||
Output: BeforeToolCallResult(block=false) or nothing
|
||||
↓
|
||||
validateToolArguments(tool_call)
|
||||
Input: Dict("command" => "ls -la")
|
||||
Output: Dict("command" => "ls -la")
|
||||
↓
|
||||
Return: PreparedToolCall("prepared", tool_call, bash_tool, validated_args)
|
||||
↓
|
||||
executePreparedToolCall(prepared)
|
||||
↓
|
||||
tool.execute("tc_123", Dict("command" => "ls -la"), signal, on_update)
|
||||
↓
|
||||
Bash tool executes "ls -la" command
|
||||
Returns: AgentToolResultMutable(
|
||||
content: [TextContent("file1.md\nfile2.md\n")],
|
||||
details: BashToolDetails(...),
|
||||
usage: nothing,
|
||||
added_tool_names: nothing,
|
||||
terminate: nothing
|
||||
)
|
||||
↓
|
||||
finalizeExecutedToolCall(executed)
|
||||
↓
|
||||
after_tool_call hook (optional)
|
||||
Input: AfterToolCallContext(...)
|
||||
Output: AfterToolCallResult(...) or nothing
|
||||
↓
|
||||
Return: FinalizedToolCallOutcome(
|
||||
tool_call: ToolCall(...),
|
||||
result: AgentToolResultMutable(...),
|
||||
is_error: false
|
||||
)
|
||||
↓
|
||||
createToolResultMessage(finalized)
|
||||
↓
|
||||
Return: ToolResultMessage(
|
||||
role: "toolResult",
|
||||
tool_call_id: "tc_123",
|
||||
tool_name: "bash",
|
||||
content: [TextContent("file1.md\nfile2.md\n")],
|
||||
details: BashToolDetails(...),
|
||||
usage: nothing,
|
||||
added_tool_names: nothing,
|
||||
is_error: false,
|
||||
timestamp: Int64(...)
|
||||
)
|
||||
```
|
||||
|
||||
### Example 3: Custom Message Conversion
|
||||
|
||||
```
|
||||
BashExecutionMessage (custom, for logging)
|
||||
│
|
||||
role: "custom"
|
||||
command: "ls -la"
|
||||
output: "file1.md\nfile2.md\n"
|
||||
exit_code: 0
|
||||
cancelled: false
|
||||
truncated: false
|
||||
full_output_path: nothing
|
||||
timestamp: 1234567890
|
||||
exclude_from_context: false
|
||||
↓
|
||||
convertToLlmMessage(BashExecutionMessage)
|
||||
↓
|
||||
bashExecutionToText(msg)
|
||||
Output: "Ran `ls -la`\n```\nfile1.md\nfile2.md\n```\n"
|
||||
↓
|
||||
Return: UserMessage(
|
||||
"user",
|
||||
[TextContent("Ran `ls -la`\n```\nfile1.md\nfile2.md\n```\n")],
|
||||
1234567890
|
||||
)
|
||||
↓
|
||||
(Excluded if exclude_from_context = true)
|
||||
|
||||
────────────────────────────────────────────────────────────────────
|
||||
|
||||
CompactionSummaryMessage (custom, for history compression)
|
||||
│
|
||||
role: "compactionSummary"
|
||||
summary: "Previous 100 turns about Python programming"
|
||||
tokens_before: 15000
|
||||
timestamp: 1234567890
|
||||
↓
|
||||
convertToLlmMessage(CompactionSummaryMessage)
|
||||
↓
|
||||
Text = COMPACTION_SUMMARY_PREFIX + summary + COMPACTION_SUMMARY_SUFFIX
|
||||
Result: "<summary>\nPrevious 100 turns about Python programming\n</summary>"
|
||||
↓
|
||||
Return: UserMessage(
|
||||
"user",
|
||||
[TextContent("<summary>...\nPrevious 100 turns...\n</summary>")],
|
||||
1234567890
|
||||
)
|
||||
|
||||
## Summary
|
||||
|
||||
The type system in AgentCore.jl provides:
|
||||
|
||||
+334
-179
@@ -1,6 +1,6 @@
|
||||
# AgentCore.jl - Session Management Deep Dive
|
||||
|
||||
## Session Architecture
|
||||
## Session Architecture with Data Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
@@ -20,34 +20,75 @@
|
||||
│ ▼ ▼ ▼ ▼ ▼ │
|
||||
│ Message Message Compaction Message BranchSummary │
|
||||
│ │
|
||||
│ Data Flow: │
|
||||
│ AgentMessage[] (AgentState.messages) │
|
||||
│ │ │
|
||||
│ └─► appendMessage() → MessageEntry │
|
||||
│ └─► storage.appendEntry() → JSONL file │
|
||||
│ │
|
||||
│ To navigate to E2 (fork point): │
|
||||
│ Session.moveTo(E2) │
|
||||
│ session.moveTo(E2) │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │────▶│ E5' │ (new branch) │
|
||||
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
|
||||
│ │ │ │
|
||||
│ │ ▼ 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
|
||||
|
||||
All entry types extend `abstract type SessionTreeEntry end` and embed a
|
||||
`base::SessionTreeEntryBase` struct containing `type`, `id`, `parent_id`, and `timestamp`.
|
||||
|
||||
```julia
|
||||
abstract type SessionTreeEntry end
|
||||
|
||||
struct SessionTreeEntryBase
|
||||
type::String
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
end
|
||||
```
|
||||
|
||||
### 1. MessageEntry
|
||||
|
||||
```julia
|
||||
struct MessageEntry <: SessionTreeEntry
|
||||
type::String # "message"
|
||||
id::String # Unique entry ID
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String # ISO 8601 timestamp
|
||||
message::AgentMessage # The actual message
|
||||
base::SessionTreeEntryBase
|
||||
message::AgentMessage
|
||||
end
|
||||
```
|
||||
|
||||
@@ -57,11 +98,8 @@ end
|
||||
|
||||
```julia
|
||||
struct ThinkingLevelChangeEntry <: SessionTreeEntry
|
||||
type::String # "thinking_level_change"
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
thinking_level::String # "off", "minimal", "low", "medium", etc.
|
||||
base::SessionTreeEntryBase
|
||||
thinking_level::String
|
||||
end
|
||||
```
|
||||
|
||||
@@ -71,12 +109,9 @@ end
|
||||
|
||||
```julia
|
||||
struct ModelChangeEntry <: SessionTreeEntry
|
||||
type::String # "model_change"
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
provider::String # "openai", "anthropic", etc.
|
||||
model_id::String # Model identifier
|
||||
base::SessionTreeEntryBase
|
||||
provider::String
|
||||
model_id::String
|
||||
end
|
||||
```
|
||||
|
||||
@@ -86,10 +121,7 @@ end
|
||||
|
||||
```julia
|
||||
struct ActiveToolsChangeEntry <: SessionTreeEntry
|
||||
type::String # "active_tools_change"
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
base::SessionTreeEntryBase
|
||||
active_tool_names::Vector{String}
|
||||
end
|
||||
```
|
||||
@@ -99,18 +131,15 @@ end
|
||||
### 5. CompactionEntry
|
||||
|
||||
```julia
|
||||
struct CompactionEntry <: SessionTreeEntry
|
||||
type::String # "compaction"
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
summary::String # Summary of compacted history
|
||||
struct CompactionEntry{T} <: SessionTreeEntry
|
||||
base::SessionTreeEntryBase
|
||||
summary::String
|
||||
first_kept_entry_id::Union{String, Nothing}
|
||||
tokens_before::Int64 # Context size before compaction
|
||||
tokens_before::Int64
|
||||
retained_tail::Union{Vector{AgentMessage}, Nothing}
|
||||
details::Union{Any, Nothing}
|
||||
details::Union{T, Nothing}
|
||||
usage::Union{Usage, Nothing}
|
||||
from_hook::Bool # Whether triggered by hook
|
||||
from_hook::Bool
|
||||
end
|
||||
```
|
||||
|
||||
@@ -125,14 +154,11 @@ end
|
||||
### 6. BranchSummaryEntry
|
||||
|
||||
```julia
|
||||
struct BranchSummaryEntry <: SessionTreeEntry
|
||||
type::String # "branch_summary"
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
from_id::String # Branch point entry ID
|
||||
summary::String # Summary of branch history
|
||||
details::Union{Any, Nothing}
|
||||
struct BranchSummaryEntry{T} <: SessionTreeEntry
|
||||
base::SessionTreeEntryBase
|
||||
from_id::String
|
||||
summary::String
|
||||
details::Union{T, Nothing}
|
||||
usage::Union{Usage, Nothing}
|
||||
from_hook::Bool
|
||||
end
|
||||
@@ -143,13 +169,10 @@ end
|
||||
### 7. CustomEntry
|
||||
|
||||
```julia
|
||||
struct CustomEntry <: SessionTreeEntry
|
||||
type::String # Custom type
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
struct CustomEntry{T} <: SessionTreeEntry
|
||||
base::SessionTreeEntryBase
|
||||
custom_type::String
|
||||
data::Union{Any, Nothing}
|
||||
data::Union{T, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
@@ -158,14 +181,11 @@ end
|
||||
### 8. CustomMessageEntry
|
||||
|
||||
```julia
|
||||
struct CustomMessageEntry <: SessionTreeEntry
|
||||
type::String
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
struct CustomMessageEntry{T} <: SessionTreeEntry
|
||||
base::SessionTreeEntryBase
|
||||
custom_type::String
|
||||
content::String
|
||||
details::Union{Any, Nothing}
|
||||
details::Union{T, Nothing}
|
||||
display::Bool
|
||||
end
|
||||
```
|
||||
@@ -176,11 +196,8 @@ end
|
||||
|
||||
```julia
|
||||
struct LabelEntry <: SessionTreeEntry
|
||||
type::String
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
target_id::String # Entry being labeled
|
||||
base::SessionTreeEntryBase
|
||||
target_id::String
|
||||
label::Union{String, Nothing}
|
||||
end
|
||||
```
|
||||
@@ -191,10 +208,7 @@ end
|
||||
|
||||
```julia
|
||||
struct SessionInfoEntry <: SessionTreeEntry
|
||||
type::String
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
base::SessionTreeEntryBase
|
||||
name::Union{String, Nothing}
|
||||
end
|
||||
```
|
||||
@@ -205,10 +219,7 @@ end
|
||||
|
||||
```julia
|
||||
struct LeafEntry <: SessionTreeEntry
|
||||
type::String
|
||||
id::String
|
||||
parent_id::Union{String, Nothing}
|
||||
timestamp::String
|
||||
base::SessionTreeEntryBase
|
||||
target_id::Union{String, Nothing}
|
||||
end
|
||||
```
|
||||
@@ -221,86 +232,95 @@ end
|
||||
abstract type SessionStorage{T<:SessionMetadata} end
|
||||
```
|
||||
|
||||
### Storage Methods
|
||||
### Storage Methods (actual implementation signatures)
|
||||
|
||||
```julia
|
||||
# Metadata
|
||||
getMetadata(storage::SessionStorage)::Promise{T}
|
||||
getMetadata(storage::SessionStorage)::T
|
||||
|
||||
# Leaf management
|
||||
getLeafId(storage::SessionStorage)::Promise{Union{String, Nothing}}
|
||||
setLeafId(storage::SessionStorage, leaf_id::String)::Promise{Nothing}
|
||||
getLeafId(storage::SessionStorage)::Union{String, Nothing}
|
||||
setLeafId(storage::SessionStorage, leaf_id::Union{String, Nothing})::Nothing
|
||||
|
||||
# Entry management
|
||||
createEntryId(storage::SessionStorage)::Promise{String}
|
||||
appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Promise{Nothing}
|
||||
getEntry(storage::SessionStorage, id::String)::Promise{Union{SessionTreeEntry, Nothing}}
|
||||
createEntryId(storage::SessionStorage)::String
|
||||
appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Nothing
|
||||
getEntry(storage::SessionStorage, id::String)::Union{SessionTreeEntry, Nothing}
|
||||
|
||||
# Query
|
||||
findEntries(storage::SessionStorage, type::String)::Promise{Vector{SessionTreeEntry}}
|
||||
getLabel(storage::SessionStorage, id::String)::Promise{Union{String, Nothing}}
|
||||
getSessionName(storage::SessionStorage)::Promise{Union{String, Nothing}}
|
||||
findEntries(storage::SessionStorage, type::String)::Vector{SessionTreeEntry}
|
||||
getLabel(storage::SessionStorage, id::String)::Union{String, Nothing}
|
||||
getSessionName(storage::SessionStorage)::Union{String, Nothing}
|
||||
|
||||
# Branch navigation
|
||||
getPathToRootOrCompaction(
|
||||
storage::SessionStorage,
|
||||
leaf_id::String,
|
||||
)::Promise{Vector{SessionTreeEntry}}
|
||||
|
||||
getEntries(storage::SessionStorage, options::Dict{String, Any})::Promise{Vector{SessionTreeEntry}}
|
||||
getPathToRootOrCompaction(storage::SessionStorage, leaf_id::Union{String, Nothing})::Vector{SessionTreeEntry}
|
||||
getEntries(storage::SessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry}
|
||||
|
||||
# Stats
|
||||
getSessionStats(storage::SessionStorage)::Promise{SessionStats}
|
||||
getSessionStats(storage::SessionStorage)::SessionStats
|
||||
```
|
||||
|
||||
## 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 │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
File: session.jsonl
|
||||
File: session.jsonl (conceptual - not yet implemented)
|
||||
|
||||
Entry 1 (Metadata):
|
||||
{"type":"session","id":"meta_1","created_at":"2024-01-01T00:00:00Z","cwd":"/path","path":"/path/session.jsonl"}
|
||||
Entry 1 (Metadata via SessionHeader):
|
||||
{"type":"session","version":3,"id":"meta_1","timestamp":"...","cwd":"/path","parent_session":null,"metadata":{}}
|
||||
|
||||
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):
|
||||
{"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):
|
||||
{"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):
|
||||
{"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):
|
||||
{"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):
|
||||
{"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):
|
||||
{"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:
|
||||
- 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)
|
||||
- Leaf entry points to current position in tree
|
||||
- To fork, create new branch from any entry
|
||||
- In-memory mode uses Vector + Dict by_id for fast access
|
||||
```
|
||||
|
||||
## InMemorySessionStorage
|
||||
|
||||
```julia
|
||||
mutable struct InMemorySessionStorage
|
||||
metadata::SessionMetadata
|
||||
mutable struct InMemorySessionStorage{T<:SessionMetadata} <: SessionStorage{T}
|
||||
metadata::T
|
||||
entries::Vector{SessionTreeEntry}
|
||||
by_id::Dict{String, SessionTreeEntry}
|
||||
labels_by_id::Dict{String, String}
|
||||
leaf_id::Union{String, Nothing}
|
||||
entries::Dict{String, SessionTreeEntry}
|
||||
labels::Dict{String, String}
|
||||
end
|
||||
```
|
||||
|
||||
@@ -317,6 +337,19 @@ end
|
||||
mutable struct Session{T<:SessionMetadata}
|
||||
storage::SessionStorage{T}
|
||||
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
|
||||
```
|
||||
|
||||
@@ -326,14 +359,10 @@ end
|
||||
|
||||
```julia
|
||||
function appendMessage(session::Session, message::AgentMessage)::String
|
||||
entry = MessageEntry(
|
||||
"message",
|
||||
createEntryId(session.storage),
|
||||
getLeafId(session.storage),
|
||||
create_timestamp(),
|
||||
return appendTypedEntry(session, MessageEntry(
|
||||
SessionTreeEntryBase("message", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
|
||||
message,
|
||||
)
|
||||
return appendTypedEntry(session, entry)
|
||||
))
|
||||
end
|
||||
```
|
||||
|
||||
@@ -354,18 +383,34 @@ tool_id = appendMessage(session, ToolResultMessage(...))
|
||||
#### appendThinkingLevelChange()
|
||||
|
||||
```julia
|
||||
function appendThinkingLevelChange(
|
||||
session::Session,
|
||||
thinking_level::String,
|
||||
)::String
|
||||
entry = ThinkingLevelChangeEntry(
|
||||
"thinking_level_change",
|
||||
createEntryId(session.storage),
|
||||
getLeafId(session.storage),
|
||||
create_timestamp(),
|
||||
function appendThinkingLevelChange(session::Session, thinking_level::String)::String
|
||||
return appendTypedEntry(session, ThinkingLevelChangeEntry(
|
||||
SessionTreeEntryBase("thinking_level_change", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
|
||||
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
|
||||
```
|
||||
|
||||
@@ -382,11 +427,8 @@ function appendCompaction(
|
||||
usage::Union{Usage, Nothing}=nothing,
|
||||
retained_tail::Union{Vector{AgentMessage}, Nothing}=nothing,
|
||||
)::String
|
||||
entry = CompactionEntry(
|
||||
"compaction",
|
||||
createEntryId(session.storage),
|
||||
getLeafId(session.storage),
|
||||
create_timestamp(),
|
||||
return appendTypedEntry(session, CompactionEntry(
|
||||
SessionTreeEntryBase("compaction", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
|
||||
summary,
|
||||
first_kept_entry_id,
|
||||
tokens_before,
|
||||
@@ -394,8 +436,7 @@ function appendCompaction(
|
||||
details,
|
||||
usage,
|
||||
from_hook,
|
||||
)
|
||||
return appendTypedEntry(session, entry)
|
||||
))
|
||||
end
|
||||
```
|
||||
|
||||
@@ -407,25 +448,24 @@ function moveTo(
|
||||
entry_id::Union{String, Nothing},
|
||||
summary::Union{Dict{String, Any}, Nothing}=nothing,
|
||||
)::Union{String, Nothing}
|
||||
# Set new leaf
|
||||
setLeafId(session.storage, entry_id)
|
||||
|
||||
# Optionally create branch summary
|
||||
if !isnothing(summary)
|
||||
return appendTypedEntry(session, BranchSummaryEntry(
|
||||
"branch_summary",
|
||||
createEntryId(session.storage),
|
||||
entry_id,
|
||||
create_timestamp(),
|
||||
entry_id,
|
||||
summary["summary"],
|
||||
get(summary, "details", nothing),
|
||||
get(summary, "usage", nothing),
|
||||
get(summary, "from_hook", false),
|
||||
))
|
||||
# Validate entry exists
|
||||
if !isnothing(entry_id) && isnothing(getEntry(session, entry_id))
|
||||
throw(SessionError("not_found", "Entry $(entry_id) not found"))
|
||||
end
|
||||
|
||||
return nothing
|
||||
# Set new leaf (creates a LeafEntry)
|
||||
setLeafId(session.storage, entry_id)
|
||||
# Optionally create branch summary
|
||||
if isnothing(summary)
|
||||
return nothing
|
||||
end
|
||||
return appendTypedEntry(session, BranchSummaryEntry(
|
||||
SessionTreeEntryBase("branch_summary", createEntryId(session.storage), entry_id, create_timestamp()),
|
||||
entry_id,
|
||||
summary["summary"],
|
||||
get(summary, "details", nothing),
|
||||
get(summary, "usage", nothing),
|
||||
get(summary, "from_hook", false),
|
||||
))
|
||||
end
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
```julia
|
||||
function buildSessionContext(
|
||||
path_entries::Vector{SessionTreeEntry},
|
||||
options::SessionContextBuildOptions=SessionContextBuildOptions(),
|
||||
options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
|
||||
)::SessionContext
|
||||
state = deriveSessionContextState(path_entries)
|
||||
context_entries = buildContextEntries(path_entries, options)
|
||||
@@ -459,14 +505,36 @@ function buildSessionContext(
|
||||
end
|
||||
return SessionContext(messages, state.thinking_level, state.model, state.active_tool_names)
|
||||
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
|
||||
|
||||
```julia
|
||||
function defaultContextEntryTransform(
|
||||
path_entries::Vector{SessionTreeEntry},
|
||||
)::Vector{SessionTreeEntry}
|
||||
function defaultContextEntryTransform(path_entries::Vector{SessionTreeEntry})::Vector{SessionTreeEntry}
|
||||
compaction = nothing
|
||||
for entry in path_entries
|
||||
if entry isa CompactionEntry
|
||||
@@ -474,25 +542,26 @@ function defaultContextEntryTransform(
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if isnothing(compaction)
|
||||
return copy(path_entries)
|
||||
end
|
||||
|
||||
# Include compaction entry
|
||||
entries = [compaction]
|
||||
|
||||
# Include retained tail if present
|
||||
|
||||
entries::Vector{SessionTreeEntry} = [compaction]
|
||||
compaction_idx = findfirst(
|
||||
(entry) -> entry isa CompactionEntry && entry.id == compaction.id,
|
||||
path_entries,
|
||||
)
|
||||
|
||||
if !isnothing(compaction.retained_tail)
|
||||
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
|
||||
append!(entries, path_entries[compaction_idx+1:end])
|
||||
for i in compaction_idx+1:length(path_entries)
|
||||
push!(entries, path_entries[i])
|
||||
end
|
||||
return entries
|
||||
end
|
||||
|
||||
# Otherwise include entries after first_kept_entry_id
|
||||
|
||||
if !isnothing(compaction.first_kept_entry_id)
|
||||
found_first_kept = false
|
||||
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
|
||||
for i in 1:compaction_idx-1
|
||||
entry = path_entries[i]
|
||||
if entry.id == compaction.first_kept_entry_id
|
||||
@@ -503,11 +572,26 @@ function defaultContextEntryTransform(
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Include entries after compaction
|
||||
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
|
||||
append!(entries, path_entries[compaction_idx+1:end])
|
||||
|
||||
|
||||
for i in compaction_idx+1:length(path_entries)
|
||||
push!(entries, path_entries[i])
|
||||
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
|
||||
end
|
||||
```
|
||||
@@ -519,12 +603,12 @@ function sessionEntryToContextMessages(
|
||||
entry::SessionTreeEntry,
|
||||
index::Int64,
|
||||
entries::Vector{SessionTreeEntry},
|
||||
options::SessionContextBuildOptions=SessionContextBuildOptions(),
|
||||
options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
|
||||
)::Vector{AgentMessage}
|
||||
if entry isa MessageEntry
|
||||
return [entry.message]
|
||||
end
|
||||
|
||||
|
||||
if entry isa CustomMessageEntry
|
||||
return [createCustomMessage(
|
||||
entry.custom_type,
|
||||
@@ -534,7 +618,7 @@ function sessionEntryToContextMessages(
|
||||
entry.timestamp,
|
||||
)]
|
||||
end
|
||||
|
||||
|
||||
if entry isa CompactionEntry
|
||||
messages = [createCompactionSummaryMessage(
|
||||
entry.summary,
|
||||
@@ -546,7 +630,7 @@ function sessionEntryToContextMessages(
|
||||
end
|
||||
return messages
|
||||
end
|
||||
|
||||
|
||||
if entry isa BranchSummaryEntry
|
||||
return [createBranchSummaryMessage(
|
||||
entry.summary,
|
||||
@@ -554,16 +638,15 @@ function sessionEntryToContextMessages(
|
||||
entry.timestamp,
|
||||
)]
|
||||
end
|
||||
|
||||
|
||||
if entry isa CustomEntry
|
||||
# Custom projectors can transform custom entries
|
||||
if !isnothing(options.entry_projectors) && haskey(options.entry_projectors, entry.custom_type)
|
||||
projector = options.entry_projectors[entry.custom_type]
|
||||
return projector(entry, index, entries)
|
||||
end
|
||||
return AgentMessage[]
|
||||
end
|
||||
|
||||
|
||||
return AgentMessage[]
|
||||
end
|
||||
```
|
||||
@@ -610,6 +693,16 @@ Key Points:
|
||||
- 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
|
||||
|
||||
### Why Compaction?
|
||||
@@ -641,7 +734,7 @@ LLM context windows have limits:
|
||||
|
||||
# 4. Update storage
|
||||
# - Append CompactionEntry
|
||||
# - Update leaf to CompactionEntry
|
||||
# - Leaf automatically points to CompactionEntry (leafIdAfterEntry)
|
||||
```
|
||||
|
||||
### Compaction Example
|
||||
@@ -697,8 +790,10 @@ using AgentCore
|
||||
|
||||
# 1. Create storage
|
||||
storage = JsonlSessionStorage(
|
||||
SessionMetadata("session_1", "2024-01-01T00:00:00Z"),
|
||||
"/path/to/session.jsonl",
|
||||
SessionHeader("session", 3, "session_1", created_at, "/path", nothing, nothing),
|
||||
SessionTreeEntry[],
|
||||
nothing,
|
||||
)
|
||||
|
||||
# 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))
|
||||
msg4_id = appendMessage(session, AssistantMessage("assistant", [TextContent("I can...")], ...))
|
||||
|
||||
# 7. Compact context (100K tokens → 20K)
|
||||
# 7. Compact context (100K tokens -> 20K)
|
||||
compact_id = appendCompaction(
|
||||
session,
|
||||
"User asked about capabilities and assistant explained",
|
||||
@@ -733,31 +828,91 @@ compact_id = appendCompaction(
|
||||
# 8. Fork and branch
|
||||
session.moveTo(msg2_id) # Go back to msg2
|
||||
|
||||
# 9. Create new branch
|
||||
branch_id = appendBranchSummary(
|
||||
# 9. Continue on new branch (moveTo creates branch summary when summary is provided)
|
||||
branch_id = moveTo(
|
||||
session,
|
||||
"User changed direction to focus on file operations",
|
||||
msg2_id,
|
||||
Dict("focus" => "files"),
|
||||
Dict("summary" => "User changed direction", "details" => Dict("focus" => "files")),
|
||||
)
|
||||
|
||||
# 10. Continue on new branch
|
||||
msg5_id = appendMessage(session, UserMessage("user", [TextContent("Let's work with files")], timestamp))
|
||||
|
||||
# 11. Query session context
|
||||
context = buildSessionContext(session)
|
||||
context = buildContext(session)
|
||||
|
||||
# 12. Get stats
|
||||
stats = getSessionStats(session)
|
||||
println("Messages: $(stats.message_count)")
|
||||
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
|
||||
|
||||
1. **Use compaction** for long conversations to stay within context limits
|
||||
2. **Create branch summaries** when forking to document divergent paths
|
||||
3. **Retain tail messages** after compaction for context
|
||||
2. **Create branch summaries** when forking to document divergent paths (via `moveTo()` with summary)
|
||||
3. **Retain tail messages** after compaction for context (`retained_tail` field)
|
||||
4. **Track token usage** to optimize compaction timing
|
||||
5. **Use InMemorySessionStorage** for testing
|
||||
6. **Use `getBranch(session)`** to get the current path from leaf to root/compaction
|
||||
7. **Use `buildContext(session)`** as the convenient Session method for building context
|
||||
8. **Use `mergeContextBuildOptions(session, options)`** to combine session-level and call-level transforms/projectors
|
||||
|
||||
+193
-620
@@ -1,434 +1,187 @@
|
||||
# AgentCore.jl - Tools Deep Dive
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tool Layer │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ AgentTool │
|
||||
│ - name: String (identifier) │
|
||||
│ - label: String (display name) │
|
||||
│ - description: String (what it does) │
|
||||
│ - parameters: JSON schema │
|
||||
│ - execute::Function (main logic) │
|
||||
│ - prepare_arguments::Union{Function, Nothing} │
|
||||
│ - execution_mode::Union{ToolExecutionMode, Nothing} │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ BashTool │ │ ReadTool │ │ WriteTool │
|
||||
│ - bash() │ │ - read() │ │ - write() │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
┌─────────────┐
|
||||
│ EditTool │
|
||||
│ - edit() │
|
||||
└─────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tool Execution Flow │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Assistant Message
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ AssistantMessage: │
|
||||
│ content: [ │
|
||||
│ TextContent("I'll check the files..."), │
|
||||
│ ToolCall("bash", {command: "ls -la"}), │
|
||||
│ ToolCall("read", {path: "README.md"}) │
|
||||
│ ] │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
│
|
||||
AssistantMessage (from LLM)
|
||||
content::Vector{MessageContent}
|
||||
└─ Contains: TextContent[] and ToolCall[]
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ AgentLoop.executeToolCalls() │
|
||||
│ - Extract ToolCalls from message content │
|
||||
│ - Determine execution mode (sequential/parallel) │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
│
|
||||
├─► executeToolCallsSequential()
|
||||
│ (for tools that require order)
|
||||
│
|
||||
└─► executeToolCallsParallel()
|
||||
(for independent tools)
|
||||
|
||||
│
|
||||
├─► prepareToolCall()
|
||||
│ - before_tool_call hook (optional)
|
||||
│ - validate arguments
|
||||
│ - prepare arguments (optional)
|
||||
│
|
||||
├─► execute()
|
||||
│ - Tool-specific logic
|
||||
│ - Return AgentToolResult
|
||||
│
|
||||
├─► finalizeExecutedToolCall()
|
||||
│ - after_tool_call hook (optional)
|
||||
│
|
||||
└─► createToolResultMessage()
|
||||
- Emit ToolResultMessage
|
||||
|
||||
│
|
||||
Agent.execute() (in agent.jl)
|
||||
└─ before_tool_call hook (Agent.before_tool_call, optional)
|
||||
Input: BeforeToolCallContext
|
||||
Output: BeforeToolCallResult (block, reason)
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ ToolResultMessage │
|
||||
│ - tool_call_id: "ref to original ToolCall" │
|
||||
│ - tool_name: "bash" │
|
||||
│ - content: [TextContent("file1.md\nfile2.md\n")] │
|
||||
│ - is_error: false │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
│
|
||||
For each ToolCall:
|
||||
tool = find_tool(name)
|
||||
tool.execute(tool_call_id, args, signal, on_update, context)
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ AgentState.messages.append(tool_result) │
|
||||
│ - Next turn: LLM sees tool results │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
AgentToolResult{T}(content, details, usage, added_tool_names, terminate)
|
||||
▼
|
||||
└─ 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
|
||||
|
||||
### 1. BashTool
|
||||
### 1. BashTool (`tools/bash.jl`)
|
||||
|
||||
```julia
|
||||
struct BashToolOptions{TContext}
|
||||
command_prefix::Union{String, Nothing}
|
||||
prepare::Union{BashPrepare{TContext}, Nothing}
|
||||
struct BashExecution
|
||||
command::String
|
||||
cwd::String
|
||||
env::Dict{String, String}
|
||||
inherit_env::Bool
|
||||
end
|
||||
|
||||
struct BashPrepare{TContext}
|
||||
mutable struct BashPrepare{TContext}
|
||||
function::Function
|
||||
context::TContext
|
||||
signal::Union{Any, Nothing}
|
||||
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}
|
||||
full_output_path::Union{String, Nothing}
|
||||
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
|
||||
function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing)
|
||||
return AgentTool(
|
||||
"bash",
|
||||
"bash",
|
||||
"Execute a bash command in the current working directory.",
|
||||
Dict{String, Any}(),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Execute command
|
||||
result = executeBashCommand(params, signal, on_update)
|
||||
|
||||
# Return result
|
||||
return AgentToolResult(
|
||||
[TextContent(result.output)],
|
||||
BashToolDetails(result.truncation, result.full_path),
|
||||
nothing,
|
||||
nothing,
|
||||
result.terminate,
|
||||
)
|
||||
end,
|
||||
nothing, # prepare_arguments
|
||||
nothing, # execution_mode (default: use config)
|
||||
)
|
||||
mutable struct ReadToolDetails
|
||||
truncation::Union{Any, Nothing}
|
||||
end
|
||||
|
||||
mutable struct ReadToolOptions
|
||||
auto_resize_images::Bool
|
||||
image_processor::Union{Any, Nothing}
|
||||
end
|
||||
|
||||
function createReadTool{TContext}(options::Union{ReadToolOptions, Nothing}=nothing) where TContext
|
||||
```
|
||||
|
||||
**Execute signature**: `(tool_call_id, params, signal, on_update, context) -> AgentToolResult`
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
**Parameters Schema**:
|
||||
```json
|
||||
{
|
||||
"command": "string",
|
||||
"timeout": "number (optional)",
|
||||
"cwd": "string (optional)",
|
||||
"env": "object (optional)"
|
||||
}
|
||||
```
|
||||
Configured via `Agent(Dict(...))` options:
|
||||
- `:beforeToolCall` → `Agent.before_tool_call`
|
||||
- `:afterToolCall` → `Agent.after_tool_call`
|
||||
- `:prepareNextTurn` → `Agent.prepare_next_turn`
|
||||
- `:prepareNextTurnWithContext` → `Agent.prepare_next_turn_with_context`
|
||||
|
||||
**Example**:
|
||||
```julia
|
||||
# Create tool
|
||||
bash_tool = createBashTool()
|
||||
|
||||
# Agent receives command
|
||||
tool_call = ToolCall("tool", "tc1", "bash", Dict(
|
||||
"command" => "ls -la",
|
||||
"timeout" => 30
|
||||
), nothing)
|
||||
|
||||
# Execute
|
||||
result = bash_tool.execute(
|
||||
"tc1",
|
||||
Dict("command" => "ls -la", "timeout" => 30),
|
||||
nothing,
|
||||
on_update, # Callback for streaming output
|
||||
nothing,
|
||||
)
|
||||
|
||||
# Result
|
||||
AgentToolResult(
|
||||
[TextContent("total 12\n-rw-r--r-- 1 user user 100 Jan 1 file1.md\n-rw-r--r-- 1 user user 200 Jan 2 file2.md\n")],
|
||||
BashToolDetails(truncation_info, nothing),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
```
|
||||
|
||||
### 2. ReadTool
|
||||
|
||||
```julia
|
||||
struct ReadToolOptions{TContext}
|
||||
max_size::Union{Int64, Nothing}
|
||||
max_lines::Union{Int64, Nothing}
|
||||
image_processor::Union{ReadImageProcessor, Nothing}
|
||||
prepare::Union{ReadPrepare{TContext}, Nothing}
|
||||
end
|
||||
|
||||
struct ReadImageProcessor
|
||||
function::Function
|
||||
context::Any
|
||||
end
|
||||
|
||||
struct ReadImageProcessorResult
|
||||
content::Vector{MessageContent}
|
||||
usage::Union{Usage, Nothing}
|
||||
end
|
||||
```
|
||||
|
||||
#### createReadTool()
|
||||
|
||||
```julia
|
||||
function createReadTool{TContext}(options::Union{ReadToolOptions{TContext}, Nothing}=nothing)
|
||||
return AgentTool(
|
||||
"read",
|
||||
"read",
|
||||
"Read a file from the file system.",
|
||||
Dict{String, Any}(),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Read file
|
||||
result = readFileSystem(params, signal, options)
|
||||
|
||||
# Process content
|
||||
content = if isImage(params.path)
|
||||
# Image processing
|
||||
image_result = options.image_processor.function(result.path, context)
|
||||
image_result.content
|
||||
else
|
||||
# Text content
|
||||
[TextContent(result.content)]
|
||||
end
|
||||
|
||||
return AgentToolResult(
|
||||
content,
|
||||
ReadToolDetails(result.size, result.truncated, result.full_path),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
**Parameters Schema**:
|
||||
```json
|
||||
{
|
||||
"path": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```julia
|
||||
# Create tool
|
||||
read_tool = createReadTool()
|
||||
|
||||
# Agent requests to read file
|
||||
tool_call = ToolCall("tool", "tc2", "read", Dict(
|
||||
"path" => "src/main.jl"
|
||||
), nothing)
|
||||
|
||||
# Execute
|
||||
result = read_tool.execute("tc2", Dict("path" => "src/main.jl"), nothing, nothing, nothing)
|
||||
|
||||
# Result
|
||||
AgentToolResult(
|
||||
[TextContent("module Main\nfunction main()\n println(\"Hello\")\nend\nend\n")],
|
||||
ReadToolDetails(1234, false, "/path/to/src/main.jl"),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
```
|
||||
|
||||
### 3. WriteTool
|
||||
|
||||
```julia
|
||||
struct WriteToolInput
|
||||
path::String
|
||||
content::String
|
||||
end
|
||||
```
|
||||
|
||||
#### createWriteTool()
|
||||
|
||||
```julia
|
||||
function createWriteTool{TContext}(options::Union{WriteToolOptions{TContext}, Nothing}=nothing)
|
||||
return AgentTool(
|
||||
"write",
|
||||
"write",
|
||||
"Write content to a file.",
|
||||
Dict{String, Any}(),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Write file
|
||||
result = writeToFile(params, signal)
|
||||
|
||||
return AgentToolResult(
|
||||
[TextContent(result.message)],
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
**Parameters Schema**:
|
||||
```json
|
||||
{
|
||||
"path": "string",
|
||||
"content": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```julia
|
||||
# Create tool
|
||||
write_tool = createWriteTool()
|
||||
|
||||
# Agent wants to write file
|
||||
tool_call = ToolCall("tool", "tc3", "write", Dict(
|
||||
"path" => "output.txt",
|
||||
"content" => "Hello World"
|
||||
), nothing)
|
||||
|
||||
# Execute
|
||||
result = write_tool.execute("tc3", Dict(
|
||||
"path" => "output.txt",
|
||||
"content" => "Hello World"
|
||||
), nothing, nothing, nothing)
|
||||
|
||||
# Result
|
||||
AgentToolResult(
|
||||
[TextContent("File written: output.txt (11 bytes)")],
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
```
|
||||
|
||||
### 4. EditTool
|
||||
|
||||
```julia
|
||||
struct EditToolInput
|
||||
path::String
|
||||
find::String
|
||||
replacement::String
|
||||
end
|
||||
|
||||
struct EditToolDetails
|
||||
edits::Vector{Edit}
|
||||
before_content::String
|
||||
after_content::String
|
||||
end
|
||||
```
|
||||
|
||||
#### createEditTool()
|
||||
|
||||
```julia
|
||||
function createEditTool{TContext}(options::Union{EditToolOptions{TContext}, Nothing}=nothing)
|
||||
return AgentTool(
|
||||
"edit",
|
||||
"edit",
|
||||
"Edit a file by finding and replacing text.",
|
||||
Dict{String, Any}(),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Read file
|
||||
before_content = read(params.path)
|
||||
|
||||
# Apply edit
|
||||
after_content = replace(before_content, params.find => params.replacement)
|
||||
|
||||
# Write file
|
||||
write(params.path, after_content)
|
||||
|
||||
return AgentToolResult(
|
||||
[TextContent("Edit applied successfully")],
|
||||
EditToolDetails([Edit(params.find, params.replacement)], before_content, after_content),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
**Parameters Schema**:
|
||||
```json
|
||||
{
|
||||
"path": "string",
|
||||
"find": "string",
|
||||
"replacement": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```julia
|
||||
# Create tool
|
||||
edit_tool = createEditTool()
|
||||
|
||||
# Agent wants to replace text
|
||||
tool_call = ToolCall("tool", "tc4", "edit", Dict(
|
||||
"path" => "README.md",
|
||||
"find" => "v1.0.0",
|
||||
"replacement" => "v2.0.0"
|
||||
), nothing)
|
||||
|
||||
# Execute
|
||||
result = edit_tool.execute("tc4", Dict(
|
||||
"path" => "README.md",
|
||||
"find" => "v1.0.0",
|
||||
"replacement" => "v2.0.0"
|
||||
), nothing, nothing, nothing)
|
||||
|
||||
# Result
|
||||
AgentToolResult(
|
||||
[TextContent("Edit applied: README.md")],
|
||||
EditToolDetails([Edit("v1.0.0", "v2.0.0")], "Version 1.0.0", "Version 2.0.0"),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
```
|
||||
|
||||
## Tool Execution Hooks
|
||||
|
||||
### before_tool_call
|
||||
### BeforeToolCallContext / BeforeToolCallResult (from types.jl)
|
||||
|
||||
```julia
|
||||
struct BeforeToolCallContext
|
||||
@@ -444,32 +197,7 @@ struct BeforeToolCallResult
|
||||
end
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
function myBeforeToolCall(context, signal)
|
||||
tool_name = context.tool_call.name
|
||||
|
||||
# Block dangerous commands
|
||||
if tool_name == "bash" && contains(context.args["command"], "rm -rf /")
|
||||
return BeforeToolCallResult(
|
||||
true,
|
||||
"Blocking dangerous command: rm -rf /"
|
||||
)
|
||||
end
|
||||
|
||||
# Log tool execution
|
||||
println("Executing tool: $tool_name")
|
||||
|
||||
return nothing # Allow execution
|
||||
end
|
||||
|
||||
# Configure agent
|
||||
agent = Agent(Dict(
|
||||
:beforeToolCall => myBeforeToolCall,
|
||||
))
|
||||
```
|
||||
|
||||
### after_tool_call
|
||||
### AfterToolCallContext / AfterToolCallResult (from types.jl)
|
||||
|
||||
```julia
|
||||
struct AfterToolCallContext
|
||||
@@ -490,37 +218,7 @@ struct AfterToolCallResult
|
||||
end
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
function myAfterToolCall(context, signal)
|
||||
tool_name = context.tool_call.name
|
||||
|
||||
# Modify bash output
|
||||
if tool_name == "bash"
|
||||
# Add timestamp to output
|
||||
new_content = [
|
||||
TextContent("[Executed at $(Dates.now())]\n"),
|
||||
context.result.content[1],
|
||||
]
|
||||
return AfterToolCallResult(
|
||||
content = new_content,
|
||||
details = context.result.details,
|
||||
is_error = context.is_error,
|
||||
usage = context.result.usage,
|
||||
terminate = context.result.terminate,
|
||||
)
|
||||
end
|
||||
|
||||
return nothing # Use original result
|
||||
end
|
||||
|
||||
# Configure agent
|
||||
agent = Agent(Dict(
|
||||
:afterToolCall => myAfterToolCall,
|
||||
))
|
||||
```
|
||||
|
||||
### prepare_next_turn
|
||||
### PrepareNextTurnContext / AgentLoopTurnUpdate (from types.jl)
|
||||
|
||||
```julia
|
||||
struct PrepareNextTurnContext
|
||||
@@ -537,193 +235,73 @@ struct AgentLoopTurnUpdate
|
||||
end
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```julia
|
||||
function myPrepareNextTurn(context, signal)
|
||||
# Check if we should use a different model
|
||||
last_message = context.message
|
||||
tool_results = context.tool_results
|
||||
|
||||
# If tool execution had errors, use more capable model
|
||||
has_errors = any(r -> r.is_error, tool_results)
|
||||
if has_errors
|
||||
return AgentLoopTurnUpdate(
|
||||
context = context.context,
|
||||
model = Model("gpt-4", "GPT-4", "openai", "openai", "", ...),
|
||||
thinking_level = THINKING_HIGH,
|
||||
)
|
||||
end
|
||||
|
||||
return nothing # Keep current settings
|
||||
end
|
||||
|
||||
# Configure agent
|
||||
agent = Agent(Dict(
|
||||
:prepareNextTurn => myPrepareNextTurn,
|
||||
))
|
||||
```
|
||||
|
||||
## Tool Execution Modes
|
||||
|
||||
### Sequential Execution
|
||||
|
||||
```julia
|
||||
# Tools run one at a time, in order
|
||||
# Use case: Tools that modify shared state
|
||||
|
||||
# Configure tool
|
||||
bash_tool = AgentTool(
|
||||
"bash",
|
||||
"bash",
|
||||
"Execute bash command",
|
||||
...,
|
||||
execute,
|
||||
nothing,
|
||||
EXECUTION_SEQUENTIAL, # Force sequential
|
||||
)
|
||||
|
||||
# Or configure globally
|
||||
# Configure on Agent
|
||||
agent = Agent(Dict(
|
||||
:toolExecution => EXECUTION_SEQUENTIAL,
|
||||
))
|
||||
```
|
||||
|
||||
**Example Scenario**:
|
||||
```julia
|
||||
# Sequential execution (correct order)
|
||||
|
||||
1. Tool 1: create_directory("build/")
|
||||
└─ Creates build/ directory
|
||||
|
||||
2. Tool 2: write("build/app.js", "...")
|
||||
└─ Writes file to build/
|
||||
|
||||
(If parallel: might fail because build/ doesn't exist yet)
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
### Parallel Execution (default)
|
||||
|
||||
```julia
|
||||
# Tools run concurrently
|
||||
# Use case: Independent operations
|
||||
|
||||
# Default behavior
|
||||
agent = Agent(Dict(
|
||||
:toolExecution => EXECUTION_PARALLEL, # Default
|
||||
:toolExecution => EXECUTION_PARALLEL,
|
||||
))
|
||||
```
|
||||
|
||||
**Example Scenario**:
|
||||
```julia
|
||||
# Parallel execution (independent operations)
|
||||
|
||||
1. Tool 1: read("README.md") ─────┐
|
||||
2. Tool 2: read("CHANGELOG.md") ─┼─► Run simultaneously
|
||||
3. Tool 3: read("LICENSE") ──────┘
|
||||
|
||||
(Parallel: All three read operations can happen at once)
|
||||
(Sequential: Would wait for each read to complete)
|
||||
```
|
||||
|
||||
## Custom Tools
|
||||
|
||||
### Example: Database Tool
|
||||
Tools can also specify their own mode:
|
||||
|
||||
```julia
|
||||
function createDatabaseTool()
|
||||
return AgentTool(
|
||||
"database",
|
||||
"database",
|
||||
"Execute SQL queries against the database.",
|
||||
Dict{String, Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"query" => Dict("type" => "string"),
|
||||
"params" => Dict("type" => "array", "items" => Dict("type" => "string")),
|
||||
),
|
||||
"required" => ["query"],
|
||||
),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Execute query
|
||||
query = params["query"]
|
||||
result = executeQuery(query)
|
||||
|
||||
# Format output
|
||||
output = formatQueryResult(result)
|
||||
|
||||
return AgentToolResult(
|
||||
[TextContent(output)],
|
||||
Dict("rows_affected" => result.rows_affected),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
EXECUTION_SEQUENTIAL,
|
||||
)
|
||||
end
|
||||
|
||||
# Usage
|
||||
db_tool = createDatabaseTool()
|
||||
agent = Agent(Dict(:tools => [db_tool]))
|
||||
agent_tool = AgentTool(
|
||||
"name",
|
||||
"label",
|
||||
"description",
|
||||
params_schema,
|
||||
execute_fn,
|
||||
nothing,
|
||||
EXECUTION_SEQUENTIAL, # or EXECUTION_PARALLEL
|
||||
)
|
||||
```
|
||||
|
||||
### Example: HTTP Request Tool
|
||||
## Tool Exports (from tools/index.jl)
|
||||
|
||||
```julia
|
||||
function createHTTPTool()
|
||||
return AgentTool(
|
||||
"http",
|
||||
"http",
|
||||
"Make HTTP requests.",
|
||||
Dict{String, Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"url" => Dict("type" => "string"),
|
||||
"method" => Dict("type" => "string", "enum" => ["GET", "POST", "PUT", "DELETE"]),
|
||||
"body" => Dict("type" => "string"),
|
||||
"headers" => Dict("type" => "object"),
|
||||
),
|
||||
"required" => ["url", "method"],
|
||||
),
|
||||
(tool_call_id, params, signal, on_update, context) -> begin
|
||||
# Make request
|
||||
url = params["url"]
|
||||
method = params["method"]
|
||||
body = get(params, "body", nothing)
|
||||
headers = get(params, "headers", Dict())
|
||||
|
||||
response = makeHTTPRequest(method, url, body, headers)
|
||||
|
||||
return AgentToolResult(
|
||||
[TextContent(response.body)],
|
||||
Dict(
|
||||
"status_code" => response.status_code,
|
||||
"headers" => response.headers,
|
||||
),
|
||||
nothing,
|
||||
nothing,
|
||||
nothing,
|
||||
)
|
||||
end,
|
||||
nothing,
|
||||
EXECUTION_PARALLEL,
|
||||
)
|
||||
end
|
||||
export
|
||||
createBashTool,
|
||||
createReadTool,
|
||||
createWriteTool,
|
||||
createEditTool,
|
||||
BashExecution,
|
||||
BashPrepare,
|
||||
BashToolDetails,
|
||||
BashToolInput,
|
||||
BashToolOptions,
|
||||
EditToolDetails,
|
||||
EditToolInput,
|
||||
ReadToolDetails,
|
||||
ReadToolInput,
|
||||
ReadToolOptions,
|
||||
ReadImageProcessor,
|
||||
ReadImageProcessorResult,
|
||||
WriteToolInput
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
## Example: Creating and Using Tools
|
||||
|
||||
```julia
|
||||
using AgentCore
|
||||
|
||||
# 1. Create tools
|
||||
# Create tools
|
||||
bash_tool = createBashTool()
|
||||
read_tool = createReadTool()
|
||||
write_tool = createWriteTool()
|
||||
|
||||
# 2. Configure hooks
|
||||
# Configure hooks
|
||||
before_hook = (context, signal) -> begin
|
||||
println("About to execute: $(context.tool_call.name)")
|
||||
return nothing
|
||||
@@ -738,22 +316,17 @@ after_hook = (context, signal) -> begin
|
||||
return nothing
|
||||
end
|
||||
|
||||
# 3. Create agent
|
||||
# Create agent with tools and hooks
|
||||
agent = Agent(Dict(
|
||||
:systemPrompt => "You are a helpful assistant with file system access.",
|
||||
:tools => [bash_tool, read_tool, write_tool],
|
||||
:beforeToolCall => before_hook,
|
||||
:afterToolCall => after_hook,
|
||||
:toolExecution => EXECUTION_PARALLEL,
|
||||
))
|
||||
|
||||
# 4. Run conversation
|
||||
# Run prompt
|
||||
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
|
||||
|
||||
+500
-603
File diff suppressed because it is too large
Load Diff
+344
-513
File diff suppressed because it is too large
Load Diff
+212
-24
@@ -37,7 +37,7 @@ agent = Agent(Dict(
|
||||
prompt(agent, "Hello!")
|
||||
|
||||
# Wait for completion
|
||||
wait_for_idle(agent)
|
||||
waitForIdle(agent)
|
||||
```
|
||||
|
||||
### Understanding the Flow
|
||||
@@ -83,6 +83,12 @@ User Code
|
||||
- `steer()` - Queue message for next turn
|
||||
- `followUp()` - Queue message after stop
|
||||
- `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
|
||||
|
||||
@@ -113,9 +119,14 @@ User Code
|
||||
|
||||
**Key methods**:
|
||||
- `appendMessage()` - Add message
|
||||
- `appendCompaction()` - Compress history
|
||||
- `appendCompaction()` - Compress history with summary
|
||||
- `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
|
||||
|
||||
@@ -165,32 +176,209 @@ AgentStartEvent
|
||||
└─ AgentEndEvent
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
## Complete Data Flow with Type Transformations
|
||||
|
||||
### Message Transformation
|
||||
This documentation shows how data is transformed through the agent lifecycle.
|
||||
|
||||
### Message Type Hierarchy
|
||||
|
||||
```
|
||||
AgentMessage[] (internal)
|
||||
Message (for LLM API)
|
||||
├── UserMessage (role: "user")
|
||||
│ └── content::Vector{MessageContent}
|
||||
│ ├── TextContent (text::String)
|
||||
│ └── ImageContent (data::String, mime_type::String)
|
||||
├── AssistantMessage (role: "assistant")
|
||||
│ ├── content::Vector{MessageContent}
|
||||
│ │ ├── TextContent
|
||||
│ │ └── ToolCall (id, name, arguments::Dict{String, Any})
|
||||
│ ├── usage::Usage
|
||||
│ ├── stop_reason::String
|
||||
│ └── timestamp::Timestamp
|
||||
└── ToolResultMessage (role: "toolResult")
|
||||
├── tool_call_id::String
|
||||
├── tool_name::String
|
||||
├── content::Vector{MessageContent}
|
||||
├── details::Any
|
||||
├── usage::Union{Usage, Nothing}
|
||||
├── is_error::Bool
|
||||
└── timestamp::Timestamp
|
||||
|
||||
AgentMessage (internal, 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)
|
||||
▼
|
||||
AgentMessage[] (transformed)
|
||||
└─► normalizePromptInput()
|
||||
Input: "Hello!"::String
|
||||
Output: [UserMessage("user", [TextContent("Hello!")], timestamp)]
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Step 2: AgentLoop Processing │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
runAgentLoop()
|
||||
│
|
||||
├─► transform_context() (optional hook)
|
||||
│ Input: [UserMessage(...)]::Vector{AgentMessage}
|
||||
│ Output: [UserMessage(...)]::Vector{AgentMessage}
|
||||
│
|
||||
├─► convert_to_llm()
|
||||
│ Input: [UserMessage(...)]::Vector{AgentMessage}
|
||||
│ Output: [UserMessage(...)]::Vector{Message}
|
||||
│
|
||||
├─► stream_fn() - LLM API call
|
||||
│ Input: model, Context(...), config
|
||||
│ Output: AssistantMessage with ToolCall[]
|
||||
│
|
||||
├─► executeToolCalls()
|
||||
│ Input: AssistantMessage (with ToolCall[])
|
||||
│ Output: ToolResultMessage[]
|
||||
│
|
||||
└─► Emit events and append to context.messages
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Step 3: Final Conversation State │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
context.messages::Vector{AgentMessage}
|
||||
├─ UserMessage("user", [TextContent("Hello!")], ...)
|
||||
├─ AssistantMessage("assistant", [
|
||||
│ TextContent("Hi there!"),
|
||||
│ ToolCall("bash", {...})
|
||||
│ ], ...)
|
||||
└─ ToolResultMessage("toolResult", "bash", [TextContent("...")], ...)
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Step 4: AgentEndEvent (final output) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
AgentEndEvent(messages::Vector{AgentMessage})
|
||||
└─ Contains full conversation history
|
||||
User Input (String / AgentMessage / Vector{AgentMessage})
|
||||
│
|
||||
├─► normalizePromptInput()
|
||||
│ Input: input::Union{String, AgentMessage, Vector{AgentMessage}}
|
||||
│ Output: Vector{AgentMessage}
|
||||
│ • String → UserMessage("user", [TextContent(input)], timestamp)
|
||||
│ • AgentMessage → [input]
|
||||
│ • Vector{AgentMessage} → input (pass-through)
|
||||
│
|
||||
├─► prompt(agent, messages)
|
||||
│ └─► runPromptMessages()
|
||||
│
|
||||
├─ convert_to_llm()
|
||||
▼
|
||||
Message[] (LLM API)
|
||||
AgentLoop Execution:
|
||||
│
|
||||
├─► transform_context() (optional hook)
|
||||
│ Input: context.messages::Vector{AgentMessage}
|
||||
│ Output: messages::Vector{AgentMessage} (transformed)
|
||||
│
|
||||
├─► convert_to_llm()
|
||||
│ Input: messages::Vector{AgentMessage}
|
||||
│ Output: llm_messages::Vector{Message}
|
||||
│
|
||||
│ AgentMessage → Message mapping:
|
||||
│ • UserMessage → UserMessage (pass-through)
|
||||
│ • AssistantMessage → AssistantMessage (pass-through)
|
||||
│ • ToolResultMessage → ToolResultMessage (pass-through)
|
||||
│ • BashExecutionMessage → UserMessage (text conversion)
|
||||
│ • CompactionSummaryMessage → UserMessage (text wrapped)
|
||||
│ • BranchSummaryMessage → UserMessage (text wrapped)
|
||||
│
|
||||
├─► LLM API Call (stream_fn)
|
||||
│ Input: model, Context(system_prompt, llm_messages, tools), config
|
||||
│ Output: Stream{AssistantMessageEvent}
|
||||
│
|
||||
├─► AssistantMessage (returned from LLM)
|
||||
│ content::Vector{MessageContent}
|
||||
│ └─ Contains: TextContent[] and/or ToolCall[]
|
||||
│
|
||||
├─► executeToolCalls() (if ToolCall[] in content)
|
||||
│ │
|
||||
│ ├─► prepareToolCall() for each ToolCall
|
||||
│ │ Input: tool_call::ToolCall
|
||||
│ │ Output: PreparedToolCall or ImmediateToolCallOutcome
|
||||
│ │
|
||||
│ ├─► executePreparedToolCall() (if prepared)
|
||||
│ │ Input: PreparedToolCall
|
||||
│ │ Output: ExecutedToolCallOutcome
|
||||
│ │ tool.execute() returns AgentToolResultMutable
|
||||
│ │
|
||||
│ ├─► finalizeExecutedToolCall()
|
||||
│ │ Input: ExecutedToolCallOutcome
|
||||
│ │ Output: FinalizedToolCallOutcome
|
||||
│ │
|
||||
│ └─► createToolResultMessage()
|
||||
│ Input: FinalizedToolCallOutcome
|
||||
│ Output: ToolResultMessage
|
||||
│ • role: "toolResult"
|
||||
│ • tool_call_id, tool_name
|
||||
│ • content::Vector{MessageContent}
|
||||
│ • details, usage, added_tool_names
|
||||
│ • is_error, timestamp
|
||||
│
|
||||
└─► Append to context.messages and new_messages
|
||||
│
|
||||
▼
|
||||
Vector{AgentMessage} (final conversation history)
|
||||
Contains: [UserMessage, AssistantMessage, ToolResultMessage, ...]
|
||||
```
|
||||
|
||||
### Tool Execution Flow
|
||||
|
||||
```
|
||||
ToolCall (in assistant message)
|
||||
ToolCall (in AssistantMessage.content)
|
||||
│
|
||||
├─ before_tool_call hook (optional)
|
||||
│ Input: BeforeToolCallContext
|
||||
│ Output: BeforeToolCallResult (block, reason) or nothing
|
||||
│
|
||||
├─ before_tool_call hook
|
||||
├─ prepareToolCall()
|
||||
├─ execute()
|
||||
├─ after_tool_call hook
|
||||
│ Input: tool_call::ToolCall
|
||||
│ Output: Union{PreparedToolCall, ImmediateToolCallOutcome}
|
||||
│ • Validates tool exists
|
||||
│ • Runs before_tool_call hook
|
||||
│ • Runs prepare_arguments hook (optional)
|
||||
│ • Runs validateToolArguments (optional)
|
||||
│
|
||||
├─ executePreparedToolCall() (if prepared)
|
||||
│ Input: PreparedToolCall
|
||||
│ Output: ExecutedToolCallOutcome
|
||||
│ tool.execute() returns AgentToolResultMutable
|
||||
│
|
||||
├─ finalizeExecutedToolCall()
|
||||
│ Input: ExecutedToolCallOutcome
|
||||
│ Output: FinalizedToolCallOutcome
|
||||
│ Runs after_tool_call hook (optional)
|
||||
│
|
||||
└─ createToolResultMessage()
|
||||
Input: FinalizedToolCallOutcome
|
||||
Output: ToolResultMessage
|
||||
• role: "toolResult"
|
||||
• tool_call_id, tool_name
|
||||
• content::Vector{MessageContent}
|
||||
• details, usage, added_tool_names
|
||||
• is_error, timestamp
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
@@ -253,7 +441,7 @@ appendMessage(session, user_message)
|
||||
appendMessage(session, assistant_message)
|
||||
|
||||
# Build context from session
|
||||
context = buildSessionContext(session)
|
||||
context = buildContext(session)
|
||||
```
|
||||
|
||||
### Pattern 2: Long Conversations
|
||||
@@ -277,7 +465,7 @@ end
|
||||
session.moveTo(branch_point_id)
|
||||
|
||||
# Create new branch
|
||||
appendBranchSummary(session, "Exploring alternative approach")
|
||||
moveTo(session, branch_point_id, summary=["summary" => "Exploring alternative approach"])
|
||||
appendMessage(session, new_user_message)
|
||||
```
|
||||
|
||||
@@ -286,13 +474,13 @@ appendMessage(session, new_user_message)
|
||||
```julia
|
||||
# Create custom tool
|
||||
custom_tool = AgentTool(
|
||||
"custom",
|
||||
"custom",
|
||||
"Does custom thing",
|
||||
...,
|
||||
execute_function,
|
||||
nothing,
|
||||
EXECUTION_PARALLEL,
|
||||
"custom", # name
|
||||
"Custom", # label
|
||||
"Does custom thing", # description
|
||||
parameters, # parameter schema
|
||||
execute_function, # execute
|
||||
nothing, # prepare_arguments (optional)
|
||||
EXECUTION_PARALLEL, # execution_mode
|
||||
)
|
||||
|
||||
# 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