static tool loading
This commit is contained in:
+259
-285
@@ -9,7 +9,7 @@ This document describes the complete tool lifecycle in the YiemAgent framework,
|
||||
1. [Quick Start: Tool Lifecycle](#1-quick-start-tool-lifecycle)
|
||||
2. [Overview](#2-overview)
|
||||
3. [Tool Definition — The `agentTool` Struct](#3-tool-definition--the-agenttool-struct)
|
||||
4. [Tool Registration — Per-Agent Tool Stores](#4-tool-registration--per-agent-tool-stores)
|
||||
4. [Tool Registration — Static Registration](#4-tool-registration--static-registration)
|
||||
5. [The Agent Loop — High-Level Flow](#5-the-agent-loop--high-level-flow)
|
||||
6. [Message Processing Pipeline](#6-message-processing-pipeline)
|
||||
7. [Tool Call Extraction from LLM Response](#7-tool-call-extraction-from-llm-response)
|
||||
@@ -23,7 +23,8 @@ This document describes the complete tool lifecycle in the YiemAgent framework,
|
||||
15. [Self-Modifying Tools](#15-self-modifying-tools)
|
||||
16. [Complete End-to-End Example](#16-complete-end-to-end-example)
|
||||
17. [Tool File Contract](#17-tool-file-contract)
|
||||
18. [Appendix: Type Reference](#18-appendix-type-reference)
|
||||
18. [Adding New Tools](#18-adding-new-tools)
|
||||
19. [Appendix: Type Reference](#19-appendix-type-reference)
|
||||
|
||||
---
|
||||
|
||||
@@ -43,76 +44,52 @@ tool = listTool(store) # Returns an agentTool that, when executed, lists all to
|
||||
**Result extraction:**
|
||||
```julia
|
||||
result = tool.execute("call-1", Dict{String,Any}(), nothing, x->x)
|
||||
# result.content[1].text => "Available tools:\n- getTime: Time Lookup — Get current local time...\n- getWeather: Weather Lookup — Fetch current weather..."
|
||||
# result.content[1].text => "Available tools:\n- getWeather: Weather Lookup — Fetch current weather...\n- getTime: Time Lookup — Get current local time..."
|
||||
```
|
||||
|
||||
**Source:** `toolRegistry.jl:54-82`
|
||||
**Source:** `toolRegistry.jl:43-98`
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Load — `loadTools()`
|
||||
### Step 2: Register — `register_all_tools()`
|
||||
|
||||
Load all tool modules from a directory into a `toolStore`. Each `.jl` file must define `getTool()::agentTool`. `listTool` is auto-registered so the LLM can discover available tools.
|
||||
Tools are statically defined in `src/tools/` and registered at module initialization via `register_all_tools()`. Each tool function (e.g., `getWeatherTool()`, `getTimeTool()`, `writeToolTool()`) is called to create the `agentTool` struct. `listTool` is auto-registered so the LLM can discover available tools.
|
||||
|
||||
```julia
|
||||
using YiemAgent, YiemAgent.toolRegistry
|
||||
|
||||
store = toolStore(name="myAgent")
|
||||
tools = loadTools(store, "src/tools")
|
||||
# Scans src/tools/ for .jl files, wraps each in a submodule, calls getTool(), registers in store.tools
|
||||
tools = register_all_tools(store)
|
||||
# Calls getWeatherTool(), getTimeTool(), writeToolTool() to create agentTool structs
|
||||
# Also auto-registers listTools for runtime discovery
|
||||
```
|
||||
|
||||
**Result extraction:**
|
||||
```julia
|
||||
all_tools = getTools(store) # OrderedDict{String, agentTool}
|
||||
# Keys: "getTime", "getWeather", "writeTool", "listTools"
|
||||
getTime_tool = all_tools["getTime"]
|
||||
# Keys: "getWeather", "getTime", "writeTool", "listTools"
|
||||
getWeather_tool = all_tools["getWeather"]
|
||||
|
||||
# Manual registration (alternative to loadTools)
|
||||
# Manual registration (alternative to register_all_tools)
|
||||
registerTool(store, my_tool)
|
||||
clearTools(store) # Clear all tools from store
|
||||
```
|
||||
|
||||
**Source:** `toolRegistry.jl:126-178`
|
||||
**Source:** `toolRegistry.jl:38-40, 127-143`
|
||||
|
||||
---
|
||||
|
||||
### Step 2.5: Create Agent with Tools
|
||||
|
||||
Wire the loaded tools into a new `yiemAgent` instance. The `tools` parameter is deep-copied into `agent._state.tools`; `_tool_store` is kept for runtime registration.
|
||||
Wire the registered tools into a new `yiemAgent` instance. The `yiemAgent` constructor calls `register_all_tools()` automatically.
|
||||
|
||||
```julia
|
||||
using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry
|
||||
|
||||
# 1. Set up toolStore and load tools (auto-registers listTools)
|
||||
store = toolStore(name="myAgent")
|
||||
loadTools(store, "src/tools")
|
||||
|
||||
# 2. Create agent — pass tools + _tool_store
|
||||
# Create agent — tools are registered automatically via register_all_tools()
|
||||
agent = yiemAgent(
|
||||
systemPrompt = "You are a helpful assistant.",
|
||||
model = my_model,
|
||||
tools = getTools(store), # OrderedDict{String, agentTool}
|
||||
llmCall = my_llm_call, # Function that calls the LLM API
|
||||
agentEventSink = my_event_sink, # Function for TUI/logging
|
||||
_tool_store = store, # For runtime registerTool() calls
|
||||
)
|
||||
```
|
||||
|
||||
**Manual registration** (without `loadTools`):
|
||||
|
||||
```julia
|
||||
store = toolStore(name="myAgent")
|
||||
registerTool(store, getTime_tool)
|
||||
registerTool(store, getWeather_tool)
|
||||
registerTool(store, listTool(store)) # needed for manual registration
|
||||
|
||||
agent = yiemAgent(
|
||||
tools = getTools(store),
|
||||
llmCall = my_llm_call,
|
||||
agentEventSink = my_event_sink,
|
||||
_tool_store = store,
|
||||
)
|
||||
```
|
||||
|
||||
@@ -120,17 +97,15 @@ agent = yiemAgent(
|
||||
|
||||
| Parameter | Type | Required | Purpose |
|
||||
|-----------|------|----------|---------|
|
||||
| `systemPrompt` | `String` | No (default: "You are helpful assistant.") | System prompt text |
|
||||
| `model` | `llmModel` | No | LLM model config |
|
||||
| `tools` | `OrderedDict{String, agentTool}` | No | Available tools (deep-copied) |
|
||||
| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history |
|
||||
| `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM |
|
||||
| `agentEventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
|
||||
| `_tool_store` | `toolStore` | No | Runtime tool registry for `registerTool()` |
|
||||
| `systemPrompt` | `String` | No (default: "You are helpful assistant.") | System prompt text |
|
||||
| `model` | `llmModel` | No | LLM model config |
|
||||
| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history |
|
||||
|
||||
Optional hooks: `prepareContext`, `formatMsgForLLM`, `beforeToolCall`, `afterToolCall`, `sessionId`, `maxRetryDelayMs`, `parallelToolExecute`.
|
||||
|
||||
**Source:** `type.jl:609-657`, `toolRegistry.jl:38-40, 191-195`
|
||||
**Source:** `type.jl:609-657`, `toolRegistry.jl:127-143`
|
||||
|
||||
---
|
||||
|
||||
@@ -146,13 +121,13 @@ sig = nothing
|
||||
op = x -> x # no-op partial result callback
|
||||
|
||||
# Execute a loaded tool directly
|
||||
result = getTime_tool.execute("call-1", Dict("city" => "Tokyo"), sig, op)
|
||||
result = getWeather_tool.execute("call-1", Dict("city" => "Tokyo"), sig, op)
|
||||
```
|
||||
|
||||
**Via agent loop (production):**
|
||||
```
|
||||
user message → runAgent(agent, Dict("role"=>"user", "content"=>...))
|
||||
→ _agentLoop detects message → @spawn _process_message(agent)
|
||||
→ _agentLoop detects message → @spawn _processMessage(agent)
|
||||
→ prepareContext → formatMsgForLLM → llmCall
|
||||
→ LLM returns tool_calls
|
||||
→ executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||
@@ -168,10 +143,10 @@ user message → runAgent(agent, Dict("role"=>"user", "content"=>...))
|
||||
|
||||
**`agentToolResult`** (raw tool output, `type.jl:429-434`):
|
||||
```julia
|
||||
result = getTime_tool.execute("call-1", Dict("city" => "Tokyo"), nothing, x->x)
|
||||
result = getWeather_tool.execute("call-1", Dict("city" => "Tokyo"), nothing, x->x)
|
||||
|
||||
result.content[1] # textContent("Current time in Tokyo: ...")
|
||||
result.content[1].text # "Current time in Tokyo: 2026-08-10T..."
|
||||
result.content[1] # textContent("Weather in Tokyo: Sunny, 22°C")
|
||||
result.content[1].text # "Weather in Tokyo: Sunny, 22°C"
|
||||
result.details # Dict{Any,Any}() — tool-specific metadata
|
||||
result.usage # nothing — llmUsage tracking (optional)
|
||||
result.terminate # false — signals loop termination
|
||||
@@ -182,7 +157,7 @@ result.terminate # false — signals loop termination
|
||||
msg = batch.messages[1] # toolResultMessage
|
||||
|
||||
msg.toolCallId # "call-1"
|
||||
msg.toolName # "getTime"
|
||||
msg.toolName # "getWeather"
|
||||
msg.content # Vector{messageContent}
|
||||
msg.isError # false
|
||||
msg.details # tool-specific metadata
|
||||
@@ -204,7 +179,7 @@ Each phase has a single responsibility and produces an intermediate result:
|
||||
| Phase | Function | Input | Output | Purpose |
|
||||
|-------|----------|-------|--------|---------|
|
||||
| Prepare | `prepareToolCall()` | `agentContext`, `assistantMessage`, `agentToolCall`, `agentLoopConfig`, `abortSignal` | `preparedToolCall` or `immediateOutcome` | Resolve tool, validate args, run pre-hook |
|
||||
| Execute | `executePreparedToolCall()` | `preparedToolCall`, `abortSignal`, `emit` | `executedOutcome` | Call `tool.execute()`, stream partial results |
|
||||
| Execute | `executePreparedToolCall()` | `preparedToolCall`, `abortSignal`, `agentEventSink` | `executedOutcome` | Call `tool.execute()`, stream partial results |
|
||||
| Finalize | `finalizeExecutedToolCall()` | `agentContext`, `assistantMessage`, `preparedToolCall`, `executedOutcome`, `agentLoopConfig`, `abortSignal` | `finalizedOutcome` | Run post-hook, emit end event |
|
||||
|
||||
The pipeline ensures that **every tool call produces a result**, even on failure. Errors are captured as `immediateOutcome`, `executedOutcome`, or `finalizedOutcome` with `isError=true`, then converted to `toolResultMessage` objects that are fed back to the LLM conversation history.
|
||||
@@ -276,9 +251,9 @@ The `terminate` flag is checked at the batch level. See [Section 10](#10-tool-ca
|
||||
|
||||
---
|
||||
|
||||
## 4. Tool Registration — Per-Agent Tool Stores
|
||||
## 4. Tool Registration — Static Registration
|
||||
|
||||
**Source:** `toolRegistry.jl`
|
||||
**Source:** `toolRegistry.jl`, `YiemAgent.jl`
|
||||
|
||||
### How `toolStore` Works
|
||||
|
||||
@@ -293,36 +268,25 @@ end
|
||||
|
||||
`store.tools` is an `OrderedDict` — it provides O(1) lookup by tool name and preserves insertion order for iteration. `getTools(store)` returns this `OrderedDict` directly (not a copy), so mutations on the returned value affect the store.
|
||||
|
||||
### How `loadTools(store, dir)` Works
|
||||
### Static Registration — `register_all_tools()`
|
||||
|
||||
```julia
|
||||
function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool}
|
||||
function register_all_tools(store::toolStore)::OrderedDict{String, agentTool}
|
||||
```
|
||||
|
||||
**Source:** `toolRegistry.jl:126-178`
|
||||
**Source:** `YiemAgent.jl:20-29`
|
||||
|
||||
1. **Scans** `dir` for `.jl` files (excluding files matching `registry` in name)
|
||||
2. **Sorts** filenames alphabetically for deterministic registration order
|
||||
3. **Wraps** each file in a dynamically created submodule:
|
||||
```julia
|
||||
# For "getWeather.jl" → module _tool_getWeather
|
||||
module _tool_getWeather
|
||||
using ..type
|
||||
using Dates, UUIDs, DataStructures, JSON
|
||||
# (file contents here)
|
||||
end
|
||||
```
|
||||
4. **Evaluates** `getTool()` within the submodule scope using `Core.eval(mod, :(getTool()))` — this avoids world-age issues
|
||||
5. **Validates** the return value is an `agentTool` instance
|
||||
6. **Registers** the tool in `store.tools`
|
||||
7. **Auto-registers** `listTool(store)` so the LLM can discover available tools at runtime
|
||||
1. **Calls each tool's definition function** — `getWeatherTool()`, `getTimeTool()`, `writeToolTool()` — which return `agentTool` structs
|
||||
2. **Registers each tool** via `registerTool(store, tool)`
|
||||
3. **Auto-registers** `listTool(store)` so the LLM can discover available tools at runtime
|
||||
|
||||
### Why Submodules?
|
||||
### Why Static?
|
||||
|
||||
Each tool file is loaded into its own **namespaced submodule**. This means:
|
||||
- `validateRequiredArgs`, `prepareArguments`, `executeTool`, and helper functions defined in `getTime.jl` are scoped under `_tool_getTime`
|
||||
- No name collisions between tools — `getTime.validateRequiredArgs` is distinct from `getWeather.validateRequiredArgs`
|
||||
- The module reference is kept alive by the functions stored in `agentTool` (closures in `execute`, `validateRequiredArgs`, `prepareArguments`) so they don't get garbage collected
|
||||
Tools are **statically included** in `YiemAgent.jl` via `include()`. This means:
|
||||
- Tool functions live in the `YiemAgent` module, not in dynamically created submodules
|
||||
- No world-age issues when calling `tool.execute()` (Julia compiles dispatch in the same world)
|
||||
- Simpler tool definition — no need to wrap in a `module ... end` block
|
||||
- Better compiler optimization (inlining, type inference)
|
||||
|
||||
### Registration API
|
||||
|
||||
@@ -331,9 +295,9 @@ Each tool file is loaded into its own **namespaced submodule**. This means:
|
||||
store1 = toolStore(name="agent1")
|
||||
store2 = toolStore(name="agent2")
|
||||
|
||||
# Load tools into specific stores (auto-registers listTools)
|
||||
tools1 = loadTools(store1, "src/tools/weather_tools") # agent1 only
|
||||
tools2 = loadTools(store2, "src/tools/wine_tools") # agent2 only
|
||||
# Load all tools (auto-registers listTools)
|
||||
tools1 = register_all_tools(store1) # all agents get the same tools
|
||||
tools2 = register_all_tools(store2)
|
||||
|
||||
# Manual registration (per-store)
|
||||
registerTool(store1, my_tool)
|
||||
@@ -359,8 +323,8 @@ Each `toolStore` is completely independent — tools registered in one store do
|
||||
storeA = toolStore(name="A")
|
||||
storeB = toolStore(name="B")
|
||||
|
||||
registerTool(storeA, getTime_tool)
|
||||
registerTool(storeB, getWeather_tool)
|
||||
registerTool(storeA, getTimeTool())
|
||||
registerTool(storeB, getWeatherTool())
|
||||
|
||||
getTools(storeA) # only contains getTime
|
||||
getTools(storeB) # only contains getWeather
|
||||
@@ -385,7 +349,6 @@ yiemAgent struct contains:
|
||||
- inputChannel (Channel, capacity 16) ← user sends messages here via runAgent()
|
||||
- followUpChannel (Channel, capacity 32) ← user sends follow-ups here via followUp()
|
||||
- outputChannel (Channel, capacity 16) ← agent sends responses here via takeResponse()
|
||||
- _tool_store (toolStore) ← per-agent isolated tool registry
|
||||
```
|
||||
|
||||
### Loop States
|
||||
@@ -395,7 +358,7 @@ The loop tracks 6 states (documented at `agentCore.jl:39-75`):
|
||||
| State | `processingTask` | `activeRun` | `inputChannel` | `followUpChannel` | Behavior |
|
||||
|-------|-----------------|-------------|----------------|-------------------|----------|
|
||||
| 1 | `nothing` | `false` | empty | empty | Idle, waiting |
|
||||
| 2 | `nothing` | `false` | has msg | empty | New message → spawn `_process_message` |
|
||||
| 2 | `nothing` | `false` | has msg | empty | New message → spawn `_processMessage` |
|
||||
| 3 | running | `true` | empty | empty | Processing, no new input |
|
||||
| 4 | running | `true` | has msg | empty | New message while busy → queued |
|
||||
| 5 | running | `true` | empty | has msg | Follow-up while busy → queued |
|
||||
@@ -414,9 +377,9 @@ function _agentLoop(agent::yiemAgent)
|
||||
drain both channels, break loop
|
||||
end
|
||||
|
||||
# 3. If agent is idle, spawn _process_message
|
||||
# 3. If agent is idle, spawn _processMessage
|
||||
if agent._state.activeRun == false
|
||||
processingTask = Threads.@spawn _process_message(agent)
|
||||
processingTask = Threads.@spawn _processMessage(agent)
|
||||
agent._state.activeRun = true
|
||||
end
|
||||
|
||||
@@ -445,12 +408,12 @@ end
|
||||
|
||||
**Source:** `agentCore.jl:175-311`
|
||||
|
||||
`_process_message(agent)` is the core function that processes a batch of user messages through the LLM pipeline.
|
||||
`_processMessage(agent)` is the core function that processes a batch of user messages through the LLM pipeline.
|
||||
|
||||
### Pipeline Steps
|
||||
|
||||
```julia
|
||||
function _process_message(agent::yiemAgent)::assistantMessage
|
||||
function _processMessage(agent::yiemAgent)::assistantMessage
|
||||
final_response = nothing
|
||||
|
||||
while true # Loop until LLM returns response without tool calls
|
||||
@@ -466,38 +429,38 @@ function _process_message(agent::yiemAgent)::assistantMessage
|
||||
end
|
||||
|
||||
# ── Step 2: Prepare context ─────────────────────────────────
|
||||
preparedContext = agent.prepareContext(agent._state)
|
||||
state = agentState(systemPrompt, nothing, tools, messages)
|
||||
preparedContext = prepareContext(state, agentEventSink)
|
||||
# Default: deep copies systemPrompt, messages, tools from agentState → agentContext
|
||||
# Override point: filter tools, inject context, modify system prompt
|
||||
|
||||
# ── Step 3: Format for LLM ──────────────────────────────────
|
||||
formatted_messages = agent.formatMsgForLLM(preparedContext)
|
||||
formattedMessages = formatMsgForLLM(preparedContext, agentEventSink)
|
||||
# Converts agentContext → Dict("messages" => [...]) in OpenAI format
|
||||
# Wraps systemPrompt as system role, converts each messageContent block
|
||||
|
||||
# ── Step 4: Call LLM ────────────────────────────────────────
|
||||
response = agent.llmCall(formatted_messages)
|
||||
response = llmCall(formattedMessages)
|
||||
# Returns assistantMessage with content::Vector{messageContent}
|
||||
# Each content block has a type: "text", "thinking", or "tool_call"
|
||||
|
||||
# ── Step 5: Extract tool calls ──────────────────────────────
|
||||
has_tool_calls, tool_call_list = extract_tool_calls(response.content)
|
||||
hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response)
|
||||
# Inspects content blocks for "tool_calls" or "tool_call" Dict entries
|
||||
|
||||
# ── Step 6: Execute tool calls or return ────────────────────
|
||||
if has_tool_calls && !isempty(tool_call_list)
|
||||
if hasToolCalls && !isempty(toolCallList)
|
||||
# Build context and config
|
||||
context = agentContext(agent._state.systemPrompt, agent._state.messages, agent._state.tools)
|
||||
config = agentLoopConfig(agent._state.tools, agent.beforeToolCall, agent.afterToolCall, ...)
|
||||
signal = nothing
|
||||
emit = agent.agentEventSink
|
||||
context = agentContext(systemPrompt, messages, tools)
|
||||
config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential")
|
||||
signal = abortSignal(false)
|
||||
|
||||
# Execute tool calls (sequential or parallel)
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||
batch = executeToolCalls(context, assistant_msg, toolCallList, config, signal, agentEventSink)
|
||||
|
||||
# Save results to conversation history
|
||||
for tool_result in batch.messages
|
||||
push!(agent._state.messages, tool_result)
|
||||
push!(messages, tool_result)
|
||||
end
|
||||
|
||||
# Check termination
|
||||
@@ -517,10 +480,6 @@ function _process_message(agent::yiemAgent)::assistantMessage
|
||||
end
|
||||
```
|
||||
|
||||
### Debug Note
|
||||
|
||||
There is a deliberate `error(5555555)` at `agentCore.jl:214` that halts execution after the LLM call. This appears to be a debugging/staging marker. Remove or replace it before production use.
|
||||
|
||||
---
|
||||
|
||||
## 7. Tool Call Extraction from LLM Response
|
||||
@@ -652,51 +611,29 @@ function prepareToolCall(
|
||||
function executePreparedToolCall(
|
||||
prep::preparedToolCall,
|
||||
signal::Union{Nothing, abortSignal},
|
||||
emit::Function,
|
||||
agentEventSink,
|
||||
)::executedOutcome
|
||||
```
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. **Initialize streaming state:**
|
||||
```julia
|
||||
updateEvents = promise[] # vector to collect update event handles
|
||||
accepting = true # guard to prevent duplicate emissions
|
||||
```
|
||||
1. **Call `tool.execute()`:**
|
||||
```julia
|
||||
result = prep.tool.execute(
|
||||
prep.toolCall.id,
|
||||
prep.args,
|
||||
signal,
|
||||
agentEventSink # serves as onPartialResult callback
|
||||
)
|
||||
return executedOutcome(result, false)
|
||||
```
|
||||
|
||||
2. **Call `tool.execute()`:**
|
||||
```julia
|
||||
result = prep.tool.execute(
|
||||
prep.toolCall.id,
|
||||
prep.args,
|
||||
signal,
|
||||
partialResult -> begin
|
||||
if accepting
|
||||
push!(updateEvents,
|
||||
emit(toolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name,
|
||||
prep.toolCall.arguments, partialResult)))
|
||||
end
|
||||
end
|
||||
)
|
||||
```
|
||||
|
||||
3. **Wait for streaming to settle:**
|
||||
```julia
|
||||
accepting = false
|
||||
wait.(updateEvents) # wait for all pending update event handlers
|
||||
return executedOutcome(result, false)
|
||||
```
|
||||
|
||||
4. **On error:**
|
||||
```julia
|
||||
catch err
|
||||
accepting = false
|
||||
wait.(updateEvents)
|
||||
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||
end
|
||||
```
|
||||
|
||||
**Streaming design:** The `accepting` guard prevents emitting updates after the call completes. If the tool's `execute` function yields after emitting updates but before returning, no duplicate or stale updates are emitted.
|
||||
2. **On error:**
|
||||
```julia
|
||||
catch err
|
||||
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||
end
|
||||
```
|
||||
|
||||
### 8.3 Phase 3: Finalize — `finalizeExecutedToolCall()`
|
||||
|
||||
@@ -716,49 +653,36 @@ function finalizeExecutedToolCall(
|
||||
**Steps:**
|
||||
|
||||
1. **Extract execution result:**
|
||||
```julia
|
||||
result = executed.result
|
||||
isError = executed.isError
|
||||
```
|
||||
```julia
|
||||
result = executed.result
|
||||
isError = executed.isError
|
||||
```
|
||||
|
||||
2. **Run `afterToolCall` hook** — if `config.afterToolCall !== nothing`:
|
||||
- Passes `afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context)` and `signal`
|
||||
- Hook can mutate the result:
|
||||
```julia
|
||||
after = config.afterToolCall(afterToolCallContext(...))
|
||||
if after !== nothing
|
||||
result = merge(result, dict(
|
||||
:content => get(after, :content, result.content),
|
||||
:details => get(after, :details, result.details),
|
||||
:usage => get(after, :usage, result.usage),
|
||||
:terminate => get(after, :terminate, result.terminate)
|
||||
))
|
||||
isError = get(after, :isError, isError)
|
||||
end
|
||||
```
|
||||
- Common use cases: mask sensitive data, normalize usage, flip `terminate` based on business logic
|
||||
- On error: `result = createErrorToolResult(sprint(showerror, err)); isError = true`
|
||||
- Passes `afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context)` and `signal`
|
||||
- Hook can mutate the result:
|
||||
```julia
|
||||
after = config.afterToolCall(afterToolCallContext(...))
|
||||
if after !== nothing
|
||||
result = merge(result, dict(
|
||||
:content => get(after, :content, result.content),
|
||||
:details => get(after, :details, result.details),
|
||||
:usage => get(after, :usage, result.usage),
|
||||
:terminate => get(after, :terminate, result.terminate)
|
||||
))
|
||||
isError = get(after, :isError, isError)
|
||||
end
|
||||
```
|
||||
- Common use cases: mask sensitive data, normalize usage, flip `terminate` based on business logic
|
||||
- On error: `result = createErrorToolResult(sprint(showerror, err)); isError = true`
|
||||
|
||||
3. **Return:**
|
||||
```julia
|
||||
return finalizedOutcome(prep.toolCall, result, isError)
|
||||
```
|
||||
```julia
|
||||
return finalizedOutcome(prep.toolCall, result, isError)
|
||||
```
|
||||
|
||||
**Source:** `type.jl:787-791` — `finalizedOutcome` holds the original tool call reference, final result (post-hook), and error status.
|
||||
|
||||
### 8.4 Emission — `emitToolExecutionEnd()`
|
||||
|
||||
**Source:** `agentCore.jl:736-739`
|
||||
|
||||
```julia
|
||||
function emitToolExecutionEnd(finalized::finalizedOutcome, emit::Function)
|
||||
emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||
finalized.result, finalized.isError))
|
||||
end
|
||||
```
|
||||
|
||||
This is called immediately after finalization, before building the `toolResultMessage`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Execution Modes — Sequential vs Parallel
|
||||
@@ -774,7 +698,7 @@ function executeToolCalls(
|
||||
toolCalls::Vector{agentToolCall},
|
||||
config::agentLoopConfig,
|
||||
signal::Union{Nothing, abortSignal},
|
||||
emit::Function,
|
||||
agentEventSink,
|
||||
)::agentToolCallBatch
|
||||
```
|
||||
|
||||
@@ -809,18 +733,15 @@ function executeToolCallsSequential(...)::agentToolCallBatch
|
||||
messages = toolResultMessage[]
|
||||
|
||||
for tc in toolCalls
|
||||
emit(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
||||
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||
|
||||
if prep isa immediateOutcome
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
else
|
||||
executed = executePreparedToolCall(prep, signal, emit)
|
||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||
end
|
||||
|
||||
emitToolExecutionEnd(finalized, emit)
|
||||
push!(messages, createToolResultMessage(finalized))
|
||||
push!(finalizedCalls, finalized)
|
||||
|
||||
@@ -842,19 +763,15 @@ function executeToolCallsParallel(...)::agentToolCallBatch
|
||||
entries = union{finalizedOutcome, task{finalizedOutcome}}[]
|
||||
|
||||
for tc in toolCalls
|
||||
emit(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
||||
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||
|
||||
if prep isa immediateOutcome
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
emitToolExecutionEnd(finalized, emit)
|
||||
push!(entries, finalized) # immediate outcome — no task
|
||||
else
|
||||
task = task() do
|
||||
executed = executePreparedToolCall(prep, signal, emit)
|
||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||
emitToolExecutionEnd(finalized, emit)
|
||||
return finalized
|
||||
end
|
||||
schedule(task)
|
||||
@@ -923,12 +840,12 @@ From the type documentation (`type.jl:803-815`):
|
||||
| Unrecoverable error | A tool hits a fatal condition (auth token expired, database connection lost) |
|
||||
| Async handoff | A tool triggers a long-running external operation; the external system will later resume via `continue()` |
|
||||
|
||||
### Batch Processing in `_process_message()`
|
||||
### Batch Processing in `_processMessage()`
|
||||
|
||||
**Source:** `agentCore.jl:266-307`
|
||||
|
||||
```julia
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
||||
batch = executeToolCalls(context, response, tool_call_list, config, signal, agentEventSink)
|
||||
|
||||
# Save results to conversation history
|
||||
for tool_result in batch.messages
|
||||
@@ -985,9 +902,9 @@ function createToolResultMessage(f::finalizedOutcome)::toolResultMessage
|
||||
f.result.content, # content (Vector{messageContent})
|
||||
f.result.details, # details
|
||||
f.result.usage, # usage
|
||||
get(f.result, :addedToolNames, string[]), # addedToolNames (for dynamic tools)
|
||||
nothing, # addedToolNames (for dynamic tools)
|
||||
f.isError, # isError
|
||||
nowMillis(), # timestamp
|
||||
now(), # timestamp
|
||||
)
|
||||
end
|
||||
```
|
||||
@@ -1048,9 +965,6 @@ Tool call fails at any phase
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
emitToolExecutionEnd(finalized, emit)
|
||||
│
|
||||
▼
|
||||
createToolResultMessage(finalized)
|
||||
│
|
||||
▼
|
||||
@@ -1119,14 +1033,14 @@ end
|
||||
|-------|-------------|------|
|
||||
| `toolExecStartEvent` | `executeToolCalls*()` loop | Before `prepareToolCall()` for each tool call |
|
||||
| `toolExecUpdateEvent` | `executePreparedToolCall()` | Inside `onPartialResult` callback during `tool.execute()` |
|
||||
| `toolExecEndEvent` | `emitToolExecutionEnd()` | After `finalizeExecutedToolCall()` for each tool call |
|
||||
| `toolExecEndEvent` | `finalizeExecutedToolCall()` | After finalization for each tool call |
|
||||
|
||||
### Event Sink
|
||||
|
||||
The `emit` function is passed through the entire call chain:
|
||||
The `agentEventSink` function is passed through the entire call chain:
|
||||
|
||||
```julia
|
||||
emit = agent.agentEventSink # set during yiemAgent construction
|
||||
agentEventSink = agent.agentEventSink # set during yiemAgent construction
|
||||
```
|
||||
|
||||
The `agentEventSink` function is a user-provided callback that receives all events. This is typically used by:
|
||||
@@ -1143,8 +1057,8 @@ The `agentEventSink` function is a user-provided callback that receives all even
|
||||
|
||||
| Hook | Signature | Called | Purpose |
|
||||
|------|-----------|--------|---------|
|
||||
| `prepareContext` | `(state::agentState) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
|
||||
| `formatMsgForLLM` | `(ctx::agentContext) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
|
||||
| `prepareContext` | `(state::agentState, agentEventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
|
||||
| `formatMsgForLLM` | `(ctx::agentContext, agentEventSink) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
|
||||
| `llmCall` | `(messages::Dict) -> assistantMessage` | After formatting | Actually invoke the LLM API |
|
||||
| `beforeToolCall` | `(msgCtx::beforeToolCallContext, signal) -> Union{Nothing, Dict}` | In `prepareToolCall` | Ask for user permission, block execution, abort |
|
||||
| `afterToolCall` | `(afterToolCallContext::afterToolCallContext, signal) -> Union{Nothing, Dict}` | In `finalizeExecutedToolCall` | Mutate result, mask data, flip `terminate` |
|
||||
@@ -1211,7 +1125,7 @@ end
|
||||
**Source:** `utils.jl:111-125`
|
||||
|
||||
```julia
|
||||
function prepareContext(state::agentState)::agentContext
|
||||
function prepareContext(state::agentState, agentEventSink)::agentContext
|
||||
# TODO: filter tools from state.tools based on user intent
|
||||
filteredTools = state.tools
|
||||
|
||||
@@ -1238,7 +1152,7 @@ end
|
||||
Default implementation converts `agentContext` to OpenAI-compatible format:
|
||||
|
||||
```julia
|
||||
function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
||||
function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||
messages = Vector{Dict{String, Any}}()
|
||||
|
||||
# System prompt as system message
|
||||
@@ -1280,14 +1194,14 @@ The framework supports tools that modify the tool system itself at runtime.
|
||||
1. Converts `inputSchema` Dict into `Dict{String,Any}(...)` string literal
|
||||
2. Indents `executeCode` with 4 spaces
|
||||
3. Wraps it inside `function executeTool(...)::agentToolResult ... end`
|
||||
4. Appends `getTool()` returning an `agentTool` struct
|
||||
4. Appends `writeToolTool()` returning an `agentTool` struct
|
||||
5. Writes the combined string to `src/tools/<name>.jl`
|
||||
|
||||
### `listTool` — Discover Available Tools
|
||||
|
||||
**Source:** `toolRegistry.jl:55-82`
|
||||
**Source:** `toolRegistry.jl:43-98`
|
||||
|
||||
Each `toolStore` gets its own `listTool` instance bound to that store via `listTool(store)`, so each agent sees only its own tools. `loadTools` auto-registers one, so the LLM can discover available tools at runtime. Also useful for **collision detection** before creating a new tool via `writeTool`.
|
||||
Each `toolStore` gets its own `listTool` instance bound to that store via `listTool(store)`, so each agent sees only its own tools. `register_all_tools` auto-registers one, so the LLM can discover available tools at runtime. Also useful for **collision detection** before creating a new tool via `writeTool`.
|
||||
|
||||
### Self-Tooling Workflow
|
||||
|
||||
@@ -1301,9 +1215,11 @@ Each `toolStore` gets its own `listTool` instance bound to that store via `listT
|
||||
- executeCode: "query = args[\"query\"]\nresult = search(query)\n..."
|
||||
- (optional) validateCode, prepareCode
|
||||
3. writeTool generates src/tools/searchWine.jl
|
||||
4. Agent restarts (or hot-reloads) → loadTools(agent._tool_store, "src/tools") picks up the new file
|
||||
5. Agent calls searchWine(query="cabernet")
|
||||
6. Result: "Found 5 cabernet wines..."
|
||||
4. Developer adds `include("tools/searchWine.jl")` to YiemAgent.jl
|
||||
5. Developer adds `registerTool(store, searchWineTool())` to register_all_tools() in YiemAgent.jl
|
||||
6. Developer restarts Julia — new tool is loaded
|
||||
7. Agent calls searchWine(query="cabernet")
|
||||
8. Result: "Found 5 cabernet wines..."
|
||||
```
|
||||
|
||||
### `writeTool` Input Schema
|
||||
@@ -1328,14 +1244,14 @@ Each `toolStore` gets its own `listTool` instance bound to that store via `listT
|
||||
```
|
||||
USER SENDS MESSAGE
|
||||
└─> runAgent(agent, "What's the weather in Tokyo?")
|
||||
└─> put!(agent.inputChannel, Dict("role" => "user", "content" => [...]))
|
||||
└─> put!(agent.inputChannel, Dict("role" => "user", "content" => [...]))
|
||||
|
||||
|
||||
LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
|
||||
└─> _agentLoop: detects msg in inputChannel
|
||||
└─> Threads.@spawn _process_message(agent)
|
||||
└─> @spawn _processMessage(agent)
|
||||
|
||||
── _process_message ──────────────────────────────────────────────
|
||||
── _processMessage ──────────────────────────────────────────────
|
||||
│
|
||||
│ Step 1: Drain inputChannel
|
||||
│ raw_msg = Dict("role" => "user", "content" => [...])
|
||||
@@ -1367,36 +1283,34 @@ LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
|
||||
│
|
||||
│ Step 6: Execute tool calls
|
||||
│ context = agentContext(systemPrompt, messages, tools)
|
||||
│ config = agentLoopConfig(tools, beforeToolCall, afterToolCall, "sequential")
|
||||
│ batch = executeToolCalls(context, response, tool_call_list, config, nothing, emit)
|
||||
│ config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential")
|
||||
│ batch = executeToolCalls(context, response, tool_call_list, config, signal, agentEventSink)
|
||||
|
||||
|
||||
LOOP ITERATION 1 — executeToolCallsSequential
|
||||
│
|
||||
│ ── executeToolCallsSequential ──────────────────────────────
|
||||
│ │
|
||||
│ │ For tc = agentToolCall("call_1", "getWeather", ...):
|
||||
│ │
|
||||
│ │ emit(toolExecStartEvent("call_1", "getWeather", {"city": "Tokyo"}))
|
||||
│ │
|
||||
│ │ PREPARE:
|
||||
│ │ tool = context.tools["getWeather"] → found!
|
||||
│ │ validatedArgs = validateToolArguments(tool, tc)
|
||||
│ │ → validateRequiredArgs(Dict("city" => "Tokyo"), inputSchema) → passes
|
||||
│ │ beforeToolCall_hook(...) → nothing (skipped)
|
||||
│ │ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
|
||||
│ │
|
||||
│ │ EXECUTE:
|
||||
│ │ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, onPartialResult)
|
||||
│ │ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false)
|
||||
│ │ → executedOutcome(result, false)
|
||||
│ │
|
||||
│ │ FINALIZE:
|
||||
│ │ afterToolCall_hook(...) → nothing (skipped)
|
||||
│ │ → finalizedOutcome(tc, result, false)
|
||||
│ │
|
||||
│ │ emit(toolExecEndEvent("call_1", "getWeather", result, false))
|
||||
│ │ msg = createToolResultMessage(finalized)
|
||||
│ │ → toolResultMessage("tool", "call_1", "getWeather", [...], {}, nothing, [], false, ts)
|
||||
│ │
|
||||
│ └─> agentToolCallBatch([msg], false)
|
||||
│ For tc = agentToolCall("call_1", "getWeather", ...):
|
||||
│
|
||||
│ PREPARE:
|
||||
│ tool = context.tools["getWeather"] → found!
|
||||
│ validatedArgs = validateToolArguments(tool, tc)
|
||||
│ → validateRequiredArgs(Dict("city" => "Tokyo"), inputSchema) → passes
|
||||
│ beforeToolCall_hook(...) → nothing (skipped)
|
||||
│ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
|
||||
│
|
||||
│ EXECUTE:
|
||||
│ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, agentEventSink)
|
||||
│ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false)
|
||||
│ → executedOutcome(result, false)
|
||||
│
|
||||
│ FINALIZE:
|
||||
│ afterToolCall_hook(...) → nothing (skipped)
|
||||
│ → finalizedOutcome(tc, result, false)
|
||||
│
|
||||
│ msg = createToolResultMessage(finalized)
|
||||
│ → toolResultMessage("tool", "call_1", "getWeather", [...], {}, nothing, [], false, ts)
|
||||
│
|
||||
└─> agentToolCallBatch([msg], false)
|
||||
│
|
||||
│ Save results:
|
||||
│ for tool_result in batch.messages
|
||||
@@ -1408,7 +1322,7 @@ LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
|
||||
|
||||
|
||||
LOOP ITERATION 2 — LLM RETURNS FINAL TEXT RESPONSE
|
||||
── _process_message (second iteration) ───────────────────────────
|
||||
── _processMessage (second iteration) ───────────────────────────
|
||||
│
|
||||
│ Step 1: Drain inputChannel → empty
|
||||
│
|
||||
@@ -1437,12 +1351,12 @@ AGENT LOOP: SEND RESPONSE TO USER
|
||||
|
||||
## 17. Tool File Contract
|
||||
|
||||
Each `.jl` file in `src/tools/` must conform to the following contract:
|
||||
Each `.jl` file in `src/tools/` follows a flat, static structure:
|
||||
|
||||
### Required Function
|
||||
|
||||
```julia
|
||||
function getTool()::agentTool
|
||||
function <name>Tool()::agentTool
|
||||
# Must return an agentTool instance
|
||||
end
|
||||
```
|
||||
@@ -1451,22 +1365,22 @@ end
|
||||
|
||||
```julia
|
||||
# Argument preparation (before validation)
|
||||
function prepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
||||
function <name>PrepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
||||
# Return modified args, or args unchanged
|
||||
return args
|
||||
end
|
||||
|
||||
# Custom validation (before execution)
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
function <name>ValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
# Return nothing to pass, or error string to fail
|
||||
return nothing
|
||||
end
|
||||
|
||||
# Core execution
|
||||
function executeTool(toolCallId::String,
|
||||
args::Dict{String,Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
function <name>Execute(toolCallId::String,
|
||||
args::Dict{String,Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
# Return agentToolResult with content, details, usage, terminate
|
||||
return agentToolResult([textContent("result")], Dict{Any,Any}(), nothing, false)
|
||||
end
|
||||
@@ -1477,7 +1391,8 @@ end
|
||||
```julia
|
||||
# src/tools/myTool.jl
|
||||
|
||||
using Dates # ← tool declares its own dependencies (registry injects only `using ..type`)
|
||||
using .type # ← provides agentTool, textContent, agentToolResult, etc.
|
||||
using Dates # ← tool's own dependencies
|
||||
|
||||
# Optional: helper functions
|
||||
function helper_function(...)
|
||||
@@ -1485,24 +1400,24 @@ function helper_function(...)
|
||||
end
|
||||
|
||||
# Optional: prepareArguments
|
||||
function prepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
||||
function myToolPrepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
||||
return args
|
||||
end
|
||||
|
||||
# Optional: validateRequiredArgs
|
||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
function myToolValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||
return nothing
|
||||
end
|
||||
|
||||
# Required: executeTool
|
||||
function executeTool(toolCallId::String, args::Dict{String,Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
# Required: execute function
|
||||
function myToolExecute(toolCallId::String, args::Dict{String,Any},
|
||||
signal::Union{Nothing,abortSignal},
|
||||
onPartialResult::Function)::agentToolResult
|
||||
...
|
||||
end
|
||||
|
||||
# Required: getTool
|
||||
function getTool()::agentTool
|
||||
# Required: getTool function
|
||||
function myToolTool()::agentTool
|
||||
return agentTool(
|
||||
name = "myTool",
|
||||
label = "My Tool",
|
||||
@@ -1512,9 +1427,9 @@ function getTool()::agentTool
|
||||
"properties" => Dict(...),
|
||||
"required" => [...]
|
||||
),
|
||||
execute = executeTool,
|
||||
prepareArguments = prepareArguments,
|
||||
validateRequiredArgs = validateRequiredArgs,
|
||||
execute = myToolExecute,
|
||||
prepareArguments = myToolPrepareArguments,
|
||||
validateRequiredArgs = myToolValidateRequiredArgs,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
@@ -1522,51 +1437,110 @@ end
|
||||
|
||||
### Dependencies
|
||||
|
||||
Each tool file **declares its own dependencies** via `using` statements at the top of the file. The registry does **not** inject any standard library packages — if a tool needs `Dates`, `JSON`, `HTTP`, `CSV`, or any other package, it must include its own `using` statements.
|
||||
Each tool file declares its own dependencies via `using` statements:
|
||||
|
||||
```julia
|
||||
# src/tools/getTime.jl
|
||||
using .type
|
||||
using Dates
|
||||
|
||||
function executeTool(...)
|
||||
function getTimeExecute(...)
|
||||
now() # Dates.now requires `using Dates`
|
||||
end
|
||||
```
|
||||
|
||||
```julia
|
||||
# src/tools/myApiTool.jl
|
||||
using .type
|
||||
using HTTP, JSON
|
||||
|
||||
function executeTool(...)
|
||||
function myApiToolExecute(...)
|
||||
response = HTTP.get("https://api.example.com")
|
||||
data = JSON.parse(String(response.body))
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
### Module Isolation
|
||||
### Why Flat Modules?
|
||||
|
||||
When `loadTools()` loads a file, it wraps it in a dynamically created submodule. The registry injects **only** `using ..type` to make core types (`agentTool`, `textContent`, `agentToolResult`, `abortSignal`, etc.) available:
|
||||
|
||||
```julia
|
||||
# User writes in src/tools/myTool.jl:
|
||||
using Dates, HTTP, JSON # ← tool's own dependencies
|
||||
|
||||
function getTool()::agentTool ... end
|
||||
|
||||
# loadTools() creates:
|
||||
module _tool_myTool
|
||||
using ..type # ← injected by registry (core types only)
|
||||
using Dates, HTTP, JSON # ← from tool file
|
||||
# (user's code here)
|
||||
end
|
||||
```
|
||||
|
||||
All functions in the file are scoped under `_tool_myTool`, preventing name collisions with other tools. The module reference is kept alive by the function objects stored in `agentTool`, preventing garbage collection of closures.
|
||||
All tool files are **statically included** in `YiemAgent.jl` via `include()`. This means:
|
||||
- All functions live in the `YiemAgent` module, avoiding world-age issues
|
||||
- `using .type` makes core types (`agentTool`, `textContent`, `agentToolResult`, `abortSignal`) available
|
||||
- Functions are named with a `<toolName>` prefix to avoid name collisions (e.g., `getWeatherExecute`, `getTimeExecute`)
|
||||
- The `...Tool()` function (e.g., `getWeatherTool()`) returns the `agentTool` struct for registration
|
||||
|
||||
---
|
||||
|
||||
## 18. Appendix: Type Reference
|
||||
## 18. Adding New Tools
|
||||
|
||||
To add a new tool (e.g., `searchWine.jl`):
|
||||
|
||||
### Step 1: Create `src/tools/searchWine.jl`
|
||||
|
||||
```julia
|
||||
using .type
|
||||
# using AdditionalPkg # add if needed
|
||||
|
||||
function searchWineExecute(toolCallId::String, args::Dict{String,Any},
|
||||
signal::Union{Nothing,abortSignal}, onPartialResult)
|
||||
query = get(args, "query", "")
|
||||
result = search_wine_db(query)
|
||||
return agentToolResult(
|
||||
[textContent("Found $(length(result)) wines")],
|
||||
Dict{Any,Any}("count" => length(result)),
|
||||
nothing, false
|
||||
)
|
||||
end
|
||||
|
||||
function searchWineTool()::agentTool
|
||||
return agentTool(
|
||||
name = "searchWine",
|
||||
label = "Search Wine",
|
||||
description = "Search wine database...",
|
||||
inputSchema = Dict{String,Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"query" => Dict("type" => "string", "description" => "Search query")
|
||||
),
|
||||
"required" => ["query"]
|
||||
),
|
||||
execute = searchWineExecute,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing,
|
||||
parallelToolExecute = false
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
### Step 2: Include in `src/YiemAgent.jl` (before `toolRegistry.jl`)
|
||||
|
||||
```julia
|
||||
include("tools/getWeather.jl")
|
||||
include("tools/getTime.jl")
|
||||
include("tools/searchWine.jl") # ← add here
|
||||
include("tools/writeTool.jl")
|
||||
```
|
||||
|
||||
### Step 3: Register in `register_all_tools()` in `YiemAgent.jl`
|
||||
|
||||
```julia
|
||||
function register_all_tools(store::toolRegistry.toolStore)
|
||||
registerTool(store, getWeatherTool())
|
||||
registerTool(store, getTimeTool())
|
||||
registerTool(store, searchWineTool()) # ← add here
|
||||
registerTool(store, writeToolTool())
|
||||
registerTool(store, listTool(store))
|
||||
return store.tools
|
||||
end
|
||||
```
|
||||
|
||||
### Step 4: Restart Julia
|
||||
|
||||
The module recompiles on next load. The new tool is available immediately.
|
||||
|
||||
---
|
||||
|
||||
## 19. Appendix: Type Reference
|
||||
|
||||
### Message Types
|
||||
|
||||
@@ -1604,7 +1578,7 @@ All functions in the file are scoped under `_tool_myTool`, preventing name colli
|
||||
|------|--------|-------------|
|
||||
| `agentContext` | `type.jl:299` | Conversation snapshot (systemPrompt, messages, tools) |
|
||||
| `agentState` | `type.jl:310` | Mutable runtime state (systemPrompt, model, tools, messages, pendingToolCalls, activeRun, errorMessage) |
|
||||
| `agentLoopConfig` | `type.jl:403` | Loop config (tools, beforeToolCall, afterToolCall, toolExecution) |
|
||||
| `agentLoopConfig` | `type.jl:403` | Loop config (beforeToolCall, afterToolCall, toolExecution) |
|
||||
| `abortSignal` | `type.jl:416` | Abort flag (`aborted::Bool`) |
|
||||
| `beforeToolCallContext` | `type.jl:445` | Context for beforeToolCall (message, toolCall, args, context) |
|
||||
| `afterToolCallContext` | `type.jl:463` | Context for afterToolCall (message, toolCall, args, result, isError, context) |
|
||||
|
||||
Reference in New Issue
Block a user