22 KiB
22 KiB
AgentCore.jl - Tools Deep Dive
Tool Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ Tool Layer │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ AgentTool │
│ - name: String (identifier) │
│ - label: String (display name) │
│ - description: String (what it does) │
│ - parameters: JSON schema │
│ - execute::Function (main logic) │
│ - prepare_arguments::Union{Function, Nothing} │
│ - execution_mode::Union{ToolExecutionMode, Nothing} │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ BashTool │ │ ReadTool │ │ WriteTool │
│ - bash() │ │ - read() │ │ - write() │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐
│ EditTool │
│ - edit() │
└─────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Tool Execution Flow │
└─────────────────────────────────────────────────────────────────────────────┘
Assistant Message
┌────────────────────────────────────────────────────────┐
│ AssistantMessage: │
│ content: [ │
│ TextContent("I'll check the files..."), │
│ ToolCall("bash", {command: "ls -la"}), │
│ ToolCall("read", {path: "README.md"}) │
│ ] │
└────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ AgentLoop.executeToolCalls() │
│ - Extract ToolCalls from message content │
│ - Determine execution mode (sequential/parallel) │
└────────────────────────────────────────────────────────┘
│
├─► executeToolCallsSequential()
│ (for tools that require order)
│
└─► executeToolCallsParallel()
(for independent tools)
│
├─► prepareToolCall()
│ - before_tool_call hook (optional)
│ - validate arguments
│ - prepare arguments (optional)
│
├─► execute()
│ - Tool-specific logic
│ - Return AgentToolResult
│
├─► finalizeExecutedToolCall()
│ - after_tool_call hook (optional)
│
└─► createToolResultMessage()
- Emit ToolResultMessage
│
▼
┌────────────────────────────────────────────────────────┐
│ ToolResultMessage │
│ - tool_call_id: "ref to original ToolCall" │
│ - tool_name: "bash" │
│ - content: [TextContent("file1.md\nfile2.md\n")] │
│ - is_error: false │
└────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ AgentState.messages.append(tool_result) │
│ - Next turn: LLM sees tool results │
└────────────────────────────────────────────────────────┘
Built-in Tools
1. BashTool
struct BashToolOptions{TContext}
command_prefix::Union{String, Nothing}
prepare::Union{BashPrepare{TContext}, Nothing}
end
struct BashPrepare{TContext}
function::Function
context::TContext
signal::Union{Any, Nothing}
end
struct BashToolDetails
truncation::Union{Any, Nothing}
full_output_path::Union{String, Nothing}
end
createBashTool()
function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing)
return AgentTool(
"bash",
"bash",
"Execute a bash command in the current working directory.",
Dict{String, Any}(),
(tool_call_id, params, signal, on_update, context) -> begin
# Execute command
result = executeBashCommand(params, signal, on_update)
# Return result
return AgentToolResult(
[TextContent(result.output)],
BashToolDetails(result.truncation, result.full_path),
nothing,
nothing,
result.terminate,
)
end,
nothing, # prepare_arguments
nothing, # execution_mode (default: use config)
)
end
Parameters Schema:
{
"command": "string",
"timeout": "number (optional)",
"cwd": "string (optional)",
"env": "object (optional)"
}
Example:
# 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
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()
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:
{
"path": "string"
}
Example:
# 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
struct WriteToolInput
path::String
content::String
end
createWriteTool()
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:
{
"path": "string",
"content": "string"
}
Example:
# 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
struct EditToolInput
path::String
find::String
replacement::String
end
struct EditToolDetails
edits::Vector{Edit}
before_content::String
after_content::String
end
createEditTool()
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:
{
"path": "string",
"find": "string",
"replacement": "string"
}
Example:
# 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
struct BeforeToolCallContext
assistant_message::AssistantMessage
tool_call::ToolCall
args::Any
context::AgentContext
end
struct BeforeToolCallResult
block::Union{Bool, Nothing}
reason::Union{String, Nothing}
end
Usage:
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
struct AfterToolCallContext
assistant_message::AssistantMessage
tool_call::ToolCall
args::Any
result::AgentToolResult
is_error::Bool
context::AgentContext
end
struct AfterToolCallResult
content::Union{Vector{MessageContent}, Nothing}
details::Union{Any, Nothing}
is_error::Union{Bool, Nothing}
usage::Union{Usage, Nothing}
terminate::Union{Bool, Nothing}
end
Usage:
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
struct PrepareNextTurnContext
message::AssistantMessage
tool_results::Vector{ToolResultMessage}
context::AgentContext
new_messages::Vector{AgentMessage}
end
struct AgentLoopTurnUpdate
context::Union{AgentContext, Nothing}
model::Union{Model, Nothing}
thinking_level::Union{ThinkingLevel, Nothing}
end
Usage:
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
# 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
agent = Agent(Dict(
:toolExecution => EXECUTION_SEQUENTIAL,
))
Example Scenario:
# 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
# Tools run concurrently
# Use case: Independent operations
# Default behavior
agent = Agent(Dict(
:toolExecution => EXECUTION_PARALLEL, # Default
))
Example Scenario:
# 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
function createDatabaseTool()
return AgentTool(
"database",
"database",
"Execute SQL queries against the database.",
Dict{String, Any}(
"type" => "object",
"properties" => Dict(
"query" => Dict("type" => "string"),
"params" => Dict("type" => "array", "items" => Dict("type" => "string")),
),
"required" => ["query"],
),
(tool_call_id, params, signal, on_update, context) -> begin
# Execute query
query = params["query"]
result = executeQuery(query)
# Format output
output = formatQueryResult(result)
return AgentToolResult(
[TextContent(output)],
Dict("rows_affected" => result.rows_affected),
nothing,
nothing,
nothing,
)
end,
nothing,
EXECUTION_SEQUENTIAL,
)
end
# Usage
db_tool = createDatabaseTool()
agent = Agent(Dict(:tools => [db_tool]))
Example: HTTP Request Tool
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
Complete Example
using AgentCore
# 1. Create tools
bash_tool = createBashTool()
read_tool = createReadTool()
write_tool = createWriteTool()
# 2. Configure hooks
before_hook = (context, signal) -> begin
println("About to execute: $(context.tool_call.name)")
return nothing
end
after_hook = (context, signal) -> begin
if context.is_error
println("Tool failed: $(context.tool_call.name)")
else
println("Tool completed: $(context.tool_call.name)")
end
return nothing
end
# 3. Create agent
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,
))
# 4. Run conversation
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
- Use sequential execution for tools that depend on shared state
- Use parallel execution for independent operations
- Implement before_tool_call hook for logging and validation
- Implement after_tool_call hook for result modification
- Use prepare_next_turn hook for dynamic model/thinking level changes
- Return terminate=true from tool when agent should stop
- Include usage statistics in tool results when possible