Merge pull request 'V0.8.0 add tools' (#41) from v0.8.0-add_tools into v0.8.0

Reviewed-on: #41
This commit was merged in pull request #41.
This commit is contained in:
ton
2026-08-09 11:47:02 +00:00
18 changed files with 885 additions and 678 deletions
+58 -94
View File
@@ -2,7 +2,7 @@
julia_version = "1.12.6"
manifest_format = "2.0"
project_hash = "1c1379a2cec320abc347f3acb5ee815ba9855aa6"
project_hash = "0db36d4fb31037ba05065476e6aebaf4cd0e1e8c"
[[deps.Accessors]]
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
@@ -97,9 +97,9 @@ uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
version = "0.7.8"
[[deps.CommonSolve]]
git-tree-sha1 = "eeaad7cef88554c2fa56b5a3f71cfd5cb708c662"
git-tree-sha1 = "cf963add2340ad9960e5eb22844e61ad8f931fe1"
uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
version = "0.2.11"
version = "0.2.13"
[[deps.Compat]]
deps = ["TOML", "UUIDs"]
@@ -146,9 +146,9 @@ version = "1.6.0"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
[[deps.Crayons]]
git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15"
git-tree-sha1 = "54b76cbb40d9a0f5368c880725b2f141da77c94f"
uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
version = "4.1.1"
version = "4.2.0"
[[deps.DBInterface]]
git-tree-sha1 = "a444404b3f94deaa43ca2a58e18153a82695282b"
@@ -168,9 +168,9 @@ version = "1.8.2"
[[deps.DataStructures]]
deps = ["OrderedCollections"]
git-tree-sha1 = "6fb53a69613a0b2b68a0d12671717d307ab8b24e"
git-tree-sha1 = "b0bc6d2cad1fed8b7fd59a1551a991cb3d2809e6"
uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
version = "0.19.5"
version = "0.19.6"
[[deps.DataValueInterfaces]]
git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
@@ -208,9 +208,9 @@ version = "1.11.0"
[[deps.Distributions]]
deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "Roots", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"]
git-tree-sha1 = "cd3c5ac74cd3923c8945c6a81518c46abd0e73a3"
git-tree-sha1 = "d2facc77c08c1c2bfb1a77c148edd05b3db5410b"
uuid = "31c24e10-a181-5473-b8eb-7969acd0382f"
version = "0.25.129"
version = "0.25.130"
[deps.Distributions.extensions]
DistributionsChainRulesCoreExt = "ChainRulesCore"
@@ -240,15 +240,9 @@ uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56"
version = "1.0.7"
[[deps.ExprTools]]
git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec"
git-tree-sha1 = "d2e49e7efd29719d6f28b891b0e0e159daa9d2b4"
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
version = "0.1.10"
[[deps.EzXML]]
deps = ["Printf", "XML2_jll"]
git-tree-sha1 = "7ea1aa5869e2626ccae84480e4f37185bc6f41d3"
uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
version = "1.2.3"
version = "0.1.11"
[[deps.FileIO]]
deps = ["Pkg", "Requires", "UUIDs"]
@@ -277,9 +271,9 @@ version = "1.11.0"
[[deps.FillArrays]]
deps = ["LinearAlgebra"]
git-tree-sha1 = "2f979084d1e13948a3352cf64a25df6bd3b4dca3"
git-tree-sha1 = "5bad39456d9f0166184fce2248783dd9862645c1"
uuid = "1a297f60-69ca-5386-bcde-b61e274b549b"
version = "1.16.0"
version = "1.17.0"
weakdeps = ["PDMats", "SparseArrays", "StaticArrays", "Statistics"]
[deps.FillArrays.extensions]
@@ -300,11 +294,11 @@ version = "1.1.0"
[[deps.GeneralUtils]]
deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "Graphs", "HTTP", "JSON", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "StringDistances", "UUIDs"]
git-tree-sha1 = "93293126d24d3929ef6a5067f347bc28c6582c71"
git-tree-sha1 = "129b8fa1bf3bf6d8c0a080f39db09b9b986ef8de"
repo-rev = "main"
repo-url = "https://git.yiem.cc/ton/GeneralUtils"
uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
version = "0.5.10"
version = "0.5.11"
[[deps.Graphs]]
deps = ["ArnoldiMethod", "DataStructures", "Inflate", "LinearAlgebra", "Random", "SimpleTraits", "SparseArrays", "Statistics"]
@@ -321,9 +315,9 @@ version = "1.14.0"
[[deps.HTTP]]
deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"]
git-tree-sha1 = "c2c808326222b6dc4bec295a83b55f79aeec98e0"
git-tree-sha1 = "0a58fbbdee93d132a2fb1159b7f5e1b5c2465e71"
uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3"
version = "2.5.5"
version = "2.6.4"
[[deps.HashArrayMappedTries]]
git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae"
@@ -332,9 +326,9 @@ version = "0.2.0"
[[deps.HypergeometricFunctions]]
deps = ["Gamma", "LinearAlgebra"]
git-tree-sha1 = "18d7deab5fb0440dc6a7b6993c5c27b25420de10"
git-tree-sha1 = "31bb6c92405c084617facc1d7ed9eb6c402d061e"
uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a"
version = "0.3.29"
version = "0.3.30"
[[deps.ICU_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
@@ -412,9 +406,9 @@ version = "1.8.0"
[[deps.JSON]]
deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"]
git-tree-sha1 = "c89d196f5ffb64bfbf80985b699ea913b0d2c211"
git-tree-sha1 = "65979512c25a0727f050e6e4be40f0fd9ec893f7"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "1.6.1"
version = "1.7.0"
weakdeps = ["ArrowTypes"]
[deps.JSON.extensions]
@@ -432,9 +426,9 @@ weakdeps = ["ArrowTypes"]
[[deps.JuliaInterpreter]]
deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"]
git-tree-sha1 = "58927c485919bf17ea308d9d82156de1adf4b006"
git-tree-sha1 = "c3d401f110454b4ea24a76be33f6ee0d7d385103"
uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
version = "0.10.12"
version = "0.11.4"
[[deps.JuliaSyntaxHighlighting]]
deps = ["StyledStrings"]
@@ -506,12 +500,6 @@ version = "1.11.3+1"
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
version = "1.11.0"
[[deps.Libiconv_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809"
uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531"
version = "1.18.0+0"
[[deps.LinearAlgebra]]
deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
@@ -539,9 +527,9 @@ version = "1.11.0"
[[deps.LoweredCodeUtils]]
deps = ["CodeTracking", "Compiler", "JuliaInterpreter"]
git-tree-sha1 = "3733419e9a71156b389f3e331672d2e95436783f"
git-tree-sha1 = "1d4c737ab26f51ceed52ab2019c09b7660eb7440"
uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
version = "3.6.2"
version = "3.8.0"
[[deps.MacroTools]]
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
@@ -599,9 +587,9 @@ version = "0.1.1"
[[deps.NanoDates]]
deps = ["Dates", "Parsers"]
git-tree-sha1 = "850a0557ae5934f6e67ac0dc5ca13d0328422d1f"
git-tree-sha1 = "77c7e98ca39aefb481f9b97a2f4f5c5471c08a1d"
uuid = "46f1a544-deae-4307-8689-c12aa3c955c6"
version = "1.0.3"
version = "1.1.0"
[[deps.NetworkOptions]]
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
@@ -640,15 +628,15 @@ uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e"
version = "0.5.6+0"
[[deps.OrderedCollections]]
git-tree-sha1 = "94ba93778373a53bfd5a0caaf7d809c445292ff4"
git-tree-sha1 = "05f45c2e0de6259db764adbfd2f1dc6d3f8de13c"
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
version = "1.8.2"
version = "2.0.1"
[[deps.PDMats]]
deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
git-tree-sha1 = "26766d4b5f1a410c218a19b85a672c6edb693c65"
git-tree-sha1 = "123266c25174ef6c8d4718920abc206452cf8de6"
uuid = "90014a1f-27ba-587c-ab20-58faa44d9150"
version = "0.11.40"
version = "0.11.41"
weakdeps = ["StatsBase"]
[deps.PDMats.extensions]
@@ -656,9 +644,9 @@ weakdeps = ["StatsBase"]
[[deps.Parsers]]
deps = ["Dates", "PrecompileTools", "UUIDs"]
git-tree-sha1 = "32a4e09c5f29402573d673901778a0e03b0807b9"
git-tree-sha1 = "3de8f5e6e90ebfa8d6d1f86997d6cdcd6a912ff3"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "2.8.6"
version = "2.8.7"
[[deps.Pkg]]
deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"]
@@ -688,15 +676,15 @@ uuid = "21216c6a-2e73-6563-6e65-726566657250"
version = "1.5.2"
[[deps.PrettyPrinting]]
git-tree-sha1 = "142ee93724a9c5d04d78df7006670a93ed1b244e"
git-tree-sha1 = "0b7f4ad437e31c51cf5b91fb103579b04025170a"
uuid = "54e16d92-306c-5ea0-a30b-337be88ac337"
version = "0.4.2"
version = "0.4.3"
[[deps.PrettyTables]]
deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"]
git-tree-sha1 = "ebf455bb866ee6737030e3d3816bb6a0683c4325"
git-tree-sha1 = "4ac881f5432bd93463a41767a814a45245be22b6"
uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
version = "3.4.0"
version = "3.4.6"
[deps.PrettyTables.extensions]
PrettyTablesExcelExt = "XLSX"
@@ -757,15 +745,15 @@ version = "1.3.1"
[[deps.Reseau]]
deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"]
git-tree-sha1 = "0eab6d95ed40c2ef3992255c1c71e4f9748932b5"
git-tree-sha1 = "701ef63506992668c0069a4d1ac1540b77258e6c"
uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
version = "1.3.1"
version = "1.3.6"
[[deps.Revise]]
deps = ["CRC32c", "CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"]
git-tree-sha1 = "27e3ee13fc8739a59b380d6163d6a82f52c03bd7"
deps = ["CRC32c", "CodeTracking", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"]
git-tree-sha1 = "ec46aed6a3a8cc6b67839ca361e7b4aa32eaeee1"
uuid = "295af30f-e4ad-537b-8983-00126c2a3abe"
version = "3.15.1"
version = "3.16.3"
weakdeps = ["Distributed"]
[deps.Revise.extensions]
@@ -779,15 +767,15 @@ version = "0.9.0"
[[deps.Rmath_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "58cdd8fb2201a6267e1db87ff148dd6c1dbd8ad8"
git-tree-sha1 = "6d40b2fe70437b01397d2a4d5b020008da4e7019"
uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f"
version = "0.5.1+0"
version = "0.5.2+0"
[[deps.Roots]]
deps = ["Accessors", "CommonSolve", "Printf"]
git-tree-sha1 = "a7caaf7ba8cf307112ca443784d1b56b4a591455"
git-tree-sha1 = "7fb25a964849d90a0446366cdefca822e0e84900"
uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
version = "3.0.5"
version = "3.0.6"
[deps.Roots.extensions]
RootsChainRulesCoreExt = "ChainRulesCore"
@@ -840,12 +828,6 @@ git-tree-sha1 = "084c47c7c5ce5cfecefa0a98dff69eb3646b5a80"
uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c"
version = "1.4.10"
[[deps.Serde]]
deps = ["CSV", "Dates", "EzXML", "JSON", "TOML", "UUIDs", "YAML"]
git-tree-sha1 = "f397fc8779cc53e4677c2708f3802c6996f28d00"
uuid = "db9b398d-9517-45f8-9a95-92af99003e0e"
version = "3.7.2"
[[deps.Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
version = "1.11.0"
@@ -879,9 +861,9 @@ version = "1.12.0"
[[deps.SpecialFunctions]]
deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
git-tree-sha1 = "6547cbdd8ce32efba0d21c5a40fa96d1a3548f9f"
git-tree-sha1 = "c3ac026e735264e9bdc6a9bcbd1b1e781b36e3bc"
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
version = "2.8.0"
version = "2.8.3"
[deps.SpecialFunctions.extensions]
SpecialFunctionsChainRulesCoreExt = "ChainRulesCore"
@@ -932,9 +914,9 @@ version = "0.34.12"
[[deps.StatsFuns]]
deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"]
git-tree-sha1 = "770240df9a3b8888065046948f7a09b4e0f997d5"
git-tree-sha1 = "91a5737baed20ee31f3faea0e51f57461f6a689e"
uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c"
version = "2.2.0"
version = "2.2.1"
[deps.StatsFuns.extensions]
StatsFunsChainRulesCoreExt = "ChainRulesCore"
@@ -950,17 +932,11 @@ git-tree-sha1 = "cd83a04baf746e3b43b83c61b7de77ab0409b80a"
uuid = "88034a9c-02f8-509d-84a9-84ec65e18404"
version = "1.0.0"
[[deps.StringEncodings]]
deps = ["Libiconv_jll"]
git-tree-sha1 = "b765e46ba27ecf6b44faf70df40c57aa3a547dcb"
uuid = "69024149-9ee7-55f6-a4c4-859efe599b68"
version = "0.3.7"
[[deps.StringManipulation]]
deps = ["PrecompileTools"]
git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5"
git-tree-sha1 = "8a90c1d77c3277a5d43b83927b3cbe2c70a37484"
uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
version = "0.4.4"
version = "0.4.7"
[[deps.StructTypes]]
deps = ["Dates", "UUIDs"]
@@ -970,9 +946,9 @@ version = "1.11.0"
[[deps.StructUtils]]
deps = ["Dates", "UUIDs"]
git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8"
git-tree-sha1 = "c65ae4aa47e543c278aea0a3468786d33021a3ff"
uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42"
version = "2.8.2"
version = "2.8.4"
[deps.StructUtils.extensions]
StructUtilsMeasurementsExt = ["Measurements"]
@@ -1046,9 +1022,9 @@ uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
version = "0.11.3"
[[deps.URIs]]
git-tree-sha1 = "bef26fb046d031353ef97a82e3fdb6afe7f21b1a"
git-tree-sha1 = "3b0738bd7c5645641845da25cbd99800b8718689"
uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
version = "1.6.1"
version = "1.6.2"
[[deps.UTCDateTimes]]
deps = ["Dates", "TimeZones"]
@@ -1076,23 +1052,11 @@ git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7"
uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60"
version = "1.6.1"
[[deps.XML2_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"]
git-tree-sha1 = "3f3315d89fc954a28f5b471bce698ed6e27481be"
uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a"
version = "2.15.3+0"
[[deps.YAML]]
deps = ["Base64", "Dates", "Printf", "StringEncodings"]
git-tree-sha1 = "a1c0c7585346251353cddede21f180b96388c403"
uuid = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"
version = "0.4.16"
[[deps.YiemAgent]]
deps = ["Base64", "CSV", "DataFrames", "DataStructures", "Dates", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serde", "Serialization", "URIs", "UUIDs"]
deps = ["Base64", "CSV", "DataFrames", "DataStructures", "Dates", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs"]
path = "."
uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2"
version = "0.7.4"
version = "0.8.0"
[[deps.Zlib_jll]]
deps = ["Libdl"]
-2
View File
@@ -19,7 +19,6 @@ PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
SQLLLM = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3"
Serde = "db9b398d-9517-45f8-9a95-92af99003e0e"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
@@ -34,4 +33,3 @@ JSON = "1.6.1"
LLMMCTS = "0.1.5"
NATS = "0.1.0"
SQLLLM = "0.2.8"
Serde = "3.7.2"
+30 -19
View File
@@ -1,23 +1,34 @@
# YiemAgent
## TODO
- [WORKING] build prompt()
- [ ] build agent runLoop()
- [ ] build MCP server connector
- [ ] executeplan() to execute the plan
- [ ] add comprehensive tests
Julia framework for building agents with tool use.
## Changelog
## Getting Started
### Version 0.8.0
- Converted snake_case fields to camelCase:
- `llmModel`: `base_url``baseUrl`, `context_window``contextWindow`, `max_tokens``maxTokens`
- Converted PascalCase type references to camelCase:
- `AgentState``agentState`
- `AgentTool``agentTool`
- `AgentMessage``agentMessage`
- `PendingMessageQueue``pendingMessageQueue`
- `ActiveRun``activeRun`
- `StreamFn``streamFn`
- `ThinkingLevel``thinkingLevel`
- `ToolExecutionMode``toolExecutionMode`
1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, ...`
2. Create a `yiemAgent` with `loadTools("src/tools")`
3. Call `run_agent(agent, "message")` then `take_response(agent)`
## Architecture
```
src/
├── YiemAgent.jl # Module entry point
├── type.jl # Core types (messages, tools, agent state)
├── utils.jl # Message formatting, validation
├── agentCore.jl # Agent loop, tool execution pipeline
├── api.jl # Public API (run_agent, take_response, etc.)
└── tools/
├── registry.jl # Tool registry (loadTools, registerTool, listTools)
├── getWeather.jl # Weather lookup tool
├── getTime.jl # Time lookup tool
├── writeTool.jl # Create new tool files (self-modifying)
└── README.md # Tool development guide
```
## Tool Development
See `src/tools/README.md` for:
- Tool anatomy (schema, execute, getTool)
- Validation hooks
- Agent loop lifecycle
- Self-modifying tools (`writeTool`)
-92
View File
@@ -1,92 +0,0 @@
# Dynamic Tool Loading
Tools can be loaded dynamically from `.jl` files in the `src/tools/` directory without hardcoding filenames in the main module.
## How It Works
1. `src/tools/registry.jl` defines a `loadTools(dir::String)` function that scans a directory for `.jl` files
2. Each tool file must define a single function: `getTool()::agentTool`
3. `loadTools()` sorts files alphabetically, includes each one, calls `getTool()`, and registers the result
4. Loaded tools are returned as `Vector{agentTool}` for use when constructing a `yiemAgent`
## Directory Structure
```
src/
├── tools/
│ ├── registry.jl # Tool loader (do not edit)
│ ├── getWeather.jl # Your tool
│ └── query_db.jl # Another tool
├── type.jl
├── utils.jl
├── agentCore.jl
├── api.jl
└── YiemAgent.jl
```
## Creating a Tool
Each `.jl` file in `src/tools/` must define `getTool()` returning an `agentTool`:
```julia
# src/tools/getWeather.jl
function getTool()::agentTool
return agentTool(
name = "getWeather",
label = "Weather Lookup",
description = "Fetch current weather and forecast for a given city.",
inputSchema = Dict{String,Any}(
"type" => "object",
"properties" => Dict(
"city" => Dict("type" => "string", "description" => "City and country"),
"units" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"], "default" => "celsius")
),
"required" => ["city"]
),
execute = (toolCallId, args, signal, onPartialResult) -> begin
city = args["city"]
return agentToolResult(
[textContent("Weather in $(city): Sunny, 22C")],
Dict{Any,Any}(), nothing, false
)
end,
prepareArguments = nothing,
parallelToolExecute = false
)
end
```
No `module` wrapper needed — the registry includes each file in the current module scope so all types (`agentTool`, `textContent`, `agentToolResult`, etc.) resolve correctly.
## Loading Tools
```julia
using .YiemAgent
using .YiemAgent: toolRegistry
# Load all tool files from src/tools/
tools = YiemAgent.loadTools(joinpath(@__DIR__, "src", "tools"))
# Create agent with loaded tools
agent = yiemAgent(
systemPrompt = "You are a helpful assistant.",
model = my_model,
tools = tools,
llmCall = my_llm_call,
agentEventSink = my_event_sink
)
```
## Available Functions
| Function | Description |
|----------|-------------|
| `loadTools(dir::String)` | Scan directory and load all `.jl` tool files |
| `registerTool(tool::agentTool)` | Register a single tool into the global registry |
| `getTools()` | Get deep copy of all registered tools |
| `listTools()` | List all registered tools as `(name, label)` pairs |
| `clearTools()` | Clear the global registry |
## File Loading Order
Files are sorted alphabetically before loading, so `01_database.jl` loads before `02_weather.jl`. This ensures deterministic registration order.
+1 -1
View File
@@ -3,7 +3,7 @@ module agentCore
export _agent_loop, OpenAiToUserMessage
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames, Serde, Base.Threads
DataFrames, Base.Threads
using GeneralUtils
using ..type, ..utils
+1 -1
View File
@@ -3,7 +3,7 @@ module api
export prompt
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames, Serde
DataFrames
using GeneralUtils
using ..type, ..utils
+300 -343
View File
@@ -2,43 +2,6 @@
Tools allow the agent to perform actions and fetch data. Each tool defines a **schema** (what arguments it accepts) and an **execution function** (what it does).
## Quick Start
Add a new tool by creating a `.jl` file in `src/tools/`. The file must define a `getTool()` function that returns an `agentTool`:
```julia
# src/tools/my_tool.jl
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
city = args["city"]
return agentToolResult(
[textContent("Hello from $(city)!")],
Dict{Any,Any}(), nothing, false
)
end
function getTool()::agentTool
return agentTool(
name = "my_tool",
label = "My Tool",
description = "Says hello to a city.",
inputSchema = Dict{String,Any}(
"type" => "object",
"properties" => Dict(
"city" => Dict("type" => "string", "description" => "City name")
),
"required" => ["city"]
),
execute = executeTool,
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = false
)
end
```
When `loadTools()` or `registerTool()` is called, the tool becomes available to the agent.
## Tool Anatomy
Each tool has 3 main parts:
@@ -155,220 +118,177 @@ end
Return `nothing` to pass, or an error `String` to fail. The error is fed back to the LLM so it can retry with corrected arguments.
## Tool Lifecycle — Framework Internals
## Tool Discovery and Lifecycle
This section traces the full code path from the moment the LLM returns tool calls to the final result being fed back into the conversation. All code references are to `agentCore.jl`.
The agent iterates through tools via a **discover → execute → loop** cycle. Here is the complete flow from the framework author's perspective:
### Phase 1: Detect Tool Calls in LLM Response
After the LLM returns an `assistantMessage`, the loop at `agentCore.jl:220-244` inspects each `content` block:
### The Agent Loop
```julia
# agentCore.jl:217-244
has_tool_calls = false
# agentCore.jl:175 - _process_message()
while true
# 1. Drain messages from inputChannel
while isready(agent.inputChannel)
raw_msg = take!(agent.inputChannel)
user_msg = OpenAiToUserMessage(raw_msg)
push!(agent._state.messages, user_msg)
end
# 2. Format messages for LLM
ctx = agent.prepareContext(agent._state)
formatted = agent.formatMsgForLLM(ctx)
# 3. Call LLM
response = agent.llmCall(formatted)
# 4. Check if LLM used tool calls
if has_tool_calls(response.content)
# 5. Execute tools, feed results back to LLM, loop
else
# 6. No tool calls — return final response
break
end
end
```
### Step 1: Tool Discovery
Tools are discovered from `agent._state.tools`, which is a `Vector{agentTool}` populated during agent creation:
```julia
# Loading tools
tools = loadTools("src/tools") # returns Vector{agentTool}
# Passing to agent
agent = yiemAgent(
systemPrompt = "...",
tools = tools, # ← tools stored in agent._state.tools
llmCall = my_llm_call,
agentEventSink = my_event_sink,
)
```
When the LLM response contains tool calls, the agent builds an `agentContext` with those tools:
```julia
context = agentContext(
agent._state.systemPrompt,
agent._state.messages,
agent._state.tools, # ← tools available for discovery
)
```
### Step 2: Extract Tool Calls from LLM Response
The agent inspects the `response.content` blocks for `tool_calls`:
```julia
# agentCore.jl:217-245
tool_call_list = agentToolCall[]
for content_block in response.content
if content_block isa Dict
# OpenAI-style: type == "tool_calls" with array of tool calls
if get(content_block, :type, "") == "tool_calls"
for tc_data in get(content_block, :tool_calls, [])
tc = agentToolCall(
type="function",
id=get(tc_data, :id, string(uuid4())),
name=get(tc_data, :function, Dict{String,Any}())[:name],
arguments=get(tc_data, :function, Dict{String,Any}())[:arguments],
)
push!(tool_call_list, tc)
end
# Alternative style: type == "tool_call" single dict per block
elseif get(content_block, :type, "") == "tool_call"
tc = agentToolCall(
type="function",
id=get(tc_data, :id, string(uuid4())),
name=get(tc_data, :name, ""),
arguments=get(tc_data, :arguments, Dict{String,Any}()),
)
push!(tool_call_list, tc)
end
end
end
```
Each content block with `type == "tool_calls"` or `type == "tool_call"` extracts an `agentToolCall` (id, name, arguments dict) and collects them into a `Vector{agentToolCall}`.
### Phase 2: Dispatch to Sequential or Parallel Execution
At `agentCore.jl:247`, the framework checks if any tool calls exist and decides execution mode:
```julia
# agentCore.jl:247-265
context = agentContext(agent._state.systemPrompt, agent._state.messages, agent._state.tools)
config = agentLoopConfig(
agent._state.tools,
agent.beforeToolCall,
agent.afterToolCall,
agent.parallelToolExecute ? "parallel" : "sequential",
)
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
```
`executeToolCalls` (`agentCore.jl:988-1015`) checks:
- `config.toolExecution == "sequential"` → sequential mode
- Any tool has `parallelToolExecute == false` → sequential mode
- Otherwise → parallel mode
### Phase 3: Per-Call Preparation (`prepareToolCall`)
Each tool call goes through `prepareToolCall` (`agentCore.jl:511-547`):
```
1. Look up tool by name: find(t -> t.name == tc.name, context.tools)
2. If not found → immediateOutcome("Tool X not found", true)
3. Run tool.prepareArguments (if defined) → transforms raw LLM args
4. Run validateToolArguments → validateRequiredArgs (hook or default)
→ if fails → throws ArgumentError → caught below
5. Run beforeToolCall hook (if defined) → can block execution
→ if blocked → immediateOutcome("Tool execution was blocked", true)
6. Return preparedToolCall(tool, tc, validatedArgs)
```
If any step throws (validation, prepareArguments, beforeToolCall), the catch block at `agentCore.jl:545` converts it to an `immediateOutcome`:
```julia
catch err
return immediateOutcome(createErrorToolResult(sprint(showerror, err)), true)
end
```
### Phase 4: Execution (`executePreparedToolCall`)
For each `preparedToolCall`, `executePreparedToolCall` (`agentCore.jl:589-617`) runs:
```julia
function executePreparedToolCall(prep::preparedToolCall, signal, emit)::executedOutcome
updateEvents = promise[]
accepting = true
try
result = prep.tool.execute(
prep.toolCall.id, prep.args, signal,
partialResult -> begin
if accepting
push!(updateEvents, emit(toolExecUpdateEvent(..., partialResult)))
if content_block isa Dict
if get(content_block, :type, "") == "tool_calls"
# OpenAI format: {"type": "tool_calls", "tool_calls": [...]}
for tc_data in get(content_block, :tool_calls, [])
tc = agentToolCall(
type = "function",
id = get(tc_data, :id, string(uuid4())),
name = get(tc_data, :function, Dict())[:name],
arguments = get(tc_data, :function, Dict())[:arguments],
)
push!(tool_call_list, tc)
end
elseif get(content_block, :type, "") == "tool_call"
# Alternative format: single tool_call block
tc = agentToolCall(
type = "function",
id = get(content_block, :id, string(uuid4())),
name = get(content_block, :name, ""),
arguments = get(content_block, :arguments, Dict()),
)
push!(tool_call_list, tc)
end
end
)
accepting = false
wait.(updateEvents)
return executedOutcome(result, false)
catch err
accepting = false
wait.(updateEvents)
return executedOutcome(createErrorToolResult(sprint(showerror, err)), true)
end
end
```
Key behaviors:
- Calls `tool.execute(id, args, signal, onPartialResult)` — your tool's `executeTool` function
- `signal` can be checked inside `executeTool` for cancellation
- `onPartialResult` is called for streaming updates, which are emitted as `toolExecutionUpdate` events
- `accepting` guard prevents emitting updates after the result is already captured
- `wait.(updateEvents)` ensures all streaming updates are delivered before returning
- Execution errors are caught and returned as `executedOutcome(isError=true)` — never thrown
### Phase 5: Finalization (`finalizeExecutedToolCall`)
After execution, `finalizeExecutedToolCall` (`agentCore.jl:675-706`) runs the `afterToolCall` hook:
```julia
function finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal)::finalizedOutcome
result = executed.result
isError = executed.isError
if config.afterToolCall !== nothing
try
after = config.afterToolCall(afterCtx(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal)
if after !== nothing
# Hook can mutate: content, details, usage, terminate, isError
result = merge(result, dict(...))
isError = get(after, :isError, isError)
end
catch err
result = createErrorToolResult(sprint(showerror, err))
isError = true
end
end
return finalizedOutcome(prep.toolCall, result, isError)
end
```
The hook can:
- Mask sensitive data from result content
- Normalize usage tracking
- Flip `terminate: true` based on business logic
- Wrap errors in friendlier messages for the LLM
### Step 3: Execute Each Tool Call
If the hook itself throws, the error is caught and converted to an error outcome.
### Phase 6: Emit Events and Create Result Message
Each call emits `toolExecutionEnd`:
For each tool call, the agent runs through the **prepare → execute → finalize** pipeline:
```julia
function emitToolExecutionEnd(finalized::finalizedOutcome, emit::Function)
emit(toolExecEndEvent(finalized.toolCall.id, finalized.toolCall.name, finalized.result, finalized.isError))
# agentCore.jl:247-302
if has_tool_calls && length(tool_call_list) > 0
context = agentContext(agent._state.systemPrompt, agent._state.messages, agent._state.tools)
config = agentLoopConfig(agent._state.tools, agent.beforeToolCall, agent.afterToolCall, execution_mode)
signal = nothing
emit = agent.agentEventSink
# Execute all tool calls (sequential or parallel)
batch = executeToolCalls(context, response, tool_call_list, config, signal, emit)
# Save results to conversation history
for tool_result in batch.messages
push!(agent._state.messages, tool_result)
end
# If any tool requested termination, break the loop
if batch.terminate
final_response = build_final_response(batch)
break
end
# Otherwise, loop back to step 2 (format + call LLM again)
end
```
Then creates the `toolResultMessage` for conversation history (`agentCore.jl:373-379`):
### Step 4: The Per-Call Pipeline
```julia
function createToolResultMessage(f::finalizedOutcome)::toolResultMessage
return toolResultMessage(
"toolResult", f.toolCall.id, f.toolCall.name,
f.result.content, f.result.details, f.result.usage,
get(f.result, :addedToolNames, string[]), f.isError, nowMillis()
)
end
```
### Phase 7: Batch Assembly and Loop Control
In `executeToolCallsSequential` (`agentCore.jl:795-829`) or `executeToolCallsParallel` (`agentCore.jl:888-936`), all results are collected:
```julia
messages = toolResultMessage[]
for finalized in finalizedCalls
push!(messages, createToolResultMessage(finalized))
end
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
```
`shouldTerminate` (`agentCore.jl:409`) returns `true` only if ALL tools in the batch set `result.terminate == true`. If `false`, the agent loop at `agentCore.jl:176-308` feeds the tool results back to the LLM for another turn.
### Data Flow Summary
Each tool call goes through three phases:
```
response.content (Vector{Any})
└── phase 1: parse content blocks
└── tool_call_list :: Vector{agentToolCall}
└── phase 2: dispatch to sequential/parallel
└── phase 3: prepareToolCall
└── preparedToolCall or immediateOutcome
└── phase 4: executePreparedToolCall
└── executedOutcome
└── phase 5: finalizeExecutedToolCall
└── finalizedOutcome
└── phase 6: createToolResultMessage
└── toolResultMessage
└── phase 7: agentToolCallBatch
└── pushed to agent._state.messages
└── loop back to LLM
┌─────────────────────────────────────────────────────────────────┐
│ PREPARE → prepareToolCall() │
│ │
│ 1. Find tool by name in context.tools │
│ 2. Transform args via tool.prepareArguments (if defined) │
│ 3. Validate via tool.validateRequiredArgs (or default) │
│ 4. Run beforeToolCall hook (if defined) │
│ └── on any failure → return immediateOutcome (skip execution) │
│ └── success → return preparedToolCall │
├─────────────────────────────────────────────────────────────────┤
│ EXECUTE → executePreparedToolCall() │
│ │
│ 1. emit toolExecutionStart event │
│ 2. call tool.execute(toolCallId, args, signal, onPartialResult)│
│ 3. wait for all pending update events │
│ └── on error → return executedOutcome(isError=true) │
│ └── success → return executedOutcome(isError=false) │
├─────────────────────────────────────────────────────────────────┤
│ FINALIZE → finalizeExecutedToolCall() │
│ │
│ 1. Run afterToolCall hook (if defined) │
│ - can mutate content, details, usage, terminate, isError │
│ 2. emit toolExecutionEnd event │
│ 3. createToolResultMessage → adds to conversation history │
│ └── return finalizedOutcome │
└─────────────────────────────────────────────────────────────────┘
```
### Step 5: Feed Results Back to LLM
Tool results are added to `agent._state.messages` as `toolResultMessage` objects. On the next loop iteration, `formatMsgForLLM()` converts them to OpenAI format and the LLM receives the results:
```
Conversation history after tool execution:
[system] "You are a helpful assistant."
[user] "What's the weather in Tokyo?"
[assistant] (tool_calls: getWeather(city="Tokyo"))
[tool] tool_call_id="call_1", tool_name="getWeather", content="Weather in Tokyo: Sunny, 22°C"
```
The LLM then decides: call another tool, or return a final text answer.
## Execution Modes
### Sequential
@@ -424,124 +344,175 @@ tool = getTool() # from your tool module
registerTool(tool)
```
## Using Tools with an Agent
Loading tools only registers them — you must pass them to the `yiemAgent` and provide an `llmCall` function. Here is the complete flow:
## Complete Lifecycle Example
```julia
using .YiemAgent
using .toolRegistry
# 1. Load tools from the tools directory
tools = loadTools("src/tools")
# [toolRegistry] Loading tool from: src/tools/getTime.jl
# [toolRegistry] Loaded tool: getTime — Time Lookup
# [toolRegistry] Loading tool from: src/tools/getWeather.jl
# [toolRegistry] Loaded tool: getWeather — Weather Lookup
# 2. Define your LLM call function
function my_llm_call(messages::Dict)::assistantMessage
# Call your LLM API here (OpenAI, Anthropic, local model, etc.)
# Return an assistantMessage with the response content
# If the LLM wants to call a tool, include tool_call content blocks
...
end
# 3. Define your event sink (optional, for logging/debugging)
function my_event_sink(event)
if event isa toolExecStartEvent
println("[EVENT] Tool start: $(event.toolName)")
elseif event isa toolExecEndEvent
status = event.isError ? "ERROR" : "OK"
println("[EVENT] Tool end: $(event.toolName)$status")
end
end
# 4. Create the agent with tools
agent = yiemAgent(
systemPrompt = "You are a helpful assistant that can check weather and time.",
model = my_model,
tools = tools, # pass loaded tools
llmCall = my_llm_call, # your LLM function
agentEventSink = my_event_sink, # event handler
)
# 5. Send a message and get a response
# ─── USER SENDS MESSAGE ───────────────────────────────────────────
run_agent(agent, "What's the weather in Tokyo?")
# ─── LOOP ITERATION 1 ─────────────────────────────────────────────
# Agent formats messages and calls LLM
formatted = agent.formatMsgForLLM(agent.prepareContext(agent._state))
response = agent.llmCall(formatted)
# LLM returns: {"content": [{"type": "tool_calls", "tool_calls": [{"name": "getWeather", "arguments": {"city": "Tokyo"}}]}]}
# Agent extracts tool call, builds context
context = agentContext(systemPrompt, messages, agent._state.tools)
tool_call_list = [agentToolCall("call_1", "getWeather", Dict("city" => "Tokyo"))]
# PREPARE: find tool, validate args
tool = find(t -> t.name == "getWeather", context.tools) # found!
validateRequiredArgs(Dict("city" => "Tokyo"), tool.inputSchema) # passes
beforeToolCall_hook(agentMsgCtx, nothing) # nil, skipped
# EXECUTE: call tool.execute()
result = tool.execute("call_1", Dict("city" => "Tokyo"), nothing, onPartialResult)
# Returns: agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], Dict(), nothing, false)
# FINALIZE: afterToolCall hook, emit events
finalized = finalizedOutcome(tc, result, false)
emit(toolExecEndEvent("call_1", "getWeather", result, false))
msg = createToolResultMessage(finalized) # toolResultMessage for conversation history
# Add result to conversation
push!(agent._state.messages, msg)
# Messages now: [user: "What's the weather?", assistant: {tool_calls: getWeather}, tool: "Sunny, 22°C"]
# ─── LOOP ITERATION 2 ─────────────────────────────────────────────
# LLM called again with tool result included
formatted = agent.formatMsgForLLM(agent.prepareContext(agent._state))
response = agent.llmCall(formatted)
# LLM returns: {"content": [{"type": "text", "text": "The weather in Tokyo is sunny, 22°C."}]}
# No tool calls detected → break loop, return final response
return assistantMessage(content=[textContent("The weather in Tokyo is sunny, 22°C.")], ...)
# ─── USER RECEIVES RESPONSE ───────────────────────────────────────
response = take_response(agent)
# response.content contains the LLM's reply (with tool results if applicable)
println(response.content)
# 6. When done, stop the agent
stop_agent(agent)
# => "[textContent(\"The weather in Tokyo is sunny, 22°C.\")]"
```
### How It Works
## Self-Modifying Tools
1. **User sends a message** via `run_agent(agent, "What's the weather in Tokyo?")`. The message goes into `inputChannel`.
The framework includes tools that allow the agent to create new tools at runtime.
2. **Agent loop** (`_agent_loop`) picks it up, converts it to a `userMessage`, and adds it to `agent._state.messages`.
### `writeTool` — Create New Tool Files
3. **LLM is called** via `agent.llmCall(formatted_messages)`. The LLM sees the system prompt, conversation history, and the tool definitions in the prompt (via `formatMsgForLLM`).
`writeTool` is a **file writer**, not a code generator. The LLM provides the tool logic as `executeCode` (the actual Julia code), and `writeTool` wraps it in the required boilerplate.
4. **If the LLM uses a tool**, it returns a response with `tool_call` content blocks. The agent:
- Extracts each tool call (name, arguments)
- Runs validation (`validateRequiredArgs` or default)
- Executes the tool (or returns an error if validation fails)
- Feeds the result back as a `toolResultMessage` in the conversation
**How it works:**
5. **LLM is called again** with the tool results. This repeats until the LLM returns a text response with no tool calls.
The LLM constructs `writeTool` with:
- **`executeCode`** — the actual tool logic (Julia code body, NOT wrapped in a function)
- **`name`, `label`, `description`** — tool metadata
- **`inputSchema`** — parameter schema in MCP format
- **`validateCode`, `prepareCode`** (optional) — custom validation/preparation logic
6. **Final response** is sent to `outputChannel` — retrieve it with `take_response(agent)`.
`writeTool` produces `src/tools/<name>.jl` by:
1. Converting the `inputSchema` Dict into a Julia `Dict{String,Any}(...)` string literal
2. Indenting `executeCode` with 4 spaces
3. Wrapping it inside a `function executeTool(...)::agentToolResult ... end` template
4. Appending the `getTool()` definition that returns an `agentTool` struct
5. Writing the combined string to disk
### Minimal Working Example
**Workflow:**
```
LLM decides: "Need a searchWine tool. I'll provide the logic."
LLM calls writeTool:
name: "searchWine"
executeCode: "query = args[\"query\"]\nresult = search(query)\nreturn ..."
writeTool wraps it → src/tools/searchWine.jl:
function executeTool(...)::agentToolResult
query = args["query"] ← LLM code (indented 4 spaces)
result = search(query)
return agentToolResult(...)
end
function getTool()::agentTool
return agentTool(name="searchWine", ...)
end
Restart → loadTools("src/tools") loads searchWine.jl
```
**Example specification:**
```julia
using .YiemAgent
using .toolRegistry
# Load tools
tools = loadTools("src/tools")
# Mock LLM that echoes back a tool call, then a text response
call_count = 0
function mock_llm_call(messages::Dict)::assistantMessage
global call_count += 1
if call_count == 1
# First call: LLM decides to use getWeather
return assistantMessage(
content=[
Dict("type" => "tool_calls",
"tool_calls" => [Dict("id" => "call_1", "name" => "getWeather",
"arguments" => Dict("city" => "Tokyo"))])
],
model = "mock",
usage = llmUsage(0, 0)
)
else
# Second call: LLM returns text (after tool result)
return assistantMessage(
content = [textContent("The weather in Tokyo is sunny, 22°C.")],
model = "mock",
usage = llmUsage(0, 0)
)
end
end
# Create agent
agent = yiemAgent(
systemPrompt = "You are a helpful assistant.",
tools = tools,
llmCall = mock_llm_call,
agentEventSink = e -> nothing, # no events
Dict(
"name" => "searchWine",
"label" => "Wine Search",
"description" => "Search a wine database by name, region, or variety",
"inputSchema" => Dict(
"type" => "object",
"properties" => Dict(
"query" => Dict("type" => "string", "description" => "Search query"),
"maxResults" => Dict("type" => "integer", "default" => 10)
),
"required" => ["query"]
),
"executeCode" => """
query = args["query"]
max_results = get(args, "maxResults", 10)
# Perform search logic here
result = "Found 3 wines matching: $query"
return agentToolResult([textContent(result)], Dict{Any,Any}(), nothing, false)
""",
"parallel" => false
)
```
# Run
run_agent(agent, "What's the weather in Tokyo?")
response = take_response(agent)
**Optional hooks:**
stop_agent(agent)
| Field | Description |
|---|---|
| `validateCode` | Custom validation Julia code (runs before execute). Return `nothing` to pass, or an error `String` to fail. |
| `prepareCode` | Argument preparation code (runs before validation). Return modified args dict. |
### `listTools` — Discover Available Tools
Returns all registered tools. Primarily useful for **collision detection** before creating a new tool via `writeTool` — the LLM checks existing names before picking a unique one.
```julia
# Result from listTools:
# Available tools:
# - getWeather: Weather Lookup — Fetch current weather and forecast for a given city.
# - getTime: Time Lookup — Get current local time for a timezone or city.
# - writeTool: Create Tool — Generate new tool files...
# - listTools: List Tools — List all available tools with their names and labels...
```
### Complete Self-Tooling Example
```
User: "I need to search for wines. Do you have a tool for that?"
# ─── LOOP: Agent realizes no wine search tool exists ─────────────────
# LLM generates the tool logic and calls writeTool to write it to disk
[Tool Call] writeTool(name="searchWine", label="Wine Search",
description="Search a wine database by name, region, or variety",
inputSchema={...},
executeCode="query = args[\"query\"]\nresult = \"Found wines...\"\nreturn agentToolResult([textContent(result)], ...)")
# writeTool generates src/tools/searchWine.jl
# ─── SYSTEM RESTARTS ─────────────────────────────────────────────────
# loadTools("src/tools") loads searchWine.jl alongside all other tools
# ─── Agent calls the new tool ─────────────────────────────────────────
[Tool Call] searchWine(query="cabernet", maxResults=5)
# Result: "Found 5 cabernet wines..."
# ─── Final response ──────────────────────────────────────────────────
"The search found 5 cabernet wines: ..."
```
## Available Tools
@@ -550,19 +521,5 @@ stop_agent(agent)
|---|---|---|
| `getWeather` | Fetch weather for a city | Default (JSON Schema required) |
| `getTime` | Get current time for a timezone or city | Custom (cross-field + format) |
## Example: Error Flow
When the LLM calls a tool with invalid arguments:
```
User: "What's the weather?"
└── LLM: call getWeather() with no arguments
└── prepareToolCall → validateRequiredArgs → "Missing required arguments: city"
└── immediateOutcome → error tool result
└── LLM sees: "Missing required arguments: city"
└── LLM retries: call getWeather(city="Tokyo")
└── executeTool → "Weather in Tokyo: Sunny, 22°C"
```
The agent feeds the error back to the LLM as a tool result message, allowing it to self-correct.
| `writeTool` | Create a new Julia tool module at runtime | Built-in (name + schema validation) |
| `listTools` | List all available tools with descriptions | None (no arguments) |
+15 -32
View File
@@ -36,35 +36,6 @@ function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}
return nothing
end
"""
Execute the getTime tool.
# Arguments
- `toolCallId::String`: Unique identifier for this tool call
- `args::Dict{String,Any}`: Parsed arguments from the LLM
- `signal::Union{Nothing,abortSignal}`: Optional abort signal
- `onPartialResult::Function`: Callback for streaming partial results
# Returns
- `agentToolResult`: Result content with current time data
"""
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
tz = get(args, "timezone", nothing)
city = get(args, "city", "")
# Simulate time lookup — replace with actual timezone API call
if tz !== nothing
result = "Current time in $(tz): $(now())"
else
result = "Current time in $(city): $(now())"
end
return agentToolResult(
[textContent(result)],
Dict{Any,Any}(), nothing, false
)
end
"""
Define and return the getTime agentTool.
"""
@@ -76,12 +47,24 @@ function getTool()::agentTool
inputSchema = Dict{String,Any}(
"type" => "object",
"properties" => Dict(
"timezone" => Dict("type" => "string", "description", "IANA timezone, e.g. 'America/New_York'"),
"city" => Dict("type" => "string", "description", "City name as fallback")
"timezone" => Dict("type" => "string", "description" => "IANA timezone, e.g. 'America/New_York'"),
"city" => Dict("type" => "string", "description" => "City name as fallback")
),
"required" => []
),
execute = executeTool,
execute = (toolCallId, args, signal, onPartialResult) -> begin
tz = get(args, "timezone", nothing)
city = get(args, "city", "")
if tz !== nothing
result = "Current time in $(tz): $(now())"
else
result = "Current time in $(city): $(now())"
end
return agentToolResult(
[textContent(result)],
Dict{Any,Any}(), nothing, false
)
end,
prepareArguments = nothing,
validateRequiredArgs = validateRequiredArgs,
parallelToolExecute = false
+10 -31
View File
@@ -1,33 +1,3 @@
"""
Execute the getWeather tool.
# Arguments
- `toolCallId::String`: Unique identifier for this tool call
- `args::Dict{String,Any}`: Parsed arguments from the LLM
- `signal::Union{Nothing,abortSignal}`: Optional abort signal
- `onPartialResult::Function`: Callback for streaming partial results
# Returns
- `agentToolResult`: Result content with weather data
"""
function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult
city = get(args, "city", "")
units = get(args, "units", "celsius")
# Simulate weather fetch — replace with actual API call
# You can call onPartialResult() here for streaming progress updates:
# onPartialResult(Dict("status" => "Fetching weather data..."))
# onPartialResult(Dict("status" => "Processing..."))
temp = units == "fahrenheit" ? "72" : "22"
unit_symbol = units == "celsius" ? "°C" : "°F"
return agentToolResult(
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
Dict{Any,Any}(), nothing, false
)
end
"""
Define and return the getWeather agentTool.
"""
@@ -44,7 +14,16 @@ function getTool()::agentTool
),
"required" => ["city"]
),
execute = executeTool, # reference the function defined above
execute = (toolCallId, args, signal, onPartialResult) -> begin
city = get(args, "city", "")
units = get(args, "units", "celsius")
temp = units == "fahrenheit" ? "72" : "22"
unit_symbol = units == "celsius" ? "°C" : "°F"
return agentToolResult(
[textContent("Weather in $(city): Sunny, $(temp)$(unit_symbol)")],
Dict{Any,Any}(), nothing, false
)
end,
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = false
+46 -14
View File
@@ -1,12 +1,53 @@
module toolRegistry
export loadTools, registerTool, getTools, listTools, clearTools
export loadTools, registerTool, getTools, clearTools
using Dates
using JSON
using ..type
# Global registry — populated at runtime by loadTools() or registerTool()
const _registry = Vector{agentTool}()
# Auto-register the built-in listTools tool
function __init__()
registerTool(_listTool())
end
"""
List tool definition — lets the agent query available tools for collision detection
when creating new tools via writeTool.
"""
function _listTool()::agentTool
return agentTool(
name = "listTools",
label = "List Tools",
description = "List all available tools with their names, labels, and descriptions. Use this before creating a new tool to check for name collisions.",
inputSchema = Dict{String,Any}(
"type" => "object",
"properties" => Dict{String,Any}(),
"required" => Any[]
),
execute = (toolCallId, args, signal, onPartialResult) -> begin
tools = getTools()
if isempty(tools)
result_text = "No tools registered."
else
lines = String["- $(t.name): $(t.label)$(t.description)" for t in tools]
result_text = "Available tools:\n" * join(lines, "\n")
end
return agentToolResult(
[textContent(result_text)],
Dict{Any,Any}("count" => length(tools)),
nothing, false
)
end,
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = false
)
end
"""
Load all tool modules from a directory.
@@ -35,7 +76,7 @@ function getTool()::agentTool
execute = (toolCallId, args, signal, onPartialResult) -> begin
city = args["city"]
return agentToolResult(
[textContent("Sunny, 22C in $(city)")],
[textContent("Sunny, 22C in Bangkok")],
Dict{Any,Any}(), nothing, false
)
end,
@@ -60,7 +101,7 @@ function loadTools(dir::String)::Vector{agentTool}
end
tools = agentTool[]
jl_files = filter(f -> endswith(f, ".jl"), readdir(dir))
jl_files = filter(f -> endswith(f, ".jl") && !occursin(r"(?i)registry", f), readdir(dir))
sort!(jl_files)
for filename in jl_files
@@ -80,7 +121,8 @@ function loadTools(dir::String)::Vector{agentTool}
end
# Call getTool() — it runs in current scope where types are visible
tool = getTool()
# Use invokelatest to handle world-age semantics after include()
tool = invokelatest(getTool)
if !(tool isa agentTool)
throw(ArgumentError(
"getTool() in $(filepath) did not return an agentTool instance, got: $(typeof(tool))"
@@ -120,16 +162,6 @@ function getTools()::Vector{agentTool}
return deepcopy(_registry)
end
"""
List all registered tool names and labels.
# Returns
- `Vector{Tuple{String,String}}`: Pairs of (name, label)
"""
function listTools()::Vector{Tuple{String,String}}
return [(t.name, t.label) for t in _registry]
end
"""
Clear all registered tools from the global registry.
"""
+271
View File
@@ -0,0 +1,271 @@
"""
Tool that writes new Julia tool module files to disk.
The agent can use this tool when it encounters a task that no existing tool
can handle. Provide the tool's name, label, description, inputSchema, and
execute logic as Julia code. The tool is written to `src/tools/<name>.jl`.
After calling this tool, restart the agent so `loadTools("src/tools")` picks
up the new file. The new tool is immediately available.
# Example
1. Agent calls writeTool with a spec for a "searchWine" tool
2. writeTool generates src/tools/searchWine.jl
3. Restart agent — loadTools() picks up the new file
4. Agent calls searchWine with args
# How It Works
writeTool is a **file writer**, not a code generator. The LLM provides the
tool logic as `executeCode`, and writeTool wraps it in Julia boilerplate:
- Converts `inputSchema` Dict into Julia `Dict{String,Any}(...)` string
- Indents `executeCode` with 4 spaces
- Wraps it inside `function executeTool(...)::agentToolResult ... end`
- Appends `getTool()` returning an `agentTool` struct
- Writes the combined string to `src/tools/<name>.jl`
# Important Notes
- The `executeCode` string is embedded literally into the generated tool.
Use `args["param_name"]` to access input parameters.
- The code string should be the function body (NOT wrapped in a function).
Lines will be indented with 4 spaces inside the execute function.
- Tool names must be valid Julia identifiers (lowercase letters, digits, underscores,
no leading digits or special characters).
"""
"""
Validate that a tool name is a valid Julia identifier.
"""
function validateToolName(name::String)::Union{Nothing,String}
if !occursin(r"^[a-zA-Z_][a-zA-Z0-9_!]*$", name)
return "Invalid tool name: '$name'. Tool names must be valid Julia identifiers (letters, digits, underscores, starting with a letter or underscore)."
end
return nothing
end
"""
Indent a multi-line code string by the specified number of spaces.
"""
function indent_code(code::String, n::Int)::String
prefix = " "^n
lines = split(code, '\n')
result_lines = String[prefix * line for line in lines]
return join(result_lines, "\n")
end
"""
Convert a Julia Dict to a valid Julia Dict{String,Any}(...) literal string.
"""
function dict_to_julia_literal(d)::String
if d isa Dict
items = String[]
for (k, v) in d
key_str = json_string(k)
val_str = value_to_julia(v)
push!(items, "$key_str => $val_str")
end
return "Dict{String,Any}(" * join(items, ", ") * ")"
else
return value_to_julia(d)
end
end
function value_to_julia(v)::String
if v isa Dict
return dict_to_julia_literal(v)
elseif v isa Vector
items = [value_to_julia(x) for x in v]
return "[" * join(items, ", ") * "]"
elseif v isa String
escaped = replace(v, "\\" => "\\\\")
escaped = replace(escaped, "\"" => "\\\"")
return "\"$escaped\""
elseif v isa Number
return string(v)
elseif v isa Bool
return string(v)
elseif v === nothing
return "nothing"
else
return "\"$(v)\""
end
end
"""
Convert any Julia value to a JSON string.
"""
function json_string(v)::String
return JSON.json(v)
end
"""
Define and return the writeTool agentTool.
"""
function getTool()::agentTool
return agentTool(
name = "writeTool",
label = "Create Tool",
description = "Write a new Julia tool module file to src/tools/<name>.jl. The LLM provides the tool logic as executeCode; writeTool wraps it in Julia boilerplate and writes the file. Restart the agent to load the new tool.",
inputSchema = Dict{String,Any}(
"type" => "object",
"properties" => Dict(
"name" => Dict("type" => "string", "description" => "Unique tool name (valid Julia identifier, no spaces or special chars)"),
"label" => Dict("type" => "string", "description" => "Human-readable tool name shown in tool descriptions"),
"description" => Dict("type" => "string", "description" => "What the tool does (shown to LLM for tool selection decisions)"),
"inputSchema" => Dict(
"type" => "object",
"description" => "JSON Schema describing tool parameters in MCP format"
),
"executeCode" => Dict("type" => "string", "description" => "Julia code for the execute function body. Use args[\"key\"] to access parameters. Do NOT wrap in a function definition."),
"validateCode" => Dict("type" => "string", "optional" => true, "description" => "Optional custom validation Julia code (runs before execute). Use args[\"key\"] to access parameters. Return nothing to pass, or a string error message to fail."),
"prepareCode" => Dict("type" => "string", "optional" => true, "description" => "Optional argument preparation code (runs before validation). Return modified args dict."),
"parallel" => Dict("type" => "boolean", "default" => false, "description" => "Whether this tool can run in parallel with other tools")
),
"required" => ["name", "label", "description", "inputSchema", "executeCode"]
),
execute = (toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function) -> begin
tool_name = get(args, "name", "")::String
tool_label = get(args, "label", tool_name)::String
tool_description = get(args, "description", "")::String
tool_schema = get(args, "inputSchema", Dict{String,Any}())::Dict{String,Any}
execute_code = get(args, "executeCode", "")::String
validate_code = get(args, "validateCode", nothing)::Union{String,Nothing}
prepare_code = get(args, "prepareCode", nothing)::Union{String,Nothing}
parallel = get(args, "parallel", false)::Bool
# Validate tool name
name_err = validateToolName(tool_name)
if name_err !== nothing
return agentToolResult(
[textContent(name_err)],
Dict{Any,Any}(), nothing, false
)
end
# Validate required fields
if isempty(tool_name)
return agentToolResult(
[textContent("Missing required field: 'name'")],
Dict{Any,Any}(), nothing, false
)
end
if isempty(tool_description)
return agentToolResult(
[textContent("Missing required field: 'description'")],
Dict{Any,Any}(), nothing, false
)
end
if isempty(execute_code)
return agentToolResult(
[textContent("Missing required field: 'executeCode'")],
Dict{Any,Any}(), nothing, false
)
end
onPartialResult(Dict("status" => "Generating tool: $tool_name"))
# Build the tool file path
script_dir = dirname(@__FILE__)
tools_dir = dirname(script_dir)
filepath = joinpath(tools_dir, "$(tool_name).jl")
# Check for naming conflicts
if isfile(filepath)
return agentToolResult(
[textContent("Tool file already exists: $filepath. Rename the tool or delete the existing file first.")],
Dict{Any,Any}(), nothing, false
)
end
onPartialResult(Dict("status" => "Writing file: $(basename(filepath))"))
# Convert schema Dict to a Julia Dict literal string
schema_literal = dict_to_julia_literal(tool_schema)
# Build optional validation function
validate_section = if validate_code !== nothing && !isempty(validate_code)
indented = indent_code(validate_code, 4)
"function validateRequiredArgs(args::Dict{String,Any})::Union{Nothing,String}\n$indented\n return nothing\nend\n"
else
""
end
# Build optional prepare function
prepare_section = if prepare_code !== nothing && !isempty(prepare_code)
indented = indent_code(prepare_code, 4)
"function prepareArguments(args::Dict{String,Any})::Dict{String,Any}\n$indented\n return args\nend\n"
else
""
end
# Indent user's execute code for embedding inside execute function body
indented_exec = indent_code(execute_code, 4)
# Escape description for Julia string literal
escaped_desc = replace(tool_description, "\\" => "\\\\")
escaped_desc = replace(escaped_desc, "\"" => "\\\"")
# Build the complete tool file content
parts = String[]
push!(parts, "# Auto-generated tool: $tool_name\n")
push!(parts, "# Generated by writeTool at $(now())\n\n")
if !isempty(validate_section)
push!(parts, validate_section)
push!(parts, "\n")
end
if !isempty(prepare_section)
push!(parts, prepare_section)
push!(parts, "\n")
end
push!(parts, "\n")
push!(parts, "# Execute function\n")
push!(parts, "function executeTool(toolCallId::String, args::Dict{String,Any}, signal::Union{Nothing,abortSignal}, onPartialResult::Function)::agentToolResult\n")
push!(parts, "$indented_exec\n")
push!(parts, "end\n\n")
push!(parts, "# Tool definition\n")
push!(parts, "function getTool()::agentTool\n")
push!(parts, " return agentTool(\n")
push!(parts, " name = \"$(tool_name)\",\n")
push!(parts, " label = \"$(tool_label)\",\n")
push!(parts, " description = \"$(escaped_desc)\",\n")
push!(parts, " inputSchema = $schema_literal,\n")
push!(parts, " execute = executeTool,\n")
if validate_code !== nothing && !isempty(validate_code)
push!(parts, " validateRequiredArgs = validateRequiredArgs,\n")
else
push!(parts, " validateRequiredArgs = nothing,\n")
end
if prepare_code !== nothing && !isempty(prepare_code)
push!(parts, " prepareArguments = prepareArguments,\n")
else
push!(parts, " prepareArguments = nothing,\n")
end
push!(parts, " parallelToolExecute = $parallel\n")
push!(parts, " )\n")
push!(parts, "end\n")
tool_code = join(parts)
# Write the file — tool is loaded on next agent restart via loadTools()
write(filepath, tool_code)
onPartialResult(Dict("status" => "Done"))
return agentToolResult(
[textContent("Tool '$(tool_name)' written to $filepath. Restart the agent so loadTools() picks it up, then call listTools to verify.")],
Dict{Any,Any}(
"file" => filepath,
"name" => tool_name,
"label" => tool_label,
"description" => tool_description,
),
nothing, false
)
end,
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = false
)
end
+13 -2
View File
@@ -9,7 +9,7 @@
# Message types
userMessage, assistantMessage, toolResultMessage,
# Tool types
agentTool, validateRequiredArgs
agentTool, validateRequiredArgs,
# Context types
agentContext, agentState, agentToolCall, prepareNextTurnContext,
# Loop & execution types
@@ -248,7 +248,7 @@ tool = agentTool(
execute=(toolCallId, args, signal, onPartialResult) -> begin
city = args["city"]
return agentToolResult(
[textContent("Sunny, 22C in $(city)")],
[textContent("Sunny, 22C in Bangkok")],
Dict{Any,Any}(), nothing, false
)
end,
@@ -269,6 +269,17 @@ struct agentTool # A tool available to the agent
parallelToolExecute::Bool # Override: run tool calls sequentially or in parallel
end
"""
Keyword constructor for agentTool — allows `agentTool(name=..., label=..., ...)`.
"""
function agentTool(; name::String, label::String, description::String, inputSchema::Any,
execute::Function, prepareArguments::Union{Function, Nothing}=nothing,
validateRequiredArgs::Union{Function, Nothing}=nothing,
parallelToolExecute::Bool=false)
return agentTool(name, label, description, inputSchema, execute,
prepareArguments, validateRequiredArgs, parallelToolExecute)
end
# ------------------------------------------------------------------------------------------------ #
# Agent context #
+2 -2
View File
@@ -113,7 +113,7 @@ function prepareContext(state::agentState)::agentContext
#TODO filter tools from state.tools based on user intend in user message and tool description
filteredTools = state.tools
#TODO add tools to current system prompt
#TODO add filtered tools to the current system prompt / modify systemPrompt here
preparedSystemPrompt = state.systemPrompt
#TODO add system prompt, adjust/modify and inject additional context into messages
@@ -375,7 +375,7 @@ function validateToolArguments(tool::agentTool, prepared::agentToolCall)::Dict{S
end
return prepared.arguments
end
end
+1 -1
View File
@@ -4,7 +4,7 @@ export addNewMessage, conversation, decisionMaker, reflector, generatechat,
generalconversation, detectWineryName, generateSituationReport
using JSON, DataStructures, Dates, UUIDs, HTTP, Random, PrettyPrinting, Serialization,
DataFrames, Serde
DataFrames
using GeneralUtils
using ..type, ..util, ..llmfunction
+1 -1
View File
@@ -5,7 +5,7 @@ export virtualWineUserChatbox, jsoncorrection, search_wine_database!, # recomme
extractWineAttributes_2, paraphrase, SQLexecution
using HTTP, JSON, URIs, Random, PrettyPrinting, UUIDs, Dates, DataFrames, DataStructures,
Base64, Serde, LibPQ, NATS
Base64, LibPQ, NATS
using GeneralUtils, SQLLLM
using ..type, ..util
-41
View File
@@ -1,41 +0,0 @@
# This file is machine-generated - editing it directly is not advised
julia_version = "1.11.4"
manifest_format = "2.0"
project_hash = "71d91126b5a1fb1020e1098d9d492de2a4438fd2"
[[deps.Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
version = "1.11.0"
[[deps.InteractiveUtils]]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
version = "1.11.0"
[[deps.Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
version = "1.11.0"
[[deps.Markdown]]
deps = ["Base64"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
version = "1.11.0"
[[deps.Random]]
deps = ["SHA"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
version = "1.11.0"
[[deps.SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
version = "0.7.0"
[[deps.Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
version = "1.11.0"
[[deps.Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
version = "1.11.0"
-2
View File
@@ -1,2 +0,0 @@
[deps]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+136
View File
@@ -0,0 +1,136 @@
using Test
using YiemAgent
using YiemAgent.toolRegistry
using YiemAgent.type
# Path to the real tools directory
TOOLS_DIR = joinpath(@__DIR__, "..", "src", "tools")
@testset "loadTools" begin
# ------------------------------------------------------------------ #
# 1. loadTools throws on non-existent directory #
# ------------------------------------------------------------------ #
@test_throws ArgumentError loadTools("/nonexistent/dir/that/does/not/exist")
# ------------------------------------------------------------------ #
# 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(bad_dir)
# ------------------------------------------------------------------ #
# 3. loadTools loads actual tool files from src/tools/ #
# ------------------------------------------------------------------ #
loaded = loadTools(TOOLS_DIR)
@test !isempty(loaded)
@test length(loaded) == 3
names = [t.name for t in loaded]
@test "getTime" in names
@test "getWeather" in names
@test "writeTool" in names
# ------------------------------------------------------------------ #
# 4. loadTools returns tools sorted alphabetically by filename #
# (getTime.jl < getWeather.jl < writeTool.jl) #
# because 'T' < 'W' in ASCII #
# ------------------------------------------------------------------ #
@test loaded[1].name == "getTime"
@test loaded[2].name == "getWeather"
@test loaded[3].name == "writeTool"
# ------------------------------------------------------------------ #
# 5. Verify loaded tool fields are correct #
# ------------------------------------------------------------------ #
# getTime
time_tool = loaded[1]
@test time_tool.name == "getTime"
@test time_tool.label == "Time Lookup"
@test time_tool.validateRequiredArgs !== nothing
@test time_tool.parallelToolExecute == false
@test time_tool.inputSchema["required"] == Any[]
# getWeather
weather = loaded[2]
@test weather.name == "getWeather"
@test weather.label == "Weather Lookup"
@test weather.execute !== nothing
@test weather.parallelToolExecute == false
@test weather.inputSchema["required"] == ["city"]
# writeTool
wt = loaded[3]
@test wt.name == "writeTool"
@test wt.label == "Create Tool"
@test wt.execute !== nothing
@test "name" in wt.inputSchema["required"]
@test "executeCode" in wt.inputSchema["required"]
# ------------------------------------------------------------------ #
# 6. Tool execution returns valid results #
# ------------------------------------------------------------------ #
sig = nothing
op = x -> x # no-op partial result callback
# execute getTime
result_t = time_tool.execute("call-1", Dict{String,Any}("city" => "Tokyo"), sig, op)
@test result_t isa agentToolResult
@test result_t.content[1] isa textContent
@test occursin("Tokyo", result_t.content[1].text)
# execute getTime with timezone
result_tz = time_tool.execute("call-2", Dict{String,Any}("timezone" => "America/New_York"), sig, op)
@test result_tz isa agentToolResult
@test occursin("America/New_York", result_tz.content[1].text)
# execute getWeather
result_w = weather.execute("call-3", Dict{String,Any}("city" => "Bangkok"), sig, op)
@test result_w isa agentToolResult
@test result_w.content[1] isa textContent
@test occursin("Bangkok", result_w.content[1].text)
# execute getWeather with units
result_w2 = weather.execute("call-4", Dict{String,Any}("city" => "London", "units" => "fahrenheit"), sig, op)
@test occursin("72°F", result_w2.content[1].text)
# ------------------------------------------------------------------ #
# 7. getTools / registerTool / clearTools #
# ------------------------------------------------------------------ #
registry_tools = getTools()
@test !isempty(registry_tools)
@test any(t -> t.name == "getTime", registry_tools)
@test any(t -> t.name == "getWeather", registry_tools)
clearTools()
@test isempty(getTools())
test_tool = agentTool(
name = "manualTool",
label = "Manual Tool",
description = "Registered manually",
inputSchema = Dict{String,Any}("type" => "object", "properties" => Dict{String,Any}(), "required" => Any[]),
execute = (toolCallId, args, signal, onPartialResult) ->
agentToolResult([textContent("manual")], Dict{Any,Any}(), nothing, false),
prepareArguments = nothing,
validateRequiredArgs = nothing,
parallelToolExecute = true
)
registerTool(test_tool)
reg = getTools()
@test any(t -> t.name == "manualTool", reg)
@test count(t -> t.name == "manualTool", reg) == 1
@test reg[1].parallelToolExecute == true
# ------------------------------------------------------------------ #
# 8. getTools returns deep copy (mutations don't affect registry) #
# ------------------------------------------------------------------ #
copy1 = getTools()
copy2 = getTools()
@test copy1 !== copy2
empty!(copy1)
@test !isempty(getTools())
end