From 096baad09db4f177c778b831204925d47d8f0751 Mon Sep 17 00:00:00 2001 From: narawat Date: Tue, 1 Sep 2026 10:36:27 +0700 Subject: [PATCH] update --- Manifest.toml | 224 +++--- Project.toml | 4 +- README.md | 424 ++++++++---- README_tools.md | 1599 ------------------------------------------- my_AI_prompt.md | 1 + src/agentCore.jl | 79 +-- src/api.jl | 10 + src/toolRegistry.jl | 124 ++-- src/utils.jl | 113 ++- 9 files changed, 608 insertions(+), 1970 deletions(-) delete mode 100644 README_tools.md create mode 100644 my_AI_prompt.md diff --git a/Manifest.toml b/Manifest.toml index dbec9cc..5ca0ac4 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,8 +1,20 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.12.6" +julia_version = "1.12.7" manifest_format = "2.0" -project_hash = "3ff1783eadf40ccb51801954aa0a8df935689752" +project_hash = "eeddbbbbefd50bd46b098a2d54b2795d85daa76e" + +[[deps.AWS]] +deps = ["Base64", "Compat", "Dates", "Downloads", "GitHub", "HTTP", "IniFile", "JSON", "MD5", "Mocking", "OrderedCollections", "Random", "SHA", "ScopedValues", "Sockets", "URIs", "UUIDs", "XMLDict"] +git-tree-sha1 = "842075b12a7f763c5f815a04037a99b6ac346df9" +uuid = "fbe9abb3-538b-5e4e-ba9e-bc94f4f92ebc" +version = "1.99.0" + +[[deps.AWSS3]] +deps = ["AWS", "ArrowTypes", "Base64", "Compat", "Dates", "EzXML", "FilePathsBase", "HTTP", "MbedTLS", "Mocking", "OrderedCollections", "Retry", "SymDict", "URIs", "UUIDs", "XMLDict"] +git-tree-sha1 = "af99cea71364c8c4483405a6b03697d197ab4d3b" +uuid = "1c724243-ef5b-51ab-93f4-b0a88ac62a95" +version = "0.11.6" [[deps.Accessors]] deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"] @@ -86,9 +98,9 @@ version = "1.11.0" [[deps.CSV]] deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "PrecompileTools", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings", "WorkerUtilities"] -git-tree-sha1 = "8d8e0b0f350b8e1c91420b5e64e5de774c2f0f4d" +git-tree-sha1 = "abed1e735dd4152f48c90cf0767e1790e25f332f" uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" -version = "0.10.16" +version = "0.10.17" [[deps.CodeTracking]] deps = ["InteractiveUtils", "REPL", "UUIDs"] @@ -110,9 +122,9 @@ version = "0.4.6" [[deps.CodecZlib]] deps = ["TranscodingStreams", "Zlib_jll"] -git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" +git-tree-sha1 = "970758a3d591a2a5c2a907c53f2e2f8c1b1d3537" uuid = "944b1d66-785c-5afd-91f1-9de20f533193" -version = "0.7.8" +version = "0.7.9" [[deps.CodecZstd]] deps = ["TranscodingStreams", "Zstd_jll"] @@ -121,9 +133,10 @@ uuid = "6b39b394-51ab-5f42-8807-6242bab2b4c2" version = "0.8.7" [[deps.CommonSolve]] -git-tree-sha1 = "cf963add2340ad9960e5eb22844e61ad8f931fe1" +deps = ["PrecompileTools"] +git-tree-sha1 = "6c389fa857f6ca5a95474b52a52023fd77f24cb7" uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" -version = "0.2.13" +version = "0.2.14" [[deps.Compat]] deps = ["TOML", "UUIDs"] @@ -143,7 +156,7 @@ version = "0.1.1" [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.3.0+1" +version = "1.3.1+2" [[deps.CompositionsBase]] git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" @@ -181,9 +194,9 @@ uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" version = "4.2.0" [[deps.DBInterface]] -git-tree-sha1 = "a444404b3f94deaa43ca2a58e18153a82695282b" +git-tree-sha1 = "265d7f87f329c063cbde41920f9d00713a43cedd" uuid = "a10d1c49-ce27-4219-8d33-6db1a4562965" -version = "2.6.1" +version = "2.7.0" [[deps.DataAPI]] git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe" @@ -238,9 +251,9 @@ version = "1.11.0" [[deps.Distributions]] deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "Roots", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] -git-tree-sha1 = "d2facc77c08c1c2bfb1a77c148edd05b3db5410b" +git-tree-sha1 = "a958ab3a40c755563f5e1405c0846cb0446bf19d" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" -version = "0.25.130" +version = "0.25.131" [deps.Distributions.extensions] DistributionsChainRulesCoreExt = "ChainRulesCore" @@ -274,15 +287,11 @@ git-tree-sha1 = "d2e49e7efd29719d6f28b891b0e0e159daa9d2b4" uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" version = "0.1.11" -[[deps.FileIO]] -deps = ["Pkg", "Requires", "UUIDs"] -git-tree-sha1 = "6621fef488e496356c9c9625d0562c12a6070819" -uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -version = "1.20.0" -weakdeps = ["HTTP"] - - [deps.FileIO.extensions] - HTTPExt = "HTTP" +[[deps.EzXML]] +deps = ["Printf", "XML2_jll"] +git-tree-sha1 = "7ea1aa5869e2626ccae84480e4f37185bc6f41d3" +uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" +version = "1.2.3" [[deps.FilePathsBase]] deps = ["Compat", "Dates"] @@ -318,17 +327,24 @@ uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" version = "1.11.0" [[deps.Gamma]] -git-tree-sha1 = "86f86b6168a016ed88e4ae4e64577b98c3b59e8e" +deps = ["LogExpFunctions"] +git-tree-sha1 = "becc397f7cfb06e343496ae6ffb04818a851da51" uuid = "a0844989-3bd2-4988-8bea-c9407ab0941b" -version = "1.1.0" +version = "1.2.0" [[deps.GeneralUtils]] -deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "Graphs", "HTTP", "JSON", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "StringDistances", "UUIDs"] -git-tree-sha1 = "129b8fa1bf3bf6d8c0a080f39db09b9b986ef8de" +deps = ["AWS", "AWSS3", "CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "Graphs", "HTTP", "JSON", "LibPQ", "NATS", "OrderedCollections", "PrettyPrinting", "Random", "Revise", "SHA", "StringDistances", "URIs", "UUIDs"] +git-tree-sha1 = "deb2a19735d937b6265d18f55ce0788ac574e9c7" repo-rev = "main" repo-url = "https://git.yiem.cc/ton/GeneralUtils" uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe" -version = "0.5.11" +version = "0.6.6" + +[[deps.GitHub]] +deps = ["Base64", "Dates", "HTTP", "JSON", "MbedTLS", "Sockets", "SodiumSeal", "URIs"] +git-tree-sha1 = "33de1bc5665494f0a9669fd6027e28180709d874" +uuid = "bc5e4493-9b4d-5f90-b8aa-2b2bcaad7a26" +version = "5.13.0" [[deps.Graphs]] deps = ["ArnoldiMethod", "DataStructures", "Inflate", "LinearAlgebra", "Random", "SimpleTraits", "SparseArrays", "Statistics"] @@ -345,9 +361,9 @@ version = "1.14.0" [[deps.HTTP]] deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"] -git-tree-sha1 = "0a58fbbdee93d132a2fb1159b7f5e1b5c2465e71" +git-tree-sha1 = "49a90cd540681d5b79bf5c8ca13c92677e37005e" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "2.6.4" +version = "2.6.6" [[deps.HashArrayMappedTries]] git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" @@ -377,6 +393,11 @@ git-tree-sha1 = "d1b1b796e47d94588b3757fe84fbf65a5ec4a80d" uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" version = "0.1.5" +[[deps.IniFile]] +git-tree-sha1 = "f550e6e32074c939295eb5ea6de31849ac2c9625" +uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f" +version = "0.5.1" + [[deps.InlineStrings]] git-tree-sha1 = "8f3d257792a522b4601c24a577954b0a8cd7334d" uuid = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" @@ -436,9 +457,9 @@ version = "1.8.0" [[deps.JSON]] deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"] -git-tree-sha1 = "65979512c25a0727f050e6e4be40f0fd9ec893f7" +git-tree-sha1 = "c7345ab1a7ca4dc8a02c9f6510da0d9857bbe513" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "1.7.0" +version = "1.7.1" weakdeps = ["ArrowTypes"] [deps.JSON.extensions] @@ -471,18 +492,10 @@ git-tree-sha1 = "0f2899fdadaab4b8f57db558ba21bdb4fb52f1f0" uuid = "b39eb1a6-c29a-53d7-8c32-632cd16f18da" version = "1.21.3+0" -[[deps.LLMMCTS]] -deps = ["GeneralUtils", "JSON", "PrettyPrinting"] -git-tree-sha1 = "3dff98131dfa79be8c9bd84fc51cb0ba1832c472" -repo-rev = "main" -repo-url = "https://git.yiem.cc/ton/LLMMCTS" -uuid = "d76c5a4d-449e-4835-8cc4-dd86ec44f241" -version = "0.1.5" - [[deps.LaTeXStrings]] -git-tree-sha1 = "dda21b8cbd6a6c40d9d02a73230f9d70fed6918c" +git-tree-sha1 = "f88f3ccef05a6a72a0cf0ed417c8fd68530f4ab2" uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" -version = "1.4.0" +version = "1.4.1" [[deps.LayerDicts]] git-tree-sha1 = "6087ad3521d6278ebe5c27ae55e7bbb15ca312cb" @@ -530,6 +543,12 @@ 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" @@ -567,6 +586,12 @@ git-tree-sha1 = "191686b1ac1ea9c89fc52e996ad15d1d241d1e33" uuid = "5ced341a-0733-55b8-9ab6-a4889d929147" version = "1.10.1+0" +[[deps.MD5]] +deps = ["Random", "SHA"] +git-tree-sha1 = "1576f756617d31eb397a4a517b68562fd28dc2b4" +uuid = "6ac74813-4b46-53a4-afec-0b5dc9d7885c" +version = "0.2.3" + [[deps.MacroTools]] git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" @@ -655,7 +680,7 @@ version = "0.8.7+0" [[deps.OpenSSL_jll]] deps = ["Artifacts", "Libdl"] uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.5.4+0" +version = "3.5.6+0" [[deps.OpenSpecFun_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] @@ -684,15 +709,6 @@ git-tree-sha1 = "3de8f5e6e90ebfa8d6d1f86997d6cdcd6a912ff3" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" version = "2.8.7" -[[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.12.1" -weakdeps = ["REPL"] - - [deps.Pkg.extensions] - REPLExt = "REPL" - [[deps.PooledArrays]] deps = ["DataAPI", "Future"] git-tree-sha1 = "36d8b4b899628fb92c2749eb488d884a926614d3" @@ -718,9 +734,9 @@ version = "0.4.3" [[deps.PrettyTables]] deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"] -git-tree-sha1 = "4ac881f5432bd93463a41767a814a45245be22b6" +git-tree-sha1 = "1b8aa19f229b1cea7fc93874a52e49db6a854450" uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" -version = "3.4.6" +version = "3.4.8" [deps.PrettyTables.extensions] PrettyTablesExcelExt = "XLSX" @@ -781,15 +797,20 @@ version = "1.3.1" [[deps.Reseau]] deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"] -git-tree-sha1 = "701ef63506992668c0069a4d1ac1540b77258e6c" +git-tree-sha1 = "aaee74fab545e9af4b21d10c2203c93c636c4ae0" uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36" -version = "1.3.6" +version = "1.4.1" + +[[deps.Retry]] +git-tree-sha1 = "41ac127cd281bb33e42aba46a9d3b25cd35fc6d5" +uuid = "20febd7b-183b-5ae2-ac4a-720e7ce64774" +version = "0.4.1" [[deps.Revise]] deps = ["CRC32c", "CodeTracking", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"] -git-tree-sha1 = "ec46aed6a3a8cc6b67839ca361e7b4aa32eaeee1" +git-tree-sha1 = "ab0f5630e37968bc49b8c4cedd3279a7d30b9486" uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" -version = "3.16.3" +version = "3.17.0" weakdeps = ["Distributed"] [deps.Revise.extensions] @@ -809,9 +830,9 @@ version = "0.5.2+0" [[deps.Roots]] deps = ["Accessors", "CommonSolve", "Printf"] -git-tree-sha1 = "7fb25a964849d90a0446366cdefca822e0e84900" +git-tree-sha1 = "13a9e0164267bb9ebc55c1c5d72fceea8f09555b" uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" -version = "3.0.6" +version = "3.0.7" [deps.Roots.extensions] RootsChainRulesCoreExt = "ChainRulesCore" @@ -833,14 +854,6 @@ version = "3.0.6" uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" version = "0.7.0" -[[deps.SQLLLM]] -deps = ["CSV", "DataFrames", "DataStructures", "Dates", "FileIO", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "PrettyPrinting", "Random", "Revise", "StatsBase", "Tables", "URIs", "UUIDs"] -git-tree-sha1 = "bae2fd2e2b087753fbb3415896be41df1ae0eb90" -repo-rev = "main" -repo-url = "https://git.yiem.cc/ton/SQLLLM" -uuid = "2ebc79c7-cc10-4a3a-9665-d2e1d61e63d3" -version = "0.2.8" - [[deps.SQLStrings]] git-tree-sha1 = "55de0530689832b1d3d43491ee6b67bd54d3323c" uuid = "af517c2e-c243-48fa-aab8-efac3db270f5" @@ -884,6 +897,12 @@ git-tree-sha1 = "907703e0d50846f300650d7225bdcab145b7bca9" uuid = "4f5b5e99-b0ad-42cd-b47a-334e172ec8bd" version = "1.1.2" +[[deps.SodiumSeal]] +deps = ["Base64", "Libdl", "libsodium_jll"] +git-tree-sha1 = "80cef67d2953e33935b41c6ab0a178b9987b1c99" +uuid = "2133526b-2bfb-4018-ac12-889fb3908a75" +version = "0.1.1" + [[deps.SortingAlgorithms]] deps = ["DataStructures"] git-tree-sha1 = "13cd91cc9be159e3f4d95b857fa2aa383b53772a" @@ -897,9 +916,9 @@ version = "1.12.0" [[deps.SpecialFunctions]] deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] -git-tree-sha1 = "c3ac026e735264e9bdc6a9bcbd1b1e781b36e3bc" +git-tree-sha1 = "429071b23f4c9a13fb6582f807cc2ef454082408" uuid = "276daf66-3868-5448-9aa4-cd146d93841b" -version = "2.8.3" +version = "2.9.0" [deps.SpecialFunctions.extensions] SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" @@ -909,9 +928,9 @@ version = "2.8.3" [[deps.StaticArrays]] deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] -git-tree-sha1 = "246a8bb2e6667f832eea063c3a56aef96429a3db" +git-tree-sha1 = "fac51faf3bb96e8bc0bf6f9f39ca4955652776bb" uuid = "90137ffa-7385-5640-81b9-e52037218182" -version = "1.9.18" +version = "1.9.19" [deps.StaticArrays.extensions] StaticArraysChainRulesCoreExt = "ChainRulesCore" @@ -928,9 +947,9 @@ version = "1.4.4" [[deps.Statistics]] deps = ["LinearAlgebra"] -git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0" +git-tree-sha1 = "389592b8592e5fb48f498ff60dc6acb4a0e62953" uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -version = "1.11.1" +version = "1.11.4" weakdeps = ["SparseArrays"] [deps.Statistics.extensions] @@ -944,9 +963,9 @@ version = "1.8.0" [[deps.StatsBase]] deps = ["AliasTables", "DataAPI", "DataStructures", "IrrationalConstants", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] -git-tree-sha1 = "e4d7a1a0edc20af42689ea6f4f3587a2175d50ee" +git-tree-sha1 = "adb9da019510162e67a4493fc235c23203d8b09e" uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -version = "0.34.12" +version = "0.34.13" [[deps.StatsFuns]] deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"] @@ -964,15 +983,15 @@ version = "2.2.1" [[deps.StringDistances]] deps = ["Distances", "StatsAPI"] -git-tree-sha1 = "cd83a04baf746e3b43b83c61b7de77ab0409b80a" +git-tree-sha1 = "456f343c7a6c8f52d8b501c0da2a05538ab1b907" uuid = "88034a9c-02f8-509d-84a9-84ec65e18404" -version = "1.0.0" +version = "1.0.1" [[deps.StringManipulation]] deps = ["PrecompileTools"] -git-tree-sha1 = "8a90c1d77c3277a5d43b83927b3cbe2c70a37484" +git-tree-sha1 = "773065c6e0e903924a9d838259be74338422aef2" uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e" -version = "0.4.7" +version = "0.5.0" [[deps.StringViews]] git-tree-sha1 = "f2dcb92855b31ad92fe8f079d4f75ac57c93e4b8" @@ -987,9 +1006,9 @@ version = "1.11.0" [[deps.StructUtils]] deps = ["Dates", "UUIDs"] -git-tree-sha1 = "c65ae4aa47e543c278aea0a3468786d33021a3ff" +git-tree-sha1 = "2d0fc55c61321ba245c47be599570d11bac50303" uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" -version = "2.8.4" +version = "2.8.5" [deps.StructUtils.extensions] StructUtilsMeasurementsExt = ["Measurements"] @@ -1014,6 +1033,12 @@ deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" version = "7.8.3+2" +[[deps.SymDict]] +deps = ["Test"] +git-tree-sha1 = "0108ccdaea3ef69d9680eeafc8d5ad198b896ec8" +uuid = "2da68c74-98d7-5633-99d6-8493888d7b1e" +version = "0.3.0" + [[deps.TOML]] deps = ["Dates"] uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" @@ -1021,9 +1046,9 @@ version = "1.0.3" [[deps.TZJData]] deps = ["Artifacts"] -git-tree-sha1 = "72df96b3a595b7aab1e101eb07d2a435963a97e2" +git-tree-sha1 = "d3b30a9f29a898e21fb4bdf280101b7a12e1f39c" uuid = "dc5dba14-91b3-4cab-a142-028a31da12f7" -version = "1.5.0+2025b" +version = "1.5.1+2025b" [[deps.TableTraits]] deps = ["IteratorInterfaceExtensions"] @@ -1033,14 +1058,9 @@ version = "1.0.1" [[deps.Tables]] deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"] -git-tree-sha1 = "0f38a06c83f0007bbab3cf911262841c9a0f07e0" +git-tree-sha1 = "a94d9bdda1b7bed0046cea645639ab3f62196fac" uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -version = "1.13.0" - -[[deps.Tar]] -deps = ["ArgTools", "SHA"] -uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" -version = "1.10.0" +version = "1.14.0" [[deps.Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] @@ -1063,9 +1083,9 @@ uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" version = "0.11.3" [[deps.URIs]] -git-tree-sha1 = "3b0738bd7c5645641845da25cbd99800b8718689" +git-tree-sha1 = "908fec9df6c5de98548ead82a468c95ccf6cd263" uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" -version = "1.6.2" +version = "1.7.0" [[deps.UTCDateTimes]] deps = ["Dates", "TimeZones"] @@ -1093,8 +1113,20 @@ 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.XMLDict]] +deps = ["EzXML", "IterTools", "OrderedCollections"] +git-tree-sha1 = "8e8cdc9c93e1f442eba471b56db6f86ffeb1a33a" +uuid = "228000da-037f-5747-90a9-8195ccbf91a5" +version = "0.4.2" + [[deps.YiemAgent]] -deps = ["Base64", "CSV", "DataFrames", "DataStructures", "Dates", "GeneralUtils", "HTTP", "JSON", "LLMMCTS", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "SQLLLM", "Serialization", "URIs", "UUIDs"] +deps = ["Base64", "CSV", "DataFrames", "DataStructures", "Dates", "GeneralUtils", "HTTP", "JSON", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "Serialization", "URIs", "UUIDs"] path = "." uuid = "e012c34b-7f78-48e0-971c-7abb83b6f0a2" version = "0.8.0" @@ -1122,12 +1154,12 @@ uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8" version = "1.0.21+0" [[deps.msghandler]] -deps = ["Arrow", "Base64", "DataFrames", "Dates", "GeneralUtils", "HTTP", "JSON", "NATS", "PrettyPrinting", "Revise", "UUIDs"] -git-tree-sha1 = "e82a79cf6602541ea25409aded57b2ace4a7c29f" +deps = ["Arrow", "Base64", "DataFrames", "Dates", "GeneralUtils", "HTTP", "JSON", "NATS", "PrettyPrinting", "UUIDs"] +git-tree-sha1 = "e7889a120327db84cdf1b1acf4a2c7caba6e8ef7" repo-rev = "main" repo-url = "https://git.yiem.cc/ton/msghandler" uuid = "f2724d33-f338-4a57-b9f8-1be882570d10" -version = "1.2.1" +version = "1.4.0" [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] diff --git a/Project.toml b/Project.toml index 31ce449..62dde12 100644 --- a/Project.toml +++ b/Project.toml @@ -26,8 +26,8 @@ msghandler = "f2724d33-f338-4a57-b9f8-1be882570d10" Base64 = "1.11.0" CSV = "0.10.15" DataFrames = "1.7.0" -GeneralUtils = "0.5.10" +GeneralUtils = "0.6.6" HTTP = "2.4.0" JSON = "1.6.1" NATS = "0.1.0" -msghandler = "1.2.1" +msghandler = "1.4.0" diff --git a/README.md b/README.md index ba83600..841f3ad 100644 --- a/README.md +++ b/README.md @@ -2,138 +2,310 @@ Julia framework for building agents with tool use and MCP (Model Context Protocol) support. -## Getting Started +## Quick Start -1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, NATS, DataFrames` -2. Create a callable `mcpServer` struct that communicates with an MCP server via NATS -3. Create a `yiemAgent` with an LLM callable and MCP server: - ```julia - agent = YiemAgent.yiemAgent(llmCall; mcpServer=mcpServer, eventSink=yourSink) - ``` -4. Call `promptAgent(agent, message)` then `takeResponse(agent)` +```julia +using YiemAgent + +# 1. Create an LLM callable (see "Custom LLM Callable" below) +llmCall = ... + +# 2. Create a callable MCP server (see "MCP Server Integration" below) +mcpServer = ... + +# 3. Create the agent +agent = yiemAgent(llmCall; mcpServer=mcpServer, eventSink=println) + +# 4. Interact +promptAgent(agent, "What tools do you have?") +response = takeResponse(agent) + +# 5. Stop when done +stopAgent(agent) +``` + +## Agent API + +| Function | Description | +|----------|-------------| +| `promptAgent(agent, text)` | Send plain text to the agent's input channel | +| `promptAgent(agent, msg)` | Send a structured message (OpenAI format dict) to the input channel | +| `takeResponse(agent)` | Block and take the agent's response from output channel | +| `followUp(agent, text)` | Send plain text follow-up while agent is still processing | +| `followUp(agent, msg)` | Send a structured follow-up (OpenAI format dict) while agent is processing | +| `stopAgent(agent)` | Gracefully stop the agent and close channels | + +## Creating a yiemAgent + +```julia +yiemAgent(llmCall; + systemPrompt::String = "You are a helpful assistant.", + model = nothing, + messages::Vector{agentMessage} = agentMessage[], + prepareContext::Function = prepareContext, + formatMsgForLLM::Function = formatMsgForLLM, + beforeToolCall::Function = beforeToolCall, + afterToolCall::Function = afterToolCall, + sessionId::Union{String, Nothing} = nothing, + maxRetryDelayMs::Union{Int64, Nothing} = nothing, + parallelToolExecute::Bool = false, + eventSink::Function = eventSink, + mcpServer = nothing +) +``` + +| Parameter | Description | +|-----------|-------------| +| `llmCall` | **(required)** Function that receives formatted messages and returns an LLM response | +| `systemPrompt` | System prompt for the agent | +| `model` | LLM model configuration (`llmModel` struct) | +| `messages` | Initial conversation messages | +| `prepareContext` | Hook to transform messages/context before each LLM call | +| `formatMsgForLLM` | Hook to convert agent context to LLM API format | +| `beforeToolCall` | Hook to block/allow tool execution | +| `afterToolCall` | Hook to post-process tool results | +| `sessionId` | Optional session identifier for MCP server | +| `maxRetryDelayMs` | Maximum retry delay in milliseconds | +| `parallelToolExecute` | Run multiple tool calls in parallel | +| `eventSink` | Callback for agent events/debug messages | +| `mcpServer` | Callable struct for MCP server communication | + +## Usage Examples + +### Text Messages + +```julia +# Send plain text +promptAgent(agent, "What tools do you have?") +response = takeResponse(agent) + +# Follow-up while agent is processing +promptAgent(agent, "Tell me about wine") +followUp(agent, "Also include prices") +response = takeResponse(agent) +``` + +### Structured Messages (OpenAI Format) + +```julia +promptAgent(agent, Dict("role" => "user", "content" => "Hello")) +response = takeResponse(agent) +``` + +### Output Format + +`takeResponse()` returns a `Dict{String, Any}` in OpenAI message format: + +```julia +Dict( + "role" => "assistant", + "content" => [ + Dict("type" => "text", "text" => "The weather in Tokyo is Sunny, 22°C."), + # ... more content blocks + ] +) +``` + +Content block types: + +| Block Type | Fields | Description | +|------------|--------|-------------| +| `text` | `"type" => "text"`, `"text" => "..."` | Plain text response from the LLM | +| `image_url` | `"type" => "image_url"`, `"image_url" => Dict("url" => "...")` | Image content (rare, used by multimodal models) | + +Example — extracting the text response: + +```julia +response = takeResponse(agent) +text_response = first(c["text"] for c in response["content"] if c["type"] == "text") +``` + +### Multimodal Messages (Text + Image) + +```julia +# Encode images to data URIs +image_path = "test/image.png" +image_bytes = read(image_path) +base64_string = base64encode(image_bytes) +data_uri = "data:image/png;base64,$(base64_string)" + +# Build message with text and image content blocks +message = Dict( + "role" => "user", + "content" => [ + Dict("type" => "text", "text" => "Do you have Brunello di Montalcino?"), + Dict("type" => "image_url", "image_url" => Dict("url" => data_uri)) + ] +) +promptAgent(agent, message) +response = takeResponse(agent) +``` + +### Custom Hooks + +```julia +# beforeToolCall — request user approval for destructive tools +function my_before_tool_call(context::beforeToolCallContext, signal::abortSignal)::beforeToolCallResult + if context.toolCall.name == "deleteFile" + return beforeToolCallResult(true, "User must approve file deletion") + end + return beforeToolCallResult(false, "N/A") +end + +# afterToolCall — mask sensitive data from results +function my_after_tool_call(context::afterToolCallContext, signal::abortSignal)::Union{agentToolResult, Nothing} + if context.isError + return agentToolResult( + [textContent("The tool encountered an error. Please try again.")], + context.result.details, context.result.usage, context.result.terminate + ) + end + return nothing +end + +agent = yiemAgent(llmCall; + mcpServer=mcpServer, + beforeToolCall=my_before_tool_call, + afterToolCall=my_after_tool_call, + eventSink=println +) +``` + +### Custom LLM Callable + +```julia +function my_llm_call(messages_dict::Dict{String, Any}) + # messages_dict contains "messages" key with OpenAI-format message array, + # plus optional "tools", "temperature", etc. + + # Call your LLM provider (OpenAI, Anthropic, local model, etc.) + raw_response = call_my_llm_provider(messages_dict) + + # Return a dict with these fields: + return Dict( + "message" => Dict("role" => "assistant", "content" => raw_response["content"]), + "finish_reason" => raw_response["finish_reason"], + "api" => "my_provider", + "provider" => "my_provider", + "model" => "my-model-name", + "usage" => llmUsage(100, 50) + ) +end +``` + +### Custom Context Preparation + +```julia +function my_prepare_context(state::agentState, eventSink)::agentContext + sysPrompt = state.systemPrompt * "\nCurrent time: $(now())" + msgs = deepcopy(state.messages) + + # Prune old messages to fit context window + max_messages = 20 + if length(msgs) > max_messages + msgs = msgs[end-max_messages+1:end] + end + + return agentContext(sysPrompt, msgs, state.tools) +end + +agent = yiemAgent(llmCall; prepareContext=my_prepare_context, ...) +``` + +## MCP Server Integration + +YiemAgent uses JSON-RPC 2.0 for MCP communication. The `mcpServer` callable struct must implement `tools/list` and `tools/call` methods. + +### Protocol + +```julia +# tools/list — returns tool definitions +mcpServer("tools/list") +# → Dict("jsonrpc"=>"2.0", "id"=>1, "result"=>Dict("tools"=>[...], "nextCursor"=>...)) + +# tools/call — executes a tool +mcpServer("tools/call", toolName, arguments) +# → Dict("jsonrpc"=>"2.0", "id"=>2, "result"=>Dict("content"=>[...], "isError"=>...)) + +# Protocol error responses include "error" instead of "result": +Dict("jsonrpc"=>"2.0", "id"=>2, "error"=>Dict("code"=>-32602, "message"=>"...")) +``` + +### Example: NATS-based MCP Server Client + +```julia +struct mcpServer + natsConn::NATS.Connection + topic::String + senderID::String + fileserver_url::String +end + +# tools/list implementation +function (c::mcpServer)(method::String) + payload = Dict("jsonrpc" => "2.0", "id" => 1, "method" => method, "params" => Dict{String, Any}()) + payloads = [("payload", payload, "dictionary")] + _, msg_envelope = msghandler.smartpack(c.topic, payloads; + sender_id=c.senderID, msg_purpose="mcp_tools_list", fileserver_url=c.fileserver_url) + reply = NATS.request(c.natsConn, c.topic, msg_envelope, timeout=180) + incoming = msghandler.smartunpack(String(reply.payload)) + return incoming["payloads"][1][2] +end + +# tools/call implementation +function (c::mcpServer)(method::String, toolName::String, arguments::Dict{String, Any}) + payload = Dict("jsonrpc" => "2.0", "id" => 2, "method" => method, + "params" => Dict("name" => toolName, "arguments" => arguments)) + payloads = [("payload", payload, "dictionary")] + _, msg_envelope = msghandler.smartpack(c.topic, payloads; + sender_id=c.senderID, msg_purpose="mcp_tool_call", fileserver_url=c.fileserver_url) + reply = NATS.request(c.natsConn, c.topic, msg_envelope, timeout=180) + incoming = msghandler.smartunpack(String(reply.payload)) + return incoming["payloads"][1][2] +end +``` + +## Tool Discovery + +Tools are discovered dynamically via the MCP server: + +1. `listTools` is the only pre-registered tool at agent creation +2. When the LLM calls `listTools()`, tools are discovered from the MCP server and registered at runtime +3. Supports pagination via `nextCursor` for large tool sets +4. New tools become immediately available without restarting the agent + +## Hooks + +Hooks allow customization of the agent's behavior at key points in the execution pipeline. + +| Hook | Signature | Purpose | +|------|-----------|---------| +| `prepareContext` | `(state::agentState, eventSink) -> agentContext` | Transform messages/context before LLM call (filter tools, inject context, prune messages) | +| `formatMsgForLLM` | `(ctx::agentContext, eventSink) -> Dict` | Convert agent context to LLM API message format | +| `beforeToolCall` | `(context::beforeToolCallContext, signal::abortSignal) -> beforeToolCallResult` | Block/allow tool execution; request user approval | +| `afterToolCall` | `(context::afterToolCallContext, signal::abortSignal) -> Union{agentToolResult, Nothing}` | Post-process tool results; mask sensitive data; set `terminate` flag | +| `eventSink` | `(msg) -> nothing` | Callback for agent events/debug messages throughout the pipeline | + +## Agent Lifecycle + +1. **Creation** — `yiemAgent()` spawns a background task (`_agentLoop`) listening on `inputChannel` and `followUpChannel` +2. **Input** — User messages enter via `promptAgent()` (immediate) or `followUp()` (queued while agent is busy) +3. **Processing** — `_processMessage()` handles LLM calls, tool execution, and conversation history in a loop +4. **Tool Execution Pipeline** — each tool call follows three phases: + - `prepareToolCall` — resolve tool, validate arguments, run `beforeToolCall` hook + - `executePreparedToolCall` — invoke `tool.execute()` + - `finalizeExecutedToolCall` — run `afterToolCall` hook, construct `toolResultMessage` +5. **Termination** — tool `execute` functions can set `terminate=true` to stop the agent loop +6. **Shutdown** — `stopAgent()` sends a `:shutdown` signal, drains channels, and closes the agent ## Architecture ``` src/ ├── YiemAgent.jl # Module entry point -├── type.jl # Core types (messages, tools, agent state) +├── type.jl # Core types (messages, tools, agent state, events) ├── utils.jl # Message formatting, context preparation, validation -├── agentCore.jl # Agent loop, tool execution pipeline -├── api.jl # Public API (promptAgent, takeResponse, followUp, stopAgent) -└── toolRegistry.jl # Tool store, MCP discovery, listTools registration +├── toolRegistry.jl # Tool store, MCP discovery, listTools registration +├── agentCore.jl # Agent loop, tool execution pipeline, yiemAgent struct +└── api.jl # Public API (promptAgent, takeResponse, followUp, stopAgent) ``` - -## MCP Protocol - -YiemAgent uses JSON-RPC 2.0 for MCP communication. The `mcpServer` callable struct must implement: - -```julia -# tools/list — returns tool definitions -mcpServer("tools/list") # → Dict("jsonrpc"=>"2.0", "id"=>1, "result"=>Dict("tools"=>[...], "nextCursor"=>...)) - -# tools/call — executes a tool -mcpServer("tools/call", toolName, arguments) # → Dict("jsonrpc"=>"2.0", "id"=>2, "result"=>Dict("content"=>[...], "isError"=>...)) -``` - -Protocol error responses include `"error"` instead of `"result"`: -```julia -Dict("jsonrpc"=>"2.0", "id"=>2, "error"=>Dict("code"=>-32602, "message"=>"...")) -``` - -### Implementing the MCP Server Client - -You must provide a callable struct that communicates with your MCP server. Example using NATS: - -```julia -struct mcpServer - natsConn::NATS.Connection - topic::String - senderID::String - fileserver_url::String -end - -# tools/list implementation -function (c::mcpServer)(method::String) - if method != "tools/list" - error("mcpServer: unexpected method '$method' (expected 'tools/list')") - end - - payload = Dict("jsonrpc" => "2.0", "id" => 1, "method" => method, "params" => Dict{String, Any}()) - payloads = [("payload", payload, "dictionary")] - _, msg_envelope_json_str = msghandler.smartpack( - c.topic, payloads; - sender_id=c.senderID, - msg_purpose="mcp_tools_list", - fileserver_url=c.fileserver_url) - - reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180) - incoming_env = msghandler.smartunpack(String(reply.payload)) - return incoming_env["payloads"][1][2] -end - -# tools/call implementation -function (c::mcpServer)(method::String, toolName::String, arguments::Dict{String, Any}) - if method != "tools/call" - error("mcpServer: unexpected method '$method' (expected 'tools/call')") - end - - payload = Dict( - "jsonrpc" => "2.0", - "id" => 2, - "method" => method, - "params" => Dict( - "name" => toolName, - "arguments" => arguments - ) - ) - payloads = [("payload", payload, "dictionary")] - _, msg_envelope_json_str = msghandler.smartpack( - c.topic, payloads; - sender_id=c.senderID, - msg_purpose="mcp_tool_call", - fileserver_url=c.fileserver_url) - - reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180) - incoming_env = msghandler.smartunpack(String(reply.payload)) - return incoming_env["payloads"][1][2] -end -``` - -See `test/runtest.jl` for the complete working example. - -## Tool Discovery - -Tools are discovered dynamically via MCP server: - -1. `listTool` is the only pre-registered tool -2. When the LLM calls `listTools()`, tools are discovered from the MCP server and registered at runtime -3. Supports pagination via `nextCursor` for large tool sets - -## Agent API - -| Function | Description | -|----------|-------------| -| `promptAgent(agent, msg)` | Send a message to the agent's input channel | -| `takeResponse(agent)` | Block and take the agent's response from output channel | -| `followUp(agent, msg)` | Send a follow-up message while agent is still processing | -| `stopAgent(agent)` | Gracefully stop the agent and close channels | - -## Agent Lifecycle - -1. `yiemAgent()` spawns a background task (`_agentLoop`) listening on `inputChannel` and `followUpChannel` -2. User messages enter via `promptAgent()` or `followUp()` -3. `_processMessage()` handles LLM calls, tool execution, and conversation history -4. Tool execution follows three phases: `prepareToolCall` → `executePreparedToolCall` → `finalizeExecutedToolCall` -5. `beforeToolCall`/`afterToolCall` hooks allow pre/post-processing of tool calls -6. Tool `execute` functions can set `terminate=true` to stop the agent loop - -## Hooks - -| Hook | Signature | Purpose | -|------|-----------|---------| -| `prepareContext` | `(state, sink, llmCall) -> ctx` | Transform messages/context before LLM call | -| `formatMsgForLLM` | `(ctx, sink) -> dict` | Convert agent context to LLM API format | -| `beforeToolCall` | `(context, signal) -> result` | Block/allow tool execution | -| `afterToolCall` | `(context, signal) -> result` | Post-process tool results | -| `eventSink` | `(msg) -> nothing` | Callback for agent events/debug messages | diff --git a/README_tools.md b/README_tools.md deleted file mode 100644 index fb492f1..0000000 --- a/README_tools.md +++ /dev/null @@ -1,1599 +0,0 @@ -# Tools — Complete End-to-End Lifecycle - -This document describes the complete tool lifecycle in the YiemAgent framework, from definition through execution, for framework authors and maintainers who need a thorough understanding of the architecture. - ---- - -## Table of Contents - -1. [Quick Start: Tool Lifecycle](#1-quick-start-tool-lifecycle) -2. [Overview](#2-overview) -3. [Tool Definition — The `agentTool` Struct](#3-tool-definition--the-agenttool-struct) -4. [Tool Registration — Static Registration](#4-tool-registration--static-registration) -5. [The Agent Loop — High-Level Flow](#5-the-agent-loop--high-level-flow) -6. [Message Processing Pipeline](#6-message-processing-pipeline) -7. [Tool Call Extraction from LLM Response](#7-tool-call-extraction-from-llm-response) -8. [The Per-Call Pipeline — Prepare, Execute, Finalize](#8-the-per-call-pipeline--prepare-execute-finalize) -9. [Execution Modes — Sequential vs Parallel](#9-execution-modes--sequential-vs-parallel) -10. [Tool Call Batches & Termination Logic](#10-tool-call-batches--termination-logic) -11. [Tool Result Message Creation](#11-tool-result-message-creation) -12. [Error Handling & Recovery Pattern](#12-error-handling--recovery-pattern) -13. [Event System — Tool Lifecycle Events](#13-event-system--tool-lifecycle-events) -14. [Agent Lifecycle Hooks](#14-agent-lifecycle-hooks) -15. [Self-Modifying Tools](#15-self-modifying-tools) -16. [Complete End-to-End Example](#16-complete-end-to-end-example) -17. [Tool File Contract](#17-tool-file-contract) -18. [Adding New Tools](#18-adding-new-tools) -19. [Appendix: Type Reference](#19-appendix-type-reference) - ---- - -## 1. Quick Start: Tool Lifecycle - -This section shows the complete lifecycle from tool registration through execution and result extraction. Each step maps to the detailed sections below. - -### Step 1: Discover — `listTools` - -The agent calls the `listTools` tool to see available tools and detect name collisions before creating new ones. - -```julia -# The listTools tool is auto-injected via listTool(store) — no manual registration needed -tool = listTool(store) # Returns an agentTool that, when executed, lists all tools in the store -``` - -**Result extraction:** -```julia -result = tool.execute("call-1", Dict{String,Any}(), nothing, x->x) -# result.content[1].text => "Available tools:\n- getWeather: Weather Lookup — Fetch current weather...\n- getTime: Time Lookup — Get current local time..." -``` - -**Source:** `toolRegistry.jl:43-98` - ---- - -### Step 2: Register — `register_all_tools()` - -Tools are statically defined in `src/tools/` and registered at module initialization via `register_all_tools()`. Each tool function (e.g., `getWeatherTool()`, `getTimeTool()`, `writeToolTool()`) is called to create the `agentTool` struct. `listTool` is auto-registered so the LLM can discover available tools. - -```julia -using YiemAgent, YiemAgent.toolRegistry - -store = toolStore(name="myAgent") -tools = register_all_tools(store) -# Calls getWeatherTool(), getTimeTool(), writeToolTool() to create agentTool structs -# Also auto-registers listTools for runtime discovery -``` - -**Result extraction:** -```julia -all_tools = getTools(store) # OrderedDict{String, agentTool} -# Keys: "getWeather", "getTime", "writeTool", "listTools" -getWeather_tool = all_tools["getWeather"] - -# Manual registration (alternative to register_all_tools) -registerTool(store, my_tool) -clearTools(store) # Clear all tools from store -``` - -**Source:** `toolRegistry.jl:38-40, 127-143` - ---- - -### Step 2.5: Create Agent with Tools - -Wire the registered tools into a new `yiemAgent` instance. The `yiemAgent` constructor calls `register_all_tools()` automatically. - -```julia -using YiemAgent, YiemAgent.type, YiemAgent.toolRegistry - -# Create agent — tools are registered automatically via register_all_tools() -agent = yiemAgent( - llmCall = my_llm_call, # Function that calls the LLM API - eventSink = my_event_sink, # Function for TUI/logging -) -``` - -**Key constructor parameters:** - -| Parameter | Type | Required | Purpose | -|-----------|------|----------|---------| -| `llmCall` | `Function` | **Yes** | `(messages::Dict) -> assistantMessage` — invokes the LLM | -| `eventSink` | `Function` | **Yes** | `(event) -> nothing` — receives tool lifecycle events | -| `systemPrompt` | `String` | No (default: "You are helpful assistant.") | System prompt text | -| `model` | `llmModel` | No | LLM model config | -| `messages` | `Vector{agentMessage}` | No (default: empty) | Initial conversation history | - -Optional hooks: `prepareContext`, `formatMsgForLLM`, `beforeToolCall`, `afterToolCall`, `sessionId`, `maxRetryDelayMs`, `parallelToolExecute`. - -**Source:** `type.jl:609-657`, `toolRegistry.jl:127-143` - ---- - -### Step 3: Use — Tool Execution - -Tools can be used in two ways: - -**Direct execution (testing / standalone):** -```julia -using YiemAgent.type - -sig = nothing -op = x -> x # no-op partial result callback - -# Execute a loaded tool directly -result = getWeather_tool.execute("call-1", Dict("city" => "Tokyo"), sig, op) -``` - -**Via agent loop (production):** -``` -user message → promptAgent(agent, Dict("role"=>"user", "content"=>...)) - → _agentLoop detects message → @spawn _processMessage(agent) - → prepareContext → formatMsgForLLM → llmCall - → LLM returns tool_calls - → executeToolCalls(context, response, tool_call_list, config, signal, emit) - → prepareToolCall → executePreparedToolCall → finalizeExecutedToolCall - → createToolResultMessage → batch.messages (toolResultMessage[]) -``` - -**Source:** Direct: `test/toolTest.jl:81-99` | Agent: `agentCore.jl:35-311` - ---- - -### Step 4: Extract Result - -**`agentToolResult`** (raw tool output, `type.jl:429-434`): -```julia -result = getWeather_tool.execute("call-1", Dict("city" => "Tokyo"), nothing, x->x) - -result.content[1] # textContent("Weather in Tokyo: Sunny, 22°C") -result.content[1].text # "Weather in Tokyo: Sunny, 22°C" -result.details # Dict{Any,Any}() — tool-specific metadata -result.usage # nothing — llmUsage tracking (optional) -result.terminate # false — signals loop termination -``` - -**`toolResultMessage`** (wrapped for conversation history, `type.jl:152-191`): -```julia -msg = batch.messages[1] # toolResultMessage - -msg.toolCallId # "call-1" -msg.toolName # "getWeather" -msg.content # Vector{messageContent} -msg.isError # false -msg.details # tool-specific metadata -msg.timestamp # DateTime -``` - ---- - -## 2. Overview - -The tool system follows a **three-phase pipeline** per tool call: - -``` -PREPARE → EXECUTE → FINALIZE -``` - -Each phase has a single responsibility and produces an intermediate result: - -| Phase | Function | Input | Output | Purpose | -|-------|----------|-------|--------|---------| -| Prepare | `prepareToolCall()` | `agentContext`, `assistantMessage`, `agentToolCall`, `agentLoopConfig`, `abortSignal` | `preparedToolCall` or `immediateOutcome` | Resolve tool, validate args, run pre-hook | -| Execute | `executePreparedToolCall()` | `preparedToolCall`, `abortSignal`, `eventSink` | `executedOutcome` | Call `tool.execute()`, stream partial results | -| Finalize | `finalizeExecutedToolCall()` | `agentContext`, `assistantMessage`, `preparedToolCall`, `executedOutcome`, `agentLoopConfig`, `abortSignal` | `finalizedOutcome` | Run post-hook, emit end event | - -The pipeline ensures that **every tool call produces a result**, even on failure. Errors are captured as `immediateOutcome`, `executedOutcome`, or `finalizedOutcome` with `isError=true`, then converted to `toolResultMessage` objects that are fed back to the LLM conversation history. - ---- - -## 3. Tool Definition — The `agentTool` Struct - -**Source:** `type.jl:261-281` - -Every tool is an `agentTool` struct with the following fields: - -```julia -struct agentTool - name::String # Unique identifier (e.g. "getWeather") - label::String # Human-readable name (e.g. "Weather Lookup") - description::String # What the tool does (shown to the LLM for selection) - inputSchema::Any # JSON Schema (MCP format) describing parameters - execute::Function # Core execution: (toolCallId, args, signal, onPartialResult) -> agentToolResult - prepareArguments::Union{Function, Nothing} # Optional: (args) -> modified_args (before validation) - validateRequiredArgs::Union{Function, Nothing} # Optional: (args) -> Union{Nothing, String} error - parallelToolExecute::Bool # Override: run in parallel with other tools -end -``` - -### Field Details - -| Field | Required | Signature | Purpose | -|-------|----------|-----------|---------| -| `name` | Yes | `String` | Unique key for tool lookup in `context.tools[name]` | -| `label` | Yes | `String` | Human-readable name for display | -| `description` | Yes | `String` | Shown to LLM for tool selection decisions | -| `inputSchema` | Yes | `Dict{String,Any}` | JSON Schema (MCP format) with `type`, `properties`, `required` | -| `execute` | Yes | `Function` | The actual tool logic (see signature below) | -| `prepareArguments` | No | `Function` | Transforms args before validation; `(args::Dict) -> Dict` | -| `validateRequiredArgs` | No | `Function` | Custom validation; `(args::Dict) -> Union{Nothing, String}` | -| `parallelToolExecute` | No | `Bool` | Default `false`. When `true`, allows parallel execution | - -### Execute Function Signature - -```julia -execute(toolCallId::String, - args::AbstractDict{String, Any}, - signal::Union{Nothing,abortSignal}, - onPartialResult::Function)::agentToolResult -``` - -| Parameter | Description | -|-----------|-------------| -| `toolCallId` | Unique ID from the LLM's tool call (e.g., `"call_abc123"`) | -| `args` | Validated arguments from the LLM, already passed through `prepareArguments` and `validateRequiredArgs` | -| `signal` | Optional `abortSignal` for cancellable operations. Check `signal.aborted` to abort early. | -| `onPartialResult` | Callback for streaming progress: `onPartialResult(partial_data)` emits `toolExecUpdateEvent` | - -### Returns — `agentToolResult` - -**Source:** `type.jl:429-434` - -```julia -struct agentToolResult - content::Vector{messageContent} # Output content (textContent, etc.) - details::Dict{Any,Any} # Tool-specific metadata (e.g., counts, IDs) - usage::Union{llmUsage, Nothing} # Token usage tracking (optional) - terminate::Bool # If true, signals the tool requested loop termination -end -``` - -The `terminate` flag is checked at the batch level. See [Section 10](#10-tool-call-batches--termination-logic) for details. - ---- - -## 4. Tool Registration — Static Registration - -**Source:** `toolRegistry.jl`, `YiemAgent.jl` - -### How `toolStore` Works - -The registry uses **per-agent isolated storage** via the `toolStore` struct. Each agent gets its own store, so tool registration is independent — `registerTool(store, tool)` only affects that agent's tool set. - -```julia -struct toolStore - tools::OrderedDict{String, agentTool} # keyed by name for O(1) lookup + ordered iteration - name::String # identifier for debugging/logs -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. - -### Static Registration — `register_all_tools()` - -```julia -function register_all_tools(store::toolStore)::OrderedDict{String, agentTool} -``` - -**Source:** `YiemAgent.jl:20-29` - -1. **Calls each tool's definition function** — `getWeatherTool()`, `getTimeTool()`, `writeToolTool()` — which return `agentTool` structs -2. **Registers each tool** via `registerTool(store, tool)` -3. **Auto-registers** `listTool(store)` so the LLM can discover available tools at runtime - -### Why Static? - -Tools are **statically included** in `YiemAgent.jl` via `include()`. This means: -- Tool functions live in the `YiemAgent` module, not in dynamically created submodules -- No world-age issues when calling `tool.execute()` (Julia compiles dispatch in the same world) -- Simpler tool definition — no need to wrap in a `module ... end` block -- Better compiler optimization (inlining, type inference) - -### Registration API - -```julia -# Create per-agent stores -store1 = toolStore(name="agent1") -store2 = toolStore(name="agent2") - -# Load all tools (auto-registers listTools) -tools1 = register_all_tools(store1) # all agents get the same tools -tools2 = register_all_tools(store2) - -# Manual registration (per-store) -registerTool(store1, my_tool) - -# Query (returns OrderedDict keyed by tool name, in registration order) -all_tools = getTools(store1) # OrderedDict{String, agentTool} — O(1) lookup + deterministic order - -# Clear (per-store) -clearTools(store1) # only clears store1 -``` - -`getTools(store)` returns the internal `OrderedDict` directly, giving callers: -- O(1) lookup by tool name -- Deterministic iteration order (registration order) -- Consistency with `agentState.tools` (also `OrderedDict{String, agentTool}`) -- No copy overhead — mutations on the returned value affect the store - -### Per-Agent Isolation - -Each `toolStore` is completely independent — tools registered in one store do not appear in another: - -```julia -storeA = toolStore(name="A") -storeB = toolStore(name="B") - -registerTool(storeA, getTimeTool()) -registerTool(storeB, getWeatherTool()) - -getTools(storeA) # only contains getTime -getTools(storeB) # only contains getWeather - -clearTools(storeA) # storeB is unaffected -``` - -This ensures that `yiemAgent` instances with different `tool_store` references operate with completely isolated tool sets. - ---- - -## 5. The Agent Loop — High-Level Flow - -**Source:** `agentCore.jl:35-145` - -The `_agentLoop()` function runs as a background `@spawn` task, created when `yiemAgent` is constructed. - -### Channel Architecture - -``` -yiemAgent struct contains: - - inputChannel (Channel, capacity 16) ← user sends messages here via promptAgent() - - followUpChannel (Channel, capacity 32) ← user sends follow-ups here via followUp() - - outputChannel (Channel, capacity 16) ← agent sends responses here via takeResponse() -``` - -### Loop States - -The loop tracks 6 states (documented at `agentCore.jl:39-75`): - -| State | `processingTask` | `activeRun` | `inputChannel` | `followUpChannel` | Behavior | -|-------|-----------------|-------------|----------------|-------------------|----------| -| 1 | `nothing` | `false` | empty | empty | Idle, waiting | -| 2 | `nothing` | `false` | has msg | empty | New message → spawn `_processMessage` | -| 3 | running | `true` | empty | empty | Processing, no new input | -| 4 | running | `true` | has msg | empty | New message while busy → queued | -| 5 | running | `true` | empty | has msg | Follow-up while busy → queued | -| 6 | done | `false` | empty | empty | Task completed → send result, reset | - -### Loop Logic (simplified) - -```julia -function _agentLoop(agent::yiemAgent) - while true - # 1. Wait for message from inputChannel (blocking poll) - msg = fetch!(agent.inputChannel) # agentCore.jl:84 - - # 2. Handle shutdown signal - if msg === :shutdown - drain both channels, break loop - end - - # 3. If agent is idle, spawn _processMessage - if agent._state.activeRun == false - processingTask = Threads.@spawn _processMessage(agent) - agent._state.activeRun = true - end - - # 4. While processing: check for followUp messages - if istaskdone(processingTask) == false && isready(agent.followUpChannel) - drain followUpChannel → push to inputChannel - continue # wait for current processing to finish - end - - # 5. When processing completes - if istaskdone(processingTask) == true - result = fetch(processingTask) - put!(agent.outputChannel, result) - agent._state.activeRun = false - processingTask = nothing - end - end -end -``` - -**Key design:** The loop always checks `inputChannel` before `followUpChannel`. Follow-up messages are merged into `inputChannel` only when the current processing task is active, ensuring they are processed after the primary message completes but before new input arrives. - ---- - -## 6. Message Processing Pipeline - -**Source:** `agentCore.jl:175-311` - -`_processMessage(agent)` is the core function that processes a batch of user messages through the LLM pipeline. - -### Pipeline Steps - -```julia -function _processMessage(agent::yiemAgent)::assistantMessage - final_response = nothing - - while true # Loop until LLM returns response without tool calls - # ── Step 1: Drain inputChannel ────────────────────────────── - while isready(agent.inputChannel) - raw_msg = take!(agent.inputChannel) - if raw_msg === :shutdown - put!(agent.inputChannel, :shutdown) - break - end - user_msg = OpenAiToUserMessage(raw_msg) # Convert Dict → userMessage - push!(agent._state.messages, user_msg) - end - - # ── Step 2: Prepare context ───────────────────────────────── - state = agentState(systemPrompt, nothing, tools, messages) - preparedContext = prepareContext(state, eventSink) - # Default: deep copies systemPrompt, messages, tools from agentState → agentContext - # Override point: filter tools, inject context, modify system prompt - - # ── Step 3: Format for LLM ────────────────────────────────── - formattedMessages = formatMsgForLLM(preparedContext, eventSink) - # Converts agentContext → Dict("messages" => [...]) in OpenAI format - # Wraps systemPrompt as system role, converts each messageContent block - - # ── Step 4: Call LLM ──────────────────────────────────────── - response = llmCall(formattedMessages) - # Returns assistantMessage with content::Vector{messageContent} - # Each content block has a type: "text", "thinking", or "tool_call" - - # ── Step 5: Extract tool calls ────────────────────────────── - hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response) - # Inspects content blocks for "tool_calls" or "tool_call" Dict entries - - # ── Step 6: Execute tool calls or return ──────────────────── - if hasToolCalls && !isempty(toolCallList) - # Build context and config - context = agentContext(systemPrompt, messages, tools) - config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential") - signal = abortSignal(false) - - # Execute tool calls (sequential or parallel) - batch = executeToolCalls(context, assistant_msg, toolCallList, config, signal, eventSink) - - # Save results to conversation history - for tool_result in batch.messages - push!(messages, tool_result) - end - - # Check termination - if batch.terminate - final_response = build_final_response(batch) - break - end - # Otherwise, loop back to Step 1 (drain any new input) and call LLM again - else - # No tool calls — this is the final response - final_response = response - break - end - end - - return final_response -end -``` - ---- - -## 7. Tool Call Extraction from LLM Response - -**Source:** `agentCore.jl:217-245` - -After the LLM call, the agent inspects `response.content` (a `Vector{messageContent}`) for tool call blocks. Two formats are supported: - -### Format 1: OpenAI `tool_calls` array - -```julia -# Response content block: -Dict( - :type => "tool_calls", - :tool_calls => [ - Dict(:id => "call_1", :name => "getWeather", :arguments => Dict("city" => "Tokyo")), - Dict(:id => "call_2", :name => "getTime", :arguments => Dict("timezone" => "Asia/Tokyo")), - ] -) -``` - -### Format 2: Single `tool_call` block - -```julia -Dict( - :type => "tool_call", - :id => "call_1", - :name => "getWeather", - :arguments => Dict("city" => "Tokyo") -) -``` - -### Extraction Logic - -```julia -tool_call_list = agentToolCall[] - -for content_block in response.content - if content_block isa Dict - # OpenAI format: 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())), # fallback UUID - name = get(tc_data, :function, Dict())[:name], - arguments = get(tc_data, :function, Dict())[:arguments], - ) - push!(tool_call_list, tc) - end - # Alternative format: single tool call - elseif get(content_block, :type, "") == "tool_call" - tc_data = content_block - 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 -``` - -The `agentToolCall` struct is defined at `type.jl:362-367`: - -```julia -struct agentToolCall - type::String # Always "function" - id::String # Unique tool call identifier - name::String # Tool name (matches context.tools keys) - arguments::Dict{String, Any} # Parsed tool arguments -end -``` - ---- - -## 8. The Per-Call Pipeline — Prepare, Execute, Finalize - -This is the core of the tool execution system. Each tool call (whether part of a batch or standalone) goes through exactly three phases. - -### 8.1 Phase 1: Prepare — `prepareToolCall()` - -**Source:** `agentCore.jl:511-547` - -```julia -function prepareToolCall( - context::agentContext, - assistantMsg::assistantMessage, - toolCall::agentToolCall, - config::agentLoopConfig, - signal::Union{Nothing, abortSignal}, -)::Union{preparedToolCall, immediateOutcome} -``` - -**Steps:** - -1. **Resolve tool by name** — `get(context.tools, toolCall.name, nothing)` - - If `nothing` → `immediateOutcome(createErrorToolResult("Tool X not found"), true)` - -2. **Prepare arguments** — `prepareToolCallArguments(tool, toolCall)` - - Calls `tool.prepareArguments(toolCall.arguments)` if defined - - Returns the toolCall with transformed arguments - - If no hook or no change, returns original `toolCall` - -3. **Validate arguments** — `validateToolArguments(tool, prepared)` - - Calls `tool.validateRequiredArgs(prepared.arguments)` if defined, otherwise uses default `validateRequiredArgs(prepared.arguments, tool.inputSchema)` - - Default validator checks `inputSchema["required"]` array - - On failure: throws `ArgumentError(error_string)`, caught by the try-catch below - -4. **Run `beforeToolCall` hook** — if `config.beforeToolCall !== nothing` - - Passes `beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context)` and `signal` - - Hook can return `nothing` (proceed), or `Dict(:block => true, :reason => "...")` (reject) - - If `signal.aborted == true` → `immediateOutcome(createErrorToolResult("Operation aborted"), true)` - - If `before.block == true` → `immediateOutcome(createErrorToolResult(get(before, :reason, "blocked")), true)` - -5. **Return success** → `preparedToolCall(tool, toolCall, validatedArgs)` - -**Source:** `type.jl:681-685` — `preparedToolCall` holds the resolved tool, original call metadata, and validated arguments together. - -**Key design principle:** Preparation **never throws**. Every failure path returns an `immediateOutcome` with `isError=true`, ensuring the agent loop always has a valid result to feed back to the LLM. - -### 8.2 Phase 2: Execute — `executePreparedToolCall()` - -**Source:** `agentCore.jl:589-617` - -```julia -function executePreparedToolCall( - prep::preparedToolCall, - signal::Union{Nothing, abortSignal}, - eventSink, -)::executedOutcome -``` - -**Steps:** - -1. **Call `tool.execute()`:** - ```julia - result = prep.tool.execute( - prep.toolCall.id, - prep.args, - signal, - eventSink # serves as onPartialResult callback - ) - return executedOutcome(result, false) - ``` - -2. **On error:** - ```julia - catch err - return executedOutcome(createErrorToolResult(sprint(showerror, err)), true) - end - ``` - -### 8.3 Phase 3: Finalize — `finalizeExecutedToolCall()` - -**Source:** `agentCore.jl:675-706` - -```julia -function finalizeExecutedToolCall( - context::agentContext, - assistantMsg::assistantMessage, - prep::preparedToolCall, - executed::executedOutcome, - config::agentLoopConfig, - signal::Union{Nothing,abortSignal}, -)::finalizedOutcome -``` - -**Steps:** - -1. **Extract execution result:** - ```julia - result = executed.result - isError = executed.isError - ``` - -2. **Run `afterToolCall` hook** — if `config.afterToolCall !== nothing`: - - Passes `afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context)` and `signal` - - Hook can mutate the result: - ```julia - after = config.afterToolCall(afterToolCallContext(...)) - if after !== nothing - result = merge(result, dict( - :content => get(after, :content, result.content), - :details => get(after, :details, result.details), - :usage => get(after, :usage, result.usage), - :terminate => get(after, :terminate, result.terminate) - )) - isError = get(after, :isError, isError) - end - ``` - - Common use cases: mask sensitive data, normalize usage, flip `terminate` based on business logic - - On error: `result = createErrorToolResult(sprint(showerror, err)); isError = true` - -3. **Return:** - ```julia - return finalizedOutcome(prep.toolCall, result, isError) - ``` - -**Source:** `type.jl:787-791` — `finalizedOutcome` holds the original tool call reference, final result (post-hook), and error status. - ---- - -## 9. Execution Modes — Sequential vs Parallel - -**Source:** `agentCore.jl:795-936, 988-1011` - -### Dispatcher — `executeToolCalls()` - -```julia -function executeToolCalls( - context::agentContext, - assistantMsg::assistantMessage, - toolCalls::Vector{agentToolCall}, - config::agentLoopConfig, - signal::Union{Nothing, abortSignal}, - eventSink, -)::agentToolCallBatch -``` - -**Decision logic** (`agentCore.jl:997-1010`): - -```julia -hasSequential = false -for tc in toolCalls - t = get(context.tools, tc.name, nothing) - if t !== nothing && !t.parallelToolExecute - hasSequential = true - break - end -end - -if config.toolExecution == "sequential" || hasSequential - return executeToolCallsSequential(...) -else - return executeToolCallsParallel(...) -end -``` - -**Rule:** If the global config is `"sequential"` **OR** any tool in the batch has `parallelToolExecute = false`, the entire batch runs sequentially. Sequential is the safe default. - -### Sequential Execution — `executeToolCallsSequential()` - -**Source:** `agentCore.jl:795-829` - -```julia -function executeToolCallsSequential(...)::agentToolCallBatch - finalizedCalls = finalizedOutcome[] - messages = toolResultMessage[] - - for tc in toolCalls - prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink) - - if prep isa immediateOutcome - finalized = finalizedOutcome(tc, prep.result, prep.isError) - else - executed = executePreparedToolCall(prep, signal, eventSink) - finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) - end - - push!(messages, createToolResultMessage(finalized)) - push!(finalizedCalls, finalized) - - if signal !== nothing && signal.aborted - break # abort: skip remaining calls - end - end - - return agentToolCallBatch(messages, shouldTerminate(finalizedCalls)) -end -``` - -### Parallel Execution — `executeToolCallsParallel()` - -**Source:** `agentCore.jl:888-936` - -```julia -function executeToolCallsParallel(...)::agentToolCallBatch - entries = union{finalizedOutcome, task{finalizedOutcome}}[] - - for tc in toolCalls - prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink) - - if prep isa immediateOutcome - finalized = finalizedOutcome(tc, prep.result, prep.isError) - push!(entries, finalized) # immediate outcome — no task - else - task = task() do - executed = executePreparedToolCall(prep, signal, eventSink) - finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config, signal) - return finalized - end - schedule(task) - push!(entries, task) # pending task - end - - if signal !== nothing && signal.aborted - break # abort during preparation — skip remaining - end - end - - # Collect results in original order - finalizedCalls = finalizedOutcome[] - for entry in entries - outcome = entry isa task ? fetch(entry) : entry - push!(finalizedCalls, outcome) - end - - messages = toolResultMessage[] - for f in finalizedCalls - push!(messages, createToolResultMessage(f)) - end - - return agentToolCallBatch(messages, shouldTerminate(finalizedCalls)) -end -``` - -**Key design:** All tool calls are **prepared** sequentially (validation, hooks), then prepared calls are **executed** concurrently as separate tasks. Results are collected in the original call order via `fetch()`. - -**Trade-off:** Parallel execution reduces wall-clock time for independent tools but can overwhelm external resources (rate limits, connection pools, disk I/O). - ---- - -## 10. Tool Call Batches & Termination Logic - -**Source:** `type.jl:793-838` - -### `agentToolCallBatch` - -```julia -struct agentToolCallBatch - messages::Vector{toolResultMessage} # Tool result messages for this batch - terminate::Bool # Whether the batch should terminate the loop -end -``` - -### Termination Logic — `shouldTerminate()` - -**Source:** `agentCore.jl:409-411` - -```julia -function shouldTerminate(batches::Vector{finalizedOutcome})::Bool - return !isempty(batches) && all(b -> b.result.terminate, batches) -end -``` - -**Rule:** `terminate` is `true` **only when every tool in the batch** has `result.terminate == true`. This prevents a single tool that sets `terminate: true` (e.g., for metadata purposes) from accidentally stopping the agent when other tools did not intend to terminate. - -### When to Set `terminate = true` - -From the type documentation (`type.jl:803-815`): - -| Use Case | Description | -|----------|-------------| -| Task completion | A tool like `deploy` or `submit` finishes its work and signals the agent to stop | -| Unrecoverable error | A tool hits a fatal condition (auth token expired, database connection lost) | -| Async handoff | A tool triggers a long-running external operation; the external system will later resume via `continue()` | - -### Batch Processing in `_processMessage()` - -**Source:** `agentCore.jl:266-307` - -```julia -batch = executeToolCalls(context, response, tool_call_list, config, signal, eventSink) - -# Save results to conversation history -for tool_result in batch.messages - push!(agent._state.messages, tool_result) -end - -if batch.terminate - # Build final response from tool results - final_content = [textContent("Tool execution completed.")] - for tool_result in batch.messages - for content_block in tool_result.content - if content_block isa textContent - append!(final_content, [content_block]) - elseif content_block isa Dict - if haskey(content_block, :text) - push!(final_content, textContent(content_block[:text])) - end - end - end - end - final_response = assistantMessage( - role = "assistant", - content = final_content, - api = response.api, - model = response.model, - usage = response.usage, - stopReason = "tool_use_terminated", - errorMessage = if any(x -> x.isError, batch.messages) - "One or more tool calls failed" - else - nothing - end, - timestamp = now(), - ) - break # exit the while loop -end -# If batch.terminate == false, loop back to call LLM again with tool results -``` - ---- - -## 11. Tool Result Message Creation - -**Source:** `agentCore.jl:373-379` - -### `createToolResultMessage()` - -```julia -function createToolResultMessage(f::finalizedOutcome)::toolResultMessage - return toolResultMessage( - "toolResult", # role - f.toolCall.id, # toolCallId - f.toolCall.name, # toolName - f.result.content, # content (Vector{messageContent}) - f.result.details, # details - f.result.usage, # usage - nothing, # addedToolNames (for dynamic tools) - f.isError, # isError - now(), # timestamp - ) -end -``` - -### `toolResultMessage` Struct - -**Source:** `type.jl:152-191` - -```julia -struct toolResultMessage <: agentMessage - role::String # Always "tool" - toolCallId::String # ID matching the original tool call - toolName::String # Name of the executed tool - content::Vector{messageContent} # Tool output content - details::Any # Additional tool-specific details - usage::Union{llmUsage, Nothing} # Token usage if applicable - addedToolNames::Union{Vector{String}, Nothing} # Tools added during execution - isError::Bool # Whether the tool call resulted in an error - timestamp::Timestamp # When the result was recorded -end -``` - -### 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" -``` - -On the next loop iteration, `formatMsgForLLM()` converts `toolResultMessage` to OpenAI format: - -```julia -Dict( - "role" => "tool", - "tool_call_id" => "call_1", - "content" => [Dict("type" => "text", "text" => "Weather in Tokyo: Sunny, 22°C")] -) -``` - ---- - -## 12. Error Handling & Recovery Pattern - -The framework uses a **result-based error handling** pattern instead of exceptions for tool call failures. This ensures the LLM always receives a tool result message, giving it the information to recover. - -### Error Flow - -``` -Tool call fails at any phase - │ - ▼ -┌─────────────────────────┐ -│ Phase: Prepare │ → immediateOutcome(error_result, true) -│ Phase: Execute │ → executedOutcome(error_result, true) -│ Phase: Finalize (hook) │ → finalizedOutcome(tc, error_result, true) -└─────────────────────────┘ - │ - ▼ -createToolResultMessage(finalized) - │ - ▼ -push!(agent._state.messages, toolResultMessage) - │ - ▼ -formatMsgForLLM() → LLM receives error as tool result - │ - ▼ -LLM can: retry with corrected args, report failure, or ask user for clarification -``` - -### `createErrorToolResult()` - -**Source:** `agentCore.jl:339-341` - -```julia -function createErrorToolResult(msg::String)::agentToolResult - return agentToolResult([textContent("text", msg)], Dict{Any,Any}()) -end -``` - -Returns an `agentToolResult` with a single `textContent` block containing the error message. This is wrapped in an `immediateOutcome`, `executedOutcome`, or `finalizedOutcome` depending on where the error occurred, then converted to `toolResultMessage` for the LLM. - -**Key design:** Returning a result instead of throwing allows the LLM to see the error and decide whether to retry, re-issue the call with different arguments, or report failure to the user. - ---- - -## 13. Event System — Tool Lifecycle Events - -**Source:** `type.jl:480-516` - -Each tool call emits a three-event lifecycle: - -``` -toolExecStartEvent → [zero or more toolExecUpdateEvent] → toolExecEndEvent -``` - -### Event Types - -```julia -struct toolExecStartEvent - toolCallId::String # ID of the tool call - toolName::String # Name of the tool - arguments::Dict{String, Any} # Tool arguments -end - -struct toolExecUpdateEvent - toolCallId::String # ID of the tool call - toolName::String # Name of the tool - arguments::Dict{String, Any} # Tool arguments - partialResult::Any # The partial result data -end - -struct toolExecEndEvent - toolCallId::String # ID of the tool call - toolName::String # Name of the tool - result::agentToolResult # The final tool result - isError::Bool # Whether execution resulted in an error -end -``` - -### Event Emission Points - -| Event | Emitted From | When | -|-------|-------------|------| -| `toolExecStartEvent` | `executeToolCalls*()` loop | Before `prepareToolCall()` for each tool call | -| `toolExecUpdateEvent` | `executePreparedToolCall()` | Inside `onPartialResult` callback during `tool.execute()` | -| `toolExecEndEvent` | `finalizeExecutedToolCall()` | After finalization for each tool call | - -### Event Sink - -The `eventSink` function is passed through the entire call chain: - -```julia -eventSink = agent.eventSink # set during yiemAgent construction -``` - -The `eventSink` function is a user-provided callback that receives all events. This is typically used by: -- **TUI (Terminal UI):** Display real-time progress, tool names, results -- **Logging systems:** Record tool execution history -- **Monitoring:** Track tool usage, execution times, error rates -- **Audit trails:** Log all tool calls with arguments and results - ---- - -## 14. Agent Lifecycle Hooks - -### Hook Types - -| Hook | Signature | Called | Purpose | -|------|-----------|--------|---------| -| `prepareContext` | `(state::agentState, eventSink) -> agentContext` | Before each LLM call | Filter tools, inject context, modify system prompt | -| `formatMsgForLLM` | `(ctx::agentContext, eventSink) -> Dict` | After `prepareContext` | Convert to LLM-specific format | -| `llmCall` | `(messages::Dict) -> assistantMessage` | After formatting | Actually invoke the LLM API | -| `beforeToolCall` | `(msgCtx::beforeToolCallContext, signal) -> Union{Nothing, Dict}` | In `prepareToolCall` | Ask for user permission, block execution, abort | -| `afterToolCall` | `(afterToolCallContext::afterToolCallContext, signal) -> Union{Nothing, Dict}` | In `finalizeExecutedToolCall` | Mutate result, mask data, flip `terminate` | -| `eventSink` | `(event) -> nothing` | Throughout lifecycle | Emit events for TUI, logging, monitoring | - -### `beforeToolCall` Hook - -**Source:** `agentCore.jl:530-541` - -```julia -if config.beforeToolCall !== nothing - before = config.beforeToolCall( - beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context), signal - ) - if signal !== nothing && signal.aborted - return immediateOutcome(createErrorToolResult("Operation aborted"), true) - end - if before !== nothing && before.block - return immediateOutcome( - createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true) - end -end -``` - -**Return values:** -- `nothing` — proceed with execution -- `Dict(:block => true, :reason => "...")` — block execution, error fed back to LLM -- `Dict(:block => false)` — proceed (explicit allow) - -### `afterToolCall` Hook - -**Source:** `agentCore.jl:687-703` - -```julia -if config.afterToolCall !== nothing - try - after = config.afterToolCall( - afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context), signal - ) - if after !== nothing - result = merge(result, dict( - :content => get(after, :content, result.content), - :details => get(after, :details, result.details), - :usage => get(after, :usage, result.usage), - :terminate => get(after, :terminate, result.terminate) - )) - isError = get(after, :isError, isError) - end - catch err - result = createErrorToolResult(sprint(showerror, err)) - isError = true - end -end -``` - -**Common use cases:** -- Mask sensitive data from result content before the LLM sees it (e.g., removing API keys from error messages) -- Normalize usage tracking data into a consistent format -- Inspect the result and decide to flip `terminate: true` based on business logic -- Wrap an error result in a friendlier message for the LLM to understand - -### `prepareContext` Hook - -**Source:** `utils.jl:111-125` - -```julia -function prepareContext(state::agentState, eventSink)::agentContext - # TODO: filter tools from state.tools based on user intent - filteredTools = state.tools - - # TODO: add filtered tools to the current system prompt / modify systemPrompt - preparedSystemPrompt = state.systemPrompt - - # TODO: add system prompt, adjust/modify and inject additional context into messages - preparedMessages = deepcopy(state.messages) - - return agentContext(preparedSystemPrompt, preparedMessages, filteredTools) -end -``` - -**Override points for customization:** -- Filter tools based on user intent (e.g., only show "wine" tools when user asks about wine) -- Modify the system prompt dynamically (e.g., inject current time, user preferences) -- Inject additional context (e.g., retrieved documents, current user state) -- Prune or reorder messages before formatting for the LLM - -### `formatMsgForLLM` Hook - -**Source:** `utils.jl:158-219` - -Default implementation converts `agentContext` to OpenAI-compatible format: - -```julia -function formatMsgForLLM(ctx::agentContext, eventSink)::Dict{String, Any} - messages = Vector{Dict{String, Any}}() - - # System prompt as system message - if !isempty(ctx.systemPrompt) - push!(messages, Dict( - "role" => "system", - "content" => [Dict("type" => "text", "text" => ctx.systemPrompt)] - )) - end - - # Conversation messages - for msg in ctx.messages - if msg isa userMessage - push!(messages, _userMessageToOpenAI(msg)) - elseif msg isa assistantMessage - push!(messages, _assistantMessageToOpenAI(msg)) - elseif msg isa toolResultMessage - push!(messages, _toolResultMessageToOpenAI(msg)) - end - end - - return Dict("messages" => messages) -end -``` - -Override this to produce custom LLM message formats for different APIs/providers (e.g., Anthropic, Google, Ollama). - ---- - -## 15. Self-Modifying Tools - -The framework supports tools that modify the tool system itself at runtime. - -### `writeTool` — Create New Tool Files - -**Source:** `tools/writeTool.jl` - -`writeTool` is a **file writer**, not a code generator. The LLM provides the tool logic as `executeCode` (Julia code body), and `writeTool` wraps it in Julia boilerplate: -1. Converts `inputSchema` Dict into `Dict{String,Any}(...)` string literal -2. Indents `executeCode` with 4 spaces -3. Wraps it inside `function executeTool(...)::agentToolResult ... end` -4. Appends `writeToolTool()` returning an `agentTool` struct -5. Writes the combined string to `src/tools/.jl` - -### `listTool` — Discover Available Tools - -**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. `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 - -``` -1. Agent detects no existing tool handles the user's request -2. Agent calls writeTool with: - - name: "searchWine" - - label: "Wine Search" - - description: "Search a wine database..." - - inputSchema: { ... } - - executeCode: "query = args[\"query\"]\nresult = search(query)\n..." - - (optional) validateCode, prepareCode -3. writeTool generates src/tools/searchWine.jl -4. Developer adds `include("tools/searchWine.jl")` to YiemAgent.jl -5. Developer adds `registerTool(store, searchWineTool())` to register_all_tools() in YiemAgent.jl -6. Developer restarts Julia — new tool is loaded -7. Agent calls searchWine(query="cabernet") -8. Result: "Found 5 cabernet wines..." -``` - -### `writeTool` Input Schema - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `name` | `String` | Yes | Valid Julia identifier (letters, digits, underscores) | -| `label` | `String` | Yes | Human-readable tool name | -| `description` | `String` | Yes | What the tool does | -| `inputSchema` | `Dict` | Yes | JSON Schema in MCP format | -| `executeCode` | `String` | Yes | Julia code for `executeTool` body (NOT wrapped in function) | -| `validateCode` | `String` | No | Custom validation Julia code | -| `prepareCode` | `String` | No | Argument preparation code | -| `parallel` | `Bool` | No | Whether the tool can run in parallel (default: `false`) | - ---- - -## 16. Complete End-to-End Example - -### Full Lifecycle: User Message to Tool Result - -``` -USER SENDS MESSAGE - └─> promptAgent(agent, "What's the weather in Tokyo?") - └─> put!(agent.inputChannel, Dict("role" => "user", "content" => [...])) - - -LOOP ITERATION 1 — LLM DECIDES TO USE A TOOL - └─> _agentLoop: detects msg in inputChannel - └─> @spawn _processMessage(agent) - - ── _processMessage ────────────────────────────────────────────── - │ - │ Step 1: Drain inputChannel - │ raw_msg = Dict("role" => "user", "content" => [...]) - │ user_msg = OpenAiToUserMessage(raw_msg) - │ push!(agent._state.messages, user_msg) - │ - │ Step 2: prepareContext - │ ctx = agent.prepareContext(agent._state) - │ → agentContext(systemPrompt, messages, tools) - │ - │ Step 3: formatMsgForLLM - │ formatted = agent.formatMsgForLLM(ctx) - │ → Dict("messages" => [ - │ Dict("role" => "system", "content" => [...]), - │ Dict("role" => "user", "content" => [...]), - │ ]) - │ - │ Step 4: llmCall - │ response = agent.llmCall(formatted) - │ → assistantMessage(content = [ - │ Dict(:type => "tool_calls", :tool_calls => [ - │ Dict(:id => "call_1", :name => "getWeather", - │ :arguments => Dict("city" => "Tokyo")) - │ ]) - │ ]) - │ - │ Step 5: Extract tool calls - │ tool_call_list = [agentToolCall("function", "call_1", "getWeather", ...)] - │ - │ Step 6: Execute tool calls - │ context = agentContext(systemPrompt, messages, tools) - │ config = agentLoopConfig(beforeToolCall, afterToolCall, "sequential") - │ batch = executeToolCalls(context, response, tool_call_list, config, signal, eventSink) - - -LOOP ITERATION 1 — executeToolCallsSequential - │ - │ For tc = agentToolCall("call_1", "getWeather", ...): - │ - │ PREPARE: - │ tool = context.tools["getWeather"] → found! - │ validatedArgs = validateToolArguments(tool, tc) - │ → validateRequiredArgs(Dict("city" => "Tokyo"), inputSchema) → passes - │ beforeToolCall_hook(...) → nothing (skipped) - │ → preparedToolCall(tool, tc, {"city" => "Tokyo"}) - │ - │ EXECUTE: - │ result = tool.execute("call_1", {"city" => "Tokyo"}, nothing, eventSink) - │ → agentToolResult([textContent("Weather in Tokyo: Sunny, 22°C")], {}, nothing, false) - │ → executedOutcome(result, false) - │ - │ FINALIZE: - │ afterToolCall_hook(...) → nothing (skipped) - │ → finalizedOutcome(tc, result, false) - │ - │ msg = createToolResultMessage(finalized) - │ → toolResultMessage("tool", "call_1", "getWeather", [...], {}, nothing, [], false, ts) - │ - └─> agentToolCallBatch([msg], false) - │ - │ Save results: - │ for tool_result in batch.messages - │ push!(agent._state.messages, tool_result) - │ end - │ - │ batch.terminate == false → loop back to Step 1 - │ - - -LOOP ITERATION 2 — LLM RETURNS FINAL TEXT RESPONSE - ── _processMessage (second iteration) ─────────────────────────── - │ - │ Step 1: Drain inputChannel → empty - │ - │ Step 2-3: prepareContext → formatMsgForLLM - │ → messages now include: - │ [system] "You are a helpful assistant." - │ [user] "What's the weather in Tokyo?" - │ [tool] tool_call_id="call_1", content="Weather in Tokyo: Sunny, 22°C" - │ - │ Step 4: llmCall - │ → assistantMessage(content = [Dict(:type => "text", :text => "The weather in Tokyo is sunny, 22°C.")]) - │ - │ Step 5: Extract tool calls → none - │ - │ Step 6: has_tool_calls == false → break, return final_response - │ - └─> return final_response - - -AGENT LOOP: SEND RESPONSE TO USER - └─> put!(agent.outputChannel, final_response) - └─> takeResponse(agent) → assistantMessage("The weather in Tokyo is sunny, 22°C.") -``` - ---- - -## 17. Tool File Contract - -Each `.jl` file in `src/tools/` follows a flat, static structure: - -### Required Function - -```julia -function Tool()::agentTool - # Must return an agentTool instance -end -``` - -### Optional Functions - -```julia -# Argument preparation (before validation) -function PrepareArguments(args::AbstractDict{String, Any})::AbstractDict{String, Any} - # Return modified args, or args unchanged - return args -end - -# Custom validation (before execution) -function ValidateRequiredArgs(args::AbstractDict{String, Any})::Union{Nothing,String} - # Return nothing to pass, or error string to fail - return nothing -end - -# Core execution -function Execute(toolCallId::String, - args::AbstractDict{String, Any}, - signal::Union{Nothing,abortSignal}, - onPartialResult::Function)::agentToolResult - # Return agentToolResult with content, details, usage, terminate - return agentToolResult([textContent("result")], Dict{Any,Any}(), nothing, false) -end -``` - -### File Structure - -```julia -# src/tools/myTool.jl - -using .type # ← provides agentTool, textContent, agentToolResult, etc. -using Dates # ← tool's own dependencies - -# Optional: helper functions -function helper_function(...) - ... -end - -# Optional: prepareArguments -function myToolPrepareArguments(args::AbstractDict{String, Any})::AbstractDict{String, Any} - return args -end - -# Optional: validateRequiredArgs -function myToolValidateRequiredArgs(args::AbstractDict{String, Any})::Union{Nothing,String} - return nothing -end - -# Required: execute function -function myToolExecute(toolCallId::String, args::AbstractDict{String, Any}, - signal::Union{Nothing,abortSignal}, - onPartialResult::Function)::agentToolResult - ... -end - -# Required: getTool function -function myToolTool()::agentTool - return agentTool( - name = "myTool", - label = "My Tool", - description = "What this tool does", - inputSchema = Dict{String,Any}( - "type" => "object", - "properties" => Dict(...), - "required" => [...] - ), - execute = myToolExecute, - prepareArguments = myToolPrepareArguments, - validateRequiredArgs = myToolValidateRequiredArgs, - parallelToolExecute = false - ) -end -``` - -### Dependencies - -Each tool file declares its own dependencies via `using` statements: - -```julia -# src/tools/getTime.jl -using .type -using Dates - -function getTimeExecute(...) - now() # Dates.now requires `using Dates` -end -``` - -```julia -# src/tools/myApiTool.jl -using .type -using HTTP, JSON - -function myApiToolExecute(...) - response = HTTP.get("https://api.example.com") - data = JSON.parse(String(response.body)) - ... -end -``` - -### Why Flat Modules? - -All tool files are **statically included** in `YiemAgent.jl` via `include()`. This means: -- All functions live in the `YiemAgent` module, avoiding world-age issues -- `using .type` makes core types (`agentTool`, `textContent`, `agentToolResult`, `abortSignal`) available -- Functions are named with a `` prefix to avoid name collisions (e.g., `getWeatherExecute`, `getTimeExecute`) -- The `...Tool()` function (e.g., `getWeatherTool()`) returns the `agentTool` struct for registration - ---- - -## 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::AbstractDict{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 - -| Type | Source | Description | -|------|--------|-------------| -| `messageContent` | `type.jl:67` | Abstract base for message content | -| `textContent` | `type.jl:69` | Plain text content (`text::String`) | -| `imageContent` | `type.jl:73` | Image content (`data::String`, `mimeType::String`) | -| `agentMessage` | `type.jl:82` | Abstract base for all messages | -| `userMessage` | `type.jl:84` | User message (`role`, `content`, `timestamp`) | -| `assistantMessage` | `type.jl:111` | LLM response (`role`, `content`, `api`, `provider`, `model`, `usage`, `stopReason`, `errorMessage`, `timestamp`) | -| `toolResultMessage` | `type.jl:152` | Tool result (`role`, `toolCallId`, `toolName`, `content`, `details`, `usage`, `addedToolNames`, `isError`, `timestamp`) | - -### Tool Types - -| Type | Source | Description | -|------|--------|-------------| -| `agentTool` | `type.jl:261` | Tool definition (name, label, description, schema, execute, hooks) | -| `agentToolCall` | `type.jl:362` | Tool call from LLM (type, id, name, arguments) | -| `agentToolResult` | `type.jl:429` | Tool execution result (content, details, usage, terminate) | -| `agentToolCallBatch` | `type.jl:835` | Batch of tool results (messages, terminate) | - -### Lifecycle Outcome Types - -| Type | Source | Description | -|------|--------|-------------| -| `preparedToolCall` | `type.jl:681` | After prepare: (tool, toolCall, args) | -| `immediateOutcome` | `type.jl:719` | Failed before execution: (result, isError) | -| `executedOutcome` | `type.jl:753` | After execute, before finalize: (result, isError) | -| `finalizedOutcome` | `type.jl:787` | After all phases: (toolCall, result, isError) | - -### Context & Config Types - -| Type | Source | Description | -|------|--------|-------------| -| `agentContext` | `type.jl:299` | Conversation snapshot (systemPrompt, messages, tools) | -| `agentState` | `type.jl:310` | Mutable runtime state (systemPrompt, model, tools, messages, pendingToolCalls, activeRun, errorMessage) | -| `agentLoopConfig` | `type.jl:403` | Loop config (beforeToolCall, afterToolCall, toolExecution) | -| `abortSignal` | `type.jl:416` | Abort flag (`aborted::Bool`) | -| `beforeToolCallContext` | `type.jl:445` | Context for beforeToolCall (message, toolCall, args, context) | -| `afterToolCallContext` | `type.jl:463` | Context for afterToolCall (message, toolCall, args, result, isError, context) | - -### Event Types - -| Type | Source | Description | -|------|--------|-------------| -| `toolExecStartEvent` | `type.jl:480` | (toolCallId, toolName, arguments) | -| `toolExecUpdateEvent` | `type.jl:495` | (toolCallId, toolName, arguments, partialResult) | -| `toolExecEndEvent` | `type.jl:511` | (toolCallId, toolName, result, isError) | - -### Agent Types - -| Type | Source | Description | -|------|--------|-------------| -| `agent` | `type.jl:522` | Abstract base type | -| `yiemAgent` | `type.jl:527` | High-level agent wrapper (state, channels, callbacks, task) | diff --git a/my_AI_prompt.md b/my_AI_prompt.md new file mode 100644 index 0000000..b9d0f8b --- /dev/null +++ b/my_AI_prompt.md @@ -0,0 +1 @@ +read this code \ No newline at end of file diff --git a/src/agentCore.jl b/src/agentCore.jl index b6f33d4..b587e67 100644 --- a/src/agentCore.jl +++ b/src/agentCore.jl @@ -44,81 +44,8 @@ mutable struct yiemAgent <: agent # High-level agent wrapper # Each block has a type — "text", "thinking", or "toolCall". # The code filters for type === "toolCall" blocks, then passes them to executeToolCalls(). llmCall - - # Callable struct for MCP server communication. - # Called as: mcpServer("tools/list") → returns JSON-RPC 2.0 parsed response - # mcpServer("tools/call", toolName, arguments) → returns JSON-RPC 2.0 parsed response - # - # # Example (weather tool) - # # User provides a callable struct - # struct MyMCPClient - # natsConn::NATS.Connection - # topic::String - # senderID::String - # fileserver_url::String - # end - # - # function (c::MyMCPClient)(method::String) - # payload = Dict("jsonrpc" => "2.0", "id" => 1, "method" => method, "params" => Dict{String, Any}()) - # payloads = [("payload", payload, "dictionary")] - # _, msg_envelope_json_str = msghandler.smartpack( - # c.topic, payloads; sender_id=c.senderID, - # msg_purpose="mcp_tools_list", fileserver_url=c.fileserver_url) - # reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180) - # incoming_env = msghandler.smartunpack(String(reply.payload)) - # return incoming_env["payloads"][1][2] - # end - # - # function (c::MyMCPClient)(method::String, toolName::String, arguments::AbstractDict{String, Any}) - # payload = Dict("jsonrpc" => "2.0", "id" => 2, "method" => method, - # "params" => Dict("name" => toolName, "arguments" => arguments)) - # payloads = [("payload", payload, "dictionary"),] - # _, msg_envelope_json_str = msghandler.smartpack( - # c.topic, payloads; sender_id=c.senderID, - # msg_purpose="mcp_tool_call", fileserver_url=c.fileserver_url) - # reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180) - # incoming_env = msghandler.smartunpack(String(reply.payload)) - # return incoming_env["payloads"][1][2] - # end - # - # # "tools/list" input: - # mcpServer("tools/list") - # # sending out payload before smart packed by msghandler: - # Dict("jsonrpc" => "2.0", "id" => 1, "method" => "tools/list", "params" => Dict{String, Any}()) - # # expected return after smart unpacked by msghandler: - # Dict( - # "jsonrpc" => "2.0", "id" => 1, - # "result" => Dict( - # "tools" => [ - # Dict( - # "name" => "getWeather", - # "description" => "Fetch current weather for a city.", - # "inputSchema" => Dict("type"=>"object", - # "properties" => Dict("city"=>Dict("type"=>"string", "description"=>"City name"), - # "units"=>Dict("type"=>"string", "enum"=>["celsius","fahrenheit"], "default"=>"celsius")), - # "required" => ["city"]) - # ) - # ], - # "nextCursor" => nothing - # ) - # ) - # - # # "tools/call" input: - # mcpServer("tools/call", "getWeather", Dict("city"=>"Tokyo", "units"=>"celsius")) - # # sending out payload before smart packed by msghandler: - # Dict("jsonrpc" => "2.0", "id" => 2, "method" => "tools/call", - # "params" => Dict("name" => "getWeather", "arguments" => Dict("city"=>"Tokyo", "units"=>"celsius"))) - # # expected return after smart unpacked by msghandler (success): - # Dict("jsonrpc" => "2.0", "id" => 2, - # "result" => Dict("content" => [{"type" => "text", "text" => "Weather in Tokyo: Sunny, 22°C"}], - # "isError" => false)) - # # expected return after smart unpacked by msghandler (tool error): - # Dict("jsonrpc" => "2.0", "id" => 2, - # "result" => Dict("content" => [{"type" => "text", "text" => "Error: City 'Atlantis' not found."}], - # "isError" => true)) - # # expected return after smart unpacked by msghandler (protocol error): - # Dict("jsonrpc" => "2.0", "id" => 2, - # "error" => Dict("code" => -32602, "message" => "Missing required argument 'city'")) + + # check input and output format at MCP server doc mcpServer # Callback invoked before executing a tool call (ask for user permission/confirmation/abort, etc..) @@ -1101,7 +1028,7 @@ function executePreparedToolCall( )::executedOutcome eventSink("executePreparedToolCall 1") - try #WORKING + try #WORKING pass sessionId from yiemAgent to tool call result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink) eventSink("executePreparedToolCall 2") eventSink("tool execute raw result: " * sprint(show, result)) diff --git a/src/api.jl b/src/api.jl index 7bc924e..2d06848 100644 --- a/src/api.jl +++ b/src/api.jl @@ -38,6 +38,11 @@ function promptAgent(agent::yiemAgent, msg::AbstractDict{String, Any}) return agent end +function promptAgent(agent::yiemAgent, text::String) + put!(agent.inputChannel, Dict("role" => "user", "content" => text)) + return agent +end + """ Take a response from the agent's output channel. @@ -91,6 +96,11 @@ function followUp(agent::yiemAgent, msg::AbstractDict{String, Any}) return agent end +function followUp(agent::yiemAgent, text::String) + put!(agent.followUpChannel, Dict("role" => "user", "content" => text)) + return agent +end + """ Gracefully stop the agent. diff --git a/src/toolRegistry.jl b/src/toolRegistry.jl index 5b870ec..9fbe6bc 100644 --- a/src/toolRegistry.jl +++ b/src/toolRegistry.jl @@ -60,48 +60,48 @@ function _wrap_mcp_tool(mcpserver, tool_def::AbstractDict{String, Any}; eventSin end return agentTool( - name=name, - label=title, - description=desc, - inputSchema=params, - execute=(toolCallId::String, args::AbstractDict{String, Any}, - signal::Union{Nothing,abortSignal}, - eventSink) -> begin - try - response = mcpserver("tools/call", name, args, sessionId) + name=name, + label=title, + description=desc, + inputSchema=params, + execute=(toolCallId::String, args::AbstractDict{String, Any}, + signal::Union{Nothing,abortSignal}, + eventSink) -> begin + try + response = mcpserver("tools/call", name, args, sessionId) - # Parse JSON-RPC 2.0 response envelope - if haskey(response, "error") - rpc_error = response["error"] - err_msg = get(rpc_error, "message", "Unknown MCP error") - return agentToolResult( - [textContent("MCP error: $err_msg")], - Dict{Any,Any}("error" => err_msg), - nothing, false - ) - end + # Parse JSON-RPC 2.0 response envelope + if haskey(response, "error") + rpc_error = response["error"] + err_msg = get(rpc_error, "message", "Unknown MCP error") + return agentToolResult( + [textContent("MCP error: $err_msg")], + Dict{Any,Any}("error" => err_msg), + nothing, false + ) + end - result_data = get(response, "result", response) - content_text = _extract_text_content(result_data) - is_error = get(result_data, "isError", false) + result_data = get(response, "result", response) + content_text = _extract_text_content(result_data) + is_error = get(result_data, "isError", false) - return agentToolResult( - [textContent(content_text)], - Dict{Any,Any}("isError" => is_error), - nothing, false - ) - catch e - errMsg = sprint(showerror, e) - return agentToolResult( - [textContent("MCP call error: $errMsg")], - Dict{Any,Any}("error" => errMsg), - nothing, false - ) - end - end, - prepareArguments=nothing, - validateRequiredArgs=nothing, - parallelToolExecute=false, + return agentToolResult( + [textContent(content_text)], + Dict{Any,Any}("isError" => is_error), + nothing, false + ) + catch e + errMsg = sprint(showerror, e) + return agentToolResult( + [textContent("MCP call error: $errMsg")], + Dict{Any,Any}("error" => errMsg), + nothing, false + ) + end + end, + prepareArguments=nothing, + validateRequiredArgs=nothing, + parallelToolExecute=false, ) end @@ -139,41 +139,41 @@ function register_mcp_tools(tools::OrderedDict{String, agentTool}, mcpserver; ev cursor = get(response, "nextCursor", nothing) for tool_def in tools_array - name = tool_def["name"] - if haskey(tools, name) - continue - end + name = tool_def["name"] + if haskey(tools, name) + continue + end - wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId) - tools[name] = wrapped - new_count += 1 + wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId) + tools[name] = wrapped + new_count += 1 end # Paginate: fetch remaining tools if nextCursor is present while cursor !== nothing && cursor !== "" - response = sessionId !== nothing ? mcpserver("tools/list", sessionId) : mcpserver("tools/list") - if haskey(response, "result") - response = response["result"] - end - tools_array = get(response, "tools", Any[]) - cursor = get(response, "nextCursor", nothing) - for tool_def in tools_array - name = tool_def["name"] - if haskey(tools, name) - continue - end - wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId) - tools[name] = wrapped - new_count += 1 + response = sessionId !== nothing ? mcpserver("tools/list", sessionId) : mcpserver("tools/list") + if haskey(response, "result") + response = response["result"] + end + tools_array = get(response, "tools", Any[]) + cursor = get(response, "nextCursor", nothing) + for tool_def in tools_array + name = tool_def["name"] + if haskey(tools, name) + continue end + wrapped = _wrap_mcp_tool(mcpserver, tool_def, eventSink=eventSink, sessionId=sessionId) + tools[name] = wrapped + new_count += 1 + end end println("[toolRegistry] Discovered $new_count MCP tools. Total: $(length(tools))") catch e bt = catch_backtrace() err_msg = sprint() do io - showerror(io, e, bt) - println(io) + showerror(io, e, bt) + println(io) end eventSink(err_msg) diff --git a/src/utils.jl b/src/utils.jl index 515ee28..9c4c52c 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -96,7 +96,7 @@ or pruning and reordering messages before formatting and calling the LLM. messages, and tools to be sent to the LLM # Examples -```julia +```jldoctest # Default: returns an agentContext with deep copies of system prompt, messages, and tools prepareContext(state).messages == deepcopy(state.messages) @@ -146,7 +146,7 @@ LLM message formats for different APIs/providers. an array of OpenAI-format message dicts # Examples -```julia +```jldoctest # Default output: formatMsgForLLm(ctx) == Dict("messages" => [ Dict("role" => "system", "content" => [...]), @@ -265,7 +265,7 @@ Common use cases: - `nothing` is treated as allow (equivalent to `beforeToolCallResult(false, "N/A")`). # Example -```julia +```jldoctest function beforeToolCall(context::beforeToolCallContext, signal::abortSignal) if context.toolCall.name == "deleteFile" # Block file deletion unless explicitly approved @@ -320,11 +320,106 @@ function afterToolCall(context::afterToolCallContext, signal::abortSignal end -#TODO -function eventSink(x) - +""" +Create an event sink for logging messages and/or publishing to NATS. + +# Fields +- `natsConn`: NATS connection, or `nothing` for file-only sink +- `topic`: NATS topic to publish to, or `nothing` for file-only sink +- `service_name`: Optional prefix added to log entries +- `log_file`: Path to the log file +- `max_log_entries`: Maximum number of entries to keep in the log file + +# Callable interface +```julia + (es::eventSink)(msg::String; log=true, publish=true) -> Nothing +``` +Write a timestamped log entry to file, publish to NATS, or both. + +# Examples +```julia +using NATS + +conn = NATS.connect() + +# With NATS — logs to file and publishes to "agent.events" +es = eventSink(natsConn=conn, topic="agent.events", service_name="agent1") +es("agent started") # both file + NATS (default) +es("task done"; log=false) # NATS only +es("checkpoint"; publish=false) # file only + +# File only — no NATS connection +es2 = eventSink(log_file="./agent.log", service_name="worker") +es2("ready") # file only + +NATS.drain(conn) +``` +""" +struct eventSink + natsConn::Union{NATS.Connection, Nothing} + topic::Union{String, Nothing} + service_name::String + log_file::String + max_log_entries::Int end +eventSink(; natsConn=nothing, topic=nothing, service_name="", log_file="./log/eventSink.md", max_log_entries=100) = + eventSink(natsConn, topic, service_name, log_file, max_log_entries) + +""" + rotate!(es::eventSink) -> Nothing + +Rewrite the log file keeping only the last `max_log_entries` entries. +Call after writing when the entry count reaches the limit. +""" +function rotate!(es::eventSink) + content = read(es.log_file, String) + entries = split(content, r"- \[", keepempty=false) + open(es.log_file, "w") do f + write(f, "- [") + for e in entries[2:end] + write(f, "- [", e) + end + end +end + +""" + (es::eventSink)(msg::String; log=true, publish=true) -> Nothing + +Write a timestamped log entry to file, publish to NATS, or both. + +# Arguments +- `msg`: The message to log or publish. +- `log`: If `true`, append the entry to `es.log_file` (default `true`). +- `publish`: If `true`, publish to the NATS topic (default `true`; no-op if no NATS connection was provided). +""" +function (es::eventSink)(msg::String; log::Bool=true, publish::Bool=true) + timestamp = Dates.format(now(), "yyyy-mm-ddTHH:MM:SS.sss") + svc_prefix = isempty(es.service_name) ? "" : "[$(es.service_name)] " + entry = "- [$timestamp] $svc_prefix$msg\n" + if log + log_dir = dirname(es.log_file) + mkpath(log_dir) + open(es.log_file, "a") do f + write(f, entry) + end + # Rotate after write — keep only last max_log_entries entries + content = read(es.log_file, String) + entries = split(content, r"- \[", keepempty=false) + if length(entries) > es.max_log_entries + rotate!(es) + end + end + if publish && es.topic !== nothing && es.natsConn !== nothing + try + NATS.publish(es.natsConn, es.topic, entry) + catch e + @warn "eventSink NATS publish failed" exception=e + end + end +end + + """ Convert a userMessage to OpenAI message format. @@ -432,7 +527,7 @@ Convert an OrderedDict of agentTool definitions into OpenAI function tool format Returns an empty vector when `tools` is `nothing` or empty. # Examples -```julia +```jldoctest _toolsToOpenAI(nothing) # => Dict{String, Any}[] _toolsToOpenAI(tools) # => [Dict("type" => "function", "function" => Dict("name" => "getWeather", ...))] ``` @@ -478,7 +573,7 @@ coercion, format validation, cross-field constraints). - `String` error message listing missing fields otherwise # Examples -```julia +```jldoctest schema = Dict("required" => ["city"]) args = Dict{String,Any}() validateRequiredArgs(args, schema) # => "Missing required arguments: city" @@ -533,7 +628,7 @@ false `toolExecutionStart` events. and converted to an `immediateOutcome` # Examples -```julia +```jldoctest # With validateRequiredArgs hook set on the tool validateToolArguments(toolWithHook, tc) # => validated args or throws