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)"
)