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