Files
YiemAgent/learning/06-TOOLS.md
T
2026-07-29 13:50:00 +07:00

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

  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