# 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