768 lines
22 KiB
Markdown
768 lines
22 KiB
Markdown
# 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
|
|
|
|
```julia
|
|
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()
|
|
|
|
```julia
|
|
function createBashTool{TContext}(options::Union{BashToolOptions{TContext}, Nothing}=nothing)
|
|
return AgentTool(
|
|
"bash",
|
|
"bash",
|
|
"Execute a bash command in the current working directory.",
|
|
Dict{String, Any}(),
|
|
(tool_call_id, params, signal, on_update, context) -> begin
|
|
# Execute command
|
|
result = executeBashCommand(params, signal, on_update)
|
|
|
|
# Return result
|
|
return AgentToolResult(
|
|
[TextContent(result.output)],
|
|
BashToolDetails(result.truncation, result.full_path),
|
|
nothing,
|
|
nothing,
|
|
result.terminate,
|
|
)
|
|
end,
|
|
nothing, # prepare_arguments
|
|
nothing, # execution_mode (default: use config)
|
|
)
|
|
end
|
|
```
|
|
|
|
**Parameters Schema**:
|
|
```json
|
|
{
|
|
"command": "string",
|
|
"timeout": "number (optional)",
|
|
"cwd": "string (optional)",
|
|
"env": "object (optional)"
|
|
}
|
|
```
|
|
|
|
**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
|
|
|
|
```julia
|
|
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**:
|
|
```julia
|
|
function myBeforeToolCall(context, signal)
|
|
tool_name = context.tool_call.name
|
|
|
|
# Block dangerous commands
|
|
if tool_name == "bash" && contains(context.args["command"], "rm -rf /")
|
|
return BeforeToolCallResult(
|
|
true,
|
|
"Blocking dangerous command: rm -rf /"
|
|
)
|
|
end
|
|
|
|
# Log tool execution
|
|
println("Executing tool: $tool_name")
|
|
|
|
return nothing # Allow execution
|
|
end
|
|
|
|
# Configure agent
|
|
agent = Agent(Dict(
|
|
:beforeToolCall => myBeforeToolCall,
|
|
))
|
|
```
|
|
|
|
### after_tool_call
|
|
|
|
```julia
|
|
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**:
|
|
```julia
|
|
function myAfterToolCall(context, signal)
|
|
tool_name = context.tool_call.name
|
|
|
|
# Modify bash output
|
|
if tool_name == "bash"
|
|
# Add timestamp to output
|
|
new_content = [
|
|
TextContent("[Executed at $(Dates.now())]\n"),
|
|
context.result.content[1],
|
|
]
|
|
return AfterToolCallResult(
|
|
content = new_content,
|
|
details = context.result.details,
|
|
is_error = context.is_error,
|
|
usage = context.result.usage,
|
|
terminate = context.result.terminate,
|
|
)
|
|
end
|
|
|
|
return nothing # Use original result
|
|
end
|
|
|
|
# Configure agent
|
|
agent = Agent(Dict(
|
|
:afterToolCall => myAfterToolCall,
|
|
))
|
|
```
|
|
|
|
### prepare_next_turn
|
|
|
|
```julia
|
|
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**:
|
|
```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
|
|
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
|
|
|
|
```julia
|
|
# Tools run concurrently
|
|
# Use case: Independent operations
|
|
|
|
# Default behavior
|
|
agent = Agent(Dict(
|
|
:toolExecution => EXECUTION_PARALLEL, # Default
|
|
))
|
|
```
|
|
|
|
**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
|
|
|
|
```julia
|
|
function createDatabaseTool()
|
|
return AgentTool(
|
|
"database",
|
|
"database",
|
|
"Execute SQL queries against the database.",
|
|
Dict{String, Any}(
|
|
"type" => "object",
|
|
"properties" => Dict(
|
|
"query" => Dict("type" => "string"),
|
|
"params" => Dict("type" => "array", "items" => Dict("type" => "string")),
|
|
),
|
|
"required" => ["query"],
|
|
),
|
|
(tool_call_id, params, signal, on_update, context) -> begin
|
|
# Execute query
|
|
query = params["query"]
|
|
result = executeQuery(query)
|
|
|
|
# Format output
|
|
output = formatQueryResult(result)
|
|
|
|
return AgentToolResult(
|
|
[TextContent(output)],
|
|
Dict("rows_affected" => result.rows_affected),
|
|
nothing,
|
|
nothing,
|
|
nothing,
|
|
)
|
|
end,
|
|
nothing,
|
|
EXECUTION_SEQUENTIAL,
|
|
)
|
|
end
|
|
|
|
# Usage
|
|
db_tool = createDatabaseTool()
|
|
agent = Agent(Dict(:tools => [db_tool]))
|
|
```
|
|
|
|
### Example: HTTP Request Tool
|
|
|
|
```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
|
|
```
|
|
|
|
## Complete Example
|
|
|
|
```julia
|
|
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
|
|
|
|
1. **Use sequential execution** for tools that depend on shared state
|
|
2. **Use parallel execution** for independent operations
|
|
3. **Implement before_tool_call hook** for logging and validation
|
|
4. **Implement after_tool_call hook** for result modification
|
|
5. **Use prepare_next_turn hook** for dynamic model/thinking level changes
|
|
6. **Return terminate=true** from tool when agent should stop
|
|
7. **Include usage statistics** in tool results when possible
|