mirror of
https://github.com/hanzoai/agent.git
synced 2026-09-11 21:08:51 +00:00
Closes the Hanzo-side LP-201 Tier 1 control-plane migration step. ZapClient
+ ZapAgent + consensus primitives already shipped in src/agents/zap/; this
landing wires the AgentNetwork-side cross-process lookup path.
New: agents/network/remote.py
- RemoteAgentProxy: value-shaped facade quacking like Agent on the routing
side (name + handoff_description) and dispatching invocations through
ZapClient.call_tool(`agent/<name>`, …) on the wire side.
- is_zap_wire_enabled(): single source of truth for the feature flag —
one function, one place (Hickey-style profile gate). Reads HANZO_AGENT_WIRE
case-insensitively; only the literal "zap" enables. Anything else
fail-closed (json, "true", "1", unset → JSON/local-agent path).
AgentNetwork.add_remote_agent(name, zap_uri, …): cross-process counterpart
of add_agent. Holds a NetworkNode pointing at a RemoteAgentProxy; the router
reads it like a local agent (same capabilities/metadata surface). At
dispatch the proxy's invoke() pushes over the ZAP wire — and raises with a
clear message if the operator forgot to set the env var.
Default path (no env) keeps the JSON/OpenAI-compatible wire — external API
consumers see no change. ZAP is intra-cluster only.
Tests: 7 unit tests covering both feature-flag combinations + the clone()
mirror + the fail-loud guard, all green via --noconftest (existing conftest
pulls heavy deps; tests load remote.py directly via importlib, mirroring
test_zap.py).
109 lines
3.9 KiB
Python
109 lines
3.9 KiB
Python
"""Tests for the cross-process ZAP agent proxy (network/remote.py).
|
|
|
|
Loads `remote.py` directly via importlib to avoid the heavyweight
|
|
`agents.__init__` import chain (numpy / griffe transitive deps), mirroring
|
|
the pattern used in `test_zap.py`.
|
|
"""
|
|
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
|
|
|
|
def _load_module(name: str, path: str):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
mod = importlib.util.module_from_spec(spec) # type: ignore
|
|
sys.modules[name] = mod
|
|
spec.loader.exec_module(mod) # type: ignore
|
|
return mod
|
|
|
|
|
|
_here = os.path.dirname(os.path.abspath(__file__))
|
|
_base = os.path.join(_here, "..", "src", "agents", "network")
|
|
_remote = _load_module("agents.network.remote", os.path.join(_base, "remote.py"))
|
|
|
|
|
|
RemoteAgentProxy = _remote.RemoteAgentProxy
|
|
is_zap_wire_enabled = _remote.is_zap_wire_enabled
|
|
|
|
|
|
def test_feature_flag_default_disabled(monkeypatch):
|
|
"""Default (no env) keeps the JSON/local-agent path — OpenAI-compatible."""
|
|
monkeypatch.delenv("HANZO_AGENT_WIRE", raising=False)
|
|
assert is_zap_wire_enabled() is False
|
|
|
|
|
|
def test_feature_flag_enabled_with_zap(monkeypatch):
|
|
"""`HANZO_AGENT_WIRE=zap` enables the cross-process wire."""
|
|
monkeypatch.setenv("HANZO_AGENT_WIRE", "zap")
|
|
assert is_zap_wire_enabled() is True
|
|
|
|
|
|
def test_feature_flag_case_insensitive(monkeypatch):
|
|
"""Uppercase / mixed-case values still flip the flag (operator friendliness)."""
|
|
monkeypatch.setenv("HANZO_AGENT_WIRE", "ZAP")
|
|
assert is_zap_wire_enabled() is True
|
|
monkeypatch.setenv("HANZO_AGENT_WIRE", "Zap")
|
|
assert is_zap_wire_enabled() is True
|
|
|
|
|
|
def test_feature_flag_other_values_disabled(monkeypatch):
|
|
"""Anything other than literal 'zap' keeps the flag off — fail-closed default."""
|
|
monkeypatch.setenv("HANZO_AGENT_WIRE", "json")
|
|
assert is_zap_wire_enabled() is False
|
|
monkeypatch.setenv("HANZO_AGENT_WIRE", "1")
|
|
assert is_zap_wire_enabled() is False
|
|
monkeypatch.setenv("HANZO_AGENT_WIRE", "true")
|
|
assert is_zap_wire_enabled() is False
|
|
|
|
|
|
def test_proxy_construction_defaults():
|
|
"""Defaults: empty capabilities/metadata/handoffs/tools, no instructions."""
|
|
p = RemoteAgentProxy(name="peer1", zap_uri="zap://peer-1.cluster:9999")
|
|
assert p.name == "peer1"
|
|
assert p.zap_uri == "zap://peer-1.cluster:9999"
|
|
assert p.handoff_description is None
|
|
assert p.capabilities == []
|
|
assert p.metadata == {}
|
|
assert p.handoffs == []
|
|
assert p.tools == []
|
|
assert p.instructions is None
|
|
|
|
|
|
def test_proxy_clone_preserves_and_overrides():
|
|
"""clone() mirrors Agent.clone — fields passed are overridden; rest preserved."""
|
|
base = RemoteAgentProxy(
|
|
name="peer1",
|
|
zap_uri="zap://peer-1.cluster:9999",
|
|
handoff_description="code review agent",
|
|
capabilities=["code", "review"],
|
|
metadata={"region": "us-west"},
|
|
)
|
|
cloned = base.clone(name="peer1-shadow")
|
|
assert cloned.name == "peer1-shadow"
|
|
assert cloned.zap_uri == base.zap_uri
|
|
assert cloned.handoff_description == base.handoff_description
|
|
assert cloned.capabilities == base.capabilities
|
|
assert cloned.metadata == base.metadata
|
|
# And confirm the override is shallow-copied (mutating the clone doesn't
|
|
# bleed into the original — caps lists are independent).
|
|
cloned.capabilities.append("ops")
|
|
assert base.capabilities == ["code", "review"]
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invoke_errors_without_feature_flag(monkeypatch):
|
|
"""invoke() refuses to dispatch when the wire feature-flag is off.
|
|
|
|
Fail-loud: avoids silent regression to a misconfigured local-dispatch
|
|
path when the operator forgets to set HANZO_AGENT_WIRE=zap on a node
|
|
that holds remote proxies.
|
|
"""
|
|
monkeypatch.delenv("HANZO_AGENT_WIRE", raising=False)
|
|
p = RemoteAgentProxy(name="peer1", zap_uri="zap://peer-1.cluster:9999")
|
|
with pytest.raises(RuntimeError, match="HANZO_AGENT_WIRE"):
|
|
await p.invoke("hello")
|