This commit is contained in:
2026-08-08 19:44:06 +07:00
parent 03e1dd7628
commit 2aa0d1e9a4
4 changed files with 86 additions and 165 deletions
+30 -19
View File
@@ -1,23 +1,34 @@
# YiemAgent # YiemAgent
## TODO Julia framework for building agents with tool use.
- [WORKING] build prompt()
- [ ] build agent runLoop()
- [ ] build MCP server connector
- [ ] executeplan() to execute the plan
- [ ] add comprehensive tests
## Changelog ## Getting Started
### Version 0.8.0 1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, ...`
- Converted snake_case fields to camelCase: 2. Create a `yiemAgent` with `loadTools("src/tools")`
- `llmModel`: `base_url``baseUrl`, `context_window``contextWindow`, `max_tokens``maxTokens` 3. Call `run_agent(agent, "message")` then `take_response(agent)`
- Converted PascalCase type references to camelCase:
- `AgentState``agentState` ## Architecture
- `AgentTool``agentTool`
- `AgentMessage``agentMessage` ```
- `PendingMessageQueue``pendingMessageQueue` src/
- `ActiveRun``activeRun` ├── YiemAgent.jl # Module entry point
- `StreamFn``streamFn` ├── type.jl # Core types (messages, tools, agent state)
- `ThinkingLevel``thinkingLevel` ├── utils.jl # Message formatting, validation
- `ToolExecutionMode``toolExecutionMode` ├── 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`)
-92
View File
@@ -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.
+42 -49
View File
@@ -399,15 +399,49 @@ The framework includes tools that allow the agent to create new tools at runtime
### `writeTool` — Create New Tool Files ### `writeTool` — Create New Tool Files
The `writeTool` tool generates a new Julia tool file at `src/tools/<name>.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/<name>.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:** **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 ```julia
# Agent sends this to writeTool:
Dict( Dict(
"name" => "searchWine", "name" => "searchWine",
"label" => "Wine Search", "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:** **Optional hooks:**
| Field | Description | | Field | Description |
@@ -479,7 +474,7 @@ end
### `listTools` — Discover Available Tools ### `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 ```julia
# Result from listTools: # 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 ───────────────── # ─── LOOP: Agent realizes no wine search tool exists ─────────────────
[Tool Call] listTools() # LLM generates the tool logic and calls writeTool to write it to disk
# Result: lists all available tools — no collision with existing tools
[Tool Call] writeTool(name="searchWine", label="Wine Search", [Tool Call] writeTool(name="searchWine", label="Wine Search",
description="Search a wine database by name, region, or variety", description="Search a wine database by name, region, or variety",
inputSchema={...}, inputSchema={...},
executeCode="query = args[\"query\"]\nresult = \"Found wines...\"\nreturn agentToolResult([textContent(result)], ...)") executeCode="query = args[\"query\"]\nresult = \"Found wines...\"\nreturn agentToolResult([textContent(result)], ...)")
# writeTool generates searchWine.jl # writeTool generates src/tools/searchWine.jl
# ─── SYSTEM RESTARTS ───────────────────────────────────────────────── # ─── SYSTEM RESTARTS ─────────────────────────────────────────────────
# Agent restarts — loadTools("src/tools") picks up searchWine.jl # loadTools("src/tools") loads searchWine.jl alongside all other tools
# searchWine is now available — agent uses it directly
# ─── Agent calls the new tool ───────────────────────────────────────── # ─── Agent calls the new tool ─────────────────────────────────────────
+14 -5
View File
@@ -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 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 can handle. Provide the tool's name, label, description, inputSchema, and
execute logic as Julia code. The tool is written to `src/tools/<name>.jl`. execute logic as Julia code. The tool is written to `src/tools/<name>.jl`.
After calling this tool, restart the agent so the new tool is loaded by After calling this tool, restart the agent so `loadTools("src/tools")` picks
`loadTools("src/tools")`. Then call `listTools` to verify the new tool up the new file. The new tool is immediately available.
is available.
# Example # Example
@@ -16,6 +15,16 @@ is available.
3. Restart agent — loadTools() picks up the new file 3. Restart agent — loadTools() picks up the new file
4. Agent calls searchWine with args 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/<name>.jl`
# Important Notes # Important Notes
- The `executeCode` string is embedded literally into the generated tool. - The `executeCode` string is embedded literally into the generated tool.
@@ -243,7 +252,7 @@ function getTool()::agentTool
return agentTool( return agentTool(
name = "writeTool", name = "writeTool",
label = "Create Tool", label = "Create Tool",
description = "Generate a new Julia tool module file at src/tools/<name>.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/<name>.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}( inputSchema = Dict{String,Any}(
"type" => "object", "type" => "object",
"properties" => Dict( "properties" => Dict(