Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion hive/async_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ async def remember(
async with self._lock:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None, self._stack.remember, key, value,
None,
lambda: self._stack.remember(key, value, trust=trust, tags=tags),
)

async def recall(self, key: str, default: Any = None) -> Any:
Expand Down
9 changes: 9 additions & 0 deletions hive/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class HiveConfig:
rate_limit: int = 0 # 0 = disabled
default_ttl_s: float | None = None
max_memory_nodes: int = 10_000
max_content_bytes: int = 1_048_576
jwt_secret: str | None = None
otel_endpoint: str | None = None
prometheus_port: int = 0 # 0 = disabled
Expand Down Expand Up @@ -68,6 +69,12 @@ def _parse_bool(raw: str) -> bool:
else:
kwargs[attr] = raw

# K8s/Helm manifests use HIVE_MAX_NODES (not HIVE_MAX_MEMORY_NODES).
if "max_memory_nodes" not in kwargs:
alias = os.environ.get(f"{prefix}MAX_NODES")
if alias is not None:
kwargs["max_memory_nodes"] = int(alias)

return cls(**kwargs)

def validate(self) -> None:
Expand All @@ -76,6 +83,8 @@ def validate(self) -> None:
raise ValueError("rate_limit must be >= 0")
if self.max_memory_nodes < 1:
raise ValueError("max_memory_nodes must be >= 1")
if self.max_content_bytes < 1:
raise ValueError("max_content_bytes must be >= 1")

def to_dict(self) -> dict[str, Any]:
"""Return a plain dict (useful for JSON logging)."""
Expand Down
14 changes: 12 additions & 2 deletions hive/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def __init__(
validate: bool = False,
config: HiveConfig | None = None,
rate_limiter: RateLimiter | None = None,
max_content_bytes: int = 1_048_576,
max_content_bytes: int | None = None,
circuit_breaker: CircuitBreaker | None = None,
) -> None:
self.config = config or HiveConfig()
Expand All @@ -163,12 +163,22 @@ def __init__(
tenant_id=tenant_id,
tenant_isolation=self.config.tenant_isolation,
default_ttl_s=self.config.default_ttl_s,
max_nodes=self.config.max_memory_nodes,
)
self._tenant_id = tenant_id
self._validate = validate or self.config.validate_inputs
self.rate_limiter = rate_limiter
if self.rate_limiter is None and self.config.rate_limit > 0:
self.rate_limiter = RateLimiter(
default_capacity=self.config.rate_limit,
refill_rate=max(self.config.rate_limit / 60.0, 1.0),
)
self.circuit_breaker = circuit_breaker
self._max_content_bytes = max_content_bytes
self._max_content_bytes = (
max_content_bytes
if max_content_bytes is not None
else self.config.max_content_bytes
)
self.telemetry = telemetry
self.feedback = feedback_buffer
self._policy_updater = PolicyUpdater() if feedback_buffer is not None else None
Expand Down
3 changes: 2 additions & 1 deletion scripts/hive_api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from typing import Any

from hive import HiveStack
from hive.config import HiveConfig
from hive.rule_fast import RuleFastHoneyComb


Expand All @@ -39,7 +40,7 @@

if _HAS_FASTAPI:
app = FastAPI(title="Hive Agent Memory", version="0.5.0")
stack = HiveStack(honey_comb=RuleFastHoneyComb())
stack = HiveStack(honey_comb=RuleFastHoneyComb(), config=HiveConfig.from_env())

class RouteRequest(BaseModel):
goal: str = Field(default="")
Expand Down
10 changes: 10 additions & 0 deletions tests/test_async_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ async def test_async_remember_recall():
assert await stack.recall("k") == "v"


@pytest.mark.asyncio
async def test_async_remember_passes_trust_and_tags():
stack = AsyncHiveStack()
await stack.remember("k", "v", trust=0.25, tags={"audit"})
node = stack.stack.brain.get("k")
assert node is not None
assert node.trust == 0.25
assert node.tags == {"audit"}


@pytest.mark.asyncio
async def test_async_compress_many():
stack = AsyncHiveStack(honey_comb=RuleFastHoneyComb())
Expand Down
34 changes: 34 additions & 0 deletions tests/test_enterprise_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,37 @@ def test_config_to_dict():
d = cfg.to_dict()
assert d["rate_limit"] == 100
assert "validate_inputs" in d


def test_from_env_reads_max_nodes_alias():
os.environ["HIVE_MAX_NODES"] = "42"
try:
cfg = HiveConfig.from_env()
assert cfg.max_memory_nodes == 42
finally:
del os.environ["HIVE_MAX_NODES"]


def test_from_env_reads_max_content_bytes():
os.environ["HIVE_MAX_CONTENT_BYTES"] = "2048"
try:
cfg = HiveConfig.from_env()
assert cfg.max_content_bytes == 2048
finally:
del os.environ["HIVE_MAX_CONTENT_BYTES"]


def test_stack_applies_config_memory_and_content_limits():
cfg = HiveConfig(max_memory_nodes=3, max_content_bytes=64)
stack = HiveStack(honey_comb=RuleFastHoneyComb(), config=cfg)
assert stack.brain._max_nodes == 3
assert stack._max_content_bytes == 64


def test_stack_auto_rate_limiter_from_config():
cfg = HiveConfig(rate_limit=1)
stack = HiveStack(honey_comb=RuleFastHoneyComb(), config=cfg)
assert stack.rate_limiter is not None
stack.route({"goal": "first", "available_tools": []})
limited = stack.route({"goal": "second", "available_tools": []})
assert limited.source == "ratelimit"
26 changes: 26 additions & 0 deletions tests/test_hive_api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,29 @@ def test_ready_returns_200_when_stack_is_healthy():
resp = client.get("/ready")
assert resp.status_code == 200
assert resp.json()["status"] == "ready"


@pytest.mark.skipif(not _HAS_FASTAPI, reason="fastapi not installed")
def test_api_server_applies_validate_inputs_from_env(monkeypatch):
"""K8s sets HIVE_VALIDATE_INPUTS=true; the REST server must honor it."""
from hive import HiveStack
from hive.config import HiveConfig
from hive.rule_fast import RuleFastHoneyComb

monkeypatch.setenv("HIVE_VALIDATE_INPUTS", "true")
api_server.stack = HiveStack(
honey_comb=RuleFastHoneyComb(),
config=HiveConfig.from_env(),
)
client = TestClient(api_server.app, raise_server_exceptions=False)
# RouteRequest allows any goal length; AgentState caps at 4096 when validating.
resp = client.post(
"/route",
json={"goal": "x" * 5000, "step": 0, "available_tools": []},
)
assert resp.status_code == 500
monkeypatch.delenv("HIVE_VALIDATE_INPUTS", raising=False)
api_server.stack = HiveStack(
honey_comb=RuleFastHoneyComb(),
config=HiveConfig.from_env(),
)
Loading