fix(tenki): parse sticky environment values as booleans (#5849)

* fix(tenki): parse sticky environment values as booleans

* docs(tenki): keep agent guidance within chain limit

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Daoyuan Li 2026-09-28 07:22:43 -07:00 committed by GitHub
parent 8712d495e2
commit 5850b0da9f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 51 additions and 1 deletions

View File

@ -102,6 +102,10 @@ policy or network-mode mismatch; only the provider may replace it after the
orphan grace, local teardown reservation, and cross-instance teardown lease.
Destroy the sandbox, sidecar, and both networks together.
### Tenki `sticky`
Env values stay strings; parse booleans before SDK calls (`bool("false")` is true).
### E2B Mount Uploads
E2B uploads host mounts during sandbox creation using binary file objects.

View File

@ -25,6 +25,12 @@ sandbox:
PYTHONUNBUFFERED: "1"
```
`sticky` also accepts an environment reference such as `sticky: $TENKI_STICKY`.
Boolean strings are case-insensitive: `false`, `0`, `off`, and `no` disable host
pinning; `true`, `1`, `on`, and `yes` enable it. Invalid boolean values are
rejected when the provider loads its configuration. Omitted or `null` values
keep pinning disabled.
Install the optional SDK before selecting this provider:
```bash

View File

@ -26,6 +26,8 @@ import uuid
from functools import partial
from typing import TYPE_CHECKING, Any
from pydantic import TypeAdapter
from deerflow.config import get_app_config
from deerflow.sandbox.acquire_serialization import AcquireSerializer
from deerflow.sandbox.identity import derive_sandbox_scope_token
@ -141,6 +143,7 @@ class TenkiSandboxProvider(WarmPoolLifecycleMixin[TenkiSandbox], SandboxProvider
replicas = _opt("replicas")
idle_timeout = _opt("idle_timeout")
max_duration = _opt("max_duration")
sticky = _opt("sticky", False)
environment = dict(_opt("environment") or {})
# Fail fast on a misconfigured key (e.g. "bad-key"): the per-call env goes
# through the same POSIX-name check in execute_command, but this static
@ -159,7 +162,7 @@ class TenkiSandboxProvider(WarmPoolLifecycleMixin[TenkiSandbox], SandboxProvider
# Off by default (the SDK default). Warm-pool sandboxes stay running
# between turns, so host pinning only matters to deployments that
# also pause/resume; expose it rather than decide for them.
"sticky": bool(_opt("sticky", False)),
"sticky": TypeAdapter(bool).validate_python(sticky) if sticky is not None else False,
"api_key": api_key, # None → SDK falls back to TENKI_API_KEY / TENKI_AUTH_TOKEN
"base_url": _opt("base_url"),
"image": _opt("image"), # None → Tenki account default base image

View File

@ -25,6 +25,7 @@ import pytest
from deerflow.community.tenki.provider import _BOOTSTRAP_TIMEOUT, TenkiSandboxProvider, _import_client
from deerflow.community.tenki.sandbox import TenkiSandbox
from deerflow.config.app_config import AppConfig
# ── Fake Tenki SDK ────────────────────────────────────────────────────
@ -755,6 +756,42 @@ def test_create_waits_client_side_and_configures_lifetime(monkeypatch):
provider.shutdown()
@pytest.mark.parametrize(
("value", "expected"),
[("false", False), ("0", False), ("off", False), ("no", False), ("FALSE", False), ("true", True), ("1", True), ("on", True), ("yes", True)],
)
def test_create_sticky_from_environment(monkeypatch, tmp_path, value, expected):
monkeypatch.setenv("TEST_TENKI_STICKY", value)
config_path = tmp_path / "config.yaml"
config_path.write_text("models: []\nsandbox:\n use: deerflow.community.tenki:TenkiSandboxProvider\n sticky: $TEST_TENKI_STICKY\n idle_timeout: 0\n", encoding="utf-8")
config = AppConfig.from_file(config_path)
client = _FakeClient()
monkeypatch.setattr("deerflow.community.tenki.provider.get_app_config", lambda: config)
monkeypatch.setattr("deerflow.community.tenki.provider._import_client", lambda: lambda **kw: client)
provider = TenkiSandboxProvider()
try:
provider.acquire("thread-1", user_id="u1")
assert client.create_kwargs[0]["sticky"] is expected
finally:
provider.shutdown()
@pytest.mark.parametrize("value", [False, True, None])
def test_create_sticky_native_value(monkeypatch, value):
client = _FakeClient()
provider = _install(monkeypatch, client=client, config_attrs={"sticky": value, "idle_timeout": 0})
try:
provider.acquire("thread-1", user_id="u1")
assert client.create_kwargs[0]["sticky"] is bool(value)
finally:
provider.shutdown()
def test_sticky_rejects_invalid_boolean(monkeypatch):
with pytest.raises(ValueError, match="valid boolean"):
_install(monkeypatch, config_attrs={"sticky": "not-a-boolean"})
def test_create_default_lifetime_is_explicit(monkeypatch):
client = _FakeClient()
provider = _install(monkeypatch, client=client)