File size: 2,995 Bytes
2862b00 2386a1a 2862b00 c3b49d6 2862b00 6a88274 a495d22 6a88274 a495d22 3c3df06 2862b00 2386a1a 6a88274 a495d22 6a88274 a495d22 2862b00 a495d22 c3b49d6 a495d22 2862b00 2386a1a a495d22 2862b00 c3b49d6 2862b00 a495d22 2862b00 c3b49d6 2862b00 a495d22 2862b00 2386a1a a495d22 6a88274 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | """
Python Executor - Persistent Python execution environment.
Similar to a Jupyter kernel: maintains state and tool namespace across calls.
"""
from typing import Any
from .base import Executor # noqa: F401 — imported for the conformance check below
from .namespace_kernel import MAX_OUTPUT_CHARS, NamespaceKernel # noqa: F401
__all__ = ["PythonExecutor", "MAX_OUTPUT_CHARS"]
class PythonExecutor:
"""
Persistent Python execution environment.
Maintains namespace (variables, functions) across multiple execute() calls.
This is the default `in_process` executor (`EXECUTOR=in_process`, see
`base.get_executor`): it runs generated code with `exec` in THIS process.
It conforms structurally to the `Executor` protocol (`base.Executor`) — the
module-level assertion below pins that contract. ADR-0007 Phase 1 adds a
`sandbox` executor implementing the same `Executor` interface, running code
in an isolated per-session container instead of in-process; the agent depends
only on the interface, so that swap needs no agent-loop change.
The exec/stdout-capture/namespace-seeding semantics now live in the shared
`NamespaceKernel` (`namespace_kernel.py`), so the in-process executor and the
sandbox exec-kernel can never drift. This class is a thin in-process wrapper
over that core; its public surface is unchanged.
"""
def __init__(self):
self._kernel = NamespaceKernel()
@property
def namespace(self) -> dict[str, Any]:
"""The live execution namespace (kept for callers that inspect it)."""
return self._kernel.namespace
def reset_environment(self):
"""Reset the execution environment to a clean state."""
self._kernel.reset()
def send_functions(self, functions: dict[str, Any]):
"""Inject functions into the execution namespace."""
self._kernel.send_functions(functions)
def send_variables(self, variables: dict[str, Any]):
"""Inject variables into the execution namespace."""
self._kernel.send_variables(variables)
def __call__(self, code: str) -> Any:
return self.execute(code)
def execute(self, code: str) -> Any:
"""Execute Python code in the persistent namespace, returning stdout output."""
return self._kernel.execute(code)
# PythonExecutor is the default in-process implementation of the Executor
# seam (ADR-0007 Phase 0). `Executor` is a runtime_checkable Protocol, so this
# structural check pins the contract at import time — if a method is renamed or
# dropped here, import fails loudly rather than drifting from the interface.
# `issubclass` (not `isinstance`) keeps this a class-level, method-presence
# check with no instantiation — so importing this module stays free of the
# pandas/numpy side effects `PythonExecutor()` would trigger.
assert issubclass(PythonExecutor, Executor), (
"PythonExecutor no longer conforms to the Executor protocol (base.Executor)"
)
|