mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fix hyperlight WasmSandbox cross-thread Drop and harden hosted-agent sample (#5603)
* update hyperlight to beta and move samples, add hosted agent sample * Python: Fix hyperlight WasmSandbox cross-thread Drop and harden sample Root cause: when a worker-side closure raised, the exception's __traceback__ retained frame locals that included the partially constructed PyO3 sandbox. Future.result() re-raised that exception on the caller thread, and when the caller's exception was eventually GC'd the frame locals were released off-thread, dec_ref'ing the unsendable sandbox from the wrong thread and tripping the PyO3 panic '_native_wasm::WasmSandbox is unsendable, but is being dropped on another thread'. Fix: * Add _SandboxWorker._run_on_worker which catches every exception on the worker, drops __traceback__ there, deletes the original exception, and re-raises a fresh instance on the caller thread. initialize and execute route through it; dispose keeps its bare-submit semantics. * Add an opt-in diagnostic module _drop_diagnostic (no-op unless HYPERLIGHT_TRACE_DROPS=1) that installs a sys.unraisablehook and dumps owner-thread + per-thread stacks on any future cross-thread unsendable Drop. Useful for triaging similar PyO3 regressions. * Tests: cross-thread invocation, traceback-leak isolation, _SandboxEntry attribute-shape check, and a stale-reference stress test driven through asyncio.to_thread. Sample (samples/04-hosting/foundry-hosted-agents/responses/06_hyperlight_codeact): * Dockerfile installs agent-framework-* from in-tree source with python/ as build context so unreleased fixes can be validated end-to-end. * call_server.py pins the Responses API version. * main.py enables include_detailed_errors=True so future tool failures surface the actual exception text instead of a bare 'Error: Function failed.' string. * README.md documents the in-tree-package build and the Hyperlight hypervisor requirement (/dev/kvm on Linux, MSHV on Windows). Hosted environments without hypervisor passthrough surface 'No Hypervisor was found for Sandbox'; this is a hosting constraint, not a hyperlight bug. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: remove _drop_diagnostic from hyperlight package The diagnostic module was useful while bisecting the cross-thread Drop bug, but it is no longer needed now that _SandboxWorker._run_on_worker prevents the panic at the source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: address PR review feedback on hyperlight - Use lazy agent_framework.hyperlight import in sample main.py. - Env-driven endpoint (FOUNDRY_AGENT_ENDPOINT) in call_server.py; remove personal URLs. - Align agent.yaml model deployment with manifest (gpt-4.1-mini). - Tighten Dockerfile requirements guard; drop dangling deploy.ps1 reference. - Preserve exception args when sanitizing tracebacks in _run_on_worker. - Add public _SandboxWorker.is_alive(); update test to avoid private attr. - Add namespace coverage tests for agent_framework.hyperlight lazy loader. - Add prominent note: Foundry hosted-agent runtime does not yet support Hyperlight (no hypervisor exposed); container works locally with /dev/kvm. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: bump hyperlight-sandbox dependencies to 0.4.x Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: renumber hyperlight codeact sample to 08 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Coerce worker exception args to strings for cross-thread safety Stringify exc.args on the worker thread before propagating, so any PyO3 unsendable object captured in args (e.g. via a caller-supplied callback or underlying SDK) cannot be Dropped on the calling thread. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * moved sample --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
36b9b41e3b
commit
57c901a245
@@ -34,7 +34,7 @@ Status is grouped into these buckets:
|
|||||||
| `agent-framework-foundry-local` | `python/packages/foundry_local` | `beta` |
|
| `agent-framework-foundry-local` | `python/packages/foundry_local` | `beta` |
|
||||||
| `agent-framework-gemini` | `python/packages/gemini` | `alpha` |
|
| `agent-framework-gemini` | `python/packages/gemini` | `alpha` |
|
||||||
| `agent-framework-github-copilot` | `python/packages/github_copilot` | `beta` |
|
| `agent-framework-github-copilot` | `python/packages/github_copilot` | `beta` |
|
||||||
| `agent-framework-hyperlight` | `python/packages/hyperlight` | `alpha` |
|
| `agent-framework-hyperlight` | `python/packages/hyperlight` | `beta` |
|
||||||
| `agent-framework-lab` | `python/packages/lab` | `beta` |
|
| `agent-framework-lab` | `python/packages/lab` | `beta` |
|
||||||
| `agent-framework-mem0` | `python/packages/mem0` | `beta` |
|
| `agent-framework-mem0` | `python/packages/mem0` | `beta` |
|
||||||
| `agent-framework-ollama` | `python/packages/ollama` | `beta` |
|
| `agent-framework-ollama` | `python/packages/ollama` | `beta` |
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
"""Hyperlight CodeAct namespace for optional Agent Framework connectors.
|
||||||
|
|
||||||
|
This module lazily re-exports objects from ``agent-framework-hyperlight``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_IMPORTS: dict[str, tuple[str, str]] = {
|
||||||
|
"AllowedDomain": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
|
||||||
|
"AllowedDomainInput": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
|
||||||
|
"FileMount": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
|
||||||
|
"FileMountInput": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
|
||||||
|
"HyperlightCodeActProvider": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
|
||||||
|
"HyperlightExecuteCodeTool": ("agent_framework_hyperlight", "agent-framework-hyperlight"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str) -> Any:
|
||||||
|
if name in _IMPORTS:
|
||||||
|
import_path, package_name = _IMPORTS[name]
|
||||||
|
try:
|
||||||
|
return getattr(importlib.import_module(import_path), name)
|
||||||
|
except ModuleNotFoundError as exc:
|
||||||
|
raise ModuleNotFoundError(
|
||||||
|
f"The package {package_name} is required to use `{name}`. "
|
||||||
|
f"Please use `pip install {package_name}`, or update your requirements.txt or pyproject.toml file."
|
||||||
|
) from exc
|
||||||
|
raise AttributeError(f"Module `hyperlight` has no attribute {name}.")
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__() -> list[str]:
|
||||||
|
return list(_IMPORTS.keys())
|
||||||
@@ -48,6 +48,7 @@ all = [
|
|||||||
"agent-framework-foundry",
|
"agent-framework-foundry",
|
||||||
"agent-framework-foundry-local",
|
"agent-framework-foundry-local",
|
||||||
"agent-framework-github-copilot; python_version >= '3.11'",
|
"agent-framework-github-copilot; python_version >= '3.11'",
|
||||||
|
"agent-framework-hyperlight; ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and platform_machine == 'AMD64')) and python_version < '3.14'",
|
||||||
"agent-framework-lab",
|
"agent-framework-lab",
|
||||||
"agent-framework-mem0",
|
"agent-framework-mem0",
|
||||||
"agent-framework-ollama",
|
"agent-framework-ollama",
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import agent_framework.hyperlight as hyperlight
|
||||||
|
|
||||||
|
|
||||||
|
def test_hyperlight_namespace_dir_lists_lazy_exports() -> None:
|
||||||
|
names = dir(hyperlight)
|
||||||
|
for expected in (
|
||||||
|
"AllowedDomain",
|
||||||
|
"AllowedDomainInput",
|
||||||
|
"FileMount",
|
||||||
|
"FileMountInput",
|
||||||
|
"HyperlightCodeActProvider",
|
||||||
|
"HyperlightExecuteCodeTool",
|
||||||
|
):
|
||||||
|
assert expected in names
|
||||||
|
|
||||||
|
|
||||||
|
def test_hyperlight_namespace_lazy_loads_known_attribute(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
sentinel = object()
|
||||||
|
fake_module = ModuleType("agent_framework_hyperlight")
|
||||||
|
fake_module.HyperlightCodeActProvider = sentinel # type: ignore[attr-defined]
|
||||||
|
monkeypatch.setitem(sys.modules, "agent_framework_hyperlight", fake_module)
|
||||||
|
|
||||||
|
assert hyperlight.HyperlightCodeActProvider is sentinel
|
||||||
|
|
||||||
|
|
||||||
|
def test_hyperlight_namespace_unknown_attribute_raises_attribute_error() -> None:
|
||||||
|
with pytest.raises(AttributeError, match="Module `hyperlight` has no attribute DoesNotExist."):
|
||||||
|
_ = hyperlight.DoesNotExist # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
|
||||||
|
def test_hyperlight_namespace_missing_package_raises_helpful_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setitem(sys.modules, "agent_framework_hyperlight", None)
|
||||||
|
|
||||||
|
with pytest.raises(ModuleNotFoundError, match="agent-framework-hyperlight"):
|
||||||
|
_ = hyperlight.HyperlightCodeActProvider
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# agent-framework-hyperlight
|
# agent-framework-hyperlight
|
||||||
|
|
||||||
Alpha Hyperlight-backed CodeAct integrations for Microsoft Agent Framework.
|
Hyperlight-backed CodeAct integrations for Microsoft Agent Framework.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -121,8 +121,9 @@ codeact = HyperlightCodeActProvider(
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- This package is intentionally separate from `agent-framework-core` so CodeAct
|
- This package is intentionally separate from `agent-framework-core` so CodeAct
|
||||||
usage and installation remain optional.
|
usage and installation remain optional. With `agent-framework-core[all]` (or
|
||||||
- Alpha-package samples live under `packages/hyperlight/samples/`.
|
the meta `agent-framework`) installed it is also reachable through the
|
||||||
|
lazy-loading namespace `agent_framework.hyperlight`.
|
||||||
- `file_mounts` accepts a single string shorthand, an explicit `(host_path,
|
- `file_mounts` accepts a single string shorthand, an explicit `(host_path,
|
||||||
mount_path)` pair, or a `FileMount` named tuple. The host-side path in the
|
mount_path)` pair, or a `FileMount` named tuple. The host-side path in the
|
||||||
explicit forms may be a `str` or `Path`. Use the explicit two-value form when
|
explicit forms may be a `str` or `Path`. Use the explicit two-value form when
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import shutil
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
from concurrent.futures import Future, ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any, Protocol, TypeGuard, TypeVar, cast
|
from typing import Any, Protocol, TypeGuard, TypeVar, cast
|
||||||
@@ -92,39 +92,208 @@ _T = TypeVar("_T")
|
|||||||
|
|
||||||
|
|
||||||
class _SandboxWorker:
|
class _SandboxWorker:
|
||||||
"""Single-threaded executor that confines all sandbox operations to one OS thread.
|
"""Thread-confined actor that owns a sandbox + snapshot.
|
||||||
|
|
||||||
The Hyperlight ``WasmSandbox`` is declared ``unsendable`` in PyO3, meaning it can only be
|
The Hyperlight ``WasmSandbox`` is declared ``unsendable`` in PyO3: it can only be
|
||||||
accessed from the OS thread that created it; touching it from any other thread triggers a
|
accessed *and dropped* from the OS thread that created it. Touching or
|
||||||
Rust panic that cannot be caught from Python. Every cached :class:`_SandboxEntry` therefore
|
releasing it on any other thread triggers a Rust panic
|
||||||
owns its own ``_SandboxWorker``, and *all* lifecycle and execution calls against the
|
(``"_native_wasm::WasmSandbox is unsendable, but is being dropped on another thread"``)
|
||||||
underlying sandbox object must be routed through :meth:`submit`/:meth:`run`.
|
that cannot be caught from Python.
|
||||||
|
|
||||||
|
To make this guarantee airtight, this class is an actor: the underlying
|
||||||
|
sandbox and snapshot are stored ONLY as worker-local state and are never
|
||||||
|
exposed to or returned to other threads. Public methods submit closures to
|
||||||
|
the dedicated single-thread executor and return only sendable results.
|
||||||
|
Because no caller can ever obtain a strong reference to the unsendable
|
||||||
|
objects, no caller can ever cause them to be dropped on the wrong thread.
|
||||||
|
|
||||||
|
Exception isolation: exceptions raised inside worker closures carry a
|
||||||
|
``__traceback__`` whose frames retain references to local variables --
|
||||||
|
including PyO3 unsendable sandbox/native_result objects. Letting such an
|
||||||
|
exception propagate to the calling thread would defeat the actor model:
|
||||||
|
when the calling thread GCs the exception, the traceback's frame locals
|
||||||
|
are dropped on the wrong thread and PyO3 panics. To prevent this, every
|
||||||
|
exception raised inside a worker closure is caught on the worker, the
|
||||||
|
traceback is dropped while still on the worker thread, and a sanitized
|
||||||
|
copy (preserving message and exception type) is re-raised on the caller.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_executor",)
|
__slots__ = ("_executor", "_initialized", "_sandbox", "_snapshot")
|
||||||
|
|
||||||
def __init__(self, *, name: str = "hl-sandbox") -> None:
|
def __init__(self, *, name: str = "hl-sandbox") -> None:
|
||||||
self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix=name)
|
self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix=name)
|
||||||
|
# _sandbox/_snapshot are accessed/mutated ONLY from worker-side closures.
|
||||||
|
self._sandbox: Any = None
|
||||||
|
self._snapshot: Any = None
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
def submit(self, fn: Callable[..., _T], /, *args: Any, **kwargs: Any) -> Future[_T]:
|
def _run_on_worker(self, fn: Callable[[], _T]) -> _T:
|
||||||
return self._executor.submit(fn, *args, **kwargs)
|
"""Run ``fn`` on the worker thread; sanitize any exception's traceback there.
|
||||||
|
|
||||||
def run(self, fn: Callable[..., _T], /, *args: Any, **kwargs: Any) -> _T:
|
If ``fn`` raises, the exception's ``__traceback__`` is dropped on the worker
|
||||||
return self._executor.submit(fn, *args, **kwargs).result()
|
thread (so any PyO3 unsendable locals captured in frame locals are released
|
||||||
|
on the owner thread) and a fresh exception of the same type is raised on
|
||||||
|
the caller's thread carrying only the original message.
|
||||||
|
"""
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def _wrapped() -> tuple[bool, Any]:
|
||||||
# Do not block on shutdown; stop accepting new tasks, but allow the currently running
|
try:
|
||||||
# task and any already-queued tasks to finish before the worker thread exits.
|
return True, fn()
|
||||||
|
except BaseException as exc:
|
||||||
|
exc_type = type(exc)
|
||||||
|
# Capture args (usually (message,)) so the re-raised exception keeps the
|
||||||
|
# original shape for types whose constructor doesn't accept a single str.
|
||||||
|
# Coerce each arg to ``str`` on the worker thread: if a caller-supplied
|
||||||
|
# callback (or an underlying SDK) constructed the exception with a PyO3
|
||||||
|
# unsendable object in args, forwarding it as-is would re-introduce the
|
||||||
|
# same cross-thread Drop hazard the traceback nulling avoids. Strings
|
||||||
|
# are always sendable. Fall back to the str() form if args is empty.
|
||||||
|
exc_args: tuple[str, ...] = tuple(str(a) for a in exc.args) if exc.args else (str(exc),)
|
||||||
|
# Drop the traceback on the worker thread so frame locals (which
|
||||||
|
# may include PyO3 unsendable objects) are released here, not on
|
||||||
|
# the caller thread that will receive the wrapped exception.
|
||||||
|
exc.__traceback__ = None
|
||||||
|
del exc
|
||||||
|
return False, (exc_type, exc_args)
|
||||||
|
|
||||||
|
ok, payload = self._executor.submit(_wrapped).result()
|
||||||
|
if ok:
|
||||||
|
return cast(_T, payload)
|
||||||
|
exc_type, exc_args = cast(tuple[type[BaseException], tuple[str, ...]], payload)
|
||||||
|
# Re-raise a fresh instance with no chained traceback frames from the worker.
|
||||||
|
# If the exception type's constructor rejects the captured args (rare), fall
|
||||||
|
# back to a RuntimeError carrying the string form so we never lose the signal.
|
||||||
|
try:
|
||||||
|
raise exc_type(*exc_args)
|
||||||
|
except TypeError:
|
||||||
|
raise RuntimeError(f"{exc_type.__name__}: {exc_args}") from None
|
||||||
|
|
||||||
|
def initialize(self, build_fn: Callable[[], tuple[Any, Any]]) -> None:
|
||||||
|
"""Build and install the sandbox+snapshot on the worker thread.
|
||||||
|
|
||||||
|
``build_fn`` is invoked with no arguments on the worker thread. It must
|
||||||
|
return ``(sandbox, snapshot)``. Both references are retained as worker-
|
||||||
|
local attributes; they do not escape this thread.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _init_on_worker() -> None:
|
||||||
|
sandbox, snapshot = build_fn()
|
||||||
|
self._sandbox = sandbox
|
||||||
|
self._snapshot = snapshot
|
||||||
|
self._initialized = True
|
||||||
|
# Locals fall out of scope on the worker thread; the worker-local
|
||||||
|
# attributes hold the only strong refs from now on.
|
||||||
|
|
||||||
|
self._run_on_worker(_init_on_worker)
|
||||||
|
|
||||||
|
def execute(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
code: str,
|
||||||
|
output_dir: TemporaryDirectory[str] | None,
|
||||||
|
build_contents: Callable[..., list[Content]],
|
||||||
|
) -> list[Content]:
|
||||||
|
"""Restore + run + build sendable contents — all on the worker thread.
|
||||||
|
|
||||||
|
Returns a plain ``list[Content]`` whose elements never carry strong
|
||||||
|
references to the underlying sandbox or snapshot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _on_worker() -> list[Content]:
|
||||||
|
sandbox = self._sandbox
|
||||||
|
snapshot = self._snapshot
|
||||||
|
sandbox.restore(snapshot)
|
||||||
|
_clear_directory(output_dir)
|
||||||
|
result = sandbox.run(code=code)
|
||||||
|
try:
|
||||||
|
return build_contents(
|
||||||
|
result=result,
|
||||||
|
sandbox=sandbox,
|
||||||
|
output_dir=output_dir,
|
||||||
|
code=code,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# ``result`` may carry a back-reference to the sandbox. Force its
|
||||||
|
# final dec_ref on this thread so Drop runs here, not on whatever
|
||||||
|
# thread later GCs the ``Content`` list.
|
||||||
|
del result
|
||||||
|
|
||||||
|
return self._run_on_worker(_on_worker)
|
||||||
|
|
||||||
|
def is_alive(self) -> bool:
|
||||||
|
"""Return ``True`` while the worker thread can still accept new submissions.
|
||||||
|
|
||||||
|
Useful for tests/observability; returns ``False`` after ``dispose()``.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._executor.submit(lambda: None).result(timeout=1.0)
|
||||||
|
except RuntimeError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def dispose(self) -> None:
|
||||||
|
"""Release the sandbox+snapshot on the owner worker thread, then shut down.
|
||||||
|
|
||||||
|
Safe to call multiple times. After ``dispose`` returns, the sandbox/
|
||||||
|
snapshot are guaranteed to have been released on the worker thread; any
|
||||||
|
remaining references held elsewhere have already been impossible (they
|
||||||
|
never leaked out of this object).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _dispose_on_worker() -> None:
|
||||||
|
sandbox = self._sandbox
|
||||||
|
snapshot = self._snapshot
|
||||||
|
self._sandbox = None
|
||||||
|
self._snapshot = None
|
||||||
|
close_hook = (
|
||||||
|
(getattr(sandbox, "close", None) or getattr(sandbox, "shutdown", None)) if sandbox is not None else None
|
||||||
|
)
|
||||||
|
if callable(close_hook):
|
||||||
|
with suppress(Exception):
|
||||||
|
close_hook()
|
||||||
|
# ``sandbox`` and ``snapshot`` are local on the worker thread and
|
||||||
|
# will be dec_ref'd here when this frame returns -> Drop on worker.
|
||||||
|
del sandbox, snapshot
|
||||||
|
|
||||||
|
if self._initialized:
|
||||||
|
try:
|
||||||
|
# Use the bare executor here -- _dispose_on_worker swallows its
|
||||||
|
# own errors and never raises, so traceback sanitization is not
|
||||||
|
# needed and we want dispose to remain robust during teardown.
|
||||||
|
self._executor.submit(_dispose_on_worker).result()
|
||||||
|
except RuntimeError:
|
||||||
|
# Worker already shut down; sandbox/snapshot will leak rather
|
||||||
|
# than panic on the wrong thread. This is the safest fallback.
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self._initialized = False
|
||||||
|
# Do not block on shutdown; stop accepting new tasks, but allow any
|
||||||
|
# already-queued task (including the dispose closure above) to finish.
|
||||||
self._executor.shutdown(wait=False, cancel_futures=False)
|
self._executor.shutdown(wait=False, cancel_futures=False)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _SandboxEntry:
|
class _SandboxEntry:
|
||||||
sandbox: Any
|
"""Per-config cached sandbox handle.
|
||||||
snapshot: Any
|
|
||||||
|
The unsendable sandbox/snapshot live inside ``worker`` and never appear as
|
||||||
|
Python attributes on this object. Anything stored here is sendable and
|
||||||
|
safe to GC on any thread.
|
||||||
|
"""
|
||||||
|
|
||||||
|
worker: _SandboxWorker
|
||||||
input_dir: TemporaryDirectory[str] | None
|
input_dir: TemporaryDirectory[str] | None
|
||||||
output_dir: TemporaryDirectory[str] | None
|
output_dir: TemporaryDirectory[str] | None
|
||||||
worker: _SandboxWorker = field(default_factory=_SandboxWorker)
|
|
||||||
|
def dispose(self) -> None:
|
||||||
|
"""Release the sandbox+snapshot on the worker thread and clean up temp dirs."""
|
||||||
|
self.worker.dispose()
|
||||||
|
for tmp_dir in (self.input_dir, self.output_dir):
|
||||||
|
if tmp_dir is not None:
|
||||||
|
with suppress(Exception):
|
||||||
|
tmp_dir.cleanup()
|
||||||
|
self.input_dir = None
|
||||||
|
self.output_dir = None
|
||||||
|
|
||||||
|
|
||||||
def _load_sandbox_class() -> type[Any]:
|
def _load_sandbox_class() -> type[Any]:
|
||||||
@@ -432,6 +601,23 @@ def _parse_output_files(
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _result_snapshot(result: Any) -> dict[str, Any]:
|
||||||
|
"""Return a sendable plain-dict snapshot of a sandbox.run() result.
|
||||||
|
|
||||||
|
The Hyperlight ``WasmSandbox.run()`` return value is a PyO3 ``unsendable`` object that
|
||||||
|
can carry a back-reference to the sandbox itself. Storing it on
|
||||||
|
``Content.raw_representation`` lets it ride out of the owner thread and be garbage
|
||||||
|
collected elsewhere, which trips the PyO3 ``Drop`` panic. Build a thread-safe summary
|
||||||
|
of the fields we actually surface and forward that instead, so the original result can
|
||||||
|
be released on the worker thread that produced it.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"success": bool(getattr(result, "success", False)),
|
||||||
|
"stdout": str(getattr(result, "stdout", "") or ""),
|
||||||
|
"stderr": str(getattr(result, "stderr", "") or ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _build_execution_contents(
|
def _build_execution_contents(
|
||||||
*,
|
*,
|
||||||
result: Any,
|
result: Any,
|
||||||
@@ -442,10 +628,11 @@ def _build_execution_contents(
|
|||||||
success = bool(getattr(result, "success", False))
|
success = bool(getattr(result, "success", False))
|
||||||
stdout = str(getattr(result, "stdout", "") or "").replace("\r\n", "\n") or None
|
stdout = str(getattr(result, "stdout", "") or "").replace("\r\n", "\n") or None
|
||||||
stderr = str(getattr(result, "stderr", "") or "").replace("\r\n", "\n") or None
|
stderr = str(getattr(result, "stderr", "") or "").replace("\r\n", "\n") or None
|
||||||
|
snapshot = _result_snapshot(result)
|
||||||
outputs: list[Content] = []
|
outputs: list[Content] = []
|
||||||
|
|
||||||
if stdout is not None:
|
if stdout is not None:
|
||||||
outputs.append(Content.from_text(stdout, raw_representation=result))
|
outputs.append(Content.from_text(stdout, raw_representation=snapshot))
|
||||||
|
|
||||||
outputs.extend(
|
outputs.extend(
|
||||||
_parse_output_files(
|
_parse_output_files(
|
||||||
@@ -457,7 +644,7 @@ def _build_execution_contents(
|
|||||||
|
|
||||||
if success:
|
if success:
|
||||||
if stderr is not None:
|
if stderr is not None:
|
||||||
outputs.append(Content.from_text(stderr, raw_representation=result))
|
outputs.append(Content.from_text(stderr, raw_representation=snapshot))
|
||||||
if not outputs:
|
if not outputs:
|
||||||
outputs.append(Content.from_text("Code executed successfully without output."))
|
outputs.append(Content.from_text("Code executed successfully without output."))
|
||||||
return outputs
|
return outputs
|
||||||
@@ -467,7 +654,7 @@ def _build_execution_contents(
|
|||||||
Content.from_error(
|
Content.from_error(
|
||||||
message="Execution error",
|
message="Execution error",
|
||||||
error_details=error_details,
|
error_details=error_details,
|
||||||
raw_representation=result,
|
raw_representation=snapshot,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return outputs
|
return outputs
|
||||||
@@ -533,21 +720,14 @@ class _SandboxRegistry(SandboxRuntime):
|
|||||||
Entries are keyed by ``config.cache_key()``. All operations against the underlying
|
Entries are keyed by ``config.cache_key()``. All operations against the underlying
|
||||||
sandbox object are routed through the entry's dedicated single-threaded worker, which
|
sandbox object are routed through the entry's dedicated single-threaded worker, which
|
||||||
both serializes concurrent callers and satisfies the PyO3 ``unsendable`` invariant
|
both serializes concurrent callers and satisfies the PyO3 ``unsendable`` invariant
|
||||||
that the sandbox can only be touched from the thread that created it.
|
that the sandbox can only be touched from the thread that created it. The unsendable
|
||||||
|
objects never escape the worker; this method returns only sendable plain Python data.
|
||||||
"""
|
"""
|
||||||
entry = self._get_or_create_entry(config)
|
entry = self._get_or_create_entry(config)
|
||||||
return entry.worker.run(self._run_on_worker, entry, code)
|
return entry.worker.execute(
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _run_on_worker(entry: _SandboxEntry, code: str) -> list[Content]:
|
|
||||||
entry.sandbox.restore(entry.snapshot)
|
|
||||||
_clear_directory(entry.output_dir)
|
|
||||||
result = entry.sandbox.run(code=code)
|
|
||||||
return _build_execution_contents(
|
|
||||||
result=result,
|
|
||||||
sandbox=entry.sandbox,
|
|
||||||
output_dir=entry.output_dir,
|
|
||||||
code=code,
|
code=code,
|
||||||
|
output_dir=entry.output_dir,
|
||||||
|
build_contents=_build_execution_contents,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_or_create_entry(self, config: _RunConfig) -> _SandboxEntry:
|
def _get_or_create_entry(self, config: _RunConfig) -> _SandboxEntry:
|
||||||
@@ -562,22 +742,19 @@ class _SandboxRegistry(SandboxRuntime):
|
|||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Shut down all per-entry worker threads and release per-entry resources.
|
"""Shut down all per-entry worker threads and release per-entry resources.
|
||||||
|
|
||||||
Safe to call multiple times. Runs any sandbox close hook on the entry's
|
Safe to call multiple times. Each entry's sandbox/snapshot is disposed on the
|
||||||
own worker thread to honor the PyO3 ``unsendable`` invariant.
|
worker thread that created it to honor the PyO3 ``unsendable`` invariant.
|
||||||
"""
|
"""
|
||||||
with self._entries_lock:
|
with self._entries_lock:
|
||||||
entries = list(self._entries.values())
|
entries = list(self._entries.values())
|
||||||
self._entries.clear()
|
self._entries.clear()
|
||||||
for entry in entries:
|
try:
|
||||||
close_hook = getattr(entry.sandbox, "close", None) or getattr(entry.sandbox, "shutdown", None)
|
for entry in entries:
|
||||||
if callable(close_hook):
|
entry.dispose()
|
||||||
with suppress(Exception):
|
finally:
|
||||||
entry.worker.run(close_hook)
|
# Drop our local strong references; entries' own refs to sandbox/snapshot
|
||||||
entry.worker.shutdown()
|
# were already moved into the per-worker disposal closure inside dispose().
|
||||||
for tmp_dir in (entry.input_dir, entry.output_dir):
|
del entries
|
||||||
if tmp_dir is not None:
|
|
||||||
with suppress(Exception):
|
|
||||||
tmp_dir.cleanup()
|
|
||||||
|
|
||||||
def _create_entry(self, config: _RunConfig) -> _SandboxEntry:
|
def _create_entry(self, config: _RunConfig) -> _SandboxEntry:
|
||||||
input_dir_handle = TemporaryDirectory() if config.filesystem_enabled else None
|
input_dir_handle = TemporaryDirectory() if config.filesystem_enabled else None
|
||||||
@@ -617,8 +794,6 @@ class _SandboxRegistry(SandboxRuntime):
|
|||||||
methods=list(allowed_domain.methods) if allowed_domain.methods is not None else None,
|
methods=list(allowed_domain.methods) if allowed_domain.methods is not None else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
worker = _SandboxWorker()
|
|
||||||
|
|
||||||
def _build_sandbox() -> tuple[Any, Any]:
|
def _build_sandbox() -> tuple[Any, Any]:
|
||||||
sandbox = _create_sandbox()
|
sandbox = _create_sandbox()
|
||||||
_configure_sandbox(sandbox=sandbox, expand_missing_scheme=False)
|
_configure_sandbox(sandbox=sandbox, expand_missing_scheme=False)
|
||||||
@@ -636,18 +811,17 @@ class _SandboxRegistry(SandboxRuntime):
|
|||||||
snapshot = sandbox.snapshot()
|
snapshot = sandbox.snapshot()
|
||||||
return sandbox, snapshot
|
return sandbox, snapshot
|
||||||
|
|
||||||
|
worker = _SandboxWorker()
|
||||||
try:
|
try:
|
||||||
sandbox, snapshot = worker.run(_build_sandbox)
|
worker.initialize(_build_sandbox)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
worker.shutdown()
|
worker.dispose()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return _SandboxEntry(
|
return _SandboxEntry(
|
||||||
sandbox=sandbox,
|
worker=worker,
|
||||||
snapshot=snapshot,
|
|
||||||
input_dir=input_dir_handle,
|
input_dir=input_dir_handle,
|
||||||
output_dir=output_dir_handle,
|
output_dir=output_dir_handle,
|
||||||
worker=worker,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ description = "Hyperlight CodeAct integrations for Microsoft Agent Framework."
|
|||||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
version = "1.0.0a260429"
|
version = "1.0.0b260501"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
urls.homepage = "https://aka.ms/agent-framework"
|
urls.homepage = "https://aka.ms/agent-framework"
|
||||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||||
@@ -12,7 +12,7 @@ urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=ta
|
|||||||
urls.issues = "https://github.com/microsoft/agent-framework/issues"
|
urls.issues = "https://github.com/microsoft/agent-framework/issues"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 4 - Beta",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
@@ -23,9 +23,9 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-framework-core>=1.2.2,<2",
|
"agent-framework-core>=1.2.2,<2",
|
||||||
"hyperlight-sandbox>=0.3.0,<0.4",
|
"hyperlight-sandbox>=0.4.0,<0.5",
|
||||||
"hyperlight-sandbox-backend-wasm>=0.3.0,<0.4 ; ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and platform_machine == 'AMD64')) and python_version < '3.14'",
|
"hyperlight-sandbox-backend-wasm>=0.4.0,<0.5 ; ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and platform_machine == 'AMD64')) and python_version < '3.14'",
|
||||||
"hyperlight-sandbox-python-guest>=0.3.0,<0.4",
|
"hyperlight-sandbox-python-guest>=0.4.0,<0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
@@ -53,7 +53,6 @@ markers = [
|
|||||||
extend = "../../pyproject.toml"
|
extend = "../../pyproject.toml"
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"samples/**" = ["INP", "T201"]
|
|
||||||
"tests/**" = ["D", "INP", "TD", "ERA001", "RUF", "S"]
|
"tests/**" = ["D", "INP", "TD", "ERA001", "RUF", "S"]
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
@@ -82,7 +81,7 @@ disallow_untyped_decorators = true
|
|||||||
|
|
||||||
[tool.bandit]
|
[tool.bandit]
|
||||||
targets = ["agent_framework_hyperlight"]
|
targets = ["agent_framework_hyperlight"]
|
||||||
exclude_dirs = ["tests", "samples"]
|
exclude_dirs = ["tests"]
|
||||||
|
|
||||||
[tool.poe]
|
[tool.poe]
|
||||||
executor.type = "uv"
|
executor.type = "uv"
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# Hyperlight CodeAct samples
|
|
||||||
|
|
||||||
These samples demonstrate the alpha `agent-framework-hyperlight` package.
|
|
||||||
|
|
||||||
## When to use which pattern
|
|
||||||
|
|
||||||
- **Provider pattern** (`codeact_context_provider.py`): Use when the tool
|
|
||||||
registry, file mounts, or network allow-list may change between runs, or when
|
|
||||||
you want the provider to manage CodeAct instructions and approval computation
|
|
||||||
automatically on every invocation. This is the recommended default for
|
|
||||||
production agents that need dynamic capability management or concurrent runs
|
|
||||||
sharing one provider.
|
|
||||||
|
|
||||||
- **Manual static wiring** (`codeact_manual_wiring.py`): Use when the sandbox
|
|
||||||
tool set and capabilities are fixed for the agent's lifetime. This pattern
|
|
||||||
builds instructions once, passes `execute_code` alongside direct tools in
|
|
||||||
`tools=`, and skips the per-run provider lifecycle entirely. Simpler setup,
|
|
||||||
but changes to the tool registry after construction will not update the
|
|
||||||
agent's instructions automatically.
|
|
||||||
|
|
||||||
- **Standalone tool** (`codeact_tool.py`): Use for the simplest integration
|
|
||||||
where `execute_code` is added directly to the agent tool list. The tool's own
|
|
||||||
description advertises `call_tool(...)` and the registered sandbox tools, so
|
|
||||||
no extra agent instructions are needed. Best for quick prototyping or when
|
|
||||||
CodeAct is just another tool alongside the agent's direct tools.
|
|
||||||
|
|
||||||
## Samples
|
|
||||||
|
|
||||||
- `codeact_context_provider.py` shows the provider-owned CodeAct model where the
|
|
||||||
agent only sees `execute_code` and sandbox tools are owned by
|
|
||||||
`HyperlightCodeActProvider`.
|
|
||||||
- `codeact_manual_wiring.py` shows static wiring where `HyperlightExecuteCodeTool`
|
|
||||||
and its instructions are passed directly to the `Agent` constructor.
|
|
||||||
- `codeact_tool.py` shows the standalone `HyperlightExecuteCodeTool` surface
|
|
||||||
where `execute_code` is added directly to the agent tool list.
|
|
||||||
|
|
||||||
Run the samples from the repository after installing the workspace dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run --directory packages/hyperlight python samples/codeact_context_provider.py
|
|
||||||
uv run --directory packages/hyperlight python samples/codeact_manual_wiring.py
|
|
||||||
uv run --directory packages/hyperlight python samples/codeact_tool.py
|
|
||||||
```
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
# Copyright (c) Microsoft. All rights reserved.
|
|
||||||
|
|
||||||
"""Benchmark CodeAct vs. traditional tool-calling for a multi-tool-call task.
|
|
||||||
|
|
||||||
This sample runs the same prompt against the same FoundryChatClient twice:
|
|
||||||
|
|
||||||
1. **Traditional tool-calling**: the five business tools are passed directly to
|
|
||||||
the agent, so the model calls each tool individually via the LLM tool-call
|
|
||||||
interface.
|
|
||||||
2. **CodeAct**: the same tools are registered on a HyperlightCodeActProvider
|
|
||||||
and the model sees a single ``execute_code`` tool that calls them from
|
|
||||||
inside the Hyperlight sandbox via ``call_tool(...)``.
|
|
||||||
|
|
||||||
The task (computing grand totals per user) naturally requires many tool calls
|
|
||||||
to complete. At the end, the sample prints elapsed time and token usage for
|
|
||||||
each run so the two approaches can be compared.
|
|
||||||
|
|
||||||
Run with:
|
|
||||||
cd python
|
|
||||||
uv run --directory packages/hyperlight python samples/codeact_benchmark.py
|
|
||||||
|
|
||||||
Required environment variables (loaded from ``.env`` if present):
|
|
||||||
FOUNDRY_PROJECT_ENDPOINT
|
|
||||||
FOUNDRY_MODEL
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
from typing import Annotated, Any, Literal
|
|
||||||
|
|
||||||
from agent_framework import Agent, AgentResponse, UsageDetails
|
|
||||||
from agent_framework.foundry import FoundryChatClient
|
|
||||||
from azure.identity import AzureCliCredential
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
from agent_framework_hyperlight import HyperlightCodeActProvider
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
|
|
||||||
# 1. Deterministic "business" data and tools.
|
|
||||||
|
|
||||||
_USERS: list[dict[str, Any]] = [
|
|
||||||
{"id": 1, "name": "Alice", "region": "EU", "tier": "gold"},
|
|
||||||
{"id": 2, "name": "Bob", "region": "US", "tier": "silver"},
|
|
||||||
{"id": 3, "name": "Charlie", "region": "US", "tier": "gold"},
|
|
||||||
{"id": 4, "name": "Diana", "region": "APAC", "tier": "bronze"},
|
|
||||||
{"id": 5, "name": "Evan", "region": "EU", "tier": "silver"},
|
|
||||||
{"id": 6, "name": "Fiona", "region": "US", "tier": "gold"},
|
|
||||||
{"id": 7, "name": "George", "region": "APAC", "tier": "gold"},
|
|
||||||
{"id": 8, "name": "Hana", "region": "EU", "tier": "bronze"},
|
|
||||||
]
|
|
||||||
|
|
||||||
_ORDERS: dict[int, list[dict[str, Any]]] = {
|
|
||||||
1: [{"product": "Widget", "qty": 3, "unit_price": 9.99}, {"product": "Gadget", "qty": 1, "unit_price": 19.99}],
|
|
||||||
2: [{"product": "Widget", "qty": 1, "unit_price": 9.99}],
|
|
||||||
3: [{"product": "Gadget", "qty": 2, "unit_price": 19.99}, {"product": "Thingamajig", "qty": 4, "unit_price": 4.50}],
|
|
||||||
4: [{"product": "Widget", "qty": 10, "unit_price": 9.99}],
|
|
||||||
5: [{"product": "Gadget", "qty": 1, "unit_price": 19.99}],
|
|
||||||
6: [{"product": "Widget", "qty": 2, "unit_price": 9.99}, {"product": "Thingamajig", "qty": 5, "unit_price": 4.50}],
|
|
||||||
7: [{"product": "Gadget", "qty": 3, "unit_price": 19.99}],
|
|
||||||
8: [{"product": "Thingamajig", "qty": 2, "unit_price": 4.50}],
|
|
||||||
}
|
|
||||||
|
|
||||||
_DISCOUNTS: dict[str, float] = {"gold": 0.20, "silver": 0.10, "bronze": 0.05}
|
|
||||||
_TAX_RATES: dict[str, float] = {"EU": 0.21, "US": 0.08, "APAC": 0.10}
|
|
||||||
|
|
||||||
|
|
||||||
def list_users() -> list[dict[str, Any]]:
|
|
||||||
"""Return all users as a list of dictionaries.
|
|
||||||
|
|
||||||
Each entry has keys: id (int), name (str), region (str), tier (str).
|
|
||||||
"""
|
|
||||||
return _USERS
|
|
||||||
|
|
||||||
|
|
||||||
def get_orders_for_user(
|
|
||||||
user_id: Annotated[int, "The user id whose orders to retrieve."],
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Return the user's orders as a list of dictionaries.
|
|
||||||
|
|
||||||
Each entry has keys: product (str), qty (int), unit_price (float).
|
|
||||||
"""
|
|
||||||
return _ORDERS.get(user_id, [])
|
|
||||||
|
|
||||||
|
|
||||||
def get_discount_rate(
|
|
||||||
tier: Annotated[Literal["gold", "silver", "bronze"], "The customer tier."],
|
|
||||||
) -> float:
|
|
||||||
"""Return the discount rate as a float fraction (e.g. 0.2 for 20%)."""
|
|
||||||
return _DISCOUNTS[tier]
|
|
||||||
|
|
||||||
|
|
||||||
def get_tax_rate(
|
|
||||||
region: Annotated[Literal["EU", "US", "APAC"], "The region code."],
|
|
||||||
) -> float:
|
|
||||||
"""Return the tax rate as a float fraction (e.g. 0.21 for 21%)."""
|
|
||||||
return _TAX_RATES[region]
|
|
||||||
|
|
||||||
|
|
||||||
def compute_line_total(
|
|
||||||
qty: Annotated[int, "Line item quantity."],
|
|
||||||
unit_price: Annotated[float, "Line item unit price."],
|
|
||||||
discount_rate: Annotated[float, "Discount rate as a fraction (e.g. 0.2 for 20%)."],
|
|
||||||
tax_rate: Annotated[float, "Tax rate as a fraction (e.g. 0.21 for 21%)."],
|
|
||||||
) -> float:
|
|
||||||
"""Compute a single order line total.
|
|
||||||
|
|
||||||
Formula: qty * unit_price * (1 - discount_rate) * (1 + tax_rate), rounded to 2 decimals.
|
|
||||||
"""
|
|
||||||
subtotal = qty * unit_price
|
|
||||||
discounted = subtotal * (1.0 - discount_rate)
|
|
||||||
return round(discounted * (1.0 + tax_rate), 2)
|
|
||||||
|
|
||||||
|
|
||||||
TOOLS = [list_users, get_orders_for_user, get_discount_rate, get_tax_rate, compute_line_total]
|
|
||||||
|
|
||||||
|
|
||||||
# 2. Structured output schema shared between both runs.
|
|
||||||
|
|
||||||
|
|
||||||
class UserTotal(BaseModel):
|
|
||||||
"""A user's grand total of all their orders."""
|
|
||||||
|
|
||||||
user_id: int = Field(description="The user's id.")
|
|
||||||
name: str = Field(description="The user's display name.")
|
|
||||||
grand_total: float = Field(description="Sum of all line totals, rounded to 2 decimals.")
|
|
||||||
|
|
||||||
|
|
||||||
class UserGrandTotals(BaseModel):
|
|
||||||
"""Structured output schema for both runs."""
|
|
||||||
|
|
||||||
results: list[UserTotal] = Field(description="One entry per user, sorted by grand_total descending.")
|
|
||||||
|
|
||||||
|
|
||||||
INSTRUCTIONS = "You are a careful assistant. Use the provided tools for every lookup and computation."
|
|
||||||
|
|
||||||
BENCHMARK_PROMPT = (
|
|
||||||
"For every user in our system (there are 8 of them), compute the grand total of all their orders. "
|
|
||||||
"Use the compute_line_total tool for each user's orders, after looking up the relevant discount and "
|
|
||||||
"tax rates for that user. "
|
|
||||||
"Use the provided tools for EVERY data lookup (users, orders, discount rates, tax rates) and for EVERY "
|
|
||||||
"line-total computation via compute_line_total — do not invent values or hardcode any numbers. "
|
|
||||||
"The total per order item should apply the discount first and then the tax "
|
|
||||||
"(e.g. total = qty * unit_price * (1-discount) * (1+tax)). "
|
|
||||||
"Return one entry per user, sorted by grand_total descending."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_client() -> FoundryChatClient:
|
|
||||||
"""Create a FoundryChatClient from environment variables."""
|
|
||||||
return FoundryChatClient(
|
|
||||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
|
||||||
model=os.environ["FOUNDRY_MODEL"],
|
|
||||||
credential=AzureCliCredential(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# 3. Two runners that share the same tools, prompt, and structured output schema.
|
|
||||||
|
|
||||||
|
|
||||||
async def _run_traditional() -> tuple[float, AgentResponse]:
|
|
||||||
agent = Agent(
|
|
||||||
client=get_client(),
|
|
||||||
name="TraditionalAgent",
|
|
||||||
instructions=INSTRUCTIONS,
|
|
||||||
tools=TOOLS,
|
|
||||||
default_options={"response_format": UserGrandTotals},
|
|
||||||
)
|
|
||||||
start = time.perf_counter()
|
|
||||||
result = await agent.run(BENCHMARK_PROMPT)
|
|
||||||
elapsed = time.perf_counter() - start
|
|
||||||
return elapsed, result
|
|
||||||
|
|
||||||
|
|
||||||
async def _run_codeact() -> tuple[float, AgentResponse]:
|
|
||||||
codeact = HyperlightCodeActProvider(
|
|
||||||
tools=TOOLS,
|
|
||||||
approval_mode="never_require",
|
|
||||||
)
|
|
||||||
agent = Agent(
|
|
||||||
client=get_client(),
|
|
||||||
name="CodeActAgent",
|
|
||||||
instructions=INSTRUCTIONS,
|
|
||||||
context_providers=[codeact],
|
|
||||||
default_options={"response_format": UserGrandTotals},
|
|
||||||
)
|
|
||||||
start = time.perf_counter()
|
|
||||||
result = await agent.run(BENCHMARK_PROMPT)
|
|
||||||
elapsed = time.perf_counter() - start
|
|
||||||
return elapsed, result
|
|
||||||
|
|
||||||
|
|
||||||
# 4. Report results side by side.
|
|
||||||
|
|
||||||
|
|
||||||
def _print_section(title: str) -> None:
|
|
||||||
bar = "=" * 70
|
|
||||||
print(f"\n{bar}\n{title}\n{bar}")
|
|
||||||
|
|
||||||
|
|
||||||
def _format_usage(usage: UsageDetails | None) -> str:
|
|
||||||
if usage is None:
|
|
||||||
return "usage=<none>"
|
|
||||||
return (
|
|
||||||
f"input={usage.get('input_token_count') or 0:>6} "
|
|
||||||
f"output={usage.get('output_token_count') or 0:>6} "
|
|
||||||
f"total={usage.get('total_token_count') or 0:>6}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _print_results(result: AgentResponse) -> None:
|
|
||||||
if result.value is not None:
|
|
||||||
for row in result.value.results:
|
|
||||||
print(f" user_id={row.user_id:>2} name={row.name:<8} grand_total={row.grand_total:>8.2f}")
|
|
||||||
else:
|
|
||||||
print(result.text)
|
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
|
||||||
"""Run the benchmark and print a comparison."""
|
|
||||||
trad_time, trad_result = await _run_traditional()
|
|
||||||
code_time, code_result = await _run_codeact()
|
|
||||||
|
|
||||||
_print_section("Traditional tool-calling")
|
|
||||||
print(f"time={trad_time:7.2f}s {_format_usage(trad_result.usage_details)}")
|
|
||||||
_print_results(trad_result)
|
|
||||||
|
|
||||||
_print_section("CodeAct (HyperlightCodeActProvider)")
|
|
||||||
print(f"time={code_time:7.2f}s {_format_usage(code_result.usage_details)}")
|
|
||||||
_print_results(code_result)
|
|
||||||
|
|
||||||
_print_section("Comparison")
|
|
||||||
trad_total = (trad_result.usage_details or {}).get("total_token_count") or 0
|
|
||||||
code_total = (code_result.usage_details or {}).get("total_token_count") or 0
|
|
||||||
|
|
||||||
def pct(new: float, old: float) -> str:
|
|
||||||
if old == 0:
|
|
||||||
return "n/a"
|
|
||||||
delta = (new - old) / old * 100
|
|
||||||
sign = "+" if delta >= 0 else ""
|
|
||||||
return f"{sign}{delta:.1f}%"
|
|
||||||
|
|
||||||
print(f"time : traditional={trad_time:7.2f}s codeact={code_time:7.2f}s delta={pct(code_time, trad_time)}")
|
|
||||||
print(f"tokens : traditional={trad_total:7d} codeact={code_total:7d} delta={pct(code_total, trad_total)}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
|
import dataclasses
|
||||||
|
import gc
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
@@ -1042,9 +1045,8 @@ def test_sandbox_registry_close_shuts_down_workers(monkeypatch: pytest.MonkeyPat
|
|||||||
registry.close()
|
registry.close()
|
||||||
|
|
||||||
assert registry._entries == {}
|
assert registry._entries == {}
|
||||||
# Submitting after shutdown must fail; this proves the executor was actually torn down.
|
# After shutdown, the worker must report itself as no longer accepting work.
|
||||||
with pytest.raises(RuntimeError):
|
assert worker.is_alive() is False
|
||||||
worker.submit(lambda: None)
|
|
||||||
|
|
||||||
|
|
||||||
def test_sandbox_registry_close_releases_per_entry_resources(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
def test_sandbox_registry_close_releases_per_entry_resources(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||||
@@ -1125,3 +1127,243 @@ async def test_make_sandbox_callback_propagates_exceptions() -> None:
|
|||||||
callback = execute_code_module._make_sandbox_callback(boom)
|
callback = execute_code_module._make_sandbox_callback(boom)
|
||||||
with pytest.raises(RuntimeError, match="nope"):
|
with pytest.raises(RuntimeError, match="nope"):
|
||||||
callback(x=1)
|
callback(x=1)
|
||||||
|
|
||||||
|
|
||||||
|
class _OwnerThreadTrackedResult:
|
||||||
|
"""Fake sandbox.run() return value that mirrors a PyO3 ``unsendable`` object's Drop.
|
||||||
|
|
||||||
|
Records (rather than panics, since CPython swallows __del__ exceptions) the OS thread
|
||||||
|
that finalized the object, so tests can assert it was dropped on the sandbox's owner
|
||||||
|
thread and not on whatever thread happened to GC it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
drop_thread_violations: list[str] = []
|
||||||
|
|
||||||
|
def __init__(self, *, owner_thread: int, success: bool = True, stdout: str = "", stderr: str = "") -> None:
|
||||||
|
self._owner_thread = owner_thread
|
||||||
|
self.success = success
|
||||||
|
self.stdout = stdout
|
||||||
|
self.stderr = stderr
|
||||||
|
|
||||||
|
def __del__(self) -> None:
|
||||||
|
ident = threading.get_ident()
|
||||||
|
if ident != self._owner_thread:
|
||||||
|
type(self).drop_thread_violations.append(
|
||||||
|
f"_OwnerThreadTrackedResult dropped on thread {ident}, owner was {self._owner_thread}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _ResultDropTrackingFakeSandbox(_FakeSandbox):
|
||||||
|
"""Fake sandbox whose ``run()`` returns an owner-thread-tracking result."""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._owner_thread = threading.get_ident()
|
||||||
|
|
||||||
|
def run(self, code: str) -> Any:
|
||||||
|
del code
|
||||||
|
# Real Hyperlight runs almost always have non-empty stdout (the executed Python
|
||||||
|
# ``print`` output); that is the path where _build_execution_contents attaches
|
||||||
|
# raw_representation=result and the unsendable object escapes the worker thread.
|
||||||
|
return _OwnerThreadTrackedResult(owner_thread=self._owner_thread, success=True, stdout="hello\n")
|
||||||
|
|
||||||
|
|
||||||
|
def test_sandbox_run_result_is_finalized_on_owner_thread(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Regression: the object returned by ``sandbox.run`` must not escape its owner thread.
|
||||||
|
|
||||||
|
The Hyperlight ``WasmSandbox`` is unsendable; the value its ``run()`` returns can carry
|
||||||
|
a back-reference to the sandbox and is itself unsendable. Attaching it to
|
||||||
|
``Content.raw_representation`` lets it ride out of the worker thread and be garbage
|
||||||
|
collected on whichever thread the asyncio loop / agent state ends up on, which trips
|
||||||
|
the PyO3 ``Drop`` panic. Drop must happen on the worker thread that ran ``run()``.
|
||||||
|
"""
|
||||||
|
_OwnerThreadTrackedResult.drop_thread_violations.clear()
|
||||||
|
_FakeSandbox.instances.clear()
|
||||||
|
monkeypatch.setattr(execute_code_module, "_load_sandbox_class", lambda: _ResultDropTrackingFakeSandbox)
|
||||||
|
|
||||||
|
execute_code = HyperlightExecuteCodeTool()
|
||||||
|
|
||||||
|
def _drive() -> None:
|
||||||
|
# Run the whole invocation inside a helper frame so every local
|
||||||
|
# reference (contents, awaitable, asyncio frames) dies when the
|
||||||
|
# function returns. Anything still pinning the result is the bug.
|
||||||
|
contents = asyncio.run(execute_code.invoke(arguments={"code": "None"}))
|
||||||
|
assert contents and contents[0].type == "text"
|
||||||
|
|
||||||
|
_drive()
|
||||||
|
for _ in range(3):
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
assert _OwnerThreadTrackedResult.drop_thread_violations == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_sandbox_is_finalized_on_owner_thread_after_registry_close(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Regression: dropping the sandbox object itself must occur on its owner thread.
|
||||||
|
|
||||||
|
``_SandboxRegistry.close()`` previously held entries in a local list whose lifetime
|
||||||
|
extended onto the caller's thread. When that list went out of scope the unsendable
|
||||||
|
sandbox was finalized on the caller's thread, panicking PyO3 with
|
||||||
|
"WasmSandbox is unsendable, but is being dropped by another thread".
|
||||||
|
"""
|
||||||
|
drop_violations: list[str] = []
|
||||||
|
|
||||||
|
class _OwnerDropFakeSandbox(_FakeSandbox):
|
||||||
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._owner_thread = threading.get_ident()
|
||||||
|
# Do not pin ourselves on the class-level instances list; we want the
|
||||||
|
# registry/entry to hold the only strong reference so that dispose-time
|
||||||
|
# drop is what determines the finalizer thread.
|
||||||
|
_FakeSandbox.instances.remove(self)
|
||||||
|
|
||||||
|
def __del__(self) -> None:
|
||||||
|
ident = threading.get_ident()
|
||||||
|
if ident != self._owner_thread:
|
||||||
|
drop_violations.append(f"sandbox dropped on thread {ident}, owner was {self._owner_thread}")
|
||||||
|
|
||||||
|
monkeypatch.setattr(execute_code_module, "_load_sandbox_class", lambda: _OwnerDropFakeSandbox)
|
||||||
|
|
||||||
|
registry = execute_code_module._SandboxRegistry()
|
||||||
|
execute_code = HyperlightExecuteCodeTool(_registry=registry)
|
||||||
|
asyncio.run(execute_code.invoke(arguments={"code": "None"}))
|
||||||
|
|
||||||
|
registry.close()
|
||||||
|
|
||||||
|
# Release the registry/tool references and force a GC. With the fix in place the
|
||||||
|
# sandbox is already disposed on the worker thread inside close(); dropping these
|
||||||
|
# local references must not trigger a wrong-thread __del__ now.
|
||||||
|
del registry
|
||||||
|
del execute_code
|
||||||
|
for _ in range(3):
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
assert drop_violations == [], f"sandbox was dropped off-thread despite registry close: {drop_violations}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_worker_failure_does_not_leak_unsendable_via_exception_traceback(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Regression: an exception raised inside a worker closure must not leak unsendable refs.
|
||||||
|
|
||||||
|
Production failure mode: ``_build_sandbox`` (or ``sandbox.run``) raises on the
|
||||||
|
worker thread. ``concurrent.futures`` propagates the exception via
|
||||||
|
``Future.result()`` to the caller's thread. Python's exception object retains
|
||||||
|
``__traceback__`` whose frames reference local variables -- including the
|
||||||
|
partially-built PyO3 unsendable sandbox. When the caller's thread eventually
|
||||||
|
GCs the exception, those locals are dec_ref'd on the wrong thread and PyO3
|
||||||
|
panics with
|
||||||
|
``_native_wasm::WasmSandbox is unsendable, but is being dropped on another thread``.
|
||||||
|
|
||||||
|
The fix routes every worker closure through ``_run_on_worker``, which catches
|
||||||
|
the exception on the worker thread, drops its traceback there, and re-raises
|
||||||
|
a fresh exception on the caller side carrying only the message.
|
||||||
|
"""
|
||||||
|
drop_violations: list[str] = []
|
||||||
|
|
||||||
|
class _RaisingFakeSandbox(_FakeSandbox):
|
||||||
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._owner_thread = threading.get_ident()
|
||||||
|
_FakeSandbox.instances.remove(self)
|
||||||
|
# Simulate production bug: build raises while ``self`` is alive in
|
||||||
|
# the calling frame's locals -- the exception traceback will retain
|
||||||
|
# a reference to this object.
|
||||||
|
raise RuntimeError("simulated build failure with unsendable in frame locals")
|
||||||
|
|
||||||
|
def __del__(self) -> None:
|
||||||
|
ident = threading.get_ident()
|
||||||
|
if ident != self._owner_thread:
|
||||||
|
drop_violations.append(f"sandbox dropped on thread {ident}, owner was {self._owner_thread}")
|
||||||
|
|
||||||
|
monkeypatch.setattr(execute_code_module, "_load_sandbox_class", lambda: _RaisingFakeSandbox)
|
||||||
|
|
||||||
|
registry = execute_code_module._SandboxRegistry()
|
||||||
|
execute_code = HyperlightExecuteCodeTool(_registry=registry)
|
||||||
|
|
||||||
|
async def _drive(tool: HyperlightExecuteCodeTool) -> None:
|
||||||
|
for _ in range(4):
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await tool.invoke(arguments={"code": "None"})
|
||||||
|
|
||||||
|
asyncio.run(_drive(execute_code))
|
||||||
|
registry.close()
|
||||||
|
|
||||||
|
del registry
|
||||||
|
del execute_code
|
||||||
|
for _ in range(5):
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
assert drop_violations == [], (
|
||||||
|
f"sandbox dropped off-thread despite worker raising on the owner thread: {drop_violations}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sandbox_entry_does_not_expose_unsendable_attributes() -> None:
|
||||||
|
"""Architectural regression: the entry must not hold sandbox/snapshot as attributes.
|
||||||
|
|
||||||
|
The unsendable PyO3 sandbox/snapshot must live ONLY inside the per-entry worker
|
||||||
|
thread, accessible only via worker-submitted closures. Any direct ``entry.sandbox``
|
||||||
|
or ``entry.snapshot`` attribute would let callers obtain a strong reference that
|
||||||
|
can be released on a non-owner thread, triggering PyO3's unsendable Drop panic
|
||||||
|
(the production bug we are fixing).
|
||||||
|
"""
|
||||||
|
fields = {f.name for f in dataclasses.fields(execute_code_module._SandboxEntry)}
|
||||||
|
assert "sandbox" not in fields, "_SandboxEntry must not expose `sandbox` directly"
|
||||||
|
assert "snapshot" not in fields, "_SandboxEntry must not expose `snapshot` directly"
|
||||||
|
# Whatever attributes remain must be sendable / safe to GC on any thread.
|
||||||
|
assert fields <= {"worker", "input_dir", "output_dir"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_sandbox_survives_external_thread_holding_stale_reference(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Regression: stale refs held by external executors must not cause wrong-thread Drop.
|
||||||
|
|
||||||
|
Production traceback was ``concurrent.futures.thread._worker:95 del work_item`` on
|
||||||
|
``asyncio_0`` -- an external ``ThreadPoolExecutor`` whose ``_WorkItem`` transitively
|
||||||
|
held a strong reference to the sandbox via ``self._registry.execute``. When that
|
||||||
|
work_item was deleted on the external worker thread, the sandbox's refcount could
|
||||||
|
reach zero there, panicking PyO3.
|
||||||
|
|
||||||
|
With the actor-model refactor, ``HyperlightExecuteCodeTool._run_code`` runs the
|
||||||
|
sandbox call via ``asyncio.to_thread(self._registry.execute, ...)`` which creates
|
||||||
|
an external work_item containing ``self._registry.execute`` -- but that reference
|
||||||
|
transitively holds only the registry, not the sandbox. The sandbox lives entirely
|
||||||
|
inside the per-entry ``_SandboxWorker`` and never escapes; so when the external
|
||||||
|
work_item is deleted on a non-owner thread, the sandbox's refcount cannot reach
|
||||||
|
zero there.
|
||||||
|
"""
|
||||||
|
drop_violations: list[str] = []
|
||||||
|
|
||||||
|
class _OwnerDropFakeSandbox(_FakeSandbox):
|
||||||
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._owner_thread = threading.get_ident()
|
||||||
|
_FakeSandbox.instances.remove(self)
|
||||||
|
|
||||||
|
def __del__(self) -> None:
|
||||||
|
ident = threading.get_ident()
|
||||||
|
if ident != self._owner_thread:
|
||||||
|
drop_violations.append(f"sandbox dropped on thread {ident}, owner was {self._owner_thread}")
|
||||||
|
|
||||||
|
monkeypatch.setattr(execute_code_module, "_load_sandbox_class", lambda: _OwnerDropFakeSandbox)
|
||||||
|
|
||||||
|
registry = execute_code_module._SandboxRegistry()
|
||||||
|
execute_code = HyperlightExecuteCodeTool(_registry=registry)
|
||||||
|
|
||||||
|
async def _drive_many(tool: HyperlightExecuteCodeTool) -> None:
|
||||||
|
# Many concurrent invocations push work_items into asyncio's default executor;
|
||||||
|
# each work_item's args transitively reference the registry. If the registry
|
||||||
|
# were the sandbox holder, the work_items' deletion on asyncio_0/asyncio_1 etc.
|
||||||
|
# could trigger a wrong-thread Drop -- which is exactly the production bug.
|
||||||
|
await asyncio.gather(*[tool.invoke(arguments={"code": "None"}) for _ in range(8)])
|
||||||
|
|
||||||
|
asyncio.run(_drive_many(execute_code))
|
||||||
|
registry.close()
|
||||||
|
|
||||||
|
del registry
|
||||||
|
del execute_code
|
||||||
|
for _ in range(5):
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
assert drop_violations == []
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Hyperlight CodeAct context provider
|
||||||
|
|
||||||
|
Demonstrates the provider-owned [Hyperlight](https://github.com/hyperlight-dev/hyperlight)
|
||||||
|
CodeAct flow. `HyperlightCodeActProvider` injects an `execute_code` tool into the
|
||||||
|
agent and keeps the registered sandbox tools (`compute`, `fetch_data`) hidden
|
||||||
|
from the model — the model must call them from inside the sandbox using
|
||||||
|
`call_tool(...)`.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install agent-framework agent-framework-hyperlight --pre
|
||||||
|
```
|
||||||
|
|
||||||
|
> The Hyperlight Wasm backend is currently published only for `linux/x86_64` and
|
||||||
|
> `win32/AMD64` with Python `<3.14`. On other platforms `execute_code` will fail
|
||||||
|
> at runtime when it tries to create the sandbox.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- An Azure AI Foundry project endpoint (`FOUNDRY_PROJECT_ENDPOINT`)
|
||||||
|
- A deployed model (`FOUNDRY_MODEL`)
|
||||||
|
- Azure CLI authenticated (`az login`)
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python code_act.py
|
||||||
|
```
|
||||||
|
|
||||||
|
See [`code_act.py`](code_act.py) for the full annotated example.
|
||||||
+1
-2
@@ -10,11 +10,10 @@ from typing import Annotated, Any, Literal
|
|||||||
|
|
||||||
from agent_framework import Agent, FunctionInvocationContext, function_middleware, tool
|
from agent_framework import Agent, FunctionInvocationContext, function_middleware, tool
|
||||||
from agent_framework.foundry import FoundryChatClient
|
from agent_framework.foundry import FoundryChatClient
|
||||||
|
from agent_framework.hyperlight import HyperlightCodeActProvider
|
||||||
from azure.identity import AzureCliCredential
|
from azure.identity import AzureCliCredential
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from agent_framework_hyperlight import HyperlightCodeActProvider
|
|
||||||
|
|
||||||
"""This sample demonstrates the provider-owned Hyperlight CodeAct flow.
|
"""This sample demonstrates the provider-owned Hyperlight CodeAct flow.
|
||||||
|
|
||||||
The sample keeps `compute` and `fetch_data` off the direct agent tool surface and
|
The sample keeps `compute` and `fetch_data` off the direct agent tool surface and
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Hyperlight local code interpreter
|
||||||
|
|
||||||
|
Demonstrates the standalone [Hyperlight](https://github.com/hyperlight-dev/hyperlight)
|
||||||
|
`HyperlightExecuteCodeTool` — a sandboxed local code interpreter that the agent
|
||||||
|
can invoke directly. Two patterns are shown:
|
||||||
|
|
||||||
|
| File | Pattern |
|
||||||
|
|------|---------|
|
||||||
|
| [`local_code_interpreter.py`](local_code_interpreter.py) | **Standalone tool** — `HyperlightExecuteCodeTool` is added to the agent tool list and self-describes its sandbox tools, so no extra agent instructions are needed. Best for quick prototyping. |
|
||||||
|
| [`local_code_interpreter_manual_wiring.py`](local_code_interpreter_manual_wiring.py) | **Manual static wiring** — sandbox tools and CodeAct instructions are built once and passed to the `Agent` constructor alongside a direct-only tool (`send_email`). Best when the tool set is fixed for the agent's lifetime. |
|
||||||
|
|
||||||
|
For the recommended provider-driven pattern (with dynamic tool / capability
|
||||||
|
management), see
|
||||||
|
[`../../context_providers/code_act/`](../../context_providers/code_act/).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install agent-framework agent-framework-hyperlight --pre
|
||||||
|
```
|
||||||
|
|
||||||
|
> The Hyperlight Wasm backend is currently published only for `linux/x86_64` and
|
||||||
|
> `win32/AMD64` with Python `<3.14`. On other platforms `execute_code` will fail
|
||||||
|
> at runtime when it tries to create the sandbox.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- An Azure AI Foundry project endpoint (`FOUNDRY_PROJECT_ENDPOINT`)
|
||||||
|
- A deployed model (`FOUNDRY_MODEL`)
|
||||||
|
- Azure CLI authenticated (`az login`)
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python local_code_interpreter.py
|
||||||
|
python local_code_interpreter_manual_wiring.py
|
||||||
|
```
|
||||||
+1
-2
@@ -8,11 +8,10 @@ from typing import Annotated, Any, Literal
|
|||||||
|
|
||||||
from agent_framework import Agent, tool
|
from agent_framework import Agent, tool
|
||||||
from agent_framework.foundry import FoundryChatClient
|
from agent_framework.foundry import FoundryChatClient
|
||||||
|
from agent_framework.hyperlight import HyperlightExecuteCodeTool
|
||||||
from azure.identity import AzureCliCredential
|
from azure.identity import AzureCliCredential
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from agent_framework_hyperlight import HyperlightExecuteCodeTool
|
|
||||||
|
|
||||||
"""This sample demonstrates the standalone Hyperlight execute_code tool.
|
"""This sample demonstrates the standalone Hyperlight execute_code tool.
|
||||||
|
|
||||||
The sample adds `HyperlightExecuteCodeTool` directly to the agent. The tool's
|
The sample adds `HyperlightExecuteCodeTool` directly to the agent. The tool's
|
||||||
+1
-2
@@ -8,11 +8,10 @@ from typing import Annotated, Any, Literal
|
|||||||
|
|
||||||
from agent_framework import Agent, tool
|
from agent_framework import Agent, tool
|
||||||
from agent_framework.foundry import FoundryChatClient
|
from agent_framework.foundry import FoundryChatClient
|
||||||
|
from agent_framework.hyperlight import HyperlightExecuteCodeTool
|
||||||
from azure.identity import AzureCliCredential
|
from azure.identity import AzureCliCredential
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from agent_framework_hyperlight import HyperlightExecuteCodeTool
|
|
||||||
|
|
||||||
"""This sample demonstrates manual static wiring of CodeAct without a provider.
|
"""This sample demonstrates manual static wiring of CodeAct without a provider.
|
||||||
|
|
||||||
Instead of using `HyperlightCodeActProvider` with `context_providers=`, this
|
Instead of using `HyperlightCodeActProvider` with `context_providers=`, this
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||||
|
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Build this image with the repository's `python/` directory as the build context so
|
||||||
|
# the in-tree agent-framework packages can be installed from source. From the repo root:
|
||||||
|
#
|
||||||
|
# docker build \
|
||||||
|
# -f python/samples/04-hosting/foundry-hosted-agents/responses/08_hyperlight_codeact/Dockerfile \
|
||||||
|
# -t <acr>.azurecr.io/<image>:<tag> \
|
||||||
|
# python/
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the in-tree agent-framework packages we need. Order matters for editable
|
||||||
|
# installs because of inter-package dependencies; we install in dependency order
|
||||||
|
# below. Hyperlight backends are platform gated, so we install them via pip
|
||||||
|
# resolution rather than copying the wheels.
|
||||||
|
COPY packages/core /opt/af/core
|
||||||
|
COPY packages/openai /opt/af/openai
|
||||||
|
COPY packages/foundry /opt/af/foundry
|
||||||
|
COPY packages/foundry_hosting /opt/af/foundry_hosting
|
||||||
|
COPY packages/hyperlight /opt/af/hyperlight
|
||||||
|
|
||||||
|
# Copy just the sample we care about into the user agent location.
|
||||||
|
COPY samples/04-hosting/foundry-hosted-agents/responses/08_hyperlight_codeact/ /app/user_agent/
|
||||||
|
WORKDIR /app/user_agent
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip \
|
||||||
|
&& pip install --no-cache-dir /opt/af/core \
|
||||||
|
&& pip install --no-cache-dir /opt/af/openai \
|
||||||
|
&& pip install --no-cache-dir /opt/af/foundry \
|
||||||
|
&& pip install --no-cache-dir /opt/af/foundry_hosting \
|
||||||
|
&& pip install --no-cache-dir /opt/af/hyperlight \
|
||||||
|
&& if grep -Eq '^[[:space:]]*[^#[:space:]]' requirements.txt; then pip install --no-cache-dir -r requirements.txt; fi
|
||||||
|
|
||||||
|
EXPOSE 8088
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# What this sample demonstrates
|
||||||
|
|
||||||
|
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that
|
||||||
|
runs Python in a [Hyperlight](https://github.com/hyperlight-dev/hyperlight)
|
||||||
|
WebAssembly sandbox via the **CodeAct** pattern, hosted using the **Responses
|
||||||
|
protocol**. The model is only given a single `execute_code` tool. Local Python
|
||||||
|
tools (`compute`, `fetch_data`) are registered on `HyperlightCodeActProvider`
|
||||||
|
and are reachable from inside the sandbox via `call_tool(...)`, never as
|
||||||
|
direct LLM tools. All of this can be run as a container, however not under all circumstances.
|
||||||
|
|
||||||
|
> **⚠️ Foundry hosted-agent runtime support is in progress.**
|
||||||
|
> Hyperlight requires a hypervisor (`/dev/kvm` on Linux, MSHV on Windows). The
|
||||||
|
> default Foundry hosted-agent runtime does not currently expose a hypervisor
|
||||||
|
> to the workload container, so deploying this sample as a Foundry hosted
|
||||||
|
> agent will fail at runtime with
|
||||||
|
> `Failed to create sandbox: ... No Hypervisor was found for Sandbox`.
|
||||||
|
> The sample container itself works end-to-end when run locally with
|
||||||
|
> `docker run --device=/dev/kvm ...` (see [Hypervisor requirement](#hypervisor-requirement)
|
||||||
|
> below). We are working with the platform team to enable a hypervisor-capable
|
||||||
|
> hosting target.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Model integration
|
||||||
|
|
||||||
|
The agent uses `FoundryChatClient` to talk to a Foundry-hosted model deployment.
|
||||||
|
A `HyperlightCodeActProvider` is attached as a context provider, which on every
|
||||||
|
run injects the `execute_code` tool plus the CodeAct instructions that teach the
|
||||||
|
model how to author Python that calls `call_tool(...)` for sandbox-only tools.
|
||||||
|
|
||||||
|
See [`main.py`](main.py) for the full implementation.
|
||||||
|
|
||||||
|
### Agent hosting
|
||||||
|
|
||||||
|
The agent is hosted with `ResponsesHostServer` from
|
||||||
|
`agent-framework-foundry-hosting`, which exposes a REST endpoint compatible with
|
||||||
|
the OpenAI Responses protocol.
|
||||||
|
|
||||||
|
> The Hyperlight Wasm backend is currently published only for `linux/x86_64` and
|
||||||
|
> `win32/AMD64` with Python `<3.14`. The hosted container runs `python:3.12-slim`
|
||||||
|
> on linux/x86_64, which is supported.
|
||||||
|
|
||||||
|
### Hypervisor requirement
|
||||||
|
|
||||||
|
Hyperlight executes guest WebAssembly inside a micro-VM and **requires a
|
||||||
|
hypervisor on the host**:
|
||||||
|
|
||||||
|
- **Linux:** `/dev/kvm` must be present *and* the container must have access to
|
||||||
|
it (`docker run --device=/dev/kvm ...`).
|
||||||
|
- **Windows:** the Microsoft Hypervisor Platform (MSHV) must be enabled.
|
||||||
|
|
||||||
|
Without a hypervisor, sandbox creation fails with:
|
||||||
|
|
||||||
|
```
|
||||||
|
Failed to create sandbox: failed to build ProtoWasmSandbox: No Hypervisor was found for Sandbox
|
||||||
|
```
|
||||||
|
|
||||||
|
This affects hosted environments that don't expose `/dev/kvm` to the workload
|
||||||
|
container (most managed PaaS, including the default Foundry hosted-agent
|
||||||
|
runtime). To run this sample as a hosted agent you need a hosting target with
|
||||||
|
nested virtualization and `/dev/kvm` device passthrough — for example an Azure
|
||||||
|
VM, AKS nodes with KVM enabled, or Azure Container Instances configured for
|
||||||
|
nested virt.
|
||||||
|
|
||||||
|
## Running the Agent Host
|
||||||
|
|
||||||
|
Follow the instructions in the
|
||||||
|
[Running the Agent Host Locally](../../foundry-hosted-agents//README.md#running-the-agent-host-locally)
|
||||||
|
section of the README in the Foundry Hosted Agent directory.
|
||||||
|
|
||||||
|
## Interacting with the agent
|
||||||
|
|
||||||
|
Send a POST request to the server with a JSON body containing an `"input"`
|
||||||
|
field. The model should respond by calling `execute_code` with Python that uses
|
||||||
|
`call_tool(...)` to reach the sandbox-only tools:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8088/responses \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"input": "Fetch all users, find the admins, multiply 7 by 6, and print the users, admins and multiplication result. Use execute_code with call_tool(...)."}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploying the Agent to Foundry
|
||||||
|
|
||||||
|
Deploying this container to Foundry will not work yet, as soon as it does, we will update this sample.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: agent-framework-agent-with-hyperlight-codeact-responses
|
||||||
|
description: >
|
||||||
|
An Agent Framework agent with a Hyperlight CodeAct sandbox hosted by Foundry.
|
||||||
|
metadata:
|
||||||
|
tags:
|
||||||
|
- Agent Framework
|
||||||
|
- AI Agent Hosting
|
||||||
|
- Azure AI AgentServer
|
||||||
|
- Responses Protocol
|
||||||
|
- Streaming
|
||||||
|
- Hyperlight CodeAct
|
||||||
|
template:
|
||||||
|
name: agent-framework-agent-with-hyperlight-codeact-responses
|
||||||
|
kind: hosted
|
||||||
|
protocols:
|
||||||
|
- protocol: responses
|
||||||
|
version: 1.0.0
|
||||||
|
environment_variables:
|
||||||
|
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||||
|
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||||
|
resources:
|
||||||
|
- kind: model
|
||||||
|
id: gpt-4.1-mini
|
||||||
|
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||||
|
|
||||||
|
kind: hosted
|
||||||
|
name: agent-framework-agent-with-hyperlight-codeact-responses
|
||||||
|
description: |
|
||||||
|
An Agent Framework agent with a Hyperlight CodeAct sandbox hosted by Foundry.
|
||||||
|
metadata:
|
||||||
|
tags:
|
||||||
|
- Agent Framework
|
||||||
|
- AI Agent Hosting
|
||||||
|
- Azure AI AgentServer
|
||||||
|
- Responses Protocol
|
||||||
|
- Streaming
|
||||||
|
- Hyperlight CodeAct
|
||||||
|
protocols:
|
||||||
|
- protocol: responses
|
||||||
|
version: 1.0.0
|
||||||
|
resources:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 2Gi
|
||||||
|
environment_variables:
|
||||||
|
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||||
|
value: gpt-4.1-mini
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.10"
|
||||||
|
# dependencies = [
|
||||||
|
# "openai>=1.50,<3",
|
||||||
|
# "azure-identity>=1.19,<2",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
# Run with: uv run call_server.py
|
||||||
|
|
||||||
|
# Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
"""Call the deployed Hyperlight CodeAct Foundry hosted agent via the OpenAI client."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from azure.identity import AzureCliCredential
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
# Set FOUNDRY_AGENT_ENDPOINT to your deployed agent endpoint, e.g.
|
||||||
|
# https://<your-foundry-resource>.services.ai.azure.com/api/projects/<project>/agents/<agent-name>
|
||||||
|
ENDPOINT = os.environ.get(
|
||||||
|
"FOUNDRY_AGENT_ENDPOINT",
|
||||||
|
"https://<your-foundry-resource>.services.ai.azure.com"
|
||||||
|
"/api/projects/<project>/agents/<agent-name>",
|
||||||
|
)
|
||||||
|
SCOPE = "https://ai.azure.com/.default"
|
||||||
|
PROMPT = (
|
||||||
|
"Fetch all users, find the admins, multiply 7 by 6, and print the users, "
|
||||||
|
"admins and multiplication result. Use execute_code with call_tool(...)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
token = AzureCliCredential().get_token(SCOPE).token
|
||||||
|
client = OpenAI(base_url=ENDPOINT, api_key=token, default_query={"api-version": "v1"})
|
||||||
|
response = client.responses.create(model="hosted-agent", input=PROMPT)
|
||||||
|
print(response.output_text)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Copyright (c) Microsoft. All rights reserved.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from typing import Annotated, Any, Literal
|
||||||
|
|
||||||
|
from agent_framework import Agent, tool
|
||||||
|
from agent_framework.foundry import FoundryChatClient
|
||||||
|
from agent_framework.hyperlight import HyperlightCodeActProvider
|
||||||
|
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||||
|
from azure.identity import DefaultAzureCredential
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
@tool(approval_mode="never_require")
|
||||||
|
def compute(
|
||||||
|
operation: Annotated[
|
||||||
|
Literal["add", "subtract", "multiply", "divide"],
|
||||||
|
"Math operation: add, subtract, multiply, or divide.",
|
||||||
|
],
|
||||||
|
a: Annotated[float, "First numeric operand."],
|
||||||
|
b: Annotated[float, "Second numeric operand."],
|
||||||
|
) -> float:
|
||||||
|
"""Perform a math operation for sandboxed code."""
|
||||||
|
operations = {
|
||||||
|
"add": a + b,
|
||||||
|
"subtract": a - b,
|
||||||
|
"multiply": a * b,
|
||||||
|
"divide": a / b if b else float("inf"),
|
||||||
|
}
|
||||||
|
return operations[operation]
|
||||||
|
|
||||||
|
|
||||||
|
@tool(approval_mode="never_require")
|
||||||
|
async def fetch_data(
|
||||||
|
table: Annotated[str, "Name of the simulated table to query."],
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Fetch records from a named table."""
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
data: dict[str, list[dict[str, Any]]] = {
|
||||||
|
"users": [
|
||||||
|
{"id": 1, "name": "Alice", "role": "admin"},
|
||||||
|
{"id": 2, "name": "Bob", "role": "user"},
|
||||||
|
{"id": 3, "name": "Charlie", "role": "admin"},
|
||||||
|
],
|
||||||
|
"products": [
|
||||||
|
{"id": 101, "name": "Widget", "price": 9.99},
|
||||||
|
{"id": 102, "name": "Gadget", "price": 19.99},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return data.get(table, [])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 1. Create the Foundry chat client.
|
||||||
|
client = FoundryChatClient(
|
||||||
|
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||||
|
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||||
|
credential=DefaultAzureCredential(),
|
||||||
|
function_invocation_configuration={"include_detailed_errors": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Register sandbox tools on a Hyperlight CodeAct provider. The model only
|
||||||
|
# sees `execute_code`; `compute` and `fetch_data` are reachable from
|
||||||
|
# inside the sandbox via `call_tool(...)`.
|
||||||
|
codeact = HyperlightCodeActProvider(
|
||||||
|
tools=[compute, fetch_data],
|
||||||
|
approval_mode="never_require",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Build the agent. History is managed by the hosting infrastructure, so
|
||||||
|
# request the model not to persist server-side conversation state.
|
||||||
|
agent = Agent(
|
||||||
|
client=client,
|
||||||
|
instructions="You are a helpful assistant. Keep your answers brief.",
|
||||||
|
context_providers=[codeact],
|
||||||
|
default_options={"store": False},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Serve the agent over the Foundry Responses protocol.
|
||||||
|
server = ResponsesHostServer(agent)
|
||||||
|
server.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# agent-framework, agent-framework-foundry-hosting, and agent-framework-hyperlight
|
||||||
|
# are installed from local source by the Dockerfile (build context = python/).
|
||||||
|
# Add any sample-only third-party deps here.
|
||||||
@@ -222,4 +222,4 @@ This will package your agent and deploy it to the Foundry environment, making it
|
|||||||
|
|
||||||
For the full deployment guide, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
|
For the full deployment guide, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
|
||||||
|
|
||||||
Once deployed, learn more about how to manage deployed agents in the [official management guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/manage-hosted-agent).
|
Once deployed, learn more about how to manage deployed agents in the [official management guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/manage-hosted-agent).
|
||||||
|
|||||||
Generated
+25
-23
@@ -379,6 +379,7 @@ all = [
|
|||||||
{ name = "agent-framework-foundry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "agent-framework-foundry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
{ name = "agent-framework-foundry-local", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "agent-framework-foundry-local", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
{ name = "agent-framework-github-copilot", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" },
|
{ name = "agent-framework-github-copilot", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" },
|
||||||
|
{ name = "agent-framework-hyperlight", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32')" },
|
||||||
{ name = "agent-framework-lab", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "agent-framework-lab", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
{ name = "agent-framework-mem0", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "agent-framework-mem0", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
{ name = "agent-framework-ollama", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "agent-framework-ollama", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
@@ -407,6 +408,7 @@ requires-dist = [
|
|||||||
{ name = "agent-framework-foundry", marker = "extra == 'all'", editable = "packages/foundry" },
|
{ name = "agent-framework-foundry", marker = "extra == 'all'", editable = "packages/foundry" },
|
||||||
{ name = "agent-framework-foundry-local", marker = "extra == 'all'", editable = "packages/foundry_local" },
|
{ name = "agent-framework-foundry-local", marker = "extra == 'all'", editable = "packages/foundry_local" },
|
||||||
{ name = "agent-framework-github-copilot", marker = "python_full_version >= '3.11' and extra == 'all'", editable = "packages/github_copilot" },
|
{ name = "agent-framework-github-copilot", marker = "python_full_version >= '3.11' and extra == 'all'", editable = "packages/github_copilot" },
|
||||||
|
{ name = "agent-framework-hyperlight", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'all') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'all')", editable = "packages/hyperlight" },
|
||||||
{ name = "agent-framework-lab", marker = "extra == 'all'", editable = "packages/lab" },
|
{ name = "agent-framework-lab", marker = "extra == 'all'", editable = "packages/lab" },
|
||||||
{ name = "agent-framework-mem0", marker = "extra == 'all'", editable = "packages/mem0" },
|
{ name = "agent-framework-mem0", marker = "extra == 'all'", editable = "packages/mem0" },
|
||||||
{ name = "agent-framework-ollama", marker = "extra == 'all'", editable = "packages/ollama" },
|
{ name = "agent-framework-ollama", marker = "extra == 'all'", editable = "packages/ollama" },
|
||||||
@@ -601,7 +603,7 @@ requires-dist = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-framework-hyperlight"
|
name = "agent-framework-hyperlight"
|
||||||
version = "1.0.0a260429"
|
version = "1.0.0b260501"
|
||||||
source = { editable = "packages/hyperlight" }
|
source = { editable = "packages/hyperlight" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||||
@@ -613,9 +615,9 @@ dependencies = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "agent-framework-core", editable = "packages/core" },
|
{ name = "agent-framework-core", editable = "packages/core" },
|
||||||
{ name = "hyperlight-sandbox", specifier = ">=0.3.0,<0.4" },
|
{ name = "hyperlight-sandbox", specifier = ">=0.4.0,<0.5" },
|
||||||
{ name = "hyperlight-sandbox-backend-wasm", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32')", specifier = ">=0.3.0,<0.4" },
|
{ name = "hyperlight-sandbox-backend-wasm", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32')", specifier = ">=0.4.0,<0.5" },
|
||||||
{ name = "hyperlight-sandbox-python-guest", specifier = ">=0.3.0,<0.4" },
|
{ name = "hyperlight-sandbox-python-guest", specifier = ">=0.4.0,<0.5" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1636,7 +1638,7 @@ name = "clr-loader"
|
|||||||
version = "0.2.10"
|
version = "0.2.10"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -2949,35 +2951,35 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyperlight-sandbox"
|
name = "hyperlight-sandbox"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/fe/ce88996ea3e3e05130d6f0e8cd2ffbe9ab9bf3d9448b7050d4b8d0802b0a/hyperlight_sandbox-0.3.0.tar.gz", hash = "sha256:00491ce267ffbdb206377c79b4afd86510177ad73f4daf2ef7fce02b54eaf801", size = 9251, upload-time = "2026-04-07T03:49:52.542Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/de/5e/14c69eac7e1c74fbd556c6f890729a3d232d32d65cd9f8cfde72c0534e61/hyperlight_sandbox-0.4.0.tar.gz", hash = "sha256:90d7b91d4d8e17054e282b0daed55c261392a748dafc57e6416d3184cdac910b", size = 9262, upload-time = "2026-05-02T00:00:02.866Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/33/e6dcd6729308d13570ae2d3be0e476019a6f3fea387d7549bb1f77ce0408/hyperlight_sandbox-0.3.0-py3-none-any.whl", hash = "sha256:ba8e6779d64e9c187acd93456851ebafaed2f49380e5d132bc0906a4080d2217", size = 5723, upload-time = "2026-04-07T03:49:53.276Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/e3/b8c106a274c08a30261105afa5511e0ec55960e86b2f6c51e3095e96647c/hyperlight_sandbox-0.4.0-py3-none-any.whl", hash = "sha256:7ae44d2448ed6ecadb368373c7e45eb395521e7774c86a1cbc1ef9cdfc25cd2a", size = 5723, upload-time = "2026-05-02T00:00:03.811Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyperlight-sandbox-backend-wasm"
|
name = "hyperlight-sandbox-backend-wasm"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/91/c9d68cad7996fdd2f1facef1453156bdd8d52eefa976cc8c827c13029497/hyperlight_sandbox_backend_wasm-0.3.0-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:eda362f5f737b0823326290d7627c76ce0547a78e70f07f8c9d177e34622fc02", size = 3806454, upload-time = "2026-04-07T03:49:24.238Z" },
|
{ url = "https://files.pythonhosted.org/packages/b6/7c/355864a5bc814eeb8788ea35eb2cdf6c60f34afd7a7cd4433a368f26f60e/hyperlight_sandbox_backend_wasm-0.4.0-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:5b5da47c21aeebd2a7bb9394bbd481fe285fafc5b48d1f3685034e203219bcb5", size = 3921265, upload-time = "2026-05-01T23:59:20.42Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/6f/6b2399a1caf59dd19b635d99ee1add0c975af7bc3317f5d0f1f9c3f90aa0/hyperlight_sandbox_backend_wasm-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:79347b7ae94f2786691b04cb52130dabc5991e0c03b42a24bad8adc766832655", size = 3283951, upload-time = "2026-04-07T03:49:17.137Z" },
|
{ url = "https://files.pythonhosted.org/packages/46/ec/628fafcbf1483f86df6bf9412b23da68b5cd1156d91b0998ed5c62d0b9c4/hyperlight_sandbox_backend_wasm-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:c514e662fed41ee8222b09da5f1130ac856c91cc4523b354bb8e7b96da352611", size = 3387473, upload-time = "2026-05-01T23:59:25.548Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/f2/b380c34a0ce8d486a05adb66757f98cca029e1fb1c96b1c29be0d25d3882/hyperlight_sandbox_backend_wasm-0.3.0-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:aff9eec4803fb535a140298e2632529f4150fcf3c6ea3ff2ae4571572a836116", size = 3806601, upload-time = "2026-04-07T03:49:22.853Z" },
|
{ url = "https://files.pythonhosted.org/packages/23/d2/09812f51a02e39236bfb5bfc40b4021e98f07e2f32f8d6a72745884d49f8/hyperlight_sandbox_backend_wasm-0.4.0-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c0935698f58a144150000e978e061fa78daa6f2ac1861980b32571f7c27a53fd", size = 3921230, upload-time = "2026-05-01T23:59:18.635Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/5a/fb78cfd934e0523887b8d5b073b7b2aed3b545add21cda3aa95929ac1659/hyperlight_sandbox_backend_wasm-0.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:b6151704dd19862c9869b115752b4504b45d0b2eeb46aa9385a1a3b8be11cfa8", size = 3284164, upload-time = "2026-04-07T03:49:18.556Z" },
|
{ url = "https://files.pythonhosted.org/packages/34/b2/56181b5a21c17ad4636686de3463706a85883aa70dd3b1b160dd7e95627b/hyperlight_sandbox_backend_wasm-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b9486b7617615334d4b06d9462ad57edfe3f161d40d880b866db7d240b4d04e", size = 3388119, upload-time = "2026-05-01T23:59:21.966Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/bc/4e21f5c7ccd9307ac63a61c71b62a57ee4a9e6eec77fc72ff072907a21f5/hyperlight_sandbox_backend_wasm-0.3.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:cfd1d22ce221774d82a5174d268d56ff70fc1a23fb993a6491358b5d0ed169bf", size = 3802901, upload-time = "2026-04-07T03:49:19.845Z" },
|
{ url = "https://files.pythonhosted.org/packages/79/e5/3cdf21594eb28de7ca1a5a1ade27e137c8f3d7ab48d65fed87a3b74c4039/hyperlight_sandbox_backend_wasm-0.4.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ff4627950708909202ee24c6175dc41e9c05479f89393575e3de0f14e6f5a193", size = 3918189, upload-time = "2026-05-01T23:59:16.666Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/41/646be9b0c7bb0f9192e45a77414673aa414eb316c92b5312efe6fb4ce802/hyperlight_sandbox_backend_wasm-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:229ab494a422f2de895a2a27ad6a6a2daed710ea062d7c213878bbe5f5b32fa7", size = 3281220, upload-time = "2026-04-07T03:49:21.368Z" },
|
{ url = "https://files.pythonhosted.org/packages/5b/97/b1bb9893bbeb979d133dc542520125dcbf8394d1a2537e753118b37c7cab/hyperlight_sandbox_backend_wasm-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cce7dc28b9ded034a11a9a8cf7b9ffb838e29006be8d2e01646dd131ba501b73", size = 3383520, upload-time = "2026-05-01T23:59:27.261Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/3a/f8ec4a41fffba4036dfc3cbddc3dfb6e87466b01afe1cb0a50cc6a0f0eed/hyperlight_sandbox_backend_wasm-0.3.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:b91905ee2ddd36a78b0dd13b1a62be99a995a45121587c111692591e40b36912", size = 3802789, upload-time = "2026-04-07T03:49:15.614Z" },
|
{ url = "https://files.pythonhosted.org/packages/8c/29/deee4e31086628750f0ce1f67da1e28c613fd2df68465de130cbfe51e72d/hyperlight_sandbox_backend_wasm-0.4.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:88e194515e4784f68676b6906c98a4000f913c93172cf07981d8a977e756bbd6", size = 3917939, upload-time = "2026-05-01T23:59:14.805Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/62/dfa8c15102f9b8ec5c3b5ffb54b99d60c75e7a6e4d00540757656bc5a5d8/hyperlight_sandbox_backend_wasm-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:eff682761c3b86abfe7e0d523ea0e6d5c7e8299302917c53918743b82c9d1ea2", size = 3280501, upload-time = "2026-04-07T03:49:13.939Z" },
|
{ url = "https://files.pythonhosted.org/packages/15/2a/6822aec3c04c46893406d0d6ed576dbdb4b5c1d76a0124dc220bb45b0d34/hyperlight_sandbox_backend_wasm-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d1cd2269a5651ea9be1f94a3e3388f6af69e41dbc2b808c3b806481fe17ce163", size = 3383110, upload-time = "2026-05-01T23:59:23.736Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyperlight-sandbox-python-guest"
|
name = "hyperlight-sandbox-python-guest"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/23/6a/f182c4315d31a98dd3b82f9274638e3adb399779584af93c5087bb2f814f/hyperlight_sandbox_python_guest-0.3.0.tar.gz", hash = "sha256:b1de5d8e87375dc6bef744ecd7ae2a7f43d5f6b913b4e990e9872bd439c0b19e", size = 21554625, upload-time = "2026-04-07T03:49:42.672Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/fd/816d1f3f277ff149a45da5381967aa04c22bc7702b5c14f0acfd9db2cee7/hyperlight_sandbox_python_guest-0.4.0.tar.gz", hash = "sha256:64c3c6c13fe550bf5b680fa0b965cf62bc4668084cc275c3467e3c015e6ead36", size = 21657381, upload-time = "2026-05-01T23:59:46.589Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/8e/4cd754928464f56528645c7421ccbb3fcbe45ad2542f899712b0f2f2c0e1/hyperlight_sandbox_python_guest-0.3.0-py3-none-any.whl", hash = "sha256:3c55a7420666ad9a208893dbdf7ad1b5c8ad4f3a94b1a56e64979719c7ce95c1", size = 21716481, upload-time = "2026-04-07T03:49:39.885Z" },
|
{ url = "https://files.pythonhosted.org/packages/98/ba/efb9aacf993f0ac142da5beb9177b221e49dc860c6ea398de236015a52a0/hyperlight_sandbox_python_guest-0.4.0-py3-none-any.whl", hash = "sha256:0789eb794b99606288402ed3921b5e2630800a69d24117ecd9b82e816568202d", size = 21822062, upload-time = "2026-05-01T23:59:50.99Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5132,8 +5134,8 @@ name = "powerfx"
|
|||||||
version = "0.0.34"
|
version = "0.0.34"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
|
||||||
{ name = "pythonnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "pythonnet", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -5806,7 +5808,7 @@ name = "pythonnet"
|
|||||||
version = "3.0.5"
|
version = "3.0.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
{ name = "clr-loader", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
|||||||
Reference in New Issue
Block a user