managed-deepagents 0.1.3.dev15__py3-none-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- managed_deepagents/__init__.py +158 -0
- managed_deepagents/__main__.py +3 -0
- managed_deepagents/_binaries/mda +0 -0
- managed_deepagents/_binaries/mda.exe +0 -0
- managed_deepagents/runtime.py +442 -0
- managed_deepagents-0.1.3.dev15.dist-info/METADATA +114 -0
- managed_deepagents-0.1.3.dev15.dist-info/RECORD +10 -0
- managed_deepagents-0.1.3.dev15.dist-info/WHEEL +5 -0
- managed_deepagents-0.1.3.dev15.dist-info/entry_points.txt +2 -0
- managed_deepagents-0.1.3.dev15.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Managed Deep Agents for the Python ecosystem.
|
|
2
|
+
|
|
3
|
+
This package provides two things:
|
|
4
|
+
|
|
5
|
+
* the ``define_deep_agent`` authoring interface (the v0 contract for MDA), and
|
|
6
|
+
* the prebuilt, cross-compiled ``mda`` CLI. Each published wheel bundles the
|
|
7
|
+
binary for a single platform under ``_binaries/`` and exposes it through the
|
|
8
|
+
``mda`` console script.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"define_deep_agent",
|
|
20
|
+
"define_sandbox",
|
|
21
|
+
"DeepAgentDefinition",
|
|
22
|
+
"SandboxDefinition",
|
|
23
|
+
"MANAGED_KEYS",
|
|
24
|
+
"main",
|
|
25
|
+
"binary_path",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
#: Properties the managed runtime owns. Agent authors never set these — MDA
|
|
29
|
+
#: wires the backend, store, and checkpointer at deploy time.
|
|
30
|
+
MANAGED_KEYS = ("backend", "store", "checkpointer")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DeepAgentDefinition:
|
|
34
|
+
"""The pre-runtime spec returned by :func:`define_deep_agent`.
|
|
35
|
+
|
|
36
|
+
It is intentionally *not* a compiled graph: the CLI generates an entry
|
|
37
|
+
module that hands this definition to the managed runtime, which injects the
|
|
38
|
+
managed backend/store/checkpointer and compiles it into a Deep Agent.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
kind = "deep-agent"
|
|
42
|
+
|
|
43
|
+
def __init__(self, config: dict[str, Any]) -> None:
|
|
44
|
+
self.config = config
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str: # pragma: no cover - debug helper
|
|
47
|
+
keys = ", ".join(sorted(self.config))
|
|
48
|
+
return f"DeepAgentDefinition({keys})"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def define_deep_agent(**config: Any) -> DeepAgentDefinition:
|
|
52
|
+
"""Define a Managed Deep Agent.
|
|
53
|
+
|
|
54
|
+
Accepts the full ``create_deep_agent`` keyword surface except the managed
|
|
55
|
+
keys (``backend``, ``store``, ``checkpointer``, ``system_prompt``) and
|
|
56
|
+
returns a pre-runtime spec. ``mda deploy`` generates an entry module that
|
|
57
|
+
hands this definition to the managed runtime, which injects the
|
|
58
|
+
platform-owned backend/store/checkpointer, embeds the system prompt from
|
|
59
|
+
``instructions.md``, and compiles it into a Deep Agent.
|
|
60
|
+
"""
|
|
61
|
+
if "system_prompt" in config:
|
|
62
|
+
raise TypeError(
|
|
63
|
+
"system_prompt is managed by MDA and cannot be set in "
|
|
64
|
+
"define_deep_agent(...). Write your agent's system prompt in "
|
|
65
|
+
"instructions.md next to agent.py; the CLI embeds it at deploy time."
|
|
66
|
+
)
|
|
67
|
+
managed = [key for key in MANAGED_KEYS if key in config]
|
|
68
|
+
if managed:
|
|
69
|
+
raise TypeError(
|
|
70
|
+
f"{', '.join(managed)} {'is' if len(managed) == 1 else 'are'} owned by "
|
|
71
|
+
"the managed runtime and cannot be set in define_deep_agent(...). "
|
|
72
|
+
"Configure the sandbox under sandbox/; the store and checkpointer are "
|
|
73
|
+
"wired automatically."
|
|
74
|
+
)
|
|
75
|
+
return DeepAgentDefinition(config)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SandboxDefinition:
|
|
79
|
+
"""The pre-runtime sandbox spec returned by :func:`define_sandbox`.
|
|
80
|
+
|
|
81
|
+
A Managed Deep Agent declares its execution environment in
|
|
82
|
+
``sandbox/__init__.py`` by importing a real provider class (such as
|
|
83
|
+
``deepagents``' ``LangSmithSandbox``) and passing it to
|
|
84
|
+
:func:`define_sandbox`. MDA owns how it is *resolved*: construction,
|
|
85
|
+
run-scoped naming, reuse across turns, the provisioning ``setup.sh``, and
|
|
86
|
+
lifecycle/TTL.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
kind = "sandbox"
|
|
90
|
+
|
|
91
|
+
#: Option keys MDA owns; ``define_sandbox`` callers must not set these.
|
|
92
|
+
MANAGED_OPTION_KEYS = ("name", "image", "snapshot", "image_name")
|
|
93
|
+
|
|
94
|
+
def __init__(self, provider: Any, options: dict[str, Any]) -> None:
|
|
95
|
+
self.provider = provider
|
|
96
|
+
self.options = options
|
|
97
|
+
|
|
98
|
+
def __repr__(self) -> str: # pragma: no cover - debug helper
|
|
99
|
+
name = getattr(self.provider, "__name__", repr(self.provider))
|
|
100
|
+
return f"SandboxDefinition(provider={name}, options={sorted(self.options)})"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def define_sandbox(provider: Any, **options: Any) -> SandboxDefinition:
|
|
104
|
+
"""Declare the managed sandbox provider for a Managed Deep Agent.
|
|
105
|
+
|
|
106
|
+
``provider`` is a sandbox provider class (e.g.
|
|
107
|
+
``from deepagents import LangSmithSandbox``). The keyword options are the
|
|
108
|
+
provider's own ``create(...)`` options, plus the managed reuse/lifecycle
|
|
109
|
+
knobs ``scope`` (``"thread"`` | ``"tenant"`` | ``"actor"``, default
|
|
110
|
+
``"thread"``) and ``idle_ttl_seconds``.
|
|
111
|
+
|
|
112
|
+
MDA owns naming, scoping, lifecycle, image/snapshot construction, reuse, the
|
|
113
|
+
provisioning ``setup.sh``, and cleanup, so image-related keys cannot be set
|
|
114
|
+
here.
|
|
115
|
+
"""
|
|
116
|
+
managed = [key for key in SandboxDefinition.MANAGED_OPTION_KEYS if key in options]
|
|
117
|
+
if managed:
|
|
118
|
+
raise TypeError(
|
|
119
|
+
f"{', '.join(managed)} {'is' if len(managed) == 1 else 'are'} owned by "
|
|
120
|
+
"the managed runtime and cannot be set in define_sandbox(...). MDA "
|
|
121
|
+
"supplies the sandbox image/snapshot and run-scoped name automatically."
|
|
122
|
+
)
|
|
123
|
+
return SandboxDefinition(provider, options)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def binary_path() -> Path:
|
|
127
|
+
"""Return the path to the bundled ``mda`` binary for this platform."""
|
|
128
|
+
name = "mda.exe" if os.name == "nt" else "mda"
|
|
129
|
+
return Path(__file__).parent / "_binaries" / name
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main() -> int:
|
|
133
|
+
"""Entry point for the ``mda`` console script.
|
|
134
|
+
|
|
135
|
+
Forwards all arguments to the bundled binary and returns its exit code.
|
|
136
|
+
"""
|
|
137
|
+
binary = binary_path()
|
|
138
|
+
if not binary.exists():
|
|
139
|
+
sys.stderr.write(
|
|
140
|
+
"managed-deepagents: the prebuilt mda binary was not found in this "
|
|
141
|
+
"installation. Reinstall the platform-specific wheel.\n"
|
|
142
|
+
)
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
argv = [str(binary), *sys.argv[1:]]
|
|
146
|
+
|
|
147
|
+
# On POSIX, replace the current process so signals propagate cleanly.
|
|
148
|
+
if os.name == "posix":
|
|
149
|
+
os.execv(str(binary), argv)
|
|
150
|
+
return 0 # unreachable
|
|
151
|
+
|
|
152
|
+
import subprocess
|
|
153
|
+
|
|
154
|
+
return subprocess.run(argv).returncode
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
raise SystemExit(main())
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""Managed runtime seam for compiled Managed Deep Agents.
|
|
2
|
+
|
|
3
|
+
This module is consumed only by the entry module that ``mda deploy`` generates —
|
|
4
|
+
agent authors never import it directly. It is the point where MDA injects the
|
|
5
|
+
platform-owned backend (the configured sandbox), plus the system prompt embedded
|
|
6
|
+
from ``instructions.md``, and — in a later milestone — the managed store and
|
|
7
|
+
checkpointer.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import inspect
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from . import DeepAgentDefinition, SandboxDefinition
|
|
18
|
+
|
|
19
|
+
__all__ = ["compile_managed_agent", "announce_sandbox_plan"]
|
|
20
|
+
|
|
21
|
+
#: Absolute path the provisioning script is uploaded to inside the sandbox.
|
|
22
|
+
_SETUP_REMOTE_PATH = "/tmp/mda-setup.sh"
|
|
23
|
+
|
|
24
|
+
#: Created sandboxes, keyed by scope, reused for the lifetime of this runtime
|
|
25
|
+
#: process. This guarantees ``setup.sh`` runs only once per newly created
|
|
26
|
+
#: sandbox; reused sandboxes (same scope key) skip provisioning entirely.
|
|
27
|
+
_managed_sandboxes: dict[str, Any] = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compile_managed_agent(
|
|
31
|
+
definition: DeepAgentDefinition,
|
|
32
|
+
config: Any | None = None,
|
|
33
|
+
*,
|
|
34
|
+
system_prompt: str | None = None,
|
|
35
|
+
sandbox: SandboxDefinition | None = None,
|
|
36
|
+
setup_script: str | None = None,
|
|
37
|
+
) -> Any:
|
|
38
|
+
"""Compile a :class:`DeepAgentDefinition` into a runnable Deep Agent.
|
|
39
|
+
|
|
40
|
+
``system_prompt`` is the text the CLI embedded from ``instructions.md``.
|
|
41
|
+
``sandbox`` is the ``sandbox/__init__.py`` declaration, and ``setup_script``
|
|
42
|
+
is the contents of ``sandbox/setup.sh`` (run once when a sandbox is first
|
|
43
|
+
provisioned). All are merged here so authors never set them directly.
|
|
44
|
+
|
|
45
|
+
The managed store/checkpointer wiring lands in a follow-up milestone; this
|
|
46
|
+
signature is the stable injection point.
|
|
47
|
+
"""
|
|
48
|
+
# Imported lazily so the CLI wheel does not require ``deepagents`` at import
|
|
49
|
+
# time; it is present in the deployed runtime.
|
|
50
|
+
from deepagents import create_deep_agent
|
|
51
|
+
|
|
52
|
+
merged = dict(definition.config)
|
|
53
|
+
if system_prompt is not None:
|
|
54
|
+
merged["system_prompt"] = system_prompt
|
|
55
|
+
|
|
56
|
+
# TODO(mda): inject the managed store/checkpointer here, scoped by ``config``
|
|
57
|
+
# identity / thread_id.
|
|
58
|
+
if sandbox is not None:
|
|
59
|
+
merged["backend"] = _resolve_managed_sandbox(sandbox, setup_script, config)
|
|
60
|
+
|
|
61
|
+
return create_deep_agent(**merged)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
#: Cache key for the single, process-wide sandbox used under ``mda dev``.
|
|
65
|
+
#:
|
|
66
|
+
#: Deploys key sandboxes per scope (thread/tenant/actor) for isolation. Dev is a
|
|
67
|
+
#: single developer iterating locally, so it shares one sandbox across every
|
|
68
|
+
#: thread: that lets :func:`announce_sandbox_plan` create it eagerly at startup
|
|
69
|
+
#: (to report local-vs-remote and the temp dir up front) and have the first real
|
|
70
|
+
#: run reuse it instead of provisioning a second one.
|
|
71
|
+
_DEV_SANDBOX_SCOPE_KEY = "__mda_dev__"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def announce_sandbox_plan(
|
|
75
|
+
sandbox: SandboxDefinition, setup_script: str | None = None
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Eagerly resolve the ``mda dev`` sandbox at startup so the developer sees
|
|
78
|
+
the definitive decision — local vs. the configured provider, and the temp
|
|
79
|
+
dir path when local — right when the dev server boots, not on the first run.
|
|
80
|
+
|
|
81
|
+
The generated entry calls this at module load (graph registration); because
|
|
82
|
+
dev shares one sandbox (:data:`_DEV_SANDBOX_SCOPE_KEY`), the first real run
|
|
83
|
+
reuses what is created here. The authoritative "created" log (with the temp
|
|
84
|
+
dir) is emitted by :func:`_create_dev_sandbox`.
|
|
85
|
+
|
|
86
|
+
No-op outside ``mda dev``, so the generated entry can always call it.
|
|
87
|
+
"""
|
|
88
|
+
if not _is_dev_mode():
|
|
89
|
+
return
|
|
90
|
+
_resolve_managed_sandbox(sandbox, setup_script, None)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _resolve_managed_sandbox(
|
|
94
|
+
sandbox: SandboxDefinition,
|
|
95
|
+
setup_script: str | None,
|
|
96
|
+
config: Any | None,
|
|
97
|
+
) -> Any:
|
|
98
|
+
"""Resolve (and cache) the scoped sandbox backend, provisioning it with
|
|
99
|
+
``setup.sh`` the first time it is created."""
|
|
100
|
+
key = _DEV_SANDBOX_SCOPE_KEY if _is_dev_mode() else _sandbox_scope_key(sandbox, config)
|
|
101
|
+
backend = _managed_sandboxes.get(key)
|
|
102
|
+
if backend is None:
|
|
103
|
+
backend = _create_managed_sandbox(sandbox, setup_script)
|
|
104
|
+
_managed_sandboxes[key] = backend
|
|
105
|
+
return backend
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _sandbox_scope_key(sandbox: SandboxDefinition, config: Any | None) -> str:
|
|
109
|
+
"""Derive the reuse key from the run: by thread without identity, or by the
|
|
110
|
+
configured tenant/actor scope when identity is present."""
|
|
111
|
+
configurable = {}
|
|
112
|
+
if isinstance(config, dict):
|
|
113
|
+
configurable = config.get("configurable") or {}
|
|
114
|
+
identity = configurable.get("identity") or {}
|
|
115
|
+
tenant_id = (identity.get("tenant") or {}).get("id")
|
|
116
|
+
actor_id = (identity.get("actor") or {}).get("id")
|
|
117
|
+
thread_id = configurable.get("thread_id")
|
|
118
|
+
|
|
119
|
+
scope = sandbox.options.get("scope", "thread")
|
|
120
|
+
if scope == "tenant":
|
|
121
|
+
key = tenant_id
|
|
122
|
+
elif scope == "actor":
|
|
123
|
+
key = actor_id
|
|
124
|
+
else:
|
|
125
|
+
key = thread_id
|
|
126
|
+
|
|
127
|
+
return key or tenant_id or actor_id or thread_id or "default"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _create_managed_sandbox(sandbox: SandboxDefinition, setup_script: str | None) -> Any:
|
|
131
|
+
# ``mda dev`` never provisions a real provider sandbox the way a deploy does
|
|
132
|
+
# (it builds no snapshot/image), so it gets its own resolution path with a
|
|
133
|
+
# local fallback. The flag gating ensures a real deployment can never be
|
|
134
|
+
# silently downgraded to local shell execution.
|
|
135
|
+
if _is_dev_mode():
|
|
136
|
+
return _create_dev_sandbox(sandbox, setup_script)
|
|
137
|
+
return _create_provider_sandbox(sandbox, setup_script)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _create_provider_sandbox(
|
|
141
|
+
sandbox: SandboxDefinition, setup_script: str | None
|
|
142
|
+
) -> Any:
|
|
143
|
+
"""Production path: build the configured provider's sandbox and provision it.
|
|
144
|
+
|
|
145
|
+
Any failure propagates — a misconfigured deployment must surface, not
|
|
146
|
+
silently degrade to local shell execution.
|
|
147
|
+
"""
|
|
148
|
+
# Strip MDA-managed knobs before handing the rest to the provider factory.
|
|
149
|
+
provider_options = {
|
|
150
|
+
key: value
|
|
151
|
+
for key, value in sandbox.options.items()
|
|
152
|
+
if key not in ("scope", "idle_ttl_seconds")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
backend = _construct_sandbox_backend(sandbox.provider, provider_options)
|
|
156
|
+
|
|
157
|
+
if setup_script and setup_script.strip():
|
|
158
|
+
_run_setup_script(backend, setup_script)
|
|
159
|
+
|
|
160
|
+
return backend
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _create_dev_sandbox(sandbox: SandboxDefinition, setup_script: str | None) -> Any:
|
|
164
|
+
"""Dev path: prefer the configured provider when its credentials are present,
|
|
165
|
+
but fall back to a local temp-directory sandbox when credentials are missing
|
|
166
|
+
*or* the provider cannot be created (e.g. ``LangSmithSandbox``, which needs a
|
|
167
|
+
snapshot that only ``mda deploy`` builds). This keeps ``mda dev`` working
|
|
168
|
+
offline and resilient to provider/config gaps while still exercising real
|
|
169
|
+
providers that can spin up from an API key alone.
|
|
170
|
+
"""
|
|
171
|
+
cred = _provider_credential_status(sandbox.provider)
|
|
172
|
+
|
|
173
|
+
if cred.status != "missing":
|
|
174
|
+
try:
|
|
175
|
+
backend = _create_provider_sandbox(sandbox, setup_script)
|
|
176
|
+
if cred.status == "available":
|
|
177
|
+
_log_dev_sandbox_once(
|
|
178
|
+
f"[mda dev] using the configured {cred.provider} sandbox."
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
_log_dev_sandbox_once(
|
|
182
|
+
"[mda dev] using the configured sandbox provider "
|
|
183
|
+
f"({cred.provider})."
|
|
184
|
+
)
|
|
185
|
+
return backend
|
|
186
|
+
except Exception as error: # noqa: BLE001 - dev fallback is intentional
|
|
187
|
+
return _create_local_sandbox(
|
|
188
|
+
setup_script,
|
|
189
|
+
f"the configured {cred.provider} sandbox could not be created "
|
|
190
|
+
f"({error}); using a local temporary directory instead",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
missing = ", ".join(cred.missing) or "the provider credentials"
|
|
194
|
+
return _create_local_sandbox(
|
|
195
|
+
setup_script,
|
|
196
|
+
"no credentials found for the configured sandbox provider "
|
|
197
|
+
f"({cred.provider}); using a local temporary directory instead — set "
|
|
198
|
+
f"{missing} to use it",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _create_local_sandbox(setup_script: str | None, reason: str) -> Any:
|
|
203
|
+
"""Provision the local dev sandbox and run ``setup.sh`` inline inside it."""
|
|
204
|
+
backend = _create_local_dev_sandbox(reason)
|
|
205
|
+
if setup_script and setup_script.strip():
|
|
206
|
+
# Run the script inline rather than uploading it first: the local backend
|
|
207
|
+
# virtualizes file paths (uploads land under the temp root) but executes
|
|
208
|
+
# shell commands against the real filesystem, so an uploaded
|
|
209
|
+
# ``/tmp/mda-setup.sh`` would not be found by ``bash``. Executed inline,
|
|
210
|
+
# it runs with the temp root as its working directory.
|
|
211
|
+
_assert_setup_succeeded(_await_if_needed(backend.execute(setup_script)))
|
|
212
|
+
return backend
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
#: Guards the one-time dev sandbox notice so it logs at most once per process.
|
|
216
|
+
#:
|
|
217
|
+
#: ``_create_managed_sandbox`` runs once per sandbox *scope key*
|
|
218
|
+
#: (thread/tenant/actor), not once per process. The ``mda dev`` LangGraph server
|
|
219
|
+
#: is long-lived and serves many threads, so each new thread misses the
|
|
220
|
+
#: ``_managed_sandboxes`` cache and provisions a fresh sandbox — without this
|
|
221
|
+
#: guard the banner would reprint on the first turn of every new thread. The
|
|
222
|
+
#: message is session-level context, so once is enough.
|
|
223
|
+
_logged_dev_sandbox_notice = False
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _is_dev_mode() -> bool:
|
|
227
|
+
"""Whether MDA is running under ``mda dev``.
|
|
228
|
+
|
|
229
|
+
The CLI sets ``MDA_DEV`` when it launches the local LangGraph dev server; it
|
|
230
|
+
is never set for ``mda deploy``.
|
|
231
|
+
"""
|
|
232
|
+
return bool(os.environ.get("MDA_DEV"))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
#: Per-provider environment variables MDA expects to hold the credentials each
|
|
236
|
+
#: sandbox provider needs. Keyed by the provider's ``provider_id`` (when set) or
|
|
237
|
+
#: its class name.
|
|
238
|
+
#:
|
|
239
|
+
#: The value is a list of alternative credential *sets*: a provider is
|
|
240
|
+
#: considered credentialed if **every** variable in **any one** set is present.
|
|
241
|
+
#: An empty list means the provider needs no credentials (e.g. it runs locally).
|
|
242
|
+
#:
|
|
243
|
+
#: Sources: the deepagents sandbox docs and each provider's own SDK docs.
|
|
244
|
+
#: - LangSmith: https://docs.langchain.com/langsmith/sandboxes
|
|
245
|
+
#: - Daytona / E2B / Modal / Runloop / Vercel / AgentCore: provider SDK docs.
|
|
246
|
+
_PROVIDER_CREDENTIAL_ENV_VARS: dict[str, list[list[str]]] = {
|
|
247
|
+
# LangSmith managed sandboxes.
|
|
248
|
+
"LangSmithSandbox": [["LANGSMITH_API_KEY"]],
|
|
249
|
+
# Daytona (``langchain-daytona`` / deepagents ``DaytonaSandbox``).
|
|
250
|
+
"DaytonaSandbox": [["DAYTONA_API_KEY"]],
|
|
251
|
+
# E2B (``langchain-e2b`` / deepagents ``E2BSandbox``).
|
|
252
|
+
"E2BSandbox": [["E2B_API_KEY"]],
|
|
253
|
+
# Modal requires both a token id and secret.
|
|
254
|
+
"ModalSandbox": [["MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET"]],
|
|
255
|
+
# Runloop devboxes.
|
|
256
|
+
"RunloopSandbox": [["RUNLOOP_API_KEY"]],
|
|
257
|
+
# Vercel: either an OIDC token, or an access token + team + project ids.
|
|
258
|
+
"VercelSandbox": [
|
|
259
|
+
["VERCEL_OIDC_TOKEN"],
|
|
260
|
+
["VERCEL_TOKEN", "VERCEL_TEAM_ID", "VERCEL_PROJECT_ID"],
|
|
261
|
+
],
|
|
262
|
+
# AWS Bedrock AgentCore: standard AWS credential resolution.
|
|
263
|
+
"AgentCoreSandbox": [
|
|
264
|
+
["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
|
|
265
|
+
["AWS_PROFILE"],
|
|
266
|
+
],
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass(frozen=True)
|
|
271
|
+
class _ProviderCredentialStatus:
|
|
272
|
+
#: One of ``"available"`` (credentials present or none needed),
|
|
273
|
+
#: ``"missing"`` (a known provider's credentials are absent), or
|
|
274
|
+
#: ``"unknown"`` (provider not in ``_PROVIDER_CREDENTIAL_ENV_VARS``).
|
|
275
|
+
status: str
|
|
276
|
+
#: The provider id / class name used for the lookup.
|
|
277
|
+
provider: str
|
|
278
|
+
#: Env vars from the primary credential set, surfaced when ``missing``.
|
|
279
|
+
missing: tuple[str, ...] = ()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _provider_key(provider: Any) -> str:
|
|
283
|
+
"""Resolve the lookup key for a sandbox provider (its id, else class name)."""
|
|
284
|
+
provider_id = getattr(provider, "provider_id", None) or getattr(
|
|
285
|
+
provider, "providerId", None
|
|
286
|
+
)
|
|
287
|
+
if isinstance(provider_id, str) and provider_id.strip():
|
|
288
|
+
return provider_id.strip()
|
|
289
|
+
name = getattr(provider, "__name__", None) or type(provider).__name__
|
|
290
|
+
return str(name).strip()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _credential_set_satisfied(env_vars: list[str]) -> bool:
|
|
294
|
+
"""Whether every variable in a credential set is set and non-empty."""
|
|
295
|
+
return all((os.environ.get(name) or "").strip() for name in env_vars)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _provider_credential_status(provider: Any) -> _ProviderCredentialStatus:
|
|
299
|
+
"""Determine whether the configured provider's credentials are available.
|
|
300
|
+
|
|
301
|
+
Lets ``mda dev`` choose between the real provider and the local fallback.
|
|
302
|
+
"""
|
|
303
|
+
key = _provider_key(provider)
|
|
304
|
+
sets = _PROVIDER_CREDENTIAL_ENV_VARS.get(key)
|
|
305
|
+
if sets is None:
|
|
306
|
+
return _ProviderCredentialStatus("unknown", key or "unknown")
|
|
307
|
+
if not sets or any(_credential_set_satisfied(s) for s in sets):
|
|
308
|
+
return _ProviderCredentialStatus("available", key)
|
|
309
|
+
return _ProviderCredentialStatus("missing", key, tuple(sets[0]))
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _create_local_dev_sandbox(reason: str) -> Any:
|
|
313
|
+
"""Provision a local, throwaway sandbox rooted at a fresh OS temp directory.
|
|
314
|
+
|
|
315
|
+
Uses ``deepagents``' ``LocalShellBackend``, which runs commands and file
|
|
316
|
+
operations against the host within the temp dir — no remote sandbox, no
|
|
317
|
+
credentials, no network. Intended only for ``mda dev``.
|
|
318
|
+
|
|
319
|
+
``reason`` explains *why* the local sandbox is being used (missing
|
|
320
|
+
credentials, provider creation failure, …) and is logged once alongside the
|
|
321
|
+
temp dir path.
|
|
322
|
+
"""
|
|
323
|
+
import tempfile
|
|
324
|
+
|
|
325
|
+
from deepagents import LocalShellBackend
|
|
326
|
+
|
|
327
|
+
root_dir = tempfile.mkdtemp(prefix="mda-dev-sandbox-")
|
|
328
|
+
_log_dev_sandbox_once(f"[mda dev] {reason}:\n {root_dir}")
|
|
329
|
+
|
|
330
|
+
return LocalShellBackend(root_dir, virtual_mode=True, inherit_env=True)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _log_dev_sandbox_once(message: str) -> None:
|
|
334
|
+
"""Emit a dev sandbox notice at most once per process.
|
|
335
|
+
|
|
336
|
+
See ``_logged_dev_sandbox_notice`` for why the dedupe matters: the notice is
|
|
337
|
+
reached once per sandbox scope key, and the dev server serves many threads.
|
|
338
|
+
"""
|
|
339
|
+
global _logged_dev_sandbox_notice
|
|
340
|
+
if _logged_dev_sandbox_notice:
|
|
341
|
+
return
|
|
342
|
+
_logged_dev_sandbox_notice = True
|
|
343
|
+
print(message)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _construct_sandbox_backend(provider: Any, options: dict[str, Any]) -> Any:
|
|
347
|
+
"""Construct the provider's sandbox backend.
|
|
348
|
+
|
|
349
|
+
Providers that expose a ``create(...)`` factory (the contract the TypeScript
|
|
350
|
+
SDK relies on, and the forward-compatible path for ``deepagents``) are used
|
|
351
|
+
directly. Otherwise, MDA falls back to building a LangSmith sandbox and
|
|
352
|
+
wrapping it with the provider class — the shape of today's
|
|
353
|
+
``deepagents.LangSmithSandbox(sandbox=...)``.
|
|
354
|
+
"""
|
|
355
|
+
create = getattr(provider, "create", None)
|
|
356
|
+
if callable(create):
|
|
357
|
+
result = create(**options)
|
|
358
|
+
return _await_if_needed(result)
|
|
359
|
+
|
|
360
|
+
return _wrap_langsmith_sandbox(provider, options)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _wrap_langsmith_sandbox(provider: Any, options: dict[str, Any]) -> Any:
|
|
364
|
+
"""Build a LangSmith sandbox and wrap it with ``provider``.
|
|
365
|
+
|
|
366
|
+
Used when the provider has no ``create(...)`` factory, which is the case for
|
|
367
|
+
the current ``deepagents.LangSmithSandbox`` whose constructor wraps an
|
|
368
|
+
existing ``langsmith.sandbox.Sandbox``.
|
|
369
|
+
"""
|
|
370
|
+
try:
|
|
371
|
+
from langsmith.sandbox import SandboxClient
|
|
372
|
+
except ImportError as error: # pragma: no cover - depends on runtime deps
|
|
373
|
+
raise RuntimeError(
|
|
374
|
+
"The configured sandbox provider does not expose a create(...) "
|
|
375
|
+
"factory and LangSmith is not installed, so MDA cannot construct it. "
|
|
376
|
+
"Install `langsmith` with sandbox support or use a provider that "
|
|
377
|
+
"exposes `create(...)`."
|
|
378
|
+
) from error
|
|
379
|
+
|
|
380
|
+
# Pass through only the options the LangSmith sandbox lifecycle understands;
|
|
381
|
+
# `default_timeout` is applied to the backend wrapper below, not the sandbox.
|
|
382
|
+
create_kwargs: dict[str, Any] = {}
|
|
383
|
+
if "idle_ttl_seconds" in options:
|
|
384
|
+
create_kwargs["idle_ttl_seconds"] = options["idle_ttl_seconds"]
|
|
385
|
+
if "snapshot_id" in options:
|
|
386
|
+
create_kwargs["snapshot_id"] = options["snapshot_id"]
|
|
387
|
+
|
|
388
|
+
client = SandboxClient()
|
|
389
|
+
sandbox = client.create_sandbox(**create_kwargs)
|
|
390
|
+
backend = provider(sandbox)
|
|
391
|
+
|
|
392
|
+
default_timeout = options.get("default_timeout")
|
|
393
|
+
if default_timeout is not None and hasattr(backend, "_default_timeout"):
|
|
394
|
+
backend._default_timeout = default_timeout # noqa: SLF001
|
|
395
|
+
|
|
396
|
+
return backend
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _run_setup_script(backend: Any, script: str) -> None:
|
|
400
|
+
"""Provision a freshly created sandbox by running ``setup.sh`` inside it."""
|
|
401
|
+
upload_files = getattr(backend, "upload_files", None)
|
|
402
|
+
if callable(upload_files):
|
|
403
|
+
_await_if_needed(upload_files([(_SETUP_REMOTE_PATH, script.encode("utf-8"))]))
|
|
404
|
+
result = _await_if_needed(backend.execute(f"bash {_SETUP_REMOTE_PATH}"))
|
|
405
|
+
else:
|
|
406
|
+
result = _await_if_needed(backend.execute(script))
|
|
407
|
+
|
|
408
|
+
_assert_setup_succeeded(result)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _assert_setup_succeeded(result: Any) -> None:
|
|
412
|
+
exit_code = getattr(result, "exit_code", None)
|
|
413
|
+
if isinstance(exit_code, int) and exit_code != 0:
|
|
414
|
+
detail = (getattr(result, "output", "") or "").strip()
|
|
415
|
+
message = f"sandbox setup.sh failed with exit code {exit_code}"
|
|
416
|
+
if detail:
|
|
417
|
+
message = f"{message}: {detail}"
|
|
418
|
+
raise RuntimeError(message)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _await_if_needed(value: Any) -> Any:
|
|
422
|
+
"""Resolve a value that may be awaitable.
|
|
423
|
+
|
|
424
|
+
The compiled Python entry runs synchronously, so if a provider returns a
|
|
425
|
+
coroutine we drive it to completion on a private event loop.
|
|
426
|
+
"""
|
|
427
|
+
if not inspect.isawaitable(value):
|
|
428
|
+
return value
|
|
429
|
+
|
|
430
|
+
import asyncio
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
asyncio.get_running_loop()
|
|
434
|
+
except RuntimeError:
|
|
435
|
+
return asyncio.run(value)
|
|
436
|
+
|
|
437
|
+
# Already inside a running loop: run the coroutine on a dedicated loop in a
|
|
438
|
+
# worker thread so we don't deadlock the caller's loop.
|
|
439
|
+
import concurrent.futures
|
|
440
|
+
|
|
441
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
442
|
+
return pool.submit(asyncio.run, value).result()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: managed-deepagents
|
|
3
|
+
Version: 0.1.3.dev15
|
|
4
|
+
Summary: Managed Deep Agents — the define_deep_agent authoring interface plus the CLI that compiles and deploys a code-first Deep Agent repository to a managed LangGraph runtime.
|
|
5
|
+
Author: LangChain
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/langchain-ai/managed-deepagents-sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/langchain-ai/managed-deepagents-sdk
|
|
9
|
+
Keywords: langchain,langgraph,deepagents,agents,cli
|
|
10
|
+
Classifier: Programming Language :: Rust
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
17
|
+
|
|
18
|
+
<!-- markdownlint-disable MD033 MD041 -->
|
|
19
|
+
|
|
20
|
+
<div align="center">
|
|
21
|
+
<a href="https://www.langchain.com/langsmith-managed-deep-agents-waitlist">
|
|
22
|
+
<img alt="Managed Deep Agents logo" src="https://raw.githubusercontent.com/langchain-ai/managed-deepagents-sdk/main/.github/assets/logo.png" width="70%">
|
|
23
|
+
</a>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div align="center">
|
|
27
|
+
<h3>Python authoring package and CLI launcher for Managed Deep Agents.</h3>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
> [!IMPORTANT]
|
|
31
|
+
> **Active development / private beta.** Managed Deep Agents is in active
|
|
32
|
+
> development and currently in private beta. The PyPI package API and managed
|
|
33
|
+
> runtime contract may change. [Join the
|
|
34
|
+
> waitlist](https://www.langchain.com/langsmith-managed-deep-agents-waitlist)
|
|
35
|
+
> for access and updates.
|
|
36
|
+
|
|
37
|
+
`managed-deepagents` is the PyPI package for authoring Managed Deep Agents in
|
|
38
|
+
Python. It includes:
|
|
39
|
+
|
|
40
|
+
- `define_deep_agent`, the Python authoring contract for managed agents.
|
|
41
|
+
- `mda`, the CLI used to build and deploy your agent to LangSmith.
|
|
42
|
+
- `managed_deepagents.runtime`, the runtime helper used by generated managed
|
|
43
|
+
entry modules.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install managed-deepagents
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> [!NOTE]
|
|
52
|
+
> **Private beta: dev releases only.** We currently publish only PEP 440
|
|
53
|
+
> pre-release (dev) versions and no stable version yet. pip skips pre-releases
|
|
54
|
+
> by default, so install with `--pre`:
|
|
55
|
+
>
|
|
56
|
+
> ```bash
|
|
57
|
+
> pip install --pre managed-deepagents
|
|
58
|
+
> ```
|
|
59
|
+
|
|
60
|
+
This package requires Python 3.9 or newer. Each platform wheel bundles the
|
|
61
|
+
prebuilt `mda` binary for its OS and CPU architecture and exposes it through the
|
|
62
|
+
`mda` console script.
|
|
63
|
+
|
|
64
|
+
## Define an Agent
|
|
65
|
+
|
|
66
|
+
Create an `agent.py` that defines an `agent`:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from pathlib import Path
|
|
70
|
+
|
|
71
|
+
from managed_deepagents import define_deep_agent
|
|
72
|
+
|
|
73
|
+
instructions = Path("instructions.md").read_text()
|
|
74
|
+
|
|
75
|
+
agent = define_deep_agent(
|
|
76
|
+
model="openai:gpt-5.5",
|
|
77
|
+
system_prompt=instructions,
|
|
78
|
+
tools=[query_db],
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`define_deep_agent` accepts the `create_deep_agent` keyword surface minus the
|
|
83
|
+
managed keys: `backend`, `store`, and `checkpointer`. Those are provided by the
|
|
84
|
+
managed runtime when your agent is deployed.
|
|
85
|
+
|
|
86
|
+
## Project Shape
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
my-agent/
|
|
90
|
+
agent.py
|
|
91
|
+
instructions.md
|
|
92
|
+
tools/
|
|
93
|
+
middleware/
|
|
94
|
+
skills/
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The CLI copies your project files into the managed build and generates the entry
|
|
98
|
+
module that connects your definition to the hosted runtime.
|
|
99
|
+
|
|
100
|
+
## CLI
|
|
101
|
+
|
|
102
|
+
Build locally:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
mda build ./my-agent
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Deploy to LangSmith:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
mda deploy ./my-agent
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The generated build is written to `<root>/.mda/build` by default.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
managed_deepagents/__init__.py,sha256=m6twfLrHeO14y9-x5W4HQ5C92duYG3JJpHRliyKwmks,5737
|
|
2
|
+
managed_deepagents/__main__.py,sha256=BHLi_-4Bx1gsZn-z6x7wECPn1YTxdc74y0deRea9FzY,45
|
|
3
|
+
managed_deepagents/runtime.py,sha256=GDTC0-QAa5q6YrJBlj4V3v_naerRLgNiznXWxTCRLWk,17900
|
|
4
|
+
managed_deepagents/_binaries/mda,sha256=JvwHPx6G2MHgD6CjJMOfcKJwIzBt9gU0d6CLYLxuo1E,3200208
|
|
5
|
+
managed_deepagents/_binaries/mda.exe,sha256=imV_avbhmIbcQqxptTffjlW9c9NngNQ2OAhxEHH7REc,2435072
|
|
6
|
+
managed_deepagents-0.1.3.dev15.dist-info/METADATA,sha256=s-8Szgz151_o7XYgOF56JV2N8ep3of4znBlzI5q8PjI,3272
|
|
7
|
+
managed_deepagents-0.1.3.dev15.dist-info/WHEEL,sha256=84gBEa4fGSDR6CJj9KMeLIZ46AIp14gh61YGBn-qQNM,97
|
|
8
|
+
managed_deepagents-0.1.3.dev15.dist-info/entry_points.txt,sha256=Z7JjAzLk9VYdYVvN9xdG91cZ1-pCHj3GFfSRiT0SObw,48
|
|
9
|
+
managed_deepagents-0.1.3.dev15.dist-info/top_level.txt,sha256=fdBo4O0Hlol_w022tTLB5ftZpamb4yJoQ3MUvjgTGfA,19
|
|
10
|
+
managed_deepagents-0.1.3.dev15.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
managed_deepagents
|