This commit is contained in:
2026-07-31 11:41:53 +07:00
parent c9a7661e93
commit 7876ff21eb
8 changed files with 1671 additions and 2168 deletions
+15 -12
View File
@@ -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,
@@ -290,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))
@@ -303,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
@@ -319,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
@@ -334,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
@@ -367,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)
@@ -375,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
+212 -161
View File
@@ -53,19 +53,22 @@ agentLoopContinue()
│ Output: N/A (writes to new_messages and context.messages) │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ while true: │ │
│ │ 1. Get steering/follow-up messages (if any) │ │
│ │ 2. Emit messages as UserMessage │ │
│ │ 3. streamAssistantResponse() │ │
│ │ - Input: context.messages::Vector{AgentMessage} │ │
│ │ - Output: message::AssistantMessage │ │
│ │ 4. Execute tool calls (sequential or parallel) │ │
│ │ - Input: AssistantMessage with ToolCall[] │ │
│ │ - Output: tool_results::Vector{ToolResultMessage} │ │
│ │ 5. Emit TurnEndEvent │ │
│ │ 6. prepare_next_turn (optional) │ │
│ │ 7. should_stop_after_turn? (check termination) │ │
│ │ 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 │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
@@ -179,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()
@@ -236,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
@@ -255,104 +274,119 @@ 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)
# 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
# Check for errors
if message.stop_reason in ("error", "aborted")
emit(TurnEndEvent(message, []))
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
# Execute tool calls
tool_calls = filter(c -> c isa ToolCall, message.content)
tool_results = []
has_more_tool_calls = false
emit(TurnEndEvent(message, tool_results))
if !isempty(tool_calls)
executed_batch = if message.stop_reason == "length"
failToolCallsFromTruncatedMessage(tool_calls, emit)
else
executeToolCalls(
current_context,
message,
config,
signal,
emit,
# Optional: prepare next turn (model/thinking/context changes)
next_turn_context = PrepareNextTurnContext(
message, tool_results, current_context, new_messages
)
next_turn_snapshot = prepare_next_turn(config, next_turn_context)
if !isnothing(next_turn_snapshot)
current_context = next_turn_snapshot.context
# Rebuild config with updated model/thinking + preserved fields
config = AgentLoopConfig(
model = next_turn_snapshot.model,
reasoning = next_turn_snapshot.thinking_level,
convert_to_llm = config.convert_to_llm,
transform_context = config.transform_context,
get_api_key = config.get_api_key,
should_stop_after_turn = config.should_stop_after_turn,
prepare_next_turn = config.prepare_next_turn,
get_steering_messages = config.get_steering_messages,
get_follow_up_messages = config.get_follow_up_messages,
tool_execution = config.tool_execution,
before_tool_call = config.before_tool_call,
after_tool_call = config.after_tool_call,
max_tokens = config.max_tokens,
temperature = config.temperature,
reasoning = config.reasoning,
cache_retention = config.cache_retention,
session_id = config.session_id,
headers = config.headers,
metadata = config.metadata,
transport = config.transport,
signal = signal,
api_key = config.api_key,
on_payload = config.on_payload,
on_response = config.on_response,
max_retry_delay_ms = config.max_retry_delay_ms,
)
end
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
@@ -422,7 +456,7 @@ function executeToolCalls(
current_context::AgentContext,
assistant_message::AssistantMessage,
config::AgentLoopConfig,
signal::Union{Nothing, AbortSignal>,
signal::Union{Nothing, AbortSignal},
emit::AgentEventSink,
)::ExecutedToolCallBatch
```
@@ -572,7 +606,7 @@ Input: tool_call::ToolCall
```julia
function executePreparedToolCall(
prepared::PreparedToolCall,
signal::Union{Nothing, AbortSignal>,
signal::Union{Nothing, AbortSignal},
emit::AgentEventSink,
)::ExecutedToolCallOutcome
```
@@ -590,14 +624,18 @@ Input: prepared::PreparedToolCall
args::Any
signal::Union{Any, Nothing}
on_update::Function (partial_result → void)
Output: AgentToolResultMutable
- content::Vector{MessageContent}
- details::Any
- usage::Union{Usage, Nothing}
- added_tool_names::Union{Vector{String}, Nothing}
- terminate::Union{Bool, Nothing}
Collect update events from on_update callbacks
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
@@ -612,7 +650,7 @@ function finalizeExecutedToolCall(
prepared::PreparedToolCall,
executed::ExecutedToolCallOutcome,
config::AgentLoopConfig,
signal::Union{Nothing, AbortSignal>,
signal::Union{Nothing, AbortSignal},
)::FinalizedToolCallOutcome
```
@@ -632,18 +670,23 @@ Input: executed::ExecutedToolCallOutcome
is_error,
context
)
Output: AfterToolCallResult (optional patches)
- content::Union{Vector{MessageContent}, Nothing}
- details::Union{Any, Nothing}
- is_error::Union{Bool, Nothing}
- usage::Union{Usage, Nothing}
- terminate::Union{Bool, Nothing}
Apply patches to result (if any)
result.content = result.content patches.content
result.details = result.details patches.details
is_error = is_error patches.is_error
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)
@@ -699,45 +742,39 @@ Input: finalized::FinalizedToolCallOutcome
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 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
@@ -948,16 +985,16 @@ ToolCall (in AssistantMessage.content)
### 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
@@ -977,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,
@@ -986,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
+51 -3
View File
@@ -248,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**:
@@ -288,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
@@ -297,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
@@ -311,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
@@ -532,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
@@ -589,6 +624,8 @@ Vector{AgentMessage} (internal conversation history)
│ (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)
@@ -607,6 +644,7 @@ Vector{Message} (for LLM API)
], "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: Vector{Message}
@@ -618,6 +656,7 @@ Vector{Message} (for LLM API)
], "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),
]
```
@@ -636,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)
+294 -177
View File
@@ -27,15 +27,16 @@
│ └─► 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) │
└─────┘ │
│ └───────────────────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────────────────────┘
```
@@ -49,43 +50,45 @@ AgentState.messages::Vector{AgentMessage}
│ ▼
│ ┌──────────────────────────────────────────────────────────────┐
│ │ appendMessage(session, AgentMessage) │
│ │ Input: message::AgentMessage
│ │ Input: session::Session, message::AgentMessage │
│ │ Output: entry_id::String │
│ │ │
│ │ Steps: │
│ │ 1. Create MessageEntry: │
│ │ - type: "message"
│ │ - id: createEntryId(storage)
│ │ - parent_id: getLeafId(storage) │
│ │ - timestamp: create_timestamp() │
│ │ - message: copy(message) │
│ │ - base: SessionTreeEntryBase(type, id, leaf_id, time)
│ │ - message: the AgentMessage
│ │ 2. storage.appendEntry(entry) │
│ │ - Write JSONL line to file
│ │ - Update leaf_id
│ │ - In-memory: push to entries vector, update by_id dict
│ │ - JSONL: would append to file (TODO)
│ │ 3. Return entry.id │
│ └──────────────────────────────────────────────────────────────┘
└─► Entry stored in JSONL:
└─► Entry stored in JSONL (conceptual):
{"type":"message","id":"msg_1","parent_id":null,"timestamp":"...","message":{...}}
```
## Entry Types
## Entry Types
All entry types extend `abstract type SessionTreeEntry end` and embed a
`base::SessionTreeEntryBase` struct containing `type`, `id`, `parent_id`, and `timestamp`.
```julia
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
```
@@ -95,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
```
@@ -109,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
```
@@ -124,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
```
@@ -137,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
```
@@ -163,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
@@ -181,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
```
@@ -196,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
```
@@ -214,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
```
@@ -229,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
```
@@ -243,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
```
@@ -259,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
```
@@ -355,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
```
@@ -364,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
```
@@ -392,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
```
@@ -420,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,
@@ -432,8 +436,7 @@ function appendCompaction(
details,
usage,
from_hook,
)
return appendTypedEntry(session, entry)
))
end
```
@@ -445,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
```
@@ -482,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)
@@ -497,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
@@ -517,20 +547,21 @@ function defaultContextEntryTransform(
return copy(path_entries)
end
# Include compaction entry
entries = [compaction]
entries::Vector{SessionTreeEntry} = [compaction]
compaction_idx = findfirst(
(entry) -> entry isa CompactionEntry && entry.id == compaction.id,
path_entries,
)
# Include retained tail if present
if !isnothing(compaction.retained_tail)
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
append!(entries, path_entries[compaction_idx+1:end])
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
@@ -542,9 +573,24 @@ function defaultContextEntryTransform(
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
@@ -557,7 +603,7 @@ 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]
@@ -594,7 +640,6 @@ function sessionEntryToContextMessages(
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)
@@ -648,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?
@@ -679,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
@@ -735,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
@@ -756,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",
@@ -771,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
+200 -658
View File
@@ -1,465 +1,187 @@
# AgentCore.jl - Tools Deep Dive
## Tool Architecture with Data Flow
## Tool Types (from types.jl)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Tool Layer │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ AgentTool │
│ - name: String (identifier) │
│ - label: String (display name) │
│ - description: String (what it does) │
│ - parameters::Any (JSON schema or type) │
│ - execute::Function (main logic) │
│ - prepare_arguments::Union{Function, Nothing} │
│ - execution_mode::Union{ToolExecutionMode, Nothing} │
└─────────────────────────────────────────────────────────────────────────────┘
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ BashTool │ │ ReadTool │ │ WriteTool │
│ - bash() │ │ - read() │ │ - write() │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐
│ EditTool │
│ - edit() │
└─────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Tool Execution Data Flow │
└─────────────────────────────────────────────────────────────────────────────┘
Input: AssistantMessage (from LLM)
content::Vector{MessageContent}
└─ Contains: TextContent[] and ToolCall[]
┌─────────────────────────────────────────────────────────────────────┐
│ extract ToolCalls │
│ filter(c -> c isa ToolCall, assistant_message.content) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ ToolCall Type │
│ • type::String ("tool") │
│ • id::String (unique identifier) │
│ • name::String (tool name to execute) │
│ • arguments::Dict{String, Any} (JSON-like arguments) │
│ • partial_json::Union{String, Nothing} │
└─────────────────────────────────────────────────────────────────────┘
├─► prepareToolCall()
│ Input: tool_call::ToolCall
│ Output: Union{PreparedToolCall, ImmediateToolCallOutcome}
│ Steps:
│ 1. Find tool by name in context.tools
│ 2. before_tool_call hook (optional)
│ Input: BeforeToolCallContext
│ Output: BeforeToolCallResult (block, reason)
│ 3. prepareToolCallArguments() (optional)
│ Input: tool_call.arguments::Dict{String, Any}
│ Output: prepared_arguments::Any
│ 4. validateToolArguments()
│ Input: prepared_tool_call.arguments
│ Output: validated_args::Any
│ 5. Return: PreparedToolCall(kind, tool_call, tool, args)
├─► executePreparedToolCall() (if prepared)
│ Input: PreparedToolCall
│ Output: ExecutedToolCallOutcome
│ tool.execute(tool_call.id, args, signal, on_update)
│ Input: tool_call_id::String
│ args::Any
│ signal::Union{Any, Nothing}
│ on_update::Function (streaming updates)
│ Output: AgentToolResultMutable
│ • content::Vector{MessageContent}
│ • details::Any
│ • usage::Union{Usage, Nothing}
│ • terminate::Union{Bool, Nothing}
├─► finalizeExecutedToolCall()
│ Input: ExecutedToolCallOutcome
│ Output: FinalizedToolCallOutcome
│ Steps:
│ 1. after_tool_call hook (optional)
│ Input: AfterToolCallContext
│ Output: AfterToolCallResult (patches)
│ 2. Apply patches to result
│ 3. Return: FinalizedToolCallOutcome(tool_call, result, is_error)
└─► createToolResultMessage()
Input: FinalizedToolCallOutcome
Output: ToolResultMessage
• role: "toolResult"
• tool_call_id::String (matches ToolCall.id)
• tool_name::String (matches ToolCall.name)
• content::Vector{MessageContent}
• details::Any
• usage::Union{Usage, Nothing}
• added_tool_names::Union{Vector{String}, Nothing}
• is_error::Bool
• timestamp::Timestamp (Int64)
┌─────────────────────────────────────────────────────────────────────┐
│ ToolResultMessage[] (one per ToolCall) │
└─────────────────────────────────────────────────────────────────────┘
├─► Append to context.messages (AgentState.messages)
└─► Next turn: LLM sees tool results as input
```
## Built-in Tools
## Built-in Tools
### 1. BashTool
### AgentTool (struct)
```julia
struct BashToolOptions{TContext}
command_prefix::Union{String, Nothing}
prepare::Union{BashPrepare{TContext}, Nothing}
struct AgentTool{TParameters, TDetails}
name::String # tool identifier
label::String # display name
description::String # what it does
parameters::TParameters # JSON schema or type
execute::Function # (tool_call_id, params, signal, on_update, context) -> AgentToolResult
prepare_arguments::Union{Function, Nothing}
execution_mode::Union{ToolExecutionMode, Nothing}
end
```
### AgentToolResult (struct)
```julia
struct AgentToolResult{T}
content::Vector{MessageContent}
details::T
usage::Union{Usage, Nothing}
added_tool_names::Union{Vector{String}, Nothing}
terminate::Union{Bool, Nothing}
end
```
### ToolCall (struct)
```julia
struct ToolCall
type::String # always "tool"
id::String # unique identifier
name::String # tool name to execute
arguments::Dict{String, Any} # JSON-like arguments
partial_json::Union{String, Nothing}
end
```
### ToolExecutionMode (enum)
```julia
@enum ToolExecutionMode begin
EXECUTION_SEQUENTIAL = "sequential"
EXECUTION_PARALLEL = "parallel"
end
```
## Tool Execution Flow
```
AssistantMessage (from LLM)
content::Vector{MessageContent}
└─ Contains: TextContent[] and ToolCall[]
Agent.execute() (in agent.jl)
└─ before_tool_call hook (Agent.before_tool_call, optional)
Input: BeforeToolCallContext
Output: BeforeToolCallResult (block, reason)
For each ToolCall:
tool = find_tool(name)
tool.execute(tool_call_id, args, signal, on_update, context)
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 (`tools/bash.jl`)
```julia
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)
mutable struct ReadToolDetails
truncation::Union{Any, Nothing}
end
# 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 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
@@ -475,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
@@ -521,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
@@ -568,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
@@ -769,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
File diff suppressed because it is too large Load Diff
+321 -490
View File
File diff suppressed because it is too large Load Diff
+34 -20
View File
@@ -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
@@ -193,20 +204,23 @@ Message (for LLM API)
├── is_error::Bool
└── timestamp::Timestamp
AgentMessage (internal, extends Message)
AgentMessage (internal, abstract type)
├── UserMessage (same as above)
├── AssistantMessage (same as above)
├── ToolResultMessage (same as above)
├── ToolResultMessage (same as above, plus: role, added_tool_names)
├── BashExecutionMessage (custom)
│ ├── role, command, output, exit_code
│ ├── cancelled, truncated, exclude_from_context
│ └── timestamp
│ ├── cancelled, truncated, full_output_path, timestamp
│ └── exclude_from_context
├── CompactionSummaryMessage (custom)
│ ├── summary, tokens_before, timestamp
│ ├── role, summary, tokens_before, timestamp
│ └── converted to UserMessage for LLM
── BranchSummaryMessage (custom)
├── summary, from_id, 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
@@ -427,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
@@ -451,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)
```
@@ -460,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