Compare commits

...

4 commits

Author SHA1 Message Date
Adam Moussa
b7623205c6
Merge b4137b63eb into cfb5624663 2026-07-17 12:02:33 -04:00
Adam Moussa
cfb5624663
feat(open-swe): add E2B sandbox provider (port of upstream 48217b68, #1489) (#199)
Some checks are pending
CI / Lint (push) Waiting to run
CI / Format check (push) Waiting to run
CI / Typecheck (push) Waiting to run
CI / Unit tests (push) Waiting to run
CI / Playwright E2E (push) Waiting to run
CI / Docker build smoke (push) Waiting to run
CI / Triage ledger up to date (push) Waiting to run
CI / ui bun.lock in sync (push) Waiting to run
Additive sandbox provider adapted to this fork's synchronous create_sandbox
factory: agent/integrations/e2b.py registered as a lazy-import entry in
SANDBOX_FACTORIES, so the e2b/langchain-e2b imports only load when
SANDBOX_TYPE=e2b (dark-safe on unset env; unset E2B_API_KEY raises a clean
ValueError only when the provider is selected). Supports reconnect-by-id and
optional E2B_TEMPLATE. Non-langsmith providers skip the GitHub proxy step,
so the GitHub-App token flow is untouched.

Upstream: langchain-ai/open-swe 48217b68 (#1489), re-implemented against the
fork's sync sandbox lifecycle rather than cherry-picked (upstream ships on
the deferred async sandbox.py base).

Docs: provider tables/lists in README, CUSTOMIZATION, INSTALLATION; the
CUSTOMIZATION registration example now shows the lazy-tuple form the code
actually uses. uv.lock refreshed (adds e2b, langchain-e2b, dockerfile-parse).
2026-07-16 18:23:42 -04:00
Adam Moussa
22c517a54f
fix: stale admin model defaults after model upgrades (#1709) (#200)
* fix: migrate stale admin model defaults

Normalize retired model IDs before validation and in settings responses so full admin updates remain saveable after model upgrades.

* fix: restrict retired model migration

Only migrate explicitly retired model IDs so malformed provider model names and efforts continue to fail validation.

(cherry picked from commit 62e0ca2d4898ebc3c2ae8887030a364779d907cb)

Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
2026-07-16 18:03:44 -04:00
Adam Moussa
7b9eff62e9
fix: enforce terse Slack tool messages (#1717) (#197)
(cherry picked from commit 092abafa4cb955c3823f727d35d7bad94e1147ab)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
2026-07-16 17:51:17 -04:00
14 changed files with 392 additions and 16 deletions

View file

@ -63,6 +63,7 @@ Set the `SANDBOX_TYPE` environment variable to switch providers. Each provider h
| `langsmith` (default) | `agent/integrations/langsmith.py` | `LANGSMITH_API_KEY_PROD`, `SANDBOX_TYPE="langsmith"` |
| `daytona` | `agent/integrations/daytona.py` | `DAYTONA_API_KEY`, `SANDBOX_TYPE="daytona"`, optional `DAYTONA_SANDBOX_SNAPSHOT` |
| `runloop` | `agent/integrations/runloop.py` | `RUNLOOP_API_KEY`, `SANDBOX_TYPE="runloop"` |
| `e2b` | `agent/integrations/e2b.py` | `E2B_API_KEY`, `SANDBOX_TYPE="e2b"`, optional `E2B_TEMPLATE` |
| `modal` | `agent/integrations/modal.py` | Modal credentials, `SANDBOX_TYPE="modal"` |
| `local` | `agent/integrations/local.py` | None (no isolation — development only), `SANDBOX_TYPE="local"` |
@ -86,14 +87,12 @@ def create_my_provider_sandbox(sandbox_id: str | None = None):
...
```
2. **Register it** in `agent/utils/sandbox.py` by importing your factory and adding it to `SANDBOX_FACTORIES`:
2. **Register it** in `agent/utils/sandbox.py` by adding it to `SANDBOX_FACTORIES`:
```python
from agent.integrations.my_provider import create_my_provider_sandbox
SANDBOX_FACTORIES = {
...
"my_provider": create_my_provider_sandbox,
"my_provider": ("agent.integrations.my_provider", "create_my_provider_sandbox"),
}
```

View file

@ -599,7 +599,7 @@ REVIEWER_OUTCOMES_DATASET=""
PUBLIC_REPO_ORG_GATE=""
# === Sandbox (optional) ===
# Provider: langsmith (default), modal, daytona, runloop, or local. See CUSTOMIZATION.md.
# Provider: langsmith (default), modal, daytona, runloop, e2b, or local. See CUSTOMIZATION.md.
SANDBOX_TYPE="langsmith"
DEFAULT_SANDBOX_SNAPSHOT_ID="" # Required when SANDBOX_TYPE=langsmith (see step 4c)
DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES="" # Root FS size in bytes (default: 32 GiB)

View file

@ -54,7 +54,7 @@ create_deep_agent(
Every task runs in its own **isolated cloud sandbox** — a remote Linux environment with full shell access. The repo is cloned in, the agent gets full permissions, and the blast radius of any mistake is fully contained. No production access, no confirmation prompts.
Open SWE supports multiple sandbox providers out of the box — [Modal](https://modal.com/), [Daytona](https://www.daytona.io/), [Runloop](https://www.runloop.ai/), and [LangSmith](https://smith.langchain.com/) — and you can plug in your own. See the [Customization Guide](CUSTOMIZATION.md#1-sandbox) for details.
Open SWE supports multiple sandbox providers out of the box — [Modal](https://modal.com/), [Daytona](https://www.daytona.io/), [Runloop](https://www.runloop.ai/), [E2B](https://e2b.dev/), and [LangSmith](https://smith.langchain.com/) — and you can plug in your own. See the [Customization Guide](CUSTOMIZATION.md#1-sandbox) for details.
This follows the principle all three companies converge on: **isolate first, then give full permissions inside the boundary.**

View file

@ -110,6 +110,39 @@ class TeamSettingsUpdate(BaseModel):
@model_validator(mode="after")
def _validate_model_pairs(self) -> TeamSettingsUpdate:
self.default_agent_model, self.default_agent_reasoning_effort = _normalize_stale_model_pair(
self.default_agent_model,
self.default_agent_reasoning_effort,
)
self.default_agent_subagent_model, self.default_agent_subagent_reasoning_effort = (
_normalize_stale_model_pair(
self.default_agent_subagent_model,
self.default_agent_subagent_reasoning_effort,
)
)
self.default_reviewer_model, self.default_reviewer_reasoning_effort = (
_normalize_stale_model_pair(
self.default_reviewer_model,
self.default_reviewer_reasoning_effort,
)
)
(
self.default_reviewer_subagent_model,
self.default_reviewer_subagent_reasoning_effort,
) = _normalize_stale_model_pair(
self.default_reviewer_subagent_model,
self.default_reviewer_subagent_reasoning_effort,
)
self.default_grouping_model, self.default_grouping_reasoning_effort = (
_normalize_stale_model_pair(
self.default_grouping_model,
self.default_grouping_reasoning_effort,
)
)
self.default_chat_model, self.default_chat_reasoning_effort = _normalize_stale_model_pair(
self.default_chat_model,
self.default_chat_reasoning_effort,
)
_validate_model_effort_pair(
self.default_agent_model, self.default_agent_reasoning_effort, "agent"
)
@ -168,6 +201,53 @@ def _validate_model_effort_pair(model: str | None, effort: str | None, role: str
raise ValueError(f"effort {effort!r} not supported by {role} model {model!r}")
# Model ids retired from SUPPORTED_MODELS mapped to their current successors,
# so stored admin defaults stay saveable/readable across model upgrades. Direct
# anthropic: ids moved to the Bedrock inference profile in the provider
# migration (#62); OpenAI/Google were dropped entirely and take the global
# default. anthropic:claude-fable-5 deliberately maps to Opus, not Bedrock
# Fable: Fable is gated behind the provider-data-share opt-in and a stale
# record must never resurface it.
_RETIRED_MODEL_REPLACEMENTS: dict[str, str] = {
"anthropic:claude-opus-4-7": "bedrock_converse:us.anthropic.claude-opus-4-8",
"anthropic:claude-opus-4-8": "bedrock_converse:us.anthropic.claude-opus-4-8",
"anthropic:claude-fable-5": "bedrock_converse:us.anthropic.claude-opus-4-8",
"openai:gpt-5.5": "bedrock_converse:us.anthropic.claude-opus-4-8",
"google_genai:gemini-3.5-flash": "bedrock_converse:us.anthropic.claude-opus-4-8",
}
def _normalize_stale_model_pair(
model: str | None, effort: str | None
) -> tuple[str | None, str | None]:
if model is None:
return model, effort
return _RETIRED_MODEL_REPLACEMENTS.get(model, model), effort
_MODEL_PAIR_FIELDS: tuple[tuple[str, str], ...] = (
("default_agent_model", "default_agent_reasoning_effort"),
("default_agent_subagent_model", "default_agent_subagent_reasoning_effort"),
("default_reviewer_model", "default_reviewer_reasoning_effort"),
("default_reviewer_subagent_model", "default_reviewer_subagent_reasoning_effort"),
("default_grouping_model", "default_grouping_reasoning_effort"),
("default_chat_model", "default_chat_reasoning_effort"),
)
def normalize_team_settings_for_response(settings: dict[str, Any]) -> dict[str, Any]:
value = dict(settings)
for model_field, effort_field in _MODEL_PAIR_FIELDS:
model = value.get(model_field)
effort = value.get(effort_field)
if isinstance(model, str):
value[model_field], value[effort_field] = _normalize_stale_model_pair(
model,
effort if isinstance(effort, str) else None,
)
return value
def _client():
return get_client()
@ -241,7 +321,7 @@ async def get_team_settings() -> dict[str, Any]:
"review_author_context_enabled",
):
merged.pop(stale_field, None)
return merged
return normalize_team_settings_for_response(merged)
async def upsert_team_settings(update: TeamSettingsUpdate) -> dict[str, Any]:

46
agent/integrations/e2b.py Normal file
View file

@ -0,0 +1,46 @@
import os
from deepagents.backends.protocol import SandboxBackendProtocol
from e2b import Sandbox
from langchain_e2b import E2BSandbox
DEFAULT_E2B_SANDBOX_TIMEOUT = 60 * 60
E2B_TEMPLATE_ENV = "E2B_TEMPLATE"
def create_e2b_sandbox(sandbox_id: str | None = None) -> SandboxBackendProtocol:
"""Create or reconnect to an E2B sandbox.
Args:
sandbox_id: Optional existing sandbox ID to reconnect to.
If None, creates a new sandbox.
Returns:
E2BSandbox instance implementing SandboxBackendProtocol.
"""
api_key = os.getenv("E2B_API_KEY")
if not api_key:
raise ValueError("E2B_API_KEY environment variable is required")
template = os.getenv(E2B_TEMPLATE_ENV)
if template is not None:
template = template.strip()
if not template:
raise ValueError(f"{E2B_TEMPLATE_ENV} must not be empty")
if sandbox_id:
sandbox = Sandbox.connect(
sandbox_id,
timeout=DEFAULT_E2B_SANDBOX_TIMEOUT,
api_key=api_key,
)
elif template:
sandbox = Sandbox.create(
template=template,
timeout=DEFAULT_E2B_SANDBOX_TIMEOUT,
api_key=api_key,
)
else:
sandbox = Sandbox.create(timeout=DEFAULT_E2B_SANDBOX_TIMEOUT, api_key=api_key)
return E2BSandbox(sandbox=sandbox)

View file

@ -84,7 +84,7 @@ OPEN_SWE_SHARED_BASE = """You are **Open SWE**, an open-source agent built on La
### Communication
- Focus on the substance and keep summaries brief. Use light markdown (`###`/`####` headings, bold, code) — avoid `#`/`##` titles.
- In Slack, keep every reply terse — a few sentences at most. Lead with the answer or outcome; skip preamble, restating the request, and step-by-step recaps. Do not paste long output, diffs, file listings, or multi-section write-ups into a Slack reply. When the response would genuinely be long — a detailed report, a design/analysis write-up, a large code or log excerpt — write that content to a Markdown file under `/workspace/plans/` and publish it with the `save_plan` tool, then post a terse Slack reply with a one-line summary and the plan-review link so the user can read the full version there. This non-plan share path does not enter plan mode; use it instead of splitting a long answer across multiple Slack messages.
- Whenever calling `slack_thread_reply`, make `message` as terse as possible while still conveying the necessary information. Default to one sentence containing only the outcome/status and link, or one blocking question. Omit greetings, preambles, headings, recaps, implementation details, and redundant context; use bullets only when multiple items are essential. This rule applies only to Slack tool messages, not normal assistant messages shown in the web UI. Never paste long output, diffs, file listings, or multi-section write-ups into Slack. When detail is necessary, write it to a Markdown file under `/workspace/plans/`, publish it with `save_plan`, and send only a one-line summary plus the plan-review link. This non-plan share path does not enter plan mode.
- In Slack, when a user asks to “break out,” “split out,” or “start a separate thread” for part of the work, summarize the requested aspect and relevant context into self-contained instructions, then call `slack_start_new_thread` instead of only replying in the current thread.
- In Slack, when acknowledging a user follow-up while you continue working, prefer `slack_add_reaction` with the default `eyes` reaction over posting a perfunctory “Updating…” / “I’ll check…” confirmation reply.
- For Slack-triggered information-only answers, post only a concise summary in the associated Slack thread with `slack_thread_reply`, then provide the complete answer inline in your final assistant response. For other Slack updates, keep thread replies brief and avoid duplicating the same text later.

View file

@ -32,11 +32,13 @@ async def slack_thread_reply(
) -> dict[str, Any]:
"""Post a message to the current Slack thread.
Use this for clarifying questions, mid-run progress updates, and the final
summary. You can call this multiple times during a run — if you're about to
do long-running work (cloning, large refactors, big test runs) consider
posting a brief status update first so the user knows what's happening.
Always end the run with a final reply summarizing what you did.
Use this for clarifying questions, essential progress updates, and the final
outcome. Make `message` as terse as possible: default to one sentence with
only the outcome/status and link, or one blocking question. Omit greetings,
preambles, headings, recaps, implementation details, and redundant context;
use bullets only when multiple items are essential. This terseness rule is
specific to Slack tool messages, not normal web UI assistant messages.
Always end the run with a terse final outcome.
Format messages using Slack's mrkdwn format, NOT standard Markdown.
Key differences: *bold*, _italic_, ~strikethrough~, <url|link text>,

View file

@ -10,6 +10,7 @@ SANDBOX_FACTORIES: dict[str, tuple[str, str]] = {
"daytona": ("agent.integrations.daytona", "create_daytona_sandbox"),
"modal": ("agent.integrations.modal", "create_modal_sandbox"),
"runloop": ("agent.integrations.runloop", "create_runloop_sandbox"),
"e2b": ("agent.integrations.e2b", "create_e2b_sandbox"),
"local": ("agent.integrations.local", "create_local_sandbox"),
}
@ -34,7 +35,7 @@ def create_sandbox(
"""Create or reconnect to a sandbox using the configured provider.
The provider is selected via the SANDBOX_TYPE environment variable.
Supported values: langsmith (default), daytona, modal, runloop, local.
Supported values: langsmith (default), daytona, modal, runloop, e2b, local.
Args:
sandbox_id: Optional existing sandbox ID to reconnect to.

View file

@ -27,6 +27,7 @@ dependencies = [
"langchain-daytona>=0.0.7",
"langchain-modal>=0.0.5",
"langchain-runloop>=0.0.6",
"langchain-e2b==0.0.4",
"exa-py>=2.16.0",
"langchain-google-genai>=4.2.7",
"langchain-mcp-adapters>=0.3.0",

View file

@ -0,0 +1,111 @@
import importlib.util
import sys
import types
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
class _FakeSandbox:
create_calls: list[dict[str, object]] = []
connect_calls: list[tuple[str, dict[str, object]]] = []
def __init__(self, sandbox_id: str):
self.sandbox_id = sandbox_id
@classmethod
def create(cls, **kwargs):
cls.create_calls.append(kwargs)
return cls("created-sandbox")
@classmethod
def connect(cls, sandbox_id: str, **kwargs):
cls.connect_calls.append((sandbox_id, kwargs))
return cls(sandbox_id)
class _FakeE2BSandbox:
def __init__(self, *, sandbox):
self.sandbox = sandbox
def _load_e2b_module(monkeypatch):
_FakeSandbox.create_calls = []
_FakeSandbox.connect_calls = []
fake_e2b = types.ModuleType("e2b")
fake_e2b.Sandbox = _FakeSandbox
fake_langchain_e2b = types.ModuleType("langchain_e2b")
fake_langchain_e2b.E2BSandbox = _FakeE2BSandbox
monkeypatch.setitem(sys.modules, "e2b", fake_e2b)
monkeypatch.setitem(sys.modules, "langchain_e2b", fake_langchain_e2b)
module_path = ROOT / "agent" / "integrations" / "e2b.py"
spec = importlib.util.spec_from_file_location("e2b_under_test", module_path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_create_e2b_sandbox_uses_default_timeout(monkeypatch):
monkeypatch.setenv("E2B_API_KEY", "api-key")
monkeypatch.delenv("E2B_TEMPLATE", raising=False)
module = _load_e2b_module(monkeypatch)
backend = module.create_e2b_sandbox()
assert backend.sandbox.sandbox_id == "created-sandbox"
assert _FakeSandbox.create_calls == [{"timeout": 3600, "api_key": "api-key"}]
assert _FakeSandbox.connect_calls == []
def test_create_e2b_sandbox_uses_template(monkeypatch):
monkeypatch.setenv("E2B_API_KEY", "api-key")
monkeypatch.setenv("E2B_TEMPLATE", "open-swe-template")
module = _load_e2b_module(monkeypatch)
module.create_e2b_sandbox()
assert _FakeSandbox.create_calls == [
{"template": "open-swe-template", "timeout": 3600, "api_key": "api-key"}
]
def test_create_e2b_sandbox_reconnects_by_id(monkeypatch):
monkeypatch.setenv("E2B_API_KEY", "api-key")
module = _load_e2b_module(monkeypatch)
backend = module.create_e2b_sandbox("sbx_existing")
assert backend.sandbox.sandbox_id == "sbx_existing"
assert _FakeSandbox.connect_calls == [("sbx_existing", {"timeout": 3600, "api_key": "api-key"})]
assert _FakeSandbox.create_calls == []
def test_e2b_missing_api_key(monkeypatch):
monkeypatch.delenv("E2B_API_KEY", raising=False)
monkeypatch.delenv("E2B_TEMPLATE", raising=False)
module = _load_e2b_module(monkeypatch)
try:
module.create_e2b_sandbox()
except ValueError as exc:
assert "E2B_API_KEY" in str(exc)
else:
raise AssertionError("expected missing E2B_API_KEY to fail")
def test_e2b_rejects_empty_template(monkeypatch):
monkeypatch.setenv("E2B_API_KEY", "api-key")
monkeypatch.setenv("E2B_TEMPLATE", " ")
module = _load_e2b_module(monkeypatch)
try:
module.create_e2b_sandbox()
except ValueError as exc:
assert "E2B_TEMPLATE must not be empty" in str(exc)
else:
raise AssertionError("expected empty E2B_TEMPLATE to fail")

View file

@ -88,6 +88,20 @@ def test_construct_system_prompt_identifies_own_repo() -> None:
assert "Open SWE" in OPEN_SWE_SHARED_BASE
def test_shared_base_requires_terse_slack_replies_with_share_path() -> None:
from agent.prompt import OPEN_SWE_SHARED_BASE
assert "calling `slack_thread_reply`" in OPEN_SWE_SHARED_BASE
assert "as terse as possible" in OPEN_SWE_SHARED_BASE
assert "Default to one sentence" in OPEN_SWE_SHARED_BASE
assert "applies only to Slack tool messages" in OPEN_SWE_SHARED_BASE
assert "not normal assistant messages shown in the web UI" in OPEN_SWE_SHARED_BASE
assert "Never paste long output" in OPEN_SWE_SHARED_BASE
assert "`save_plan`" in OPEN_SWE_SHARED_BASE
assert "plan-review link" in OPEN_SWE_SHARED_BASE
assert "does not enter plan mode" in OPEN_SWE_SHARED_BASE
def test_harness_profile_replaces_deepagents_base_for_supported_providers() -> None:
"""The Open SWE base prompt is registered per provider and replaces the SDK base."""
import deepagents.profiles.harness.harness_profiles as hp

View file

@ -11,12 +11,18 @@ from agent.dashboard.options import (
gate_fable_model,
provider_fallback_pair,
)
from agent.dashboard.team_settings import get_team_default_model
from agent.dashboard.team_settings import (
TeamSettingsUpdate,
get_team_default_model,
normalize_team_settings_for_response,
)
STALE_ANTHROPIC = "bedrock_converse:us.anthropic.claude-opus-4-7"
SUPPORTED_ANTHROPIC = "bedrock_converse:us.anthropic.claude-opus-4-8"
STALE_SONNET = "bedrock_converse:us.anthropic.claude-sonnet-4-9"
SUPPORTED_SONNET = "bedrock_converse:us.anthropic.claude-sonnet-5"
RETIRED_DIRECT_ANTHROPIC = "anthropic:claude-opus-4-8"
RETIRED_FABLE = "anthropic:claude-fable-5"
def test_provider_fallback_preserves_provider_and_effort() -> None:
@ -79,6 +85,67 @@ def test_profile_stale_anthropic_upgrades_to_supported() -> None:
assert normalize_profile_overrides(profile) == (SUPPORTED_ANTHROPIC, "high")
def test_team_settings_update_normalizes_retired_models() -> None:
update = TeamSettingsUpdate(
default_agent_model=SUPPORTED_ANTHROPIC,
default_agent_reasoning_effort="medium",
default_agent_subagent_model=RETIRED_DIRECT_ANTHROPIC,
default_agent_subagent_reasoning_effort="medium",
default_reviewer_model=RETIRED_DIRECT_ANTHROPIC,
default_reviewer_reasoning_effort="medium",
default_reviewer_subagent_model=RETIRED_DIRECT_ANTHROPIC,
default_reviewer_subagent_reasoning_effort="low",
)
assert update.default_agent_subagent_model == SUPPORTED_ANTHROPIC
assert update.default_reviewer_model == SUPPORTED_ANTHROPIC
assert update.default_reviewer_subagent_model == SUPPORTED_ANTHROPIC
def test_team_settings_update_maps_retired_fable_to_opus_not_fable() -> None:
update = TeamSettingsUpdate(
fable_enabled=True,
default_agent_model=RETIRED_FABLE,
default_agent_reasoning_effort="high",
)
assert update.default_agent_model == SUPPORTED_ANTHROPIC
assert update.default_agent_model not in FABLE_MODEL_IDS
def test_team_settings_update_rejects_unknown_model() -> None:
with pytest.raises(ValueError, match="unsupported agent model"):
TeamSettingsUpdate(
default_agent_model="openai:gpt-6",
default_agent_reasoning_effort="medium",
)
def test_team_settings_update_rejects_invalid_effort_for_retired_model() -> None:
with pytest.raises(ValueError, match="effort 'bogus' not supported"):
TeamSettingsUpdate(
default_agent_model=RETIRED_DIRECT_ANTHROPIC,
default_agent_reasoning_effort="bogus",
)
def test_team_settings_response_normalizes_retired_models() -> None:
settings = normalize_team_settings_for_response(
{
"default_agent_subagent_model": RETIRED_DIRECT_ANTHROPIC,
"default_agent_subagent_reasoning_effort": "medium",
"default_reviewer_model": "openai:gpt-5.5",
"default_reviewer_reasoning_effort": "medium",
"default_reviewer_subagent_model": RETIRED_FABLE,
"default_reviewer_subagent_reasoning_effort": "low",
}
)
assert settings["default_agent_subagent_model"] == SUPPORTED_ANTHROPIC
assert settings["default_reviewer_model"] == SUPPORTED_ANTHROPIC
assert settings["default_reviewer_subagent_model"] == SUPPORTED_ANTHROPIC
def test_profile_without_model_defers_to_team_default() -> None:
assert normalize_profile_overrides({"reasoning_effort": "high"}) == (None, None)

View file

@ -19,6 +19,15 @@ def _config() -> dict[str, Any]:
}
def test_slack_thread_reply_prompt_requires_slack_only_terseness() -> None:
prompt = slack_reply_tool.slack_thread_reply.__doc__ or ""
assert "as terse as possible" in prompt
assert "default to one sentence" in prompt
assert "specific to Slack tool messages" in prompt
assert "not normal web UI assistant messages" in prompt
async def test_slack_thread_reply_returns_structured_error_for_msg_too_long(
monkeypatch: pytest.MonkeyPatch,
) -> None:

48
uv.lock generated
View file

@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.11"
resolution-markers = [
"python_full_version >= '3.14'",
@ -727,6 +727,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
name = "dockerfile-parse"
version = "2.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/92/df/929ee0b5d2c8bd8d713c45e71b94ab57c7e11e322130724d54f469b2cd48/dockerfile-parse-2.0.1.tar.gz", hash = "sha256:3184ccdc513221983e503ac00e1aa504a2aa8f84e5de673c46b0b6eee99ec7bc", size = 24556, upload-time = "2023-07-18T13:36:07.897Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/6c/79cd5bc1b880d8c1a9a5550aa8dacd57353fa3bb2457227e1fb47383eb49/dockerfile_parse-2.0.1-py2.py3-none-any.whl", hash = "sha256:bdffd126d2eb26acf1066acb54cb2e336682e1d72b974a40894fac76a4df17f6", size = 14845, upload-time = "2023-07-18T13:36:06.052Z" },
]
[[package]]
name = "docstring-parser"
version = "0.18.0"
@ -736,6 +745,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
]
[[package]]
name = "e2b"
version = "2.33.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "dockerfile-parse" },
{ name = "h2" },
{ name = "httpcore" },
{ name = "httpx" },
{ name = "packaging" },
{ name = "protobuf" },
{ name = "python-dateutil" },
{ name = "rich" },
{ name = "typing-extensions" },
{ name = "wcmatch" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/76/93a08f6ed33e53c030ed931916c9e69eb81cd3b84c500649c7cf4cefd86d/e2b-2.33.0.tar.gz", hash = "sha256:0a14fa7f99fc09b540db0c191b2ce5b1fd4cc3ed837dbd64385add99305776a3", size = 185400, upload-time = "2026-07-16T09:24:45.284Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/21/2c7bdcd1c6b188b8ae6ee57e542b1e80436cc6692e955abcf8b9355ea9bf/e2b-2.33.0-py3-none-any.whl", hash = "sha256:c10cb697b713d418f5d70f5c9c707e923630a491bd30009c10c4daeb4e21fe51", size = 337079, upload-time = "2026-07-16T09:24:43.972Z" },
]
[[package]]
name = "exa-py"
version = "2.16.0"
@ -1506,6 +1537,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/84/00/7a203f464e5ce08390129c51b60598a23b4aba5fde4b02ed7521da284368/langchain_daytona-0.0.7-py3-none-any.whl", hash = "sha256:3374c99afc51b290539dcc127f4d4a61deb986e06c2d3a8cdc342eb868246ddb", size = 5456, upload-time = "2026-06-03T20:25:58.859Z" },
]
[[package]]
name = "langchain-e2b"
version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deepagents" },
{ name = "e2b" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3c/1a/ab8db1288a70b25bfb2489327fae0d3a82ea07231d446083a2dcb0bfc174/langchain_e2b-0.0.4.tar.gz", hash = "sha256:bf543ad7394b83c3f097c9914adfc59bb3af3af9837dea9ada5064abcf4666d2", size = 207862, upload-time = "2026-06-17T21:25:25.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/6f/4e2263f5372019bd05fc8c7fe50674b041cc6b22df3faf3037897587413f/langchain_e2b-0.0.4-py3-none-any.whl", hash = "sha256:6cf20f155ebf6b1a8f36d69dec4b5925c242dfcf779209c94ccb18fda3296419", size = 7482, upload-time = "2026-06-17T21:25:24.025Z" },
]
[[package]]
name = "langchain-fireworks"
version = "1.4.4"
@ -2192,6 +2236,7 @@ dependencies = [
{ name = "langchain-anthropic" },
{ name = "langchain-aws" },
{ name = "langchain-daytona" },
{ name = "langchain-e2b" },
{ name = "langchain-fireworks" },
{ name = "langchain-google-genai" },
{ name = "langchain-mcp-adapters" },
@ -2227,6 +2272,7 @@ requires-dist = [
{ name = "langchain-anthropic", specifier = ">=1.4.6" },
{ name = "langchain-aws", specifier = ">=1.6.2" },
{ name = "langchain-daytona", specifier = ">=0.0.7" },
{ name = "langchain-e2b", specifier = "==0.0.4" },
{ name = "langchain-fireworks", specifier = ">=1.4.4" },
{ name = "langchain-google-genai", specifier = ">=4.2.7" },
{ name = "langchain-mcp-adapters", specifier = ">=0.3.0" },