update
This commit is contained in:
@@ -36,8 +36,8 @@ end
|
|||||||
# Create agent with options
|
# Create agent with options
|
||||||
agent = Agent(Dict{Symbol, Any}(
|
agent = Agent(Dict{Symbol, Any}(
|
||||||
:systemPrompt => "You are a helpful assistant",
|
:systemPrompt => "You are a helpful assistant",
|
||||||
:model => Model(...),
|
:model => Model("", "", "unknown", "unknown", "", false, String[], ModelCost(0.0, 0.0, 0.0, 0.0), 0, 0),
|
||||||
:thinkingLevel => THINKING_MEDIUM,
|
:thinkingLevel => THINKING_OFF,
|
||||||
:tools => [bash_tool, read_tool],
|
:tools => [bash_tool, read_tool],
|
||||||
:steeringMode => QUEUE_ONE_AT_A_TIME,
|
:steeringMode => QUEUE_ONE_AT_A_TIME,
|
||||||
:followUpMode => QUEUE_ONE_AT_A_TIME,
|
:followUpMode => QUEUE_ONE_AT_A_TIME,
|
||||||
@@ -290,7 +290,7 @@ agent = Agent(Dict(:transformContext => myTransformContext))
|
|||||||
# Hook before tool execution
|
# Hook before tool execution
|
||||||
function myBeforeToolCall(context, signal)
|
function myBeforeToolCall(context, signal)
|
||||||
println("About to execute: $(context.tool_call.name)")
|
println("About to execute: $(context.tool_call.name)")
|
||||||
return nothing # Return block=true to prevent execution
|
return BeforeToolCallResult(nothing, nothing) # Return BeforeToolCallResult(true, "reason") to block
|
||||||
end
|
end
|
||||||
|
|
||||||
agent = Agent(Dict(:beforeToolCall => myBeforeToolCall))
|
agent = Agent(Dict(:beforeToolCall => myBeforeToolCall))
|
||||||
@@ -303,8 +303,11 @@ agent = Agent(Dict(:beforeToolCall => myBeforeToolCall))
|
|||||||
function myAfterToolCall(context, signal)
|
function myAfterToolCall(context, signal)
|
||||||
# Can modify tool result
|
# Can modify tool result
|
||||||
return AfterToolCallResult(
|
return AfterToolCallResult(
|
||||||
content = context.result.content,
|
context.result.content,
|
||||||
terminate = context.result.terminate
|
context.result.details,
|
||||||
|
nothing,
|
||||||
|
nothing,
|
||||||
|
context.result.terminate
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -319,9 +322,9 @@ function myPrepareNextTurn(context, signal)
|
|||||||
# context: PrepareNextTurnContext
|
# context: PrepareNextTurnContext
|
||||||
# Returns AgentLoopTurnUpdate or nothing
|
# Returns AgentLoopTurnUpdate or nothing
|
||||||
return AgentLoopTurnUpdate(
|
return AgentLoopTurnUpdate(
|
||||||
context = context.context,
|
context.context, # context
|
||||||
model = context.context.model, # Can change model
|
context.context.model, # model - can change
|
||||||
thinking_level = THINKING_HIGH # Can change thinking level
|
THINKING_HIGH # thinking_level - can change
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -334,11 +337,11 @@ agent = Agent(Dict(:prepareNextTurn => myPrepareNextTurn))
|
|||||||
# Check if agent is busy
|
# Check if agent is busy
|
||||||
if !isnothing(agent.active_run)
|
if !isnothing(agent.active_run)
|
||||||
# Agent is processing
|
# Agent is processing
|
||||||
abort(agent) # Abort current run
|
abort(agent) # Abort current run (NOTE: implementation is a TODO stub)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Wait for completion
|
# Wait for completion
|
||||||
wait_for_idle(agent) # Returns Promise
|
waitForIdle(agent) # Returns Promise
|
||||||
```
|
```
|
||||||
|
|
||||||
## Complete Example
|
## Complete Example
|
||||||
@@ -367,7 +370,7 @@ end
|
|||||||
prompt(agent, "What's in the current directory?")
|
prompt(agent, "What's in the current directory?")
|
||||||
|
|
||||||
# 4. Wait for completion
|
# 4. Wait for completion
|
||||||
wait_for_idle(agent)
|
waitForIdle(agent)
|
||||||
|
|
||||||
# 5. Check final state
|
# 5. Check final state
|
||||||
state = get_state(agent)
|
state = get_state(agent)
|
||||||
@@ -375,7 +378,7 @@ println("Total messages: $(length(state.messages))")
|
|||||||
|
|
||||||
# 6. Continue with steering
|
# 6. Continue with steering
|
||||||
steer(agent, UserMessage(...))
|
steer(agent, UserMessage(...))
|
||||||
wait_for_idle(agent)
|
waitForIdle(agent)
|
||||||
|
|
||||||
# 7. Clean up
|
# 7. Clean up
|
||||||
unsubscribe() # Stop listening
|
unsubscribe() # Stop listening
|
||||||
|
|||||||
+212
-161
@@ -53,19 +53,22 @@ agentLoopContinue()
|
|||||||
│ Output: N/A (writes to new_messages and context.messages) │
|
│ Output: N/A (writes to new_messages and context.messages) │
|
||||||
│ │
|
│ │
|
||||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||||
│ │ while true: │ │
|
│ │ while true (outer loop: follow-up messages) │ │
|
||||||
│ │ 1. Get steering/follow-up messages (if any) │ │
|
│ │ has_more_tool_calls = true │ │
|
||||||
│ │ 2. Emit messages as UserMessage │ │
|
│ │ while has_more_tool_calls || !isempty(pending_messages) │ │
|
||||||
│ │ 3. streamAssistantResponse() │ │
|
│ │ 1. Emit TurnStartEvent (on subsequent turns) │ │
|
||||||
│ │ - Input: context.messages::Vector{AgentMessage} │ │
|
│ │ 2. If pending_messages: emit MessageStart/End, drain queue │ │
|
||||||
│ │ - Output: message::AssistantMessage │ │
|
│ │ 3. streamAssistantResponse() │ │
|
||||||
│ │ 4. Execute tool calls (sequential or parallel) │ │
|
│ │ 4. If error/aborted: emit TurnEnd, AgentEnd, return │ │
|
||||||
│ │ - Input: AssistantMessage with ToolCall[] │ │
|
│ │ 5. Execute tool calls (sequential or parallel) │ │
|
||||||
│ │ - Output: tool_results::Vector{ToolResultMessage} │ │
|
│ │ 6. has_more_tool_calls = !batch.terminate │ │
|
||||||
│ │ 5. Emit TurnEndEvent │ │
|
│ │ 7. Emit TurnEndEvent │ │
|
||||||
│ │ 6. prepare_next_turn (optional) │ │
|
│ │ 8. prepare_next_turn (optional config update) │ │
|
||||||
│ │ 7. should_stop_after_turn? (check termination) │ │
|
│ │ 9. should_stop_after_turn? (early return) │ │
|
||||||
│ │ 8. Loop continues if not terminated │ │
|
│ │ 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_api_key::Union{Function, Nothing}
|
||||||
get_steering_messages::Union{Function, Nothing}
|
get_steering_messages::Union{Function, Nothing}
|
||||||
get_follow_up_messages::Union{Function, Nothing}
|
get_follow_up_messages::Union{Function, Nothing}
|
||||||
|
should_stop_after_turn::Union{Function, Nothing}
|
||||||
|
max_tokens::Union{Int64, Nothing}
|
||||||
|
temperature::Union{Float64, Nothing}
|
||||||
|
cache_retention::Union{String, Nothing}
|
||||||
|
headers::Union{Dict{String, String}, Nothing}
|
||||||
|
metadata::Union{Dict{String, Any}, Nothing}
|
||||||
|
signal::Union{Any, Nothing}
|
||||||
|
api_key::Union{String, Nothing}
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- `should_stop_after_turn(context::PrepareNextTurnContext)::Bool` — Default returns `false`. Use to implement custom termination logic (e.g., max turns, tool-specific termination).
|
||||||
|
- `max_tokens`, `temperature`, `cache_retention` — Passed through to the LLM API provider.
|
||||||
|
- `signal`, `api_key` — Per-request overrides for abort handling and authentication.
|
||||||
|
- `headers`, `metadata` — Passed through to the LLM API provider.
|
||||||
|
|
||||||
## Main Functions
|
## Main Functions
|
||||||
|
|
||||||
### agentLoop()
|
### agentLoop()
|
||||||
@@ -236,11 +253,13 @@ function runAgentLoop(
|
|||||||
**Purpose**: Execute agent loop with initial prompts
|
**Purpose**: Execute agent loop with initial prompts
|
||||||
|
|
||||||
**Flow**:
|
**Flow**:
|
||||||
1. Copy prompts to new_messages
|
1. Copy prompts to `new_messages`
|
||||||
2. Append prompts to context.messages
|
2. Create `current_context` with prompts appended to `context.messages`
|
||||||
3. Emit AgentStartEvent
|
3. Emit `AgentStartEvent`
|
||||||
4. For each prompt: emit MessageStartEvent, MessageEndEvent
|
4. Emit `TurnStartEvent`
|
||||||
5. Call runLoop()
|
5. For each prompt: emit `MessageStartEvent`, `MessageEndEvent`
|
||||||
|
6. Call `runLoop()` — handles the main loop, tool execution, and termination
|
||||||
|
7. Return `new_messages`
|
||||||
|
|
||||||
### runLoop() - The Heart of AgentLoop
|
### runLoop() - The Heart of AgentLoop
|
||||||
|
|
||||||
@@ -255,104 +274,119 @@ function runLoop(
|
|||||||
)::Nothing
|
)::Nothing
|
||||||
```
|
```
|
||||||
|
|
||||||
**Main Loop**:
|
**Main Loop** (simplified — shows structure; actual code has type annotations):
|
||||||
```julia
|
```julia
|
||||||
current_context = initial_context
|
current_context = initial_context
|
||||||
config = initial_config
|
config = initial_config
|
||||||
first_turn = true
|
first_turn = true
|
||||||
pending_messages = get_steering_messages()
|
pending_messages = get_steering_messages(config)
|
||||||
|
|
||||||
while true
|
while true
|
||||||
# Process steering/follow-up messages
|
has_more_tool_calls = true
|
||||||
while !isempty(pending_messages)
|
|
||||||
|
# Inner loop: process pending messages AND/OR tool results
|
||||||
|
while has_more_tool_calls || !isempty(pending_messages)
|
||||||
if !first_turn
|
if !first_turn
|
||||||
emit(TurnStartEvent())
|
emit(TurnStartEvent())
|
||||||
else
|
else
|
||||||
first_turn = false
|
first_turn = false
|
||||||
end
|
end
|
||||||
|
|
||||||
# Emit pending messages
|
# Emit pending messages (steering / follow-up)
|
||||||
for message in pending_messages
|
if !isempty(pending_messages)
|
||||||
emit(MessageStartEvent(message))
|
for message in pending_messages
|
||||||
emit(MessageEndEvent(message))
|
emit(MessageStartEvent(message))
|
||||||
push!(current_context.messages, message)
|
emit(MessageEndEvent(message))
|
||||||
push!(new_messages, message)
|
push!(current_context.messages, message)
|
||||||
|
push!(new_messages, message)
|
||||||
|
end
|
||||||
|
pending_messages = AgentMessage[]
|
||||||
end
|
end
|
||||||
|
|
||||||
pending_messages = []
|
# Stream assistant response
|
||||||
end
|
message = streamAssistantResponse(
|
||||||
|
current_context, config, signal, emit, stream_function
|
||||||
|
)
|
||||||
|
push!(new_messages, message)
|
||||||
|
|
||||||
# Stream assistant response
|
# Early exit on error/abort
|
||||||
message = streamAssistantResponse(
|
if message.stop_reason in ("error", "aborted")
|
||||||
current_context,
|
emit(TurnEndEvent(message, ToolResultMessage[]))
|
||||||
config,
|
emit(AgentEndEvent(new_messages))
|
||||||
signal,
|
return
|
||||||
emit,
|
end
|
||||||
stream_function,
|
|
||||||
)
|
|
||||||
push!(new_messages, message)
|
|
||||||
|
|
||||||
# Check for errors
|
# Execute tool calls (if any)
|
||||||
if message.stop_reason in ("error", "aborted")
|
tool_calls = filter(c -> c isa ToolCall, message.content)
|
||||||
emit(TurnEndEvent(message, []))
|
tool_results = ToolResultMessage[]
|
||||||
emit(AgentEndEvent(new_messages))
|
has_more_tool_calls = false
|
||||||
return
|
if !isempty(tool_calls)
|
||||||
end
|
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
|
emit(TurnEndEvent(message, tool_results))
|
||||||
tool_calls = filter(c -> c isa ToolCall, message.content)
|
|
||||||
tool_results = []
|
|
||||||
has_more_tool_calls = false
|
|
||||||
|
|
||||||
if !isempty(tool_calls)
|
# Optional: prepare next turn (model/thinking/context changes)
|
||||||
executed_batch = if message.stop_reason == "length"
|
next_turn_context = PrepareNextTurnContext(
|
||||||
failToolCallsFromTruncatedMessage(tool_calls, emit)
|
message, tool_results, current_context, new_messages
|
||||||
else
|
)
|
||||||
executeToolCalls(
|
next_turn_snapshot = prepare_next_turn(config, next_turn_context)
|
||||||
current_context,
|
if !isnothing(next_turn_snapshot)
|
||||||
message,
|
current_context = next_turn_snapshot.context
|
||||||
config,
|
# Rebuild config with updated model/thinking + preserved fields
|
||||||
signal,
|
config = AgentLoopConfig(
|
||||||
emit,
|
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
|
end
|
||||||
append!(tool_results, executed_batch.messages)
|
|
||||||
has_more_tool_calls = !executed_batch.terminate
|
|
||||||
|
|
||||||
for result in tool_results
|
# Check termination
|
||||||
push!(current_context.messages, result)
|
if should_stop_after_turn(config, next_turn_context)
|
||||||
push!(new_messages, result)
|
emit(AgentEndEvent(new_messages))
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Get next steering messages
|
||||||
|
pending_messages = get_steering_messages(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
emit(TurnEndEvent(message, tool_results))
|
# Check follow-up messages (processed only after all tool calls complete)
|
||||||
|
follow_up_messages = get_follow_up_messages(config)
|
||||||
# Prepare next turn (optional)
|
|
||||||
next_turn_context = PrepareNextTurnContext(
|
|
||||||
message, tool_results, current_context, new_messages
|
|
||||||
)
|
|
||||||
next_turn_snapshot = prepare_next_turn(config, next_turn_context)
|
|
||||||
|
|
||||||
if !isnothing(next_turn_snapshot)
|
|
||||||
current_context = next_turn_snapshot.context
|
|
||||||
config = AgentLoopConfig(
|
|
||||||
model = next_turn_snapshot.model,
|
|
||||||
reasoning = next_turn_snapshot.thinking_level,
|
|
||||||
# ... other config fields
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Check if should stop
|
|
||||||
if should_stop_after_turn(config, next_turn_context)
|
|
||||||
emit(AgentEndEvent(new_messages))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# Get next pending messages
|
|
||||||
pending_messages = get_steering_messages()
|
|
||||||
|
|
||||||
# Check follow-up messages
|
|
||||||
follow_up_messages = get_follow_up_messages()
|
|
||||||
if !isempty(follow_up_messages)
|
if !isempty(follow_up_messages)
|
||||||
pending_messages = follow_up_messages
|
pending_messages = follow_up_messages
|
||||||
continue
|
continue
|
||||||
@@ -422,7 +456,7 @@ function executeToolCalls(
|
|||||||
current_context::AgentContext,
|
current_context::AgentContext,
|
||||||
assistant_message::AssistantMessage,
|
assistant_message::AssistantMessage,
|
||||||
config::AgentLoopConfig,
|
config::AgentLoopConfig,
|
||||||
signal::Union{Nothing, AbortSignal>,
|
signal::Union{Nothing, AbortSignal},
|
||||||
emit::AgentEventSink,
|
emit::AgentEventSink,
|
||||||
)::ExecutedToolCallBatch
|
)::ExecutedToolCallBatch
|
||||||
```
|
```
|
||||||
@@ -572,7 +606,7 @@ Input: tool_call::ToolCall
|
|||||||
```julia
|
```julia
|
||||||
function executePreparedToolCall(
|
function executePreparedToolCall(
|
||||||
prepared::PreparedToolCall,
|
prepared::PreparedToolCall,
|
||||||
signal::Union{Nothing, AbortSignal>,
|
signal::Union{Nothing, AbortSignal},
|
||||||
emit::AgentEventSink,
|
emit::AgentEventSink,
|
||||||
)::ExecutedToolCallOutcome
|
)::ExecutedToolCallOutcome
|
||||||
```
|
```
|
||||||
@@ -590,14 +624,18 @@ Input: prepared::PreparedToolCall
|
|||||||
args::Any
|
args::Any
|
||||||
signal::Union{Any, Nothing}
|
signal::Union{Any, Nothing}
|
||||||
on_update::Function (partial_result → void)
|
on_update::Function (partial_result → void)
|
||||||
Output: AgentToolResultMutable
|
Output: AgentToolResultMutable (defined in agent_loop.jl)
|
||||||
- content::Vector{MessageContent}
|
- content::Vector{MessageContent}
|
||||||
- details::Any
|
- details::Any
|
||||||
- usage::Union{Usage, Nothing}
|
- usage::Union{Usage, Nothing}
|
||||||
- added_tool_names::Union{Vector{String}, Nothing}
|
- added_tool_names::Union{Vector{String}, Nothing}
|
||||||
- terminate::Union{Bool, Nothing}
|
- terminate::Union{Bool, Nothing}
|
||||||
↓
|
↓
|
||||||
Collect update events from on_update callbacks
|
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)
|
Return: ExecutedToolCallOutcome(result, is_error=false)
|
||||||
- result::AgentToolResultMutable
|
- result::AgentToolResultMutable
|
||||||
@@ -612,7 +650,7 @@ function finalizeExecutedToolCall(
|
|||||||
prepared::PreparedToolCall,
|
prepared::PreparedToolCall,
|
||||||
executed::ExecutedToolCallOutcome,
|
executed::ExecutedToolCallOutcome,
|
||||||
config::AgentLoopConfig,
|
config::AgentLoopConfig,
|
||||||
signal::Union{Nothing, AbortSignal>,
|
signal::Union{Nothing, AbortSignal},
|
||||||
)::FinalizedToolCallOutcome
|
)::FinalizedToolCallOutcome
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -632,18 +670,23 @@ Input: executed::ExecutedToolCallOutcome
|
|||||||
is_error,
|
is_error,
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
Output: AfterToolCallResult (optional patches)
|
Output: AfterToolCallResult (optional patches)
|
||||||
- content::Union{Vector{MessageContent}, Nothing}
|
- content::Union{Vector{MessageContent}, Nothing}
|
||||||
- details::Union{Any, Nothing}
|
- details::Union{Any, Nothing}
|
||||||
- is_error::Union{Bool, Nothing}
|
- is_error::Union{Bool, Nothing}
|
||||||
- usage::Union{Usage, Nothing}
|
- usage::Union{Usage, Nothing}
|
||||||
- terminate::Union{Bool, Nothing}
|
- terminate::Union{Bool, Nothing}
|
||||||
↓
|
↓
|
||||||
Apply patches to result (if any)
|
Apply patches to result (if patches not nothing, replace non-nothing fields)
|
||||||
result.content = result.content ∪ patches.content
|
result = AgentToolResultMutable(
|
||||||
result.details = result.details ∪ patches.details
|
patches.content != nothing ? patches.content : result.content,
|
||||||
is_error = is_error ∪ patches.is_error
|
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
|
Return: FinalizedToolCallOutcome
|
||||||
- tool_call::ToolCall (original)
|
- tool_call::ToolCall (original)
|
||||||
- result::AgentToolResultMutable (final)
|
- result::AgentToolResultMutable (final)
|
||||||
@@ -699,45 +742,39 @@ Input: finalized::FinalizedToolCallOutcome
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
│ Sequential Execution Flow │
|
│ Sequential Execution Flow (Strict Order) │
|
||||||
└─────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
┌──────┐
|
TC1 TC2 TC3
|
||||||
│ TC1 │ ──► prepareToolCall()
|
│ │ │
|
||||||
└──────┘ │
|
▼ ▼ ▼
|
||||||
▼
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||||
┌──────────────┐
|
│ prepareToolCall()│───▶│ prepareToolCall()│───▶│ prepareToolCall()│
|
||||||
│ execute() │ ──► Wait for completion
|
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||||
└──────────────┘ │
|
│ │ │
|
||||||
│ ▼
|
▼ ▼ ▼
|
||||||
├───────────── createToolResultMessage()
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||||
│ │
|
│ execute() │ │ execute() │ │ execute() │
|
||||||
▼ ▼
|
│ (blocking) │ │ (blocking) │ │ (blocking) │
|
||||||
┌──────────────┐ ┌──────────┐
|
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||||
│ TC2 │ ──► │ │ Result1 │
|
│ │ │
|
||||||
└──────┘ └──────────┘
|
▼ ▼ ▼
|
||||||
│
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||||
▼
|
│ finalize() │ │ finalize() │ │ finalize() │
|
||||||
┌──────────────┐
|
│ + emit events │ │ + emit events │ │ + emit events │
|
||||||
│ execute() │
|
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||||
└──────────────┘
|
│ │ │
|
||||||
│
|
▼ ▼ ▼
|
||||||
▼
|
Result1 Result2 Result3
|
||||||
┌──────────────┐
|
│ │ │
|
||||||
│ TC3 │ ──► │
|
└───────────────────────┴───────────────────────┘
|
||||||
└──────┘ │
|
│
|
||||||
│ ▼
|
▼
|
||||||
├───── createToolResultMessage()
|
┌────────────────────────┐
|
||||||
│ │
|
│ ExecutedToolCallBatch │
|
||||||
▼ ▼
|
│ (Result1, Result2, │
|
||||||
┌──────────────┐ ┌──────────┐
|
│ Result3, terminate) │
|
||||||
│ execute() │ │ │ Result2 │
|
└────────────────────────┘
|
||||||
└──────────────┘ └──────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌──────────┐
|
|
||||||
│ Result3 │
|
|
||||||
└──────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Parallel Execution
|
### Parallel Execution
|
||||||
@@ -948,16 +985,16 @@ ToolCall (in AssistantMessage.content)
|
|||||||
### 3. Turn Termination
|
### 3. Turn Termination
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
# Turn ends when:
|
# The outer while-true loop exits when:
|
||||||
# 1. No more pending messages
|
# 1. No pending messages AND no tool results to reprocess (inner loop ends)
|
||||||
# 2. No more tool calls to execute
|
# 2. No follow-up messages to queue
|
||||||
# 3. should_stop_after_turn() returns true
|
# 3. should_stop_after_turn() returns true (checked after each tool-call batch)
|
||||||
|
|
||||||
# Reasons to stop:
|
# Termination conditions:
|
||||||
# - Max turns reached
|
# - message.stop_reason in ("error", "aborted") → immediate return
|
||||||
# - Tool returned terminate=true
|
# - should_stop_after_turn() hook returns true → return AgentEndEvent
|
||||||
# - Error or abort
|
# - tool result batch has terminate=true → has_more_tool_calls = false, exit inner loop
|
||||||
# - Steering/follow-up queues empty
|
# - No pending messages, no follow-up messages → break outer loop
|
||||||
```
|
```
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
@@ -977,6 +1014,12 @@ using AgentCore
|
|||||||
config = AgentLoopConfig(
|
config = AgentLoopConfig(
|
||||||
model = my_model,
|
model = my_model,
|
||||||
reasoning = THINKING_MEDIUM,
|
reasoning = THINKING_MEDIUM,
|
||||||
|
session_id = nothing,
|
||||||
|
on_payload = nothing,
|
||||||
|
on_response = nothing,
|
||||||
|
transport = "auto",
|
||||||
|
thinking_budgets = nothing,
|
||||||
|
max_retry_delay_ms = nothing,
|
||||||
tool_execution = EXECUTION_PARALLEL,
|
tool_execution = EXECUTION_PARALLEL,
|
||||||
before_tool_call = myBeforeToolCallHook,
|
before_tool_call = myBeforeToolCallHook,
|
||||||
after_tool_call = myAfterToolCallHook,
|
after_tool_call = myAfterToolCallHook,
|
||||||
@@ -986,6 +1029,14 @@ config = AgentLoopConfig(
|
|||||||
get_api_key = myGetApiKey,
|
get_api_key = myGetApiKey,
|
||||||
get_steering_messages = myGetSteeringMessages,
|
get_steering_messages = myGetSteeringMessages,
|
||||||
get_follow_up_messages = myGetFollowUpMessages,
|
get_follow_up_messages = myGetFollowUpMessages,
|
||||||
|
should_stop_after_turn = myShouldStopHook, # Default: always return false
|
||||||
|
max_tokens = nothing,
|
||||||
|
temperature = nothing,
|
||||||
|
cache_retention = nothing,
|
||||||
|
headers = nothing,
|
||||||
|
metadata = nothing,
|
||||||
|
signal = nothing,
|
||||||
|
api_key = nothing,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start agent loop
|
# Start agent loop
|
||||||
|
|||||||
@@ -248,6 +248,9 @@ struct ToolResultMessage <: Message
|
|||||||
is_error::Bool # True if tool execution failed
|
is_error::Bool # True if tool execution failed
|
||||||
timestamp::Timestamp
|
timestamp::Timestamp
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Note: AgentToolResult{T} (types.jl) - generic result type with type param T
|
||||||
|
# AgentToolResultMutable (agent_loop.jl) - mutable variant used internally
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage**:
|
**Usage**:
|
||||||
@@ -288,6 +291,8 @@ end
|
|||||||
- `prepare_arguments`: Optional preprocessing
|
- `prepare_arguments`: Optional preprocessing
|
||||||
- `execution_mode`: Sequential or parallel
|
- `execution_mode`: Sequential or parallel
|
||||||
|
|
||||||
|
**Note:** `AgentHarnessTool` (`harness_types.jl:91`) is a harness-specific variant with the same structure but uses camelCase field names (`prepareArguments`, `executionMode`) and includes additional type parameters `{TContext, TParameters, TDetails}`.
|
||||||
|
|
||||||
### Tool Execution Function Signature
|
### Tool Execution Function Signature
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
@@ -297,12 +302,12 @@ execute::Function(
|
|||||||
signal::Union{Any, Nothing}, # Abort signal
|
signal::Union{Any, Nothing}, # Abort signal
|
||||||
on_update::Function, # Callback for streaming updates
|
on_update::Function, # Callback for streaming updates
|
||||||
context::Any, # Tool context
|
context::Any, # Tool context
|
||||||
)::AgentToolResult
|
)::AgentToolResult{T}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Returns**:
|
**Returns** (`AgentToolResult{T}` from `types.jl`):
|
||||||
```julia
|
```julia
|
||||||
AgentToolResult(
|
AgentToolResult{T}(
|
||||||
content::Vector{MessageContent}, # Result content
|
content::Vector{MessageContent}, # Result content
|
||||||
details::T, # Tool-specific details
|
details::T, # Tool-specific details
|
||||||
usage::Union{Usage, Nothing}, # Usage statistics
|
usage::Union{Usage, Nothing}, # Usage statistics
|
||||||
@@ -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
|
## AgentContext
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
@@ -532,6 +541,32 @@ mutable struct BranchSummaryMessage
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CustomMessage
|
||||||
|
|
||||||
|
**Note:** There are two `CustomMessage` types in the codebase:
|
||||||
|
|
||||||
|
1. **Types.CustomMessage** (`types.jl:155`) - A simple wrapper that holds another `AgentMessage` with a custom type label:
|
||||||
|
```julia
|
||||||
|
struct CustomMessage <: AgentMessage
|
||||||
|
message::AgentMessage
|
||||||
|
custom_type::String
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Messages.CustomMessage{T}** (`messages.jl:42`) - A standalone mutable message with content, display flag, and details:
|
||||||
|
```julia
|
||||||
|
mutable struct CustomMessage{T}
|
||||||
|
role::String
|
||||||
|
custom_type::String
|
||||||
|
content::Union{String, Vector{MessageContent}}
|
||||||
|
display::Bool
|
||||||
|
details::Union{T, Nothing}
|
||||||
|
timestamp::Timestamp
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Only `Messages.CustomMessage{T}` is converted by `convertToLlmMessage()` to a `UserMessage`.
|
||||||
|
|
||||||
## AgentState
|
## AgentState
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
@@ -589,6 +624,8 @@ Vector{AgentMessage} (internal conversation history)
|
|||||||
│ (wrapped with COMPACTION_SUMMARY_PREFIX/SUFFIX)
|
│ (wrapped with COMPACTION_SUMMARY_PREFIX/SUFFIX)
|
||||||
│ • BranchSummaryMessage → UserMessage
|
│ • BranchSummaryMessage → UserMessage
|
||||||
│ (wrapped with BRANCH_SUMMARY_PREFIX/SUFFIX)
|
│ (wrapped with BRANCH_SUMMARY_PREFIX/SUFFIX)
|
||||||
|
│ • CustomMessage → UserMessage
|
||||||
|
│ (content field used directly, string→TextContent)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Vector{Message} (for LLM API)
|
Vector{Message} (for LLM API)
|
||||||
@@ -607,6 +644,7 @@ Vector{Message} (for LLM API)
|
|||||||
], "openai", "openai", "gpt-4", Usage(...), "done", nothing, 1234567891),
|
], "openai", "openai", "gpt-4", Usage(...), "done", nothing, 1234567891),
|
||||||
BashExecutionMessage("custom", "ls -la", "file1.md\nfile2.md\n", 0, false, false, nothing, 1234567892, false),
|
BashExecutionMessage("custom", "ls -la", "file1.md\nfile2.md\n", 0, false, false, nothing, 1234567892, false),
|
||||||
CompactionSummaryMessage("compactionSummary", "Previous conversation compacted", 1000, 1234567893),
|
CompactionSummaryMessage("compactionSummary", "Previous conversation compacted", 1000, 1234567893),
|
||||||
|
CustomMessage("custom", "someCustomType", "Some custom content", true, nothing, 1234567894),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Output: Vector{Message}
|
# Output: Vector{Message}
|
||||||
@@ -618,6 +656,7 @@ Vector{Message} (for LLM API)
|
|||||||
], "openai", "openai", "gpt-4", Usage(...), "done", nothing, 1234567891),
|
], "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("Ran `ls -la`\n```\nfile1.md\nfile2.md\n```\n")], 1234567892),
|
||||||
UserMessage("user", [TextContent("<summary>Previous conversation compacted</summary>")], 1234567893),
|
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)
|
return UserMessage("user", [TextContent(text)], m.timestamp)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function convertToLlmMessage(m::CustomMessage)::Union{UserMessage, Nothing}
|
||||||
|
content = if m.content isa String
|
||||||
|
[TextContent(m.content)]
|
||||||
|
else
|
||||||
|
m.content
|
||||||
|
end
|
||||||
|
return UserMessage("user", content, m.timestamp)
|
||||||
|
end
|
||||||
|
|
||||||
function convertToLlmMessage(m::BranchSummaryMessage)
|
function convertToLlmMessage(m::BranchSummaryMessage)
|
||||||
text = BRANCH_SUMMARY_PREFIX * m.summary * BRANCH_SUMMARY_SUFFIX
|
text = BRANCH_SUMMARY_PREFIX * m.summary * BRANCH_SUMMARY_SUFFIX
|
||||||
return UserMessage("user", [TextContent(text)], m.timestamp)
|
return UserMessage("user", [TextContent(text)], m.timestamp)
|
||||||
|
|||||||
+294
-177
@@ -27,15 +27,16 @@
|
|||||||
│ └─► storage.appendEntry() → JSONL file │
|
│ └─► storage.appendEntry() → JSONL file │
|
||||||
│ │
|
│ │
|
||||||
│ To navigate to E2 (fork point): │
|
│ To navigate to E2 (fork point): │
|
||||||
│ Session.moveTo(E2) │
|
│ session.moveTo(E2) │
|
||||||
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
|
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||||
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │────▶│ E5' │ (new branch) │
|
│ │ E1 │────▶│ E2 │────▶│ E3' │────▶│ E4' │────▶│ E5' │ (new branch) │
|
||||||
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
|
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ ▼ create BranchSummary │
|
│ │ ▼ create BranchSummary │
|
||||||
│ │ ┌─────┐ │
|
│ │ ┌─────┐ │
|
||||||
│ └──────│ E6 │ (branch summary) │
|
│ │ │ E6 │ (branch summary) │
|
||||||
│ └─────┘ │
|
│ │ └─────┘ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -49,43 +50,45 @@ AgentState.messages::Vector{AgentMessage}
|
|||||||
│ ▼
|
│ ▼
|
||||||
│ ┌──────────────────────────────────────────────────────────────┐
|
│ ┌──────────────────────────────────────────────────────────────┐
|
||||||
│ │ appendMessage(session, AgentMessage) │
|
│ │ appendMessage(session, AgentMessage) │
|
||||||
│ │ Input: message::AgentMessage │
|
│ │ Input: session::Session, message::AgentMessage │
|
||||||
│ │ Output: entry_id::String │
|
│ │ Output: entry_id::String │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ Steps: │
|
│ │ Steps: │
|
||||||
│ │ 1. Create MessageEntry: │
|
│ │ 1. Create MessageEntry: │
|
||||||
│ │ - type: "message" │
|
│ │ - base: SessionTreeEntryBase(type, id, leaf_id, time) │
|
||||||
│ │ - id: createEntryId(storage) │
|
│ │ - message: the AgentMessage │
|
||||||
│ │ - parent_id: getLeafId(storage) │
|
|
||||||
│ │ - timestamp: create_timestamp() │
|
|
||||||
│ │ - message: copy(message) │
|
|
||||||
│ │ 2. storage.appendEntry(entry) │
|
│ │ 2. storage.appendEntry(entry) │
|
||||||
│ │ - Write JSONL line to file │
|
│ │ - In-memory: push to entries vector, update by_id dict │
|
||||||
│ │ - Update leaf_id │
|
│ │ - JSONL: would append to file (TODO) │
|
||||||
│ │ 3. Return entry.id │
|
│ │ 3. Return entry.id │
|
||||||
│ └──────────────────────────────────────────────────────────────┘
|
│ └──────────────────────────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
└─► Entry stored in JSONL:
|
└─► Entry stored in JSONL (conceptual):
|
||||||
{"type":"message","id":"msg_1","parent_id":null,"timestamp":"...","message":{...}}
|
{"type":"message","id":"msg_1","parent_id":null,"timestamp":"...","message":{...}}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Entry Types
|
## Entry Types
|
||||||
|
|
||||||
## Entry Types
|
All entry types extend `abstract type SessionTreeEntry end` and embed a
|
||||||
|
`base::SessionTreeEntryBase` struct containing `type`, `id`, `parent_id`, and `timestamp`.
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
abstract type SessionTreeEntry end
|
abstract type SessionTreeEntry end
|
||||||
|
|
||||||
|
struct SessionTreeEntryBase
|
||||||
|
type::String
|
||||||
|
id::String
|
||||||
|
parent_id::Union{String, Nothing}
|
||||||
|
timestamp::String
|
||||||
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1. MessageEntry
|
### 1. MessageEntry
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct MessageEntry <: SessionTreeEntry
|
struct MessageEntry <: SessionTreeEntry
|
||||||
type::String # "message"
|
base::SessionTreeEntryBase
|
||||||
id::String # Unique entry ID
|
message::AgentMessage
|
||||||
parent_id::Union{String, Nothing}
|
|
||||||
timestamp::String # ISO 8601 timestamp
|
|
||||||
message::AgentMessage # The actual message
|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -95,11 +98,8 @@ end
|
|||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct ThinkingLevelChangeEntry <: SessionTreeEntry
|
struct ThinkingLevelChangeEntry <: SessionTreeEntry
|
||||||
type::String # "thinking_level_change"
|
base::SessionTreeEntryBase
|
||||||
id::String
|
thinking_level::String
|
||||||
parent_id::Union{String, Nothing}
|
|
||||||
timestamp::String
|
|
||||||
thinking_level::String # "off", "minimal", "low", "medium", etc.
|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -109,12 +109,9 @@ end
|
|||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct ModelChangeEntry <: SessionTreeEntry
|
struct ModelChangeEntry <: SessionTreeEntry
|
||||||
type::String # "model_change"
|
base::SessionTreeEntryBase
|
||||||
id::String
|
provider::String
|
||||||
parent_id::Union{String, Nothing}
|
model_id::String
|
||||||
timestamp::String
|
|
||||||
provider::String # "openai", "anthropic", etc.
|
|
||||||
model_id::String # Model identifier
|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -124,10 +121,7 @@ end
|
|||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct ActiveToolsChangeEntry <: SessionTreeEntry
|
struct ActiveToolsChangeEntry <: SessionTreeEntry
|
||||||
type::String # "active_tools_change"
|
base::SessionTreeEntryBase
|
||||||
id::String
|
|
||||||
parent_id::Union{String, Nothing}
|
|
||||||
timestamp::String
|
|
||||||
active_tool_names::Vector{String}
|
active_tool_names::Vector{String}
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
@@ -137,18 +131,15 @@ end
|
|||||||
### 5. CompactionEntry
|
### 5. CompactionEntry
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct CompactionEntry <: SessionTreeEntry
|
struct CompactionEntry{T} <: SessionTreeEntry
|
||||||
type::String # "compaction"
|
base::SessionTreeEntryBase
|
||||||
id::String
|
summary::String
|
||||||
parent_id::Union{String, Nothing}
|
|
||||||
timestamp::String
|
|
||||||
summary::String # Summary of compacted history
|
|
||||||
first_kept_entry_id::Union{String, Nothing}
|
first_kept_entry_id::Union{String, Nothing}
|
||||||
tokens_before::Int64 # Context size before compaction
|
tokens_before::Int64
|
||||||
retained_tail::Union{Vector{AgentMessage}, Nothing}
|
retained_tail::Union{Vector{AgentMessage}, Nothing}
|
||||||
details::Union{Any, Nothing}
|
details::Union{T, Nothing}
|
||||||
usage::Union{Usage, Nothing}
|
usage::Union{Usage, Nothing}
|
||||||
from_hook::Bool # Whether triggered by hook
|
from_hook::Bool
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -163,14 +154,11 @@ end
|
|||||||
### 6. BranchSummaryEntry
|
### 6. BranchSummaryEntry
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct BranchSummaryEntry <: SessionTreeEntry
|
struct BranchSummaryEntry{T} <: SessionTreeEntry
|
||||||
type::String # "branch_summary"
|
base::SessionTreeEntryBase
|
||||||
id::String
|
from_id::String
|
||||||
parent_id::Union{String, Nothing}
|
summary::String
|
||||||
timestamp::String
|
details::Union{T, Nothing}
|
||||||
from_id::String # Branch point entry ID
|
|
||||||
summary::String # Summary of branch history
|
|
||||||
details::Union{Any, Nothing}
|
|
||||||
usage::Union{Usage, Nothing}
|
usage::Union{Usage, Nothing}
|
||||||
from_hook::Bool
|
from_hook::Bool
|
||||||
end
|
end
|
||||||
@@ -181,13 +169,10 @@ end
|
|||||||
### 7. CustomEntry
|
### 7. CustomEntry
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct CustomEntry <: SessionTreeEntry
|
struct CustomEntry{T} <: SessionTreeEntry
|
||||||
type::String # Custom type
|
base::SessionTreeEntryBase
|
||||||
id::String
|
|
||||||
parent_id::Union{String, Nothing}
|
|
||||||
timestamp::String
|
|
||||||
custom_type::String
|
custom_type::String
|
||||||
data::Union{Any, Nothing}
|
data::Union{T, Nothing}
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -196,14 +181,11 @@ end
|
|||||||
### 8. CustomMessageEntry
|
### 8. CustomMessageEntry
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct CustomMessageEntry <: SessionTreeEntry
|
struct CustomMessageEntry{T} <: SessionTreeEntry
|
||||||
type::String
|
base::SessionTreeEntryBase
|
||||||
id::String
|
|
||||||
parent_id::Union{String, Nothing}
|
|
||||||
timestamp::String
|
|
||||||
custom_type::String
|
custom_type::String
|
||||||
content::String
|
content::String
|
||||||
details::Union{Any, Nothing}
|
details::Union{T, Nothing}
|
||||||
display::Bool
|
display::Bool
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
@@ -214,11 +196,8 @@ end
|
|||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct LabelEntry <: SessionTreeEntry
|
struct LabelEntry <: SessionTreeEntry
|
||||||
type::String
|
base::SessionTreeEntryBase
|
||||||
id::String
|
target_id::String
|
||||||
parent_id::Union{String, Nothing}
|
|
||||||
timestamp::String
|
|
||||||
target_id::String # Entry being labeled
|
|
||||||
label::Union{String, Nothing}
|
label::Union{String, Nothing}
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
@@ -229,10 +208,7 @@ end
|
|||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct SessionInfoEntry <: SessionTreeEntry
|
struct SessionInfoEntry <: SessionTreeEntry
|
||||||
type::String
|
base::SessionTreeEntryBase
|
||||||
id::String
|
|
||||||
parent_id::Union{String, Nothing}
|
|
||||||
timestamp::String
|
|
||||||
name::Union{String, Nothing}
|
name::Union{String, Nothing}
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
@@ -243,10 +219,7 @@ end
|
|||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct LeafEntry <: SessionTreeEntry
|
struct LeafEntry <: SessionTreeEntry
|
||||||
type::String
|
base::SessionTreeEntryBase
|
||||||
id::String
|
|
||||||
parent_id::Union{String, Nothing}
|
|
||||||
timestamp::String
|
|
||||||
target_id::Union{String, Nothing}
|
target_id::Union{String, Nothing}
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
@@ -259,86 +232,95 @@ end
|
|||||||
abstract type SessionStorage{T<:SessionMetadata} end
|
abstract type SessionStorage{T<:SessionMetadata} end
|
||||||
```
|
```
|
||||||
|
|
||||||
### Storage Methods
|
### Storage Methods (actual implementation signatures)
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
# Metadata
|
# Metadata
|
||||||
getMetadata(storage::SessionStorage)::Promise{T}
|
getMetadata(storage::SessionStorage)::T
|
||||||
|
|
||||||
# Leaf management
|
# Leaf management
|
||||||
getLeafId(storage::SessionStorage)::Promise{Union{String, Nothing}}
|
getLeafId(storage::SessionStorage)::Union{String, Nothing}
|
||||||
setLeafId(storage::SessionStorage, leaf_id::String)::Promise{Nothing}
|
setLeafId(storage::SessionStorage, leaf_id::Union{String, Nothing})::Nothing
|
||||||
|
|
||||||
# Entry management
|
# Entry management
|
||||||
createEntryId(storage::SessionStorage)::Promise{String}
|
createEntryId(storage::SessionStorage)::String
|
||||||
appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Promise{Nothing}
|
appendEntry(storage::SessionStorage, entry::SessionTreeEntry)::Nothing
|
||||||
getEntry(storage::SessionStorage, id::String)::Promise{Union{SessionTreeEntry, Nothing}}
|
getEntry(storage::SessionStorage, id::String)::Union{SessionTreeEntry, Nothing}
|
||||||
|
|
||||||
# Query
|
# Query
|
||||||
findEntries(storage::SessionStorage, type::String)::Promise{Vector{SessionTreeEntry}}
|
findEntries(storage::SessionStorage, type::String)::Vector{SessionTreeEntry}
|
||||||
getLabel(storage::SessionStorage, id::String)::Promise{Union{String, Nothing}}
|
getLabel(storage::SessionStorage, id::String)::Union{String, Nothing}
|
||||||
getSessionName(storage::SessionStorage)::Promise{Union{String, Nothing}}
|
getSessionName(storage::SessionStorage)::Union{String, Nothing}
|
||||||
|
|
||||||
# Branch navigation
|
# Branch navigation
|
||||||
getPathToRootOrCompaction(
|
getPathToRootOrCompaction(storage::SessionStorage, leaf_id::Union{String, Nothing})::Vector{SessionTreeEntry}
|
||||||
storage::SessionStorage,
|
getEntries(storage::SessionStorage, options::Dict{String, Any})::Vector{SessionTreeEntry}
|
||||||
leaf_id::String,
|
|
||||||
)::Promise{Vector{SessionTreeEntry}}
|
|
||||||
|
|
||||||
getEntries(storage::SessionStorage, options::Dict{String, Any})::Promise{Vector{SessionTreeEntry}}
|
|
||||||
|
|
||||||
# Stats
|
# Stats
|
||||||
getSessionStats(storage::SessionStorage)::Promise{SessionStats}
|
getSessionStats(storage::SessionStorage)::SessionStats
|
||||||
```
|
```
|
||||||
|
|
||||||
## JsonlSessionStorage
|
## JsonlSessionStorage
|
||||||
|
|
||||||
|
```
|
||||||
|
mutable struct JsonlSessionStorage{T<:SessionMetadata} <: SessionStorage{T}
|
||||||
|
file_path::String
|
||||||
|
metadata::T
|
||||||
|
entries::Vector{SessionTreeEntry} # ordered list
|
||||||
|
by_id::Dict{String, SessionTreeEntry} # fast lookup by id
|
||||||
|
labels_by_id::Dict{String, String} # label cache
|
||||||
|
current_leaf_id::Union{String, Nothing} # current branch tip
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
│ JSONL Storage Format │
|
│ JSONL Storage Format │
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
File: session.jsonl
|
File: session.jsonl (conceptual - not yet implemented)
|
||||||
|
|
||||||
Entry 1 (Metadata):
|
Entry 1 (Metadata via SessionHeader):
|
||||||
{"type":"session","id":"meta_1","created_at":"2024-01-01T00:00:00Z","cwd":"/path","path":"/path/session.jsonl"}
|
{"type":"session","version":3,"id":"meta_1","timestamp":"...","cwd":"/path","parent_session":null,"metadata":{}}
|
||||||
|
|
||||||
Entry 2 (Message):
|
Entry 2 (Message):
|
||||||
{"type":"message","id":"msg_1","parent_id":null,"timestamp":"2024-01-01T00:00:01Z","message":{"role":"user","content":[{"type":"text","text":"Hello"}]}}
|
{"type":"message","id":"msg_1","parent_id":null,"timestamp":"...","message":{"role":"user",...}}
|
||||||
|
|
||||||
Entry 3 (Thinking Level):
|
Entry 3 (Thinking Level):
|
||||||
{"type":"thinking_level_change","id":"tl_1","parent_id":"msg_1","timestamp":"2024-01-01T00:00:02Z","thinking_level":"medium"}
|
{"type":"thinking_level_change","id":"tl_1","parent_id":"msg_1","timestamp":"...","thinking_level":"medium"}
|
||||||
|
|
||||||
Entry 4 (Model Change):
|
Entry 4 (Model Change):
|
||||||
{"type":"model_change","id":"mc_1","parent_id":"tl_1","timestamp":"2024-01-01T00:00:03Z","provider":"openai","model_id":"gpt-4"}
|
{"type":"model_change","id":"mc_1","parent_id":"tl_1","timestamp":"...","provider":"openai","model_id":"gpt-4"}
|
||||||
|
|
||||||
Entry 5 (Compaction):
|
Entry 5 (Compaction):
|
||||||
{"type":"compaction","id":"comp_1","parent_id":"mc_1","timestamp":"2024-01-01T00:00:04Z","summary":"Previous messages summarized...","first_kept_entry_id":"msg_3","tokens_before":100000,"tokens_after":50000}
|
{"type":"compaction","id":"comp_1","parent_id":"mc_1","timestamp":"...","summary":"...","first_kept_entry_id":"msg_3","tokens_before":100000}
|
||||||
|
|
||||||
Entry 6 (Branch Summary):
|
Entry 6 (Branch Summary):
|
||||||
{"type":"branch_summary","id":"branch_1","parent_id":"comp_1","timestamp":"2024-01-01T00:00:05Z","from_id":"msg_3","summary":"Branch from message 3"}
|
{"type":"branch_summary","id":"branch_1","parent_id":"comp_1","timestamp":"...","from_id":"msg_3","summary":"..."}
|
||||||
|
|
||||||
Entry 7 (Active Tools):
|
Entry 7 (Active Tools):
|
||||||
{"type":"active_tools_change","id":"tools_1","parent_id":"branch_1","timestamp":"2024-01-01T00:00:06Z","active_tool_names":["bash","read"]}
|
{"type":"active_tools_change","id":"tools_1","parent_id":"branch_1","timestamp":"...","active_tool_names":["bash","read"]}
|
||||||
|
|
||||||
Entry 8 (Leaf):
|
Entry 8 (Leaf):
|
||||||
{"type":"leaf","id":"leaf_1","parent_id":"tools_1","timestamp":"2024-01-01T00:00:07Z","target_id":"msg_5"}
|
{"type":"leaf","id":"leaf_1","parent_id":"tools_1","timestamp":"...","target_id":"msg_5"}
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Each line is a JSON object (JSONL format)
|
- Each line is a JSON object (JSONL format) - TODO: file I/O not yet implemented
|
||||||
- parent_id references previous entry (linked list structure)
|
- parent_id references previous entry (linked list structure)
|
||||||
- Leaf entry points to current position in tree
|
- Leaf entry points to current position in tree
|
||||||
- To fork, create new branch from any entry
|
- To fork, create new branch from any entry
|
||||||
|
- In-memory mode uses Vector + Dict by_id for fast access
|
||||||
```
|
```
|
||||||
|
|
||||||
## InMemorySessionStorage
|
## InMemorySessionStorage
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
mutable struct InMemorySessionStorage
|
mutable struct InMemorySessionStorage{T<:SessionMetadata} <: SessionStorage{T}
|
||||||
metadata::SessionMetadata
|
metadata::T
|
||||||
|
entries::Vector{SessionTreeEntry}
|
||||||
|
by_id::Dict{String, SessionTreeEntry}
|
||||||
|
labels_by_id::Dict{String, String}
|
||||||
leaf_id::Union{String, Nothing}
|
leaf_id::Union{String, Nothing}
|
||||||
entries::Dict{String, SessionTreeEntry}
|
|
||||||
labels::Dict{String, String}
|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -355,6 +337,19 @@ end
|
|||||||
mutable struct Session{T<:SessionMetadata}
|
mutable struct Session{T<:SessionMetadata}
|
||||||
storage::SessionStorage{T}
|
storage::SessionStorage{T}
|
||||||
context_build_options::SessionContextBuildOptions
|
context_build_options::SessionContextBuildOptions
|
||||||
|
|
||||||
|
function Session(storage::SessionStorage, context_build_options=SessionContextBuildOptions(nothing, nothing))
|
||||||
|
new{typeof(storage.metadata)}(storage, context_build_options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### SessionContextBuildOptions
|
||||||
|
|
||||||
|
```julia
|
||||||
|
mutable struct SessionContextBuildOptions
|
||||||
|
entry_transforms::Union{Vector{Function}, Nothing}
|
||||||
|
entry_projectors::Union{Dict{String, Function}, Nothing}
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -364,14 +359,10 @@ end
|
|||||||
|
|
||||||
```julia
|
```julia
|
||||||
function appendMessage(session::Session, message::AgentMessage)::String
|
function appendMessage(session::Session, message::AgentMessage)::String
|
||||||
entry = MessageEntry(
|
return appendTypedEntry(session, MessageEntry(
|
||||||
"message",
|
SessionTreeEntryBase("message", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
|
||||||
createEntryId(session.storage),
|
|
||||||
getLeafId(session.storage),
|
|
||||||
create_timestamp(),
|
|
||||||
message,
|
message,
|
||||||
)
|
))
|
||||||
return appendTypedEntry(session, entry)
|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -392,18 +383,34 @@ tool_id = appendMessage(session, ToolResultMessage(...))
|
|||||||
#### appendThinkingLevelChange()
|
#### appendThinkingLevelChange()
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function appendThinkingLevelChange(
|
function appendThinkingLevelChange(session::Session, thinking_level::String)::String
|
||||||
session::Session,
|
return appendTypedEntry(session, ThinkingLevelChangeEntry(
|
||||||
thinking_level::String,
|
SessionTreeEntryBase("thinking_level_change", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
|
||||||
)::String
|
|
||||||
entry = ThinkingLevelChangeEntry(
|
|
||||||
"thinking_level_change",
|
|
||||||
createEntryId(session.storage),
|
|
||||||
getLeafId(session.storage),
|
|
||||||
create_timestamp(),
|
|
||||||
thinking_level,
|
thinking_level,
|
||||||
)
|
))
|
||||||
return appendTypedEntry(session, entry)
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
#### appendModelChange()
|
||||||
|
|
||||||
|
```julia
|
||||||
|
function appendModelChange(session::Session, provider::String, model_id::String)::String
|
||||||
|
return appendTypedEntry(session, ModelChangeEntry(
|
||||||
|
SessionTreeEntryBase("model_change", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
|
||||||
|
provider,
|
||||||
|
model_id,
|
||||||
|
))
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
#### appendActiveToolsChange()
|
||||||
|
|
||||||
|
```julia
|
||||||
|
function appendActiveToolsChange(session::Session, active_tool_names::Vector{String})::String
|
||||||
|
return appendTypedEntry(session, ActiveToolsChangeEntry(
|
||||||
|
SessionTreeEntryBase("active_tools_change", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
|
||||||
|
active_tool_names,
|
||||||
|
))
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -420,11 +427,8 @@ function appendCompaction(
|
|||||||
usage::Union{Usage, Nothing}=nothing,
|
usage::Union{Usage, Nothing}=nothing,
|
||||||
retained_tail::Union{Vector{AgentMessage}, Nothing}=nothing,
|
retained_tail::Union{Vector{AgentMessage}, Nothing}=nothing,
|
||||||
)::String
|
)::String
|
||||||
entry = CompactionEntry(
|
return appendTypedEntry(session, CompactionEntry(
|
||||||
"compaction",
|
SessionTreeEntryBase("compaction", createEntryId(session.storage), getLeafId(session.storage), create_timestamp()),
|
||||||
createEntryId(session.storage),
|
|
||||||
getLeafId(session.storage),
|
|
||||||
create_timestamp(),
|
|
||||||
summary,
|
summary,
|
||||||
first_kept_entry_id,
|
first_kept_entry_id,
|
||||||
tokens_before,
|
tokens_before,
|
||||||
@@ -432,8 +436,7 @@ function appendCompaction(
|
|||||||
details,
|
details,
|
||||||
usage,
|
usage,
|
||||||
from_hook,
|
from_hook,
|
||||||
)
|
))
|
||||||
return appendTypedEntry(session, entry)
|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -445,25 +448,24 @@ function moveTo(
|
|||||||
entry_id::Union{String, Nothing},
|
entry_id::Union{String, Nothing},
|
||||||
summary::Union{Dict{String, Any}, Nothing}=nothing,
|
summary::Union{Dict{String, Any}, Nothing}=nothing,
|
||||||
)::Union{String, Nothing}
|
)::Union{String, Nothing}
|
||||||
# Set new leaf
|
# Validate entry exists
|
||||||
setLeafId(session.storage, entry_id)
|
if !isnothing(entry_id) && isnothing(getEntry(session, entry_id))
|
||||||
|
throw(SessionError("not_found", "Entry $(entry_id) not found"))
|
||||||
# Optionally create branch summary
|
|
||||||
if !isnothing(summary)
|
|
||||||
return appendTypedEntry(session, BranchSummaryEntry(
|
|
||||||
"branch_summary",
|
|
||||||
createEntryId(session.storage),
|
|
||||||
entry_id,
|
|
||||||
create_timestamp(),
|
|
||||||
entry_id,
|
|
||||||
summary["summary"],
|
|
||||||
get(summary, "details", nothing),
|
|
||||||
get(summary, "usage", nothing),
|
|
||||||
get(summary, "from_hook", false),
|
|
||||||
))
|
|
||||||
end
|
end
|
||||||
|
# Set new leaf (creates a LeafEntry)
|
||||||
return nothing
|
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
|
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
|
## Build Session Context
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function buildSessionContext(
|
function buildSessionContext(
|
||||||
path_entries::Vector{SessionTreeEntry},
|
path_entries::Vector{SessionTreeEntry},
|
||||||
options::SessionContextBuildOptions=SessionContextBuildOptions(),
|
options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
|
||||||
)::SessionContext
|
)::SessionContext
|
||||||
state = deriveSessionContextState(path_entries)
|
state = deriveSessionContextState(path_entries)
|
||||||
context_entries = buildContextEntries(path_entries, options)
|
context_entries = buildContextEntries(path_entries, options)
|
||||||
@@ -497,14 +505,36 @@ function buildSessionContext(
|
|||||||
end
|
end
|
||||||
return SessionContext(messages, state.thinking_level, state.model, state.active_tool_names)
|
return SessionContext(messages, state.thinking_level, state.model, state.active_tool_names)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function deriveSessionContextState(path_entries::Vector{SessionTreeEntry})::Dict{String, Any}
|
||||||
|
thinking_level = "off"
|
||||||
|
model = nothing
|
||||||
|
active_tool_names = nothing
|
||||||
|
|
||||||
|
for entry in path_entries
|
||||||
|
if entry isa ThinkingLevelChangeEntry
|
||||||
|
thinking_level = entry.thinking_level
|
||||||
|
elseif entry isa ModelChangeEntry
|
||||||
|
model = Dict("provider" => entry.provider, "modelId" => entry.model_id)
|
||||||
|
elseif entry isa MessageEntry && entry.message.role == "assistant"
|
||||||
|
model = Dict("provider" => entry.message.provider, "modelId" => entry.message.model)
|
||||||
|
elseif entry isa ActiveToolsChangeEntry
|
||||||
|
active_tool_names = copy(entry.active_tool_names)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Dict(
|
||||||
|
"thinking_level" => thinking_level,
|
||||||
|
"model" => model,
|
||||||
|
"active_tool_names" => active_tool_names,
|
||||||
|
)
|
||||||
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
### Context Entry Transform
|
### Context Entry Transform
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function defaultContextEntryTransform(
|
function defaultContextEntryTransform(path_entries::Vector{SessionTreeEntry})::Vector{SessionTreeEntry}
|
||||||
path_entries::Vector{SessionTreeEntry},
|
|
||||||
)::Vector{SessionTreeEntry}
|
|
||||||
compaction = nothing
|
compaction = nothing
|
||||||
for entry in path_entries
|
for entry in path_entries
|
||||||
if entry isa CompactionEntry
|
if entry isa CompactionEntry
|
||||||
@@ -517,20 +547,21 @@ function defaultContextEntryTransform(
|
|||||||
return copy(path_entries)
|
return copy(path_entries)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Include compaction entry
|
entries::Vector{SessionTreeEntry} = [compaction]
|
||||||
entries = [compaction]
|
compaction_idx = findfirst(
|
||||||
|
(entry) -> entry isa CompactionEntry && entry.id == compaction.id,
|
||||||
|
path_entries,
|
||||||
|
)
|
||||||
|
|
||||||
# Include retained tail if present
|
|
||||||
if !isnothing(compaction.retained_tail)
|
if !isnothing(compaction.retained_tail)
|
||||||
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
|
for i in compaction_idx+1:length(path_entries)
|
||||||
append!(entries, path_entries[compaction_idx+1:end])
|
push!(entries, path_entries[i])
|
||||||
|
end
|
||||||
return entries
|
return entries
|
||||||
end
|
end
|
||||||
|
|
||||||
# Otherwise include entries after first_kept_entry_id
|
|
||||||
if !isnothing(compaction.first_kept_entry_id)
|
if !isnothing(compaction.first_kept_entry_id)
|
||||||
found_first_kept = false
|
found_first_kept = false
|
||||||
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
|
|
||||||
for i in 1:compaction_idx-1
|
for i in 1:compaction_idx-1
|
||||||
entry = path_entries[i]
|
entry = path_entries[i]
|
||||||
if entry.id == compaction.first_kept_entry_id
|
if entry.id == compaction.first_kept_entry_id
|
||||||
@@ -542,9 +573,24 @@ function defaultContextEntryTransform(
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Include entries after compaction
|
for i in compaction_idx+1:length(path_entries)
|
||||||
compaction_idx = findfirst(e -> e.id == compaction.id, path_entries)
|
push!(entries, path_entries[i])
|
||||||
append!(entries, path_entries[compaction_idx+1:end])
|
end
|
||||||
|
|
||||||
|
return entries
|
||||||
|
end
|
||||||
|
|
||||||
|
function buildContextEntries(
|
||||||
|
path_entries::Vector{SessionTreeEntry},
|
||||||
|
options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
|
||||||
|
)::Vector{SessionTreeEntry}
|
||||||
|
entries = defaultContextEntryTransform(path_entries)
|
||||||
|
|
||||||
|
if !isnothing(options.entry_transforms)
|
||||||
|
for transform in options.entry_transforms
|
||||||
|
entries = transform(entries)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
end
|
end
|
||||||
@@ -557,7 +603,7 @@ function sessionEntryToContextMessages(
|
|||||||
entry::SessionTreeEntry,
|
entry::SessionTreeEntry,
|
||||||
index::Int64,
|
index::Int64,
|
||||||
entries::Vector{SessionTreeEntry},
|
entries::Vector{SessionTreeEntry},
|
||||||
options::SessionContextBuildOptions=SessionContextBuildOptions(),
|
options::SessionContextBuildOptions=SessionContextBuildOptions(nothing, nothing),
|
||||||
)::Vector{AgentMessage}
|
)::Vector{AgentMessage}
|
||||||
if entry isa MessageEntry
|
if entry isa MessageEntry
|
||||||
return [entry.message]
|
return [entry.message]
|
||||||
@@ -594,7 +640,6 @@ function sessionEntryToContextMessages(
|
|||||||
end
|
end
|
||||||
|
|
||||||
if entry isa CustomEntry
|
if entry isa CustomEntry
|
||||||
# Custom projectors can transform custom entries
|
|
||||||
if !isnothing(options.entry_projectors) && haskey(options.entry_projectors, entry.custom_type)
|
if !isnothing(options.entry_projectors) && haskey(options.entry_projectors, entry.custom_type)
|
||||||
projector = options.entry_projectors[entry.custom_type]
|
projector = options.entry_projectors[entry.custom_type]
|
||||||
return projector(entry, index, entries)
|
return projector(entry, index, entries)
|
||||||
@@ -648,6 +693,16 @@ Key Points:
|
|||||||
- Each branch has independent tail
|
- Each branch has independent tail
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### getPathToRootOrCompaction
|
||||||
|
|
||||||
|
Walks from a leaf back to the root, handling compaction entries:
|
||||||
|
|
||||||
|
```julia
|
||||||
|
# When encountering a CompactionEntry:
|
||||||
|
# - If retained_tail is set: stop (compaction covers the tail)
|
||||||
|
# - Otherwise: skip to first_kept_entry_id and continue walking
|
||||||
|
```
|
||||||
|
|
||||||
## Compaction Strategy
|
## Compaction Strategy
|
||||||
|
|
||||||
### Why Compaction?
|
### Why Compaction?
|
||||||
@@ -679,7 +734,7 @@ LLM context windows have limits:
|
|||||||
|
|
||||||
# 4. Update storage
|
# 4. Update storage
|
||||||
# - Append CompactionEntry
|
# - Append CompactionEntry
|
||||||
# - Update leaf to CompactionEntry
|
# - Leaf automatically points to CompactionEntry (leafIdAfterEntry)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compaction Example
|
### Compaction Example
|
||||||
@@ -735,8 +790,10 @@ using AgentCore
|
|||||||
|
|
||||||
# 1. Create storage
|
# 1. Create storage
|
||||||
storage = JsonlSessionStorage(
|
storage = JsonlSessionStorage(
|
||||||
SessionMetadata("session_1", "2024-01-01T00:00:00Z"),
|
|
||||||
"/path/to/session.jsonl",
|
"/path/to/session.jsonl",
|
||||||
|
SessionHeader("session", 3, "session_1", created_at, "/path", nothing, nothing),
|
||||||
|
SessionTreeEntry[],
|
||||||
|
nothing,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Create session
|
# 2. Create session
|
||||||
@@ -756,7 +813,7 @@ mc_id = appendModelChange(session, "openai", "gpt-4")
|
|||||||
msg3_id = appendMessage(session, UserMessage("user", [TextContent("What can you do?")], timestamp))
|
msg3_id = appendMessage(session, UserMessage("user", [TextContent("What can you do?")], timestamp))
|
||||||
msg4_id = appendMessage(session, AssistantMessage("assistant", [TextContent("I can...")], ...))
|
msg4_id = appendMessage(session, AssistantMessage("assistant", [TextContent("I can...")], ...))
|
||||||
|
|
||||||
# 7. Compact context (100K tokens → 20K)
|
# 7. Compact context (100K tokens -> 20K)
|
||||||
compact_id = appendCompaction(
|
compact_id = appendCompaction(
|
||||||
session,
|
session,
|
||||||
"User asked about capabilities and assistant explained",
|
"User asked about capabilities and assistant explained",
|
||||||
@@ -771,31 +828,91 @@ compact_id = appendCompaction(
|
|||||||
# 8. Fork and branch
|
# 8. Fork and branch
|
||||||
session.moveTo(msg2_id) # Go back to msg2
|
session.moveTo(msg2_id) # Go back to msg2
|
||||||
|
|
||||||
# 9. Create new branch
|
# 9. Continue on new branch (moveTo creates branch summary when summary is provided)
|
||||||
branch_id = appendBranchSummary(
|
branch_id = moveTo(
|
||||||
session,
|
session,
|
||||||
"User changed direction to focus on file operations",
|
|
||||||
msg2_id,
|
msg2_id,
|
||||||
Dict("focus" => "files"),
|
Dict("summary" => "User changed direction", "details" => Dict("focus" => "files")),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 10. Continue on new branch
|
# 10. Continue on new branch
|
||||||
msg5_id = appendMessage(session, UserMessage("user", [TextContent("Let's work with files")], timestamp))
|
msg5_id = appendMessage(session, UserMessage("user", [TextContent("Let's work with files")], timestamp))
|
||||||
|
|
||||||
# 11. Query session context
|
# 11. Query session context
|
||||||
context = buildSessionContext(session)
|
context = buildContext(session)
|
||||||
|
|
||||||
# 12. Get stats
|
# 12. Get stats
|
||||||
stats = getSessionStats(session)
|
stats = getSessionStats(session)
|
||||||
println("Messages: $(stats.message_count)")
|
println("Messages: $(stats.message_count)")
|
||||||
println("Total tokens: $(stats.total_tokens)")
|
println("Total tokens: $(stats.total_tokens)")
|
||||||
println("Cost: $$(stats.cost_total)")
|
println("Cost: \$(stats.cost_total)")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Repo Interface
|
||||||
|
|
||||||
|
### Session Repository Methods
|
||||||
|
|
||||||
|
```julia
|
||||||
|
# Create a new session
|
||||||
|
create(repo::SessionRepo, options::TCreateOptions)::Session
|
||||||
|
|
||||||
|
# Open an existing session
|
||||||
|
open(repo::SessionRepo, metadata::TMetadata)::Session
|
||||||
|
|
||||||
|
# List sessions
|
||||||
|
list(repo::SessionRepo, options::TListOptions)::Vector{TMetadata}
|
||||||
|
|
||||||
|
# Delete a session
|
||||||
|
delete(repo::SessionRepo, metadata::TMetadata)::Nothing
|
||||||
|
|
||||||
|
# Fork a session (copy branch from entry)
|
||||||
|
fork(repo::SessionRepo, source::TMetadata, options::Dict{String, Any})::Session
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSONL vs In-Memory Repos
|
||||||
|
|
||||||
|
| Feature | JsonlSessionRepo | InMemorySessionRepo |
|
||||||
|
|---------|------------------|---------------------|
|
||||||
|
| Persistence | File-based (TODO) | In-memory only |
|
||||||
|
| Use case | Production | Testing |
|
||||||
|
| Fork | Not implemented | Uses getEntriesToFork |
|
||||||
|
| Metadata | JsonlSessionMetadata | SessionMetadata |
|
||||||
|
|
||||||
|
### Fork Behavior (`getEntriesToFork`)
|
||||||
|
|
||||||
|
```julia
|
||||||
|
function getEntriesToFork(storage, options)::Vector{SessionTreeEntry}
|
||||||
|
# If no entryId specified, fork from current leaf (full copy)
|
||||||
|
if !haskey(options, :entryId) || isnothing(options[:entryId])
|
||||||
|
return getEntries(storage, Dict{String, Any}())
|
||||||
|
end
|
||||||
|
|
||||||
|
target = getEntry(storage, options[:entryId])
|
||||||
|
position = get(options, "position", "before")
|
||||||
|
|
||||||
|
if position == "at"
|
||||||
|
# Fork includes the target entry
|
||||||
|
effective_leaf_id = target.id
|
||||||
|
else
|
||||||
|
# Fork before the target (parent)
|
||||||
|
# Target must be a user message
|
||||||
|
if target isa MessageEntry && target.message.role != "user"
|
||||||
|
throw(SessionError("invalid_fork_target", "Not a user message"))
|
||||||
|
end
|
||||||
|
effective_leaf_id = target.parent_id
|
||||||
|
end
|
||||||
|
|
||||||
|
return getPathToRootOrCompaction(storage, effective_leaf_id)
|
||||||
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
1. **Use compaction** for long conversations to stay within context limits
|
1. **Use compaction** for long conversations to stay within context limits
|
||||||
2. **Create branch summaries** when forking to document divergent paths
|
2. **Create branch summaries** when forking to document divergent paths (via `moveTo()` with summary)
|
||||||
3. **Retain tail messages** after compaction for context
|
3. **Retain tail messages** after compaction for context (`retained_tail` field)
|
||||||
4. **Track token usage** to optimize compaction timing
|
4. **Track token usage** to optimize compaction timing
|
||||||
5. **Use InMemorySessionStorage** for testing
|
5. **Use InMemorySessionStorage** for testing
|
||||||
|
6. **Use `getBranch(session)`** to get the current path from leaf to root/compaction
|
||||||
|
7. **Use `buildContext(session)`** as the convenient Session method for building context
|
||||||
|
8. **Use `mergeContextBuildOptions(session, options)`** to combine session-level and call-level transforms/projectors
|
||||||
|
|||||||
+200
-658
@@ -1,465 +1,187 @@
|
|||||||
# AgentCore.jl - Tools Deep Dive
|
# AgentCore.jl - Tools Deep Dive
|
||||||
|
|
||||||
## Tool Architecture with Data Flow
|
## Tool Types (from types.jl)
|
||||||
|
|
||||||
```
|
### AgentTool (struct)
|
||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ 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
|
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct BashToolOptions{TContext}
|
struct AgentTool{TParameters, TDetails}
|
||||||
command_prefix::Union{String, Nothing}
|
name::String # tool identifier
|
||||||
prepare::Union{BashPrepare{TContext}, Nothing}
|
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
|
end
|
||||||
|
|
||||||
struct BashPrepare{TContext}
|
mutable struct BashPrepare{TContext}
|
||||||
function::Function
|
function::Function
|
||||||
context::TContext
|
context::TContext
|
||||||
signal::Union{Any, Nothing}
|
signal::Union{Any, Nothing}
|
||||||
end
|
end
|
||||||
|
|
||||||
struct BashToolDetails
|
mutable struct BashToolOptions{TContext}
|
||||||
|
command_prefix::Union{String, Nothing}
|
||||||
|
prepare::Union{BashPrepare{TContext}, Nothing}
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct BashToolDetails
|
||||||
truncation::Union{Any, Nothing}
|
truncation::Union{Any, Nothing}
|
||||||
full_output_path::Union{String, Nothing}
|
full_output_path::Union{String, Nothing}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing) where TContext
|
||||||
```
|
```
|
||||||
|
|
||||||
#### createBashTool()
|
**Execute signature**: `(tool_call_id, params, signal, on_update, context) -> AgentToolResult`
|
||||||
|
|
||||||
|
**Note**: The actual bash execution is a TODO stub in the current source.
|
||||||
|
|
||||||
|
### 2. ReadTool (`tools/read.jl`)
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing)
|
mutable struct ReadToolDetails
|
||||||
return AgentTool(
|
truncation::Union{Any, Nothing}
|
||||||
"bash",
|
end
|
||||||
"bash",
|
|
||||||
"Execute a bash command in the current working directory.",
|
|
||||||
Dict{String, Any}(),
|
|
||||||
(tool_call_id, params, signal, on_update, context) -> begin
|
|
||||||
# Execute command
|
|
||||||
result = executeBashCommand(params, signal, on_update)
|
|
||||||
|
|
||||||
# Return result
|
mutable struct ReadToolOptions
|
||||||
return AgentToolResult(
|
auto_resize_images::Bool
|
||||||
[TextContent(result.output)],
|
image_processor::Union{Any, Nothing}
|
||||||
BashToolDetails(result.truncation, result.full_path),
|
end
|
||||||
nothing,
|
|
||||||
nothing,
|
function createReadTool{TContext}(options::Union{ReadToolOptions, Nothing}=nothing) where TContext
|
||||||
result.terminate,
|
```
|
||||||
)
|
|
||||||
end,
|
**Execute signature**: `(tool_call_id, params, signal, on_update, context) -> AgentToolResult`
|
||||||
nothing, # prepare_arguments
|
|
||||||
nothing, # execution_mode (default: use config)
|
### 3. WriteTool (`tools/write.jl`)
|
||||||
)
|
|
||||||
|
```julia
|
||||||
|
function createWriteTool{TContext}() where TContext
|
||||||
|
```
|
||||||
|
|
||||||
|
**Execute signature**: `(tool_call_id, params, signal, on_update, context) -> AgentToolResult`
|
||||||
|
|
||||||
|
### 4. EditTool (`tools/edit.jl`)
|
||||||
|
|
||||||
|
```julia
|
||||||
|
mutable struct EditToolDetails
|
||||||
|
diff::String
|
||||||
|
patch::String
|
||||||
|
first_changed_line::Union{Int64, Nothing}
|
||||||
|
end
|
||||||
|
|
||||||
|
function createEditTool{TContext}() where TContext
|
||||||
|
```
|
||||||
|
|
||||||
|
**Execute signature**: `(tool_call_id, params, signal, on_update, context) -> AgentToolResult`
|
||||||
|
|
||||||
|
## Tool Hooks (on Agent struct)
|
||||||
|
|
||||||
|
The `Agent` struct in `agent.jl` has these hook fields:
|
||||||
|
|
||||||
|
```julia
|
||||||
|
mutable struct Agent
|
||||||
|
...
|
||||||
|
before_tool_call::Union{Function, Nothing}
|
||||||
|
after_tool_call::Union{Function, Nothing}
|
||||||
|
prepare_next_turn::Union{Function, Nothing}
|
||||||
|
prepare_next_turn_with_context::Union{Function, Nothing}
|
||||||
|
...
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters Schema**:
|
Configured via `Agent(Dict(...))` options:
|
||||||
```json
|
- `:beforeToolCall` → `Agent.before_tool_call`
|
||||||
{
|
- `:afterToolCall` → `Agent.after_tool_call`
|
||||||
"command": "string",
|
- `:prepareNextTurn` → `Agent.prepare_next_turn`
|
||||||
"timeout": "number (optional)",
|
- `:prepareNextTurnWithContext` → `Agent.prepare_next_turn_with_context`
|
||||||
"cwd": "string (optional)",
|
|
||||||
"env": "object (optional)"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example**:
|
### BeforeToolCallContext / BeforeToolCallResult (from types.jl)
|
||||||
```julia
|
|
||||||
# Create tool
|
|
||||||
bash_tool = createBashTool()
|
|
||||||
|
|
||||||
# Agent receives command
|
|
||||||
tool_call = ToolCall("tool", "tc1", "bash", Dict(
|
|
||||||
"command" => "ls -la",
|
|
||||||
"timeout" => 30
|
|
||||||
), nothing)
|
|
||||||
|
|
||||||
# Execute
|
|
||||||
result = bash_tool.execute(
|
|
||||||
"tc1",
|
|
||||||
Dict("command" => "ls -la", "timeout" => 30),
|
|
||||||
nothing,
|
|
||||||
on_update, # Callback for streaming output
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Result
|
|
||||||
AgentToolResult(
|
|
||||||
[TextContent("total 12\n-rw-r--r-- 1 user user 100 Jan 1 file1.md\n-rw-r--r-- 1 user user 200 Jan 2 file2.md\n")],
|
|
||||||
BashToolDetails(truncation_info, nothing),
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. ReadTool
|
|
||||||
|
|
||||||
```julia
|
|
||||||
struct ReadToolOptions{TContext}
|
|
||||||
max_size::Union{Int64, Nothing}
|
|
||||||
max_lines::Union{Int64, Nothing}
|
|
||||||
image_processor::Union{ReadImageProcessor, Nothing}
|
|
||||||
prepare::Union{ReadPrepare{TContext}, Nothing}
|
|
||||||
end
|
|
||||||
|
|
||||||
struct ReadImageProcessor
|
|
||||||
function::Function
|
|
||||||
context::Any
|
|
||||||
end
|
|
||||||
|
|
||||||
struct ReadImageProcessorResult
|
|
||||||
content::Vector{MessageContent}
|
|
||||||
usage::Union{Usage, Nothing}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### createReadTool()
|
|
||||||
|
|
||||||
```julia
|
|
||||||
function createReadTool{TContext}(options::Union{ReadToolOptions{TContext}, Nothing}=nothing)
|
|
||||||
return AgentTool(
|
|
||||||
"read",
|
|
||||||
"read",
|
|
||||||
"Read a file from the file system.",
|
|
||||||
Dict{String, Any}(),
|
|
||||||
(tool_call_id, params, signal, on_update, context) -> begin
|
|
||||||
# Read file
|
|
||||||
result = readFileSystem(params, signal, options)
|
|
||||||
|
|
||||||
# Process content
|
|
||||||
content = if isImage(params.path)
|
|
||||||
# Image processing
|
|
||||||
image_result = options.image_processor.function(result.path, context)
|
|
||||||
image_result.content
|
|
||||||
else
|
|
||||||
# Text content
|
|
||||||
[TextContent(result.content)]
|
|
||||||
end
|
|
||||||
|
|
||||||
return AgentToolResult(
|
|
||||||
content,
|
|
||||||
ReadToolDetails(result.size, result.truncated, result.full_path),
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters Schema**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"path": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```julia
|
|
||||||
# Create tool
|
|
||||||
read_tool = createReadTool()
|
|
||||||
|
|
||||||
# Agent requests to read file
|
|
||||||
tool_call = ToolCall("tool", "tc2", "read", Dict(
|
|
||||||
"path" => "src/main.jl"
|
|
||||||
), nothing)
|
|
||||||
|
|
||||||
# Execute
|
|
||||||
result = read_tool.execute("tc2", Dict("path" => "src/main.jl"), nothing, nothing, nothing)
|
|
||||||
|
|
||||||
# Result
|
|
||||||
AgentToolResult(
|
|
||||||
[TextContent("module Main\nfunction main()\n println(\"Hello\")\nend\nend\n")],
|
|
||||||
ReadToolDetails(1234, false, "/path/to/src/main.jl"),
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. WriteTool
|
|
||||||
|
|
||||||
```julia
|
|
||||||
struct WriteToolInput
|
|
||||||
path::String
|
|
||||||
content::String
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### createWriteTool()
|
|
||||||
|
|
||||||
```julia
|
|
||||||
function createWriteTool{TContext}(options::Union{WriteToolOptions{TContext}, Nothing}=nothing)
|
|
||||||
return AgentTool(
|
|
||||||
"write",
|
|
||||||
"write",
|
|
||||||
"Write content to a file.",
|
|
||||||
Dict{String, Any}(),
|
|
||||||
(tool_call_id, params, signal, on_update, context) -> begin
|
|
||||||
# Write file
|
|
||||||
result = writeToFile(params, signal)
|
|
||||||
|
|
||||||
return AgentToolResult(
|
|
||||||
[TextContent(result.message)],
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters Schema**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"path": "string",
|
|
||||||
"content": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```julia
|
|
||||||
# Create tool
|
|
||||||
write_tool = createWriteTool()
|
|
||||||
|
|
||||||
# Agent wants to write file
|
|
||||||
tool_call = ToolCall("tool", "tc3", "write", Dict(
|
|
||||||
"path" => "output.txt",
|
|
||||||
"content" => "Hello World"
|
|
||||||
), nothing)
|
|
||||||
|
|
||||||
# Execute
|
|
||||||
result = write_tool.execute("tc3", Dict(
|
|
||||||
"path" => "output.txt",
|
|
||||||
"content" => "Hello World"
|
|
||||||
), nothing, nothing, nothing)
|
|
||||||
|
|
||||||
# Result
|
|
||||||
AgentToolResult(
|
|
||||||
[TextContent("File written: output.txt (11 bytes)")],
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. EditTool
|
|
||||||
|
|
||||||
```julia
|
|
||||||
struct EditToolInput
|
|
||||||
path::String
|
|
||||||
find::String
|
|
||||||
replacement::String
|
|
||||||
end
|
|
||||||
|
|
||||||
struct EditToolDetails
|
|
||||||
edits::Vector{Edit}
|
|
||||||
before_content::String
|
|
||||||
after_content::String
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### createEditTool()
|
|
||||||
|
|
||||||
```julia
|
|
||||||
function createEditTool{TContext}(options::Union{EditToolOptions{TContext}, Nothing}=nothing)
|
|
||||||
return AgentTool(
|
|
||||||
"edit",
|
|
||||||
"edit",
|
|
||||||
"Edit a file by finding and replacing text.",
|
|
||||||
Dict{String, Any}(),
|
|
||||||
(tool_call_id, params, signal, on_update, context) -> begin
|
|
||||||
# Read file
|
|
||||||
before_content = read(params.path)
|
|
||||||
|
|
||||||
# Apply edit
|
|
||||||
after_content = replace(before_content, params.find => params.replacement)
|
|
||||||
|
|
||||||
# Write file
|
|
||||||
write(params.path, after_content)
|
|
||||||
|
|
||||||
return AgentToolResult(
|
|
||||||
[TextContent("Edit applied successfully")],
|
|
||||||
EditToolDetails([Edit(params.find, params.replacement)], before_content, after_content),
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters Schema**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"path": "string",
|
|
||||||
"find": "string",
|
|
||||||
"replacement": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```julia
|
|
||||||
# Create tool
|
|
||||||
edit_tool = createEditTool()
|
|
||||||
|
|
||||||
# Agent wants to replace text
|
|
||||||
tool_call = ToolCall("tool", "tc4", "edit", Dict(
|
|
||||||
"path" => "README.md",
|
|
||||||
"find" => "v1.0.0",
|
|
||||||
"replacement" => "v2.0.0"
|
|
||||||
), nothing)
|
|
||||||
|
|
||||||
# Execute
|
|
||||||
result = edit_tool.execute("tc4", Dict(
|
|
||||||
"path" => "README.md",
|
|
||||||
"find" => "v1.0.0",
|
|
||||||
"replacement" => "v2.0.0"
|
|
||||||
), nothing, nothing, nothing)
|
|
||||||
|
|
||||||
# Result
|
|
||||||
AgentToolResult(
|
|
||||||
[TextContent("Edit applied: README.md")],
|
|
||||||
EditToolDetails([Edit("v1.0.0", "v2.0.0")], "Version 1.0.0", "Version 2.0.0"),
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tool Execution Hooks
|
|
||||||
|
|
||||||
### before_tool_call
|
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct BeforeToolCallContext
|
struct BeforeToolCallContext
|
||||||
@@ -475,32 +197,7 @@ struct BeforeToolCallResult
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage**:
|
### AfterToolCallContext / AfterToolCallResult (from types.jl)
|
||||||
```julia
|
|
||||||
function myBeforeToolCall(context, signal)
|
|
||||||
tool_name = context.tool_call.name
|
|
||||||
|
|
||||||
# Block dangerous commands
|
|
||||||
if tool_name == "bash" && contains(context.args["command"], "rm -rf /")
|
|
||||||
return BeforeToolCallResult(
|
|
||||||
true,
|
|
||||||
"Blocking dangerous command: rm -rf /"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Log tool execution
|
|
||||||
println("Executing tool: $tool_name")
|
|
||||||
|
|
||||||
return nothing # Allow execution
|
|
||||||
end
|
|
||||||
|
|
||||||
# Configure agent
|
|
||||||
agent = Agent(Dict(
|
|
||||||
:beforeToolCall => myBeforeToolCall,
|
|
||||||
))
|
|
||||||
```
|
|
||||||
|
|
||||||
### after_tool_call
|
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct AfterToolCallContext
|
struct AfterToolCallContext
|
||||||
@@ -521,37 +218,7 @@ struct AfterToolCallResult
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage**:
|
### PrepareNextTurnContext / AgentLoopTurnUpdate (from types.jl)
|
||||||
```julia
|
|
||||||
function myAfterToolCall(context, signal)
|
|
||||||
tool_name = context.tool_call.name
|
|
||||||
|
|
||||||
# Modify bash output
|
|
||||||
if tool_name == "bash"
|
|
||||||
# Add timestamp to output
|
|
||||||
new_content = [
|
|
||||||
TextContent("[Executed at $(Dates.now())]\n"),
|
|
||||||
context.result.content[1],
|
|
||||||
]
|
|
||||||
return AfterToolCallResult(
|
|
||||||
content = new_content,
|
|
||||||
details = context.result.details,
|
|
||||||
is_error = context.is_error,
|
|
||||||
usage = context.result.usage,
|
|
||||||
terminate = context.result.terminate,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
return nothing # Use original result
|
|
||||||
end
|
|
||||||
|
|
||||||
# Configure agent
|
|
||||||
agent = Agent(Dict(
|
|
||||||
:afterToolCall => myAfterToolCall,
|
|
||||||
))
|
|
||||||
```
|
|
||||||
|
|
||||||
### prepare_next_turn
|
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
struct PrepareNextTurnContext
|
struct PrepareNextTurnContext
|
||||||
@@ -568,193 +235,73 @@ struct AgentLoopTurnUpdate
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage**:
|
|
||||||
```julia
|
|
||||||
function myPrepareNextTurn(context, signal)
|
|
||||||
# Check if we should use a different model
|
|
||||||
last_message = context.message
|
|
||||||
tool_results = context.tool_results
|
|
||||||
|
|
||||||
# If tool execution had errors, use more capable model
|
|
||||||
has_errors = any(r -> r.is_error, tool_results)
|
|
||||||
if has_errors
|
|
||||||
return AgentLoopTurnUpdate(
|
|
||||||
context = context.context,
|
|
||||||
model = Model("gpt-4", "GPT-4", "openai", "openai", "", ...),
|
|
||||||
thinking_level = THINKING_HIGH,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
return nothing # Keep current settings
|
|
||||||
end
|
|
||||||
|
|
||||||
# Configure agent
|
|
||||||
agent = Agent(Dict(
|
|
||||||
:prepareNextTurn => myPrepareNextTurn,
|
|
||||||
))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tool Execution Modes
|
## Tool Execution Modes
|
||||||
|
|
||||||
### Sequential Execution
|
### Sequential Execution
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
# Tools run one at a time, in order
|
# Configure on Agent
|
||||||
# Use case: Tools that modify shared state
|
|
||||||
|
|
||||||
# Configure tool
|
|
||||||
bash_tool = AgentTool(
|
|
||||||
"bash",
|
|
||||||
"bash",
|
|
||||||
"Execute bash command",
|
|
||||||
...,
|
|
||||||
execute,
|
|
||||||
nothing,
|
|
||||||
EXECUTION_SEQUENTIAL, # Force sequential
|
|
||||||
)
|
|
||||||
|
|
||||||
# Or configure globally
|
|
||||||
agent = Agent(Dict(
|
agent = Agent(Dict(
|
||||||
:toolExecution => EXECUTION_SEQUENTIAL,
|
:toolExecution => EXECUTION_SEQUENTIAL,
|
||||||
))
|
))
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example Scenario**:
|
### Parallel Execution (default)
|
||||||
```julia
|
|
||||||
# Sequential execution (correct order)
|
|
||||||
|
|
||||||
1. Tool 1: create_directory("build/")
|
|
||||||
└─ Creates build/ directory
|
|
||||||
|
|
||||||
2. Tool 2: write("build/app.js", "...")
|
|
||||||
└─ Writes file to build/
|
|
||||||
|
|
||||||
(If parallel: might fail because build/ doesn't exist yet)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parallel Execution
|
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
# Tools run concurrently
|
|
||||||
# Use case: Independent operations
|
|
||||||
|
|
||||||
# Default behavior
|
|
||||||
agent = Agent(Dict(
|
agent = Agent(Dict(
|
||||||
:toolExecution => EXECUTION_PARALLEL, # Default
|
:toolExecution => EXECUTION_PARALLEL,
|
||||||
))
|
))
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example Scenario**:
|
Tools can also specify their own mode:
|
||||||
```julia
|
|
||||||
# Parallel execution (independent operations)
|
|
||||||
|
|
||||||
1. Tool 1: read("README.md") ─────┐
|
|
||||||
2. Tool 2: read("CHANGELOG.md") ─┼─► Run simultaneously
|
|
||||||
3. Tool 3: read("LICENSE") ──────┘
|
|
||||||
|
|
||||||
(Parallel: All three read operations can happen at once)
|
|
||||||
(Sequential: Would wait for each read to complete)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Custom Tools
|
|
||||||
|
|
||||||
### Example: Database Tool
|
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function createDatabaseTool()
|
agent_tool = AgentTool(
|
||||||
return AgentTool(
|
"name",
|
||||||
"database",
|
"label",
|
||||||
"database",
|
"description",
|
||||||
"Execute SQL queries against the database.",
|
params_schema,
|
||||||
Dict{String, Any}(
|
execute_fn,
|
||||||
"type" => "object",
|
nothing,
|
||||||
"properties" => Dict(
|
EXECUTION_SEQUENTIAL, # or EXECUTION_PARALLEL
|
||||||
"query" => Dict("type" => "string"),
|
)
|
||||||
"params" => Dict("type" => "array", "items" => Dict("type" => "string")),
|
|
||||||
),
|
|
||||||
"required" => ["query"],
|
|
||||||
),
|
|
||||||
(tool_call_id, params, signal, on_update, context) -> begin
|
|
||||||
# Execute query
|
|
||||||
query = params["query"]
|
|
||||||
result = executeQuery(query)
|
|
||||||
|
|
||||||
# Format output
|
|
||||||
output = formatQueryResult(result)
|
|
||||||
|
|
||||||
return AgentToolResult(
|
|
||||||
[TextContent(output)],
|
|
||||||
Dict("rows_affected" => result.rows_affected),
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
nothing,
|
|
||||||
EXECUTION_SEQUENTIAL,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
db_tool = createDatabaseTool()
|
|
||||||
agent = Agent(Dict(:tools => [db_tool]))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example: HTTP Request Tool
|
## Tool Exports (from tools/index.jl)
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function createHTTPTool()
|
export
|
||||||
return AgentTool(
|
createBashTool,
|
||||||
"http",
|
createReadTool,
|
||||||
"http",
|
createWriteTool,
|
||||||
"Make HTTP requests.",
|
createEditTool,
|
||||||
Dict{String, Any}(
|
BashExecution,
|
||||||
"type" => "object",
|
BashPrepare,
|
||||||
"properties" => Dict(
|
BashToolDetails,
|
||||||
"url" => Dict("type" => "string"),
|
BashToolInput,
|
||||||
"method" => Dict("type" => "string", "enum" => ["GET", "POST", "PUT", "DELETE"]),
|
BashToolOptions,
|
||||||
"body" => Dict("type" => "string"),
|
EditToolDetails,
|
||||||
"headers" => Dict("type" => "object"),
|
EditToolInput,
|
||||||
),
|
ReadToolDetails,
|
||||||
"required" => ["url", "method"],
|
ReadToolInput,
|
||||||
),
|
ReadToolOptions,
|
||||||
(tool_call_id, params, signal, on_update, context) -> begin
|
ReadImageProcessor,
|
||||||
# Make request
|
ReadImageProcessorResult,
|
||||||
url = params["url"]
|
WriteToolInput
|
||||||
method = params["method"]
|
|
||||||
body = get(params, "body", nothing)
|
|
||||||
headers = get(params, "headers", Dict())
|
|
||||||
|
|
||||||
response = makeHTTPRequest(method, url, body, headers)
|
|
||||||
|
|
||||||
return AgentToolResult(
|
|
||||||
[TextContent(response.body)],
|
|
||||||
Dict(
|
|
||||||
"status_code" => response.status_code,
|
|
||||||
"headers" => response.headers,
|
|
||||||
),
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
nothing,
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
nothing,
|
|
||||||
EXECUTION_PARALLEL,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Complete Example
|
## Example: Creating and Using Tools
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
using AgentCore
|
using AgentCore
|
||||||
|
|
||||||
# 1. Create tools
|
# Create tools
|
||||||
bash_tool = createBashTool()
|
bash_tool = createBashTool()
|
||||||
read_tool = createReadTool()
|
read_tool = createReadTool()
|
||||||
write_tool = createWriteTool()
|
write_tool = createWriteTool()
|
||||||
|
|
||||||
# 2. Configure hooks
|
# Configure hooks
|
||||||
before_hook = (context, signal) -> begin
|
before_hook = (context, signal) -> begin
|
||||||
println("About to execute: $(context.tool_call.name)")
|
println("About to execute: $(context.tool_call.name)")
|
||||||
return nothing
|
return nothing
|
||||||
@@ -769,22 +316,17 @@ after_hook = (context, signal) -> begin
|
|||||||
return nothing
|
return nothing
|
||||||
end
|
end
|
||||||
|
|
||||||
# 3. Create agent
|
# Create agent with tools and hooks
|
||||||
agent = Agent(Dict(
|
agent = Agent(Dict(
|
||||||
:systemPrompt => "You are a helpful assistant with file system access.",
|
:systemPrompt => "You are a helpful assistant with file system access.",
|
||||||
:tools => [bash_tool, read_tool, write_tool],
|
:tools => [bash_tool, read_tool, write_tool],
|
||||||
:beforeToolCall => before_hook,
|
:beforeToolCall => before_hook,
|
||||||
:afterToolCall => after_hook,
|
:afterToolCall => after_hook,
|
||||||
|
:toolExecution => EXECUTION_PARALLEL,
|
||||||
))
|
))
|
||||||
|
|
||||||
# 4. Run conversation
|
# Run prompt
|
||||||
prompt(agent, "List files in current directory and read the first one")
|
prompt(agent, "List files in current directory and read the first one")
|
||||||
|
|
||||||
# 5. Agent will:
|
|
||||||
# - Execute bash("ls -la") tool
|
|
||||||
# - Parse output to find first file
|
|
||||||
# - Execute read("path/to/file") tool
|
|
||||||
# - Return content to user
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|||||||
+500
-603
File diff suppressed because it is too large
Load Diff
+321
-490
File diff suppressed because it is too large
Load Diff
+34
-20
@@ -37,7 +37,7 @@ agent = Agent(Dict(
|
|||||||
prompt(agent, "Hello!")
|
prompt(agent, "Hello!")
|
||||||
|
|
||||||
# Wait for completion
|
# Wait for completion
|
||||||
wait_for_idle(agent)
|
waitForIdle(agent)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Understanding the Flow
|
### Understanding the Flow
|
||||||
@@ -83,6 +83,12 @@ User Code
|
|||||||
- `steer()` - Queue message for next turn
|
- `steer()` - Queue message for next turn
|
||||||
- `followUp()` - Queue message after stop
|
- `followUp()` - Queue message after stop
|
||||||
- `subscribe()` - Listen to events
|
- `subscribe()` - Listen to events
|
||||||
|
- `waitForIdle()` - Wait for agent to finish processing
|
||||||
|
- `reset!()` - Clear transcript state and queued messages
|
||||||
|
- `clearAllQueues()` - Remove all queued steering and follow-up messages
|
||||||
|
- `hasQueuedMessages()` - Check if queues have pending messages
|
||||||
|
- `abort()` - Abort the current run
|
||||||
|
- `get_state()` - Get the current agent state
|
||||||
|
|
||||||
### AgentLoop
|
### AgentLoop
|
||||||
|
|
||||||
@@ -113,9 +119,14 @@ User Code
|
|||||||
|
|
||||||
**Key methods**:
|
**Key methods**:
|
||||||
- `appendMessage()` - Add message
|
- `appendMessage()` - Add message
|
||||||
- `appendCompaction()` - Compress history
|
- `appendCompaction()` - Compress history with summary
|
||||||
- `moveTo()` - Navigate branches
|
- `moveTo()` - Navigate branches
|
||||||
- `buildSessionContext()` - Build context for LLM
|
- `buildContext()` - Build context for LLM
|
||||||
|
- `getBranch()` - Get branch entries
|
||||||
|
- `getSessionStats()` - Get session statistics
|
||||||
|
- `appendThinkingLevelChange()` - Record thinking level change
|
||||||
|
- `appendModelChange()` - Record model change
|
||||||
|
- `appendActiveToolsChange()` - Record active tools change
|
||||||
|
|
||||||
### Tools
|
### Tools
|
||||||
|
|
||||||
@@ -193,20 +204,23 @@ Message (for LLM API)
|
|||||||
├── is_error::Bool
|
├── is_error::Bool
|
||||||
└── timestamp::Timestamp
|
└── timestamp::Timestamp
|
||||||
|
|
||||||
AgentMessage (internal, extends Message)
|
AgentMessage (internal, abstract type)
|
||||||
├── UserMessage (same as above)
|
├── UserMessage (same as above)
|
||||||
├── AssistantMessage (same as above)
|
├── AssistantMessage (same as above)
|
||||||
├── ToolResultMessage (same as above)
|
├── ToolResultMessage (same as above, plus: role, added_tool_names)
|
||||||
├── BashExecutionMessage (custom)
|
├── BashExecutionMessage (custom)
|
||||||
│ ├── role, command, output, exit_code
|
│ ├── role, command, output, exit_code
|
||||||
│ ├── cancelled, truncated, exclude_from_context
|
│ ├── cancelled, truncated, full_output_path, timestamp
|
||||||
│ └── timestamp
|
│ └── exclude_from_context
|
||||||
├── CompactionSummaryMessage (custom)
|
├── CompactionSummaryMessage (custom)
|
||||||
│ ├── summary, tokens_before, timestamp
|
│ ├── role, summary, tokens_before, timestamp
|
||||||
│ └── converted to UserMessage for LLM
|
│ └── converted to UserMessage for LLM
|
||||||
└── BranchSummaryMessage (custom)
|
├── BranchSummaryMessage (custom)
|
||||||
├── summary, from_id, timestamp
|
│ ├── role, summary, from_id, timestamp
|
||||||
└── converted to UserMessage for LLM
|
│ └── converted to UserMessage for LLM
|
||||||
|
└── CustomMessage (custom, extends AgentMessage)
|
||||||
|
├── message::AgentMessage
|
||||||
|
└── custom_type::String
|
||||||
```
|
```
|
||||||
|
|
||||||
### Complete Conversation Flow
|
### Complete Conversation Flow
|
||||||
@@ -427,7 +441,7 @@ appendMessage(session, user_message)
|
|||||||
appendMessage(session, assistant_message)
|
appendMessage(session, assistant_message)
|
||||||
|
|
||||||
# Build context from session
|
# Build context from session
|
||||||
context = buildSessionContext(session)
|
context = buildContext(session)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pattern 2: Long Conversations
|
### Pattern 2: Long Conversations
|
||||||
@@ -451,7 +465,7 @@ end
|
|||||||
session.moveTo(branch_point_id)
|
session.moveTo(branch_point_id)
|
||||||
|
|
||||||
# Create new branch
|
# Create new branch
|
||||||
appendBranchSummary(session, "Exploring alternative approach")
|
moveTo(session, branch_point_id, summary=["summary" => "Exploring alternative approach"])
|
||||||
appendMessage(session, new_user_message)
|
appendMessage(session, new_user_message)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -460,13 +474,13 @@ appendMessage(session, new_user_message)
|
|||||||
```julia
|
```julia
|
||||||
# Create custom tool
|
# Create custom tool
|
||||||
custom_tool = AgentTool(
|
custom_tool = AgentTool(
|
||||||
"custom",
|
"custom", # name
|
||||||
"custom",
|
"Custom", # label
|
||||||
"Does custom thing",
|
"Does custom thing", # description
|
||||||
...,
|
parameters, # parameter schema
|
||||||
execute_function,
|
execute_function, # execute
|
||||||
nothing,
|
nothing, # prepare_arguments (optional)
|
||||||
EXECUTION_PARALLEL,
|
EXECUTION_PARALLEL, # execution_mode
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add to agent
|
# Add to agent
|
||||||
|
|||||||
Reference in New Issue
Block a user