mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
feat(engine): Phase 3 — Monty Python executor with RLM pattern
Add CodeAct execution (Tier 1) using the Monty embedded Python interpreter, following the Recursive Language Model (RLM) pattern from arXiv:2512.24601. Key additions: - executor/scripting.rs: Monty integration with FunctionCall-based tool dispatch, catch_unwind panic safety, resource limits (30s, 64MB, 1M allocs) - LlmResponse::Code variant + ExecutionTier::Scripting - Context-as-variables (RLM 3.4): thread messages, goal, step_number, previous_results injected as Python variables — LLM context stays lean while code accesses data selectively - llm_query(prompt, context) (RLM 3.5): recursive subagent calls from within Python code — results stored as variables, not injected into parent's attention window (symbolic composition) - Compact output metadata between code steps instead of full stdout - MontyObject ↔ serde_json::Value bidirectional conversion - Updated architecture plan with RLM design principles 74 tests passing, zero clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
Generated
+431
-8
@@ -80,7 +80,9 @@ checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"version_check",
|
||||
"zerocopy 0.8.42",
|
||||
]
|
||||
@@ -386,12 +388,51 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-polyfill"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "attribute-derive"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77"
|
||||
dependencies = [
|
||||
"attribute-derive-macro",
|
||||
"derive-where",
|
||||
"manyhow",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attribute-derive-macro"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61"
|
||||
dependencies = [
|
||||
"collection_literals",
|
||||
"interpolator",
|
||||
"manyhow",
|
||||
"proc-macro-utils",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"quote-use",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
@@ -964,6 +1005,21 @@ dependencies = [
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -1106,6 +1162,17 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
@@ -1246,6 +1313,15 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
@@ -1436,12 +1512,32 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "collection_literals"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -1597,7 +1693,7 @@ dependencies = [
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
"target-lexicon 0.12.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1644,7 +1740,7 @@ dependencies = [
|
||||
"cranelift-codegen",
|
||||
"log",
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
"target-lexicon 0.12.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1661,7 +1757,7 @@ checksum = "bb2e75d1bd43dfec10924798f15e6474f1dbf63b0024506551aa19394dbe72ab"
|
||||
dependencies = [
|
||||
"cranelift-codegen",
|
||||
"libc",
|
||||
"target-lexicon",
|
||||
"target-lexicon 0.12.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1724,6 +1820,12 @@ dependencies = [
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "crokey"
|
||||
version = "1.4.0"
|
||||
@@ -2054,6 +2156,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive-where"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -2418,6 +2531,17 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
@@ -2698,6 +2822,30 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "get-size-derive2"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b6d1e2f75c16bfbcd0f95d84f99858a6e2f885c2287d1f5c3a96e8444a34b4"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "get-size2"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49cf31a6d70300cf81461098f7797571362387ef4bf85d32ac47eaa59b3a5a1a"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"get-size-derive2",
|
||||
"hashbrown 0.16.1",
|
||||
"ordermap",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.24"
|
||||
@@ -2823,6 +2971,15 @@ dependencies = [
|
||||
"zerocopy 0.8.42",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hash32"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -2873,6 +3030,20 @@ dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.7.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
|
||||
dependencies = [
|
||||
"atomic-polyfill",
|
||||
"hash32",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"spin",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -3402,6 +3573,12 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interpolator"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
|
||||
|
||||
[[package]]
|
||||
name = "io-extras"
|
||||
version = "0.18.4"
|
||||
@@ -3537,6 +3714,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"monty",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3567,6 +3745,18 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-macro"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
@@ -3612,6 +3802,15 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
@@ -3994,6 +4193,29 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "manyhow"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587"
|
||||
dependencies = [
|
||||
"manyhow-macros",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "manyhow-macros"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495"
|
||||
dependencies = [
|
||||
"proc-macro-utils",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.36.1"
|
||||
@@ -4130,6 +4352,30 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "monty"
|
||||
version = "0.0.8"
|
||||
source = "git+https://github.com/pydantic/monty.git?branch=main#60538204fd9cd420d1a93042bb52f7168fb9fca2"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"fancy-regex",
|
||||
"hashbrown 0.16.1",
|
||||
"indexmap 2.13.0",
|
||||
"itertools 0.14.0",
|
||||
"libm",
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"postcard",
|
||||
"pyo3-build-config",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_text_size",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanoid"
|
||||
version = "0.4.0"
|
||||
@@ -4237,6 +4483,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4453,6 +4700,15 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordermap"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfa78c92071bbd3628c22b1a964f7e0eb201dc1456555db072beb1662ecd6715"
|
||||
dependencies = [
|
||||
"indexmap 2.13.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.5.2"
|
||||
@@ -4849,6 +5105,7 @@ dependencies = [
|
||||
"cobs",
|
||||
"embedded-io 0.4.0",
|
||||
"embedded-io 0.6.1",
|
||||
"heapless",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -4950,6 +5207,17 @@ dependencies = [
|
||||
"toml_edit 0.25.4+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-utils"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -5023,6 +5291,15 @@ dependencies = [
|
||||
"sptr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7"
|
||||
dependencies = [
|
||||
"target-lexicon 0.13.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
@@ -5087,6 +5364,28 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote-use"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"quote-use-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote-use-macros"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35"
|
||||
dependencies = [
|
||||
"proc-macro-utils",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
@@ -5513,6 +5812,72 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_python_ast"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bitflags 2.11.0",
|
||||
"compact_str",
|
||||
"get-size2",
|
||||
"is-macro",
|
||||
"memchr",
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.1",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_python_parser"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bstr",
|
||||
"compact_str",
|
||||
"get-size2",
|
||||
"memchr",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_trivia",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.1",
|
||||
"static_assertions",
|
||||
"unicode-ident",
|
||||
"unicode-normalization",
|
||||
"unicode_names2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_python_trivia"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_source_file"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"ruff_text_size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_text_size"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/astral-sh/ruff.git?rev=6ded4bed1651e30b34dd04cdaa50c763036abb0d#6ded4bed1651e30b34dd04cdaa50c763036abb0d"
|
||||
dependencies = [
|
||||
"get-size2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.40.0"
|
||||
@@ -6275,6 +6640,15 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
@@ -6373,6 +6747,27 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -6487,6 +6882,12 @@ version = "0.12.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
@@ -7376,6 +7777,28 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "unicode_names2"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd"
|
||||
dependencies = [
|
||||
"phf 0.11.3",
|
||||
"unicode_names2_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode_names2_generator"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91e5b84611016120197efd7dc93ef76774f4e084cd73c9fb3ea4a86c570c56e"
|
||||
dependencies = [
|
||||
"getopts",
|
||||
"log",
|
||||
"phf_codegen 0.11.3",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
@@ -7752,7 +8175,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"sptr",
|
||||
"target-lexicon",
|
||||
"target-lexicon 0.12.16",
|
||||
"wasm-encoder 0.221.3",
|
||||
"wasmparser 0.221.3",
|
||||
"wasmtime-asm-macros",
|
||||
@@ -7839,7 +8262,7 @@ dependencies = [
|
||||
"log",
|
||||
"object 0.36.7",
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
"target-lexicon 0.12.16",
|
||||
"thiserror 1.0.69",
|
||||
"wasmparser 0.221.3",
|
||||
"wasmtime-environ",
|
||||
@@ -7866,7 +8289,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
"target-lexicon 0.12.16",
|
||||
"wasm-encoder 0.221.3",
|
||||
"wasmparser 0.221.3",
|
||||
"wasmprinter",
|
||||
@@ -7968,7 +8391,7 @@ dependencies = [
|
||||
"cranelift-codegen",
|
||||
"gimli",
|
||||
"object 0.36.7",
|
||||
"target-lexicon",
|
||||
"target-lexicon 0.12.16",
|
||||
"wasmparser 0.221.3",
|
||||
"wasmtime-cranelift",
|
||||
"wasmtime-environ",
|
||||
@@ -8183,7 +8606,7 @@ dependencies = [
|
||||
"gimli",
|
||||
"regalloc2",
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
"target-lexicon 0.12.16",
|
||||
"wasmparser 0.221.3",
|
||||
"wasmtime-cranelift",
|
||||
"wasmtime-environ",
|
||||
|
||||
@@ -86,15 +86,34 @@ The engine defines three traits that the host crate implements:
|
||||
|
||||
## Execution Loop
|
||||
|
||||
`ExecutionLoop::run()` mirrors `run_agentic_loop()`:
|
||||
`ExecutionLoop::run()` handles three `LlmResponse` variants:
|
||||
|
||||
1. Check signals (Stop, InjectMessage) via `mpsc::Receiver`
|
||||
2. Build context (messages + available actions from active leases)
|
||||
3. Call LLM via `LlmBackend::complete()`
|
||||
4. If text: check tool intent nudge, return if final response
|
||||
5. If action calls: for each call, find lease → check policy → consume use → execute via `EffectExecutor` → record result
|
||||
6. Record Step, emit ThreadEvents
|
||||
7. Repeat until: text response, stop signal, max iterations, or approval needed
|
||||
4. **If `Text`**: check tool intent nudge, return if final response
|
||||
5. **If `ActionCalls`** (Tier 0): for each call, find lease → check policy → consume use → execute via `EffectExecutor` → record result
|
||||
6. **If `Code`** (Tier 1): execute Python via Monty with context-as-variables and `llm_query()` support → compact metadata in context
|
||||
7. Record Step, emit ThreadEvents
|
||||
8. Repeat until: text response, stop signal, max iterations, or approval needed
|
||||
|
||||
## CodeAct / Monty Integration (Tier 1)
|
||||
|
||||
Python execution via Monty interpreter (`executor/scripting.rs`). Follows the RLM (Recursive Language Model) pattern.
|
||||
|
||||
**Context as variables** (not attention input):
|
||||
- Thread messages injected as `context` Python variable
|
||||
- Thread goal as `goal`, step index as `step_number`
|
||||
- Prior action results as `previous_results` dict
|
||||
- The LLM's chat context stays lean; full data lives in REPL variables
|
||||
|
||||
**Tool dispatch**: Unknown function calls suspend the VM → lease check → policy check → `EffectExecutor` → result returned to Python.
|
||||
|
||||
**`llm_query(prompt, context)`**: Recursive subagent call. Suspends VM → spawns single-shot LLM call → returns text result as Python string. Results stay as variables (symbolic composition), not injected into parent's attention window.
|
||||
|
||||
**Compact output metadata**: Between code steps, only a summary is added to chat context (`"[code output] stdout (4532 chars): The results show..."`) — not the full output. This prevents context bloat across iterations.
|
||||
|
||||
**Resource limits**: 30s timeout, 64MB memory, 1M allocations. All execution wrapped in `catch_unwind` for Monty panic safety.
|
||||
|
||||
## Capability Leases
|
||||
|
||||
@@ -125,8 +144,9 @@ CredentialedNetwork, Compute, Financial
|
||||
1. **No dependency on main `ironclaw` crate** — clean separation, testable in isolation
|
||||
2. **No safety logic** — sanitization/leak detection is applied at the adapter boundary (`EffectExecutor` impl)
|
||||
3. **Event sourcing from day one** — every thread records a complete event log via `ThreadEvent`
|
||||
4. **Tier 0 only (MVP)** — structured tool calls. CodeAct (Tier 1-3) added in Phase 3
|
||||
4. **Tier 0 + Tier 1** — structured tool calls (Tier 0) and embedded Python via Monty (Tier 1, CodeAct)
|
||||
5. **Engine owns its message type** — `ThreadMessage` is simpler than `ChatMessage`; bridge adapters handle conversion
|
||||
6. **RLM pattern** — context as variable (not attention input), recursive `llm_query()`, compact output metadata between steps
|
||||
|
||||
## Code Style
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ dist = false
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
monty = { git = "https://github.com/pydantic/monty.git", branch = "main" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::traits::llm::{LlmBackend, LlmCallConfig};
|
||||
use crate::types::error::EngineError;
|
||||
use crate::types::event::EventKind;
|
||||
use crate::types::message::ThreadMessage;
|
||||
use crate::types::step::{LlmResponse, Step, StepStatus};
|
||||
use crate::types::step::{ExecutionTier, LlmResponse, Step, StepStatus};
|
||||
use crate::types::thread::{Thread, ThreadState};
|
||||
|
||||
/// The core execution loop for a thread.
|
||||
@@ -216,6 +216,82 @@ impl ExecutionLoop {
|
||||
return Ok(outcome);
|
||||
}
|
||||
}
|
||||
|
||||
LlmResponse::Code { code, content } => {
|
||||
nudge_count = 0;
|
||||
|
||||
// Record assistant message with the code
|
||||
self.thread.add_message(ThreadMessage::assistant(
|
||||
content.unwrap_or_else(|| format!("```python\n{code}\n```")),
|
||||
));
|
||||
|
||||
step.status = StepStatus::Executing;
|
||||
step.tier = ExecutionTier::Scripting;
|
||||
|
||||
let exec_ctx = ThreadExecutionContext {
|
||||
thread_id: self.thread.id,
|
||||
thread_type: self.thread.thread_type,
|
||||
project_id: self.thread.project_id,
|
||||
user_id: self.user_id.clone(),
|
||||
step_id: step.id,
|
||||
};
|
||||
|
||||
// Execute via Monty (with LLM for recursive llm_query)
|
||||
let code_result = crate::executor::scripting::execute_code(
|
||||
&code,
|
||||
&self.thread,
|
||||
&self.llm,
|
||||
&self.effects,
|
||||
&self.leases,
|
||||
&self.policy,
|
||||
&exec_ctx,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Track recursive LLM token usage
|
||||
self.thread.total_tokens_used +=
|
||||
code_result.recursive_tokens.total();
|
||||
|
||||
// Record events
|
||||
for event_kind in code_result.events {
|
||||
self.thread.add_event(event_kind);
|
||||
}
|
||||
|
||||
// Add action results as messages
|
||||
for result in &code_result.action_results {
|
||||
self.thread.add_message(ThreadMessage::action_result(
|
||||
&result.call_id,
|
||||
&result.action_name,
|
||||
serde_json::to_string(&result.output).unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
|
||||
step.action_results = code_result.action_results;
|
||||
|
||||
// Use compact metadata for output (RLM pattern:
|
||||
// keep LLM context lean, full data in REPL variables)
|
||||
let metadata = crate::executor::scripting::compact_output_metadata(
|
||||
&code_result.stdout,
|
||||
&code_result.return_value,
|
||||
);
|
||||
self.thread.add_message(ThreadMessage::system(metadata));
|
||||
|
||||
step.status = StepStatus::Completed;
|
||||
step.completed_at = Some(chrono::Utc::now());
|
||||
self.thread.add_event(EventKind::StepCompleted {
|
||||
step_id: step.id,
|
||||
tokens: step.tokens_used,
|
||||
});
|
||||
self.thread.step_count += 1;
|
||||
|
||||
// Check if approval is needed
|
||||
if let Some(outcome) = code_result.need_approval {
|
||||
self.thread
|
||||
.transition_to(ThreadState::Waiting, Some("awaiting approval".into()))?;
|
||||
return Ok(outcome);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
pub mod context;
|
||||
pub mod intent;
|
||||
pub mod loop_engine;
|
||||
pub mod scripting;
|
||||
pub mod structured;
|
||||
|
||||
pub use loop_engine::ExecutionLoop;
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
//! Tier 1 executor: embedded Python via Monty.
|
||||
//!
|
||||
//! Executes LLM-generated Python code using the Monty interpreter. Tool
|
||||
//! calls happen as regular function calls in the code — Monty suspends at
|
||||
//! each unknown function, and we delegate to the `EffectExecutor`.
|
||||
//!
|
||||
//! Follows the RLM (Recursive Language Model) pattern: thread context is
|
||||
//! injected as Python variables (not LLM attention input), and `llm_query()`
|
||||
//! enables recursive subagent spawning from within code.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use monty::{
|
||||
ExcType, ExtFunctionResult, LimitedTracker, MontyException, MontyObject, MontyRun,
|
||||
NameLookupResult, PrintWriter, ResourceLimits, RunProgress,
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::capability::lease::LeaseManager;
|
||||
use crate::capability::policy::{PolicyDecision, PolicyEngine};
|
||||
use crate::traits::effect::{EffectExecutor, ThreadExecutionContext};
|
||||
use crate::traits::llm::{LlmBackend, LlmCallConfig};
|
||||
use crate::types::error::EngineError;
|
||||
use crate::types::event::EventKind;
|
||||
use crate::types::message::{MessageRole, ThreadMessage};
|
||||
use crate::types::step::{ActionResult, LlmResponse, TokenUsage};
|
||||
use crate::types::thread::Thread;
|
||||
|
||||
/// Result of executing a code block.
|
||||
pub struct CodeExecutionResult {
|
||||
/// The Python return value, converted to JSON.
|
||||
pub return_value: serde_json::Value,
|
||||
/// Captured print output.
|
||||
pub stdout: String,
|
||||
/// All action calls that were made during execution.
|
||||
pub action_results: Vec<ActionResult>,
|
||||
/// Events generated during execution.
|
||||
pub events: Vec<EventKind>,
|
||||
/// If set, execution was interrupted for approval.
|
||||
pub need_approval: Option<crate::runtime::messaging::ThreadOutcome>,
|
||||
/// Tokens used by recursive llm_query() calls.
|
||||
pub recursive_tokens: TokenUsage,
|
||||
}
|
||||
|
||||
/// Default resource limits for Monty execution.
|
||||
fn default_limits() -> ResourceLimits {
|
||||
ResourceLimits::new()
|
||||
.max_duration(Duration::from_secs(30))
|
||||
.max_allocations(1_000_000)
|
||||
.max_memory(64 * 1024 * 1024) // 64 MB
|
||||
}
|
||||
|
||||
/// Maximum length of output metadata included in LLM context.
|
||||
const OUTPUT_METADATA_MAX_PREVIEW: usize = 120;
|
||||
|
||||
/// Build a compact metadata summary of code output instead of the full text.
|
||||
pub fn compact_output_metadata(stdout: &str, return_value: &serde_json::Value) -> String {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
if !stdout.is_empty() {
|
||||
let preview: String = stdout.chars().take(OUTPUT_METADATA_MAX_PREVIEW).collect();
|
||||
let truncated = if stdout.len() > OUTPUT_METADATA_MAX_PREVIEW {
|
||||
"..."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
parts.push(format!(
|
||||
"stdout ({} chars): {preview}{truncated}",
|
||||
stdout.len()
|
||||
));
|
||||
}
|
||||
|
||||
if *return_value != serde_json::Value::Null {
|
||||
let val_str = serde_json::to_string(return_value).unwrap_or_default();
|
||||
let preview: String = val_str.chars().take(OUTPUT_METADATA_MAX_PREVIEW).collect();
|
||||
let truncated = if val_str.len() > OUTPUT_METADATA_MAX_PREVIEW {
|
||||
"..."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
parts.push(format!(
|
||||
"return ({} chars): {preview}{truncated}",
|
||||
val_str.len()
|
||||
));
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
"[code executed, no output]".into()
|
||||
} else {
|
||||
format!("[code output] {}", parts.join("; "))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Context injection (RLM 3.4) ────────────────────────────
|
||||
|
||||
/// Build Monty input variables from thread state.
|
||||
///
|
||||
/// Injects thread context as Python variables so the LLM's code can
|
||||
/// access it selectively (RLM pattern: context as variable, not attention input).
|
||||
fn build_context_inputs(thread: &Thread) -> (Vec<String>, Vec<MontyObject>) {
|
||||
let mut names = Vec::new();
|
||||
let mut values = Vec::new();
|
||||
|
||||
// `context` — thread messages as a list of dicts
|
||||
let messages: Vec<MontyObject> = thread
|
||||
.messages
|
||||
.iter()
|
||||
.map(|msg| {
|
||||
let mut pairs = vec![
|
||||
(
|
||||
MontyObject::String("role".into()),
|
||||
MontyObject::String(format!("{:?}", msg.role)),
|
||||
),
|
||||
(
|
||||
MontyObject::String("content".into()),
|
||||
MontyObject::String(msg.content.clone()),
|
||||
),
|
||||
];
|
||||
if let Some(ref name) = msg.action_name {
|
||||
pairs.push((
|
||||
MontyObject::String("action_name".into()),
|
||||
MontyObject::String(name.clone()),
|
||||
));
|
||||
}
|
||||
MontyObject::dict(pairs)
|
||||
})
|
||||
.collect();
|
||||
names.push("context".into());
|
||||
values.push(MontyObject::List(messages));
|
||||
|
||||
// `goal` — the thread's goal string
|
||||
names.push("goal".into());
|
||||
values.push(MontyObject::String(thread.goal.clone()));
|
||||
|
||||
// `step_number` — current step index
|
||||
names.push("step_number".into());
|
||||
values.push(MontyObject::Int(thread.step_count as i64));
|
||||
|
||||
// `previous_results` — dict of {call_id: result_json} from prior action results
|
||||
let result_pairs: Vec<(MontyObject, MontyObject)> = thread
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|m| m.role == MessageRole::ActionResult)
|
||||
.filter_map(|m| {
|
||||
let call_id = m.action_call_id.as_ref()?;
|
||||
Some((
|
||||
MontyObject::String(call_id.clone()),
|
||||
MontyObject::String(m.content.clone()),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
names.push("previous_results".into());
|
||||
values.push(MontyObject::dict(result_pairs));
|
||||
|
||||
(names, values)
|
||||
}
|
||||
|
||||
/// Execute a Python code block using Monty.
|
||||
///
|
||||
/// Thread context is injected as Python variables (RLM pattern).
|
||||
/// Unknown function calls suspend the VM and route to the `EffectExecutor`.
|
||||
/// `llm_query(prompt, context)` calls spawn recursive child LLM calls.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute_code(
|
||||
code: &str,
|
||||
thread: &Thread,
|
||||
llm: &Arc<dyn LlmBackend>,
|
||||
effects: &Arc<dyn EffectExecutor>,
|
||||
leases: &LeaseManager,
|
||||
policy: &PolicyEngine,
|
||||
context: &ThreadExecutionContext,
|
||||
capability_policies: &[crate::types::capability::PolicyRule],
|
||||
) -> Result<CodeExecutionResult, EngineError> {
|
||||
let mut stdout = String::new();
|
||||
let mut action_results = Vec::new();
|
||||
let mut events = Vec::new();
|
||||
let mut recursive_tokens = TokenUsage::default();
|
||||
|
||||
// Build context variables (RLM 3.4)
|
||||
let (input_names, input_values) = build_context_inputs(thread);
|
||||
|
||||
// Parse and compile (wrap in catch_unwind — Monty 0.0.x can panic)
|
||||
let runner = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
MontyRun::new(code.to_string(), "step.py", input_names)
|
||||
})) {
|
||||
Ok(Ok(runner)) => runner,
|
||||
Ok(Err(e)) => {
|
||||
return Err(EngineError::Effect {
|
||||
reason: format!("Python parse error: {e}"),
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(EngineError::Effect {
|
||||
reason: "Monty VM panicked during code parsing".into(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Start execution with resource limits and context inputs
|
||||
let tracker = LimitedTracker::new(default_limits());
|
||||
|
||||
let run_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
runner.start(input_values, tracker, PrintWriter::Collect(&mut stdout))
|
||||
}));
|
||||
|
||||
let mut progress = match run_result {
|
||||
Ok(Ok(p)) => p,
|
||||
Ok(Err(e)) => {
|
||||
return Err(EngineError::Effect {
|
||||
reason: format!("Python execution error: {e}"),
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(EngineError::Effect {
|
||||
reason: "Monty VM panicked during execution start".into(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Drive the execution loop — suspend at each function call
|
||||
let mut call_counter = 0u32;
|
||||
loop {
|
||||
match progress {
|
||||
RunProgress::Complete(obj) => {
|
||||
return Ok(CodeExecutionResult {
|
||||
return_value: monty_to_json(&obj),
|
||||
stdout,
|
||||
action_results,
|
||||
events,
|
||||
need_approval: None,
|
||||
recursive_tokens,
|
||||
});
|
||||
}
|
||||
|
||||
RunProgress::FunctionCall(call) => {
|
||||
call_counter += 1;
|
||||
let call_id = format!("code_call_{call_counter}");
|
||||
let action_name = call.function_name.clone();
|
||||
let params = monty_args_to_json(&call.args, &call.kwargs);
|
||||
|
||||
debug!(action = %action_name, call_id = %call_id, "Monty: function call");
|
||||
|
||||
// Handle llm_query() — recursive subagent call (RLM 3.5)
|
||||
let ext_result = if action_name == "llm_query" {
|
||||
handle_llm_query(&call.args, &call.kwargs, llm, &mut recursive_tokens).await
|
||||
} else {
|
||||
// Regular tool dispatch through lease + policy
|
||||
let dispatch = dispatch_action(
|
||||
&action_name,
|
||||
&call_id,
|
||||
params.clone(),
|
||||
thread,
|
||||
effects,
|
||||
leases,
|
||||
policy,
|
||||
context,
|
||||
capability_policies,
|
||||
&mut action_results,
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
|
||||
match dispatch {
|
||||
DispatchResult::Ok(r) => r,
|
||||
DispatchResult::NeedApproval => {
|
||||
return Ok(CodeExecutionResult {
|
||||
return_value: serde_json::Value::Null,
|
||||
stdout,
|
||||
action_results,
|
||||
events,
|
||||
need_approval: Some(
|
||||
crate::runtime::messaging::ThreadOutcome::NeedApproval {
|
||||
action_name,
|
||||
call_id,
|
||||
parameters: params,
|
||||
},
|
||||
),
|
||||
recursive_tokens,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Resume Monty
|
||||
progress = resume_monty(
|
||||
call.resume(ext_result, PrintWriter::Collect(&mut stdout)),
|
||||
)?;
|
||||
}
|
||||
|
||||
RunProgress::NameLookup(lookup) => {
|
||||
let name = lookup.name.clone();
|
||||
debug!(name = %name, "Monty: unresolved name");
|
||||
progress = resume_monty(
|
||||
lookup.resume(
|
||||
NameLookupResult::Undefined,
|
||||
PrintWriter::Collect(&mut stdout),
|
||||
),
|
||||
)?;
|
||||
}
|
||||
|
||||
RunProgress::OsCall(os_call) => {
|
||||
warn!(function = ?os_call.function, "Monty: OS call denied");
|
||||
let err = ExtFunctionResult::Error(MontyException::new(
|
||||
ExcType::OSError,
|
||||
Some("OS operations are not permitted in CodeAct scripts".into()),
|
||||
));
|
||||
progress = resume_monty(
|
||||
os_call.resume(err, PrintWriter::Collect(&mut stdout)),
|
||||
)?;
|
||||
}
|
||||
|
||||
RunProgress::ResolveFutures(_) => {
|
||||
return Err(EngineError::Effect {
|
||||
reason: "async/await is not supported in CodeAct scripts".into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── llm_query() — recursive subagent (RLM 3.5) ─────────────
|
||||
|
||||
/// Handle a `llm_query(prompt, context)` call from within Python code.
|
||||
///
|
||||
/// Spawns a single-shot LLM call with the given prompt and context.
|
||||
/// The result is returned as a MontyObject (string), not injected into
|
||||
/// the parent's attention window (RLM pattern: symbolic composition).
|
||||
async fn handle_llm_query(
|
||||
args: &[MontyObject],
|
||||
kwargs: &[(MontyObject, MontyObject)],
|
||||
llm: &Arc<dyn LlmBackend>,
|
||||
recursive_tokens: &mut TokenUsage,
|
||||
) -> ExtFunctionResult {
|
||||
// Extract prompt (first arg or kwarg "prompt")
|
||||
let prompt = extract_string_arg(args, kwargs, "prompt", 0);
|
||||
let context_arg = extract_string_arg(args, kwargs, "context", 1);
|
||||
|
||||
let prompt = match prompt {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return ExtFunctionResult::Error(MontyException::new(
|
||||
ExcType::TypeError,
|
||||
Some("llm_query() requires a 'prompt' argument".into()),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Build messages for the child LLM call
|
||||
let mut messages = Vec::new();
|
||||
if let Some(ctx) = context_arg {
|
||||
messages.push(ThreadMessage::system(format!(
|
||||
"You are a sub-agent. Here is the context:\n\n{ctx}"
|
||||
)));
|
||||
}
|
||||
messages.push(ThreadMessage::user(prompt));
|
||||
|
||||
// Make the LLM call (no tools — pure text completion)
|
||||
let config = LlmCallConfig {
|
||||
force_text: true,
|
||||
..LlmCallConfig::default()
|
||||
};
|
||||
|
||||
match llm.complete(&messages, &[], &config).await {
|
||||
Ok(output) => {
|
||||
recursive_tokens.input_tokens += output.usage.input_tokens;
|
||||
recursive_tokens.output_tokens += output.usage.output_tokens;
|
||||
|
||||
let response_text = match output.response {
|
||||
LlmResponse::Text(text) => text,
|
||||
LlmResponse::ActionCalls { content, .. } => content.unwrap_or_default(),
|
||||
LlmResponse::Code { content, .. } => content.unwrap_or_default(),
|
||||
};
|
||||
ExtFunctionResult::Return(MontyObject::String(response_text))
|
||||
}
|
||||
Err(e) => ExtFunctionResult::Error(MontyException::new(
|
||||
ExcType::RuntimeError,
|
||||
Some(format!("llm_query failed: {e}")),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a string argument by name (kwarg) or position (positional arg).
|
||||
fn extract_string_arg(
|
||||
args: &[MontyObject],
|
||||
kwargs: &[(MontyObject, MontyObject)],
|
||||
name: &str,
|
||||
position: usize,
|
||||
) -> Option<String> {
|
||||
// Check kwargs first
|
||||
for (k, v) in kwargs {
|
||||
if let MontyObject::String(key) = k
|
||||
&& key == name
|
||||
{
|
||||
return Some(monty_to_string(v));
|
||||
}
|
||||
}
|
||||
// Then positional
|
||||
args.get(position).map(monty_to_string)
|
||||
}
|
||||
|
||||
/// Convert any MontyObject to a string representation.
|
||||
fn monty_to_string(obj: &MontyObject) -> String {
|
||||
match obj {
|
||||
MontyObject::String(s) => s.clone(),
|
||||
MontyObject::None => "None".into(),
|
||||
MontyObject::Bool(b) => b.to_string(),
|
||||
MontyObject::Int(i) => i.to_string(),
|
||||
MontyObject::Float(f) => f.to_string(),
|
||||
other => serde_json::to_string(&monty_to_json(other)).unwrap_or_else(|_| format!("{other:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dispatch result ─────────────────────────────────────────
|
||||
|
||||
enum DispatchResult {
|
||||
Ok(ExtFunctionResult),
|
||||
NeedApproval,
|
||||
}
|
||||
|
||||
/// Dispatch an action call through lease + policy + effect executor.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn dispatch_action(
|
||||
action_name: &str,
|
||||
call_id: &str,
|
||||
params: serde_json::Value,
|
||||
thread: &Thread,
|
||||
effects: &Arc<dyn EffectExecutor>,
|
||||
leases: &LeaseManager,
|
||||
policy: &PolicyEngine,
|
||||
context: &ThreadExecutionContext,
|
||||
capability_policies: &[crate::types::capability::PolicyRule],
|
||||
action_results: &mut Vec<ActionResult>,
|
||||
events: &mut Vec<EventKind>,
|
||||
) -> DispatchResult {
|
||||
// Find lease
|
||||
let lease = match leases.find_lease_for_action(thread.id, action_name).await {
|
||||
Some(l) => l,
|
||||
None => {
|
||||
events.push(EventKind::ActionFailed {
|
||||
step_id: context.step_id,
|
||||
action_name: action_name.into(),
|
||||
call_id: call_id.into(),
|
||||
error: format!("no lease for action '{action_name}'"),
|
||||
});
|
||||
return DispatchResult::Ok(ExtFunctionResult::NotFound(action_name.into()));
|
||||
}
|
||||
};
|
||||
|
||||
// Find action definition and check policy
|
||||
let action_def = effects
|
||||
.available_actions(std::slice::from_ref(&lease))
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|actions| actions.into_iter().find(|a| a.name == action_name));
|
||||
|
||||
if let Some(ref action_def) = action_def {
|
||||
match policy.evaluate(action_def, &lease, capability_policies) {
|
||||
PolicyDecision::Deny { reason } => {
|
||||
events.push(EventKind::ActionFailed {
|
||||
step_id: context.step_id,
|
||||
action_name: action_name.into(),
|
||||
call_id: call_id.into(),
|
||||
error: reason.clone(),
|
||||
});
|
||||
return DispatchResult::Ok(ExtFunctionResult::Error(MontyException::new(
|
||||
ExcType::RuntimeError,
|
||||
Some(format!("denied: {reason}")),
|
||||
)));
|
||||
}
|
||||
PolicyDecision::RequireApproval { .. } => {
|
||||
events.push(EventKind::ApprovalRequested {
|
||||
action_name: action_name.into(),
|
||||
call_id: call_id.into(),
|
||||
});
|
||||
return DispatchResult::NeedApproval;
|
||||
}
|
||||
PolicyDecision::Allow => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Consume lease use
|
||||
if let Err(e) = leases.consume_use(lease.id).await {
|
||||
return DispatchResult::Ok(ExtFunctionResult::Error(MontyException::new(
|
||||
ExcType::RuntimeError,
|
||||
Some(format!("lease exhausted: {e}")),
|
||||
)));
|
||||
}
|
||||
|
||||
// Execute the action
|
||||
match effects
|
||||
.execute_action(action_name, params, &lease, context)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
events.push(EventKind::ActionExecuted {
|
||||
step_id: context.step_id,
|
||||
action_name: action_name.into(),
|
||||
call_id: call_id.into(),
|
||||
duration_ms: result.duration.as_millis() as u64,
|
||||
});
|
||||
let monty_obj = json_to_monty(&result.output);
|
||||
action_results.push(result);
|
||||
DispatchResult::Ok(ExtFunctionResult::Return(monty_obj))
|
||||
}
|
||||
Err(e) => {
|
||||
action_results.push(ActionResult {
|
||||
call_id: call_id.into(),
|
||||
action_name: action_name.into(),
|
||||
output: serde_json::json!({"error": e.to_string()}),
|
||||
is_error: true,
|
||||
duration: Duration::ZERO,
|
||||
});
|
||||
events.push(EventKind::ActionFailed {
|
||||
step_id: context.step_id,
|
||||
action_name: action_name.into(),
|
||||
call_id: call_id.into(),
|
||||
error: e.to_string(),
|
||||
});
|
||||
DispatchResult::Ok(ExtFunctionResult::Error(MontyException::new(
|
||||
ExcType::RuntimeError,
|
||||
Some(e.to_string()),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap Monty resume results with error conversion.
|
||||
fn resume_monty<T: monty::ResourceTracker>(
|
||||
result: Result<RunProgress<T>, MontyException>,
|
||||
) -> Result<RunProgress<T>, EngineError> {
|
||||
result.map_err(|e| EngineError::Effect {
|
||||
reason: format!("Python execution error: {e}"),
|
||||
})
|
||||
}
|
||||
|
||||
// ── MontyObject ↔ JSON conversion ───────────────────────────
|
||||
|
||||
/// Convert a MontyObject to serde_json::Value.
|
||||
fn monty_to_json(obj: &MontyObject) -> serde_json::Value {
|
||||
match obj {
|
||||
MontyObject::None => serde_json::Value::Null,
|
||||
MontyObject::Bool(b) => serde_json::Value::Bool(*b),
|
||||
MontyObject::Int(i) => serde_json::json!(i),
|
||||
MontyObject::BigInt(i) => serde_json::Value::String(i.to_string()),
|
||||
MontyObject::Float(f) => serde_json::json!(f),
|
||||
MontyObject::String(s) => serde_json::Value::String(s.clone()),
|
||||
MontyObject::List(items) | MontyObject::Tuple(items) => {
|
||||
serde_json::Value::Array(items.iter().map(monty_to_json).collect())
|
||||
}
|
||||
MontyObject::Dict(pairs) => {
|
||||
let map: serde_json::Map<String, serde_json::Value> = pairs
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
let key = match k {
|
||||
MontyObject::String(s) => s.clone(),
|
||||
other => format!("{other:?}"),
|
||||
};
|
||||
(key, monty_to_json(v))
|
||||
})
|
||||
.collect();
|
||||
serde_json::Value::Object(map)
|
||||
}
|
||||
MontyObject::Set(items) | MontyObject::FrozenSet(items) => {
|
||||
serde_json::Value::Array(items.iter().map(monty_to_json).collect())
|
||||
}
|
||||
MontyObject::Bytes(b) => serde_json::Value::String(
|
||||
b.iter().map(|byte| format!("{byte:02x}")).collect(),
|
||||
),
|
||||
other => serde_json::Value::String(format!("{other:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert serde_json::Value to MontyObject.
|
||||
fn json_to_monty(val: &serde_json::Value) -> MontyObject {
|
||||
match val {
|
||||
serde_json::Value::Null => MontyObject::None,
|
||||
serde_json::Value::Bool(b) => MontyObject::Bool(*b),
|
||||
serde_json::Value::Number(n) => {
|
||||
if let Some(i) = n.as_i64() {
|
||||
MontyObject::Int(i)
|
||||
} else if let Some(f) = n.as_f64() {
|
||||
MontyObject::Float(f)
|
||||
} else {
|
||||
MontyObject::String(n.to_string())
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(s) => MontyObject::String(s.clone()),
|
||||
serde_json::Value::Array(arr) => {
|
||||
MontyObject::List(arr.iter().map(json_to_monty).collect())
|
||||
}
|
||||
serde_json::Value::Object(map) => MontyObject::dict(
|
||||
map.iter()
|
||||
.map(|(k, v)| (MontyObject::String(k.clone()), json_to_monty(v)))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Monty function call args + kwargs to a JSON object.
|
||||
fn monty_args_to_json(
|
||||
args: &[MontyObject],
|
||||
kwargs: &[(MontyObject, MontyObject)],
|
||||
) -> serde_json::Value {
|
||||
let mut map = serde_json::Map::new();
|
||||
if !args.is_empty() {
|
||||
map.insert(
|
||||
"_args".into(),
|
||||
serde_json::Value::Array(args.iter().map(monty_to_json).collect()),
|
||||
);
|
||||
}
|
||||
for (k, v) in kwargs {
|
||||
let key = match k {
|
||||
MontyObject::String(s) => s.clone(),
|
||||
other => format!("{other:?}"),
|
||||
};
|
||||
map.insert(key, monty_to_json(v));
|
||||
}
|
||||
serde_json::Value::Object(map)
|
||||
}
|
||||
@@ -41,10 +41,11 @@ pub enum StepStatus {
|
||||
/// Which execution tier handles the step's code/actions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ExecutionTier {
|
||||
/// Tier 0: structured tool calls (MVP).
|
||||
/// Tier 0: structured tool calls.
|
||||
Structured,
|
||||
/// Tier 1: embedded Python via Monty.
|
||||
Scripting,
|
||||
// Future tiers:
|
||||
// Scripting, // Tier 1: embedded Starlark/Rhai
|
||||
// Wasm, // Tier 2: WASM sandbox
|
||||
// Container, // Tier 3: Docker container
|
||||
}
|
||||
@@ -84,7 +85,7 @@ impl Step {
|
||||
|
||||
// ── LLM response types ─────────────────────────────────────
|
||||
|
||||
/// Response from the LLM: either text or action calls.
|
||||
/// Response from the LLM: text, action calls, or executable code.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum LlmResponse {
|
||||
/// Final text response.
|
||||
@@ -94,6 +95,13 @@ pub enum LlmResponse {
|
||||
calls: Vec<ActionCall>,
|
||||
content: Option<String>,
|
||||
},
|
||||
/// Executable Python code (CodeAct). Tool calls happen as function
|
||||
/// calls within the code; the runtime suspends at each one and
|
||||
/// delegates to the EffectExecutor.
|
||||
Code {
|
||||
code: String,
|
||||
content: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A request from the LLM to execute a capability action.
|
||||
|
||||
@@ -25,7 +25,9 @@ IronClaw currently has Session, Job, Routine, Channel, Tool, Skill, Hook, Observ
|
||||
4. **Effects, not commands** — capabilities declare their effect types; a deterministic policy engine enforces boundaries
|
||||
5. **Memory is docs, not logs** — durable knowledge is structured (summaries, lessons, playbooks), not raw history
|
||||
6. **CodeAct for capable models** — LLMs write code that composes tools, queries history, and spawns threads
|
||||
7. **Event sourcing from day one** — every thread records a complete execution trace for replay/debugging/reflection
|
||||
7. **Context as variable, not attention input** (RLM pattern) — thread context is a Python variable in the REPL, not tokens in the LLM window. The model writes code to selectively access it, avoiding context rot on long inputs
|
||||
8. **Recursive subagent spawning** (RLM pattern) — code can call `llm_query()` to spawn child threads inline. Results are stored as variables, not injected into the parent's context window
|
||||
9. **Event sourcing from day one** — every thread records a complete execution trace for replay/debugging/reflection
|
||||
|
||||
## The Five Primitives
|
||||
|
||||
@@ -86,7 +88,7 @@ crates/ironclaw_engine/
|
||||
mod.rs
|
||||
loop_engine.rs # ExecutionLoop (core loop replacing run_agentic_loop)
|
||||
structured.rs # Tier 0: structured tool calls
|
||||
scripting.rs # Tier 1: embedded Starlark/Rhai (Phase 3)
|
||||
scripting.rs # Tier 1: embedded Python via Monty (Phase 3)
|
||||
context.rs # Context builder (thread state + project docs + capabilities)
|
||||
intent.rs # Tool intent nudge detection
|
||||
|
||||
@@ -109,7 +111,7 @@ crates/ironclaw_engine/
|
||||
|
||||
Dependencies (minimal — no main crate dependency):
|
||||
- `tokio` (sync, time, macros, rt), `serde` + `serde_json`, `thiserror`, `tracing`, `uuid`, `chrono`, `async-trait`
|
||||
- Phase 3 adds: `starlark` or `rhai` (embedded scripting)
|
||||
- `monty` (git dep) — embedded Python interpreter for CodeAct (Tier 1)
|
||||
|
||||
---
|
||||
|
||||
@@ -242,80 +244,124 @@ cargo test
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: CodeAct Executor (Tier 1 — Embedded Scripting)
|
||||
## Phase 3: CodeAct Executor (Tier 1 — Monty Python + RLM Pattern)
|
||||
|
||||
**Goal:** LLMs can write code (Starlark or Rhai) that composes tools, uses control flow, and queries thread context. This is the key differentiator from the current tool-call model.
|
||||
**Goal:** LLMs write Python code that composes tools, uses control flow, queries thread context as data, and recursively spawns sub-agents. Uses the Monty interpreter (Pydantic) for sandboxed in-process execution. Follows the Recursive Language Model (RLM) pattern: context as a variable, not attention input.
|
||||
|
||||
### 3.1 Code runner trait
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait CodeRunner: Send + Sync {
|
||||
async fn execute(
|
||||
&self,
|
||||
code: &str,
|
||||
runtime_api: &dyn RuntimeApi,
|
||||
config: &CodeRunnerConfig,
|
||||
) -> Result<CodeResult, EngineError>;
|
||||
}
|
||||
```
|
||||
**Status:** Implemented (Phases 3.1–3.3). Phases 3.4–3.5 are the RLM enhancements.
|
||||
|
||||
### 3.2 Runtime API
|
||||
The API surface that code executes against:
|
||||
### 3.1 Monty integration (DONE)
|
||||
|
||||
`executor/scripting.rs` — Embeds the Monty Python interpreter (git dep, v0.0.8).
|
||||
|
||||
**Execution model:**
|
||||
1. `MontyRun::new(code, "step.py", input_names)` — parse Python code
|
||||
2. `runner.start(inputs, tracker, print_writer)` — begin execution with resource limits
|
||||
3. Loop over `RunProgress` suspension points:
|
||||
- `FunctionCall` → find lease → check policy → call `EffectExecutor` → resume with result
|
||||
- `NameLookup` → resolve or raise `NameError`
|
||||
- `OsCall` → deny with `OSError`
|
||||
- `ResolveFutures` → error (async not supported)
|
||||
- `Complete` → return value + captured stdout
|
||||
4. All execution wrapped in `catch_unwind` (Monty 0.0.x can panic)
|
||||
|
||||
**Resource limits:** 30s timeout, 64MB memory, 1M allocations, recursion depth 1000.
|
||||
|
||||
**Tool dispatch:** Unknown function calls in Python suspend the VM via `RunProgress::FunctionCall`. The engine routes through the same lease → policy → `EffectExecutor` pipeline as structured tool calls:
|
||||
```python
|
||||
# Thread operations (read-only access to thread state)
|
||||
thread.messages # list of messages
|
||||
thread.messages[-1] # last message
|
||||
thread.messages.filter(role="tool") # filter by role
|
||||
thread.goal # thread's goal
|
||||
thread.state # current state
|
||||
|
||||
# Capability actions (tool calls)
|
||||
tools.web_fetch(url="...") # invoke capability action
|
||||
tools.memory_search(query="...") # invoke capability action
|
||||
result = tools.shell(cmd="...") # invoke capability action
|
||||
|
||||
# Output
|
||||
thread.reply("response") # send final response
|
||||
thread.think("note") # add to context, not visible to user
|
||||
|
||||
# Thread spawning
|
||||
child = thread.spawn(goal="...", capabilities=["web_fetch"])
|
||||
results = thread.join([child1, child2, child3]) # fan-out/fan-in
|
||||
result = web_fetch(url="https://example.com") # suspends → EffectExecutor
|
||||
data = memory_search(query="deployment") # suspends → EffectExecutor
|
||||
for item in result["items"]: # control flow in Python
|
||||
memory_write(key=item["id"], value=item["summary"])
|
||||
```
|
||||
|
||||
### 3.3 Tier selection
|
||||
The executor analyzes the LLM response to route:
|
||||
- JSON tool calls → Tier 0 (structured, existing path)
|
||||
- Code block with only `tools.*` calls → Tier 1 (embedded scripting)
|
||||
- Code with `import os`, `tools.shell` → Tier 3 (Docker, Phase 6)
|
||||
**Type conversion:** `monty_to_json()` / `json_to_monty()` bidirectional conversion between `MontyObject` and `serde_json::Value`.
|
||||
|
||||
### 3.4 Starlark/Rhai integration
|
||||
Add `starlark` or `rhai` as dependency. Implement `CodeRunner`:
|
||||
- Parse code
|
||||
- Bind `thread.*` and `tools.*` namespaces to Rust callbacks
|
||||
- Fuel metering (prevent infinite loops)
|
||||
- Execute with timeout
|
||||
- Capture output + side effects
|
||||
### 3.2 LlmResponse::Code variant (DONE)
|
||||
|
||||
### 3.5 Prompt transformation
|
||||
When CodeAct is enabled, the system prompt changes from prose tool descriptions to API documentation:
|
||||
New `LlmResponse::Code { code, content }` variant alongside `Text` and `ActionCalls`. The `ExecutionLoop` routes `Code` responses to `scripting::execute_code()` instead of `structured::execute_action_calls()`.
|
||||
|
||||
### 3.3 ExecutionLoop integration (DONE)
|
||||
|
||||
The loop handles `LlmResponse::Code`:
|
||||
- Records assistant message with code
|
||||
- Sets `step.tier = ExecutionTier::Scripting`
|
||||
- Executes via `scripting::execute_code()`
|
||||
- Records events and action results
|
||||
- Captures stdout + return value as context for next iteration
|
||||
- Handles `NeedApproval` outcome (pauses thread)
|
||||
|
||||
### 3.4 RLM: Context as variables (TO IMPLEMENT)
|
||||
|
||||
Inspired by Recursive Language Models (arXiv:2512.24601). The key insight: **the prompt is an environment variable, not attention input.** The LLM never sees the full thread context in its window — it writes code to access it selectively.
|
||||
|
||||
**Implementation:**
|
||||
- Pass thread state as Monty input variables via `MontyRun::new(code, "step.py", input_names)`:
|
||||
- `context` — full thread message history as a Python list of dicts
|
||||
- `goal` — the thread's goal string
|
||||
- `step_number` — current step index
|
||||
- `previous_results` — dict of `{call_id: result}` from prior steps
|
||||
- Use compact output metadata between code steps: `"[code output: 4,532 chars]"` instead of full stdout in chat history
|
||||
- The LLM's chat context stays lean; the full data lives in REPL variables
|
||||
|
||||
**Before (current):** Full context in LLM attention window
|
||||
```
|
||||
Available API:
|
||||
tools.web_fetch(url: str, headers: dict = None) -> Response
|
||||
tools.memory_search(query: str, limit: int = 10) -> List[Memory]
|
||||
thread.reply(content: str)
|
||||
thread.spawn(goal: str, capabilities: list = None) -> Thread
|
||||
|
||||
Write code to accomplish the user's request.
|
||||
System: You are an agent...
|
||||
User: Analyze these 1000 items...
|
||||
[1000 items in context]
|
||||
Assistant: ```python result = web_fetch(...)```
|
||||
```
|
||||
|
||||
**After (RLM pattern):** Context as a variable
|
||||
```
|
||||
System: You have access to `context` (1000 items) and `previous_results`.
|
||||
Write Python to accomplish the goal.
|
||||
Assistant: ```python
|
||||
items = context # never loaded into LLM window
|
||||
for batch in [items[i:i+100] for i in range(0, len(items), 100)]:
|
||||
result = llm_query("summarize these items", batch)
|
||||
# result is a variable, not injected into parent context
|
||||
```
|
||||
|
||||
### 3.5 RLM: Recursive `llm_query()` within code (TO IMPLEMENT)
|
||||
|
||||
Expose `llm_query(prompt, context)` as a callable inside the Monty environment. When code calls it, Monty suspends via `FunctionCall`. The engine:
|
||||
1. Spawns a child thread with the given prompt and context
|
||||
2. Runs the child to completion (inline, blocking the parent's code)
|
||||
3. Returns the child's result as a `MontyObject`
|
||||
|
||||
This enables the core RLM patterns:
|
||||
```python
|
||||
# Partition + Map + Reduce
|
||||
chunks = [context[i:i+1000] for i in range(0, len(context), 1000)]
|
||||
summaries = []
|
||||
for chunk in chunks:
|
||||
summary = llm_query("Summarize this section", chunk)
|
||||
summaries.append(summary) # variable, not in parent's LLM context
|
||||
final = llm_query("Combine these summaries", summaries)
|
||||
|
||||
# Verification
|
||||
answer = llm_query("What is X?", context)
|
||||
verified = llm_query(f"Is this answer correct: {answer}", context)
|
||||
```
|
||||
|
||||
**Key RLM properties preserved:**
|
||||
- **Symbolic handle to context** — the parent LLM never sees child outputs in its attention window
|
||||
- **Unbounded output** — variables in the REPL can exceed the context window
|
||||
- **Recursive decomposition** — the model decides how to partition work, not the architect
|
||||
|
||||
### 3.6 Tests
|
||||
- Simple code: `tools.web_fetch(url="...")` → action executed, result captured
|
||||
- Control flow: `for item in tools.search(...): tools.memory_write(...)` → multiple actions
|
||||
- Thread data access: `thread.messages[-1].content` → correct value
|
||||
- Fuel exhaustion: infinite loop → timeout error
|
||||
- Tier selection: structured JSON → Tier 0, code block → Tier 1
|
||||
- Error handling: code raises exception → step fails gracefully
|
||||
- **Simple code execution:** `x = 1 + 2` → returns 3, no tool calls
|
||||
- **Tool call from code:** `result = web_fetch(url="...")` → `FunctionCall` suspension → effect executor called → result returned to Python
|
||||
- **Multiple tool calls in loop:** `for i in range(3): fetch(url=urls[i])` → 3 effect executor calls
|
||||
- **Context as variable:** Code accesses `context[0]` → correct value from thread messages
|
||||
- **Compact metadata:** After code step, context has metadata summary not full stdout
|
||||
- **`llm_query()` recursive call:** Code calls `llm_query("summarize", data)` → child thread spawned → result returned as variable
|
||||
- **Resource limits:** Infinite loop → Monty `TimeoutError`
|
||||
- **OS call denied:** `import os; os.listdir(".")` → `OSError`
|
||||
- **VM panic recovery:** Monty panics → `catch_unwind` returns `EngineError`, thread doesn't crash
|
||||
- **Policy deny in code:** Code calls denied action → Python `RuntimeError` raised
|
||||
- **Approval needed in code:** Code calls approval-required action → `NeedApproval` returned, code halted
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user