""" Tool that writes new Julia tool module files to disk. The agent can use this tool when it encounters a task that no existing tool can handle. Provide the tool's name, label, description, inputSchema, and execute logic as Julia code. The tool is written to `src/tools/.jl`. After calling this tool, restart the agent so `loadTools("src/tools")` picks up the new file. The new tool is immediately available. # Example 1. Agent calls writeTool with a spec for a "searchWine" tool 2. writeTool generates src/tools/searchWine.jl 3. Restart agent — loadTools() picks up the new file 4. Agent calls searchWine with args # How It Works writeTool is a **file writer**, not a code generator. The LLM provides the tool logic as `executeCode`, and writeTool wraps it in Julia boilerplate: - Converts `inputSchema` Dict into Julia `Dict{String,Any}(...)` string - Indents `executeCode` with 4 spaces - Wraps it inside `function executeTool(...)::agentToolResult ... end` - Appends `getTool()` returning an `agentTool` struct - Writes the combined string to `src/tools/.jl` # Important Notes - The `executeCode` string is embedded literally into the generated tool. Use `args["param_name"]` to access input parameters. - The code string should be the function body (NOT wrapped in a function). Lines will be indented with 4 spaces inside the execute function. - Tool names must be valid Julia identifiers (lowercase letters, digits, underscores, no leading digits or special characters). """ """ Validate that a tool name is a valid Julia identifier. """ function validateToolName(name::String)::Union{Nothing,String} if !occursin(r"^[a-zA-Z_][a-zA-Z0-9_!]*$", name) return "Invalid tool name: '$name'. Tool names must be valid Julia identifiers (letters, digits, underscores, starting with a letter or underscore)." end return nothing 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 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 """ Indent a multi-line code string by the specified number of spaces. """ function indent_code(code::String, n::Int)::String prefix = " "^n lines = split(code, '\n') result_lines = String[prefix * line for line in lines] return join(result_lines, "\n") end """ Convert a Julia Dict to a valid Julia Dict{String,Any}(...) literal string. """ function dict_to_julia_literal(d)::String if d isa Dict items = String[] for (k, v) in d key_str = json_string(k) val_str = value_to_julia(v) push!(items, "$key_str => $val_str") end return "Dict{String,Any}(" * join(items, ", ") * ")" else return value_to_julia(d) end end function value_to_julia(v)::String if v isa Dict return dict_to_julia_literal(v) elseif v isa Vector items = [value_to_julia(x) for x in v] return "[" * join(items, ", ") * "]" elseif v isa String escaped = replace(v, "\\" => "\\\\") escaped = replace(escaped, "\"" => "\\\"") return "\"$escaped\"" elseif v isa Number return string(v) elseif v isa Bool return string(v) elseif v === nothing return "nothing" else return "\"$(v)\"" end end """ Convert any Julia value to a JSON string. """ function json_string(v)::String return JSON.json(v) end """ Define and return the writeTool agentTool. """ function getTool()::agentTool return agentTool( name = "writeTool", label = "Create Tool", description = "Write a new Julia tool module file to src/tools/.jl. The LLM provides the tool logic as executeCode; writeTool wraps it in Julia boilerplate and writes the file. Restart the agent to load the new tool.", inputSchema = Dict{String,Any}( "type" => "object", "properties" => Dict( "name" => Dict("type" => "string", "description" => "Unique tool name (valid Julia identifier, no spaces or special chars)"), "label" => Dict("type" => "string", "description" => "Human-readable tool name shown in tool descriptions"), "description" => Dict("type" => "string", "description" => "What the tool does (shown to LLM for tool selection decisions)"), "inputSchema" => Dict( "type" => "object", "description" => "JSON Schema describing tool parameters in MCP format" ), "executeCode" => Dict("type" => "string", "description" => "Julia code for the execute function body. Use args[\"key\"] to access parameters. Do NOT wrap in a function definition."), "validateCode" => Dict("type" => "string", "optional" => true, "description" => "Optional custom validation Julia code (runs before execute). Use args[\"key\"] to access parameters. Return nothing to pass, or a string error message to fail."), "prepareCode" => Dict("type" => "string", "optional" => true, "description" => "Optional argument preparation code (runs before validation). Return modified args dict."), "parallel" => Dict("type" => "boolean", "default" => false, "description" => "Whether this tool can run in parallel with other tools") ), "required" => ["name", "label", "description", "inputSchema", "executeCode"] ), execute = executeTool, prepareArguments = nothing, validateRequiredArgs = nothing, parallelToolExecute = false ) end