static tool loading
This commit is contained in:
+50
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
julia_version = "1.12.6"
|
julia_version = "1.12.6"
|
||||||
manifest_format = "2.0"
|
manifest_format = "2.0"
|
||||||
project_hash = "0db36d4fb31037ba05065476e6aebaf4cd0e1e8c"
|
project_hash = "aa163e2bf572632825162936e107be18384fd40f"
|
||||||
|
|
||||||
[[deps.Accessors]]
|
[[deps.Accessors]]
|
||||||
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
|
||||||
@@ -44,6 +44,12 @@ git-tree-sha1 = "d57bd3762d308bded22c3b82d033bff85f6195c6"
|
|||||||
uuid = "ec485272-7323-5ecc-a04f-4719b315124d"
|
uuid = "ec485272-7323-5ecc-a04f-4719b315124d"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
||||||
|
[[deps.Arrow]]
|
||||||
|
deps = ["ArrowTypes", "BitIntegers", "CodecLz4", "CodecZstd", "ConcurrentUtilities", "DataAPI", "Dates", "EnumX", "Mmap", "PooledArrays", "SentinelArrays", "StringViews", "Tables", "TimeZones", "TranscodingStreams", "UUIDs"]
|
||||||
|
git-tree-sha1 = "4a69a3eadc1f7da78d950d1ef270c3a62c1f7e01"
|
||||||
|
uuid = "69666777-d1a9-59fb-9406-91d4454c9d45"
|
||||||
|
version = "2.8.1"
|
||||||
|
|
||||||
[[deps.ArrowTypes]]
|
[[deps.ArrowTypes]]
|
||||||
deps = ["Sockets", "UUIDs"]
|
deps = ["Sockets", "UUIDs"]
|
||||||
git-tree-sha1 = "404265cd8128a2515a81d5eae16de90fdef05101"
|
git-tree-sha1 = "404265cd8128a2515a81d5eae16de90fdef05101"
|
||||||
@@ -58,6 +64,12 @@ version = "1.11.0"
|
|||||||
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
|
||||||
|
[[deps.BitIntegers]]
|
||||||
|
deps = ["Random"]
|
||||||
|
git-tree-sha1 = "091d591a060e43df1dd35faab3ca284925c48e46"
|
||||||
|
uuid = "c3b6d118-76ef-56ca-8cc7-ebb389d030a1"
|
||||||
|
version = "0.3.7"
|
||||||
|
|
||||||
[[deps.BufferedStreams]]
|
[[deps.BufferedStreams]]
|
||||||
git-tree-sha1 = "6863c5b7fc997eadcabdbaf6c5f201dc30032643"
|
git-tree-sha1 = "6863c5b7fc997eadcabdbaf6c5f201dc30032643"
|
||||||
uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d"
|
uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d"
|
||||||
@@ -90,12 +102,24 @@ git-tree-sha1 = "40956acdbef3d8c7cc38cba42b56034af8f8581a"
|
|||||||
uuid = "6c391c72-fb7b-5838-ba82-7cfb1bcfecbf"
|
uuid = "6c391c72-fb7b-5838-ba82-7cfb1bcfecbf"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
|
||||||
|
[[deps.CodecLz4]]
|
||||||
|
deps = ["Lz4_jll", "TranscodingStreams"]
|
||||||
|
git-tree-sha1 = "d58afcd2833601636b48ee8cbeb2edcb086522c2"
|
||||||
|
uuid = "5ba52731-8f18-5e0d-9241-30f10d1ec561"
|
||||||
|
version = "0.4.6"
|
||||||
|
|
||||||
[[deps.CodecZlib]]
|
[[deps.CodecZlib]]
|
||||||
deps = ["TranscodingStreams", "Zlib_jll"]
|
deps = ["TranscodingStreams", "Zlib_jll"]
|
||||||
git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9"
|
git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9"
|
||||||
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
|
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
|
||||||
version = "0.7.8"
|
version = "0.7.8"
|
||||||
|
|
||||||
|
[[deps.CodecZstd]]
|
||||||
|
deps = ["TranscodingStreams", "Zstd_jll"]
|
||||||
|
git-tree-sha1 = "da54a6cd93c54950c15adf1d336cfd7d71f51a56"
|
||||||
|
uuid = "6b39b394-51ab-5f42-8807-6242bab2b4c2"
|
||||||
|
version = "0.8.7"
|
||||||
|
|
||||||
[[deps.CommonSolve]]
|
[[deps.CommonSolve]]
|
||||||
git-tree-sha1 = "cf963add2340ad9960e5eb22844e61ad8f931fe1"
|
git-tree-sha1 = "cf963add2340ad9960e5eb22844e61ad8f931fe1"
|
||||||
uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
|
uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
|
||||||
@@ -130,6 +154,12 @@ weakdeps = ["InverseFunctions"]
|
|||||||
[deps.CompositionsBase.extensions]
|
[deps.CompositionsBase.extensions]
|
||||||
CompositionsBaseInverseFunctionsExt = "InverseFunctions"
|
CompositionsBaseInverseFunctionsExt = "InverseFunctions"
|
||||||
|
|
||||||
|
[[deps.ConcurrentUtilities]]
|
||||||
|
deps = ["Serialization", "Sockets"]
|
||||||
|
git-tree-sha1 = "3c9be947934c38475bafe822c6d61aaed17f0738"
|
||||||
|
uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb"
|
||||||
|
version = "2.6.0"
|
||||||
|
|
||||||
[[deps.ConstructionBase]]
|
[[deps.ConstructionBase]]
|
||||||
git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb"
|
git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb"
|
||||||
uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
|
uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
|
||||||
@@ -531,6 +561,12 @@ git-tree-sha1 = "1d4c737ab26f51ceed52ab2019c09b7660eb7440"
|
|||||||
uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
|
uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
|
||||||
version = "3.8.0"
|
version = "3.8.0"
|
||||||
|
|
||||||
|
[[deps.Lz4_jll]]
|
||||||
|
deps = ["Artifacts", "JLLWrappers", "Libdl"]
|
||||||
|
git-tree-sha1 = "191686b1ac1ea9c89fc52e996ad15d1d241d1e33"
|
||||||
|
uuid = "5ced341a-0733-55b8-9ab6-a4889d929147"
|
||||||
|
version = "1.10.1+0"
|
||||||
|
|
||||||
[[deps.MacroTools]]
|
[[deps.MacroTools]]
|
||||||
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
|
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
|
||||||
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
|
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
|
||||||
@@ -938,6 +974,11 @@ git-tree-sha1 = "8a90c1d77c3277a5d43b83927b3cbe2c70a37484"
|
|||||||
uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
|
uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
|
|
||||||
|
[[deps.StringViews]]
|
||||||
|
git-tree-sha1 = "f2dcb92855b31ad92fe8f079d4f75ac57c93e4b8"
|
||||||
|
uuid = "354b36f9-a18e-4713-926e-db85100087ba"
|
||||||
|
version = "1.3.7"
|
||||||
|
|
||||||
[[deps.StructTypes]]
|
[[deps.StructTypes]]
|
||||||
deps = ["Dates", "UUIDs"]
|
deps = ["Dates", "UUIDs"]
|
||||||
git-tree-sha1 = "159331b30e94d7b11379037feeb9b690950cace8"
|
git-tree-sha1 = "159331b30e94d7b11379037feeb9b690950cace8"
|
||||||
@@ -1080,6 +1121,14 @@ git-tree-sha1 = "011b0a7331b41c25524b64dc42afc9683ee89026"
|
|||||||
uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8"
|
uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8"
|
||||||
version = "1.0.21+0"
|
version = "1.0.21+0"
|
||||||
|
|
||||||
|
[[deps.msghandler]]
|
||||||
|
deps = ["Arrow", "Base64", "DataFrames", "Dates", "GeneralUtils", "HTTP", "JSON", "NATS", "PrettyPrinting", "Revise", "UUIDs"]
|
||||||
|
git-tree-sha1 = "e82a79cf6602541ea25409aded57b2ace4a7c29f"
|
||||||
|
repo-rev = "main"
|
||||||
|
repo-url = "https://git.yiem.cc/ton/msghandler"
|
||||||
|
uuid = "f2724d33-f338-4a57-b9f8-1be882570d10"
|
||||||
|
version = "1.2.1"
|
||||||
|
|
||||||
[[deps.nghttp2_jll]]
|
[[deps.nghttp2_jll]]
|
||||||
deps = ["Artifacts", "Libdl"]
|
deps = ["Artifacts", "Libdl"]
|
||||||
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
|
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
|
|||||||
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||||
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
|
||||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||||
|
msghandler = "f2724d33-f338-4a57-b9f8-1be882570d10"
|
||||||
|
|
||||||
[compat]
|
[compat]
|
||||||
Base64 = "1.11.0"
|
Base64 = "1.11.0"
|
||||||
@@ -33,3 +34,4 @@ JSON = "1.6.1"
|
|||||||
LLMMCTS = "0.1.5"
|
LLMMCTS = "0.1.5"
|
||||||
NATS = "0.1.0"
|
NATS = "0.1.0"
|
||||||
SQLLLM = "0.2.8"
|
SQLLLM = "0.2.8"
|
||||||
|
msghandler = "1.2.1"
|
||||||
|
|||||||
+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)
|
1. [Quick Start: Tool Lifecycle](#1-quick-start-tool-lifecycle)
|
||||||
2. [Overview](#2-overview)
|
2. [Overview](#2-overview)
|
||||||
3. [Tool Definition — The `agentTool` Struct](#3-tool-definition--the-agenttool-struct)
|
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)
|
5. [The Agent Loop — High-Level Flow](#5-the-agent-loop--high-level-flow)
|
||||||
6. [Message Processing Pipeline](#6-message-processing-pipeline)
|
6. [Message Processing Pipeline](#6-message-processing-pipeline)
|
||||||
7. [Tool Call Extraction from LLM Response](#7-tool-call-extraction-from-llm-response)
|
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)
|
15. [Self-Modifying Tools](#15-self-modifying-tools)
|
||||||
16. [Complete End-to-End Example](#16-complete-end-to-end-example)
|
16. [Complete End-to-End Example](#16-complete-end-to-end-example)
|
||||||
17. [Tool File Contract](#17-tool-file-contract)
|
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:**
|
**Result extraction:**
|
||||||
```julia
|
```julia
|
||||||
result = tool.execute("call-1", Dict{String,Any}(), nothing, x->x)
|
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
|
```julia
|
||||||
using YiemAgent, YiemAgent.toolRegistry
|
using YiemAgent, YiemAgent.toolRegistry
|
||||||
|
|
||||||
store = toolStore(name="myAgent")
|
store = toolStore(name="myAgent")
|
||||||
tools = loadTools(store, "src/tools")
|
tools = register_all_tools(store)
|
||||||
# Scans src/tools/ for .jl files, wraps each in a submodule, calls getTool(), registers in store.tools
|
# Calls getWeatherTool(), getTimeTool(), writeToolTool() to create agentTool structs
|
||||||
# Also auto-registers listTools for runtime discovery
|
# Also auto-registers listTools for runtime discovery
|
||||||
```
|
```
|
||||||
|
|
||||||
**Result extraction:**
|
**Result extraction:**
|
||||||
```julia
|
```julia
|
||||||
all_tools = getTools(store) # OrderedDict{String, agentTool}
|
all_tools = getTools(store) # OrderedDict{String, agentTool}
|
||||||
# Keys: "getTime", "getWeather", "writeTool", "listTools"
|
# Keys: "getWeather", "getTime", "writeTool", "listTools"
|
||||||
getTime_tool = all_tools["getTime"]
|
getWeather_tool = all_tools["getWeather"]
|
||||||
|
|
||||||
# Manual registration (alternative to loadTools)
|
# Manual registration (alternative to register_all_tools)
|
||||||
registerTool(store, my_tool)
|
registerTool(store, my_tool)
|
||||||
clearTools(store) # Clear all tools from store
|
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
|
### 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
|
```julia
|
||||||
using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry
|
using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry
|
||||||
|
|
||||||
# 1. Set up toolStore and load tools (auto-registers listTools)
|
# Create agent — tools are registered automatically via register_all_tools()
|
||||||
store = toolStore(name="myAgent")
|
|
||||||
loadTools(store, "src/tools")
|
|
||||||
|
|
||||||
# 2. Create agent — pass tools + _tool_store
|
|
||||||
agent = yiemAgent(
|
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
|
llmCall = my_llm_call, # Function that calls the LLM API
|
||||||
agentEventSink = my_event_sink, # Function for TUI/logging
|
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 |
|
| 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 |
|
| `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM |
|
||||||
| `agentEventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events |
|
| `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`.
|
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
|
op = x -> x # no-op partial result callback
|
||||||
|
|
||||||
# Execute a loaded tool directly
|
# 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):**
|
**Via agent loop (production):**
|
||||||
```
|
```
|
||||||
user message → runAgent(agent, Dict("role"=>"user", "content"=>...))
|
user message → runAgent(agent, Dict("role"=>"user", "content"=>...))
|
||||||
→ _agentLoop detects message → @spawn _process_message(agent)
|
→ _agentLoop detects message → @spawn _processMessage(agent)
|
||||||
→ prepareContext → formatMsgForLLM → llmCall
|
→ prepareContext → formatMsgForLLM → llmCall
|
||||||
→ LLM returns tool_calls
|
→ LLM returns tool_calls
|
||||||
→ executeToolCalls(context, response, tool_call_list, config, signal, emit)
|
→ 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`):
|
**`agentToolResult`** (raw tool output, `type.jl:429-434`):
|
||||||
```julia
|
```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] # textContent("Weather in Tokyo: Sunny, 22°C")
|
||||||
result.content[1].text # "Current time in Tokyo: 2026-08-10T..."
|
result.content[1].text # "Weather in Tokyo: Sunny, 22°C"
|
||||||
result.details # Dict{Any,Any}() — tool-specific metadata
|
result.details # Dict{Any,Any}() — tool-specific metadata
|
||||||
result.usage # nothing — llmUsage tracking (optional)
|
result.usage # nothing — llmUsage tracking (optional)
|
||||||
result.terminate # false — signals loop termination
|
result.terminate # false — signals loop termination
|
||||||
@@ -182,7 +157,7 @@ result.terminate # false — signals loop termination
|
|||||||
msg = batch.messages[1] # toolResultMessage
|
msg = batch.messages[1] # toolResultMessage
|
||||||
|
|
||||||
msg.toolCallId # "call-1"
|
msg.toolCallId # "call-1"
|
||||||
msg.toolName # "getTime"
|
msg.toolName # "getWeather"
|
||||||
msg.content # Vector{messageContent}
|
msg.content # Vector{messageContent}
|
||||||
msg.isError # false
|
msg.isError # false
|
||||||
msg.details # tool-specific metadata
|
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 |
|
| Phase | Function | Input | Output | Purpose |
|
||||||
|-------|----------|-------|--------|---------|
|
|-------|----------|-------|--------|---------|
|
||||||
| Prepare | `prepareToolCall()` | `agentContext`, `assistantMessage`, `agentToolCall`, `agentLoopConfig`, `abortSignal` | `preparedToolCall` or `immediateOutcome` | Resolve tool, validate args, run pre-hook |
|
| 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 |
|
| 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.
|
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
|
### 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.
|
`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
|
```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)
|
1. **Calls each tool's definition function** — `getWeatherTool()`, `getTimeTool()`, `writeToolTool()` — which return `agentTool` structs
|
||||||
2. **Sorts** filenames alphabetically for deterministic registration order
|
2. **Registers each tool** via `registerTool(store, tool)`
|
||||||
3. **Wraps** each file in a dynamically created submodule:
|
3. **Auto-registers** `listTool(store)` so the LLM can discover available tools at runtime
|
||||||
```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
|
|
||||||
|
|
||||||
### Why Submodules?
|
### Why Static?
|
||||||
|
|
||||||
Each tool file is loaded into its own **namespaced submodule**. This means:
|
Tools are **statically included** in `YiemAgent.jl` via `include()`. This means:
|
||||||
- `validateRequiredArgs`, `prepareArguments`, `executeTool`, and helper functions defined in `getTime.jl` are scoped under `_tool_getTime`
|
- Tool functions live in the `YiemAgent` module, not in dynamically created submodules
|
||||||
- No name collisions between tools — `getTime.validateRequiredArgs` is distinct from `getWeather.validateRequiredArgs`
|
- No world-age issues when calling `tool.execute()` (Julia compiles dispatch in the same world)
|
||||||
- The module reference is kept alive by the functions stored in `agentTool` (closures in `execute`, `validateRequiredArgs`, `prepareArguments`) so they don't get garbage collected
|
- Simpler tool definition — no need to wrap in a `module ... end` block
|
||||||
|
- Better compiler optimization (inlining, type inference)
|
||||||
|
|
||||||
### Registration API
|
### Registration API
|
||||||
|
|
||||||
@@ -331,9 +295,9 @@ Each tool file is loaded into its own **namespaced submodule**. This means:
|
|||||||
store1 = toolStore(name="agent1")
|
store1 = toolStore(name="agent1")
|
||||||
store2 = toolStore(name="agent2")
|
store2 = toolStore(name="agent2")
|
||||||
|
|
||||||
# Load tools into specific stores (auto-registers listTools)
|
# Load all tools (auto-registers listTools)
|
||||||
tools1 = loadTools(store1, "src/tools/weather_tools") # agent1 only
|
tools1 = register_all_tools(store1) # all agents get the same tools
|
||||||
tools2 = loadTools(store2, "src/tools/wine_tools") # agent2 only
|
tools2 = register_all_tools(store2)
|
||||||
|
|
||||||
# Manual registration (per-store)
|
# Manual registration (per-store)
|
||||||
registerTool(store1, my_tool)
|
registerTool(store1, my_tool)
|
||||||
@@ -359,8 +323,8 @@ Each `toolStore` is completely independent — tools registered in one store do
|
|||||||
storeA = toolStore(name="A")
|
storeA = toolStore(name="A")
|
||||||
storeB = toolStore(name="B")
|
storeB = toolStore(name="B")
|
||||||
|
|
||||||
registerTool(storeA, getTime_tool)
|
registerTool(storeA, getTimeTool())
|
||||||
registerTool(storeB, getWeather_tool)
|
registerTool(storeB, getWeatherTool())
|
||||||
|
|
||||||
getTools(storeA) # only contains getTime
|
getTools(storeA) # only contains getTime
|
||||||
getTools(storeB) # only contains getWeather
|
getTools(storeB) # only contains getWeather
|
||||||
@@ -385,7 +349,6 @@ yiemAgent struct contains:
|
|||||||
- inputChannel (Channel, capacity 16) ← user sends messages here via runAgent()
|
- inputChannel (Channel, capacity 16) ← user sends messages here via runAgent()
|
||||||
- followUpChannel (Channel, capacity 32) ← user sends follow-ups here via followUp()
|
- followUpChannel (Channel, capacity 32) ← user sends follow-ups here via followUp()
|
||||||
- outputChannel (Channel, capacity 16) ← agent sends responses here via takeResponse()
|
- outputChannel (Channel, capacity 16) ← agent sends responses here via takeResponse()
|
||||||
- _tool_store (toolStore) ← per-agent isolated tool registry
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Loop States
|
### Loop States
|
||||||
@@ -395,7 +358,7 @@ The loop tracks 6 states (documented at `agentCore.jl:39-75`):
|
|||||||
| State | `processingTask` | `activeRun` | `inputChannel` | `followUpChannel` | Behavior |
|
| State | `processingTask` | `activeRun` | `inputChannel` | `followUpChannel` | Behavior |
|
||||||
|-------|-----------------|-------------|----------------|-------------------|----------|
|
|-------|-----------------|-------------|----------------|-------------------|----------|
|
||||||
| 1 | `nothing` | `false` | empty | empty | Idle, waiting |
|
| 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 |
|
| 3 | running | `true` | empty | empty | Processing, no new input |
|
||||||
| 4 | running | `true` | has msg | empty | New message while busy → queued |
|
| 4 | running | `true` | has msg | empty | New message while busy → queued |
|
||||||
| 5 | running | `true` | empty | has msg | Follow-up 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
|
drain both channels, break loop
|
||||||
end
|
end
|
||||||
|
|
||||||
# 3. If agent is idle, spawn _process_message
|
# 3. If agent is idle, spawn _processMessage
|
||||||
if agent._state.activeRun == false
|
if agent._state.activeRun == false
|
||||||
processingTask = Threads.@spawn _process_message(agent)
|
processingTask = Threads.@spawn _processMessage(agent)
|
||||||
agent._state.activeRun = true
|
agent._state.activeRun = true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -445,12 +408,12 @@ end
|
|||||||
|
|
||||||
**Source:** `agentCore.jl:175-311`
|
**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
|
### Pipeline Steps
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function _process_message(agent::yiemAgent)::assistantMessage
|
function _processMessage(agent::yiemAgent)::assistantMessage
|
||||||
final_response = nothing
|
final_response = nothing
|
||||||
|
|
||||||
while true # Loop until LLM returns response without tool calls
|
while true # Loop until LLM returns response without tool calls
|
||||||
@@ -466,38 +429,38 @@ function _process_message(agent::yiemAgent)::assistantMessage
|
|||||||
end
|
end
|
||||||
|
|
||||||
# ── Step 2: Prepare context ─────────────────────────────────
|
# ── 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
|
# Default: deep copies systemPrompt, messages, tools from agentState → agentContext
|
||||||
# Override point: filter tools, inject context, modify system prompt
|
# Override point: filter tools, inject context, modify system prompt
|
||||||
|
|
||||||
# ── Step 3: Format for LLM ──────────────────────────────────
|
# ── Step 3: Format for LLM ──────────────────────────────────
|
||||||
formatted_messages = agent.formatMsgForLLM(preparedContext)
|
formattedMessages = formatMsgForLLM(preparedContext, agentEventSink)
|
||||||
# Converts agentContext → Dict("messages" => [...]) in OpenAI format
|
# Converts agentContext → Dict("messages" => [...]) in OpenAI format
|
||||||
# Wraps systemPrompt as system role, converts each messageContent block
|
# Wraps systemPrompt as system role, converts each messageContent block
|
||||||
|
|
||||||
# ── Step 4: Call LLM ────────────────────────────────────────
|
# ── Step 4: Call LLM ────────────────────────────────────────
|
||||||
response = agent.llmCall(formatted_messages)
|
response = llmCall(formattedMessages)
|
||||||
# Returns assistantMessage with content::Vector{messageContent}
|
# Returns assistantMessage with content::Vector{messageContent}
|
||||||
# Each content block has a type: "text", "thinking", or "tool_call"
|
# Each content block has a type: "text", "thinking", or "tool_call"
|
||||||
|
|
||||||
# ── Step 5: Extract tool calls ──────────────────────────────
|
# ── 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
|
# Inspects content blocks for "tool_calls" or "tool_call" Dict entries
|
||||||
|
|
||||||
# ── Step 6: Execute tool calls or return ────────────────────
|
# ── Step 6: Execute tool calls or return ────────────────────
|
||||||
if has_tool_calls && !isempty(tool_call_list)
|
if hasToolCalls && !isempty(toolCallList)
|
||||||
# Build context and config
|
# Build context and config
|
||||||
context = agentContext(agent._state.systemPrompt, agent._state.messages, agent._state.tools)
|
context = agentContext(systemPrompt, messages, tools)
|
||||||
config = agentLoopConfig(agent._state.tools, agent.beforeToolCall, agent.afterToolCall, ...)
|
config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential")
|
||||||
signal = nothing
|
signal = abortSignal(false)
|
||||||
emit = agent.agentEventSink
|
|
||||||
|
|
||||||
# Execute tool calls (sequential or parallel)
|
# 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
|
# Save results to conversation history
|
||||||
for tool_result in batch.messages
|
for tool_result in batch.messages
|
||||||
push!(agent._state.messages, tool_result)
|
push!(messages, tool_result)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Check termination
|
# Check termination
|
||||||
@@ -517,10 +480,6 @@ function _process_message(agent::yiemAgent)::assistantMessage
|
|||||||
end
|
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
|
## 7. Tool Call Extraction from LLM Response
|
||||||
@@ -652,51 +611,29 @@ function prepareToolCall(
|
|||||||
function executePreparedToolCall(
|
function executePreparedToolCall(
|
||||||
prep::preparedToolCall,
|
prep::preparedToolCall,
|
||||||
signal::Union{Nothing, abortSignal},
|
signal::Union{Nothing, abortSignal},
|
||||||
emit::Function,
|
agentEventSink,
|
||||||
)::executedOutcome
|
)::executedOutcome
|
||||||
```
|
```
|
||||||
|
|
||||||
**Steps:**
|
**Steps:**
|
||||||
|
|
||||||
1. **Initialize streaming state:**
|
1. **Call `tool.execute()`:**
|
||||||
```julia
|
```julia
|
||||||
updateEvents = promise[] # vector to collect update event handles
|
result = prep.tool.execute(
|
||||||
accepting = true # guard to prevent duplicate emissions
|
prep.toolCall.id,
|
||||||
```
|
prep.args,
|
||||||
|
signal,
|
||||||
|
agentEventSink # serves as onPartialResult callback
|
||||||
|
)
|
||||||
|
return executedOutcome(result, false)
|
||||||
|
```
|
||||||
|
|
||||||
2. **Call `tool.execute()`:**
|
2. **On error:**
|
||||||
```julia
|
```julia
|
||||||
result = prep.tool.execute(
|
catch err
|
||||||
prep.toolCall.id,
|
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
||||||
prep.args,
|
end
|
||||||
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.
|
|
||||||
|
|
||||||
### 8.3 Phase 3: Finalize — `finalizeExecutedToolCall()`
|
### 8.3 Phase 3: Finalize — `finalizeExecutedToolCall()`
|
||||||
|
|
||||||
@@ -716,49 +653,36 @@ function finalizeExecutedToolCall(
|
|||||||
**Steps:**
|
**Steps:**
|
||||||
|
|
||||||
1. **Extract execution result:**
|
1. **Extract execution result:**
|
||||||
```julia
|
```julia
|
||||||
result = executed.result
|
result = executed.result
|
||||||
isError = executed.isError
|
isError = executed.isError
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Run `afterToolCall` hook** — if `config.afterToolCall !== nothing`:
|
2. **Run `afterToolCall` hook** — if `config.afterToolCall !== nothing`:
|
||||||
- Passes `afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context)` and `signal`
|
- Passes `afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context)` and `signal`
|
||||||
- Hook can mutate the result:
|
- Hook can mutate the result:
|
||||||
```julia
|
```julia
|
||||||
after = config.afterToolCall(afterToolCallContext(...))
|
after = config.afterToolCall(afterToolCallContext(...))
|
||||||
if after !== nothing
|
if after !== nothing
|
||||||
result = merge(result, dict(
|
result = merge(result, dict(
|
||||||
:content => get(after, :content, result.content),
|
:content => get(after, :content, result.content),
|
||||||
:details => get(after, :details, result.details),
|
:details => get(after, :details, result.details),
|
||||||
:usage => get(after, :usage, result.usage),
|
:usage => get(after, :usage, result.usage),
|
||||||
:terminate => get(after, :terminate, result.terminate)
|
:terminate => get(after, :terminate, result.terminate)
|
||||||
))
|
))
|
||||||
isError = get(after, :isError, isError)
|
isError = get(after, :isError, isError)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
- Common use cases: mask sensitive data, normalize usage, flip `terminate` based on business logic
|
- Common use cases: mask sensitive data, normalize usage, flip `terminate` based on business logic
|
||||||
- On error: `result = createErrorToolResult(sprint(showerror, err)); isError = true`
|
- On error: `result = createErrorToolResult(sprint(showerror, err)); isError = true`
|
||||||
|
|
||||||
3. **Return:**
|
3. **Return:**
|
||||||
```julia
|
```julia
|
||||||
return finalizedOutcome(prep.toolCall, result, isError)
|
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.
|
**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
|
## 9. Execution Modes — Sequential vs Parallel
|
||||||
@@ -774,7 +698,7 @@ function executeToolCalls(
|
|||||||
toolCalls::Vector{agentToolCall},
|
toolCalls::Vector{agentToolCall},
|
||||||
config::agentLoopConfig,
|
config::agentLoopConfig,
|
||||||
signal::Union{Nothing, abortSignal},
|
signal::Union{Nothing, abortSignal},
|
||||||
emit::Function,
|
agentEventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -809,18 +733,15 @@ function executeToolCallsSequential(...)::agentToolCallBatch
|
|||||||
messages = toolResultMessage[]
|
messages = toolResultMessage[]
|
||||||
|
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
emit(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||||
|
|
||||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
|
|
||||||
|
|
||||||
if prep isa immediateOutcome
|
if prep isa immediateOutcome
|
||||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||||
else
|
else
|
||||||
executed = executePreparedToolCall(prep, signal, emit)
|
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||||
end
|
end
|
||||||
|
|
||||||
emitToolExecutionEnd(finalized, emit)
|
|
||||||
push!(messages, createToolResultMessage(finalized))
|
push!(messages, createToolResultMessage(finalized))
|
||||||
push!(finalizedCalls, finalized)
|
push!(finalizedCalls, finalized)
|
||||||
|
|
||||||
@@ -842,19 +763,15 @@ function executeToolCallsParallel(...)::agentToolCallBatch
|
|||||||
entries = union{finalizedOutcome, task{finalizedOutcome}}[]
|
entries = union{finalizedOutcome, task{finalizedOutcome}}[]
|
||||||
|
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
emit(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||||
|
|
||||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal)
|
|
||||||
|
|
||||||
if prep isa immediateOutcome
|
if prep isa immediateOutcome
|
||||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||||
emitToolExecutionEnd(finalized, emit)
|
|
||||||
push!(entries, finalized) # immediate outcome — no task
|
push!(entries, finalized) # immediate outcome — no task
|
||||||
else
|
else
|
||||||
task = task() do
|
task = task() do
|
||||||
executed = executePreparedToolCall(prep, signal, emit)
|
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||||
emitToolExecutionEnd(finalized, emit)
|
|
||||||
return finalized
|
return finalized
|
||||||
end
|
end
|
||||||
schedule(task)
|
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) |
|
| 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()` |
|
| 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`
|
**Source:** `agentCore.jl:266-307`
|
||||||
|
|
||||||
```julia
|
```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
|
# Save results to conversation history
|
||||||
for tool_result in batch.messages
|
for tool_result in batch.messages
|
||||||
@@ -985,9 +902,9 @@ function createToolResultMessage(f::finalizedOutcome)::toolResultMessage
|
|||||||
f.result.content, # content (Vector{messageContent})
|
f.result.content, # content (Vector{messageContent})
|
||||||
f.result.details, # details
|
f.result.details, # details
|
||||||
f.result.usage, # usage
|
f.result.usage, # usage
|
||||||
get(f.result, :addedToolNames, string[]), # addedToolNames (for dynamic tools)
|
nothing, # addedToolNames (for dynamic tools)
|
||||||
f.isError, # isError
|
f.isError, # isError
|
||||||
nowMillis(), # timestamp
|
now(), # timestamp
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
@@ -1048,9 +965,6 @@ Tool call fails at any phase
|
|||||||
└─────────────────────────┘
|
└─────────────────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
emitToolExecutionEnd(finalized, emit)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
createToolResultMessage(finalized)
|
createToolResultMessage(finalized)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -1119,14 +1033,14 @@ end
|
|||||||
|-------|-------------|------|
|
|-------|-------------|------|
|
||||||
| `toolExecStartEvent` | `executeToolCalls*()` loop | Before `prepareToolCall()` for each tool call |
|
| `toolExecStartEvent` | `executeToolCalls*()` loop | Before `prepareToolCall()` for each tool call |
|
||||||
| `toolExecUpdateEvent` | `executePreparedToolCall()` | Inside `onPartialResult` callback during `tool.execute()` |
|
| `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
|
### Event Sink
|
||||||
|
|
||||||
The `emit` function is passed through the entire call chain:
|
The `agentEventSink` function is passed through the entire call chain:
|
||||||
|
|
||||||
```julia
|
```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:
|
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 |
|
| Hook | Signature | Called | Purpose |
|
||||||
|------|-----------|--------|---------|
|
|------|-----------|--------|---------|
|
||||||
| `prepareContext` | `(state::agentState) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
|
| `prepareContext` | `(state::agentState, agentEventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt |
|
||||||
| `formatMsgForLLM` | `(ctx::agentContext) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
|
| `formatMsgForLLM` | `(ctx::agentContext, agentEventSink) -> Dict` | After `prepareContext` | Convert to LLM-specific format |
|
||||||
| `llmCall` | `(messages::Dict) -> assistantMessage` | After formatting | Actually invoke the LLM API |
|
| `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 |
|
| `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` |
|
| `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`
|
**Source:** `utils.jl:111-125`
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function prepareContext(state::agentState)::agentContext
|
function prepareContext(state::agentState, agentEventSink)::agentContext
|
||||||
# TODO: filter tools from state.tools based on user intent
|
# TODO: filter tools from state.tools based on user intent
|
||||||
filteredTools = state.tools
|
filteredTools = state.tools
|
||||||
|
|
||||||
@@ -1238,7 +1152,7 @@ end
|
|||||||
Default implementation converts `agentContext` to OpenAI-compatible format:
|
Default implementation converts `agentContext` to OpenAI-compatible format:
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function formatMsgForLLM(ctx::agentContext)::Dict{String, Any}
|
function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
||||||
messages = Vector{Dict{String, Any}}()
|
messages = Vector{Dict{String, Any}}()
|
||||||
|
|
||||||
# System prompt as system message
|
# 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
|
1. Converts `inputSchema` Dict into `Dict{String,Any}(...)` string literal
|
||||||
2. Indents `executeCode` with 4 spaces
|
2. Indents `executeCode` with 4 spaces
|
||||||
3. Wraps it inside `function executeTool(...)::agentToolResult ... end`
|
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`
|
5. Writes the combined string to `src/tools/<name>.jl`
|
||||||
|
|
||||||
### `listTool` — Discover Available Tools
|
### `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
|
### 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..."
|
- executeCode: "query = args[\"query\"]\nresult = search(query)\n..."
|
||||||
- (optional) validateCode, prepareCode
|
- (optional) validateCode, prepareCode
|
||||||
3. writeTool generates src/tools/searchWine.jl
|
3. writeTool generates src/tools/searchWine.jl
|
||||||
4. Agent restarts (or hot-reloads) → loadTools(agent._tool_store, "src/tools") picks up the new file
|
4. Developer adds `include("tools/searchWine.jl")` to YiemAgent.jl
|
||||||
5. Agent calls searchWine(query="cabernet")
|
5. Developer adds `registerTool(store, searchWineTool())` to register_all_tools() in YiemAgent.jl
|
||||||
6. Result: "Found 5 cabernet wines..."
|
6. Developer restarts Julia — new tool is loaded
|
||||||
|
7. Agent calls searchWine(query="cabernet")
|
||||||
|
8. Result: "Found 5 cabernet wines..."
|
||||||
```
|
```
|
||||||
|
|
||||||
### `writeTool` Input Schema
|
### `writeTool` Input Schema
|
||||||
@@ -1328,14 +1244,14 @@ Each `toolStore` gets its own `listTool` instance bound to that store via `listT
|
|||||||
```
|
```
|
||||||
USER SENDS MESSAGE
|
USER SENDS MESSAGE
|
||||||
└─> runAgent(agent, "What's the weather in Tokyo?")
|
└─> 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
|
LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
|
||||||
└─> _agentLoop: detects msg in inputChannel
|
└─> _agentLoop: detects msg in inputChannel
|
||||||
└─> Threads.@spawn _process_message(agent)
|
└─> @spawn _processMessage(agent)
|
||||||
|
|
||||||
── _process_message ──────────────────────────────────────────────
|
── _processMessage ──────────────────────────────────────────────
|
||||||
│
|
│
|
||||||
│ Step 1: Drain inputChannel
|
│ Step 1: Drain inputChannel
|
||||||
│ raw_msg = Dict("role" => "user", "content" => [...])
|
│ raw_msg = Dict("role" => "user", "content" => [...])
|
||||||
@@ -1367,36 +1283,34 @@ LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL
|
|||||||
│
|
│
|
||||||
│ Step 6: Execute tool calls
|
│ Step 6: Execute tool calls
|
||||||
│ context = agentContext(systemPrompt, messages, tools)
|
│ context = agentContext(systemPrompt, messages, tools)
|
||||||
│ config = agentLoopConfig(tools, beforeToolCall, afterToolCall, "sequential")
|
│ config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential")
|
||||||
│ batch = executeToolCalls(context, response, tool_call_list, config, nothing, emit)
|
│ batch = executeToolCalls(context, response, tool_call_list, config, signal, agentEventSink)
|
||||||
|
|
||||||
|
|
||||||
|
LOOP ITERATION 1 — executeToolCallsSequential
|
||||||
│
|
│
|
||||||
│ ── executeToolCallsSequential ──────────────────────────────
|
│ For tc = agentToolCall("call_1", "getWeather", ...):
|
||||||
│ │
|
│
|
||||||
│ │ For tc = agentToolCall("call_1", "getWeather", ...):
|
│ PREPARE:
|
||||||
│ │
|
│ tool = context.tools["getWeather"] → found!
|
||||||
│ │ emit(toolExecStartEvent("call_1", "getWeather", {"city": "Tokyo"}))
|
│ validatedArgs = validateToolArguments(tool, tc)
|
||||||
│ │
|
│ → validateRequiredArgs(Dict("city" => "Tokyo"), inputSchema) → passes
|
||||||
│ │ PREPARE:
|
│ beforeToolCall_hook(...) → nothing (skipped)
|
||||||
│ │ tool = context.tools["getWeather"] → found!
|
│ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
|
||||||
│ │ validatedArgs = validateToolArguments(tool, tc)
|
│
|
||||||
│ │ → validateRequiredArgs(Dict("city" => "Tokyo"), inputSchema) → passes
|
│ EXECUTE:
|
||||||
│ │ beforeToolCall_hook(...) → nothing (skipped)
|
│ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, agentEventSink)
|
||||||
│ │ → preparedToolCall(tool, tc, {"city" => "Tokyo"})
|
│ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false)
|
||||||
│ │
|
│ → executedOutcome(result, false)
|
||||||
│ │ EXECUTE:
|
│
|
||||||
│ │ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, onPartialResult)
|
│ FINALIZE:
|
||||||
│ │ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false)
|
│ afterToolCall_hook(...) → nothing (skipped)
|
||||||
│ │ → executedOutcome(result, false)
|
│ → finalizedOutcome(tc, result, false)
|
||||||
│ │
|
│
|
||||||
│ │ FINALIZE:
|
│ msg = createToolResultMessage(finalized)
|
||||||
│ │ afterToolCall_hook(...) → nothing (skipped)
|
│ → toolResultMessage("tool", "call_1", "getWeather", [...], {}, nothing, [], false, ts)
|
||||||
│ │ → finalizedOutcome(tc, result, false)
|
│
|
||||||
│ │
|
└─> agentToolCallBatch([msg], false)
|
||||||
│ │ emit(toolExecEndEvent("call_1", "getWeather", result, false))
|
|
||||||
│ │ msg = createToolResultMessage(finalized)
|
|
||||||
│ │ → toolResultMessage("tool", "call_1", "getWeather", [...], {}, nothing, [], false, ts)
|
|
||||||
│ │
|
|
||||||
│ └─> agentToolCallBatch([msg], false)
|
|
||||||
│
|
│
|
||||||
│ Save results:
|
│ Save results:
|
||||||
│ for tool_result in batch.messages
|
│ 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
|
LOOP ITERATION 2 — LLM RETURNS FINAL TEXT RESPONSE
|
||||||
── _process_message (second iteration) ───────────────────────────
|
── _processMessage (second iteration) ───────────────────────────
|
||||||
│
|
│
|
||||||
│ Step 1: Drain inputChannel → empty
|
│ Step 1: Drain inputChannel → empty
|
||||||
│
|
│
|
||||||
@@ -1437,12 +1351,12 @@ AGENT LOOP: SEND RESPONSE TO USER
|
|||||||
|
|
||||||
## 17. Tool File Contract
|
## 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
|
### Required Function
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function getTool()::agentTool
|
function <name>Tool()::agentTool
|
||||||
# Must return an agentTool instance
|
# Must return an agentTool instance
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
@@ -1451,22 +1365,22 @@ end
|
|||||||
|
|
||||||
```julia
|
```julia
|
||||||
# Argument preparation (before validation)
|
# 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 modified args, or args unchanged
|
||||||
return args
|
return args
|
||||||
end
|
end
|
||||||
|
|
||||||
# Custom validation (before execution)
|
# 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 to pass, or error string to fail
|
||||||
return nothing
|
return nothing
|
||||||
end
|
end
|
||||||
|
|
||||||
# Core execution
|
# Core execution
|
||||||
function executeTool(toolCallId::String,
|
function <name>Execute(toolCallId::String,
|
||||||
args::Dict{String,Any},
|
args::Dict{String,Any},
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
onPartialResult::Function)::agentToolResult
|
onPartialResult::Function)::agentToolResult
|
||||||
# Return agentToolResult with content, details, usage, terminate
|
# Return agentToolResult with content, details, usage, terminate
|
||||||
return agentToolResult([textContent("result")], Dict{Any,Any}(), nothing, false)
|
return agentToolResult([textContent("result")], Dict{Any,Any}(), nothing, false)
|
||||||
end
|
end
|
||||||
@@ -1477,7 +1391,8 @@ end
|
|||||||
```julia
|
```julia
|
||||||
# src/tools/myTool.jl
|
# 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
|
# Optional: helper functions
|
||||||
function helper_function(...)
|
function helper_function(...)
|
||||||
@@ -1485,24 +1400,24 @@ function helper_function(...)
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Optional: prepareArguments
|
# Optional: prepareArguments
|
||||||
function prepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
function myToolPrepareArguments(args::Dict{String,Any})::Dict{String,Any}
|
||||||
return args
|
return args
|
||||||
end
|
end
|
||||||
|
|
||||||
# Optional: validateRequiredArgs
|
# Optional: validateRequiredArgs
|
||||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
function myToolValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||||
return nothing
|
return nothing
|
||||||
end
|
end
|
||||||
|
|
||||||
# Required: executeTool
|
# Required: execute function
|
||||||
function executeTool(toolCallId::String, args::Dict{String,Any},
|
function myToolExecute(toolCallId::String, args::Dict{String,Any},
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
onPartialResult::Function)::agentToolResult
|
onPartialResult::Function)::agentToolResult
|
||||||
...
|
...
|
||||||
end
|
end
|
||||||
|
|
||||||
# Required: getTool
|
# Required: getTool function
|
||||||
function getTool()::agentTool
|
function myToolTool()::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "myTool",
|
name = "myTool",
|
||||||
label = "My Tool",
|
label = "My Tool",
|
||||||
@@ -1512,9 +1427,9 @@ function getTool()::agentTool
|
|||||||
"properties" => Dict(...),
|
"properties" => Dict(...),
|
||||||
"required" => [...]
|
"required" => [...]
|
||||||
),
|
),
|
||||||
execute = executeTool,
|
execute = myToolExecute,
|
||||||
prepareArguments = prepareArguments,
|
prepareArguments = myToolPrepareArguments,
|
||||||
validateRequiredArgs = validateRequiredArgs,
|
validateRequiredArgs = myToolValidateRequiredArgs,
|
||||||
parallelToolExecute = false
|
parallelToolExecute = false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -1522,51 +1437,110 @@ end
|
|||||||
|
|
||||||
### Dependencies
|
### 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
|
```julia
|
||||||
# src/tools/getTime.jl
|
# src/tools/getTime.jl
|
||||||
|
using .type
|
||||||
using Dates
|
using Dates
|
||||||
|
|
||||||
function executeTool(...)
|
function getTimeExecute(...)
|
||||||
now() # Dates.now requires `using Dates`
|
now() # Dates.now requires `using Dates`
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
# src/tools/myApiTool.jl
|
# src/tools/myApiTool.jl
|
||||||
|
using .type
|
||||||
using HTTP, JSON
|
using HTTP, JSON
|
||||||
|
|
||||||
function executeTool(...)
|
function myApiToolExecute(...)
|
||||||
response = HTTP.get("https://api.example.com")
|
response = HTTP.get("https://api.example.com")
|
||||||
data = JSON.parse(String(response.body))
|
data = JSON.parse(String(response.body))
|
||||||
...
|
...
|
||||||
end
|
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:
|
All tool files are **statically included** in `YiemAgent.jl` via `include()`. This means:
|
||||||
|
- All functions live in the `YiemAgent` module, avoiding world-age issues
|
||||||
```julia
|
- `using .type` makes core types (`agentTool`, `textContent`, `agentToolResult`, `abortSignal`) available
|
||||||
# User writes in src/tools/myTool.jl:
|
- Functions are named with a `<toolName>` prefix to avoid name collisions (e.g., `getWeatherExecute`, `getTimeExecute`)
|
||||||
using Dates, HTTP, JSON # ← tool's own dependencies
|
- The `...Tool()` function (e.g., `getWeatherTool()`) returns the `agentTool` struct for registration
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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
|
### 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) |
|
| `agentContext` | `type.jl:299` | Conversation snapshot (systemPrompt, messages, tools) |
|
||||||
| `agentState` | `type.jl:310` | Mutable runtime state (systemPrompt, model, tools, messages, pendingToolCalls, activeRun, errorMessage) |
|
| `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`) |
|
| `abortSignal` | `type.jl:416` | Abort flag (`aborted::Bool`) |
|
||||||
| `beforeToolCallContext` | `type.jl:445` | Context for beforeToolCall (message, toolCall, args, context) |
|
| `beforeToolCallContext` | `type.jl:445` | Context for beforeToolCall (message, toolCall, args, context) |
|
||||||
| `afterToolCallContext` | `type.jl:463` | Context for afterToolCall (message, toolCall, args, result, isError, context) |
|
| `afterToolCallContext` | `type.jl:463` | Context for afterToolCall (message, toolCall, args, result, isError, context) |
|
||||||
|
|||||||
+13
-2
@@ -1,7 +1,6 @@
|
|||||||
module YiemAgent
|
module YiemAgent
|
||||||
|
|
||||||
# export agent
|
export register_all_tools
|
||||||
|
|
||||||
|
|
||||||
""" Order by dependencies of each file. The 1st included file must not depend on any other
|
""" Order by dependencies of each file. The 1st included file must not depend on any other
|
||||||
files and each file can only depend on the file included before it.
|
files and each file can only depend on the file included before it.
|
||||||
@@ -13,9 +12,21 @@ module YiemAgent
|
|||||||
include("utils.jl")
|
include("utils.jl")
|
||||||
using .utils
|
using .utils
|
||||||
|
|
||||||
|
include("tools/getWeather.jl")
|
||||||
|
include("tools/getTime.jl")
|
||||||
|
include("tools/writeTool.jl")
|
||||||
|
|
||||||
include("toolRegistry.jl")
|
include("toolRegistry.jl")
|
||||||
using .toolRegistry
|
using .toolRegistry
|
||||||
|
|
||||||
|
function register_all_tools(store::toolRegistry.toolStore)
|
||||||
|
registerTool(store, getWeatherTool())
|
||||||
|
registerTool(store, getTimeTool())
|
||||||
|
registerTool(store, writeToolTool())
|
||||||
|
registerTool(store, listTool(store))
|
||||||
|
return store.tools
|
||||||
|
end
|
||||||
|
|
||||||
# include("llmfunction.jl")
|
# include("llmfunction.jl")
|
||||||
# using .llmfunction
|
# using .llmfunction
|
||||||
|
|
||||||
|
|||||||
+132
-89
@@ -1,16 +1,23 @@
|
|||||||
module agentCore
|
module agentCore
|
||||||
|
|
||||||
export yiemAgent, _agentLoop, OpenAiToUserMessage, _extractToolCalls
|
export yiemAgent, _agentLoop, OpenAiToUserMessage, _extractToolCalls,
|
||||||
|
executePreparedToolCall, prepareToolCall, executeToolCallsSequential,
|
||||||
|
executeToolCallsParallel, executeToolCalls
|
||||||
|
|
||||||
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
|
||||||
DataFrames, Base.Threads, NATS
|
DataFrames, Base.Threads, NATS
|
||||||
using GeneralUtils
|
using GeneralUtils
|
||||||
using ..type, ..utils, ..toolRegistry
|
using ..type, ..utils, ..toolRegistry
|
||||||
|
|
||||||
|
function register_all_tools(store::toolRegistry.toolStore)
|
||||||
|
# Call parent module's version which has access to tool functions
|
||||||
|
parentmodule(@__MODULE__).register_all_tools(store)
|
||||||
|
end
|
||||||
|
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
|
|
||||||
"""
|
"""
|
||||||
docstring
|
docstring
|
||||||
"""
|
"""
|
||||||
mutable struct yiemAgent <: agent # High-level agent wrapper
|
mutable struct yiemAgent <: agent # High-level agent wrapper
|
||||||
_state::agentState # Current state (prompt, model, messages, tools, etc.)
|
_state::agentState # Current state (prompt, model, messages, tools, etc.)
|
||||||
@@ -84,7 +91,6 @@ on `inputChannel` and `followUpChannel` channels concurrently.
|
|||||||
- A new `yiemAgent` instance with an active background task
|
- A new `yiemAgent` instance with an active background task
|
||||||
"""
|
"""
|
||||||
function yiemAgent(
|
function yiemAgent(
|
||||||
toolsFolderPath::String,
|
|
||||||
llmCall,
|
llmCall,
|
||||||
;
|
;
|
||||||
systemPrompt::String="You are helpful assistant.",
|
systemPrompt::String="You are helpful assistant.",
|
||||||
@@ -106,9 +112,9 @@ function yiemAgent(
|
|||||||
followUp = Channel(32)
|
followUp = Channel(32)
|
||||||
outputChannel = Channel(16)
|
outputChannel = Channel(16)
|
||||||
|
|
||||||
# load tools from toolsFolderPath
|
# load tools (statically registered at module init)
|
||||||
toolStore1 = toolStore(name="myagent")
|
toolStore1 = toolStore(name="myagent")
|
||||||
loadTools(toolStore1, toolsFolderPath)
|
register_all_tools(toolStore1)
|
||||||
|
|
||||||
# Create struct with a placeholder task, then spawn and replace it
|
# Create struct with a placeholder task, then spawn and replace it
|
||||||
agent = yiemAgent(
|
agent = yiemAgent(
|
||||||
@@ -291,6 +297,8 @@ function _agentLoop(agent::yiemAgent)
|
|||||||
processingTask = nothing # reset
|
processingTask = nothing # reset
|
||||||
end
|
end
|
||||||
agent.agentEventSink("_agentLoop 6")
|
agent.agentEventSink("_agentLoop 6")
|
||||||
|
agent.agentEventSink(string(typeof(processingTask)))
|
||||||
|
agent.agentEventSink("_agentLoop 7")
|
||||||
end
|
end
|
||||||
catch e
|
catch e
|
||||||
# On any error, send error response and exit the loop
|
# On any error, send error response and exit the loop
|
||||||
@@ -329,7 +337,7 @@ julia> # Currently returns a placeholder echo response
|
|||||||
function _processMessage(
|
function _processMessage(
|
||||||
inputChannel::Channel,
|
inputChannel::Channel,
|
||||||
agentEventSink,
|
agentEventSink,
|
||||||
messages::Vector{agentMessage},
|
agentMsgHistory::Vector{agentMessage},
|
||||||
systemPrompt::String,
|
systemPrompt::String,
|
||||||
tools::OrderedDict{String, agentTool},
|
tools::OrderedDict{String, agentTool},
|
||||||
prepareContext::Function,
|
prepareContext::Function,
|
||||||
@@ -370,12 +378,12 @@ function _processMessage(
|
|||||||
end
|
end
|
||||||
agentEventSink("_processMessage 5")
|
agentEventSink("_processMessage 5")
|
||||||
user_msg = OpenAiToUserMessage(raw_msg)
|
user_msg = OpenAiToUserMessage(raw_msg)
|
||||||
push!(messages, user_msg)
|
push!(agentMsgHistory, user_msg)
|
||||||
agentEventSink("_processMessage 6")
|
agentEventSink("_processMessage 6")
|
||||||
end
|
end
|
||||||
agentEventSink("_processMessage 7")
|
agentEventSink("_processMessage 7")
|
||||||
# call prepareContext()
|
# call prepareContext()
|
||||||
state = agentState(systemPrompt, nothing, tools, messages)
|
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
||||||
agentEventSink("_processMessage 8")
|
agentEventSink("_processMessage 8")
|
||||||
preparedContext = prepareContext(state, agentEventSink)
|
preparedContext = prepareContext(state, agentEventSink)
|
||||||
agentEventSink("_processMessage 8")
|
agentEventSink("_processMessage 8")
|
||||||
@@ -396,7 +404,7 @@ function _processMessage(
|
|||||||
agentEventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList")
|
agentEventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList")
|
||||||
|
|
||||||
if hasToolCalls && length(toolCallList) > 0
|
if hasToolCalls && length(toolCallList) > 0
|
||||||
#WORKING Build context and config for executeToolCalls
|
# Build context and config for executeToolCalls
|
||||||
|
|
||||||
config = agentLoopConfig(
|
config = agentLoopConfig(
|
||||||
beforeToolCall,
|
beforeToolCall,
|
||||||
@@ -408,23 +416,21 @@ function _processMessage(
|
|||||||
agentEventSink("_processMessage 12")
|
agentEventSink("_processMessage 12")
|
||||||
|
|
||||||
# call executeToolCalls()
|
# call executeToolCalls()
|
||||||
|
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
|
||||||
batch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config, signal,
|
signal, agentEventSink)
|
||||||
agentEventSink)
|
|
||||||
|
|
||||||
|
|
||||||
agentEventSink("_processMessage 13")
|
agentEventSink("_processMessage 13")
|
||||||
error("debug marker")
|
|
||||||
# save toolResults to messages
|
|
||||||
for tool_result in batch.messages
|
|
||||||
push!(messages, tool_result)
|
|
||||||
end
|
|
||||||
|
|
||||||
if batch.terminate
|
# save toolResults to messages
|
||||||
# If batch requested termination, build a final response
|
for toolResult in toolResultBatch.messages
|
||||||
|
push!(agentMsgHistory, toolResult)
|
||||||
|
end
|
||||||
|
agentEventSink("_processMessage 14")
|
||||||
|
if toolResultBatch.terminate
|
||||||
|
agentEventSink("_processMessage 15")
|
||||||
|
# If toolResultBatch requested termination, build a final response
|
||||||
final_content = [textContent("Tool execution completed.")]
|
final_content = [textContent("Tool execution completed.")]
|
||||||
for tool_result in batch.messages
|
for toolResult in toolResultBatch.messages
|
||||||
for content_block in tool_result.content
|
for content_block in toolResult.content
|
||||||
if content_block isa textContent
|
if content_block isa textContent
|
||||||
append!(final_content, [content_block])
|
append!(final_content, [content_block])
|
||||||
elseif content_block isa Dict
|
elseif content_block isa Dict
|
||||||
@@ -434,6 +440,7 @@ function _processMessage(
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
agentEventSink("_processMessage 16")
|
||||||
final_response = assistantMessage(
|
final_response = assistantMessage(
|
||||||
role="assistant",
|
role="assistant",
|
||||||
content=final_content,
|
content=final_content,
|
||||||
@@ -441,7 +448,7 @@ function _processMessage(
|
|||||||
model=assistant_msg.model,
|
model=assistant_msg.model,
|
||||||
usage=assistant_msg.usage,
|
usage=assistant_msg.usage,
|
||||||
stopReason="tool_use_terminated",
|
stopReason="tool_use_terminated",
|
||||||
errorMessage=if any(x -> x.isError, batch.messages)
|
errorMessage=if any(x -> x.isError, toolResultBatch.messages)
|
||||||
"One or more tool calls failed"
|
"One or more tool calls failed"
|
||||||
else
|
else
|
||||||
nothing
|
nothing
|
||||||
@@ -451,12 +458,13 @@ function _processMessage(
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
agentEventSink("_processMessage 17")
|
||||||
# LLM did not use tool calls — this is the final response
|
# LLM did not use tool calls — this is the final response
|
||||||
final_response = assistant_msg
|
final_response = assistant_msg
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
agentEventSink("_processMessage 18")
|
||||||
return final_response
|
return final_response
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -524,7 +532,7 @@ function createToolResultMessage(f::finalizedOutcome)::toolResultMessage
|
|||||||
return toolResultMessage(
|
return toolResultMessage(
|
||||||
"toolResult", f.toolCall.id, f.toolCall.name,
|
"toolResult", f.toolCall.id, f.toolCall.name,
|
||||||
f.result.content, f.result.details, f.result.usage,
|
f.result.content, f.result.details, f.result.usage,
|
||||||
get(f.result, :addedToolNames, string[]), f.isError, nowMillis()
|
nothing, f.isError, now()
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -563,8 +571,10 @@ function _extractToolCalls(response)
|
|||||||
toolCallList = agentToolCall[]
|
toolCallList = agentToolCall[]
|
||||||
|
|
||||||
# Helper: parse args (JSON string -> Dict, or pass through)
|
# Helper: parse args (JSON string -> Dict, or pass through)
|
||||||
parse_args(raw) = raw isa AbstractDict ? Dict{String,Any}(raw) :
|
parse_args(raw) = raw isa AbstractDict && !(raw isa Dict{String,Any}) ?
|
||||||
raw isa String ? JSON.parse(raw) : Dict{String,Any}()
|
Dict{String,Any}(raw) :
|
||||||
|
raw isa String ? JSON.parse(raw) :
|
||||||
|
raw isa Dict{String,Any} ? raw : Dict{String,Any}()
|
||||||
|
|
||||||
# Helper: build agentToolCall (positional)
|
# Helper: build agentToolCall (positional)
|
||||||
make_tc(tc_data, default_id=string(uuid4())) = begin
|
make_tc(tc_data, default_id=string(uuid4())) = begin
|
||||||
@@ -713,12 +723,27 @@ end
|
|||||||
"""
|
"""
|
||||||
shouldTerminate(finalizedCalls::Vector{finalizedOutcome}) -> Bool
|
shouldTerminate(finalizedCalls::Vector{finalizedOutcome}) -> Bool
|
||||||
|
|
||||||
|
The `terminate` flag is set by tool implementations, not by the agent
|
||||||
|
or the LLM. It signals that the tool itself has completed the user's
|
||||||
|
request or encountered a fatal condition, so the agent should stop
|
||||||
|
processing further turns without calling the LLM again.
|
||||||
|
|
||||||
|
Common scenarios where a tool sets `terminate: true`:
|
||||||
|
- **Task completion**: one-shot tools like `deploy`, `submit`, or
|
||||||
|
`send_payment` finish their work and report directly to the user
|
||||||
|
instead of asking the LLM "what next?"
|
||||||
|
- **Unrecoverable error**: a tool hits a fatal condition (database
|
||||||
|
connection lost, auth token expired) and stops the agent from
|
||||||
|
retrying endlessly.
|
||||||
|
- **Async handoff**: a tool triggers a long-running external operation
|
||||||
|
and wants the agent to stop now; the external system will resume
|
||||||
|
the agent later via `continue()`.
|
||||||
|
|
||||||
Returns `true` only when every finalized call in the batch has
|
Returns `true` only when every finalized call in the batch has
|
||||||
`result.terminate == true`. All tools must agree — if any tool
|
`result.terminate == true`. All tools must agree — if any tool
|
||||||
did not request termination, the agent continues. This prevents
|
did not request termination, the agent continues. This prevents
|
||||||
a single tool that happens to set `terminate: true` (e.g. for
|
a single tool that happens to set `terminate: true` from accidentally
|
||||||
metadata purposes) from accidentally stopping the agent when
|
stopping the agent when other tools in the batch did not intend to terminate.
|
||||||
other tools in the batch did not intend to terminate.
|
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `finalizedCalls`: Vector of finalized tool call outcomes
|
- `finalizedCalls`: Vector of finalized tool call outcomes
|
||||||
@@ -739,7 +764,7 @@ true
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function shouldTerminate(batches::Vector{finalizedOutcome})::Bool
|
function shouldTerminate(batches::Vector{finalizedOutcome})::Bool
|
||||||
return !isempty(batches) && all(b -> b.result.terminate, batches)
|
return !isempty(batches) && all(b -> b.result.terminate, batches)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -849,7 +874,7 @@ function prepareToolCall(
|
|||||||
agentEventSink
|
agentEventSink
|
||||||
)::Union{preparedToolCall,immediateOutcome}
|
)::Union{preparedToolCall,immediateOutcome}
|
||||||
agentEventSink("prepareToolCall 1")
|
agentEventSink("prepareToolCall 1")
|
||||||
tool = get(context.tools, toolCall.name, nothing)
|
tool = get(context.tools, toolCall.name, nothing) # pick a called tool from tool store
|
||||||
if tool === nothing
|
if tool === nothing
|
||||||
agentEventSink("prepareToolCall 2")
|
agentEventSink("prepareToolCall 2")
|
||||||
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
||||||
@@ -859,8 +884,10 @@ function prepareToolCall(
|
|||||||
agentEventSink("prepareToolCall 3")
|
agentEventSink("prepareToolCall 3")
|
||||||
# 1. prepare arguments (tool-specific transform)
|
# 1. prepare arguments (tool-specific transform)
|
||||||
prepared = prepareToolCallArguments(tool, toolCall)
|
prepared = prepareToolCallArguments(tool, toolCall)
|
||||||
|
agentEventSink(string(prepared.arguments))
|
||||||
agentEventSink("prepareToolCall 4")
|
agentEventSink("prepareToolCall 4")
|
||||||
validatedArgs = validateToolArguments(tool, prepared)
|
validatedArgs = validateToolArguments(tool, prepared)
|
||||||
|
agentEventSink(string(validatedArgs))
|
||||||
agentEventSink("prepareToolCall 5")
|
agentEventSink("prepareToolCall 5")
|
||||||
# 2. beforeToolCall hook — can block
|
# 2. beforeToolCall hook — can block
|
||||||
if config.beforeToolCall !== nothing
|
if config.beforeToolCall !== nothing
|
||||||
@@ -886,12 +913,12 @@ function prepareToolCall(
|
|||||||
return preparedToolCall(tool, toolCall, validatedArgs)
|
return preparedToolCall(tool, toolCall, validatedArgs)
|
||||||
catch e
|
catch e
|
||||||
bt = catch_backtrace()
|
bt = catch_backtrace()
|
||||||
err_msg = sprint() do io
|
errMsg = sprint() do io
|
||||||
showerror(io, e, bt)
|
showerror(io, e, bt)
|
||||||
println(io)
|
println(io)
|
||||||
end
|
end
|
||||||
|
|
||||||
agentEventSink(err_msg)
|
agentEventSink(errMsg)
|
||||||
|
|
||||||
return immediateOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
return immediateOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
||||||
end
|
end
|
||||||
@@ -937,36 +964,40 @@ executePreparedToolCall(prep, nothing, emit)
|
|||||||
# => executedOutcome(createErrorToolResult("Connection timeout"), true)
|
# => executedOutcome(createErrorToolResult("Connection timeout"), true)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
function executePreparedToolCall(
|
function executePreparedToolCall(
|
||||||
prep::preparedToolCall,
|
prep::preparedToolCall,
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
agentEventSink,
|
agentEventSink,
|
||||||
)::executedOutcome
|
)::executedOutcome
|
||||||
agentEventSink("executePreparedToolCall 1")
|
agentEventSink("executePreparedToolCall 1")
|
||||||
updateEvents = promise[]
|
agentEventSink(prep.toolCall.id)
|
||||||
accepting = true
|
agentEventSink(prep.toolCall.name)
|
||||||
|
agentEventSink("executePreparedToolCall 2")
|
||||||
|
s = string(prep.args)
|
||||||
|
agentEventSink(s)
|
||||||
|
agentEventSink("executePreparedToolCall 3")
|
||||||
|
t = string(fieldnames(typeof(prep.tool)))
|
||||||
|
agentEventSink("executePreparedToolCall 3-1 " * t)
|
||||||
|
|
||||||
try
|
try
|
||||||
result = prep.tool.execute(
|
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, agentEventSink)
|
||||||
prep.toolCall.id, prep.args, signal,
|
agentEventSink(result.content[1].text)
|
||||||
partialResult -> begin
|
agentEventSink("executePreparedToolCall 4")
|
||||||
if accepting
|
|
||||||
push!(updateEvents,
|
|
||||||
agentEventSink(toolExecUpdateEvent(prep.toolCall.id, prep.toolCall.name,
|
|
||||||
prep.toolCall.arguments, partialResult)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
)
|
|
||||||
accepting = false
|
|
||||||
wait.(updateEvents)
|
|
||||||
return executedOutcome(result, false)
|
return executedOutcome(result, false)
|
||||||
catch err
|
catch e
|
||||||
accepting = false
|
bt = catch_backtrace()
|
||||||
wait.(updateEvents)
|
errMsg = sprint() do io
|
||||||
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
|
showerror(io, e, bt)
|
||||||
|
println(io)
|
||||||
|
end
|
||||||
|
agentEventSink(errMsg)
|
||||||
|
|
||||||
|
return executedOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# ── per-call finalization ───────────────────────────────────────
|
# ── per-call finalization ───────────────────────────────────────
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -1030,16 +1061,19 @@ function finalizeExecutedToolCall(
|
|||||||
executed::executedOutcome,
|
executed::executedOutcome,
|
||||||
config::agentLoopConfig,
|
config::agentLoopConfig,
|
||||||
signal::Union{Nothing,abortSignal},
|
signal::Union{Nothing,abortSignal},
|
||||||
|
agentEventSink
|
||||||
)::finalizedOutcome
|
)::finalizedOutcome
|
||||||
|
agentEventSink("finalizeExecutedToolCall 1")
|
||||||
result = executed.result
|
result = executed.result
|
||||||
isError = executed.isError
|
isError = executed.isError
|
||||||
|
agentEventSink("finalizeExecutedToolCall 2")
|
||||||
if config.afterToolCall !== nothing
|
if config.afterToolCall !== nothing
|
||||||
try
|
try
|
||||||
after = config.afterToolCall(
|
after = config.afterToolCall(
|
||||||
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal
|
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context),
|
||||||
|
signal
|
||||||
)
|
)
|
||||||
|
agentEventSink("finalizeExecutedToolCall 3")
|
||||||
if after !== nothing
|
if after !== nothing
|
||||||
result = merge(result, dict(:content=>get(after,:content,result.content),
|
result = merge(result, dict(:content=>get(after,:content,result.content),
|
||||||
:details=>get(after,:details,result.details),
|
:details=>get(after,:details,result.details),
|
||||||
@@ -1047,12 +1081,19 @@ function finalizeExecutedToolCall(
|
|||||||
:terminate=>get(after,:terminate,result.terminate)))
|
:terminate=>get(after,:terminate,result.terminate)))
|
||||||
isError = get(after, :isError, isError)
|
isError = get(after, :isError, isError)
|
||||||
end
|
end
|
||||||
catch err
|
catch e
|
||||||
result = createErrorToolResult(sprint(showerror, err))
|
bt = catch_backtrace()
|
||||||
|
errMsg = sprint() do io
|
||||||
|
showerror(io, e, bt)
|
||||||
|
println(io)
|
||||||
|
end
|
||||||
|
agentEventSink(errMsg)
|
||||||
|
|
||||||
|
result = createErrorToolResult(sprint(showerror, e))
|
||||||
isError = true
|
isError = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
agentEventSink("finalizeExecutedToolCall 4")
|
||||||
return finalizedOutcome(prep.toolCall, result, isError)
|
return finalizedOutcome(prep.toolCall, result, isError)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1123,30 +1164,32 @@ function executeToolCallsSequential(
|
|||||||
messages = toolResultMessage[]
|
messages = toolResultMessage[]
|
||||||
|
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
agentEventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
|
agentEventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
|
||||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
prep = prepareToolCall(context, assistantMsg, tc, config, signal, agentEventSink)
|
||||||
agentEventSink("executeToolCallsSequential 2")
|
agentEventSink("executeToolCallsSequential " * string(prep.args))
|
||||||
if prep isa immediateOutcome
|
|
||||||
agentEventSink("executeToolCallsSequential 2-1")
|
if prep isa immediateOutcome
|
||||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
agentEventSink("executeToolCallsSequential 2-1")
|
||||||
agentEventSink("executeToolCallsSequential 2-2")
|
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||||
else
|
agentEventSink("executeToolCallsSequential 2-2")
|
||||||
agentEventSink("executeToolCallsSequential 3")
|
else
|
||||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
agentEventSink("executeToolCallsSequential 3")
|
||||||
agentEventSink("executeToolCallsSequential 3-1")
|
#XXX
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||||
signal)
|
agentEventSink("executeToolCallsSequential 3-1")
|
||||||
agentEventSink("executeToolCallsSequential 3-2")
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
||||||
end
|
signal, agentEventSink)
|
||||||
agentEventSink("executeToolCallsSequential 4")
|
agentEventSink("executeToolCallsSequential 3-2")
|
||||||
agentEventSink("$(finalized.toolCall.id), $(finalized.toolCall.name),
|
end
|
||||||
$(finalized.result), $(finalized.isError)")
|
agentEventSink("executeToolCallsSequential 4")
|
||||||
push!(messages, createToolResultMessage(finalized))
|
agentEventSink("$(finalized.toolCall.id), $(finalized.toolCall.name),
|
||||||
push!(finalizedCalls, finalized)
|
$(finalized.result), $(finalized.isError)")
|
||||||
agentEventSink("executeToolCallsSequential 5")
|
push!(messages, createToolResultMessage(finalized))
|
||||||
if signal !== nothing && signal.aborted
|
push!(finalizedCalls, finalized)
|
||||||
break
|
agentEventSink("executeToolCallsSequential 5")
|
||||||
end
|
if signal !== nothing && signal.aborted
|
||||||
|
break
|
||||||
|
end
|
||||||
end
|
end
|
||||||
agentEventSink("executeToolCallsSequential 6")
|
agentEventSink("executeToolCallsSequential 6")
|
||||||
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
|
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
|
||||||
@@ -1218,12 +1261,12 @@ function executeToolCallsParallel(
|
|||||||
agentEventSink,
|
agentEventSink,
|
||||||
)::agentToolCallBatch
|
)::agentToolCallBatch
|
||||||
|
|
||||||
entries = union{finalizedOutcome,task{finalizedOutcome}}[]
|
entries = Union{finalizedOutcome,Task}[]
|
||||||
|
|
||||||
for tc in toolCalls
|
for tc in toolCalls
|
||||||
agentEventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
agentEventSink(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
|
if prep isa immediateOutcome
|
||||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||||
@@ -1231,15 +1274,15 @@ function executeToolCallsParallel(
|
|||||||
finalized.result, finalized.isError))
|
finalized.result, finalized.isError))
|
||||||
push!(entries, finalized)
|
push!(entries, finalized)
|
||||||
else
|
else
|
||||||
task = task() do
|
t = Task() do
|
||||||
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
executed = executePreparedToolCall(prep, signal, agentEventSink)
|
||||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)
|
||||||
agentEventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
agentEventSink(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name,
|
||||||
finalized.result, finalized.isError))
|
finalized.result, finalized.isError))
|
||||||
return finalized
|
return finalized
|
||||||
end
|
end
|
||||||
schedule(task)
|
schedule(t)
|
||||||
push!(entries, task)
|
push!(entries, t)
|
||||||
end
|
end
|
||||||
|
|
||||||
if signal !== nothing && signal.aborted
|
if signal !== nothing && signal.aborted
|
||||||
@@ -1249,7 +1292,7 @@ function executeToolCallsParallel(
|
|||||||
|
|
||||||
finalizedCalls = finalizedOutcome[]
|
finalizedCalls = finalizedOutcome[]
|
||||||
for entry in entries
|
for entry in entries
|
||||||
outcome = entry isa task ? fetch(entry) : entry
|
outcome = entry isa Task ? fetch(entry) : entry
|
||||||
push!(finalizedCalls, outcome)
|
push!(finalizedCalls, outcome)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+4
-96
@@ -1,6 +1,6 @@
|
|||||||
module toolRegistry
|
module toolRegistry
|
||||||
|
|
||||||
export toolStore, loadTools, registerTool, getTools, clearTools, listTool
|
export toolStore, registerTool, getTools, clearTools, listTool
|
||||||
|
|
||||||
using Dates
|
using Dates
|
||||||
using JSON, DataStructures
|
using JSON, DataStructures
|
||||||
@@ -45,7 +45,7 @@ end
|
|||||||
Return an `agentTool` definition for listing registered tools.
|
Return an `agentTool` definition for listing registered tools.
|
||||||
|
|
||||||
Each call produces a **new** tool object that captures (closes over)
|
Each call produces a **new** tool object that captures (closes over)
|
||||||
`store`. `loadTools` auto-registers one so the LLM can discover tools
|
`store`. `register_all_tools` auto-registers one so the LLM can discover tools
|
||||||
at runtime.
|
at runtime.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
@@ -55,7 +55,7 @@ at runtime.
|
|||||||
```julia
|
```julia
|
||||||
julia> store = toolStore(name="agent1");
|
julia> store = toolStore(name="agent1");
|
||||||
|
|
||||||
julia> loadTools(store, "src/tools") # auto-registers listTools
|
julia> register_all_tools(store) # auto-registers listTools
|
||||||
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
||||||
[toolRegistry:agent1] Registered tool: listTools
|
[toolRegistry:agent1] Registered tool: listTools
|
||||||
|
|
||||||
@@ -97,99 +97,7 @@ function listTool(store::toolStore)::agentTool
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
# Note: register_all_tools is defined in YiemAgent.jl where tool functions are in scope
|
||||||
Load `.jl` tool files from `dir` into `store`, then auto-register
|
|
||||||
`listTool` so the LLM can discover available tools at runtime.
|
|
||||||
|
|
||||||
Each `.jl` file must define `function getTool()::agentTool ... end`.
|
|
||||||
Files are sorted alphabetically for deterministic registration order.
|
|
||||||
Each file is loaded into its own Julia submodule to avoid name collisions.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
- `store`: Tool store to populate
|
|
||||||
- `dir`: Directory containing `.jl` tool files
|
|
||||||
|
|
||||||
# Returns
|
|
||||||
- The same `store.tools` dict (modified in place)
|
|
||||||
|
|
||||||
# Errors
|
|
||||||
- Throws `ArgumentError` if `dir` does not exist or a file lacks `getTool()`
|
|
||||||
|
|
||||||
# Example
|
|
||||||
```julia
|
|
||||||
julia> store = toolStore(name="agent1");
|
|
||||||
|
|
||||||
julia> loadTools(store, "src/tools")
|
|
||||||
[toolRegistry:agent1] Loaded tool: getWeather (Weather Lookup)
|
|
||||||
[toolRegistry:agent1] Loaded tool: getTime (Time Lookup)
|
|
||||||
[toolRegistry:agent1] Registered tool: listTools
|
|
||||||
OrderedDict{String, agentTool} with 3 entries:
|
|
||||||
"getWeather" => agentTool(...)
|
|
||||||
"getTime" => agentTool(...)
|
|
||||||
"listTools" => agentTool(...)
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
function loadTools(store::toolStore, dir::String)::OrderedDict{String, agentTool}
|
|
||||||
if !isdir(dir)
|
|
||||||
throw(ArgumentError("Tool directory does not exist: $dir"))
|
|
||||||
end
|
|
||||||
|
|
||||||
jl_files = filter(f -> endswith(f, ".jl") && !occursin(r"(?i)registry", f), readdir(dir))
|
|
||||||
sort!(jl_files)
|
|
||||||
|
|
||||||
for filename in jl_files
|
|
||||||
filepath = joinpath(dir, filename)
|
|
||||||
|
|
||||||
# Derive a unique module name from the filename only (not full path).
|
|
||||||
# e.g. "getWeather.jl" -> "_tool_getWeather"
|
|
||||||
mod_name = Symbol("_tool_", replace(rstrip(filename, '.'), ".jl" => ""))
|
|
||||||
|
|
||||||
# Build the complete module as a string and eval the parsed code.
|
|
||||||
# Julia does not allow `module ... end` inside eval(quote ...),
|
|
||||||
# and constructing the module AST by hand is fragile.
|
|
||||||
# Instead, we generate the full module source as a string,
|
|
||||||
# parse it, and eval the resulting expression.
|
|
||||||
# Each tool file declares its own dependencies via `using` statements
|
|
||||||
# at the top of the file — the registry only injects `using ..type`
|
|
||||||
# to make core types (agentTool, textContent, etc.) available.
|
|
||||||
file_content = read(filepath, String)
|
|
||||||
module_code = """
|
|
||||||
module $(mod_name)
|
|
||||||
using ..type
|
|
||||||
$(file_content)
|
|
||||||
end
|
|
||||||
"""
|
|
||||||
mod = eval(Meta.parse(module_code))
|
|
||||||
|
|
||||||
# Call getTool() via Core.eval in the submodule's scope.
|
|
||||||
# This evaluates getTool() entirely within the new module's world,
|
|
||||||
# completely avoiding world-age issues — no invokelatest needed.
|
|
||||||
# Note: all uses of `tool` must be inside the `try` block because
|
|
||||||
# Julia 1.12's SSA form doesn't track `tool` as definitely assigned
|
|
||||||
# after a `try-catch` where it's only assigned inside `try`.
|
|
||||||
try
|
|
||||||
tool = Core.eval(mod, :(getTool()))
|
|
||||||
if !(tool isa agentTool)
|
|
||||||
throw(ArgumentError(
|
|
||||||
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
|
|
||||||
))
|
|
||||||
end
|
|
||||||
store.tools[tool.name] = tool
|
|
||||||
println("[$(store.name)] Loaded tool: $(tool.name) ($(tool.label))")
|
|
||||||
catch e
|
|
||||||
if e isa UndefVarError || occursin("getTool", sprint(showerror, e))
|
|
||||||
throw(ArgumentError(
|
|
||||||
"Tool file $(filepath) does not define a `getTool()` function in module $(mod_name). " *
|
|
||||||
"Each tool file must define: function getTool()::agentTool ... end"
|
|
||||||
))
|
|
||||||
end
|
|
||||||
rethrow(e)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
registerTool(store, listTool(store))
|
|
||||||
return store.tools
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
registerTool(store::toolStore, tool::agentTool) -> OrderedDict{String, agentTool}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using .type
|
||||||
using Dates
|
using Dates
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -15,7 +16,7 @@ Demonstrates custom validation beyond simple required-field checking:
|
|||||||
- `nothing` if validation passes
|
- `nothing` if validation passes
|
||||||
- `String` error message if validation fails
|
- `String` error message if validation fails
|
||||||
"""
|
"""
|
||||||
function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
function getTimeValidateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
|
||||||
tz = get(args, "timezone", nothing)
|
tz = get(args, "timezone", nothing)
|
||||||
city = get(args, "city", "")
|
city = get(args, "city", "")
|
||||||
|
|
||||||
@@ -43,8 +44,8 @@ Execute the getTime tool.
|
|||||||
|
|
||||||
Returns mock time data for the given timezone or city.
|
Returns mock time data for the given timezone or city.
|
||||||
"""
|
"""
|
||||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
function getTimeExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||||
onPartialResult::Function)::agentToolResult
|
onPartialResult)
|
||||||
tz = get(args, "timezone", nothing)
|
tz = get(args, "timezone", nothing)
|
||||||
city = get(args, "city", "")
|
city = get(args, "city", "")
|
||||||
if tz !== nothing
|
if tz !== nothing
|
||||||
@@ -61,7 +62,7 @@ end
|
|||||||
"""
|
"""
|
||||||
Define and return the getTime agentTool.
|
Define and return the getTime agentTool.
|
||||||
"""
|
"""
|
||||||
function getTool()::agentTool
|
function getTimeTool()::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "getTime",
|
name = "getTime",
|
||||||
label = "Time Lookup",
|
label = "Time Lookup",
|
||||||
@@ -74,9 +75,9 @@ function getTool()::agentTool
|
|||||||
),
|
),
|
||||||
"required" => []
|
"required" => []
|
||||||
),
|
),
|
||||||
execute = executeTool,
|
execute = getTimeExecute,
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
validateRequiredArgs = validateRequiredArgs,
|
validateRequiredArgs = getTimeValidateRequiredArgs,
|
||||||
parallelToolExecute = false
|
parallelToolExecute = false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
+21
-12
@@ -1,24 +1,33 @@
|
|||||||
|
using msghandler
|
||||||
|
using .type
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Execute the getWeather tool.
|
Execute the getWeather tool.
|
||||||
|
|
||||||
Returns mock weather data for the given city and temperature units.
|
Returns mock weather data for the given city and temperature units.
|
||||||
"""
|
"""
|
||||||
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
function getWeatherExecute(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal},
|
||||||
onPartialResult::Function)::agentToolResult
|
agentEventSink)
|
||||||
city = get(args, "city", "")
|
|
||||||
units = get(args, "units", "celsius")
|
agentEventSink("Getting weather...")
|
||||||
temp = units == "fahrenheit" ? "72" : "22"
|
|
||||||
unit_symbol = units == "celsius" ? "°C" : "°F"
|
city = get(args, "city", "")
|
||||||
return agentToolResult(
|
units = get(args, "units", "celsius")
|
||||||
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
|
temp = units == "fahrenheit" ? "72" : "22"
|
||||||
Dict{Any,Any}(), nothing, false
|
unit_symbol = units == "celsius" ? "°C" : "°F"
|
||||||
)
|
|
||||||
|
return agentToolResult(
|
||||||
|
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
|
||||||
|
Dict{Any,Any}(),
|
||||||
|
nothing,
|
||||||
|
false
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Define and return the getWeather agentTool.
|
Define and return the getWeather agentTool.
|
||||||
"""
|
"""
|
||||||
function getTool()::agentTool
|
function getWeatherTool()::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "getWeather",
|
name = "getWeather",
|
||||||
label = "Weather Lookup",
|
label = "Weather Lookup",
|
||||||
@@ -31,7 +40,7 @@ function getTool()::agentTool
|
|||||||
),
|
),
|
||||||
"required" => ["city"]
|
"required" => ["city"]
|
||||||
),
|
),
|
||||||
execute = executeTool,
|
execute = getWeatherExecute,
|
||||||
prepareArguments = nothing,
|
prepareArguments = nothing,
|
||||||
validateRequiredArgs = nothing,
|
validateRequiredArgs = nothing,
|
||||||
parallelToolExecute = false
|
parallelToolExecute = false
|
||||||
|
|||||||
+12
-9
@@ -1,3 +1,4 @@
|
|||||||
|
using .type
|
||||||
using JSON
|
using JSON
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -7,15 +8,17 @@ 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 `loadTools(agent._tool_store, "src/tools")` picks
|
After calling this tool, add the new file to `YiemAgent.jl` with an `include()`
|
||||||
up the new file. The new tool is immediately available.
|
statement (after `include("toolRegistry.jl")`), then restart the agent.
|
||||||
|
The new tool must be registered in `register_all_tools()` in `toolRegistry.jl`.
|
||||||
|
|
||||||
# Example
|
# Example
|
||||||
|
|
||||||
1. Agent calls writeTool with a spec for a "searchWine" tool
|
1. Agent calls writeTool with a spec for a "searchWine" tool
|
||||||
2. writeTool generates src/tools/searchWine.jl
|
2. writeTool generates src/tools/searchWine.jl
|
||||||
3. Restart agent — loadTools() picks up the new file
|
3. Developer adds `include("tools/searchWine.jl")` to YiemAgent.jl
|
||||||
4. Agent calls searchWine with args
|
4. Developer adds `registerTool(store, searchWineTool())` to register_all_tools()
|
||||||
|
5. Restart agent — new tool is available
|
||||||
|
|
||||||
# How It Works
|
# How It Works
|
||||||
|
|
||||||
@@ -24,7 +27,7 @@ tool logic as `executeCode`, and writeTool wraps it in Julia boilerplate:
|
|||||||
- Converts `inputSchema` Dict into Julia `Dict{String,Any}(...)` string
|
- Converts `inputSchema` Dict into Julia `Dict{String,Any}(...)` string
|
||||||
- Indents `executeCode` with 4 spaces
|
- Indents `executeCode` with 4 spaces
|
||||||
- Wraps it inside `function executeTool(...)::agentToolResult ... end`
|
- Wraps it inside `function executeTool(...)::agentToolResult ... end`
|
||||||
- Appends `getTool()` returning an `agentTool` struct
|
- Appends `writeToolTool()` returning an `agentTool` struct
|
||||||
- Writes the combined string to `src/tools/<name>.jl`
|
- Writes the combined string to `src/tools/<name>.jl`
|
||||||
|
|
||||||
# Important Notes
|
# Important Notes
|
||||||
@@ -105,7 +108,7 @@ end
|
|||||||
"""
|
"""
|
||||||
Define and return the writeTool agentTool.
|
Define and return the writeTool agentTool.
|
||||||
"""
|
"""
|
||||||
function getTool()::agentTool
|
function writeToolTool()::agentTool
|
||||||
return agentTool(
|
return agentTool(
|
||||||
name = "writeTool",
|
name = "writeTool",
|
||||||
label = "Create Tool",
|
label = "Create Tool",
|
||||||
@@ -127,7 +130,7 @@ function getTool()::agentTool
|
|||||||
),
|
),
|
||||||
"required" => ["name", "label", "description", "inputSchema", "executeCode"]
|
"required" => ["name", "label", "description", "inputSchema", "executeCode"]
|
||||||
),
|
),
|
||||||
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) -> begin
|
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult) -> begin
|
||||||
tool_name = get(args, "name", "")::String
|
tool_name = get(args, "name", "")::String
|
||||||
tool_label = get(args, "label", tool_name)::String
|
tool_label = get(args, "label", tool_name)::String
|
||||||
tool_description = get(args, "description", "")::String
|
tool_description = get(args, "description", "")::String
|
||||||
@@ -250,13 +253,13 @@ function getTool()::agentTool
|
|||||||
|
|
||||||
tool_code = join(parts)
|
tool_code = join(parts)
|
||||||
|
|
||||||
# Write the file — tool is loaded on next agent restart via loadTools(store, "src/tools")
|
# Write the file — tool must be included in YiemAgent.jl and registered in register_all_tools()
|
||||||
write(filepath, tool_code)
|
write(filepath, tool_code)
|
||||||
|
|
||||||
onPartialResult(Dict("status" => "Done"))
|
onPartialResult(Dict("status" => "Done"))
|
||||||
|
|
||||||
return agentToolResult(
|
return agentToolResult(
|
||||||
[textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools(agent._tool_store, \"src/tools\") picks it up, then call listTools to verify.")],
|
[textContent("Tool '$(tool_name)' written to $filepath. Add include(\"tools/$(tool_name).jl\") to YiemAgent.jl and registerTool(store, $(tool_name)Tool()) to register_all_tools(), then restart the agent.")],
|
||||||
Dict{Any,Any}(
|
Dict{Any,Any}(
|
||||||
"file" => filepath,
|
"file" => filepath,
|
||||||
"name" => tool_name,
|
"name" => tool_name,
|
||||||
|
|||||||
+2
-2
@@ -264,7 +264,7 @@ struct agentTool # A tool available to the agent
|
|||||||
label::String # Human-readable tool name
|
label::String # Human-readable tool name
|
||||||
description::String # What the tool does
|
description::String # What the tool does
|
||||||
inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format)
|
inputSchema::Any # Tool parameters schema (JSON schema, MCP inputSchema format)
|
||||||
execute::Function # Tool execution function
|
execute # Tool execution function
|
||||||
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
prepareArguments::Union{Function, Nothing} # Optional argument preparation callback
|
||||||
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
|
validateRequiredArgs::Union{Function, Nothing} # Optional validation hook for required args
|
||||||
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
|
||||||
@@ -274,7 +274,7 @@ end
|
|||||||
Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ...)`.
|
Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ...)`.
|
||||||
"""
|
"""
|
||||||
function agentTool(; name::String, label::String, description::String, inputSchema::Any,
|
function agentTool(; name::String, label::String, description::String, inputSchema::Any,
|
||||||
execute::Function, prepareArguments::Union{Function, Nothing}=nothing,
|
execute, prepareArguments::Union{Function, Nothing}=nothing,
|
||||||
validateRequiredArgs::Union{Function, Nothing}=nothing,
|
validateRequiredArgs::Union{Function, Nothing}=nothing,
|
||||||
parallelToolExecute::Bool=false)
|
parallelToolExecute::Bool=false)
|
||||||
return agentTool(name, label, description, inputSchema, execute,
|
return agentTool(name, label, description, inputSchema, execute,
|
||||||
|
|||||||
+57
-3
@@ -241,7 +241,38 @@ function formatMsgForLLM(ctx::agentContext, agentEventSink)::Dict{String, Any}
|
|||||||
return openaiReadyMsg
|
return openaiReadyMsg
|
||||||
end
|
end
|
||||||
|
|
||||||
#TODO
|
"""
|
||||||
|
beforeToolCall(context::beforeToolCallContext, signal::abortSignal) -> beforeToolCallResult
|
||||||
|
|
||||||
|
Callback invoked before executing a tool call. Use this hook to inspect
|
||||||
|
the tool call and decide whether to allow, block, or modify it.
|
||||||
|
|
||||||
|
Common use cases:
|
||||||
|
- Request user approval via UI before running destructive tools.
|
||||||
|
- Validate business rules that cannot be expressed in the JSON schema.
|
||||||
|
- Check final context (e.g. session state, rate limits, permissions).
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `context::beforeToolCallContext`: Contains the assistant message, tool call,
|
||||||
|
validated arguments, and current conversation context.
|
||||||
|
- `signal::abortSignal`: Signal that may be set to abort the operation.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `beforeToolCallResult(false, "N/A")` to allow the call to proceed.
|
||||||
|
- `beforeToolCallResult(true, "Reason")` to block the call with a reason.
|
||||||
|
- `nothing` is treated as allow (equivalent to `beforeToolCallResult(false, "N/A")`).
|
||||||
|
|
||||||
|
# Example
|
||||||
|
```julia
|
||||||
|
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal)
|
||||||
|
if context.toolCall.name == "deleteFile"
|
||||||
|
# Block file deletion unless explicitly approved
|
||||||
|
return beforeToolCallResult(true, "User must approve file deletion")
|
||||||
|
end
|
||||||
|
return beforeToolCallResult(false, "N/A")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
"""
|
||||||
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal
|
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal
|
||||||
)::beforeToolCallResult
|
)::beforeToolCallResult
|
||||||
|
|
||||||
@@ -254,8 +285,31 @@ function beforeToolCall(context::beforeToolCallContext, signal::abortSignal
|
|||||||
return beforeToolCallResult(false, "N/A")
|
return beforeToolCallResult(false, "N/A")
|
||||||
end
|
end
|
||||||
|
|
||||||
#TODO
|
"""
|
||||||
function afterToolCall(context::beforeToolCallContext, signal::abortSignal
|
afterToolCall(context::afterToolCallContext, signal::abortSignal) -> Union{agentToolResult, Nothing}
|
||||||
|
|
||||||
|
Callback invoked after a tool call finishes executing (before and after errors).
|
||||||
|
Use this hook to post-process the tool result before it is fed back to the LLM.
|
||||||
|
|
||||||
|
Common use cases:
|
||||||
|
- Mask sensitive data (API keys, tokens) from result content.
|
||||||
|
- Normalize usage tracking data into a consistent format.
|
||||||
|
- Inspect the result and set `terminate: true` based on business logic
|
||||||
|
(e.g. "if deployment failed, stop the agent rather than retrying").
|
||||||
|
- Wrap error results in friendlier messages for the LLM to understand.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `context::afterToolCallContext`: Contains the assistant message, tool call,
|
||||||
|
arguments, raw result, error status, and current conversation context.
|
||||||
|
- `signal::abortSignal`: Signal that may be set to abort the operation.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- `nothing` to pass the result through unchanged.
|
||||||
|
- `agentToolResult(...)` to return a modified result (content, details, usage,
|
||||||
|
terminate flag can all be overridden).
|
||||||
|
|
||||||
|
"""
|
||||||
|
function afterToolCall(context::afterToolCallContext, signal::abortSignal
|
||||||
)::Union{agentToolResult, Nothing}
|
)::Union{agentToolResult, Nothing}
|
||||||
|
|
||||||
# modify context.result if needed and return agentToolResult
|
# modify context.result if needed and return agentToolResult
|
||||||
|
|||||||
+192
-37
@@ -1,35 +1,19 @@
|
|||||||
using Test
|
using Test
|
||||||
|
using Dates
|
||||||
using YiemAgent
|
using YiemAgent
|
||||||
using YiemAgent.toolRegistry
|
using YiemAgent.toolRegistry
|
||||||
using YiemAgent.type
|
using YiemAgent.type
|
||||||
|
using YiemAgent.agentCore
|
||||||
|
|
||||||
# Path to the real tools directory
|
@testset "register_all_tools with toolStore" begin
|
||||||
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|
||||||
|
|
||||||
@testset "loadTools with toolStore" begin
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 1. loadTools throws on non-existent directory #
|
# 1. register_all_tools registers all static tools + listTools #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
store = toolStore(name="test1")
|
store = toolStore(name="test1")
|
||||||
@test_throws ArgumentError loadTools(store, "/nonexistent/dir/that/does/not/exist")
|
loaded = register_all_tools(store)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 2. loadTools throws if a .jl file does not define getTool() #
|
|
||||||
# Must run BEFORE any other loadTools call (getTool binding #
|
|
||||||
# persists in module scope after include()). #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
bad_dir = mktempdir()
|
|
||||||
write(joinpath(bad_dir, "noTool.jl"), "x = 42\n")
|
|
||||||
@test_throws ArgumentError loadTools(store, bad_dir)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 3. loadTools loads actual tool files from src/tools/ #
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
store2 = toolStore(name="test2")
|
|
||||||
loaded = loadTools(store2, TOOLS_DIR)
|
|
||||||
@test !isempty(loaded)
|
@test !isempty(loaded)
|
||||||
@test length(loaded) == 4 # 3 files + auto-registered listTools
|
@test length(loaded) == 4 # getWeather + getTime + writeTool + listTools
|
||||||
|
|
||||||
names = [k for k in keys(loaded)]
|
names = [k for k in keys(loaded)]
|
||||||
@test "getTime" in names
|
@test "getTime" in names
|
||||||
@@ -38,16 +22,15 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
@test "listTools" in names
|
@test "listTools" in names
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 4. loadTools returns tools sorted alphabetically by filename #
|
# 2. register_all_tools returns tools in registration order #
|
||||||
# (getTime.jl < getWeather.jl < writeTool.jl) + listTools at end #
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@test collect(keys(loaded))[1] == "getTime"
|
@test collect(keys(loaded))[1] == "getWeather"
|
||||||
@test collect(keys(loaded))[2] == "getWeather"
|
@test collect(keys(loaded))[2] == "getTime"
|
||||||
@test collect(keys(loaded))[3] == "writeTool"
|
@test collect(keys(loaded))[3] == "writeTool"
|
||||||
@test collect(keys(loaded))[4] == "listTools"
|
@test collect(keys(loaded))[4] == "listTools"
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 5. Verify loaded tool fields are correct #
|
# 3. Verify loaded tool fields are correct #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# getTime
|
# getTime
|
||||||
time_tool = loaded["getTime"]
|
time_tool = loaded["getTime"]
|
||||||
@@ -74,7 +57,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
@test "executeCode" in wt.inputSchema["required"]
|
@test "executeCode" in wt.inputSchema["required"]
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 6. Tool execution returns valid results #
|
# 4. Tool execution returns valid results #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
sig = nothing
|
sig = nothing
|
||||||
op = x -> x # no-op partial result callback
|
op = x -> x # no-op partial result callback
|
||||||
@@ -98,16 +81,15 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
|
|
||||||
# execute getWeather with units
|
# execute getWeather with units
|
||||||
result_w2 = weather.execute("call-4", Dict{String,Any}("city" => "London", "units" => "fahrenheit"), sig, op)
|
result_w2 = weather.execute("call-4", Dict{String,Any}("city" => "London", "units" => "fahrenheit"), sig, op)
|
||||||
@test occursin("72°F", result_w2.content[1].text)
|
@test occursin("72\u00b0F", result_w2.content[1].text)
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 7. getTools / registerTool / clearTools (per-store isolation) #
|
# 5. getTools / registerTool / clearTools (per-store isolation) #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
store3 = toolStore(name="test3")
|
store3 = toolStore(name="test3")
|
||||||
registry_tools = getTools(store3)
|
registry_tools = getTools(store3)
|
||||||
@test isempty(registry_tools)
|
@test isempty(registry_tools)
|
||||||
|
|
||||||
# listTool is not auto-registered anymore — each store starts empty
|
|
||||||
# Register tools manually
|
# Register tools manually
|
||||||
registerTool(store3, loaded["getTime"])
|
registerTool(store3, loaded["getTime"])
|
||||||
registerTool(store3, loaded["getWeather"])
|
registerTool(store3, loaded["getWeather"])
|
||||||
@@ -143,7 +125,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
@test reg["manualTool"].parallelToolExecute == true
|
@test reg["manualTool"].parallelToolExecute == true
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 8. getTools returns direct reference (mutations affect registry) #
|
# 6. getTools returns direct reference (mutations affect registry) #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
copy1 = getTools(store3)
|
copy1 = getTools(store3)
|
||||||
copy2 = getTools(store3)
|
copy2 = getTools(store3)
|
||||||
@@ -152,7 +134,7 @@ TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
|
|||||||
@test isempty(getTools(store3)) # mutation propagates
|
@test isempty(getTools(store3)) # mutation propagates
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# 9. Per-store isolation — two stores don't share tools #
|
# 7. Per-store isolation — two stores don't share tools #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
storeA = toolStore(name="isolationA")
|
storeA = toolStore(name="isolationA")
|
||||||
storeB = toolStore(name="isolationB")
|
storeB = toolStore(name="isolationB")
|
||||||
@@ -175,10 +157,10 @@ end
|
|||||||
|
|
||||||
@testset "listTool" begin
|
@testset "listTool" begin
|
||||||
store = toolStore(name="test_list")
|
store = toolStore(name="test_list")
|
||||||
loaded = loadTools(store, TOOLS_DIR) # auto-registers getWeather, getTime, writeTool + listTools
|
register_all_tools(store) # auto-registers getWeather, getTime, writeTool + listTools
|
||||||
|
|
||||||
# loadTools auto-registers listTool
|
# register_all_tools auto-registers listTool
|
||||||
@test "listTools" in keys(loaded)
|
@test "listTools" in keys(store.tools)
|
||||||
|
|
||||||
# listTool returns an agentTool, not a string or array
|
# listTool returns an agentTool, not a string or array
|
||||||
list_t = listTool(store)
|
list_t = listTool(store)
|
||||||
@@ -199,7 +181,7 @@ end
|
|||||||
|
|
||||||
# Each listTool call creates an independent closure
|
# Each listTool call creates an independent closure
|
||||||
storeB = toolStore(name="test_listB")
|
storeB = toolStore(name="test_listB")
|
||||||
registerTool(storeB, loaded["getWeather"])
|
registerTool(storeB, store.tools["getWeather"])
|
||||||
list_tB = listTool(storeB)
|
list_tB = listTool(storeB)
|
||||||
|
|
||||||
resultA = list_t.execute("call-3", Dict{String,Any}(), nothing, x -> x)
|
resultA = list_t.execute("call-3", Dict{String,Any}(), nothing, x -> x)
|
||||||
@@ -210,3 +192,176 @@ end
|
|||||||
@test occursin("getTime", resultA.content[1].text)
|
@test occursin("getTime", resultA.content[1].text)
|
||||||
@test occursin("getTime", resultB.content[1].text) == false # storeB only has getWeather
|
@test occursin("getTime", resultB.content[1].text) == false # storeB only has getWeather
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@testset "executePreparedToolCall with static tools" begin
|
||||||
|
# Tests executePreparedToolCall with statically loaded tools.
|
||||||
|
# The world-age issue is resolved because tool.execute comes from
|
||||||
|
# a statically included module, not a dynamically created one.
|
||||||
|
|
||||||
|
store = toolStore(name="test_static")
|
||||||
|
register_all_tools(store)
|
||||||
|
|
||||||
|
weather_tool = store.tools["getWeather"]
|
||||||
|
|
||||||
|
# Create a preparedToolCall that mimics what prepareToolCall() returns
|
||||||
|
tool_call = agentToolCall(
|
||||||
|
"function", "call-static-1", "getWeather",
|
||||||
|
Dict{String,Any}("city" => "San Francisco")
|
||||||
|
)
|
||||||
|
prep = preparedToolCall(
|
||||||
|
weather_tool, tool_call, Dict{String,Any}("city" => "San Francisco")
|
||||||
|
)
|
||||||
|
sig = abortSignal(false)
|
||||||
|
|
||||||
|
# This call goes through: executePreparedToolCall -> prep.tool.execute(...)
|
||||||
|
result = executePreparedToolCall(
|
||||||
|
prep, sig, x -> nothing
|
||||||
|
)
|
||||||
|
|
||||||
|
@test result isa executedOutcome
|
||||||
|
@test result.isError == false
|
||||||
|
@test result.result.content[1] isa textContent
|
||||||
|
@test occursin("San Francisco", result.result.content[1].text)
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "executePreparedToolCall with validation (static tools)" begin
|
||||||
|
# Tests executePreparedToolCall with a tool that has custom validation hooks.
|
||||||
|
# This exercises the full tool execution path including validation.
|
||||||
|
|
||||||
|
store = toolStore(name="test_static_validate")
|
||||||
|
register_all_tools(store)
|
||||||
|
|
||||||
|
time_tool = store.tools["getTime"]
|
||||||
|
|
||||||
|
tool_call = agentToolCall(
|
||||||
|
"function", "call-static-2", "getTime",
|
||||||
|
Dict{String,Any}("timezone" => "America/New_York")
|
||||||
|
)
|
||||||
|
prep = preparedToolCall(
|
||||||
|
time_tool, tool_call, Dict{String,Any}("timezone" => "America/New_York")
|
||||||
|
)
|
||||||
|
sig = abortSignal(false)
|
||||||
|
|
||||||
|
result = executePreparedToolCall(
|
||||||
|
prep, sig, x -> nothing
|
||||||
|
)
|
||||||
|
|
||||||
|
@test result isa executedOutcome
|
||||||
|
@test result.isError == false
|
||||||
|
@test result.result.content[1] isa textContent
|
||||||
|
@test occursin("America/New_York", result.result.content[1].text)
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "executeToolCallsSequential with static tools (full pipeline)" begin
|
||||||
|
# Tests the full tool execution pipeline: executeToolCallsSequential
|
||||||
|
# which calls prepareToolCall -> executePreparedToolCall -> finalizeExecutedToolCall
|
||||||
|
# with statically loaded tools.
|
||||||
|
|
||||||
|
store = toolStore(name="test_full_pipeline")
|
||||||
|
register_all_tools(store)
|
||||||
|
|
||||||
|
# Build agentContext from the store's tools
|
||||||
|
tools = getTools(store)
|
||||||
|
ctx = agentContext(
|
||||||
|
"test system prompt",
|
||||||
|
agentMessage[],
|
||||||
|
tools
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an assistant message containing tool calls
|
||||||
|
assistant_msg = assistantMessage(
|
||||||
|
role="assistant",
|
||||||
|
content=Vector{messageContent}(),
|
||||||
|
api="openai",
|
||||||
|
provider="test",
|
||||||
|
model="test-model",
|
||||||
|
usage=llmUsage(0, 0),
|
||||||
|
stopReason="tool_calls",
|
||||||
|
errorMessage=nothing,
|
||||||
|
timestamp=now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create tool calls for multiple statically loaded tools
|
||||||
|
tool_calls = [
|
||||||
|
agentToolCall(
|
||||||
|
"function", "call-seq-1", "getWeather",
|
||||||
|
Dict{String,Any}("city" => "Tokyo")
|
||||||
|
),
|
||||||
|
agentToolCall(
|
||||||
|
"function", "call-seq-2", "getTime",
|
||||||
|
Dict{String,Any}("timezone" => "Europe/London")
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
config = agentLoopConfig(
|
||||||
|
nothing, nothing, "sequential"
|
||||||
|
)
|
||||||
|
sig = abortSignal(false)
|
||||||
|
|
||||||
|
# Execute the full pipeline
|
||||||
|
batch = executeToolCallsSequential(
|
||||||
|
ctx, assistant_msg, tool_calls, config, sig, x -> nothing
|
||||||
|
)
|
||||||
|
|
||||||
|
@test batch.messages isa Vector{toolResultMessage}
|
||||||
|
@test length(batch.messages) == 2
|
||||||
|
@test batch.messages[1].toolName == "getWeather"
|
||||||
|
@test batch.messages[1].isError == false
|
||||||
|
@test occursin("Tokyo", batch.messages[1].content[1].text)
|
||||||
|
@test batch.messages[2].toolName == "getTime"
|
||||||
|
@test batch.messages[2].isError == false
|
||||||
|
@test occursin("Europe/London", batch.messages[2].content[1].text)
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "executeToolCallsParallel with static tools (full pipeline)" begin
|
||||||
|
# Same as above but tests parallel execution path.
|
||||||
|
|
||||||
|
store = toolStore(name="test_parallel")
|
||||||
|
register_all_tools(store)
|
||||||
|
|
||||||
|
tools = getTools(store)
|
||||||
|
ctx = agentContext(
|
||||||
|
"test system prompt",
|
||||||
|
agentMessage[],
|
||||||
|
tools
|
||||||
|
)
|
||||||
|
|
||||||
|
assistant_msg = assistantMessage(
|
||||||
|
role="assistant",
|
||||||
|
content=Vector{messageContent}(),
|
||||||
|
api="openai",
|
||||||
|
provider="test",
|
||||||
|
model="test-model",
|
||||||
|
usage=llmUsage(0, 0),
|
||||||
|
stopReason="tool_calls",
|
||||||
|
errorMessage=nothing,
|
||||||
|
timestamp=now()
|
||||||
|
)
|
||||||
|
|
||||||
|
tool_calls = [
|
||||||
|
agentToolCall(
|
||||||
|
"function", "call-par-1", "getWeather",
|
||||||
|
Dict{String,Any}("city" => "Paris")
|
||||||
|
),
|
||||||
|
agentToolCall(
|
||||||
|
"function", "call-par-2", "getTime",
|
||||||
|
Dict{String,Any}("city" => "Sydney")
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
config = agentLoopConfig(
|
||||||
|
nothing, nothing, "parallel"
|
||||||
|
)
|
||||||
|
sig = abortSignal(false)
|
||||||
|
|
||||||
|
batch = executeToolCallsParallel(
|
||||||
|
ctx, assistant_msg, tool_calls, config, sig, x -> nothing
|
||||||
|
)
|
||||||
|
|
||||||
|
@test batch.messages isa Vector{toolResultMessage}
|
||||||
|
@test length(batch.messages) == 2
|
||||||
|
@test batch.messages[1].toolName == "getWeather"
|
||||||
|
@test batch.messages[1].isError == false
|
||||||
|
@test batch.messages[2].toolName == "getTime"
|
||||||
|
@test batch.messages[2].isError == false
|
||||||
|
end
|
||||||
|
|||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
using Revise, JSON, Dates, UUIDs, PrettyPrinting, LibPQ, Base64, DataFrames, DataStructures, HTTP, Base64,
|
||||||
|
NATS, Base.Threads
|
||||||
|
using YiemAgent, GeneralUtils, msghandler
|
||||||
|
|
||||||
|
|
||||||
|
""" Debug
|
||||||
|
|
||||||
|
using JSON, NATS, msghandler
|
||||||
|
using NATS
|
||||||
|
|
||||||
|
conn = NATS.connect("nats.yiem.cc")
|
||||||
|
|
||||||
|
sub = NATS.subscribe(conn, "sommanion.debug") do msg
|
||||||
|
payload = NATS.payload(msg)
|
||||||
|
@info "debug" payload
|
||||||
|
|
||||||
|
open("./log/error.log", "a") do io
|
||||||
|
println(io, payload)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
NATS.publish(conn, "sommanion.debug", "order-123")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------- inject this code into codebase to debug --------------------------- #
|
||||||
|
try
|
||||||
|
batch = someFunction(x, y, z)
|
||||||
|
catch e
|
||||||
|
bt = catch_backtrace()
|
||||||
|
err_msg = sprint() do io
|
||||||
|
showerror(io, e, bt)
|
||||||
|
println(io)
|
||||||
|
end
|
||||||
|
|
||||||
|
agentEventSink(err_msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
struct text2textInstructLLM
|
||||||
|
natsConn::NATS.Connection
|
||||||
|
topic::String
|
||||||
|
senderID::String
|
||||||
|
fileserver_url::String
|
||||||
|
end
|
||||||
|
|
||||||
|
function (t::text2textInstructLLM)(openai_msg::Dict{String, Any})
|
||||||
|
|
||||||
|
payloads = [("msg", openai_msg, "dictionary")] # List of tuples
|
||||||
|
_, msg_envelope_json_str = msghandler.smartpack(
|
||||||
|
t.topic,
|
||||||
|
payloads;
|
||||||
|
sender_id=t.senderID,
|
||||||
|
msg_purpose="text2text",
|
||||||
|
fileserver_url=t.fileserver_url)
|
||||||
|
|
||||||
|
reply = NATS.request(t.natsConn, t.topic, msg_envelope_json_str, timeout=180)
|
||||||
|
|
||||||
|
incoming_env_json_str = String(reply.payload)
|
||||||
|
incoming_env = msghandler.smartunpack(incoming_env_json_str)
|
||||||
|
_llm_response = incoming_env["payloads"][1][2]
|
||||||
|
llm_response = _llm_response["choices"][1]
|
||||||
|
|
||||||
|
return llm_response
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
struct agentEventSink
|
||||||
|
natsConn::NATS.Connection
|
||||||
|
topic::String
|
||||||
|
senderID::String
|
||||||
|
end
|
||||||
|
|
||||||
|
function (aes::agentEventSink)(msg::String)
|
||||||
|
NATS.publish(aes.natsConn, aes.topic, msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
config = JSON.parsefile("./appconfig.json")
|
||||||
|
agent_conn = NATS.connect(config["nats_server_info"]["url"])
|
||||||
|
|
||||||
|
|
||||||
|
#WORKING load tools
|
||||||
|
text2text_llm = text2textInstructLLM(agent_conn,
|
||||||
|
config["externalservice"]["servicesloadbalancer"]["nats"],
|
||||||
|
"sender",
|
||||||
|
config["externalservice"]["fileserver"]["url"])
|
||||||
|
|
||||||
|
debugNats = agentEventSink(agent_conn, "sommanion.debug", "sender")
|
||||||
|
|
||||||
|
agent = YiemAgent.yiemAgent(
|
||||||
|
text2text_llm;
|
||||||
|
agentEventSink=debugNats
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = Dict(
|
||||||
|
"role" => "user",
|
||||||
|
"content" => [
|
||||||
|
Dict("type" => "text", "text" => "What's the weather in Bangkok?"),
|
||||||
|
# Dict(
|
||||||
|
# "type" => "image_url",
|
||||||
|
# "image_url" => Dict("url" => "data:mime_type;base64,image2_base64_string")
|
||||||
|
# ),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
push!(agent.inputChannel, msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user