diff --git a/README.md b/README.md index 4702dd2..aa9f04c 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,34 @@ # YiemAgent -## TODO -- [WORKING] build prompt() -- [ ] build agent runLoop() -- [ ] build MCP server connector -- [ ] executeplan() to execute the plan -- [ ] add comprehensive tests +Julia framework for building agents with tool use. -## Changelog +## Getting Started -### Version 0.8.0 -- Converted snake_case fields to camelCase: - - `llmModel`: `base_url` → `baseUrl`, `context_window` → `contextWindow`, `max_tokens` → `maxTokens` -- Converted PascalCase type references to camelCase: - - `AgentState` → `agentState` - - `AgentTool` → `agentTool` - - `AgentMessage` → `agentMessage` - - `PendingMessageQueue` → `pendingMessageQueue` - - `ActiveRun` → `activeRun` - - `StreamFn` → `streamFn` - - `ThinkingLevel` → `thinkingLevel` - - `ToolExecutionMode` → `toolExecutionMode` +1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, ...` +2. Create a `yiemAgent` with `loadTools("src/tools")` +3. Call `run_agent(agent, "message")` then `take_response(agent)` + +## Architecture + +``` +src/ +├── YiemAgent.jl # Module entry point +├── type.jl # Core types (messages, tools, agent state) +├── utils.jl # Message formatting, validation +├── agentCore.jl # Agent loop, tool execution pipeline +├── api.jl # Public API (run_agent, take_response, etc.) +└── tools/ + ├── registry.jl # Tool registry (loadTools, registerTool, listTools) + ├── getWeather.jl # Weather lookup tool + ├── getTime.jl # Time lookup tool + ├── writeTool.jl # Create new tool files (self-modifying) + └── README.md # Tool development guide +``` + +## Tool Development + +See `src/tools/README.md` for: +- Tool anatomy (schema, execute, getTool) +- Validation hooks +- Agent loop lifecycle +- Self-modifying tools (`writeTool`) diff --git a/docs/loadtools.md b/docs/loadtools.md deleted file mode 100644 index 42f446c..0000000 --- a/docs/loadtools.md +++ /dev/null @@ -1,92 +0,0 @@ -# Dynamic Tool Loading - -Tools can be loaded dynamically from `.jl` files in the `src/tools/` directory without hardcoding filenames in the main module. - -## How It Works - -1. `src/tools/registry.jl` defines a `loadTools(dir::String)` function that scans a directory for `.jl` files -2. Each tool file must define a single function: `getTool()::agentTool` -3. `loadTools()` sorts files alphabetically, includes each one, calls `getTool()`, and registers the result -4. Loaded tools are returned as `Vector{agentTool}` for use when constructing a `yiemAgent` - -## Directory Structure - -``` -src/ -├── tools/ -│ ├── registry.jl # Tool loader (do not edit) -│ ├── getWeather.jl # Your tool -│ └── query_db.jl # Another tool -├── type.jl -├── utils.jl -├── agentCore.jl -├── api.jl -└── YiemAgent.jl -``` - -## Creating a Tool - -Each `.jl` file in `src/tools/` must define `getTool()` returning 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("Weather in $(city): Sunny, 22C")], - Dict{Any,Any}(), nothing, false - ) - end, - prepareArguments = nothing, - parallelToolExecute = false - ) -end -``` - -No `module` wrapper needed — the registry includes each file in the current module scope so all types (`agentTool`, `textContent`, `agentToolResult`, etc.) resolve correctly. - -## Loading Tools - -```julia -using .YiemAgent -using .YiemAgent: toolRegistry - -# Load all tool files from src/tools/ -tools = YiemAgent.loadTools(joinpath(@__DIR__, "src", "tools")) - -# Create agent with loaded tools -agent = yiemAgent( - systemPrompt = "You are a helpful assistant.", - model = my_model, - tools = tools, - llmCall = my_llm_call, - agentEventSink = my_event_sink -) -``` - -## Available Functions - -| Function | Description | -|----------|-------------| -| `loadTools(dir::String)` | Scan directory and load all `.jl` tool files | -| `registerTool(tool::agentTool)` | Register a single tool into the global registry | -| `getTools()` | Get deep copy of all registered tools | -| `listTools()` | List all registered tools as `(name, label)` pairs | -| `clearTools()` | Clear the global registry | - -## File Loading Order - -Files are sorted alphabetically before loading, so `01_database.jl` loads before `02_weather.jl`. This ensures deterministic registration order. diff --git a/src/tools/README.md b/src/tools/README.md index 9b7df7a..9e0c73c 100644 --- a/src/tools/README.md +++ b/src/tools/README.md @@ -399,15 +399,49 @@ The framework includes tools that allow the agent to create new tools at runtime ### `writeTool` — Create New Tool Files -The `writeTool` tool generates a new Julia tool file at `src/tools/.jl`. The agent writes the tool, then the agent (or system) restarts so `loadTools("src/tools")` picks it up. +`writeTool` is a **file writer**, not a code generator. The LLM provides the tool logic as `executeCode` (the actual Julia code), and `writeTool` wraps it in the required boilerplate. + +**How it works:** + +The LLM constructs `writeTool` with: +- **`executeCode`** — the actual tool logic (Julia code body, NOT wrapped in a function) +- **`name`, `label`, `description`** — tool metadata +- **`inputSchema`** — parameter schema in MCP format +- **`validateCode`, `prepareCode`** (optional) — custom validation/preparation logic + +`writeTool` produces `src/tools/.jl` by: +1. Converting the `inputSchema` Dict into a Julia `Dict{String,Any}(...)` string literal +2. Indenting `executeCode` with 4 spaces +3. Wrapping it inside a `function executeTool(...)::agentToolResult ... end` template +4. Appending the `getTool()` definition that returns an `agentTool` struct +5. Writing the combined string to disk **Workflow:** -1. Agent identifies a task that no existing tool can handle -2. Agent calls `writeTool` with a tool specification: +``` +LLM decides: "Need a searchWine tool. I'll provide the logic." + +LLM calls writeTool: + name: "searchWine" + executeCode: "query = args[\"query\"]\nresult = search(query)\nreturn ..." + +writeTool wraps it → src/tools/searchWine.jl: + function executeTool(...)::agentToolResult + query = args["query"] ← LLM code (indented 4 spaces) + result = search(query) + return agentToolResult(...) + end + + function getTool()::agentTool + return agentTool(name="searchWine", ...) + end + +Restart → loadTools("src/tools") loads searchWine.jl +``` + +**Example specification:** ```julia -# Agent sends this to writeTool: Dict( "name" => "searchWine", "label" => "Wine Search", @@ -431,45 +465,6 @@ Dict( ) ``` -3. Agent calls `listTools` to check for name collisions (built-in, auto-registered) -4. Agent calls `writeTool` with a unique name -5. Restart agent — `loadTools("src/tools")` picks up the new file -6. Agent's next LLM turn discovers and calls the new tool - -**Generated file format:** - -```julia -# Auto-generated tool: searchWine -# Generated by writeTool at 2026-08-08T14:00:00 - -function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult - query = args["query"] - max_results = get(args, "maxResults", 10) - result = "Found 3 wines matching: $query" - return agentToolResult([textContent(result)], Dict{Any,Any}(), nothing, false) -end - -function getTool()::agentTool - return agentTool( - name = "searchWine", - label = "Wine Search", - description = "Search a wine database by name, region, or variety", - inputSchema = Dict{String,Any}( - "type" => "object", - "properties" => Dict( - "query" => Dict("type" => "string", "description" => "Search query"), - "maxResults" => Dict("type" => "integer", "default" => 10) - ), - "required" => ["query"] - ), - execute = executeTool, - validateRequiredArgs = nothing, - prepareArguments = nothing, - parallelToolExecute = false - ) -end -``` - **Optional hooks:** | Field | Description | @@ -479,7 +474,7 @@ end ### `listTools` — Discover Available Tools -Returns a list of all registered tools with names, labels, and descriptions. +Returns all registered tools. Primarily useful for **collision detection** before creating a new tool via `writeTool` — the LLM checks existing names before picking a unique one. ```julia # Result from listTools: @@ -497,20 +492,18 @@ User: "I need to search for wines. Do you have a tool for that?" # ─── LOOP: Agent realizes no wine search tool exists ───────────────── -[Tool Call] listTools() -# Result: lists all available tools — no collision with existing tools +# LLM generates the tool logic and calls writeTool to write it to disk [Tool Call] writeTool(name="searchWine", label="Wine Search", description="Search a wine database by name, region, or variety", inputSchema={...}, executeCode="query = args[\"query\"]\nresult = \"Found wines...\"\nreturn agentToolResult([textContent(result)], ...)") -# writeTool generates searchWine.jl +# writeTool generates src/tools/searchWine.jl # ─── SYSTEM RESTARTS ───────────────────────────────────────────────── -# Agent restarts — loadTools("src/tools") picks up searchWine.jl -# searchWine is now available — agent uses it directly +# loadTools("src/tools") loads searchWine.jl alongside all other tools # ─── Agent calls the new tool ───────────────────────────────────────── diff --git a/src/tools/writeTool.jl b/src/tools/writeTool.jl index a318232..fb2b969 100644 --- a/src/tools/writeTool.jl +++ b/src/tools/writeTool.jl @@ -1,13 +1,12 @@ """ -Tool that generates new Julia tool module files. +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 the new tool is loaded by -`loadTools("src/tools")`. Then call `listTools` to verify the new tool -is available. +After calling this tool, restart the agent so `loadTools("src/tools")` picks +up the new file. The new tool is immediately available. # Example @@ -16,6 +15,16 @@ is available. 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. @@ -243,7 +252,7 @@ function getTool()::agentTool return agentTool( name = "writeTool", label = "Create Tool", - description = "Generate a new Julia tool module file at src/tools/.jl. After creation, restart the agent and call listTools to verify the new tool is loaded.", + 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(