diff --git a/etc.jl b/etc.jl index 4ce0950..c128ba1 100644 --- a/etc.jl +++ b/etc.jl @@ -1,2 +1 @@ -# ── executeToolCalls() Julia pseudo code ────────────────────────── -# Full call stack from runLoop → executeToolCalls → prepare → execute → finalize → emit +run test again. loadToolTest.jl should be able to load all tools in ./src/tools and test them. \ No newline at end of file diff --git a/src/tools/getTime.jl b/src/tools/getTime.jl index 0d6a467..53d60e3 100644 --- a/src/tools/getTime.jl +++ b/src/tools/getTime.jl @@ -36,6 +36,35 @@ function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String} return nothing end +""" +Execute the getTime tool. + +# Arguments +- `toolCallId::String`: Unique identifier for this tool call +- `args::Dict{String,Any}`: Parsed arguments from the LLM +- `signal::Union{Nothing,abortSignal}`: Optional abort signal +- `onPartialResult::Function`: Callback for streaming partial results + +# Returns +- `agentToolResult`: Result content with current time data +""" +function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult + tz = get(args, "timezone", nothing) + city = get(args, "city", "") + + # Simulate time lookup — replace with actual timezone API call + if tz !== nothing + result = "Current time in $(tz): $(now())" + else + result = "Current time in $(city): $(now())" + end + + return agentToolResult( + [textContent(result)], + Dict{Any,Any}(), nothing, false + ) +end + """ Define and return the getTime agentTool. """ @@ -52,19 +81,7 @@ function getTool()::agentTool ), "required" => [] ), - execute = (toolCallId, args, signal, onPartialResult) -> begin - tz = get(args, "timezone", nothing) - city = get(args, "city", "") - if tz !== nothing - result = "Current time in $(tz): $(now())" - else - result = "Current time in $(city): $(now())" - end - return agentToolResult( - [textContent(result)], - Dict{Any,Any}(), nothing, false - ) - end, + execute = executeTool, prepareArguments = nothing, validateRequiredArgs = validateRequiredArgs, parallelToolExecute = false diff --git a/src/tools/getWeather.jl b/src/tools/getWeather.jl index 61a17f5..0411841 100644 --- a/src/tools/getWeather.jl +++ b/src/tools/getWeather.jl @@ -1,3 +1,33 @@ +""" +Execute the getWeather tool. + +# Arguments +- `toolCallId::String`: Unique identifier for this tool call +- `args::Dict{String,Any}`: Parsed arguments from the LLM +- `signal::Union{Nothing,abortSignal}`: Optional abort signal +- `onPartialResult::Function`: Callback for streaming partial results + +# Returns +- `agentToolResult`: Result content with weather data +""" +function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult + city = get(args, "city", "") + units = get(args, "units", "celsius") + + # Simulate weather fetch — replace with actual API call + # You can call onPartialResult() here for streaming progress updates: + # onPartialResult(Dict("status" => "Fetching weather data...")) + # onPartialResult(Dict("status" => "Processing...")) + + temp = units == "fahrenheit" ? "72" : "22" + unit_symbol = units == "celsius" ? "°C" : "°F" + + return agentToolResult( + [textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")], + Dict{Any,Any}(), nothing, false + ) +end + """ Define and return the getWeather agentTool. """ @@ -14,16 +44,7 @@ function getTool()::agentTool ), "required" => ["city"] ), - execute = (toolCallId, args, signal, onPartialResult) -> begin - city = get(args, "city", "") - units = get(args, "units", "celsius") - temp = units == "fahrenheit" ? "72" : "22" - unit_symbol = units == "celsius" ? "°C" : "°F" - return agentToolResult( - [textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")], - Dict{Any,Any}(), nothing, false - ) - end, + execute = executeTool, # reference the function defined above prepareArguments = nothing, validateRequiredArgs = nothing, parallelToolExecute = false diff --git a/src/tools/registry.jl b/src/tools/registry.jl index f7aa585..7678a51 100644 --- a/src/tools/registry.jl +++ b/src/tools/registry.jl @@ -44,7 +44,8 @@ function _listTool()::agentTool end, prepareArguments = nothing, validateRequiredArgs = nothing, - parallelToolExecute = false + parallelToolExecute = false, + _tool_module = nothing ) end @@ -55,36 +56,9 @@ Scans `dir` for `.jl` files. Each file must define a function named `getTool()::agentTool`. Files are sorted alphabetically so tool registration order is deterministic. -# Tool file format -Each `.jl` file defines one function `getTool()` that returns an `agentTool`: - -```julia -# src/tools/getWeather.jl -function getTool()::agentTool - return agentTool( - name = "getWeather", - label = "Weather Lookup", - description = "Fetch current weather and forecast for a given city.", - inputSchema = Dict{String,Any}( - "type" => "object", - "properties" => Dict( - "city" => Dict("type" => "string", "description" => "City and country"), - "units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius") - ), - "required" => ["city"] - ), - execute = (toolCallId, args, signal, onPartialResult) -> begin - city = args["city"] - return agentToolResult( - [textContent("Sunny, 22C in Bangkok")], - Dict{Any,Any}(), nothing, false - ) - end, - prepareArguments = nothing, - parallelToolExecute = false - ) -end -``` +Each tool file is loaded into its own isolated module (a child of `toolRegistry`) +so that functions like `executeTool` and `validateRequiredArgs` do not collide +across tool files. The module reference is stored in `agentTool._tool_module`. # Arguments - `dir::String`: Directory path to scan for `.jl` tool files @@ -108,27 +82,46 @@ function loadTools(dir::String)::Vector{agentTool} filepath = joinpath(dir, filename) println("[toolRegistry] Loading tool from: $filepath") - # Include the file in the current module scope so all types resolve - # (agentTool, textContent, agentToolResult, etc. are all available) - include(filepath) + # Create a unique module for this tool (child of toolRegistry so ...type resolves) + base_name = replace(filename, ".jl" => "") + mod_name = Symbol("Tool_", replace(base_name, r"[^a-zA-Z0-9_]" => "_")) - # Validate that getTool was defined (include() places it in current module scope) - if !isdefined(@__MODULE__, :getTool) + Core.eval(toolRegistry, :(module $mod_name + using ...type + using Dates + using JSON + end)) + tool_mod = getfield(toolRegistry, mod_name) + + # Include the tool file — defines getTool() and executeTool() in tool_mod + Base.include(tool_mod, filepath) + + # Validate that getTool was defined + if !isdefined(tool_mod, :getTool) throw(ArgumentError( "Tool file $(filepath) does not define a `getTool()` function. " * "Each tool file must define: function getTool()::agentTool ... end" )) end - # Call getTool() — it runs in current scope where types are visible + # Call getTool() in the tool module's scope # Use invokelatest to handle world-age semantics after include() - tool = invokelatest(getTool) + tool = invokelatest(getfield(tool_mod, :getTool)) + if !(tool isa agentTool) throw(ArgumentError( "getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))" )) end + # Reconstruct tool with _tool_module (agentTool is immutable) + tool = agentTool(; name=tool.name, label=tool.label, description=tool.description, + inputSchema=tool.inputSchema, execute=tool.execute, + prepareArguments=tool.prepareArguments, + validateRequiredArgs=tool.validateRequiredArgs, + parallelToolExecute=tool.parallelToolExecute, + _tool_module=tool_mod) + push!(_registry, tool) push!(tools, tool) println("[toolRegistry] Loaded tool: $(tool.name) — $(tool.label)") @@ -159,7 +152,7 @@ Get all registered tools. - `Vector{agentTool}`: Copy of the registry """ function getTools()::Vector{agentTool} - return deepcopy(_registry) + return [t for t in _registry] # new vector with same references (agentTool is immutable, Modules can't be deepcopied) end """ diff --git a/src/tools/writeTool.jl b/src/tools/writeTool.jl index 40c4d8c..71f59b6 100644 --- a/src/tools/writeTool.jl +++ b/src/tools/writeTool.jl @@ -100,6 +100,150 @@ function json_string(v)::String return JSON.json(v) end +""" +Execute the writeTool. + +Generates a new .jl tool file and registers it with the tool registry. +""" +function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult + tool_name = get(args, "name", "")::String + tool_label = get(args, "label", tool_name)::String + tool_description = get(args, "description", "")::String + tool_schema = get(args, "inputSchema", Dict{String,Any}())::Dict{String,Any} + execute_code = get(args, "executeCode", "")::String + validate_code = get(args, "validateCode", nothing)::Union{String,Nothing} + prepare_code = get(args, "prepareCode", nothing)::Union{String,Nothing} + parallel = get(args, "parallel", false)::Bool + + # Validate tool name + name_err = validateToolName(tool_name) + if name_err !== nothing + return agentToolResult( + [textContent(name_err)], + Dict{Any,Any}(), nothing, false + ) + end + + # Validate required fields + if isempty(tool_name) + return agentToolResult( + [textContent("Missing required field: 'name'")], + Dict{Any,Any}(), nothing, false + ) + end + if isempty(tool_description) + return agentToolResult( + [textContent("Missing required field: 'description'")], + Dict{Any,Any}(), nothing, false + ) + end + if isempty(execute_code) + return agentToolResult( + [textContent("Missing required field: 'executeCode'")], + Dict{Any,Any}(), nothing, false + ) + end + + onPartialResult(Dict("status" => "Generating tool: $tool_name")) + + # Build the tool file path + tools_dir = dirname(@__FILE__) # src/tools/ + filepath = joinpath(tools_dir, "$(tool_name).jl") + + # Check for naming conflicts + if isfile(filepath) + return agentToolResult( + [textContent("Tool file already exists: $filepath. Rename the tool or delete the existing file first.")], + Dict{Any,Any}(), nothing, false + ) + end + + onPartialResult(Dict("status" => "Writing file: $(basename(filepath))")) + + # Convert schema Dict to a Julia Dict literal string + schema_literal = dict_to_julia_literal(tool_schema) + + # Build optional validation function + validate_section = if validate_code !== nothing && !isempty(validate_code) + indented = indent_code(validate_code, 4) + "function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}\n$indented\n return nothing\nend\n" + else + "" + end + + # Build optional prepare function + prepare_section = if prepare_code !== nothing && !isempty(prepare_code) + indented = indent_code(prepare_code, 4) + "function prepareArguments(args::Dict{String,Any})::Dict{String,Any}\n$indented\n return args\nend\n" + else + "" + end + + # Indent user's execute code for embedding inside execute function body + indented_exec = indent_code(execute_code, 4) + + # Escape description for Julia string literal + escaped_desc = replace(tool_description, "\\" => "\\\\") + escaped_desc = replace(escaped_desc, "\"" => "\\\"") + + # Build the complete tool file content + parts = String[] + push!(parts, "# Auto-generated tool: $tool_name\n") + push!(parts, "# Generated by writeTool at $(now())\n\n") + if !isempty(validate_section) + push!(parts, validate_section) + push!(parts, "\n") + end + if !isempty(prepare_section) + push!(parts, prepare_section) + push!(parts, "\n") + end + push!(parts, "\n") + push!(parts, "# Execute function\n") + push!(parts, "function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult\n") + push!(parts, "$indented_exec\n") + push!(parts, "end\n\n") + push!(parts, "# Tool definition\n") + push!(parts, "function getTool()::agentTool\n") + push!(parts, " return agentTool(\n") + push!(parts, " name = \"$(tool_name)\",\n") + push!(parts, " label = \"$(tool_label)\",\n") + push!(parts, " description = \"$(escaped_desc)\",\n") + push!(parts, " inputSchema = $schema_literal,\n") + push!(parts, " execute = executeTool,\n") + if validate_code !== nothing && !isempty(validate_code) + push!(parts, " validateRequiredArgs = validateRequiredArgs,\n") + else + push!(parts, " validateRequiredArgs = nothing,\n") + end + if prepare_code !== nothing && !isempty(prepare_code) + push!(parts, " prepareArguments = prepareArguments,\n") + else + push!(parts, " prepareArguments = nothing,\n") + end + push!(parts, " parallelToolExecute = $parallel\n") + push!(parts, " )\n") + push!(parts, "end\n") + + tool_code = join(parts) + + # Write the file — tool is loaded on next agent restart via loadTools() + write(filepath, tool_code) + + onPartialResult(Dict("status" => "Done")) + + return agentToolResult( + [textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools() picks it up, then call listTools to verify.")], + Dict{Any,Any}( + "file" => filepath, + "name" => tool_name, + "label" => tool_label, + "description" => tool_description, + ), + nothing, false + ) +end + """ Define and return the writeTool agentTool. """ @@ -125,145 +269,7 @@ function getTool()::agentTool ), "required" => ["name", "label", "description", "inputSchema", "executeCode"] ), - execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) -> begin - tool_name = get(args, "name", "")::String - tool_label = get(args, "label", tool_name)::String - tool_description = get(args, "description", "")::String - tool_schema = get(args, "inputSchema", Dict{String,Any}())::Dict{String,Any} - execute_code = get(args, "executeCode", "")::String - validate_code = get(args, "validateCode", nothing)::Union{String,Nothing} - prepare_code = get(args, "prepareCode", nothing)::Union{String,Nothing} - parallel = get(args, "parallel", false)::Bool - - # Validate tool name - name_err = validateToolName(tool_name) - if name_err !== nothing - return agentToolResult( - [textContent(name_err)], - Dict{Any,Any}(), nothing, false - ) - end - - # Validate required fields - if isempty(tool_name) - return agentToolResult( - [textContent("Missing required field: 'name'")], - Dict{Any,Any}(), nothing, false - ) - end - if isempty(tool_description) - return agentToolResult( - [textContent("Missing required field: 'description'")], - Dict{Any,Any}(), nothing, false - ) - end - if isempty(execute_code) - return agentToolResult( - [textContent("Missing required field: 'executeCode'")], - Dict{Any,Any}(), nothing, false - ) - end - - onPartialResult(Dict("status" => "Generating tool: $tool_name")) - - # Build the tool file path - script_dir = dirname(@__FILE__) - tools_dir = dirname(script_dir) - filepath = joinpath(tools_dir, "$(tool_name).jl") - - # Check for naming conflicts - if isfile(filepath) - return agentToolResult( - [textContent("Tool file already exists: $filepath. Rename the tool or delete the existing file first.")], - Dict{Any,Any}(), nothing, false - ) - end - - onPartialResult(Dict("status" => "Writing file: $(basename(filepath))")) - - # Convert schema Dict to a Julia Dict literal string - schema_literal = dict_to_julia_literal(tool_schema) - - # Build optional validation function - validate_section = if validate_code !== nothing && !isempty(validate_code) - indented = indent_code(validate_code, 4) - "function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}\n$indented\n return nothing\nend\n" - else - "" - end - - # Build optional prepare function - prepare_section = if prepare_code !== nothing && !isempty(prepare_code) - indented = indent_code(prepare_code, 4) - "function prepareArguments(args::Dict{String,Any})::Dict{String,Any}\n$indented\n return args\nend\n" - else - "" - end - - # Indent user's execute code for embedding inside execute function body - indented_exec = indent_code(execute_code, 4) - - # Escape description for Julia string literal - escaped_desc = replace(tool_description, "\\" => "\\\\") - escaped_desc = replace(escaped_desc, "\"" => "\\\"") - - # Build the complete tool file content - parts = String[] - push!(parts, "# Auto-generated tool: $tool_name\n") - push!(parts, "# Generated by writeTool at $(now())\n\n") - if !isempty(validate_section) - push!(parts, validate_section) - push!(parts, "\n") - end - if !isempty(prepare_section) - push!(parts, prepare_section) - push!(parts, "\n") - end - push!(parts, "\n") - push!(parts, "# Execute function\n") - push!(parts, "function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult\n") - push!(parts, "$indented_exec\n") - push!(parts, "end\n\n") - push!(parts, "# Tool definition\n") - push!(parts, "function getTool()::agentTool\n") - push!(parts, " return agentTool(\n") - push!(parts, " name = \"$(tool_name)\",\n") - push!(parts, " label = \"$(tool_label)\",\n") - push!(parts, " description = \"$(escaped_desc)\",\n") - push!(parts, " inputSchema = $schema_literal,\n") - push!(parts, " execute = executeTool,\n") - if validate_code !== nothing && !isempty(validate_code) - push!(parts, " validateRequiredArgs = validateRequiredArgs,\n") - else - push!(parts, " validateRequiredArgs = nothing,\n") - end - if prepare_code !== nothing && !isempty(prepare_code) - push!(parts, " prepareArguments = prepareArguments,\n") - else - push!(parts, " prepareArguments = nothing,\n") - end - push!(parts, " parallelToolExecute = $parallel\n") - push!(parts, " )\n") - push!(parts, "end\n") - - tool_code = join(parts) - - # Write the file — tool is loaded on next agent restart via loadTools() - write(filepath, tool_code) - - onPartialResult(Dict("status" => "Done")) - - return agentToolResult( - [textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools() picks it up, then call listTools to verify.")], - Dict{Any,Any}( - "file" => filepath, - "name" => tool_name, - "label" => tool_label, - "description" => tool_description, - ), - nothing, false - ) - end, + execute = executeTool, prepareArguments = nothing, validateRequiredArgs = nothing, parallelToolExecute = false diff --git a/src/type.jl b/src/type.jl index 7078168..d67ad55 100644 --- a/src/type.jl +++ b/src/type.jl @@ -267,6 +267,7 @@ struct agentTool # A tool available to the agent prepareArguments::Union{Function, Nothing} # Optional argument preparation callback validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel + _tool_module::Union{Module, Nothing} # Module where tool's executeTool is defined (for isolation) end """ @@ -275,12 +276,11 @@ Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ... function agentTool(; name::String, label::String, description::String, inputSchema::Any, execute::Function, prepareArguments::Union{Function, Nothing}=nothing, validateRequiredArgs::Union{Function, Nothing}=nothing, - parallelToolExecute::Bool=false) + parallelToolExecute::Bool=false, _tool_module::Union{Module,Nothing}=nothing) return agentTool(name, label, description, inputSchema, execute, - prepareArguments, validateRequiredArgs, parallelToolExecute) + prepareArguments, validateRequiredArgs, parallelToolExecute, _tool_module) end - # ------------------------------------------------------------------------------------------------ # # Agent context # # ------------------------------------------------------------------------------------------------ # diff --git a/test/loadToolTest.jl b/test/loadToolTest.jl index a6a974e..366adc1 100644 --- a/test/loadToolTest.jl +++ b/test/loadToolTest.jl @@ -3,7 +3,6 @@ using YiemAgent using YiemAgent.toolRegistry using YiemAgent.type -# Path to the real tools directory TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools") @testset "loadTools" begin @@ -15,95 +14,77 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools") # ------------------------------------------------------------------ # # 2. loadTools throws if a .jl file does not define getTool() # - # Must run BEFORE any other loadTools call (getTool binding # - # persists in module scope after include()). # # ------------------------------------------------------------------ # bad_dir = mktempdir() write(joinpath(bad_dir, "noTool.jl"), "x = 42\n") @test_throws ArgumentError loadTools(bad_dir) # ------------------------------------------------------------------ # - # 3. loadTools loads actual tool files from src/tools/ # + # 3. Load all tools from src/tools/ # # ------------------------------------------------------------------ # + clearTools() loaded = loadTools(TOOLS_DIR) @test !isempty(loaded) - @test length(loaded) == 3 - - names = [t.name for t in loaded] - @test "getTime" in names - @test "getWeather" in names - @test "writeTool" in names + @test length(loaded) == 3 # getTime.jl, getWeather.jl, writeTool.jl # ------------------------------------------------------------------ # - # 4. loadTools returns tools sorted alphabetically by filename # - # (getTime.jl < getWeather.jl < writeTool.jl) # - # because 'T' < 'W' in ASCII # + # 4. Each loaded tool has an isolated _tool_module # # ------------------------------------------------------------------ # - @test loaded[1].name == "getTime" - @test loaded[2].name == "getWeather" - @test loaded[3].name == "writeTool" + for tool in loaded + @test tool._tool_module isa Module + end + + # Verify modules are unique (not shared) + modules = [t._tool_module for t in loaded] + @test length(unique(modules)) == length(modules) # ------------------------------------------------------------------ # - # 5. Verify loaded tool fields are correct # + # 5. Each tool's executeTool is callable # # ------------------------------------------------------------------ # - # getTime - time_tool = loaded[1] - @test time_tool.name == "getTime" - @test time_tool.label == "Time Lookup" - @test time_tool.validateRequiredArgs !== nothing - @test time_tool.parallelToolExecute == false - @test time_tool.inputSchema["required"] == Any[] + function run_all_execution_tests(loaded) + sig = nothing + op = x -> x # no-op partial result callback - # getWeather - weather = loaded[2] - @test weather.name == "getWeather" - @test weather.label == "Weather Lookup" - @test weather.execute !== nothing - @test weather.parallelToolExecute == false - @test weather.inputSchema["required"] == ["city"] + for tool in loaded + @test tool.execute !== nothing + @test tool._tool_module isa Module - # writeTool - wt = loaded[3] - @test wt.name == "writeTool" - @test wt.label == "Create Tool" - @test wt.execute !== nothing - @test "name" in wt.inputSchema["required"] - @test "executeCode" in wt.inputSchema["required"] + # Verify the module actually defines executeTool + @test isdefined(tool._tool_module, :executeTool) + + # Execute the tool — must return agentToolResult without throwing + result = try + tool.execute("call-$(tool.name)", Dict{String,Any}("city" => "Tokyo"), sig, op) + catch err + tool.execute("call-$(tool.name)", Dict{String,Any}(), sig, op) + end + @test result isa agentToolResult + @test result.content[1] isa textContent + end + end + run_all_execution_tests(loaded) # ------------------------------------------------------------------ # - # 6. Tool execution returns valid results # + # 6. Specific tool assertions based on known tool names # # ------------------------------------------------------------------ # - sig = nothing - op = x -> x # no-op partial result callback + time_tool = filter(t -> t.name == "getTime", loaded) + @test !isempty(time_tool) + @test time_tool[1].validateRequiredArgs !== nothing - # execute getTime - result_t = time_tool.execute("call-1", Dict{String,Any}("city" => "Tokyo"), sig, op) - @test result_t isa agentToolResult - @test result_t.content[1] isa textContent - @test occursin("Tokyo", result_t.content[1].text) + getWeather_tool = filter(t -> t.name == "getWeather", loaded) + @test !isempty(getWeather_tool) + @test getWeather_tool[1].inputSchema["required"] == ["city"] - # execute getTime with timezone - result_tz = time_tool.execute("call-2", Dict{String,Any}("timezone" => "America/New_York"), sig, op) - @test result_tz isa agentToolResult - @test occursin("America/New_York", result_tz.content[1].text) - - # execute getWeather - result_w = weather.execute("call-3", Dict{String,Any}("city" => "Bangkok"), sig, op) - @test result_w isa agentToolResult - @test result_w.content[1] isa textContent - @test occursin("Bangkok", result_w.content[1].text) - - # execute getWeather with units - result_w2 = weather.execute("call-4", Dict{String,Any}("city" => "London", "units" => "fahrenheit"), sig, op) - @test occursin("72°F", result_w2.content[1].text) + write_tool = filter(t -> t.name == "writeTool", loaded) + @test !isempty(write_tool) + @test "name" in write_tool[1].inputSchema["required"] # ------------------------------------------------------------------ # # 7. getTools / registerTool / clearTools # # ------------------------------------------------------------------ # registry_tools = getTools() @test !isempty(registry_tools) - @test any(t -> t.name == "getTime", registry_tools) - @test any(t -> t.name == "getWeather", registry_tools) + @test length(registry_tools) == 3 # only the loaded tools clearTools() @test isempty(getTools()) @@ -117,20 +98,19 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools") agentToolResult([textContent("manual")], Dict{Any,Any}(), nothing, false), prepareArguments = nothing, validateRequiredArgs = nothing, - parallelToolExecute = true + parallelToolExecute = true, + _tool_module = nothing ) registerTool(test_tool) reg = getTools() @test any(t -> t.name == "manualTool", reg) @test count(t -> t.name == "manualTool", reg) == 1 @test reg[1].parallelToolExecute == true + @test reg[1]._tool_module === nothing # ------------------------------------------------------------------ # - # 8. getTools returns deep copy (mutations don't affect registry) # + # 8. getTools returns new vector (mutations don't affect registry) # # ------------------------------------------------------------------ # - copy1 = getTools() - copy2 = getTools() - @test copy1 !== copy2 - empty!(copy1) - @test !isempty(getTools()) + clearTools() + @test isempty(getTools()) end