You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/23 05:58:15 UTC
(maka) 02/04: feat(code-mode): add isolated Monty Python cell worker
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch feat/python-code-mode
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 127926337e5575b2aec8250136d6972b4caa0e18
Author: Wang <m4...@gmail.com>
AuthorDate: Fri Oct 23 13:29:25 2026 +0800
feat(code-mode): add isolated Monty Python cell worker
Generated-by: Codex
---
Cargo.lock | 605 +++++++++++++++++++++++++++++-
Cargo.toml | 2 +
platform/code-runtime/src/cell_context.rs | 2 +-
platform/python-runtime/Cargo.toml | 40 ++
platform/python-runtime/src/helpers.rs | 162 ++++++++
platform/python-runtime/src/lib.rs | 394 +++++++++++++++++++
platform/python-runtime/src/main.rs | 28 ++
platform/python-runtime/src/protocol.rs | 67 ++++
platform/python-runtime/src/value.rs | 91 +++++
platform/python-runtime/src/worker.rs | 287 ++++++++++++++
platform/python-runtime/tests/runtime.rs | 413 ++++++++++++++++++++
11 files changed, 2072 insertions(+), 19 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 34f653a0cd..4a654dc5f8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -406,6 +406,15 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "ar_archive_writer"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
+dependencies = [
+ "object 0.37.3",
+]
+
[[package]]
name = "arbitrary"
version = "1.4.2"
@@ -845,6 +854,35 @@ dependencies = [
"zbus",
]
+[[package]]
+name = "attribute-derive"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f9ef7a68684657ce9da341c934c73b63df7a3585a61e366d84c84ed5d3d167"
+dependencies = [
+ "attribute-derive-macro",
+ "manyhow",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "attribute-derive-macro"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300d426e5e73549125bb8e4649bdd5508f82c94ddf898fb0517246e71dfb8e98"
+dependencies = [
+ "collection_literals",
+ "interpolator",
+ "manyhow",
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "quote-use",
+ "syn 3.0.5",
+]
+
[[package]]
name = "autocfg"
version = "1.5.1"
@@ -1625,6 +1663,18 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "char_str"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9cba78a5abb515610e10af297615434b019755b5d7be14e559ff5340dff50c4"
+dependencies = [
+ "castaway",
+ "get-size2",
+ "itoa",
+ "ryu",
+]
+
[[package]]
name = "chrono"
version = "0.4.45"
@@ -1877,6 +1927,12 @@ dependencies = [
"unicode-width",
]
+[[package]]
+name = "collection_literals"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084"
+
[[package]]
name = "color_quant"
version = "1.1.0"
@@ -3256,6 +3312,12 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf"
+[[package]]
+name = "drop_bomb"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1"
+
[[package]]
name = "dtoa"
version = "1.0.11"
@@ -3657,6 +3719,17 @@ dependencies = [
"regex-syntax",
]
+[[package]]
+name = "fancy-regex"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
+dependencies = [
+ "bit-set 0.8.0",
+ "regex-automata",
+ "regex-syntax",
+]
+
[[package]]
name = "fancy-regex"
version = "0.18.0"
@@ -4205,6 +4278,31 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "get-size-derive2"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28f0daa179069133df60523e0f59d0a6bd2d3db9ccae01b630896d269c4a802e"
+dependencies = [
+ "attribute-derive",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "get-size2"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "050a5d07359ebfac7f0b74916d94cfa30575e45fdc7e9bfbb0f76d5d4203325a"
+dependencies = [
+ "compact_str 0.10.0",
+ "get-size-derive2",
+ "hashbrown 0.17.1",
+ "ordermap",
+ "smallvec",
+ "thin-vec",
+]
+
[[package]]
name = "gethostname"
version = "1.1.0"
@@ -4215,6 +4313,15 @@ dependencies = [
"windows-link 0.2.1",
]
+[[package]]
+name = "getopts"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
+dependencies = [
+ "unicode-width",
+]
+
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -5217,7 +5324,7 @@ dependencies = [
"log",
"presser",
"thiserror 2.0.20",
- "windows 0.59.0",
+ "windows 0.62.2",
]
[[package]]
@@ -6746,7 +6853,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
- "socket2 0.5.10",
+ "socket2",
"tokio",
"tower-service",
"tracing",
@@ -6764,7 +6871,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
- "windows-core 0.59.0",
+ "windows-core 0.62.2",
]
[[package]]
@@ -7152,6 +7259,12 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "interpolator"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
+
[[package]]
name = "inventory"
version = "0.3.24"
@@ -7208,6 +7321,18 @@ dependencies = [
"once_cell",
]
+[[package]]
+name = "is-macro"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8267aa6001e25494f3015f9663bbd88a18240c74483afa5f0934a1b3e4c388e9"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "is-wsl"
version = "0.4.0"
@@ -7320,6 +7445,21 @@ dependencies = [
"jiff-tzdb",
]
+[[package]]
+name = "jiter"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc2f2b4e673d798b3dbad0af24bc23b92013e680a6b9ad300b4e5e52fafe0586"
+dependencies = [
+ "ahash 0.8.12",
+ "bitvec",
+ "lexical-parse-float",
+ "num-bigint 0.4.8",
+ "num-traits",
+ "pyo3",
+ "smallvec",
+]
+
[[package]]
name = "jni"
version = "0.22.4"
@@ -7643,6 +7783,31 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
+[[package]]
+name = "lexical-parse-float"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56"
+dependencies = [
+ "lexical-parse-integer",
+ "lexical-util",
+]
+
+[[package]]
+name = "lexical-parse-integer"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34"
+dependencies = [
+ "lexical-util",
+]
+
+[[package]]
+name = "lexical-util"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17"
+
[[package]]
name = "libc"
version = "0.2.189"
@@ -9146,6 +9311,23 @@ dependencies = [
"url",
]
+[[package]]
+name = "maka-python-runtime"
+version = "0.2.0"
+dependencies = [
+ "futures-util",
+ "maka-agent-facts",
+ "maka-code-runtime",
+ "monty",
+ "monty-alloc",
+ "monty-types",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tokio-util",
+ "uuid",
+]
+
[[package]]
name = "maka-responses"
version = "0.2.0"
@@ -9633,6 +9815,29 @@ dependencies = [
"libc",
]
+[[package]]
+name = "manyhow"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1704b073316272a6785b70599f91f73b49022c2e290b065591ee6b9109ed8c7"
+dependencies = [
+ "manyhow-macros",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "manyhow-macros"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31601db71f1f5fb6f831a18aa3052d13a62528ee4f2cba7e8c9a5e258d953725"
+dependencies = [
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+]
+
[[package]]
name = "markdown"
version = "1.0.0"
@@ -9796,6 +10001,22 @@ dependencies = [
"unicase",
]
+[[package]]
+name = "minicbor"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7244eafd1e09e7c4e33043881997aa6ab6dffff3ed2cee3395a278980481d6e"
+
+[[package]]
+name = "minicbor-serde"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05ae1ca1d2c55196b389c8308119aa0e2aa8d31115f018dcc23ba3c5d350eb3"
+dependencies = [
+ "minicbor",
+ "serde",
+]
+
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -9843,6 +10064,87 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "monty"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d0f5559d7a445c22ed296fe8252aa067c80b7eb2bc70aeae341cf9967e58012"
+dependencies = [
+ "ahash 0.8.12",
+ "chrono",
+ "fancy-regex 0.17.0",
+ "getrandom 0.3.4",
+ "hashbrown 0.16.1",
+ "indexmap 2.14.2",
+ "itertools 0.14.0",
+ "itoa",
+ "jiter",
+ "libm",
+ "memchr",
+ "minicbor-serde",
+ "monty-macros",
+ "monty-types",
+ "num-bigint 0.4.8",
+ "num-integer",
+ "num-traits",
+ "postcard",
+ "ruff_python_ast",
+ "ruff_python_codegen",
+ "ruff_python_parser",
+ "ruff_python_stdlib",
+ "ruff_source_file",
+ "ruff_text_size",
+ "serde",
+ "serde_bytes",
+ "sha1 0.11.0",
+ "sha2 0.11.1",
+ "smallvec",
+ "speedate",
+ "strum 0.27.2",
+ "unicode-general-category",
+ "unicode-normalization",
+ "unicode_names2",
+]
+
+[[package]]
+name = "monty-alloc"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f9d819abd1c64c36dbfb05518a8d1ba28c55f38eb802a9c0c58818dee3f6d4"
+dependencies = [
+ "monty-types",
+]
+
+[[package]]
+name = "monty-macros"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbd7672e36311f10645e3a9e6776b643711543ec3fb415546ffde751e6d53003"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "monty-types"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "786b179998d4799dc96a5c25e91e3774a1fa8dd6cca441ddeedc9c146fd9a8cd"
+dependencies = [
+ "chrono",
+ "jiff",
+ "monty-macros",
+ "num-bigint 0.4.8",
+ "num-traits",
+ "ruff_python_stdlib",
+ "serde",
+ "serde_bytes",
+ "strum 0.27.2",
+ "unicode-general-category",
+ "web-time",
+]
+
[[package]]
name = "moxcms"
version = "0.8.1"
@@ -10339,6 +10641,7 @@ checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
dependencies = [
"num-integer",
"num-traits",
+ "serde",
]
[[package]]
@@ -10970,6 +11273,15 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "ordermap"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16a756ddcdeb6c0efc8927bdf0ed112885815a35ba350472a467be5104f01ca1"
+dependencies = [
+ "indexmap 2.14.2",
+]
+
[[package]]
name = "os_pipe"
version = "1.2.1"
@@ -11935,7 +12247,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
- "socket2 0.6.5",
+ "socket2",
"thiserror 1.0.69",
"tiny-skia 0.11.4",
"tokio",
@@ -12245,6 +12557,17 @@ dependencies = [
"toml_edit 0.25.15+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.107"
@@ -12314,6 +12637,16 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "psm"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea"
+dependencies = [
+ "ar_archive_writer",
+ "cc",
+]
+
[[package]]
name = "ptr_meta"
version = "0.1.4"
@@ -12363,6 +12696,65 @@ version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
+[[package]]
+name = "pyo3"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655"
+dependencies = [
+ "libc",
+ "num-bigint 0.4.8",
+ "num-traits",
+ "once_cell",
+ "portable-atomic",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb"
+dependencies = [
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "qoi"
version = "0.4.1"
@@ -12623,7 +13015,7 @@ dependencies = [
"quinn-udp",
"rustc-hash 2.1.3",
"rustls",
- "socket2 0.5.10",
+ "socket2",
"thiserror 2.0.20",
"tokio",
"tracing",
@@ -12662,7 +13054,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
- "socket2 0.5.10",
+ "socket2",
"tracing",
"windows-sys 0.61.2",
]
@@ -12676,6 +13068,28 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "quote-use"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48356e493823dc3c1d7e9fc7b3ff204ecec48afbdd718fc4134091b88eb63f3d"
+dependencies = [
+ "quote",
+ "quote-use-macros",
+]
+
+[[package]]
+name = "quote-use-macros"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cf1c5de6a7cc231c71e03c77aa4c7019097859ea7029156b54d2894a8430704"
+dependencies = [
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
[[package]]
name = "r-efi"
version = "5.3.0"
@@ -13892,6 +14306,119 @@ dependencies = [
"thiserror 2.0.20",
]
+[[package]]
+name = "ruff_python_ast"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aa1de604c478a790cfbcc9c1fef3060e619ed87dd776c6614862ed7e59dc461"
+dependencies = [
+ "aho-corasick",
+ "arrayvec",
+ "bitflags 2.13.2",
+ "char_str",
+ "compact_str 0.10.0",
+ "get-size2",
+ "is-macro",
+ "memchr",
+ "ruff_python_trivia",
+ "ruff_source_file",
+ "ruff_text_size",
+ "rustc-hash 2.1.3",
+ "thin-vec",
+ "thiserror 2.0.20",
+]
+
+[[package]]
+name = "ruff_python_codegen"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61039f2c61869bdabf2e5448b561ab6546b787718926c78973ebfefe03f84298"
+dependencies = [
+ "ruff_python_ast",
+ "ruff_python_literal",
+ "ruff_python_parser",
+ "ruff_source_file",
+ "ruff_text_size",
+]
+
+[[package]]
+name = "ruff_python_literal"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b6025882ae43876fda9867693e14d1d749acefb3db2763d81bfa32a8cd92682"
+dependencies = [
+ "bitflags 2.13.2",
+ "icu_properties",
+ "itertools 0.15.0",
+ "ruff_python_ast",
+]
+
+[[package]]
+name = "ruff_python_parser"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b44b423a842d5d2f10807189a260ba677d4eacdc98c5d0459b66bef67fb78d97"
+dependencies = [
+ "bitflags 2.13.2",
+ "bstr",
+ "drop_bomb",
+ "get-size2",
+ "hashbrown 0.17.1",
+ "memchr",
+ "ruff_python_ast",
+ "ruff_python_trivia",
+ "ruff_text_size",
+ "rustc-hash 2.1.3",
+ "stacker",
+ "static_assertions",
+ "thin-vec",
+ "unicode-ident",
+ "unicode-normalization",
+ "unicode_names2",
+]
+
+[[package]]
+name = "ruff_python_stdlib"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea8c1e6e69f7570e1f24c7a26ef93f6fd1475e2d76aa9ca4a78093c1f80523cb"
+dependencies = [
+ "bitflags 2.13.2",
+ "unicode-ident",
+]
+
+[[package]]
+name = "ruff_python_trivia"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990dadfad3aa3fd63040d98056112e5a377af8ada0c9c7659e8907a4b3c7cb34"
+dependencies = [
+ "itertools 0.15.0",
+ "ruff_source_file",
+ "ruff_text_size",
+ "rustc-hash 2.1.3",
+ "unicode-ident",
+]
+
+[[package]]
+name = "ruff_source_file"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed11e9366de40e79707c0e575afd8beffad58ec94c253577d516b8679cf98308"
+dependencies = [
+ "memchr",
+ "ruff_text_size",
+]
+
+[[package]]
+name = "ruff_text_size"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a4edc941f215495170a1bda0beeeb31db2fdcd67beecfe7f29d94b2663e1965"
+dependencies = [
+ "get-size2",
+]
+
[[package]]
name = "rusqlite"
version = "0.39.0"
@@ -15007,16 +15534,6 @@ dependencies = [
"serde_core",
]
-[[package]]
-name = "socket2"
-version = "0.5.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
-dependencies = [
- "libc",
- "windows-sys 0.52.0",
-]
-
[[package]]
name = "socket2"
version = "0.6.5"
@@ -15045,6 +15562,17 @@ dependencies = [
"url",
]
+[[package]]
+name = "speedate"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aba069c070b5e213f2a094deb7e5ed50ecb092be36102a4f4042e8d2056d060e"
+dependencies = [
+ "lexical-parse-float",
+ "strum 0.27.2",
+ "strum_macros 0.27.2",
+]
+
[[package]]
name = "spin"
version = "0.9.9"
@@ -15283,6 +15811,19 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+[[package]]
+name = "stacker"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "psm",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -15630,7 +16171,7 @@ dependencies = [
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
- "windows 0.59.0",
+ "windows 0.61.3",
]
[[package]]
@@ -15855,6 +16396,12 @@ dependencies = [
"unicode-width",
]
+[[package]]
+name = "thin-vec"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6a4b9ba8738cb4a4f399d37e266becfd475e75eb73425b87a05a2f2039ba63e"
+
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -16061,7 +16608,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "socket2 0.6.5",
+ "socket2",
"tokio-macros",
"tracing",
"windows-sys 0.61.2",
@@ -16694,6 +17241,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 = "uniffi"
version = "0.31.0"
diff --git a/Cargo.toml b/Cargo.toml
index 3f563132b0..a12e993db3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -56,6 +56,7 @@ members = [
"plugins/workspace/workspace-jev/engine",
"platform/js-runtime",
"platform/code-runtime",
+ "platform/python-runtime",
"plugins/agent/model",
"plugins/agent/provider-protocol",
"platform/network",
@@ -90,6 +91,7 @@ license = "Apache-2.0"
publish = false
[workspace.dependencies]
+maka-python-runtime = { path = "platform/python-runtime" }
hex = "0.4.3"
maka-ui-authoring = { path = "plugins/application/authoring/native" }
maka-app-client = { path = "products/maka/client" }
diff --git a/platform/code-runtime/src/cell_context.rs b/platform/code-runtime/src/cell_context.rs
index a7c8a25529..ba33178f1a 100644
--- a/platform/code-runtime/src/cell_context.rs
+++ b/platform/code-runtime/src/cell_context.rs
@@ -68,7 +68,7 @@ pub enum CellOutput {
},
}
-#[derive(Clone, Debug, Serialize)]
+#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ToolMetadata {
pub name: String,
pub description: String,
diff --git a/platform/python-runtime/Cargo.toml b/platform/python-runtime/Cargo.toml
new file mode 100644
index 0000000000..c73625dabc
--- /dev/null
+++ b/platform/python-runtime/Cargo.toml
@@ -0,0 +1,40 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[package]
+name = "maka-python-runtime"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+publish.workspace = true
+
+[[bin]]
+name = "maka-python-worker"
+path = "src/main.rs"
+
+[dependencies]
+maka-agent-facts.workspace = true
+maka-code-runtime.workspace = true
+futures-util.workspace = true
+monty = "=1.3.0"
+monty-types = "=1.3.0"
+monty-alloc = { version = "=1.3.0", features = ["exit-code"] }
+serde.workspace = true
+serde_json.workspace = true
+tokio = { workspace = true, features = ["process"] }
+tokio-util.workspace = true
+uuid.workspace = true
diff --git a/platform/python-runtime/src/helpers.rs b/platform/python-runtime/src/helpers.rs
new file mode 100644
index 0000000000..8bce8aeb84
--- /dev/null
+++ b/platform/python-runtime/src/helpers.rs
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use maka_code_runtime::{CellContext, CellDiagnostic, CellDiagnosticKind, CellOutput};
+use serde_json::{Value, json};
+
+pub(crate) fn invoke(
+ name: &str,
+ mut args: Vec<Value>,
+ mut kwargs: serde_json::Map<String, Value>,
+ context: &CellContext,
+) -> Result<Value, CellDiagnostic> {
+ let fields: &[&str] = match name {
+ "text" | "notify" | "audio" | "generated_image" => &["value"],
+ "image" => &["value", "detail"],
+ "store" => &["key", "value"],
+ "load" => &["key"],
+ "exit" | "yield_control" => &[],
+ _ => return Err(error("unknown Python helper")),
+ };
+ if args.len() > fields.len() {
+ return Err(error("too many helper arguments"));
+ }
+ for field in fields.iter().take(args.len()) {
+ if kwargs.contains_key(*field) {
+ return Err(error("argument supplied twice"));
+ }
+ }
+ for field in fields.iter().skip(args.len()) {
+ match kwargs.remove(*field) {
+ Some(value) => args.push(value),
+ None if name == "image" && *field == "detail" => args.push(Value::Null),
+ None => return Err(error(format!("missing argument: {field}"))),
+ }
+ }
+ if !kwargs.is_empty() {
+ return Err(error("unknown helper keyword argument"));
+ }
+ match name {
+ "text" => context.emit(CellOutput::Text {
+ text: display(&args[0]),
+ })?,
+ "notify" => context.notify(display(&args[0]))?,
+ "store" => context.store(key(&args[0])?.into(), args[1].clone())?,
+ "load" => return Ok(context.load(key(&args[0])?).unwrap_or(Value::Null)),
+ "yield_control" => context.yield_output(),
+ "exit" => {}
+ "image" => {
+ let value = &args[0];
+ let detail = if !args[1].is_null() {
+ args[1].clone()
+ } else {
+ value
+ .get("detail")
+ .or_else(|| value.pointer("/_meta/codex~1imageDetail"))
+ .cloned()
+ .unwrap_or(Value::Null)
+ };
+ if value.get("ref").is_some() {
+ emit(
+ context,
+ json!({"kind":"image","image":{"mimeType":value.get("mimeType"),"ref":value.get("ref"),"detail":detail}}),
+ )?;
+ } else {
+ media(
+ context,
+ "image",
+ value.get("image_url").unwrap_or(value),
+ detail,
+ )?;
+ }
+ }
+ "audio" => media(
+ context,
+ "audio",
+ args[0].get("audio_url").unwrap_or(&args[0]),
+ Value::Null,
+ )?,
+ "generated_image" => {
+ media(
+ context,
+ "image",
+ args[0]
+ .get("image_url")
+ .ok_or_else(|| error("missing image_url"))?,
+ Value::Null,
+ )?;
+ if let Some(hint) = args[0].get("output_hint").filter(|value| !value.is_null()) {
+ context.emit(CellOutput::Text {
+ text: display(hint),
+ })?;
+ }
+ }
+ _ => unreachable!(),
+ }
+ Ok(Value::Null)
+}
+fn media(
+ context: &CellContext,
+ kind: &str,
+ value: &Value,
+ detail: Value,
+) -> Result<(), CellDiagnostic> {
+ let content = if let Some(url) = value.as_str() {
+ let (mime, data) = url
+ .strip_prefix("data:")
+ .and_then(|value| value.split_once(";base64,"))
+ .ok_or_else(|| error("expected a base64 data URL"))?;
+ if !mime.starts_with(&format!("{kind}/"))
+ || !data
+ .trim_end_matches('=')
+ .bytes()
+ .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/'))
+ || data.len() - data.trim_end_matches('=').len() > 2
+ {
+ return Err(error("expected a base64 data URL"));
+ }
+ json!({"type":kind,"mimeType":mime,"data":data})
+ } else {
+ if value.get("type").and_then(Value::as_str) != Some(kind) {
+ return Err(error(format!("expected a {kind} content block")));
+ }
+ json!({"type":kind,"mimeType":value.get("mimeType"),"data":value.get("data")})
+ };
+ emit(
+ context,
+ json!({"kind":"media","content":content,"detail":detail}),
+ )
+}
+fn emit(context: &CellContext, value: Value) -> Result<(), CellDiagnostic> {
+ context.emit(serde_json::from_value(value).map_err(error)?)
+}
+fn key(value: &Value) -> Result<&str, CellDiagnostic> {
+ value
+ .as_str()
+ .ok_or_else(|| error("store key must be a string"))
+}
+fn display(value: &Value) -> String {
+ value
+ .as_str()
+ .map(str::to_owned)
+ .unwrap_or_else(|| value.to_string())
+}
+pub(crate) fn error(message: impl std::fmt::Display) -> CellDiagnostic {
+ CellDiagnostic::new(CellDiagnosticKind::ExecutionError, message.to_string())
+}
diff --git a/platform/python-runtime/src/lib.rs b/platform/python-runtime/src/lib.rs
new file mode 100644
index 0000000000..0deb74cf37
--- /dev/null
+++ b/platform/python-runtime/src/lib.rs
@@ -0,0 +1,394 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Optional Python Code Mode interpreter. Each cell owns a disposable Monty
+//! process; only the Host can admit tools, publish output or commit JSON state.
+mod helpers;
+mod protocol;
+mod value;
+pub mod worker;
+
+use futures_util::{StreamExt, future::BoxFuture, stream::FuturesUnordered};
+use maka_agent_facts::tools::{ToolError, ToolExecutor};
+use maka_code_runtime::{
+ CellAbort, CellContext, CellDiagnostic, CellLimits, CellResult, ToolScope, complete,
+};
+use protocol::{Reply, Request, Start};
+use serde_json::Value;
+use std::{
+ collections::{BTreeMap, HashSet},
+ path::PathBuf,
+ process::Stdio,
+ sync::Arc,
+ time::Instant,
+};
+use tokio::{
+ io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
+ process::{Child, ChildStdin, ChildStdout},
+ runtime::Handle,
+};
+use tokio_util::sync::CancellationToken;
+
+#[derive(Clone)]
+pub struct CodeExecutor {
+ limits: CellLimits,
+ worker: PathBuf,
+}
+impl CodeExecutor {
+ pub fn new(limits: CellLimits) -> Result<Self, CellAbort> {
+ let current = std::env::current_exe().map_err(internal)?;
+ let worker = current
+ .parent()
+ .ok_or_else(|| internal("executable has no parent directory"))?
+ .join(if cfg!(windows) {
+ "maka-python-worker.exe"
+ } else {
+ "maka-python-worker"
+ });
+ Self::with_worker(limits, worker)
+ }
+ /// Embeddings supply their packaged worker explicitly. The model never
+ /// chooses a command, environment or interpreter executable.
+ pub fn with_worker(limits: CellLimits, worker: PathBuf) -> Result<Self, CellAbort> {
+ if limits.timeout.is_zero()
+ || limits.timeout.as_millis() > u64::MAX as u128
+ || Instant::now().checked_add(limits.timeout).is_none()
+ || limits.heap_bytes < 8 * 1024 * 1024
+ {
+ return Err(internal("invalid cell limits"));
+ }
+ Ok(Self { limits, worker })
+ }
+ pub fn limits(&self) -> &CellLimits {
+ &self.limits
+ }
+ pub async fn execute_module(
+ &self,
+ source: String,
+ tools: Arc<dyn ToolExecutor>,
+ cancellation: CancellationToken,
+ context: CellContext,
+ ) -> Result<CellResult, CellAbort> {
+ if cancellation.is_cancelled() {
+ return Err(CellAbort::Cancelled);
+ }
+ let cancellation = cancellation.child_token();
+ let _cancel_on_drop = cancellation.clone().drop_guard();
+ let executor = self.clone();
+ // The owner task survives a dropped observer until child reaping and
+ // every admitted Host effect have settled.
+ tokio::spawn(async move { executor.run(source, tools, cancellation, context).await })
+ .await
+ .map_err(internal)?
+ }
+ async fn run(
+ &self,
+ source: String,
+ tools: Arc<dyn ToolExecutor>,
+ cancellation: CancellationToken,
+ context: CellContext,
+ ) -> Result<CellResult, CellAbort> {
+ let names = tools.names();
+ let mut unique = HashSet::new();
+ if names
+ .iter()
+ .any(|name| !unique.insert(tool_identifier(name)))
+ {
+ return Ok(complete(
+ Err(helpers::error(
+ "tool names collide after Python normalization",
+ )),
+ vec![],
+ ));
+ }
+ let scope = ToolScope::new(
+ context.clone(),
+ tools,
+ cancellation.child_token(),
+ Handle::current(),
+ );
+ let mut child = tokio::process::Command::new(&self.worker)
+ .args([
+ self.limits.heap_bytes.to_string(),
+ self.limits.timeout.as_millis().to_string(),
+ ])
+ .env_clear()
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::null())
+ .kill_on_drop(true)
+ .spawn()
+ .map_err(|error| {
+ internal(format!(
+ "cannot start bundled Python worker {}: {error}",
+ self.worker.display()
+ ))
+ })?;
+ let mut input = child
+ .stdin
+ .take()
+ .ok_or_else(|| internal("missing worker input"))?;
+ let mut output = BufReader::new(
+ child
+ .stdout
+ .take()
+ .ok_or_else(|| internal("missing worker output"))?,
+ );
+ let start = Start {
+ source,
+ names,
+ metadata: context.metadata().to_vec(),
+ };
+ let result = tokio::select! {
+ biased;
+ _=cancellation.cancelled()=>Err(CellAbort::Cancelled),
+ result=drive(&mut input,&mut output,start,&scope,&context,&self.limits)=>result,
+ };
+ scope.cancel();
+ drop(input);
+ let status = reap(&mut child).await;
+ scope.settle().await;
+ if let Some(error) = context.failure() {
+ return Err(CellAbort::Tool(error));
+ }
+ let status = status?;
+ if cancellation.is_cancelled() {
+ return Err(CellAbort::Cancelled);
+ }
+ if status.code() == Some(monty_types::OOM_EXIT_CODE) {
+ return Ok(complete(
+ Err(CellDiagnostic::limit("Python worker memory limit exceeded")),
+ scope.calls(),
+ ));
+ }
+ Ok(complete(result?, scope.calls()))
+ }
+}
+
+type Pending = FuturesUnordered<BoxFuture<'static, (u32, Result<Value, CellDiagnostic>)>>;
+async fn drive(
+ input: &mut ChildStdin,
+ output: &mut BufReader<ChildStdout>,
+ start: Start,
+ scope: &Arc<ToolScope>,
+ context: &CellContext,
+ limits: &CellLimits,
+) -> Result<Result<Value, CellDiagnostic>, CellAbort> {
+ send(input, &start).await?;
+ let mut remaining = limits.timeout;
+ let mut pending = Pending::new();
+ let mut ready = BTreeMap::new();
+ let mut admitted = HashSet::new();
+ loop {
+ let started = Instant::now();
+ let request = tokio::time::timeout(
+ remaining,
+ receive(output, limits.heap_bytes.saturating_add(4 * 1024 * 1024)),
+ )
+ .await;
+ remaining = remaining.saturating_sub(started.elapsed());
+ let request = match request {
+ Ok(request) => request?,
+ Err(_) => {
+ return Ok(Err(CellDiagnostic::limit(
+ "Python execution budget exceeded",
+ )));
+ }
+ };
+ let reply = match request {
+ Request::Complete { error } => {
+ return Ok(match error {
+ Some(error) => Err(error),
+ None => Ok(Value::Null),
+ });
+ }
+ Request::Call {
+ id,
+ name,
+ args,
+ kwargs,
+ } => {
+ if let Some(name) = name.strip_prefix("tool:") {
+ let input = match (args.as_slice(), kwargs.is_empty()) {
+ ([value], true) => Ok(value.clone()),
+ ([], false) => Ok(Value::Object(kwargs)),
+ ([], true) => Ok(serde_json::json!({})),
+ _ => Err(helpers::error(
+ "tool expects one JSON argument or keyword arguments",
+ )),
+ };
+ match input.and_then(|input| scope.start(name.into(), input)) {
+ Ok(receiver) => {
+ if !admitted.insert(id) {
+ return Err(internal("duplicate Python call identity"));
+ }
+ pending.push(Box::pin(async move {
+ (
+ id,
+ receiver.await.unwrap_or_else(|_| {
+ Err(helpers::error("tool outcome unavailable"))
+ }),
+ )
+ }));
+ Reply::Pending
+ }
+ Err(error) => Reply::Error { error },
+ }
+ } else if let Some(name) = name.strip_prefix("helper:") {
+ match helpers::invoke(name, args, kwargs, context) {
+ Ok(_) if name == "exit" => return Ok(Ok(Value::Null)),
+ Ok(_) if name == "yield_control" => {
+ admitted.insert(id);
+ pending.push(Box::pin(async move {
+ tokio::task::yield_now().await;
+ (id, Ok(Value::Null))
+ }));
+ Reply::Pending
+ }
+ Ok(value) => Reply::Return { value },
+ Err(error) => Reply::Error { error },
+ }
+ } else {
+ Reply::Error {
+ error: helpers::error("unknown external function"),
+ }
+ }
+ }
+ Request::Sleep {
+ id,
+ duration,
+ asynchronous,
+ } => {
+ if Instant::now().checked_add(duration).is_none() {
+ Reply::Error {
+ error: helpers::error("sleep duration is too large"),
+ }
+ } else if asynchronous {
+ admitted.insert(id);
+ pending.push(Box::pin(async move {
+ tokio::time::sleep(duration).await;
+ (id, Ok(Value::Null))
+ }));
+ Reply::Pending
+ } else {
+ tokio::time::sleep(duration).await;
+ Reply::Return { value: Value::Null }
+ }
+ }
+ Request::Resolve { ids } => {
+ if ids.is_empty() || ids.iter().any(|id| !admitted.contains(id)) {
+ return Err(internal("unknown pending Python call"));
+ }
+ while !ids.iter().any(|id| ready.contains_key(id)) {
+ let (id, result) = pending
+ .next()
+ .await
+ .ok_or_else(|| internal("Python futures lost their owner"))?;
+ ready.insert(id, result);
+ }
+ let results = ids
+ .into_iter()
+ .filter_map(|id| {
+ ready.remove(&id).map(|result| {
+ admitted.remove(&id);
+ (id, result)
+ })
+ })
+ .collect();
+ Reply::Resolved { results }
+ }
+ };
+ send(input, &reply).await?;
+ }
+}
+async fn send(input: &mut ChildStdin, message: &impl serde::Serialize) -> Result<(), CellAbort> {
+ let mut bytes = serde_json::to_vec(message).map_err(internal)?;
+ bytes.push(b'\n');
+ input.write_all(&bytes).await.map_err(internal)?;
+ input.flush().await.map_err(internal)
+}
+async fn receive(
+ output: &mut BufReader<ChildStdout>,
+ max_bytes: usize,
+) -> Result<Request, CellAbort> {
+ let mut line = Vec::new();
+ loop {
+ let bytes = output.fill_buf().await.map_err(internal)?;
+ if bytes.is_empty() {
+ return Err(internal("Python worker ended without a terminal frame"));
+ }
+ let end = bytes
+ .iter()
+ .position(|byte| *byte == b'\n')
+ .map(|index| index + 1);
+ let count = end.unwrap_or(bytes.len());
+ if line.len().saturating_add(count) > max_bytes {
+ return Err(internal("Python worker frame exceeds its memory budget"));
+ }
+ line.extend_from_slice(&bytes[..count]);
+ output.consume(count);
+ if end.is_some() {
+ return serde_json::from_slice(&line).map_err(internal);
+ }
+ }
+}
+async fn reap(child: &mut Child) -> Result<std::process::ExitStatus, CellAbort> {
+ let cleanup = |error| {
+ CellAbort::Tool(ToolError::CleanupUnconfirmed(format!(
+ "Python worker cleanup: {error}"
+ )))
+ };
+ if let Some(status) = child.try_wait().map_err(cleanup)? {
+ return Ok(status);
+ }
+ child.start_kill().map_err(cleanup)?;
+ child.wait().await.map_err(cleanup)
+}
+fn internal(error: impl std::fmt::Display) -> CellAbort {
+ CellAbort::Internal(error.to_string())
+}
+
+pub fn tool_identifier(name: &str) -> String {
+ let mut result: String = name
+ .chars()
+ .enumerate()
+ .map(|(index, ch)| {
+ if ch == '_' || ch.is_ascii_alphabetic() || (index > 0 && ch.is_ascii_digit()) {
+ ch
+ } else {
+ '_'
+ }
+ })
+ .collect();
+ if result.is_empty() {
+ result.push('_');
+ }
+ // Keywords cannot be attributes in Python source, even after a dot.
+ if [
+ "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
+ "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
+ "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return",
+ "try", "while", "with", "yield",
+ ]
+ .contains(&result.as_str())
+ {
+ result.push('_');
+ }
+ result
+}
diff --git a/platform/python-runtime/src/main.rs b/platform/python-runtime/src/main.rs
new file mode 100644
index 0000000000..a3988911db
--- /dev/null
+++ b/platform/python-runtime/src/main.rs
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#[global_allocator]
+static ALLOCATOR: monty_alloc::LimitedAllocator = monty_alloc::LimitedAllocator;
+
+fn main() {
+ if let Err(error) = maka_python_runtime::worker::run_stdio() {
+ eprintln!("Python worker: {error}");
+ std::process::exit(1);
+ }
+}
diff --git a/platform/python-runtime/src/protocol.rs b/platform/python-runtime/src/protocol.rs
new file mode 100644
index 0000000000..35634b9bd6
--- /dev/null
+++ b/platform/python-runtime/src/protocol.rs
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use maka_code_runtime::{CellDiagnostic, ToolMetadata};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use std::time::Duration;
+
+#[derive(Serialize, Deserialize)]
+pub(crate) struct Start {
+ pub source: String,
+ pub names: Vec<String>,
+ pub metadata: Vec<ToolMetadata>,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(tag = "kind", rename_all = "snake_case")]
+pub(crate) enum Request {
+ Call {
+ id: u32,
+ name: String,
+ args: Vec<Value>,
+ kwargs: serde_json::Map<String, Value>,
+ },
+ Sleep {
+ id: u32,
+ duration: Duration,
+ asynchronous: bool,
+ },
+ Resolve {
+ ids: Vec<u32>,
+ },
+ Complete {
+ error: Option<CellDiagnostic>,
+ },
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(tag = "kind", rename_all = "snake_case")]
+pub(crate) enum Reply {
+ Return {
+ value: Value,
+ },
+ Error {
+ error: CellDiagnostic,
+ },
+ Pending,
+ Resolved {
+ results: Vec<(u32, Result<Value, CellDiagnostic>)>,
+ },
+}
diff --git a/platform/python-runtime/src/value.rs b/platform/python-runtime/src/value.rs
new file mode 100644
index 0000000000..bdae781168
--- /dev/null
+++ b/platform/python-runtime/src/value.rs
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use monty_types::{MontyObject, ObjectRef};
+use serde_json::{Number, Value};
+
+pub(crate) fn from_json(value: Value) -> MontyObject {
+ match value {
+ Value::Null => MontyObject::none(),
+ Value::Bool(value) => MontyObject::bool(value),
+ Value::String(value) => MontyObject::string(value),
+ Value::Number(value) => {
+ if let Some(value) = value.as_i64() {
+ MontyObject::int(value)
+ } else if let Some(value) = value.as_u64() {
+ MontyObject::bigint(value.into())
+ } else {
+ MontyObject::float(value.as_f64().expect("JSON number"))
+ }
+ }
+ Value::Array(values) => MontyObject::list(values.into_iter().map(from_json)),
+ Value::Object(values) => MontyObject::dict(
+ values
+ .into_iter()
+ .map(|(key, value)| (MontyObject::string(key), from_json(value))),
+ ),
+ }
+}
+
+pub(crate) fn to_json(value: ObjectRef<'_>) -> Result<Value, String> {
+ convert(value, 0)
+}
+
+fn convert(value: ObjectRef<'_>, depth: usize) -> Result<Value, String> {
+ // The JSON transport uses serde_json's default recursion boundary (128).
+ // Cycles also terminate here; they can never silently become repr strings.
+ if depth >= 128 {
+ return Err("JSON value is cyclic or exceeds the transport nesting limit".into());
+ }
+ Ok(match value.type_name() {
+ "NoneType" => Value::Null,
+ "bool" => Value::Bool(value.as_bool().unwrap()),
+ "str" => Value::String(value.as_str().unwrap().into()),
+ "int" => {
+ let number = value
+ .py_repr()
+ .parse::<Number>()
+ .map_err(|_| "integer is outside the JSON number range")?;
+ if !number.is_i64() && !number.is_u64() {
+ return Err("integer is outside the JSON integer range".into());
+ }
+ Value::Number(number)
+ }
+ "float" => Value::Number(
+ Number::from_f64(value.as_float().unwrap()).ok_or("non-finite number is not JSON")?,
+ ),
+ "list" | "tuple" => Value::Array(
+ value
+ .items()
+ .unwrap()
+ .into_iter()
+ .map(|item| convert(item, depth + 1))
+ .collect::<Result<_, _>>()?,
+ ),
+ "dict" => {
+ let mut values = serde_json::Map::new();
+ for (key, item) in value.pairs().unwrap() {
+ let key = key.as_str().ok_or("JSON object keys must be strings")?;
+ values.insert(key.into(), convert(item, depth + 1)?);
+ }
+ Value::Object(values)
+ }
+ name => return Err(format!("{name} cannot cross the JSON boundary")),
+ })
+}
diff --git a/platform/python-runtime/src/worker.rs b/platform/python-runtime/src/worker.rs
new file mode 100644
index 0000000000..2c4ff4a156
--- /dev/null
+++ b/platform/python-runtime/src/worker.rs
@@ -0,0 +1,287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use crate::{
+ protocol::{Reply, Request, Start},
+ value::{from_json, to_json},
+};
+use maka_code_runtime::{CellDiagnostic, CellDiagnosticKind};
+use monty::{MontyRun, RunProgress};
+use monty_types::{
+ CompileOptions, ExcType, ExtFunctionResult, MontyException, MontyObject, MontyUuid,
+ NameLookupResult, OsFunctionCall, OsPolicy, PrintWriter, ResourceLimits, ResourceTracker,
+ SleepMode,
+};
+use std::{
+ io::{self, BufRead, Write},
+ time::Duration,
+};
+
+const HELPERS: &[&str] = &[
+ "text",
+ "image",
+ "audio",
+ "generated_image",
+ "notify",
+ "store",
+ "load",
+ "yield_control",
+ "exit",
+];
+
+/// Internal entry point of the bundled worker executable. Its allocator must be
+/// monty_alloc::LimitedAllocator; never install that allocator in the Host.
+pub fn run_stdio() -> Result<(), Box<dyn std::error::Error>> {
+ let mut args = std::env::args().skip(1);
+ let heap: usize = args.next().ok_or("missing heap budget")?.parse()?;
+ let timeout: u64 = args.next().ok_or("missing execution budget")?.parse()?;
+ monty_alloc::set_hard_limit(monty_types::memory_limit_with_headroom(Some(heap), false))?;
+ let stdin = io::stdin();
+ let stdout = io::stdout();
+ let mut input = stdin.lock();
+ let mut output = stdout.lock();
+ let start: Start = read(&mut input)?;
+ let result = execute(
+ start,
+ ResourceLimits {
+ max_feed_duration: Some(Duration::from_millis(timeout)),
+ max_memory: Some(heap),
+ ..Default::default()
+ },
+ &mut input,
+ &mut output,
+ );
+ let error = match result {
+ Ok(()) => None,
+ Err(WorkerError::Python(error)) => Some(diagnostic(error)),
+ Err(WorkerError::Diagnostic(error)) => Some(error),
+ Err(WorkerError::Protocol(error)) => return Err(error),
+ };
+ write(&mut output, &Request::Complete { error })?;
+ Ok(())
+}
+
+enum WorkerError {
+ Diagnostic(CellDiagnostic),
+ Python(MontyException),
+ Protocol(Box<dyn std::error::Error>),
+}
+impl From<MontyException> for WorkerError {
+ fn from(error: MontyException) -> Self {
+ Self::Python(error)
+ }
+}
+impl From<Box<dyn std::error::Error>> for WorkerError {
+ fn from(error: Box<dyn std::error::Error>) -> Self {
+ Self::Protocol(error)
+ }
+}
+
+fn execute(
+ start: Start,
+ limits: ResourceLimits,
+ input: &mut impl BufRead,
+ output: &mut impl Write,
+) -> Result<(), WorkerError> {
+ let mut inputs: Vec<_> = HELPERS
+ .iter()
+ .map(|name| MontyObject::function(format!("helper:{name}"), None))
+ .collect();
+ let mut names: Vec<String> = HELPERS.iter().map(|name| (*name).into()).collect();
+ names.extend(["tools".into(), "ALL_TOOLS".into()]);
+ inputs.push(MontyObject::module(
+ "tools",
+ MontyUuid::from_bytes(*uuid::Uuid::new_v4().as_bytes()),
+ start.names.into_iter().map(|name| {
+ (
+ MontyObject::string(crate::tool_identifier(&name)),
+ MontyObject::function(format!("tool:{name}"), None),
+ )
+ }),
+ ));
+ inputs.push(MontyObject::tuple(start.metadata.into_iter().map(|tool| {
+ MontyObject::named_tuple(
+ "Tool",
+ ["name", "description"],
+ [
+ MontyObject::string(tool.name),
+ MontyObject::string(tool.description),
+ ],
+ )
+ })));
+ let runner = match MontyRun::new(
+ start.source,
+ "code_mode.py",
+ names,
+ CompileOptions::default(),
+ ) {
+ Ok(runner) => runner.with_os_policy(OsPolicy {
+ sleep: SleepMode::CallHost,
+ ..Default::default()
+ }),
+ Err(error) => {
+ return Err(WorkerError::Diagnostic(CellDiagnostic::new(
+ CellDiagnosticKind::ParseError,
+ error.to_string(),
+ )));
+ }
+ };
+ let mut progress = runner.start(inputs, ResourceTracker::new(limits), PrintWriter::Disabled)?;
+ loop {
+ progress = match progress {
+ RunProgress::Complete(_) => return Ok(()),
+ RunProgress::FunctionCall(call) => {
+ let arguments = (|| -> Result<_, String> {
+ let args = call.args.args().map(to_json).collect::<Result<_, _>>()?;
+ let kwargs = call
+ .args
+ .kwargs()
+ .map(|(key, value)| {
+ Ok((
+ key.as_str().ok_or("keyword must be a string")?.into(),
+ to_json(value)?,
+ ))
+ })
+ .collect::<Result<_, String>>()?;
+ Ok((args, kwargs))
+ })();
+ let reply = match arguments {
+ Ok((args, kwargs)) => exchange(
+ input,
+ output,
+ &Request::Call {
+ id: call.call_id,
+ name: call.function_name.clone(),
+ args,
+ kwargs,
+ },
+ )?,
+ Err(error) => Reply::Error {
+ error: CellDiagnostic::new(CellDiagnosticKind::ExecutionError, error),
+ },
+ };
+ match reply {
+ Reply::Pending => call.resume_pending(PrintWriter::Disabled)?,
+ reply => call.resume(reply_result(reply)?, PrintWriter::Disabled)?,
+ }
+ }
+ RunProgress::ResolveFutures(wait) => {
+ let reply = exchange(
+ input,
+ output,
+ &Request::Resolve {
+ ids: wait.pending_call_ids().to_vec(),
+ },
+ )?;
+ let Reply::Resolved { results } = reply else {
+ return Err(protocol("expected resolved futures"));
+ };
+ wait.resume(
+ results
+ .into_iter()
+ .map(|(id, result)| (id, external(result)))
+ .collect(),
+ PrintWriter::Disabled,
+ )?
+ }
+ RunProgress::OsCall(call) => {
+ let (duration, asynchronous) = match call.function_call {
+ OsFunctionCall::Sleep(duration) => (duration, false),
+ OsFunctionCall::AsyncSleep(duration) => (duration, true),
+ _ => {
+ progress = call.resume(
+ MontyException::runtime_error(
+ "direct OS access is unavailable; use an authorized Host tool",
+ ),
+ PrintWriter::Disabled,
+ )?;
+ continue;
+ }
+ };
+ let reply = exchange(
+ input,
+ output,
+ &Request::Sleep {
+ id: call.call_id,
+ duration,
+ asynchronous,
+ },
+ )?;
+ let result = match reply {
+ Reply::Pending => ExtFunctionResult::Future(call.call_id),
+ reply => reply_result(reply)?,
+ };
+ call.resume(result, PrintWriter::Disabled)?
+ }
+ RunProgress::NameLookup(lookup) => {
+ lookup.resume(NameLookupResult::Undefined, PrintWriter::Disabled)?
+ }
+ };
+ }
+}
+
+fn external(result: Result<serde_json::Value, CellDiagnostic>) -> ExtFunctionResult {
+ match result {
+ Ok(value) => ExtFunctionResult::Return(from_json(value)),
+ Err(error) => ExtFunctionResult::Error(MontyException::runtime_error(error.message)),
+ }
+}
+fn reply_result(reply: Reply) -> Result<ExtFunctionResult, WorkerError> {
+ match reply {
+ Reply::Return { value } => Ok(external(Ok(value))),
+ Reply::Error { error } => Ok(external(Err(error))),
+ _ => Err(protocol("expected function result")),
+ }
+}
+fn protocol(message: &str) -> WorkerError {
+ WorkerError::Protocol(message.into())
+}
+fn exchange(
+ input: &mut impl BufRead,
+ output: &mut impl Write,
+ request: &Request,
+) -> Result<Reply, Box<dyn std::error::Error>> {
+ write(output, request)?;
+ read(input)
+}
+fn read<T: serde::de::DeserializeOwned>(
+ input: &mut impl BufRead,
+) -> Result<T, Box<dyn std::error::Error>> {
+ let mut line = String::new();
+ if input.read_line(&mut line)? == 0 {
+ return Err("Host closed the worker protocol".into());
+ }
+ Ok(serde_json::from_str(&line)?)
+}
+fn write(
+ output: &mut impl Write,
+ message: &impl serde::Serialize,
+) -> Result<(), Box<dyn std::error::Error>> {
+ serde_json::to_writer(&mut *output, message)?;
+ output.write_all(b"\n")?;
+ output.flush()?;
+ Ok(())
+}
+fn diagnostic(error: MontyException) -> CellDiagnostic {
+ let kind = match error.exc_type() {
+ ExcType::MemoryError | ExcType::TimeoutError => CellDiagnosticKind::LimitExceeded,
+ _ => CellDiagnosticKind::ExecutionError,
+ };
+ CellDiagnostic::new(kind, error.to_string())
+}
diff --git a/platform/python-runtime/tests/runtime.rs b/platform/python-runtime/tests/runtime.rs
new file mode 100644
index 0000000000..f91c3614c1
--- /dev/null
+++ b/platform/python-runtime/tests/runtime.rs
@@ -0,0 +1,413 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use maka_agent_facts::tools::{ToolError, ToolExecutor, ToolFuture};
+use maka_code_runtime::{
+ CellAbort, CellContext, CellDiagnosticKind, CellLimits, CellOutput, CellResult, CellStore,
+ ToolMetadata,
+};
+use maka_python_runtime::CodeExecutor;
+use serde_json::{Value, json};
+use std::{
+ sync::{
+ Arc,
+ atomic::{AtomicBool, Ordering},
+ },
+ time::Duration,
+};
+use tokio::sync::{Barrier, Notify};
+use tokio_util::sync::CancellationToken;
+
+struct Tools<F>(F);
+impl<F> ToolExecutor for Tools<F>
+where
+ F: Fn(String, Value, CancellationToken) -> ToolFuture + Send + Sync + 'static,
+{
+ fn names(&self) -> Vec<String> {
+ vec!["echo".into(), "wait".into()]
+ }
+ fn invoke(&self, name: String, input: Value, cancellation: CancellationToken) -> ToolFuture {
+ (self.0)(name, input, cancellation)
+ }
+}
+fn echo() -> Arc<dyn ToolExecutor> {
+ Arc::new(Tools(|_, input, _| -> ToolFuture {
+ Box::pin(async move { Ok(input) })
+ }))
+}
+fn executor(limits: CellLimits) -> CodeExecutor {
+ CodeExecutor::with_worker(limits, env!("CARGO_BIN_EXE_maka-python-worker").into()).unwrap()
+}
+fn context() -> CellContext {
+ CellContext::new(
+ CellStore::default(),
+ vec![ToolMetadata {
+ name: "echo".into(),
+ description: "echo JSON".into(),
+ }],
+ )
+}
+fn success(result: CellResult) -> usize {
+ match result {
+ CellResult::Success { value, tool_calls } => {
+ assert_eq!(value, Value::Null);
+ tool_calls.len()
+ }
+ other => panic!("{other:?}"),
+ }
+}
+async fn run(source: &str, context: CellContext) -> Result<CellResult, CellAbort> {
+ executor(CellLimits::default())
+ .execute_module(source.into(), echo(), CancellationToken::new(), context)
+ .await
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn python_fanout_and_partial_failure_use_real_host_tools() {
+ let barrier = Arc::new(Barrier::new(2));
+ let tools = Arc::new(Tools(move |_, input: Value, _| -> ToolFuture {
+ let barrier = barrier.clone();
+ Box::pin(async move {
+ barrier.wait().await;
+ if input["id"] == 2 {
+ Err(ToolError::Failed("failed-two".into()))
+ } else {
+ Ok(input)
+ }
+ })
+ }));
+ let context = context();
+ let result = tokio::time::timeout(
+ Duration::from_secs(10),
+ executor(CellLimits::default()).execute_module(
+ r#"
+import asyncio
+async def settled(n):
+ try:
+ return {"ok": True, "value": await tools.echo(id=n)}
+ except Exception as exc:
+ return {"ok": False, "error": str(exc)}
+text(await asyncio.gather(settled(1), settled(2)))
+"#
+ .into(),
+ tools,
+ CancellationToken::new(),
+ context.clone(),
+ ),
+ )
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(success(result), 2);
+ let outputs = context.take_output();
+ let CellOutput::Text { text } = &outputs[0] else {
+ panic!("{outputs:?}")
+ };
+ let output: Value = serde_json::from_str(text).unwrap();
+ assert_eq!(output[0], json!({"ok":true,"value":{"id":1}}));
+ assert_eq!(output[1]["ok"], false);
+ assert!(output[1]["error"].as_str().unwrap().contains("failed-two"));
+}
+
+#[tokio::test]
+async fn output_store_metadata_and_fresh_globals_obey_cell_contract() {
+ let store = CellStore::default();
+ let first = CellContext::new(
+ store.clone(),
+ vec![ToolMetadata {
+ name: "echo".into(),
+ description: "echo JSON".into(),
+ }],
+ );
+ success(run(r#"
+secret = "private"
+print(secret)
+store("receipt", {"text": "你好", "values": [1, True, None]})
+text(ALL_TOOLS[0].name)
+image({"type":"image", "data":"R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", "mimeType":"image/gif"}, detail="original")
+notify("ready")
+await yield_control()
+secret
+"#,first.clone()).await.unwrap());
+ let outputs = first.take_output();
+ assert_eq!(outputs.len(), 2);
+ assert!(matches!(&outputs[0],CellOutput::Text{text} if text=="echo"));
+ assert!(matches!(
+ &outputs[1],
+ CellOutput::Media {
+ detail: Some(maka_agent_facts::tool_output::ImageDetail::Original),
+ ..
+ }
+ ));
+ assert_eq!(
+ first.take_notifications().into_iter().collect::<Vec<_>>(),
+ ["ready"]
+ );
+ first.commit().unwrap();
+ let second = CellContext::new(store, vec![]);
+ success(
+ run(
+ "text(load('receipt'))\ntry:\n text(secret)\nexcept NameError:\n text('fresh')",
+ second.clone(),
+ )
+ .await
+ .unwrap(),
+ );
+ let outputs = second.take_output();
+ assert!(matches!(&outputs[1],CellOutput::Text{text} if text=="fresh"));
+ let CellOutput::Text { text } = &outputs[0] else {
+ panic!()
+ };
+ assert_eq!(
+ serde_json::from_str::<Value>(text).unwrap(),
+ json!({"text":"你好","values":[1,true,null]})
+ );
+}
+
+#[tokio::test]
+async fn json_boundary_rejects_lossy_types_and_cycles_before_effects() {
+ for value in ["{1: 'bad'}", "float('nan')", "b'bytes'", "2**100", "{1,2}"] {
+ let context = context();
+ success(
+ run(
+ &format!(
+ "try:\n await tools.echo({value})\nexcept Exception:\n text('rejected')"
+ ),
+ context.clone(),
+ )
+ .await
+ .unwrap(),
+ );
+ assert!(matches!(&context.take_output()[0],CellOutput::Text{text} if text=="rejected"));
+ }
+ let result = run("a=[]\na.append(a)\nawait tools.echo(a)", context())
+ .await
+ .unwrap();
+ assert!(matches!(result,CellResult::Failure{tool_calls,..} if tool_calls.is_empty()));
+ let context = context();
+ assert_eq!(
+ success(
+ run(
+ "text(await tools.echo({'n':18446744073709551615}))",
+ context.clone()
+ )
+ .await
+ .unwrap()
+ ),
+ 1
+ );
+ assert!(
+ matches!(&context.take_output()[0],CellOutput::Text{text} if text.contains("18446744073709551615"))
+ );
+}
+
+#[tokio::test]
+async fn cpu_memory_parse_and_os_errors_stay_inside_worker() {
+ for (source, limits, kind) in [
+ (
+ "while True: pass",
+ CellLimits {
+ timeout: Duration::from_millis(150),
+ ..Default::default()
+ },
+ CellDiagnosticKind::LimitExceeded,
+ ),
+ (
+ "items=['x'*32000 for i in range(10000)]",
+ CellLimits {
+ heap_bytes: 8 * 1024 * 1024,
+ ..Default::default()
+ },
+ CellDiagnosticKind::LimitExceeded,
+ ),
+ ("def", CellLimits::default(), CellDiagnosticKind::ParseError),
+ (
+ "open('/etc/passwd').read()",
+ CellLimits::default(),
+ CellDiagnosticKind::ExecutionError,
+ ),
+ ] {
+ let result = executor(limits)
+ .execute_module(source.into(), echo(), CancellationToken::new(), context())
+ .await
+ .unwrap();
+ assert!(
+ matches!(result,CellResult::Failure{error,..} if error.kind==kind),
+ "{source}"
+ );
+ }
+ success(run("text('Host still works')", context()).await.unwrap());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn tool_waits_and_python_sleep_do_not_spend_execution_budget() {
+ let tools = Arc::new(Tools(|_, input, _| -> ToolFuture {
+ Box::pin(async move {
+ tokio::time::sleep(Duration::from_millis(400)).await;
+ Ok(input)
+ })
+ }));
+ let result = executor(CellLimits {
+ timeout: Duration::from_millis(200),
+ ..Default::default()
+ })
+ .execute_module(
+ "import asyncio\nawait tools.wait({})\nawait asyncio.sleep(0.4)\ntext('done')".into(),
+ tools,
+ CancellationToken::new(),
+ context(),
+ )
+ .await
+ .unwrap();
+ assert_eq!(success(result), 1);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn completion_and_failure_cancel_unawaited_tools_and_wait_for_cleanup() {
+ for tail in [
+ "text('done')",
+ "raise ValueError('bad')",
+ "exit()\nawait tools.echo({})",
+ ] {
+ let entered = Arc::new(Notify::new());
+ let cancelled = Arc::new(Notify::new());
+ let release = Arc::new(Notify::new());
+ let tools = {
+ let entered = entered.clone();
+ let cancelled = cancelled.clone();
+ let release = release.clone();
+ Arc::new(Tools(
+ move |name: String, _, cancel: CancellationToken| -> ToolFuture {
+ let entered = entered.clone();
+ let cancelled = cancelled.clone();
+ let release = release.clone();
+ Box::pin(async move {
+ if name == "echo" {
+ entered.notified().await;
+ return Ok(Value::Null);
+ }
+ entered.notify_one();
+ cancel.cancelled().await;
+ cancelled.notify_one();
+ release.notified().await;
+ Ok(Value::Null)
+ })
+ },
+ ))
+ };
+ let source = format!("tools.wait({{}})\nawait tools.echo({{}})\n{tail}");
+ let mut task = tokio::spawn(async move {
+ executor(CellLimits::default())
+ .execute_module(source, tools, CancellationToken::new(), context())
+ .await
+ });
+ tokio::time::timeout(Duration::from_secs(5), cancelled.notified())
+ .await
+ .unwrap();
+ assert!(
+ tokio::time::timeout(Duration::from_millis(30), &mut task)
+ .await
+ .is_err()
+ );
+ release.notify_one();
+ let result = task.await.unwrap().unwrap();
+ match result {
+ CellResult::Success { tool_calls, .. } | CellResult::Failure { tool_calls, .. } => {
+ assert_eq!(tool_calls.len(), 2)
+ }
+ }
+ }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn cancellation_and_dropped_observer_drain_the_owned_effect() {
+ for drop_observer in [false, true] {
+ let started = Arc::new(Notify::new());
+ let cleaned = Arc::new(AtomicBool::new(false));
+ let tools = {
+ let started = started.clone();
+ let cleaned = cleaned.clone();
+ Arc::new(Tools(
+ move |_, _, cancel: CancellationToken| -> ToolFuture {
+ let started = started.clone();
+ let cleaned = cleaned.clone();
+ Box::pin(async move {
+ started.notify_one();
+ cancel.cancelled().await;
+ tokio::time::sleep(Duration::from_millis(30)).await;
+ cleaned.store(true, Ordering::SeqCst);
+ Ok(Value::Null)
+ })
+ },
+ ))
+ };
+ let cancellation = CancellationToken::new();
+ let token = cancellation.clone();
+ let task = tokio::spawn(async move {
+ executor(CellLimits::default())
+ .execute_module("await tools.wait({})".into(), tools, token, context())
+ .await
+ });
+ tokio::time::timeout(Duration::from_secs(5), started.notified())
+ .await
+ .unwrap();
+ if drop_observer {
+ task.abort();
+ } else {
+ cancellation.cancel();
+ assert!(matches!(task.await.unwrap(), Err(CellAbort::Cancelled)));
+ }
+ tokio::time::timeout(Duration::from_secs(5), async {
+ while !cleaned.load(Ordering::SeqCst) {
+ tokio::task::yield_now().await;
+ }
+ })
+ .await
+ .unwrap();
+ }
+ let cancellation = CancellationToken::new();
+ let token = cancellation.clone();
+ let task = tokio::spawn(async move {
+ executor(CellLimits::default())
+ .execute_module("while True: pass".into(), echo(), token, context())
+ .await
+ });
+ tokio::time::sleep(Duration::from_millis(50)).await;
+ cancellation.cancel();
+ assert!(matches!(
+ tokio::time::timeout(Duration::from_secs(2), task)
+ .await
+ .unwrap()
+ .unwrap(),
+ Err(CellAbort::Cancelled)
+ ));
+}
+
+#[tokio::test]
+async fn caught_fatal_tool_error_cannot_publish_success() {
+ let tools = Arc::new(Tools(|_, _, _| -> ToolFuture {
+ Box::pin(async { Err(ToolError::Persistence("journal unavailable".into())) })
+ }));
+ let context = context();
+ let result=executor(CellLimits::default()).execute_module("try:\n await tools.echo({})\nexcept Exception:\n text('caught')\nawait tools.echo({})".into(),tools,CancellationToken::new(),context).await;
+ assert!(matches!(
+ result,
+ Err(CellAbort::Tool(ToolError::Persistence(_)))
+ ));
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org