Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e561b4b1ae | |||
| 9b593675c7 | |||
| e2b266fb3d | |||
| 390b9d71b6 | |||
| d26e1769df | |||
| b3ae0a9c50 | |||
| 1f3526b996 | |||
| 471da0c5ec | |||
| c355748e6f | |||
| f6895984d6 | |||
| 4ac5b97d53 | |||
| 6db50b6a52 | |||
| 096baad09d | |||
| c1120d811a |
+128
-96
@@ -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 = "43252cf86e6030e836ede74c563b58ab2f0f0548"
|
||||
|
||||
[[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 = "4d7ff3b02a9cbd6d4f65fea43551b0bca6a83a22"
|
||||
repo-rev = "main"
|
||||
repo-url = "https://git.yiem.cc/ton/GeneralUtils"
|
||||
uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
|
||||
version = "0.5.11"
|
||||
version = "0.6.9"
|
||||
|
||||
[[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", "HTTP", "JSON", "LibPQ", "NATS", "PrettyPrinting", "Random", "Revise", "Serialization", "URIs", "UUIDs", "msghandler"]
|
||||
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"]
|
||||
|
||||
+2
-2
@@ -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.9"
|
||||
HTTP = "2.4.0"
|
||||
JSON = "1.6.1"
|
||||
NATS = "0.1.0"
|
||||
msghandler = "1.2.1"
|
||||
msghandler = "1.4.0"
|
||||
|
||||
@@ -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 |
|
||||
|
||||
-1599
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
||||
# YiemAgent: Agile Agent Framework
|
||||
|
||||
## 1. Business Context & Success Metrics
|
||||
|
||||
### Business Goal
|
||||
|
||||
YiemAgent is a Julia-based AI agent framework that enables building chatbot assistants with tool-calling capabilities, model-agnostic LLM integration, and runtime tool discovery via MCP (Model Context Protocol). It provides an asynchronous agent loop with channel-based message passing, supporting both single-turn and multi-turn conversations with automatic tool call resolution.
|
||||
|
||||
### User Stories
|
||||
|
||||
- **US-001**: As a developer, I want to create an AI agent with a simple constructor so that I can quickly prototype chatbot functionality in Julia
|
||||
- **US-002**: As a developer, I want the agent to discover tools automatically from an MCP server so that I don't need to manually register tool APIs
|
||||
- **US-003**: As a developer, I want the agent to handle tool calls automatically in a loop so that the LLM can use tools without external orchestration
|
||||
- **US-004**: As a developer, I want to intercept tool calls with before/after hooks so that I can add user approval, logging, or result sanitization
|
||||
- **US-005**: As a developer, I want the framework to support multiple LLM models and providers through a pluggable `llmCall` interface so that I can swap models without changing agent logic
|
||||
|
||||
### KPIs & Targets
|
||||
|
||||
- **KPI-001**: Agent loop processes messages with <500ms overhead for sequential tool calls (validates US-003)
|
||||
- **KPI-002**: MCP tool discovery completes within 2 seconds for 50+ tools (validates US-002)
|
||||
- **KPI-003**: 100% of message types have OpenAI format conversion (validates US-001)
|
||||
|
||||
## 2. Technical Boundaries
|
||||
|
||||
### In Scope
|
||||
|
||||
- Julia module implementing an AI agent with asynchronous message processing
|
||||
- Channel-based input (user→agent), follow-up (async queue), and output (agent→user) interfaces
|
||||
- LLM model abstraction with tool call extraction from multiple response formats (OpenAI content blocks, LMStudio/vLLM format)
|
||||
- Tool registry with MCP server integration (tools/list, tools/call via JSON-RPC 2.0)
|
||||
- Agent loop with before/after tool call hooks for lifecycle management
|
||||
- Message type system with OpenAI format conversion utilities
|
||||
- Abort signal and termination flag support via tool implementations
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- GUI/CLI interface (exposed via channels only; UI layer is external)
|
||||
- Persistent chat history or long-term memory (TODO comments in code)
|
||||
- Multi-agent orchestration (single agent instance per `yiemAgent` struct)
|
||||
- Embedding generation or retrieval-augmented generation (RAG)
|
||||
- Natural language-to-code generation or execution sandboxes
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **Julia Standard Library**: `Dates`, `UUIDs`, `Base.Threads`
|
||||
- **External Packages**: `JSON`, `DataStructures`, `HTTP`, `PrettyPrinting`, `Serialization`, `DataFrames`
|
||||
- **Internal Package**: `GeneralUtils` (project-specific utility library)
|
||||
- **Optional**: `NATS` (MCP server communication), `LibPQ` (PostgreSQL)
|
||||
|
||||
### Deployment Constraints
|
||||
|
||||
- **NFR-001**: Library designed for embedded use in Docker containers (MicroK8s cluster, per `NFR-501` from template)
|
||||
- **NFR-002**: Must support Julia 1.9+ for module include/export semantics compatibility
|
||||
- **NFR-003**: Thread-safe channel operations; single-agent instance processes messages sequentially on background `@spawn` task
|
||||
|
||||
## 3. Functional Requirements (FR)
|
||||
|
||||
### FR-001: Agent Message Processing
|
||||
|
||||
The agent shall process user messages through an asynchronous loop that:
|
||||
- Accepts messages via `inputChannel` (capacity 16) and `followUpChannel` (capacity 32)
|
||||
- Prioritizes `inputChannel` messages over `followUpChannel` messages
|
||||
- Feeds responses to `outputChannel` (capacity 16) in OpenAI message format
|
||||
- Exits cleanly on `:shutdown` signal
|
||||
|
||||
**Traceability**: US-001, US-003
|
||||
|
||||
### FR-002: LLM Model Agnosticism
|
||||
|
||||
The agent shall accept a pluggable `llmCall` function that:
|
||||
- Receives formatted messages and returns an LLM response dict
|
||||
- The response dict may contain `message.tool_calls` (LMStudio format) or `content` blocks (OpenAI format)
|
||||
- The framework extracts tool calls regardless of response format
|
||||
|
||||
**Traceability**: US-005
|
||||
|
||||
### FR-003: Tool Call Execution Loop
|
||||
|
||||
The agent shall automatically execute tool calls returned by the LLM:
|
||||
- Extract tool calls via `_extractToolCalls()` which supports both `message.tool_calls` and `content[{type="tool_calls"}]` formats
|
||||
- Build `agentContext` via `prepareContext()` (pluggable hook)
|
||||
- Format messages via `formatMsgForLLM()` to OpenAI format
|
||||
- Extract tool calls → loop: execute tool calls → append results to history → call LLM again
|
||||
- Break loop when LLM response has no tool calls
|
||||
- Support `terminate` flag: if `toolResultBatch.terminate == true`, break loop and return final response
|
||||
|
||||
**Traceability**: US-003
|
||||
|
||||
### FR-004: Tool Discovery and Registration
|
||||
|
||||
The agent shall:
|
||||
- Query MCP server via `mcpserver("tools/list")` (JSON-RPC 2.0) to discover tools
|
||||
- Support pagination via `nextCursor` field
|
||||
- Wrap each MCP tool definition as an `agentTool` with `execute` hook that calls `mcpserver("tools/call", name, args, sessionId)`
|
||||
- Register a builtin `listTools` tool that triggers runtime discovery and lists all available tools
|
||||
- Skip tools already registered (idempotent)
|
||||
|
||||
**Traceability**: US-002
|
||||
|
||||
### FR-005: Tool Call Lifecycle Hooks
|
||||
|
||||
The agent shall support two lifecycle hooks:
|
||||
- `beforeToolCall(beforeToolCallContext, signal)` — can block execution by returning `beforeToolCallResult(true, reason)`
|
||||
- `afterToolCall(afterToolCallContext)` — can transform result by returning modified `agentToolResult`
|
||||
- Errors from hooks are caught and converted to `immediateOutcome`/error tool result messages (never throw)
|
||||
|
||||
**Traceability**: US-004
|
||||
|
||||
### FR-006: Message Type Conversion
|
||||
|
||||
The agent shall:
|
||||
- Convert internal message types (`userMessage`, `assistantMessage`, `assistantMessageToolCall`, `toolResultMessage`) to OpenAI format via `_userMessageToOpenAI()`, `_assistantMessageToOpenAI()`, `_assistantMessageToolCallToOpenAI()`, `_toolResultMessageToOpenAI()`
|
||||
- Convert internal `messageContent` types (`textContent`, `imageContent`, `reasoningContent`) to OpenAI content blocks via `_messageContentToBlocks()`
|
||||
- Convert `agentTool` definitions to OpenAI function tool format via `_toolsToOpenAI()`
|
||||
|
||||
**Traceability**: US-001
|
||||
|
||||
## 4. Non-Functional Requirements (NFRs)
|
||||
|
||||
### 4.1 Performance & Scalability
|
||||
|
||||
- **NFR-101**: Agent loop message processing overhead <500ms per sequential tool call (measured KPI: KPI-001)
|
||||
- **NFR-102**: MCP tool discovery for 50+ tools completes within 2 seconds (measured KPI: KPI-002)
|
||||
- **NFR-103**: Channel buffers prevent unbounded memory growth — `inputChannel` (16), `followUpChannel` (32), `outputChannel` (16)
|
||||
|
||||
### 4.2 Availability & Reliability
|
||||
|
||||
- **NFR-201**: Agent loop catches all errors via `try/catch` in `_agentLoop()` and calls `eventSink(errMsg)` before exit (measured by event sink logging)
|
||||
- **NFR-202**: `prepareToolCall()` never throws errors — validation failures return `immediateOutcome` with error tool result
|
||||
|
||||
### 4.3 Privacy & Security
|
||||
|
||||
- **Data Classification**: Tool arguments and results may contain PII (user messages); MCP tool results treated as untrusted input
|
||||
- **No built-in auth/authz**: Responsibility of `mcpServer` implementation and `beforeToolCall` hook
|
||||
- **Input validation**: JSON schema validation via `validateToolArguments()` checks required fields only (tools need custom validation via `validateRequiredArgs` hook)
|
||||
|
||||
### 4.4 Observability & Telemetry
|
||||
|
||||
- **Required Logs**: `agentLoop` events (via `eventSink`) emit state machine steps (`_agentLoop 1` through `_agentLoop 6`), `_processMessage` steps (1 through 18), `prepareToolCall` steps (1 through 10)
|
||||
- **Critical Metrics**:
|
||||
- `agent._state.messages.length` — conversation history size (logged at each step)
|
||||
- `toolExecutionStart`/`toolExecutionEnd` events (via `toolExecStartEvent`, `toolExecEndEvent` structs)
|
||||
- **Retention**: No built-in log retention; agent is a library, does not manage external logging
|
||||
|
||||
## 5. Acceptance Conditions
|
||||
|
||||
- [ ] **FR-001**: Agent processes messages through channel system with priority ordering (inputChannel > followUpChannel)
|
||||
- [ ] **FR-002**: `llmCall` is pluggable; response formats supported include `message.tool_calls` dict and `content` array blocks
|
||||
- [ ] **FR-003**: Agent loops on tool calls until LLM returns no tool calls or terminate flag is set
|
||||
- [ ] **FR-004**: MCP tools are discovered via `tools/list` with pagination; all registered tools are callable via `tools/call`
|
||||
- [ ] **FR-005**: `beforeToolCall` hook can block; `afterToolCall` hook can transform result; errors return error tool result
|
||||
- [ ] **FR-006**: All message types (userMessage, assistantMessage, assistantMessageToolCall, toolResultMessage) convert to OpenAI format
|
||||
- [ ] **NFR-101**: Message processing overhead <500ms per sequential tool call
|
||||
- [ ] **NFR-102**: MCP discovery for 50+ tools completes <2 seconds
|
||||
- [ ] **NFR-201**: Agent loop catches errors and exits gracefully without panics
|
||||
@@ -0,0 +1,119 @@
|
||||
# Solution Design: YiemAgent Agent Framework
|
||||
|
||||
## 1. Problem Decomposition
|
||||
|
||||
| Problem | Description | User Impact |
|
||||
|---------|-------------|-------------|
|
||||
| P-001 | Developers need a chatbot framework in Julia that handles LLM calls and tool calls without writing boilerplate | Must write separate request/response handling, tool call parsing, and loop management code for each agent |
|
||||
| P-002 | Tool discovery is manual and repetitive | Must register every tool individually; no automatic discovery from MCP-compatible servers |
|
||||
| P-003 | Multiple LLM providers have different response formats | Must handle `message.tool_calls` (LMStudio/vLLM) vs `content` blocks (OpenAI API) separately |
|
||||
| P-004 | Tool execution needs lifecycle hooks for observability and control | No built-in before/after hooks for authorization, logging, or result sanitization |
|
||||
| P-005 | Agent state management is error-prone | Must handle message history, tool registration, and pending tool call tracking manually |
|
||||
|
||||
## 2. Solution Approach
|
||||
|
||||
**Approach**: "Build a channel-based agent system where the LLM talks to tools automatically, tools talk to MCP servers automatically, and you compose agents by injecting `llmCall` and `mcpServer` functions — the framework owns the loop, the state, and the format conversion."
|
||||
|
||||
**Key Principles**:
|
||||
- **Channel-based async processing**: Each agent owns three channels (`inputChannel`, `followUpChannel`, `outputChannel`); a background `@spawn` task runs the agent loop, processing messages in priority order
|
||||
- **Tool call loop**: The agent calls `llmCall()`, extracts tool calls, executes them (sequentially or in parallel), appends results, and calls `llmCall()` again — repeating until no tool calls remain
|
||||
- **Pluggable hooks**: `prepareContext`, `formatMsgForLLM`, `beforeToolCall`, `afterToolCall`, and `llmCall` are all injectable functions — the framework provides sensible defaults, but you override what you need
|
||||
- **MCP-native tool discovery**: Tools are discovered at startup via MCP `tools/list` and can be discovered at runtime via the `listTools` builtin tool (idempotent registration with pagination support)
|
||||
- **Format-agnostic LLM responses**: `_extractToolCalls()` handles both OpenAI content blocks and LMStudio/vLLM nested message format, allowing the same agent to work with any model provider
|
||||
|
||||
## 3. Decision Rationale
|
||||
|
||||
| Decision ID | Decision | Rationale | Alternatives Rejected |
|
||||
|-------------|----------|-----------|----------------------|
|
||||
| SD-001 | Use channel-based async processing with `@spawn` task | Provides non-blocking message queues; `inputChannel` blocks if full (backpressure); `followUpChannel` allows async follow-ups while agent is busy | Threading with shared state — race conditions; callback-based async — hard to compose |
|
||||
| SD-002 | Background `_agentLoop` task runs message dispatch loop | Isolates message processing from construction; agent can be created and used concurrently | Synchronous agent.run(message) — blocks caller; manual task management — boilerplate |
|
||||
| SD-003 | `_processMessage` runs `llmCall()` in an inner loop (while loop) until no tool calls remain | Handles multi-turn tool use transparently — agent decides when to stop based on LLM response | External loop in caller code — duplicates agent loop logic; single-turn only — no multi-step tool use |
|
||||
| SD-004 | MCP server integration via callable struct `mcpServer(method, args...)` | MCP is model-agnostic protocol; callable struct allows type-stable single dispatch; supports NATS transport (struct has `natsConn`, `topic`, `senderID` fields) | Ad-hoc HTTP client for MCP — breaks NATS-only transports; tight coupling to specific transport |
|
||||
| SD-005 | Tool call lifecycle: `prepareToolCall()` → `executePreparedToolCall()` → `finalizeExecutedToolCall()` — three phases, never throw errors | Preparation validates args and runs `beforeToolCall` hook (can block); execution calls `tool.execute()`; finalization runs `afterToolCall` hook. Each phase returns a result type, not exceptions — agent loop catches all and builds error tool result | `try/catch` in loop — loses context; throwing errors breaks tool result message contract |
|
||||
| SD-006 | Message types are Julia structs with keyword constructors (`userMessage(content=[...])`) | Type stable; keyword constructors provide defaults (`role="user"`, `timestamp=now()`); supports both positional and keyword usage | `Dict{String, Any}` for messages — lose type safety, docgen, IDE support |
|
||||
| SD-007 | `eventSink` function passed to all deep lifecycle calls | Provides hooks for external monitoring (logging, metrics, NATS pub/sub) without modifying core logic | Built-in `println` — can't disable or redirect; no metrics integration |
|
||||
| SD-008 | `abortSignal` struct with `.aborted::Bool` field passed to `prepareToolCall()` and hooks | Simple mutable state for interrupting long-running tool calls (no async cancellation — just flag check) | Threading with `Task.cancel()` — Julia thread model doesn't support it; `@async` spawn — complex cleanup |
|
||||
| SD-009 | `_extractToolCalls()` supports both `message.tool_calls` and `content[{type="tool_calls"}]` formats | LMStudio.jl/vLLM vs OpenAI API have different response shapes; framework must work with both | Separate parser per provider — maintenance burden; require provider wrapper — adds indirection |
|
||||
|
||||
## 4. High-Level Component Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
User[External Caller] -->|promptAgent / followUp| InputCh[inputChannel]
|
||||
|
||||
subagent AgentLoop [Agent Loop Task]
|
||||
InputCh -->|take!| Disp[Message Dispatcher]
|
||||
Disp -->|spawn| Proc[_processMessage]
|
||||
Proc -->|Channel| PrCh[processMessageInputCh]
|
||||
end
|
||||
|
||||
subagent Message Pipeline
|
||||
PrCh -->|put!| DMsg[Drain input, convert to userMessage]
|
||||
DMsg --> PC[prepareContext hook]
|
||||
PC --> FM[formatMsgForLLM hook]
|
||||
FM --> LLM[llmCall]
|
||||
LLM --> EC[_extractToolCalls]
|
||||
EC -->|hasToolCalls| TC[executeToolCalls]
|
||||
TC -->|toolResultBatch| TM[toolResultMessage]
|
||||
TM -->|push!| MH[messageHistory]
|
||||
TC -->|no tool calls break| BRK[break loop]
|
||||
BRK --> End[exit]
|
||||
end
|
||||
|
||||
subagent Tool System
|
||||
TC --> Prep[prepareToolCall]
|
||||
Prep --> Exec[executePreparedToolCall]
|
||||
Exec --> Term[finalizeExecutedToolCall]
|
||||
Term --> CRM[createToolResultMessage]
|
||||
CRM --> TM
|
||||
end
|
||||
|
||||
subagent MCP Integration
|
||||
Prep --> ToolLookup[context.tools lookup]
|
||||
ToolLookup --> MCP[MCP tools/list + tools/call]
|
||||
MCP --> Wr[_wrap_mcp_tool]
|
||||
Wr --> Reg[toolRegistry.registerAllTools]
|
||||
end
|
||||
|
||||
OutputCh[outputChannel] --> User
|
||||
Proc -->|final response| OutputCh
|
||||
```
|
||||
|
||||
**Component Descriptions**:
|
||||
- **Agent Loop Task** (`SD-001`, `SD-002`): Runs `@spawn _agentLoop()`; manages three channels; dispatches messages to `_processMessage`
|
||||
- **Message Pipeline** (`SD-003`, `SD-004`): `prepareContext` → `formatMsgForLLM` → `llmCall` → extract tool calls → loop
|
||||
- **Tool System** (`SD-005`): Three-phase lifecycle (prepare → execute → finalize); never throws; returns error tool result on failure
|
||||
- **MCP Integration** (`SD-004`): `tools/list` discovery with pagination; `_wrap_mcp_tool` creates executable `agentTool` with MCP JSON-RPC calls
|
||||
|
||||
## 5. Alternatives Considered
|
||||
|
||||
| Alternative | Pros | Cons | Decision |
|
||||
|-------------|------|------|----------|
|
||||
| Synchronous `agent.process(message)` | Simpler API; no channels or tasks | Blocks caller; no follow-up support; harder to test concurrency | **Rejected** — async channels needed for real-time chat UX |
|
||||
| JSON dicts for messages everywhere | Flexible; no type definitions | No static checking; no docgen; no IDE autocomplete; harder to serialize/deserialize | **Rejected** — Julia advantages of structs (type-stable, documented) outweigh cost |
|
||||
| Single `onToolCall` hook (pre-execute only) | Simpler hook interface | Can't sanitize results post-execution; can't set terminate flag after tool completes | **Rejected** — `afterToolCall` needed for masking secrets, normalizing usage, inspecting error status |
|
||||
| Direct HTTP calls to MCP servers (no NATS) | Simpler dependency (no NATS) | Breaks if MCP uses NATS (common in microservices); no single transport | **Rejected** — callable struct abstracts transport; NATS is one implementation option |
|
||||
| External orchestrator runs tool call loop | Caller has full control | Duplicates logic; every caller must implement same loop; harder to maintain | **Rejected** — loop is agent framework responsibility (single source of truth) |
|
||||
|
||||
## 6. Risk Assessment
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| `llmCall` throws (network timeout, API error) | Agent loop exits with error in `eventSink` | Medium | `try/catch` in `_processMessage` and `_agentLoop`; eventSink receives error string |
|
||||
| Tool `prepareArguments` or `validateRequiredArgs` hook throws | `prepareToolCall` catches and returns `immediateOutcome` error | Low | Hook errors caught; error tool result sent to LLM for recovery |
|
||||
| MCP server unreachable | Tool discovery returns 0 tools; `listTools` still available | Medium | `register_mcp_tools` returns 0 on error; tool execution calls catch errors |
|
||||
| Long-running tool call hangs | Caller blocks on `outputChannel`; no built-in timeout | Medium | `eventSink` passes abort signal; caller sets `signal.aborted = true` externally |
|
||||
| Multiple agents share same `mcpServer` struct | `mcpServer` must be thread-safe for concurrent tool calls | Low | Each thread gets own `mcpServer` instance; NATS handles concurrency |
|
||||
| Message history grows unbounded | Memory exhaustion in long sessions | Medium | No built-in pruning; `prepareContext` can filter/prune messages (TODO in code) |
|
||||
|
||||
## 7. Requirements Traceability
|
||||
|
||||
| Solution Component | Requirement ID | Decision ID | Description |
|
||||
|-------------------|----------------|-------------|-------------|
|
||||
| Channel-based async processing | FR-001 | SD-001, SD-002 | `inputChannel`, `followUpChannel`, `outputChannel` with priority dispatch |
|
||||
| LLM call loop | FR-003 | SD-003 | `_processMessage` while loop; `llmCall()` → extract → execute → append → repeat |
|
||||
| MCP tool discovery | FR-004 | SD-004, SD-009 | `tools/list` with pagination; `tools/call` execution |
|
||||
| Lifecycle hooks | FR-005 | SD-005 | `beforeToolCall` can block; `afterToolCall` transforms; never throw |
|
||||
| Message type conversion | FR-006 | SD-006 | Julia structs with keyword constructors; `_userMessageToOpenAI`, `_messageContentToBlocks` |
|
||||
| Format-agnostic extraction | FR-002 | SD-009 | `_extractToolCalls` handles both `message.tool_calls` and `content` blocks |
|
||||
| Event sink observability | NFR-201, NFR-103 | SD-007 | `eventSink` passed to all lifecycle steps; logs state machine transitions |
|
||||
@@ -0,0 +1,318 @@
|
||||
# Walkthrough: YiemAgent Agent Framework
|
||||
|
||||
## Structural Guidance
|
||||
|
||||
- One **overview sequence diagram** per participant (high-level flow)
|
||||
- **Detailed sub-diagrams** for complex interaction points (tool call loop, MCP discovery)
|
||||
- Use sequence diagrams for request/response patterns; lifecycle states for multi-step operations
|
||||
|
||||
---
|
||||
|
||||
## External Caller
|
||||
|
||||
_The Developer calling `YiemAgent.jl` via `promptAgent`, `takeResponse`, `followUp`, and `stopAgent`._
|
||||
|
||||
### Session Start: Create Agent
|
||||
|
||||
**WT-001**: Developer creates an `yiemAgent` instance with `llmCall` and `mcpServer`
|
||||
|
||||
> Cross-reference: `2-solution_design.md:SD-001` (channel-based async), `SD-004` (MCP struct)
|
||||
|
||||
**Details**:
|
||||
```julia
|
||||
mcp = mcpServer(conn, topic, senderID)
|
||||
agent = yiemAgent(llmCall, mcpServer=mcp, systemPrompt="You are a helpful assistant")
|
||||
```
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent YiemAgent Constructor
|
||||
Dev->>agent: yiemAgent(llmCall, ; mcpServer=mcp)
|
||||
agent->>agent: create channels (input=16, followUp=32, output=16)
|
||||
agent->>agent: registerAllTools(tools, mcp)
|
||||
agent->>mcp: mcp("tools/list", sessionId)
|
||||
mcp-->>agent: response (tools array + nextCursor)
|
||||
agent->>agent: _wrap_mcp_tool for each tool
|
||||
agent->>agent: registerTool(tools, listTools_tool)
|
||||
agent->>agent: println("[toolRegistry] Discovered N MCP tools")
|
||||
agent->>agent: yiemAgent struct with placeholder task
|
||||
agent->>agent: @spawn _agentLoop(agent)
|
||||
agent-->>Dev: yiemAgent instance
|
||||
```
|
||||
|
||||
> **Traceability**: FR-004 (tool discovery), US-001 (agent creation)
|
||||
|
||||
### Send Message: User Prompt
|
||||
|
||||
**WT-002**: Developer sends a user message to the agent
|
||||
|
||||
> Cross-reference: `2-solution_design.md:SD-002` (background task dispatch)
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent AgentLoop
|
||||
Dev->>agent: promptAgent(agent, text="What's the weather?")
|
||||
agent->>agent: put!(inputChannel, msg_dict)
|
||||
Dev->>agent: takeResponse(agent) # blocks
|
||||
agent->>agent: inputChannel isready
|
||||
agent->>agent: take!(inputChannel)
|
||||
agent->>Dev: (returns agent for chaining)
|
||||
```
|
||||
|
||||
> **Traceability**: FR-001 (channel processing), PR-001, SD-001, SD-002
|
||||
|
||||
### Receive Response
|
||||
|
||||
**WT-003**: Agent returns response on `outputChannel`
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent AgentLoop
|
||||
AgentLoop->>AgentLoop: _processMessage runs
|
||||
AgentLoop->>AgentLoop: while loop, llmCall, tool calls...
|
||||
AgentLoop->>AgentLoop: no more tool calls → exit loop
|
||||
AgentLoop->>AgentLoop: filter reasoningContent from final result
|
||||
AgentLoop->>AgentLoop: _assistantMessageToOpenAI(result)
|
||||
AgentLoop->>AgentLoop: put!(outputChannel, respondToUI)
|
||||
AgentLoop->>Dev: take!(outputChannel) # unblocks
|
||||
Dev->>Dev: receive OpenAI dict response
|
||||
```
|
||||
|
||||
> **Traceability**: FR-001, FR-003, FR-006, PR-001
|
||||
|
||||
### Send Follow-up While Agent Busy
|
||||
|
||||
**WT-004**: Developer sends a follow-up message while agent is processing a tool call
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent AgentLoop
|
||||
Dev->>agent: promptAgent(text="What's the weather in Tokyo?") # agent starts processing
|
||||
AgentLoop->>AgentLoop: _processMessage started (processingTask = Task)
|
||||
Dev->>agent: followUp(text="Also check Seoul") # while busy
|
||||
agent->>agent: put!(followUpChannel, msg_dict)
|
||||
AgentLoop->>AgentLoop: _processMessage completes (tool calls done)
|
||||
AgentLoop->>AgentLoop: outputChannel has message → put!(outputChannel, response), take!(next msg)
|
||||
AgentLoop->>AgentLoop: no inputChannel → check followUpChannel
|
||||
AgentLoop->>AgentLoop: isready(followUpChannel) → take!(followUpMsg), put!(inputChannel, followUpMsg)
|
||||
AgentLoop->>AgentLoop: processingTask = nothing → reset
|
||||
AgentLoop->>AgentLoop: loop back → take!(inputChannel) from followUp
|
||||
Dev->>agent: takeResponse() → blocks
|
||||
AgentLoop->>Dev: next response via outputChannel
|
||||
```
|
||||
|
||||
> **Traceability**: FR-001 (follow-up priority), PR-001, SD-001, SD-002
|
||||
|
||||
### Stop Agent
|
||||
|
||||
**WT-005**: Developer stops the agent gracefully
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent AgentLoop
|
||||
Dev->>agent: stopAgent(agent)
|
||||
agent->>agent: put!(inputChannel, :shutdown)
|
||||
AgentLoop->>AgentLoop: check newUserMsg === :shutdown
|
||||
AgentLoop->>AgentLoop: drain inputChannel (while isready)
|
||||
AgentLoop->>AgentLoop: drain followUpChannel (while isready)
|
||||
AgentLoop->>AgentLoop: break, catch, fetch(_agentLoop)
|
||||
Dev->>agent: close(inputChannel, outputChannel, followUpChannel)
|
||||
Dev-->>Dev: nothing (agent destroyed)
|
||||
```
|
||||
|
||||
> **Traceability**: FR-001, NFR-201 (graceful exit), PR-001
|
||||
|
||||
---
|
||||
|
||||
## LLM Provider
|
||||
|
||||
_The external LLM API (e.g. Anthropic, OpenAI, LMStudio) receiving formatted messages and returning responses._
|
||||
|
||||
### Receive Formatted Message
|
||||
|
||||
**WT-010**: Agent sends LLM request in OpenAI format
|
||||
|
||||
> Cross-reference: `2-solution_design.md:SD-003` (formatMsgForLLM)
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
part agent AgentLoop
|
||||
AgentLoop->>AgentLoop: preparedContext = prepareContext(state)
|
||||
AgentLoop->>AgentLoop: formattedMessages = formatMsgForLLM(preparedContext)
|
||||
Note over AgentLoop: messages: [system, user, assistant, tool, ...]<br/>tools: [{"function": {"name": "getWeather", "parameters": {...}}}]
|
||||
agentLoop->>llmCall: llmCall(formattedMessages, eventSink=eventSink)
|
||||
llmCall->>Provider: HTTP POST /v1/chat/completions (JSON)
|
||||
Provider-->>llmCall: JSON response
|
||||
llmCall-->>AgentLoop: response = JSON.Object{String, Any}(...)
|
||||
```
|
||||
|
||||
> **Traceability**: FR-002, SD-003, SD-009
|
||||
|
||||
### Receive Response: No Tool Calls
|
||||
|
||||
**WT-11**: Agent receives text-only response
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
llmCall-->>AgentLoop: response = {finish_reason=> "stop", message=> {role=> "assistant", content=> "It's 22°C in Bangkok"}}
|
||||
AgentLoop->>AgentLoop: _extractToolCalls(response)
|
||||
AgentLoop->>AgentLoop: hasToolCalls = false
|
||||
AgentLoop->>AgentLoop: assistant_msg = assistantMessage(content=[textContent("It's 22°C")], ...)
|
||||
AgentLoop->>AgentLoop: push!(agentMsgHistory, assistant_msg)
|
||||
AgentLoop->>AgentLoop: break (no tool calls)
|
||||
AgentLoop->>AgentLoop: filter reasoningContent (if any)
|
||||
AgentLoop->>AgentLoop: create final assistantMessage
|
||||
AgentLoop->>AgentLoop: put!(outputChannel, OpenAI format dict)
|
||||
```
|
||||
|
||||
> **Traceability**: FR-003 (loop exit), FR-006 (OpenAI conversion), PR-003
|
||||
|
||||
### Receive Response: With Tool Calls
|
||||
|
||||
**WT-012**: Agent receives tool call response
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
llmCall-->>AgentLoop: response = {finish_reason=> "tool_calls",<br/>message=> {role=> "assistant",<br/>reasoning_content=> "I need to call getWeather...",<br/>tool_calls=> [{"id"=>"call_1", "function"=>{name=>"getWeather", arguments=>"{\"city\":\"Bangkok\"}"}}, ...]}}
|
||||
AgentLoop->>AgentLoop: _extractToolCalls(response)
|
||||
AgentLoop->>AgentLoop: hasToolCalls = true<br/>toolCallList = [agentToolCall("getWeather", ...)]
|
||||
AgentLoop->>AgentLoop: assistant_msg = assistantMessageToolCall(toolCalls, reasoningContent, ...)
|
||||
AgentLoop->>AgentLoop: push!(agentMsgHistory, assistant_msg)
|
||||
AgentLoop->>AgentLoop: config = agentLoopConfig(beforeToolCall, afterToolCall, parallel="sequential")
|
||||
AgentLoop->>AgentLoop: toolResultBatch = executeToolCalls(...)
|
||||
...
|
||||
```
|
||||
|
||||
> **Traceability**: FR-002, FR-003, PR-003, SD-003, SD-005, SD-009
|
||||
|
||||
---
|
||||
|
||||
## MCP Server
|
||||
|
||||
_The Model Context Protocol server providing tools via JSON-RPC 2.0 (transport via NATS or HTTP).
|
||||
|
||||
### Receive tools/list Request
|
||||
|
||||
**WT-020**: Agent discovers tools on startup
|
||||
|
||||
> Cross-reference: `2-solution_design.md:SD-004` (MCP integration)
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
part agent AgentLoop
|
||||
agent->>mcp: mcp("tools/list", sessionId; eventSink=eventSink)
|
||||
Note over mcp: JSON-RPC 2.0: {"jsonrpc"=>"2.0", method=>"tools/list", "id"=>..., "params"=>{...}}
|
||||
MCP_server-->>agent: response = {<br/> "result" => {<br/> "tools" => [name, label, description, inputSchema], ...<br/> , "nextCursor" => 123<br/> }<br/>}
|
||||
agent->>agent: for tool_def in tools_array<br/> _wrap_mcp_tool(mcp, tool_def)
|
||||
agent->>agent: while cursor !== nothing → paginate
|
||||
agent->>agent: println("[toolRegistry] Discovered N MCP tools. Total: $(length(tools))")
|
||||
```
|
||||
|
||||
> **Traceability**: FR-004 (MCP discovery), PR-004
|
||||
|
||||
### Receive tools/call Request
|
||||
|
||||
**WT-021**: Agent executes tool via MCP
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
part agent Tool System
|
||||
agent->>mcp: mcp("tools/call", "getWeather", args={"city" => "Bangkok"}, sessionId; eventSink=eventSink)
|
||||
Note over mcp: JSON-RPC 2.0: {"jsonrpc"=>"2.0", method=>"tools/call", "params"=>{name, arguments, sessionId}}
|
||||
MCP_server-->>agent: response = {<br/> "result" => {<br/> "content" => [{"type"=>"text", "text"=>"Sunny, 22°C"}],<br/> "isError" => false<br/> }<br/>}
|
||||
agent->>agent: check haskey(response, "error") → no
|
||||
agent->>agent: result_data = response["result"]
|
||||
agent->>agent: content_text = _extract_text_content(result_data)
|
||||
agent->>agent: return agentToolResult([textContent(content_text)], details={isError=>false}, nothing, false)
|
||||
```
|
||||
|
||||
> **Traceability**: FR-004 (tool execution), PR-004, SD-004, SD-005
|
||||
|
||||
### Receive Error from MCP Server
|
||||
|
||||
**WT-022**: MCP server returns error
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
MCP_server-->>agent: response = {<br/> "error" => {<br/> "code" => -32601,<br/> "message" => "Method not found"<br/> }<br/>}
|
||||
agent->>agent: haskey(response, "error") → true
|
||||
agent->>agent: return agentToolResult(<br/> [textContent("MCP error: Method not found")],<br/> details={"error" => "Method not found"},<br/> nothing, false<br/>)
|
||||
agent->>AgentLoop: finalOutcome(error=true)
|
||||
agent->>AgentLoop: push!(agentMsgHistory, toolResultMessage(...))
|
||||
agent->>AgentLoop: next LLM turn receives error result
|
||||
```
|
||||
|
||||
> **Traceability**: FR-004 (error handling), NFR-202 (never throw), PR-004, SD-005
|
||||
|
||||
---
|
||||
|
||||
## Error Paths and Edge Cases
|
||||
|
||||
**WT-030**: LLM times out (network error)
|
||||
|
||||
> Same participant POV — from caller's perspective, they block on `takeResponse()` until task exits
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
part agent AgentLoop
|
||||
Dev->>agent: takeResponse(agent) # blocks
|
||||
agent->>llmCall: llmCall(messages) # calls external API
|
||||
lltimout
|
||||
LLM provider-->>AgentLoop: ERROR (timeout)
|
||||
AgentLoop->>AgentLoop: @error "Agent loop failed"
|
||||
AgentLoop->>AgentLoop: eventSink("error: ...")
|
||||
AgentLoop->>Dev: fetch(_agentLoop) throws TaskFailedException
|
||||
Dev->>agent: stopAgent(agent) # cleanup channels
|
||||
```
|
||||
|
||||
**Traceability**: PR-001, NFR-201 (catch error and exit), SD-001
|
||||
|
||||
**WT-031**: Tool execution throws exception
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
agent->>agent: executePreparedToolCall
|
||||
agent->>tool: tool.execute(toolCallId, args, signal, eventSink)
|
||||
note over tool: tool raises exception (e.g., network error, invalid JSON)
|
||||
tool-->>agent: throw Exception
|
||||
agent->>agent: catch e
|
||||
agent->>agent: executedOutcome(createErrorToolResult("error message"), true)
|
||||
agent->>AgentLoop: finalizeExecutedToolCall
|
||||
AgentLoop->>AgentLoop: createToolResultMessage(error=true)
|
||||
AgentLoop->>AgentLoop: push!(history, toolResultMessage)
|
||||
AgentLoop->>AgentLoop: next llmCall with error in message history
|
||||
```
|
||||
|
||||
**Traceability**: FR-005 (hook errors caught), NFR-202 (never throw), SD-005
|
||||
|
||||
**WT-032**: beforeToolCall hook blocks execution
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
agent->>agent: beforeToolCall(beforeCtx, signal, eventSink)
|
||||
agent->>agent: returns beforeToolCallResult(true, "User must approve")
|
||||
agent->>agent: return immediateOutcome(createErrorToolResult("Tool blocked"), true)
|
||||
agent->>AgentLoop: executeToolCalls → immediateOutcome
|
||||
AgentLoop->>AgentLoop: finalizeExecutedToolCall
|
||||
AgentLoop->>AgentLoop: createToolResultMessage(error=true)
|
||||
AgentLoop->>AgentLoop: push! to history
|
||||
```
|
||||
|
||||
**Traceability**: FR-005 (beforeToolCall can block), SD-005
|
||||
@@ -0,0 +1,518 @@
|
||||
# Specification: YiemAgent Agent Framework
|
||||
|
||||
## 1. Message Type System (`src/type.jl`)
|
||||
|
||||
### SP-001: userMessage
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `role::String` | `String` | Yes | Always `"user"` |
|
||||
| `content::Vector{messageContent}` | `Vector{messageContent}` | No | Plain text and/or image content |
|
||||
| `timestamp::Timestamp` | `DateTime` | Yes | When the message was sent |
|
||||
|
||||
**Keyword Constructor**: `userMessage(; role="user", content=[...], timestamp=now())`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006 (message type conversion)
|
||||
- Solution Design: SD-006 (Julia structs for messages)
|
||||
|
||||
### SP-002: assistantMessage
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `role::String` | `String` | Yes | Always `"assistant"` |
|
||||
| `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Text and/or image content |
|
||||
| `api::String` | `String` | Yes | API name used (e.g., `"openai"`) |
|
||||
| `provider::String` | `String` | Yes | Provider name (e.g., `"anthropic"`) |
|
||||
| `model::String` | `String` | Yes | Model identifier |
|
||||
| `usage::llmUsage` | `llmUsage` | Yes | Token usage (in/out) |
|
||||
| `stopReason::String` | `String` | Yes | Why generation stopped (e.g., `"end_turn"`) |
|
||||
| `errorMessage::Union{String, Nothing}` | `Union{String, Nothing}` | Yes | Error if generation failed |
|
||||
| `timestamp::Timestamp` | `DateTime` | Yes | When the message was received |
|
||||
|
||||
**Keyword Constructor**: `assistantMessage(; role="assistant", content=[...], api="", provider="", model=nothing, usage=llmUsage(0,0), stopReason="end_turn", errorMessage=nothing, timestamp=now())`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006 (message type conversion)
|
||||
- Solution Design: SD-006
|
||||
|
||||
### SP-003: assistantMessageToolCall
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `role::String` | `String` | Yes | Always `"assistant"` |
|
||||
| `toolCalls::Vector{agentToolCall}` | `Vector{agentToolCall}` | Yes | Tool calls to execute |
|
||||
| `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Reasoning/thinking content blocks |
|
||||
| `api::String` | `String` | Yes | API name used |
|
||||
| `provider::String` | `String` | Yes | Provider name |
|
||||
| `model::String` | `String` | Yes | Model identifier |
|
||||
| `usage::llmUsage` | `llmUsage` | Yes | Token usage |
|
||||
| `stopReason::String` | `String` | Yes | Why generation stopped (e.g., `"tool_calls"`) |
|
||||
| `errorMessage::Union{String, Nothing}` | `Union{String, Nothing}` | Yes | Error if generation failed |
|
||||
| `timestamp::Timestamp` | `DateTime` | Yes | When the message was received |
|
||||
|
||||
**Keyword Constructor**: `assistantMessageToolCall(; role="assistant", toolCalls=[...], content=[], api="", provider="", model=nothing, usage=llmUsage(0,0), stopReason="tool_calls", errorMessage=nothing, timestamp=now())`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-003 (tool call loop)
|
||||
- Solution Design: SD-005 (lifecycle phases), SD-009 (format-agnostic LLM responses)
|
||||
|
||||
### SP-004: toolResultMessage
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `role::String` | `String` | Yes | Always `"toolResult"` (note: `"tool"` in example) |
|
||||
| `toolCallId::String` | `String` | Yes | ID matching the tool call |
|
||||
| `toolName::String` | `String` | Yes | Name of the executed tool |
|
||||
| `content::Vector{messageContent}` | `Vector{messageContent}` | Yes | Tool output content |
|
||||
| `details::Any` | `Any` | Yes | Additional tool-specific details |
|
||||
| `usage::Union{llmUsage, Nothing}` | `Union{llmUsage, Nothing}` | Yes | Token usage if applicable |
|
||||
| `addedToolNames::Union{Vector{String}, Nothing}` | `Union{Vector{String}, Nothing}` | Yes | Tools added during execution |
|
||||
| `isError::Bool` | `Bool` | Yes | Whether the tool call resulted in an error |
|
||||
| `timestamp::Timestamp` | `DateTime` | Yes | When the result was recorded |
|
||||
|
||||
**Keyword Constructor**: `toolResultMessage(; role="tool", toolCallId="", toolName="", content=[], details=nothing, usage=nothing, addedToolNames=nothing, isError=false, timestamp=now())`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-005 (tool lifecycle)
|
||||
- Solution Design: SD-005
|
||||
|
||||
### SP-005: agentToolCall
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `type::String` | `String` | Yes | Always `"function"` |
|
||||
| `id::String` | `String` | Yes | Unique tool call identifier |
|
||||
| `name::String` | `String` | Yes | Tool name |
|
||||
| `arguments::Dict{String, Any}` | `Dict{String, Any}` | Yes | Parsed tool arguments |
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-003 (tool call extraction)
|
||||
- Solution Design: SD-009 (extract tool calls from LLM response)
|
||||
|
||||
### SP-006: messageContent Types
|
||||
|
||||
**textContent** (`SP-006.1`):
|
||||
```julia
|
||||
struct textContent <: messageContent
|
||||
text::String
|
||||
end
|
||||
```
|
||||
|
||||
**imageContent** (`SP-006.2`):
|
||||
```julia
|
||||
struct imageContent <: messageContent
|
||||
data::String # Base64-encoded image data
|
||||
mimeType::String # MIME type (e.g., "image/png")
|
||||
end
|
||||
```
|
||||
|
||||
**reasoningContent** (`SP-006.3`):
|
||||
```julia
|
||||
struct reasoningContent <: messageContent
|
||||
text::String
|
||||
end
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006 (message content conversion)
|
||||
- Solution Design: SD-006
|
||||
|
||||
## 2. LLM Model Configuration (`src/type.jl`)
|
||||
|
||||
### SP-007: llmModel
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `id::String` | `String` | Yes | Unique model identifier |
|
||||
| `name::String` | `String` | Yes | Human-readable model name |
|
||||
| `provider::String` | `String` | Yes | Provider name (e.g., `"anthropic"`, `"openai"`) |
|
||||
| `baseUrl::String` | `String` | Yes | API endpoint base URL |
|
||||
| `reasoning::Bool` | `Bool` | Yes | Whether the model supports chain-of-thought |
|
||||
| `input::Vector{String}` | `Vector{String}` | Yes | Supported input modalities (e.g., `"text"`, `"image"`) |
|
||||
| `cost::modelCost` | `modelCost` | Yes | Pricing information |
|
||||
| `contextWindow::Int64` | `Int64` | Yes | Maximum context length in tokens |
|
||||
| `maxTokens::Int64` | `Int64` | Yes | Maximum output tokens per completion |
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-002 (LLM agnosticism)
|
||||
- Solution Design: SD-003
|
||||
|
||||
### SP-008: modelCost
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `input::Float64` | `Float64` | Yes | Price per 1M input tokens |
|
||||
| `output::Float64` | `Float64` | Yes | Price per 1M output tokens |
|
||||
| `cache_read::Float64` | `Float64` | Yes | Price per 1M cached read tokens |
|
||||
| `cache_write::Float64` | `Float64` | Yes | Price per 1M cache write tokens |
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: N/A (internal tracking data)
|
||||
- Solution Design: SD-003
|
||||
|
||||
## 3. MCP Tool Integration (`src/toolRegistry.jl`)
|
||||
|
||||
### SP-009: tools/list Response Format
|
||||
|
||||
**Request**: `mcp("tools/list", sessionId; eventSink=eventSink)`
|
||||
|
||||
**Response** (JSON-RPC 2.0):
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "tool_name",
|
||||
"title"?: "Human-readable title",
|
||||
"label"?: "Fallback title",
|
||||
"description": "What the tool does",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": { "type": "string", "description": "..." }
|
||||
},
|
||||
"required": ["field"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"nextCursor"?: "pagination_cursor_or_empty"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response error**:
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": "Method not found"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-004 (MCP discovery)
|
||||
- Solution Design: SD-004
|
||||
|
||||
### SP-010: tools/call Request/Response Format
|
||||
|
||||
**Request**: `mcp("tools/call", toolName, args, sessionId; eventSink=eventSink)`
|
||||
|
||||
**Response** (JSON-RPC 2.0):
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"content": [
|
||||
{"type": "text", "text": "Tool output text"}
|
||||
],
|
||||
"isError": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response error**:
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"content": [
|
||||
{"type": "text", "text": "Error message"}
|
||||
],
|
||||
"isError": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-004 (tool execution)
|
||||
- Solution Design: SD-004 (`_wrap_mcp_tool`)
|
||||
|
||||
## 4. OpenAI Message Format (`src/utils.jl`)
|
||||
|
||||
### SP-011: system message format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"role" => "system",
|
||||
"content" => [Dict("type" => "text", "text" => ctx.systemPrompt)]
|
||||
)
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006
|
||||
- Solution Design: SD-006
|
||||
|
||||
### SP-012: userMessage → OpenAI format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"role" => "user",
|
||||
"content" => _messageContentToBlocks(msg.content)
|
||||
)
|
||||
```
|
||||
|
||||
Where `_messageContentToBlocks` converts:
|
||||
- `textContent(t)` → `Dict("type" => "text", "text" => t)`
|
||||
- `imageContent(data, mime)` → `Dict("type" => "image_url", "image_url" => Dict("url" => "data:$mime;base64,$data"))`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006
|
||||
- Solution Design: SD-006
|
||||
|
||||
### SP-013: assistantMessageToolCall → OpenAI format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"role" => "assistant",
|
||||
"content" => nothing,
|
||||
"tool_calls" => [
|
||||
Dict(
|
||||
"id" => tc.id,
|
||||
"type" => tc.type,
|
||||
"function" => Dict(
|
||||
"name" => tc.name,
|
||||
"arguments" => JSON.json(tc.arguments) # serialized as JSON string
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-003
|
||||
- Solution Design: SD-009
|
||||
|
||||
### SP-014: assistantMessage → OpenAI format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"role" => "assistant",
|
||||
"content" => _messageContentToBlocks(msg.content)
|
||||
)
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-006
|
||||
- Solution Design: SD-006
|
||||
|
||||
### SP-015: toolResultMessage → OpenAI format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"role" => "tool",
|
||||
"tool_call_id" => msg.toolCallId,
|
||||
"content" => _messageContentToBlocks(msg.content)
|
||||
)
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-005, FR-006
|
||||
- Solution Design: SD-005
|
||||
|
||||
### SP-016: agentTool → OpenAI tools format
|
||||
|
||||
```julia
|
||||
Dict(
|
||||
"type" => "function",
|
||||
"function" => Dict(
|
||||
"name" => tool.name,
|
||||
"description" => tool.description,
|
||||
"parameters" => tool.inputSchema
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-004
|
||||
- Solution Design: SD-004, SD-006
|
||||
|
||||
## 5. LLM Response Format Agnostic Extraction (`src/agentCore.jl`)
|
||||
|
||||
### SP-017: format 1 — LMStudio.jl / vLLM style (dict with nested `message.tool_calls`)
|
||||
|
||||
**Response dict** (key: `message::Dict`):
|
||||
```json
|
||||
{
|
||||
"finish_reason": "tool_calls",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"reasoning_content": "I need to call getWeather...",
|
||||
"tool_calls": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getWeather",
|
||||
"arguments": "{\"city\":\"Bangkok\"}" // JSON string
|
||||
},
|
||||
"id": "call_1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When no tool calls**:
|
||||
```json
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "It's 22°C in Bangkok"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-002
|
||||
- Solution Design: SD-009
|
||||
|
||||
### SP-018: format 2 — OpenAI API style (content blocks array)
|
||||
|
||||
**Content blocks with tool calls** (`SP-018.1`):
|
||||
```json
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_calls",
|
||||
"tool_calls": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getWeather",
|
||||
"arguments": {"city": "Bangkok"} // already parsed dict
|
||||
},
|
||||
"id": "call_1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Content blocks with tool_call (already parsed)** (`SP-018.2`):
|
||||
```json
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_call",
|
||||
"name": "getWeather",
|
||||
"arguments": {"city": "Bangkok"},
|
||||
"id": "call_1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Content blocks with text only** (`SP-018.3`):
|
||||
```json
|
||||
{
|
||||
"content": [
|
||||
{"type": "text", "text": "It's 22°C in Bangkok"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response dict extras** (common across all formats, `SP-018.4`):
|
||||
```json
|
||||
{
|
||||
"api": "openai",
|
||||
"provider": "anthropic",
|
||||
"model": "gpt-4",
|
||||
"usage": {"inputTokens": 100, "outputTokens": 50},
|
||||
"error_message": "Optional error"
|
||||
}
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-002
|
||||
- Solution Design: SD-009
|
||||
|
||||
## 6. Agent Loop Configuration (`src/type.jl` + `src/agentCore.jl`)
|
||||
|
||||
### SP-019: agentContext
|
||||
|
||||
```julia
|
||||
struct agentContext
|
||||
systemPrompt::String
|
||||
messages::Vector{agentMessage}
|
||||
tools::Union{OrderedDict{String, agentTool}, Nothing} # O(1) lookup by name
|
||||
end
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-003 (context preparation)
|
||||
- Solution Design: SD-003, SD-004
|
||||
|
||||
### SP-020: agentState
|
||||
|
||||
```julia
|
||||
mutable struct agentState
|
||||
systemPrompt::String
|
||||
model::Union{llmModel, Nothing}
|
||||
tools::OrderedDict{String, agentTool} # insertion-ordered
|
||||
messages::Vector{agentMessage} # history (no system prompt)
|
||||
pendingToolCalls::Vector{String} # tool call IDs waiting for results
|
||||
errorMessage::Union{String, Nothing} # last error message
|
||||
end
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-001 (agent state management)
|
||||
- Solution Design: SD-006
|
||||
|
||||
### SP-021: agentLoopConfig
|
||||
|
||||
```julia
|
||||
struct agentLoopConfig
|
||||
beforeToolCall::Union{Function, Nothing}
|
||||
afterToolCall::Union{Function, Nothing}
|
||||
toolExecution::String # "sequential" or "parallel"
|
||||
end
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-005 (lifecycle hooks)
|
||||
- Solution Design: SD-005
|
||||
|
||||
### SP-022: abortSignal
|
||||
|
||||
```julia
|
||||
struct abortSignal
|
||||
aborted::Bool
|
||||
end
|
||||
```
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: NFR-101 (timeout handling)
|
||||
- Solution Design: SD-008
|
||||
|
||||
## 7. Tool Validation Hooks (`src/utils.jl`)
|
||||
|
||||
### SP-023: validateRequiredArgs (default hook)
|
||||
|
||||
**Signature**: `validateRequiredArgs(args::AbstractDict{String, Any}, inputSchema::AbstractDict{String, Any})::Union{Nothing, String}`
|
||||
|
||||
**Behavior**:
|
||||
- Extract `required = get(inputSchema, "required", Any[])`
|
||||
- Check each required field exists in `keys(args)`
|
||||
- Return `nothing` if all present
|
||||
- Return `"Missing required arguments: $(join(missing, ", "))"` if any missing
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: NFR-301 (input validation)
|
||||
- Solution Design: SD-005
|
||||
|
||||
### SP-024: validateToolArguments (wrapper hook)
|
||||
|
||||
**Signature**: `validateToolArguments(tool::agentTool, prepared::agentToolCall)::AbstractDict{String, Any}`
|
||||
|
||||
**Behavior**:
|
||||
- If `tool.validateRequiredArgs !== nothing`: call `tool.validateRequiredArgs(prepared.arguments)`
|
||||
- Else: call `validateRequiredArgs(prepared.arguments, tool.inputSchema)`
|
||||
- If returns `!nothing`: throw `ArgumentError(result)`
|
||||
- Caller (`prepareToolCall`) catches and returns `immediateOutcome`
|
||||
|
||||
**Traceability**:
|
||||
- Requirement: FR-005 (tool validation)
|
||||
- Solution Design: SD-005
|
||||
@@ -0,0 +1,192 @@
|
||||
# Validation: YiemAgent Agent Framework
|
||||
|
||||
## Test Architecture Overview
|
||||
|
||||
Because YiemAgent is a Julia library (not a web service), validation tests focus on:
|
||||
- **Unit tests**: message type conversion, tool execution logic, format extraction
|
||||
- **Integration tests**: full agent loop with stubbed LLM and MCP
|
||||
- **Contract tests**: response format compatibility with OpenAI/LMStudio specs
|
||||
- **Performance tests**: agent loop overhead for sequential tool calls
|
||||
|
||||
Every specification rule (SP-XXX), walkthrough journey (WT-XXX), and requirement (FR-XXX) must have a corresponding test.
|
||||
|
||||
---
|
||||
|
||||
## Test Types Mapping to Source Documents
|
||||
|
||||
| Test Type | Source Document | Example Coverage |
|
||||
|-----------|-----------------|------------------|
|
||||
| **Unit tests** | `4-specification.md` (SP-001..SP-024), `src/utils.jl` (converters) | Message type → OpenAI format conversion; tool argument validation |
|
||||
| **Integration tests** | `3-walkthrough.md` (WT-001..WT-032) | Full agent loop: prompt → llmCall → tool extract → execute → response |
|
||||
| **Contract tests** | `4-specification.md` SP-017, SP-018 | Response format 1 (LMStudio/vLLM) and format 2 (OpenAI) parsed correctly |
|
||||
| **Performance tests** | `1-requirements.md` FR-003, NFR-101 | Agent loop overhead <500ms per sequential tool call |
|
||||
| **Penetration tests** | `1-requirements.md` NFR-301 | Tool argument injection; MCP spoofing |
|
||||
| **Schema validation** | `4-specification.md` SP-023, SP-024 | Required fields, type checking on tool arguments |
|
||||
|
||||
---
|
||||
|
||||
## Specification-to-Test Execution Trace
|
||||
|
||||
Every traceable item — specification rules (SP-XXX), walkthrough journeys (WT-XXX), and requirements (FR-XXX) — must have a runnable test. This trace maps each item to its corresponding test file and test function.
|
||||
|
||||
**Format**:
|
||||
|
||||
| Requirement/Spec ID | Participant | Source Path | Unit Test File | Test Function | Additional Test File(s) | CI Gate | Status |
|
||||
|---------------------|-------------|-------------|----------------|---------------|------------------------|---------|--------|
|
||||
| FR-001 | — | `1-requirements.md:3.1` | `tests/unit/channel-handling.jl` | `test_input_channel_priority` | `tests/integration/full-agent-loop.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
| FR-002 | — | `1-requirements.md:3.2` | `tests/unit/llm-format-extraction.jl` | `test_format1_tool_calls` | `tests/contract/response-format.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
| FR-003 | — | `1-requirements.md:3.3` | `tests/integration/tool-call-loop.jl` | `test_loop_exits_on_no_tool_calls` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| FR-004 | — | `1-requirements.md:3.4` | `tests/unit/mcp-tool-discovery.jl` | `test_tools_list_with_pagination` | `tests/contract/mcp-json-rpc.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
| FR-005 | — | `1-requirements.md:3.5` | `tests/unit/tool-hooks.jl` | `test_beforeToolCall_blocks` | `tests/integration/agent-loop.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
| FR-006 | — | `1-requirements.md:3.6` | `tests/unit/message-conversion.jl` | `test_userMessage_to_OpenAI` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| WT-001 | Caller | `3-walkthrough.md:#agent-creation` | `integration/agent-creation.jl` | `test_agent_construction_and_lifecycle` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-002 | Caller | `3-walkthrough.md:#send-message` | `integration/message-flow.jl` | `test_promptAgent_and_takeResponse` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-003 | Caller | `3-walkthrough.md:#receive-response` | `integration/response-format.jl` | `test_output_channel_delivery` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-004 | Caller | `3-walkthrough.md:#follow-up` | `integration/followup.jl` | `test_followUp_while_busy` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-005 | Caller | `3-walkthrough.md:#stop-agent` | `integration/shutdown.jl` | `test_stopAgent_channel_cleanup` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-010 | LLM Provider | `3-walkthrough.md:#receive-formatted` | `integration/llm-call.jl` | `test_formatMsgForLLM_output_structure` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-011 | LLM Provider | `3-walkthrough.md:#response-no-tool-calls` | `integration/non-tool-response.jl` | `test_lmm_response_no_tool_calls_breaks_loop` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-012 | LLM Provider | `3-walkthrough.md:#response-with-tool` | `integration/tool-response.jl` | `test_lmm_response_with_tool_calls` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-020 | MCP Server | `3-walkthrough.md:#discover-tools` | `unit/mcp-discovery.jl` | `test_mcp_tools_list_parsed` | `contract/mcp-json-rpc.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
| WT-021 | MCP Server | `3-walkthrough.md:#execute-tool` | `unit/mcp-tool-execute.jl` | `test_mcp_tools_call_response_parsed` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| WT-022 | MCP Server | `3-walkthrough.md:#mcp-error` | `unit/mcp-error-handling.jl` | `test_mcp_response_error_returned` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| WT-030 | Error | `3-walkthrough.md:#llm-timeout` | `integration/llm-timeout.jl` | `test_LLM_timeout_agent_loop_exits` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-031 | Error | `3-walkthrough.md:#tool-throw` | `integration/tool-error.jl` | `test_tool_execute_throws_returns_error_result` | — | `ci/julia-integration.yaml` | **TODO** |
|
||||
| WT-032 | Error | `3-walkthrough.md:#beforeToolCall-block` | `unit/beforeToolCall-block.jl` | `test_beforeToolCall_blocks_execution` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-001 | — | `4-specification.md:#SP-001:userMessage` | `unit/message-types.jl` | `test_userMessage_struct_default_roles` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-002 | — | `4-specification.md:#SP-002:assistantMessage` | `unit/message-types.jl` | `test_assistantMessage_struct_default_stopReason_end_turn` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-003 | — | `4-specification.md:#SP-003:assistantMessageToolCall` | `unit/message-types.jl` | `test_assistantMessageToolCall_struct_default_stopReason_tool_calls` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-004 | — | `4-specification.md:#SP-004:toolResultMessage` | `unit/message-types.jl` | `test_toolResultMessage_struct_defaults` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-005 | — | `4-specification.md:#SP-005:agentToolCall` | `unit/message-types.jl` | `test_agentToolCall_type_is_always_function` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-006.1,2,3 | — | `4-specification.md:#SP-006:content-types` | `unit/content-types.jl` | `test_textContent_imageContent_reasoningContent` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-017 | — | `4-specification.md:#SP-017` | `contract/response-format.jl` | `test_lmStudio_vLLM_format_parsed` | — | `ci/julia-contract.yaml` | **TODO** |
|
||||
| SP-018.1,2,3,4 | — | `4-specification.md:#SP-018` | `contract/response-format.jl` | `test_openAI_format_blocks_parsed` | — | `ci/julia-contract.yaml` | **TODO** |
|
||||
| SP-023 | — | `4-specification.md:#SP-023` | `unit/tool-validation.jl` | `test_validateRequiredArgs_missing_fields` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| SP-024 | — | `4-specification.md:#SP-024` | `unit/tool-validation.jl` | `test_validateToolArguments_throws_on_missing` | — | `ci/julia-unit.yaml` | **TODO** |
|
||||
| NFR-101 | — | `1-requirements.md:4.1` | `perf/agent-loop.jl` | `perf_agent_loop_overhead_<500ms` | `tests/perf/concurrency.jl` | `ci/julia-perf.yaml` | **TODO** |
|
||||
| NFR-102 | — | `1-requirements.md:4.1` | `perf/mcp-discovery.jl` | `perf_mcp_discovery_50_tools_<2s` | — | `ci/julia-perf.yaml` | **TODO** |
|
||||
| NFR-201 | — | `1-requirements.md:4.2` | `unit/error-handling.jl` | `test_agent_loop_catches_and_logs_errors` | `integration/llm-timeout.jl` | `ci/julia-unit.yaml` | **TODO** |
|
||||
|
||||
---
|
||||
|
||||
## Trace Rules
|
||||
|
||||
### Unit Test Rules
|
||||
|
||||
- **Every specification struct** (SP-001..SP-008, SP-019..SP-022) must have a unit test validating:
|
||||
- Default values for keyword arguments are set correctly
|
||||
- Type field matches spec (e.g., `agentToolCall.type` is always `"function"`)
|
||||
- Constructor works with both positional and keyword args
|
||||
- **Every converter function** (`_userMessageToOpenAI`, `_assistantMessageToOpenAI`, etc. SP-011..SP-016) must have a unit test:
|
||||
- Input Julia struct → output `Dict{String, Any}` matching OpenAI spec
|
||||
- Round-trip conversion preserves data (e.g., `textContent` text is preserved)
|
||||
- **Every tool validation function** (SP-023, SP-024) must have unit tests:
|
||||
- Missing required field → returns error string (not throws)
|
||||
- All required present → returns `nothing`
|
||||
- Custom `validateRequiredArgs` hook on tool takes precedence over default
|
||||
|
||||
### Integration Test Rules
|
||||
|
||||
- **Every walkthrough journey** (WT-XXX) must have at least one integration test:
|
||||
- `_agentLoop` processes messages through the full pipeline with stubbed LLM/MCP
|
||||
- `promptAgent` → `takeResponse` round-trip works
|
||||
- `followUp` while busy queues to `followUpChannel`, processes after current work
|
||||
- `stopAgent` sends `:shutdown`, drains channels, closes all 3 channels
|
||||
- **Tool call lifecycle** (FR-003, FR-005):
|
||||
- `llmCall` returns tool calls → loop calls `executeToolCalls` → results appended
|
||||
- Tool calls → no tool calls on next `llmCall` → loop exits
|
||||
- `toolResultBatch.terminate == true` → loop exits immediately
|
||||
- **Hook tests**:
|
||||
- `beforeToolCall` returning `beforeToolCallResult(true, reason)` blocks execution → error tool result
|
||||
- `afterToolCall` returning modified `agentToolResult` replaces result
|
||||
|
||||
### Contract Test Rules
|
||||
|
||||
- **Every response format** (SP-017, SP-018) must be parsed:
|
||||
- Format 1: `_extractToolCalls({message => {tool_calls => [...]}})` extracts `assistantMessageToolCall`
|
||||
- Format 2: `_extractToolCalls({content => [{type=>"tool_calls", tool_calls=[...]}}])` extracts `assistantMessageToolCall`
|
||||
- Format 2 (no tools): `_extractToolCalls({content => [{type=>"text", text=>...}]})` extracts `assistantMessage`
|
||||
- `reasoning_content` field → added to `assistantMessageToolCall.content`
|
||||
- `api`, `provider`, `model`, `usage` → copied to message fields
|
||||
|
||||
### Performance Test Rules
|
||||
|
||||
- **NFR-101**: Agent loop overhead measurement:
|
||||
- Stub LLM returns tool call → execute tool (1ms) → stub returns no tool calls → measure elapsed
|
||||
- Subtract stub costs (llm call 1ms + tool 1ms)
|
||||
- Overhead = elapsed - stub costs
|
||||
- Must be <500ms per tool call (p95)
|
||||
- **NFR-102**: MCP discovery performance:
|
||||
- Mock MCP server returns 200 tools (simulated, no real network)
|
||||
- Measure `_wrap_mcp_tool` + `registerTool` for all 200 tools
|
||||
- Must complete <2 seconds
|
||||
|
||||
### Security/Validation Rules
|
||||
|
||||
- **NFR-301 (input validation)**:
|
||||
- Tool arguments with injection patterns (e.g., `{"city" => "; rm -rf /"}`) → passed through to MCP
|
||||
- `beforeToolCall` can block dangerous tools (e.g., `toolCall.name == "delete_file"`)
|
||||
- **Penetration test**: MCP server spoofing
|
||||
- Attacker sends `{"result" => {"tools" => [{"name" => "malicious", ...}]}}` from untrusted MCP endpoint
|
||||
- Agent executes tool via MCP `tools/call` with attacker-controlled arguments
|
||||
- Mitigation: `beforeToolCall` hook can block unknown tools; app should validate MCP server URL
|
||||
|
||||
---
|
||||
|
||||
## Gap Check (Validation Review)
|
||||
|
||||
Per ASG Framework §6, verify:
|
||||
|
||||
- ✓ Every `SP-XXX` specification item has at least one unit test (listed in trace table above)
|
||||
- ✓ Every `WT-XXX` walkthrough step has at least one integration test (listed in trace table above)
|
||||
- ✓ Every NFR threshold has at least one performance test (NFR-101, NFR-102 in trace table)
|
||||
- ✓ Security NFRs have penetration test scenarios (NFR-301 in trace rules above)
|
||||
- ✓ Every `SP-017, SP-018` response format has contract tests (listed in trace table)
|
||||
- ✓ Every `SP-011..SP-016, SP-023, SP-024` converter/validation rule has unit tests (listed in trace table)
|
||||
|
||||
**Status**: All rows have **TODO** status. Tests must be written before integration into production CI pipeline.
|
||||
|
||||
---
|
||||
|
||||
## CI Pipeline
|
||||
|
||||
```yaml
|
||||
# ci/julia-pipeline.yaml (placeholder)
|
||||
name: Julia YiemAgent CI
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: julia-actions/julia-buildpkg@latest
|
||||
- run: julia --project=. -e 'using Pkg; Pkg.instantiate()'
|
||||
- run: julia --project=./tests -e 'using Test; Test.@testset "Unit tests" # include tests/unit/run_tests.jl"'
|
||||
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
needs: unit
|
||||
steps:
|
||||
- run: julia --project=./tests -e 'using Test; Test.@testset "Integration" # include tests/integration/run_tests.jl"'
|
||||
|
||||
contract:
|
||||
runs-on: ubuntu-fail
|
||||
needs: unit
|
||||
steps:
|
||||
- run: julia --project=./tests -e 'using Test; Test.@testset "Contract" # include tests/contract/run_tests.jl"'
|
||||
|
||||
perf:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unit, integration]
|
||||
steps:
|
||||
- run: julia --project=./tests -e 'using Test; Test.@testset "Performance" # include tests/perf/run_tests.jl"'
|
||||
# Fail if NFR-101 or NFR-102 thresholds breached
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: julia --eval 'using Aqua; Aqua.test_all(YiemAgent)'
|
||||
```
|
||||
|
||||
**Gap Check**: Per ASG §5, every spec item maps to a CI gate. If a `SP-XXX` or `WT-XXX` row has no test file/function, the trace table row must be filled before merging.
|
||||
@@ -0,0 +1,217 @@
|
||||
# Implementation Plan: YiemAgent Agent Framework
|
||||
|
||||
## 1. Implementation Phases
|
||||
|
||||
### Phase 1: Type System & Core Structures (COMPLETED)
|
||||
|
||||
**Files**: `src/type.jl`
|
||||
|
||||
**Completed Tasks**:
|
||||
- `IMP-001`: Define Julia struct types for messages (`userMessage`, `assistantMessage`, `assistantMessageToolCall`, `toolResultMessage`) with keyword constructors — **COMPLETED** (see `src/type.jl:95-249`)
|
||||
- `IMP-002`: Define message content types (`textContent`, `imageContent`, `reasoningContent`) with abstract `messageContent` base type — **COMPLETED** (see `src/type.jl:74-87`)
|
||||
- `IMP-003`: Define tool types (`agentTool`, `agentToolCall`) with lifecycle hooks (`prepareArguments`, `validateRequiredArgs`) — **COMPLETED** (see `src/type.jl:319-328`)
|
||||
- `IMP-004`: Define agent context/state types (`agentContext`, `agentState`) — **COMPLETED** (see `src/type.jl:357-415`)
|
||||
- `IMP-005`: Define LLM model/usage/cost types (`llmModel`, `modelCost`, `llmUsage`) — **COMPLETED** (see `src/type.jl:45-67`)
|
||||
- `IMP-006`: Define tool call lifecycle result types (`preparedToolCall`, `immediateOutcome`, `executedOutcome`, `finalizedOutcome`) — **COMPLETED** (see `src/type.jl:600-710`)
|
||||
- `IMP-007`: Define agent loop config and event types (`agentLoopConfig`, `abortSignal`, `agentToolResult`, `toolExecStartEvent`, etc.) — **COMPLETED** (see `src/type.jl:450-567`)
|
||||
- `IMP-008`: Export all types from `type` module — **COMPLETED** (see `src/type.jl:2-26`)
|
||||
|
||||
**Validation Tests**: Unit tests for struct construction with keyword args (SP-001..SP-008, SP-019..SP-022)
|
||||
|
||||
**Traceability**: FR-001, FR-002, FR-003, FR-004, FR-005, FR-006
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Utility & Formatting Functions (COMPLETED)
|
||||
|
||||
**Files**: `src/utils.jl`
|
||||
|
||||
**Completed Tasks**:
|
||||
- `IMP-010`: Implement `prepareContext(state, eventSink) -> agentContext` — **COMPLETED** (see `src/utils.jl:112-126`)
|
||||
- TODO: Filter tools based on user intent and tool description (`src/utils.jl:114`)
|
||||
- TODO: Inject system prompt context (e.g., time, session state) (`src/utils.jl:118`)
|
||||
- TODO: Prune/reorder messages (`src/utils.jl:121`)
|
||||
- `IMP-011`: Implement `formatMsgForLLM(ctx, eventSink) -> Dict{String, Any}` — **COMPLETED** (see `src/utils.jl:159-244`)
|
||||
- Converts system prompt to system message
|
||||
- Converts conversation messages via `_userMessageToOpenAI`, `_assistantMessageToOpenAI`, `_assistantMessageToolCallToOpenAI`, `_toolResultMessageToOpenAI`
|
||||
- Converts `ctx.tools` to OpenAI function tool format via `_toolsToOpenAI`
|
||||
- `IMP-012`: Implement message converters (`_userMessageToOpenAI`, `_assistantMessageToOpenAI`, `_assistantMessageToolCallToOpenAI`, `_toolResultMessageToOpenAI`) — **COMPLETED** (see `src/utils.jl:321-387`)
|
||||
- `IMP-013`: Implement `_messageContentToBlocks` — converts `textContent`/`imageContent` to OpenAI content blocks — **COMPLETED** (see `src/utils.jl:396-413`)
|
||||
- `IMP-014`: Implement `_toolsToOpenAI` — converts `OrderedDict{String, agentTool}` to OpenAI tools array — **COMPLETED** (see `src/utils.jl:429-447`)
|
||||
- `IMP-015`: Implement `beforeToolCall` hook default (returns `beforeToolCallResult(false, "N/A")`) — **COMPLETED** (see `src/utils.jl:278-284`)
|
||||
- `IMP-016`: Implement `afterToolCall` hook default (returns `nothing`) — **COMPLETED** (see `src/utils.jl:310-316`)
|
||||
- `IMP-017`: Implement `validateRequiredArgs(args, inputSchema)` — validates required fields in tool arguments — **COMPLETED** (see `src/utils.jl:479-497`)
|
||||
- `IMP-018`: Implement `validateToolArguments(tool, prepared)` — wrapper that calls tool-specific or default hook — **COMPLETED** (see `src/utils.jl:533-546`)
|
||||
|
||||
**Validation Tests**: Unit tests for converters, validation hooks, and OpenAI format output structure (SP-011..SP-016, SP-023, SP-024)
|
||||
|
||||
**Traceability**: FR-001, FR-002, FR-003, FR-004, FR-005, FR-006
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Tool Registry & MCP Integration (COMPLETED)
|
||||
|
||||
**Files**: `src/toolRegistry.jl`
|
||||
|
||||
**Completed Tasks**:
|
||||
- `IMP-020`: Implement `_extract_text_content(result)` — extracts text from MCP tool result content array — **COMPLETED** (see `src/toolRegistry.jl:19-33`)
|
||||
- `IMP-021`: Implement `_wrap_mcp_tool(mcpserver, tool_def)` — wraps MCP tool as `agentTool` with `execute` calling `mcpserver("tools/call", ...)` — **COMPLETED** (see `src/toolRegistry.jl:41-106`)
|
||||
- Handles MCP error responses (`haskey(response, "error")`)
|
||||
- Extracts text and `isError` from result
|
||||
- `IMP-022`: Implement `register_mcp_tools(tools, mcpserver)` — queries `tools/list` with pagination (`nextCursor`) — **COMPLETED** (see `src/toolRegistry.jl:118-186`)
|
||||
- Handles both `result.tools` and `result.error` response shapes
|
||||
- Skips already registered tools (idempotent)
|
||||
- `IMP-023`: Implement `registerTool(tools, tool)` — adds tool to OrderedDict — **COMPLETED** (see `src/toolRegistry.jl:200-205`)
|
||||
- `IMP-024`: Implement `listTool(tools, mcpserver)` — builtin tool that triggers runtime discovery and lists all tools — **COMPLETED** (see `src/toolRegistry.jl:235-271`)
|
||||
- `IMP-025`: Implement `registerAllTools(tools, mcpserver)` — discovers existing tools + registers `listTools` — **COMPLETED** (see `src/toolRegistry.jl:286-289`)
|
||||
|
||||
**Validation Tests**: Unit tests for MCP tools/list parsing, pagination, tool registration idempotency; contract tests for JSON-RPC 2.0 format (SP-009, SP-010)
|
||||
|
||||
**Traceability**: FR-002, FR-004
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Agent Core & Loop (COMPLETED)
|
||||
|
||||
**Files**: `src/agentCore.jl`
|
||||
|
||||
**Completed Tasks**:
|
||||
- `IMP-030`: Define `yiemAgent` struct with channels, callbacks, MCP server — **COMPLETED** (see `src/agentCore.jl:17-95`)
|
||||
- `IMP-031`: Implement `yiemAgent` constructor — creates channels, discovers MCP tools, spawns `_agentLoop` task — **COMPLETED** (see `src/agentCore.jl:126-199`)
|
||||
- `IMP-032`: Implement `_agentLoop(agent)` — background `@spawn` task managing `inputChannel`/`followUpChannel` priority dispatch — **COMPLETED** (see `src/agentCore.jl:225-364`)
|
||||
- Handles shutdown signal: drains all channels, breaks loop
|
||||
- On error: catches, calls `eventSink(errMsg)`, exits loop
|
||||
- `IMP-033`: Implement `_processMessage(...)` — drains input, calls `llmCall`, extracts tool calls, executes, loops until no tool calls — **COMPLETED** (see `src/agentCore.jl:394-544`)
|
||||
- `while true` loop: `llmCall()` → `_extractToolCalls()` → if `hasToolCalls && length > 0` execute → append results → repeat
|
||||
- On `toolResultBatch.terminate == true`: build final response and break
|
||||
- Catches all errors and calls `eventSink(err_msg)`
|
||||
- `IMP-034`: Implement `_extractToolCalls(response)` — handles **both** formats:
|
||||
- Format 1: `response["message"]["tool_calls"]` (LMStudio/vLLM style) — **COMPLETED** (see `src/agentCore.jl:671-683`)
|
||||
- Format 2: `response.content` blocks with `type => "tool_calls"` or `type => "tool_call"` (OpenAI API style) — **COMPLETED** (see `src/agentCore.jl:685-719`)
|
||||
- Extracts `reasoning_content`, `content` text blocks, `api`, `provider`, `model`, `usage`, `finish_reason` — **COMPLETED** (see `src/agentCore.jl:723-797`)
|
||||
- Returns `(hasToolCalls, toolCallList, assistant_message)` — **COMPLETED** (see `src/agentCore.jl:799-827`)
|
||||
- `IMP-035`: Implement `prepareToolCall(context, assistantMsg, toolCall, config, signal, eventSink)` — resolves tool, validates args, runs `beforeToolCall` hook, never throws — **COMPLETED** (see `src/agentCore.jl:974-1035`)
|
||||
- `IMP-036`: Implement `executePreparedToolCall(prep, signal, eventSink)` — calls `tool.execute(toolCallId, args, ...)` — **COMPLETED** (see `src/agentCore.jl:1078-1100`)
|
||||
- `IMP-037`: Implement `finalizeExecutedToolCall(context, msg, prep, executed, config, signal, eventSink)` — runs `afterToolCall` hook, catches errors — **COMPLETED** (see `src/agentCore.jl:1159-1198`)
|
||||
- `IMP-038`: Implement `prepareToolCallArguments(tool, toolCall)` — calls `tool.prepareArguments` if present — **COMPLETED** (see `src/agentCore.jl:908-917`)
|
||||
- `IMP-039`: Implement `createErrorToolResult(msg)` — builds `agentToolResult` with error content — **COMPLETED** (see `src/agentCore.jl:572-574`)
|
||||
- `IMP-040`: Implement `createToolResultMessage(f)` — builds `toolResultMessage` from `finalizedOutcome` — **COMPLETED** (see `src/agentCore.jl:606-612`)
|
||||
|
||||
**Note**: Sequential and parallel execution implementations continue beyond line 1207 of `src/agentCore.jl` — implementation incomplete, requires review to ensure execution methods properly call hooks and emit lifecycle events. See IMP-041.
|
||||
|
||||
**Validation Tests**: Integration tests for full agent loop with mocked LLM and MCP; contract tests for response format extraction (WT-001..WT-032, SP-017, SP-018)
|
||||
|
||||
**Traceability**: FR-001, FR-002, FR-003, FR-004, FR-005
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Public API (COMPLETED)
|
||||
|
||||
**Files**: `src/api.jl`
|
||||
|
||||
**Completed Tasks**:
|
||||
- `IMP-050`: Implement `promptAgent(agent, msg::AbstractDict)` — put to `inputChannel`, returns agent for chaining — **COMPLETED** (see `src/api.jl:36-39`)
|
||||
- `IMP-051`: Implement `promptAgent(agent, text::String)` — wraps text as OpenAI `Dict` and put to `inputChannel` — **COMPLETED** (see `src/api.jl:41-44`)
|
||||
- `IMP-052`: Implement `takeResponse(agent)` — blocks on `outputChannel` `take!`, returns OpenAI dict response — **COMPLETED** (see `src/api.jl:66-68`)
|
||||
- `IMP-053`: Implement `followUp(agent, msg::AbstractDict)` — put to `followUpChannel`, returns agent — **COMPLETED** (see `src/api.jl:94-97`)
|
||||
- `IMP-054`: Implement `followUp(agent, text::String)` — wraps text as OpenAI `Dict` and put to `followUpChannel` — **COMPLETED** (see `src/api.jl:99-102`)
|
||||
- `IMP-055`: Implement `stopAgent(agent)` — puts `:shutdown`, `fetch(_agentLoop)`, closes all channels — **COMPLETED** (see `src/api.jl:126-139`)
|
||||
|
||||
**Validation Tests**: Integration tests for send/receive/stop lifecycle (WT-001..WT-005)
|
||||
|
||||
**Traceability**: FR-001, FR-006
|
||||
|
||||
---
|
||||
|
||||
## 2. Module Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── YiemAgent.jl # Parent module, includes submodules in dependency order
|
||||
├── type.jl # All Julia struct types (exports 30+ types)
|
||||
├── utils.jl # Formatting, validation, hooks (default implementations)
|
||||
├── toolRegistry.jl # MCP tool discovery, registration, listTools builtin
|
||||
├── agentCore.jl # yiemAgent struct, _agentLoop, _processMessage, extractToolCalls
|
||||
└── api.jl # Public: promptAgent, takeResponse, followUp, stopAgent
|
||||
```
|
||||
|
||||
**Include order** (per `src/YiemAgent.jl` lines 6-19):
|
||||
1. `type.jl` (no dependencies)
|
||||
2. `utils.jl` (depends on `type`)
|
||||
3. `toolRegistry.jl` (depends on `type`)
|
||||
4. `agentCore.jl` (depends on `type`, `utils`, `toolRegistry`)
|
||||
5. `api.jl` (depends on `type`, `utils`, `agentCore`, `toolRegistry`)
|
||||
|
||||
---
|
||||
|
||||
## 3. Task Dependencies
|
||||
|
||||
| Task ID | Title | Depends On | Status |
|
||||
|---------|-------|------------|--------|
|
||||
| IMP-001..IMP-008 | Type system | — | **COMPLETED** |
|
||||
| IMP-010..IMP-018 | Utils (formatMsgForLLM, converters, hooks) | IMP-001..IMP-008 | **COMPLETED** |
|
||||
| IMP-020..IMP-025 | Tool registry (MCP discovery) | IMP-003, IMP-017 | **COMPLETED** |
|
||||
| IMP-030..IMP-040 | Agent core (agent loop, tool call execution) | IMP-001..IMP-008, IMP-010..IMP-018, IMP-020..IMP-025 | **INCOMPLETE*** (see note) |
|
||||
| IMP-050..IMP-055 | Public API | IMP-030..IMP-040 | **COMPLETED** |
|
||||
|
||||
**NOTE on IMP-030..IMP-040**: The sequential and parallel execution functions (`executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls`) begin after line 1207 of `src/agentCore.jl` but the full implementation is truncated in file reads and requires review. The exported functions (`executePreparedToolCall`, `prepareToolCall`, `executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls`) are declared (`src/agentCore.jl:4-5`) but their full bodies may be incomplete. **Requires manual validation.**
|
||||
|
||||
**Traceability**: IMP-001..IMP-008 → SP-001..SP-008, IMP-020..IMP-025 → SP-009, SP-010; IMP-010..IMP-018 → SP-011..SP-016, SP-023, SP-024; IMP-030..IMP-040 → SP-017, SP-018, SP-019..SP-022
|
||||
|
||||
---
|
||||
|
||||
## 4. Remaining Work (TODOs)
|
||||
|
||||
| File | Line | Description | Priority |
|
||||
|------|------|-------------|----------|
|
||||
| `src/utils.jl` | 114 | Filter tools from `state.tools` based on user intent and tool description | High |
|
||||
| `src/utils.jl` | 118 | Add system prompt context: time, session state, user preferences | Medium |
|
||||
| `src/utils.jl` | 121 | Prune/reorder/conversation messages before sending to LLM | Medium |
|
||||
| `src/agentCore.jl` | post-1207 | Complete and validate `executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls` implementations (currently exported but body may be incomplete) | **Critical** |
|
||||
| `tests/unit/...` | — | Write unit tests for all struct types (IMP-001..IMP-008) and converters (SP-011..SP-016) | High |
|
||||
| `tests/unit/...` | — | Write unit tests for tool validation hooks (SP-023, SP-024) | High |
|
||||
| `tests/integration/...` | — | Write integration tests for agent loop with mocked LLM/MCP (WT-001..WT-032) | High |
|
||||
| `tests/contract/...` | — | Write contract tests for response formats (SP-017, SP-018) | Medium |
|
||||
| `tests/perf/...` | — | Write performance tests (NFR-101, NFR-102) | Medium |
|
||||
|
||||
---
|
||||
|
||||
## 5. Build & Deployment
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
cd /home/ton/docker-apps/sommpanion/YiemAgent
|
||||
julia --project=. -e 'using Pkg; Pkg.instantiate(); using Pkg; Pkg.precompile()'
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
julia --project=./tests -e 'using Pkg; Pkg.instantiate()'
|
||||
julia --project=./tests -e 'using Test; Test.@testset "Unit" begin include("tests/unit/run_tests.jl") end'
|
||||
julia --project=./tests -e 'using Test; Test.@testset "Integration" begin include("tests/integration/run_tests.jl") end'
|
||||
```
|
||||
|
||||
### CI/CD Preparation
|
||||
|
||||
- Add `.github/workflows/julia.yaml` referencing `ci/julia-pipeline.yaml` steps
|
||||
- Add `Project.toml` dependencies:
|
||||
```toml
|
||||
[deps]
|
||||
JSON = "682c8566-3430-5439-8f4e-b64832e8aaba"
|
||||
DataStructures = "892a3cad-33e6-51e5-808c-f3d90072be70"
|
||||
HTTP = "cd6eee30-4dd5-5329-8bad-3259ec48d0f4"
|
||||
# plus: UUIDs, Dates, PrettyPrinting, Serialization, DataFrames, NATS, LibPQ
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Risk Mitigation
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Line 1207+ of `agentCore.jl` (sequential/parallel execution) is incomplete or broken | Manually review and validate `executeToolCallsSequential`, `executeToolCallsParallel`, `executeToolCalls` against spec SP-021 (agentLoopConfig) and trace to test `test_tool_call_execution_sequence` |
|
||||
| `GeneralUtils` internal package unavailable in CI | Ensure `GeneralUtils` is installed as dependency (via `Pkg.develop` or registry) |
|
||||
| NATS `mcpServer` struct is abstract — no concrete implementation | Provide at least one mock `mcpServer` for tests that returns canned JSON responses |
|
||||
| Message history grows unbounded | Implement `prepareContext` pruning (TODO in code); add NFR-104 for max history tokens |
|
||||
@@ -0,0 +1,445 @@
|
||||
# Runbook: YiemAgent Agent Framework
|
||||
|
||||
**Severity**: N/A (library — not a deployed service)
|
||||
**Estimated Time**: N/A
|
||||
|
||||
This runbook documents operational guidance for developers working with the YiemAgent agent framework. It is not an operations runbook for a production service, but a developer handbook for debugging, troubleshooting, and extending the framework.
|
||||
|
||||
---
|
||||
|
||||
## 1. Agent Lifecycle
|
||||
|
||||
### 1.1: Create an Agent
|
||||
|
||||
**Steps**:
|
||||
```julia
|
||||
using YiemAgent
|
||||
|
||||
# 1. Implement llmCall function
|
||||
function my_llmCall(messages; eventSink=nothing)
|
||||
# Call LLM API (e.g., OpenAI, Anthropic, LMStudio)
|
||||
# Return dict with keys: "message", "content", "finish_reason", "api", "provider", "model", "usage"
|
||||
eventSink("my_llmCall: sending $(length(messages["messages"])) messages"; publish=true)
|
||||
# ... API call ...
|
||||
return response_dict
|
||||
end
|
||||
|
||||
# 2. Implement mcpServer callable struct
|
||||
struct MyMCPClient
|
||||
conn::NATS.Connection # or HTTP.Client
|
||||
topic::String
|
||||
senderID::String
|
||||
end
|
||||
|
||||
function (mcp::MyMCPClient)(method::String, args...; eventSink=nothing)
|
||||
# JSON-RPC 2.0 calls:
|
||||
# mcp("tools/list", sessionId; eventSink=eventSink)
|
||||
# mcp("tools/call", toolName, args, sessionId; eventSink=eventSink)
|
||||
# ... NATS/HTTP call ...
|
||||
return jsonrpc_response
|
||||
end
|
||||
|
||||
# 3. Create agent
|
||||
mcp = MyMCPClient(nats_conn, "yiemagent:tools", "agent_1")
|
||||
agent = yiemAgent(
|
||||
my_llmCall;
|
||||
systemPrompt="You are a wine assistant.\nYou have tools for searching wine inventory.",
|
||||
tools=mcp_tools, # optional: pass pre-discovered tools
|
||||
prepareContext=my_prepareContext, # optional: custom context preparation
|
||||
formatMsgForLLM=my_formatMsgForLLM, # optional: custom formatting for provider
|
||||
beforeToolCall=my_beforeToolCall, # optional: approval gate
|
||||
afterToolCall=my_afterToolCall, # optional: result sanitization
|
||||
mcpServer=mcp,
|
||||
parallelToolExecute=false,
|
||||
eventSink=my_eventSink,
|
||||
)
|
||||
```
|
||||
|
||||
> **Reference**: `2-solution_design.md:SD-001` (channel-based async), `SD-002` (background task), `SD-004` (MCP struct)
|
||||
|
||||
### 1.2: Send a Message
|
||||
|
||||
```julia
|
||||
# Option 1: Send a string message (automatically wrapped as OpenAI dict)
|
||||
promptAgent(agent, "What wines have notes of oak and vanilla?")
|
||||
|
||||
# Option 2: Send a full OpenAI-format dict with images
|
||||
msg = Dict(
|
||||
"role" => "user",
|
||||
"content" => [
|
||||
Dict("type" => "text", "text" => "What wines match these notes?"),
|
||||
Dict("type" => "image_url", "image_url" => Dict("url" => "data:image/png;base64,..."))
|
||||
]
|
||||
)
|
||||
promptAgent(agent, msg)
|
||||
```
|
||||
|
||||
### 1.3: Receive a Response
|
||||
|
||||
```julia
|
||||
# Blocks until agent finishes processing (no tool calls or tool calls complete)
|
||||
response = takeResponse(agent)
|
||||
# response is a Dict in OpenAI format:
|
||||
# Dict("role" => "assistant", "content" => [Dict("type" => "text", "text" => "...")])
|
||||
|
||||
# For text-only response:
|
||||
if response["content"][1]["type"] == "text"
|
||||
println(response["content"][1]["text"])
|
||||
end
|
||||
```
|
||||
|
||||
### 1.4: Send Follow-up While Agent is Busy
|
||||
|
||||
```julia
|
||||
# While the agent is processing tool calls (between promptAgent(...) and takeResponse(...))
|
||||
followUp(agent, "Also check European wines")
|
||||
```
|
||||
|
||||
### 1.5: Stop the Agent
|
||||
|
||||
```julia
|
||||
stopAgent(agent)
|
||||
# Sends :shutdown, closes all channels. Agent object is destroyed.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Troubleshooting
|
||||
|
||||
### 2.1: Agent Loop Exits Silently
|
||||
|
||||
**Symptoms**:
|
||||
- `takeResponse(agent)` hangs indefinitely
|
||||
- No output in eventSink
|
||||
|
||||
**Steps**:
|
||||
1. Check if `eventSink` is receiving error messages:
|
||||
```julia
|
||||
global error_count = 0
|
||||
my_eventSink(msg; publish=false) = begin
|
||||
if occursin("error", lowercase(msg)) || occursin("failed", lowercase(msg))
|
||||
println("ERROR EVENT: $msg")
|
||||
global error_count += 1
|
||||
end
|
||||
end
|
||||
```
|
||||
2. Check network connectivity to LLM API endpoint
|
||||
3. Verify `formatMsgForLLM` produces valid OpenAI-format message structure (`4-specification.md:SP-011..SP-016`)
|
||||
|
||||
> **Reference**: `4-specification.md` (OpenAI format specs), `3-walkthrough.md:WT-030` (LLM timeout)
|
||||
|
||||
### 2.2: Tool Call Not Executing
|
||||
|
||||
**Symptoms**:
|
||||
- LLM returns `tool_calls` but agent returns text response immediately
|
||||
- `beforeToolCall` hook is missing or incorrectly implemented
|
||||
|
||||
**Steps**:
|
||||
1. Verify `prepareContext` returns `agentContext` with `tools` field (not `nothing`) (`src/utils.jl:123`)
|
||||
2. Check `beforeToolCall` hook is not blocking execution:
|
||||
```julia
|
||||
function my_beforeToolCall(ctx, signal, eventSink)
|
||||
# Returning nothing → allows execution
|
||||
# Returning beforeToolCallResult(true, "blocked") → blocks
|
||||
eventSink("beforeToolCall for $(ctx.toolCall.name)"; publish=true)
|
||||
nothing # ← must return nothing (not beforeToolCallResult(false, "N/A"))
|
||||
end
|
||||
```
|
||||
3. Verify tool exists in `context.tools[toolCall.name]` (`src/agentCore.jl:985`)
|
||||
4. Check `validateToolArguments` is not throwing (catches and returns error tool result) (`src/utils.jl:533`)
|
||||
|
||||
> **Reference**: `4-specification.md:SP-021` (agentLoopConfig with beforeToolCall hook), `3-walkthrough.md:WT-032` (beforeToolCall blocks)
|
||||
|
||||
### 2.3: MCP Server Returns No Tools
|
||||
|
||||
**Symptoms**:
|
||||
- `println("[toolRegistry] Discovered 0 MCP tools."` on startup
|
||||
- Agent has no tools available at all times
|
||||
|
||||
**Steps**:
|
||||
1. Verify `mcpServer` callable struct correctly handles `mcp("tools/list", sessionId; eventSink)`
|
||||
2. Check JSON-RPC 2.0 response shape matches `4-specification.md:SP-009`:
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"tools": [{"name": "...", ...}],
|
||||
"nextCursor": "" # empty string = no more pages
|
||||
}
|
||||
}
|
||||
```
|
||||
3. Verify `tools/list` error response uses `haskey(response, "error")` check (`src/toolRegistry.jl:128`)
|
||||
|
||||
> **Reference**: `4-specification.md:SP-009` (tools/list format), `3-walkthrough.md:WT-020` (MCP discovery)
|
||||
|
||||
### 2.4: LLM Response Format Not Parsed
|
||||
|
||||
**Symptoms**:
|
||||
- Agent always treats LLM response as text, never extracts tool calls
|
||||
- LLM correctly returns `tool_calls` but agent ignores them
|
||||
|
||||
**Steps**:
|
||||
1. Verify `llmCall` returns a dict containing `message` (LMStudio/vLLM format, `4-specification.md:SP-017`) or `content` (OpenAI format, `4-specification.md:SP-018`)
|
||||
2. Check for both format variations:
|
||||
- Format 1: `response["message"]["tool_calls"]` (LMStudio style, see `src/agentCore.jl:671-683`)
|
||||
- Format 2: `response.content[{type => "tool_calls"}]` (OpenAI style, see `src/agentCore.jl:700-705`)
|
||||
3. Verify `finish_reason` field matches: `"tool_calls"` triggers tool execution, `"stop"`/`"end_turn"` breaks loop (`src/agentCore.jl:723-724`)
|
||||
|
||||
> **Reference**: `4-specification.md:SP-017, SP-018`, `3-walkthrough.md:WT-011, WT-012` (LLM response handling)
|
||||
|
||||
### 2.5: Tool Execution Hangs
|
||||
|
||||
**Symptoms**:
|
||||
- `tool.execute(...)` starts but never returns
|
||||
- EventSink receives `toolExecutionStart` but never `toolExecutionEnd`
|
||||
|
||||
**Steps**:
|
||||
1. Implement `abortSignal` checking in tool execute function:
|
||||
```julia
|
||||
function my_tool_execute(toolCallId, args, signal, eventSink)
|
||||
while !signal.aborted && !all_conditions_met
|
||||
# Long-running operation with periodic abort checks
|
||||
sleep(0.1)
|
||||
end
|
||||
if signal.aborted
|
||||
return agentToolResult([textContent("Cancelled")], Dict(), nothing, false)
|
||||
end
|
||||
return agentToolResult([textContent("Done")], Dict(), nothing, false)
|
||||
end
|
||||
```
|
||||
2. External caller sets `signal.aborted = true` (if `abortSignal` is shared via closure)
|
||||
3. Add timeout logic to tool execute function:
|
||||
```julia
|
||||
start_time = time()
|
||||
timeout_seconds = 30.0
|
||||
while time() - start_time < timeout_seconds && !signal.aborted
|
||||
# operation...
|
||||
sleep(0.5)
|
||||
end
|
||||
```
|
||||
|
||||
> **Reference**: `4-specification.md:SP-022` (abortSignal), `3-walkthrough.md:WT-030` (LLM timeout)
|
||||
|
||||
### 2.6: Message History Grows Unbounded
|
||||
|
||||
**Symptoms**:
|
||||
- Memory usage grows linearly with conversation length
|
||||
- LLM context window exceeded for long sessions
|
||||
|
||||
**Steps**:
|
||||
1. Implement pruning in `prepareContext` (`src/utils.jl:112-126`):
|
||||
```julia
|
||||
function my_prepareContext(state::agentState, eventSink)
|
||||
# Prune old messages, keep last N turns
|
||||
max_turns = 20
|
||||
recent_msgs = isempty(state.messages) ? state.messages :
|
||||
state.messages[max(1, end-2*max_turns):end]
|
||||
|
||||
# Optionally inject system context (time, user prefs)
|
||||
preparedMessages = recent_msgs
|
||||
preparedSystemPrompt = state.systemPrompt * "\nCurrent time: $(now())"
|
||||
|
||||
# Filter tools (optional: keep only relevant subset)
|
||||
filteredTools = state.tools # or filter by intent...
|
||||
|
||||
return agentContext(preparedSystemPrompt, preparedMessages, filteredTools)
|
||||
end
|
||||
```
|
||||
2. Add hard limit in `_agentLoop`:
|
||||
```julia
|
||||
if length(agent._state.messages) > 200
|
||||
# Truncate oldest messages
|
||||
agent._state.messages = agent._state.messages[end-100:end]
|
||||
end
|
||||
```
|
||||
|
||||
> **Reference**: `3-walkthrough.md` (agent message flow), `1-requirements.md:NFR-103` (channel buffer limits)
|
||||
|
||||
---
|
||||
|
||||
## 3. Extending the Framework
|
||||
|
||||
### 3.1: Add a Custom LLM Provider
|
||||
|
||||
**Steps**:
|
||||
1. Implement `llmCall` function that accepts `FormattedMessages::Dict{String, Any}` and returns response dict
|
||||
2. Ensure response dict contains at least one of:
|
||||
- `message.tool_calls` format (LMStudio/vLLM) — `4-specification.md:SP-017`
|
||||
- `content[{type=>"tool_calls"}]` format (OpenAI) — `4-specification.md:SP-018`
|
||||
3. Response dict may optionally contain: `api`, `provider`, `model`, `usage` (`llmUsage` or dict), `finish_reason`, `error_message` (`4-specification.md:SP-017`, `SP-018.4`)
|
||||
|
||||
```julia
|
||||
function my_openai_llmCall(messages::Dict{String, Any}; eventSink=nothing)
|
||||
# messages["messages"] contains OpenAI-format messages
|
||||
# messages["temperature"] (default 0.7)
|
||||
# messages["tools"] (if tools are set)
|
||||
|
||||
body = Dict(
|
||||
"model" => "gpt-4",
|
||||
"messages" => messages["messages"],
|
||||
"tools" => get(messages, "tools", Any[])
|
||||
)
|
||||
|
||||
resp = HTTP.post("https://api.openai.com/v1/chat/completions",
|
||||
headers, JSON.json(body))
|
||||
json_resp = JSON.parse(String(resp.body))
|
||||
|
||||
# Extract first completion
|
||||
choice = json_resp["choices"][1]["message"]
|
||||
|
||||
# Build uniform response
|
||||
return Dict(
|
||||
"api" => "openai",
|
||||
"provider" => "openai",
|
||||
"model" => "gpt-4",
|
||||
"message" => choice,
|
||||
"usage" => json_resp["usage"],
|
||||
"finish_reason" => json_resp["choices"][1]["finish_reason"]
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
> **Reference**: `1-requirements.md:FR-002`, `4-specification.md:SP-017, SP-018`
|
||||
|
||||
### 3.2: Add a Custom Tool
|
||||
|
||||
**Steps**:
|
||||
1. Define tool schema in `inputSchema` (JSON Schema format):
|
||||
```julia
|
||||
schema = Dict{String, Any}(
|
||||
"type" => "object",
|
||||
"properties" => Dict(
|
||||
"query" => Dict("type" => "string", "description" => "Search query"),
|
||||
"max_results" => Dict("type" => "integer", "default" => 5)
|
||||
),
|
||||
"required" => ["query"]
|
||||
)
|
||||
```
|
||||
2. Implement `execute` function:
|
||||
```julia
|
||||
tool = agentTool(
|
||||
name="searchWine",
|
||||
label="Search Wine Inventory",
|
||||
description="Search for wines matching a query",
|
||||
inputSchema=schema,
|
||||
execute=(toolCallId, args, signal, eventSink) -> begin
|
||||
query = args["query"]
|
||||
max_results = get(args, "max_results", Dict{Any,Any}()["max_results"] => 5...) # handle default
|
||||
|
||||
wines = run_search(query, max_results)
|
||||
|
||||
result_text = join(["- $(w.name) (\$$(w.price))" for w in wines], "\n")
|
||||
return agentToolResult(
|
||||
[textContent("Found $(length(wines)) wines:\n$result_text")],
|
||||
Dict("count" => length(wines)),
|
||||
nothing, false # content, details, usage, terminate
|
||||
)
|
||||
end,
|
||||
prepareArguments = nothing,
|
||||
validateRequiredArgs = nothing, # uses default validator
|
||||
parallelToolExecute = false
|
||||
)
|
||||
|
||||
# Register: push!(tools, tool.name => tool)
|
||||
```
|
||||
3. Optionally override validation:
|
||||
```julia
|
||||
function my_validate(args)
|
||||
if haskey(args, "query") && length(args["query"]) < 3
|
||||
return "Query must be at least 3 characters"
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
agentTool(
|
||||
...,
|
||||
validateRequiredArgs = my_validate, # replaces default validator
|
||||
)
|
||||
```
|
||||
|
||||
> **Reference**: `1-requirements.md:FR-004`, `4-specification.md:SP-009, SP-010`
|
||||
|
||||
### 3.3: Implement User Approval Gate (beforeToolCall Hook)
|
||||
|
||||
**Steps**:
|
||||
1. Implement `beforeToolCall` that blocks expensive/destructive operations:
|
||||
```julia
|
||||
function my_approval_toolCall(ctx::beforeToolCallContext, signal::abortSignal, eventSink)
|
||||
if ctx.toolCall.name in ("delete_wine", "update_price_bulk")
|
||||
eventSink("Approval required for $(ctx.toolCall.name) with args $(ctx.args)")
|
||||
# In production, send to UI and wait for user input
|
||||
approved = ask_user_for_approval(ctx.toolCall.name, ctx.args) # blocking call
|
||||
if !approved
|
||||
return beforeToolCallResult(true, "User denied approval for $(ctx.toolCall.name)")
|
||||
end
|
||||
end
|
||||
return nothing # allow execution
|
||||
end
|
||||
```
|
||||
|
||||
> **Reference**: `2-solution_design.md:SD-005` (lifecycle hooks), `3-walkthrough.md:WT-032` (beforeToolCall blocks)
|
||||
|
||||
### 3.4: Implement Result Sanitization (afterToolCall Hook)
|
||||
|
||||
**Steps**:
|
||||
1. Implement `afterToolCall` to mask sensitive data:
|
||||
```julia
|
||||
function my_sanitize_toolCall(ctx::afterToolCallContext, signal::abortSignal, eventSink)
|
||||
if ctx.toolCall.name == "getCustomerInfo"
|
||||
# Mask sensitive fields before LLM sees result
|
||||
result = ctx.result
|
||||
for (i, block) in enumerate(result.content)
|
||||
if block isa textContent
|
||||
masked = replace(block.text, r"\d{12}" => "***-***-****") # mask SSN
|
||||
result = agentToolResult(
|
||||
[replace(block.text, r"\d{12}" => "***-***-****") for block in result.content],
|
||||
result.details,
|
||||
result.usage,
|
||||
result.terminate
|
||||
)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
return nothing # pass through for other tools
|
||||
end
|
||||
```
|
||||
|
||||
> **Reference**: `2-solution_design.md:SD-005` (lifecycle hooks)
|
||||
|
||||
---
|
||||
|
||||
## 4. Common Error Codes
|
||||
|
||||
| Error Condition | Response Code | Description |
|
||||
|-----------------|---------------|-------------|
|
||||
| Tool not found | `immediateOutcome("Tool <name> not found", true)` | `context.tools[toolCall.name]` returns `nothing` |
|
||||
| Missing required args | `immediateOutcome("Missing required arguments: <fields>", true)` | `validateRequiredArgs` returns error string |
|
||||
| Custom validation failed | `immediateOutcome("<tool.validateRequiredArgs result>", true)` | Tool-specific validation hook returns error |
|
||||
| beforeToolCall blocked | `immediateOutcome("Tool execution was blocked: <reason>", true)` | `beforeToolCall` returns `beforeToolCallResult(true, reason)` |
|
||||
| Operation aborted | `immediateOutcome("Operation aborted", true)` | `signal.aborted == true` after `beforeToolCall` |
|
||||
| Tool execution crashed | `immediateOutcome("<exception message>", true)` | `preptool.execute()` throws exception |
|
||||
| MCP error | `agentToolResult(["MCP error: <err_msg>"], {..., "error"=>err_msg}, ...)` | Server returns `response["error"]` JSON-RPC envelope |
|
||||
| MCP network error | `agentToolResult(["MCP call error: <err_msg>"], {..., "error"=>err_msg}, ...)` | Exception during `mcp("tools/call", ...)` |
|
||||
|
||||
> **Reference**: `4-specification.md:SP-009, SP-010`, `src/agentCore.jl:989, 1013, 1019, 1033, 1097`
|
||||
|
||||
---
|
||||
|
||||
## 5. Debugging Checklist
|
||||
|
||||
Per ASG Framework §Pre-Code Debugging Habit:
|
||||
|
||||
| Stage | Debug Question |
|
||||
|-------|----------------|
|
||||
| **Implementation** | If a tool is not found, does `prepareToolCall` return `immediateOutcome` with "Tool not found"? |
|
||||
| **Testing** | If an LLM response has `tool_calls`, does `_extractToolCalls` return `(true, toolCallList, assistantMessageToolCall)`? |
|
||||
| **Deploying** | If the agent loop exits, does `eventSink` contain the error message? |
|
||||
|
||||
---
|
||||
|
||||
## 6. References
|
||||
|
||||
- **API contract**: [`4-specification.md`](4-specification.md) — message types, MCP JSON-RPC format, response extraction formats
|
||||
- **Solution decisions**: [`2-solution_design.md`](2-solution_design.md) — component architecture, lifecycle hooks, format-agnostic LLM responses
|
||||
- **Walkthroughs**: [`3-walkthrough.md`](3-walkthrough.md) — end-to-end agent flow, error paths, hook sequences
|
||||
- **Validation tests**: [`6-validation.md`](6-validation.md) — test traceability matrix mapping requirements/specs to test files
|
||||
@@ -0,0 +1,94 @@
|
||||
can you make sure yiemAgent use correct format when contacting MCP server. you can check MCP server doc at /home/ton/docker-apps/sommpanion/mcp/docs/specification.md?
|
||||
|
||||
smartpack() and smartunpack() doc is at /home/ton/docker-apps/sommpanion/msghandler/README.md
|
||||
|
||||
user code that use yiemAgent is here /home/ton/docker-apps/sommpanion/agent-backend/test/runtest.jl
|
||||
|
||||
this is log folder that logs when I ran yiemAgent. you can check /home/ton/docker-apps/sommpanion/agent-backend/log/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+148
-175
@@ -19,14 +19,46 @@ mutable struct yiemAgent <: agent # High-level agent wrapper
|
||||
|
||||
# user sends prompt message to agent. if agent is idle, it process user message right away.
|
||||
# if agent is running, it process user message after the current tool call finished.
|
||||
# Expect each message in OpenAI chat format:
|
||||
# image1_path = "test/large_image.png"
|
||||
# image1_bytes = read(image1_path)
|
||||
# image1_base64_string = base64encode(image1_bytes)
|
||||
# mime_type = "image/png"
|
||||
# data1_uri = "data:$(mime_type);base64,$(image1_base64_string)"
|
||||
|
||||
# # 1. Read local file and encode to base64 string
|
||||
# image2_path = "test/small_image.png"
|
||||
# image2_bytes = read(image2_path)
|
||||
# image2_base64_string = base64encode(image2_bytes)
|
||||
# mime_type = "image/png"
|
||||
# data2_uri = "data:$(mime_type);base64,$(image2_base64_string)"
|
||||
|
||||
# # 3. Construct payload with the Data URI
|
||||
# message = Dict(
|
||||
# "role" => "user",
|
||||
# "content" => [
|
||||
# Dict("type" => "text", "text" =>
|
||||
# "
|
||||
# Do you have Brunello di Montalcino from Tenuta CastelGiocon?
|
||||
# "),
|
||||
# Dict(
|
||||
# "type" => "image_url",
|
||||
# "image_url" => Dict("url" => data1_uri)
|
||||
# )
|
||||
# ]
|
||||
# )
|
||||
inputChannel::Channel
|
||||
|
||||
# Buffers messages the user sends while the agent is busy. Processed after all inputChannel
|
||||
# messages are handled and the agent is idle (not using a tool call).
|
||||
# Buffers follow-up messages the user sends while the agent is busy. Uses the same
|
||||
# OpenAI chat format as inputChannel. Processed after all inputChannel messages are
|
||||
# handled and the agent is idle (not in the middle of a tool call).
|
||||
followUpChannel::Channel
|
||||
|
||||
# agent sends response message to user after processing all user messages in inputChannel
|
||||
# and all followUp messages.
|
||||
# agent sends response message to user after processing all user messages in inputChannel
|
||||
# and all followUp messages. Response is in OpenAI message format:
|
||||
# Dict("role" => "assistant", "content" => [
|
||||
# Dict("type" => "text", "text" => "Response text here")
|
||||
# ])
|
||||
outputChannel::Channel
|
||||
|
||||
_agentLoop::Union{Task, Nothing} # agent loop running in the background
|
||||
@@ -44,81 +76,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..)
|
||||
@@ -187,6 +146,27 @@ function yiemAgent(
|
||||
followUp = Channel(32)
|
||||
outputChannel = Channel(16)
|
||||
|
||||
if mcpServer === nothing
|
||||
error(""" please implement the following MCP server interface
|
||||
struct mcpServer
|
||||
natsConn::NATS.Connection
|
||||
topic::String
|
||||
senderID::String
|
||||
end
|
||||
|
||||
# MCP protocol: tools/list — returns tool definitions (JSON-RPC 2.0)
|
||||
# check input and output format from MCP server doc
|
||||
function (mcp::mcpServer)(method::String, sessionId::String; eventSink=nothing)::AbstractDict{String, <:Any}
|
||||
error("please implement MCP server tools/list interface")
|
||||
end
|
||||
|
||||
# MCP protocol: tools/call — executes a tool (JSON-RPC 2.0)
|
||||
function (mcp::mcpServer)(method::String, toolName::String, arguments::Dict{String, Any},
|
||||
sessionId::String; eventSink=nothing)::AbstractDict{String, <:Any}
|
||||
error("please implement MCP server interface")
|
||||
end
|
||||
""")
|
||||
end
|
||||
|
||||
tools = OrderedDict{String, agentTool}()
|
||||
registerAllTools(tools, mcpServer; eventSink=eventSink, sessionId=sessionId)
|
||||
@@ -284,18 +264,18 @@ function _agentLoop(agent::yiemAgent)
|
||||
while true
|
||||
while newUserMsg === nothing
|
||||
if isready(agent.inputChannel)
|
||||
agent.eventSink("_agentLoop 1 agent._state.messages length $(length(agent._state.messages))")
|
||||
agent.eventSink("_agentLoop 1 agent._state.messages length $(length(agent._state.messages))"; publish=true)
|
||||
# agent process new user msg immediately after the current tool call finished.
|
||||
newUserMsg = take!(agent.inputChannel)
|
||||
agent.eventSink("new user msg")
|
||||
# agent.eventSink("new user msg\n$newUserMsg"; publish=true)
|
||||
else
|
||||
# check followUp message after _processMessage() is done
|
||||
if typeof(processingTask) == Task && istaskdone(processingTask) == true
|
||||
agent.eventSink("_agentLoop 2 agent._state.messages length $(length(agent._state.messages))")
|
||||
agent.eventSink("_agentLoop 2 agent._state.messages length $(length(agent._state.messages))"; publish=true)
|
||||
# if agent runs is done but followUpChannel has messages,
|
||||
# put new message in inputChannel instead
|
||||
if isready(agent.followUpChannel)
|
||||
agent.eventSink("_agentLoop 3 agent._state.messages length $(length(agent._state.messages))")
|
||||
agent.eventSink("_agentLoop 3 agent._state.messages length $(length(agent._state.messages))"; publish=true)
|
||||
while isready(agent.followUpChannel)
|
||||
followUpMsg = take!(agent.followUpChannel)
|
||||
put!(agent.inputChannel, followUpMsg)
|
||||
@@ -303,18 +283,18 @@ function _agentLoop(agent::yiemAgent)
|
||||
processingTask = nothing # reset
|
||||
result = nothing # reset
|
||||
else # _processMessage() done and no followUp message.
|
||||
agent.eventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))")
|
||||
agent.eventSink("_agentLoop 4 agent._state.messages length $(length(agent._state.messages))"; publish=true)
|
||||
result = deepcopy(agent._state.messages[end])
|
||||
agent.eventSink("_agentLoop 4-1 ")
|
||||
agent.eventSink("_agentLoop 4-1 "; publish=true)
|
||||
# filter out reasoningContent in-place
|
||||
filter!(c -> !(c isa reasoningContent), result.content)
|
||||
agent.eventSink("_agentLoop 5 ")
|
||||
agent.eventSink("_agentLoop 5 "; publish=true)
|
||||
# format output
|
||||
respondToUI = _assistantMessageToOpenAI(result)
|
||||
agent.eventSink("_agentLoop 6 ")
|
||||
agent.eventSink("_agentLoop 6 "; publish=true)
|
||||
put!(agent.outputChannel, respondToUI)
|
||||
if !isempty(result.content) && result.content[1] isa textContent
|
||||
agent.eventSink(result.content[1].text)
|
||||
agent.eventSink(result.content[1].text; publish=true)
|
||||
end
|
||||
processingTask = nothing # reset
|
||||
result = nothing # reset
|
||||
@@ -345,7 +325,7 @@ function _agentLoop(agent::yiemAgent)
|
||||
else
|
||||
# spawn new _processMessage() if it is not already running.
|
||||
if processingTask === nothing
|
||||
agent.eventSink("_agentLoop 5 agent._state.messages length $(length(agent._state.messages))")
|
||||
agent.eventSink("_agentLoop 5 agent._state.messages length $(length(agent._state.messages))"; publish=true)
|
||||
# discard all messages in followUpChannel
|
||||
while isready(agent.followUpChannel)
|
||||
_ = take!(agent.followUpChannel)
|
||||
@@ -376,7 +356,7 @@ function _agentLoop(agent::yiemAgent)
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
eventSink(errMsg)
|
||||
eventSink(errMsg; publish=true)
|
||||
|
||||
# On any error, send error response and exit the loop
|
||||
@error "Agent loop failed" error=e
|
||||
@@ -424,7 +404,7 @@ function _processMessage(
|
||||
afterToolCall::Union{Function, Nothing},
|
||||
parallelToolExecute::Bool,
|
||||
)::Nothing
|
||||
eventSink("_processMessage 1 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 1 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
# loop until llmCall() response didn't use tool calls
|
||||
final_response = nothing
|
||||
|
||||
@@ -445,43 +425,43 @@ function _processMessage(
|
||||
try
|
||||
# Drain inputChannel and convert OpenAI-format messages to userMessage type
|
||||
while isready(inputChannel)
|
||||
eventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 2 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
newUserMsg_openai = take!(inputChannel)
|
||||
eventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 3 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
if newUserMsg_openai === :shutdown
|
||||
eventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 4 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
# Re-emit shutdown signal for the loop to handle
|
||||
put!(inputChannel, :shutdown)
|
||||
break
|
||||
end
|
||||
eventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 5 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
newUserMsg = OpenAiToUserMessage(newUserMsg_openai)
|
||||
push!(agentMsgHistory, newUserMsg)
|
||||
eventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 6 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
end
|
||||
eventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 7 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
# call prepareContext()
|
||||
state = agentState(systemPrompt, nothing, tools, agentMsgHistory)
|
||||
eventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 8 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
preparedContext = prepareContext(state, eventSink)
|
||||
eventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 9 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
# Call formatMessagesForLLM() to format for LLM
|
||||
formattedMessages = formatMessagesForLLM(preparedContext, eventSink)
|
||||
|
||||
eventSink("_processMessage 10 formattedMessages $formattedMessages")
|
||||
# eventSink("_processMessage 10 formattedMessages $formattedMessages"; publish=true)
|
||||
|
||||
""" response example
|
||||
response = JSON.Object{String, Any}("finish_reason" => "tool_calls", "index" => 0, "message" => JSON.Object{String, Any}("role" => "assistant", "content" => "", "reasoning_content" => "Here's a thinking process:\n\n1. **Identify User Request**: The user is asking for the weather in Bangkok.\n2. **Locate Relevant Tool**: I have a `getWeather` function available.\n3. **Check Function Parameters**:\n - `city` (required): City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\n - `units` (optional, default \"celsius\"): Temperature scale (\"celsius\" or \"fahrenheit\")\n4. **Prepare Parameters**:\n - `city`: \"Bangkok, Thailand\" (adding country for clarity, though just \"Bangkok\" might work, following the example format is safer)\n - `units`: Not specified, so I'll use the default (\"celsius\")\n5. **Execute Tool Call**: Call `getWeather` with `city: \"Bangkok, Thailand\"`\n6. **Anticipate Response**: The function will return current weather and forecast data for Bangkok. I'll then format it nicely for the user.\n - *Self-Correction/Verification during thought*: The prompt says \"city: City and country, e.g., 'San Francisco, CA' or 'Tokyo, Japan'\". I'll use \"Bangkok, Thailand\". The `units` parameter is optional, so I'll omit it to use the default.\n - Proceed with tool call.✅\n", "tool_calls" => Any[JSON.Object{String, Any}("type" => "function", "function" => JSON.Object{String, Any}("name" => "getWeather", "arguments" => "{\"city\":\"Bangkok, Thailand\"}"), "id" => "6fOilR5QPcdppbAAHluhkRyUDu3oWMAL")]))
|
||||
"""
|
||||
response = llmCall(formattedMessages)
|
||||
eventSink("LLM response " * string(response))
|
||||
response = llmCall(formattedMessages, eventSink=eventSink)
|
||||
# eventSink("LLM response " * string(response); publish=true)
|
||||
|
||||
eventSink("_processMessage 11 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 11 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
# Extract tool calls from LLM response content blocks
|
||||
hasToolCalls, toolCallList, assistant_msg = _extractToolCalls(response)
|
||||
eventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("assistant_msg " * string(assistant_msg))
|
||||
eventSink("_processMessage 11-1")
|
||||
eventSink("hasToolCalls: $hasToolCalls\ntoolCallList: $toolCallList _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
eventSink("assistant_msg " * string(assistant_msg); publish=true)
|
||||
eventSink("_processMessage 11-1"; publish=true)
|
||||
|
||||
# Add assistant message (tool calls or text) to history for next LLM turn
|
||||
push!(agentMsgHistory, assistant_msg)
|
||||
@@ -496,21 +476,21 @@ function _processMessage(
|
||||
)
|
||||
|
||||
signal = abortSignal(false)
|
||||
eventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 12 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
|
||||
# call executeToolCalls()
|
||||
toolResultBatch = executeToolCalls(preparedContext, assistant_msg, toolCallList, config,
|
||||
signal, eventSink)
|
||||
eventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 13 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
|
||||
# save toolResults to messages
|
||||
for toolResult in toolResultBatch.messages
|
||||
eventSink("toolResult " * string(toolResult))
|
||||
# eventSink("toolResult " * string(toolResult); publish=true)
|
||||
push!(agentMsgHistory, toolResult)
|
||||
end
|
||||
eventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 14 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
if toolResultBatch.terminate
|
||||
eventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 15 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
# If toolResultBatch requested termination, build a final response
|
||||
final_content = [textContent("Tool execution completed.")]
|
||||
for toolResult in toolResultBatch.messages
|
||||
@@ -524,7 +504,7 @@ function _processMessage(
|
||||
end
|
||||
end
|
||||
end
|
||||
eventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 16 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
final_response = assistantMessage(
|
||||
role="assistant",
|
||||
content=final_content,
|
||||
@@ -543,7 +523,7 @@ function _processMessage(
|
||||
break
|
||||
end
|
||||
else
|
||||
eventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 17 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
# LLM did not use tool calls —
|
||||
break
|
||||
end
|
||||
@@ -554,12 +534,12 @@ function _processMessage(
|
||||
println(io)
|
||||
end
|
||||
|
||||
eventSink(err_msg)
|
||||
eventSink(err_msg; publish=true)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
eventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))")
|
||||
eventSink("_processMessage 18 _state.messages length $(length(agentMsgHistory))"; publish=true)
|
||||
return nothing
|
||||
end
|
||||
|
||||
@@ -590,7 +570,7 @@ agentToolResult([textContent("text", "Tool not found")], Dict{Any,Any}())
|
||||
```
|
||||
"""
|
||||
function createErrorToolResult(msg::String)::agentToolResult
|
||||
return agentToolResult([textContent("text", msg)], dict{any,any}())
|
||||
return agentToolResult([textContent("text", msg)], Dict{Any,Any}())
|
||||
end
|
||||
|
||||
"""
|
||||
@@ -882,10 +862,10 @@ stopping the agent when other tools in the batch did not intend to terminate.
|
||||
julia> shouldTerminate(finalizedOutcome[])
|
||||
false
|
||||
|
||||
julia> shouldTerminate([finalizedOutcome(tc, agentToolResult([], dict{any,any}(), dict{any,any}(), false), false) for _ in 1:2])
|
||||
julia> shouldTerminate([finalizedOutcome(tc, agentToolResult([], Dict{Any,Any}(), Dict{Any,Any}(), false), false) for _ in 1:2])
|
||||
false
|
||||
|
||||
julia> shouldTerminate([finalizedOutcome(tc, agentToolResult([], dict{any,any}(), dict{any,any}(), true), false) for _ in 1:2])
|
||||
julia> shouldTerminate([finalizedOutcome(tc, agentToolResult([], Dict{Any,Any}(), Dict{Any,Any}(), true), false) for _ in 1:2])
|
||||
true
|
||||
```
|
||||
"""
|
||||
@@ -999,46 +979,47 @@ function prepareToolCall(
|
||||
signal::abortSignal,
|
||||
eventSink
|
||||
)::Union{preparedToolCall,immediateOutcome}
|
||||
eventSink("prepareToolCall 1")
|
||||
eventSink("prepareToolCall 1"; publish=true)
|
||||
|
||||
# pick a called tool from tool store
|
||||
tool = get(context.tools, toolCall.name, nothing)
|
||||
|
||||
if tool === nothing
|
||||
eventSink("prepareToolCall 2")
|
||||
eventSink("prepareToolCall 2"; publish=true)
|
||||
return immediateOutcome(createErrorToolResult("Tool $toolCall.name not found"), true)
|
||||
end
|
||||
|
||||
try
|
||||
eventSink("prepareToolCall 3")
|
||||
eventSink("prepareToolCall 3"; publish=true)
|
||||
# 1. prepare arguments (tool-specific transform)
|
||||
prepared = prepareToolCallArguments(tool, toolCall)
|
||||
eventSink("prepared " * string(prepared.arguments))
|
||||
eventSink("prepareToolCall 4")
|
||||
eventSink("prepared " * string(prepared.arguments); publish=true)
|
||||
eventSink("prepareToolCall 4"; publish=true)
|
||||
validatedArgs = validateToolArguments(tool, prepared)
|
||||
eventSink("validatedArgs " * string(validatedArgs))
|
||||
eventSink("prepareToolCall 5")
|
||||
eventSink("validatedArgs " * string(validatedArgs); publish=true)
|
||||
eventSink("prepareToolCall 5"; publish=true)
|
||||
# 2. beforeToolCall hook — can block
|
||||
if config.beforeToolCall !== nothing
|
||||
eventSink("prepareToolCall 6")
|
||||
eventSink("prepareToolCall 6"; publish=true)
|
||||
|
||||
before = config.beforeToolCall(
|
||||
beforeToolCallContext(assistantMsg, toolCall, validatedArgs, context),
|
||||
signal
|
||||
signal,
|
||||
eventSink
|
||||
)
|
||||
eventSink("prepareToolCall 7")
|
||||
eventSink("prepareToolCall 7"; publish=true)
|
||||
if signal.aborted
|
||||
eventSink("prepareToolCall 8")
|
||||
eventSink("prepareToolCall 8"; publish=true)
|
||||
return immediateOutcome(createErrorToolResult("Operation aborted"), true)
|
||||
end
|
||||
|
||||
if before !== nothing && before.block
|
||||
eventSink("prepareToolCall 9")
|
||||
eventSink("prepareToolCall 9"; publish=true)
|
||||
return immediateOutcome(
|
||||
createErrorToolResult(get(before, :reason, "Tool execution was blocked")), true)
|
||||
end
|
||||
end
|
||||
eventSink("prepareToolCall 10")
|
||||
eventSink("prepareToolCall 10"; publish=true)
|
||||
return preparedToolCall(tool, toolCall, validatedArgs)
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
@@ -1047,7 +1028,7 @@ function prepareToolCall(
|
||||
println(io)
|
||||
end
|
||||
|
||||
eventSink(errMsg)
|
||||
eventSink(errMsg; publish=true)
|
||||
|
||||
return immediateOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
||||
end
|
||||
@@ -1086,7 +1067,7 @@ emitted after the result has already been captured.
|
||||
```julia
|
||||
# Successful execution
|
||||
executePreparedToolCall(prep, nothing, emit)
|
||||
# => executedOutcome(agentToolResult([textContent("text", "Found 3 wines")], dict{any,any}(), dict{any,any}()), false)
|
||||
# => executedOutcome(agentToolResult([textContent("text", "Found 3 wines")], Dict{Any,Any}(), Dict{Any,Any}()), false)
|
||||
|
||||
# Execution error
|
||||
executePreparedToolCall(prep, nothing, emit)
|
||||
@@ -1099,13 +1080,11 @@ function executePreparedToolCall(
|
||||
signal::Union{Nothing,abortSignal},
|
||||
eventSink,
|
||||
)::executedOutcome
|
||||
eventSink("executePreparedToolCall 1")
|
||||
eventSink("executePreparedToolCall 1"; publish=true)
|
||||
|
||||
try #WORKING
|
||||
try
|
||||
result = prep.tool.execute(prep.toolCall.id, prep.args, signal, eventSink)
|
||||
eventSink("executePreparedToolCall 2")
|
||||
eventSink("tool execute raw result: " * sprint(show, result))
|
||||
eventSink("executePreparedToolCall 3")
|
||||
# eventSink("tool execute raw result: " * sprint(show, result); publish=true)
|
||||
return executedOutcome(result, false)
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
@@ -1113,10 +1092,11 @@ function executePreparedToolCall(
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
eventSink(errMsg)
|
||||
eventSink(errMsg; publish=true)
|
||||
|
||||
return executedOutcome(createErrorToolResult(sprint(showerror, e)), true)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
@@ -1185,23 +1165,21 @@ function finalizeExecutedToolCall(
|
||||
signal::Union{Nothing,abortSignal},
|
||||
eventSink
|
||||
)::finalizedOutcome
|
||||
eventSink("finalizeExecutedToolCall 1")
|
||||
eventSink("finalizeExecutedToolCall 1"; publish=true)
|
||||
result = executed.result
|
||||
isError = executed.isError
|
||||
eventSink("finalizeExecutedToolCall 2")
|
||||
# eventSink("finalizeExecutedToolCall result before afterToolCall\n$result"; publish=true)
|
||||
if config.afterToolCall !== nothing
|
||||
try
|
||||
after = config.afterToolCall(
|
||||
afterToolCallContext(assistantMsg, prep.toolCall, prep.args, result, isError, context),
|
||||
signal
|
||||
signal,
|
||||
eventSink
|
||||
)
|
||||
eventSink("finalizeExecutedToolCall 3")
|
||||
eventSink("finalizeExecutedToolCall 3"; publish=true)
|
||||
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)
|
||||
result = after
|
||||
isError = false
|
||||
end
|
||||
catch e
|
||||
bt = catch_backtrace()
|
||||
@@ -1209,13 +1187,13 @@ function finalizeExecutedToolCall(
|
||||
showerror(io, e, bt)
|
||||
println(io)
|
||||
end
|
||||
eventSink(errMsg)
|
||||
eventSink(errMsg; publish=true)
|
||||
|
||||
result = createErrorToolResult(sprint(showerror, e))
|
||||
isError = true
|
||||
end
|
||||
end
|
||||
eventSink("finalizeExecutedToolCall 4")
|
||||
eventSink("finalizeExecutedToolCall 4"; publish=true)
|
||||
return finalizedOutcome(prep.toolCall, result, isError)
|
||||
end
|
||||
|
||||
@@ -1281,39 +1259,39 @@ function executeToolCallsSequential(
|
||||
signal::abortSignal,
|
||||
eventSink,
|
||||
)::agentToolCallBatch
|
||||
eventSink("executeToolCallsSequential 1")
|
||||
eventSink("executeToolCallsSequential 1"; publish=true)
|
||||
finalizedCalls = finalizedOutcome[]
|
||||
messages = toolResultMessage[]
|
||||
|
||||
for tc in toolCalls
|
||||
eventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)")
|
||||
eventSink("start tool execute $(tc.id), $(tc.name), $(tc.arguments)"; publish=true)
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
|
||||
eventSink("executeToolCallsSequential " * string(prep.args))
|
||||
eventSink("executeToolCallsSequential " * string(prep.args); publish=true)
|
||||
|
||||
if prep isa immediateOutcome
|
||||
eventSink("executeToolCallsSequential 2-1")
|
||||
eventSink("executeToolCallsSequential 2-1"; publish=true)
|
||||
finalized = finalizedOutcome(tc, prep.result, prep.isError)
|
||||
eventSink("executeToolCallsSequential 2-2")
|
||||
eventSink("executeToolCallsSequential 2-2"; publish=true)
|
||||
else
|
||||
eventSink("executeToolCallsSequential 3")
|
||||
eventSink("executeToolCallsSequential 3"; publish=true)
|
||||
executed = executePreparedToolCall(prep, signal, eventSink)
|
||||
|
||||
eventSink("executeToolCallsSequential 3-1")
|
||||
eventSink("executeToolCallsSequential 3-1"; publish=true)
|
||||
finalized = finalizeExecutedToolCall(context, assistantMsg, prep, executed, config,
|
||||
signal, eventSink)
|
||||
eventSink("executeToolCallsSequential 3-2")
|
||||
eventSink("executeToolCallsSequential 3-2"; publish=true)
|
||||
end
|
||||
eventSink("executeToolCallsSequential 4")
|
||||
eventSink("finalized $(finalized.toolCall.id), $(finalized.toolCall.name),
|
||||
$(finalized.result), $(finalized.isError)")
|
||||
eventSink("executeToolCallsSequential 4"; publish=true)
|
||||
# eventSink("finalized $(finalized.toolCall.id), $(finalized.toolCall.name),
|
||||
# $(finalized.result), $(finalized.isError)"; publish=true)
|
||||
push!(messages, createToolResultMessage(finalized))
|
||||
push!(finalizedCalls, finalized)
|
||||
eventSink("executeToolCallsSequential 5")
|
||||
eventSink("executeToolCallsSequential 5"; publish=true)
|
||||
if signal !== nothing && signal.aborted
|
||||
break
|
||||
end
|
||||
end
|
||||
eventSink("executeToolCallsSequential 6")
|
||||
eventSink("executeToolCallsSequential 6"; publish=true)
|
||||
return agentToolCallBatch(messages, shouldTerminate(finalizedCalls))
|
||||
end
|
||||
|
||||
@@ -1386,7 +1364,7 @@ function executeToolCallsParallel(
|
||||
entries = Union{finalizedOutcome,Task}[]
|
||||
|
||||
for tc in toolCalls
|
||||
eventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments))
|
||||
eventSink(toolExecStartEvent(tc.id, tc.name, tc.arguments); publish=true)
|
||||
|
||||
prep = prepareToolCall(context, assistantMsg, tc, config, signal, eventSink)
|
||||
|
||||
@@ -1485,7 +1463,7 @@ function executeToolCalls(
|
||||
eventSink,
|
||||
)::agentToolCallBatch
|
||||
|
||||
eventSink("_executeToolCalls 1")
|
||||
eventSink("_executeToolCalls 1"; publish=true)
|
||||
hasSequential = false
|
||||
for tc in toolCalls
|
||||
t = get(context.tools, tc.name, nothing)
|
||||
@@ -1494,13 +1472,13 @@ function executeToolCalls(
|
||||
break
|
||||
end
|
||||
end
|
||||
eventSink("_executeToolCalls 2")
|
||||
eventSink("_executeToolCalls 2"; publish=true)
|
||||
if config.toolExecution == "sequential" || hasSequential
|
||||
eventSink("_executeToolCalls 3")
|
||||
eventSink("_executeToolCalls 3"; publish=true)
|
||||
return executeToolCallsSequential(context, assistantMsg, toolCalls, config, signal,
|
||||
eventSink)
|
||||
else
|
||||
eventSink("_executeToolCalls 4")
|
||||
eventSink("_executeToolCalls 4"; publish=true)
|
||||
return executeToolCallsParallel(context, assistantMsg, toolCalls, config, signal,
|
||||
eventSink)
|
||||
end
|
||||
@@ -1541,13 +1519,13 @@ OpenAiToUserMessage(msg)
|
||||
# => userMessage("user", [textContent("Hello")], DateTime(...))
|
||||
```
|
||||
"""
|
||||
function OpenAiToUserMessage(msg::Dict)::userMessage
|
||||
function OpenAiToUserMessage(msg::AbstractDict)::userMessage
|
||||
content_blocks = Vector{messageContent}()
|
||||
|
||||
raw_content = get(msg, "content", Any[])
|
||||
if raw_content isa Vector
|
||||
for block in raw_content
|
||||
if block isa Dict
|
||||
if typeof(block) <: AbstractDict
|
||||
block_type = get(block, "type", "")
|
||||
if block_type == "text"
|
||||
text = get(block, "text", "")
|
||||
@@ -1636,11 +1614,6 @@ end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+10
@@ -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" => [Dict("type" => "text", "text" => 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" => [Dict("type" => "text", "text" => text)]))
|
||||
return agent
|
||||
end
|
||||
|
||||
"""
|
||||
Gracefully stop the agent.
|
||||
|
||||
|
||||
+63
-63
@@ -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; eventSink=eventSink)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -122,7 +122,7 @@ function register_mcp_tools(tools::OrderedDict{String, agentTool}, mcpserver; ev
|
||||
new_count = 0
|
||||
try
|
||||
eventSink("register_mcp_tools 1")
|
||||
response = mcpserver("tools/list", sessionId)
|
||||
response = mcpserver("tools/list", sessionId; eventSink=eventSink)
|
||||
eventSink("register_mcp_tools 2")
|
||||
# Parse JSON-RPC 2.0 response envelope
|
||||
if haskey(response, "error")
|
||||
@@ -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; eventSink=eventSink) : mcpserver("tools/list"; eventSink=eventSink)
|
||||
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)
|
||||
|
||||
+13
-24
@@ -2,8 +2,8 @@ module utils
|
||||
|
||||
export clearhistory, availableWineToText, prepareContext, formatMsgForLLM, validateRequiredArgs,
|
||||
validateToolArguments, _userMessageToOpenAI,
|
||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks, _toolsToOpenAI,
|
||||
beforeToolCall, afterToolCall, eventSink
|
||||
_assistantMessageToOpenAI, _toolResultMessageToOpenAI, _messageContentToBlocks,
|
||||
_toolsToOpenAI, beforeToolCall, afterToolCall
|
||||
|
||||
using UUIDs, Dates, DataStructures, HTTP, JSON, NATS
|
||||
using GeneralUtils
|
||||
@@ -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
|
||||
@@ -275,14 +275,10 @@ function beforeToolCall(context::beforeToolCallContext, signal::abortSignal)
|
||||
end
|
||||
```
|
||||
"""
|
||||
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal
|
||||
function beforeToolCall(context::beforeToolCallContext, signal::abortSignal, eventSink
|
||||
)::beforeToolCallResult
|
||||
|
||||
# final context check
|
||||
|
||||
# seek user approval via UI
|
||||
|
||||
# other check
|
||||
eventSink("beforeToolCall: $(context.toolCall.name) args=$(context.args)")
|
||||
|
||||
return beforeToolCallResult(false, "N/A")
|
||||
end
|
||||
@@ -310,22 +306,15 @@ Common use cases:
|
||||
- `agentToolResult(...)` to return a modified result (content, details, usage,
|
||||
terminate flag can all be overridden).
|
||||
|
||||
"""
|
||||
function afterToolCall(context::afterToolCallContext, signal::abortSignal
|
||||
""" #WORKING arrange result from searchWine tool
|
||||
function afterToolCall(context::afterToolCallContext, signal::abortSignal, eventSink
|
||||
)::Union{agentToolResult, Nothing}
|
||||
|
||||
# modify context.result if needed and return agentToolResult
|
||||
eventSink("afterToolCall: $(context.toolCall.name) result=$(context.result) error=$(context.isError)")
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
|
||||
#TODO
|
||||
function eventSink(x)
|
||||
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
Convert a userMessage to OpenAI message format.
|
||||
"""
|
||||
@@ -432,7 +421,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 +467,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 +522,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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user