update
This commit is contained in:
@@ -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`)
|
||||
|
||||
@@ -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
@@ -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/<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:**
|
||||
|
||||
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 ─────────────────────────────────────────
|
||||
|
||||
|
||||
+14
-5
@@ -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/<name>.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/<name>.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/<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}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
|
||||
Reference in New Issue
Block a user