Let HanzoCloud carry request headers, and a tool name its default action

Enso reads its routing bounds from X-Max-Cost and X-Max-Latency-Ms on
/v1/chat/completions, and HanzoCloud could send no header but its own, so
no tool could bound a routed request. post() and the shared request path
now take extra headers that ride beside the bearer.

BaseTool runs `help` for a call that names no action. A tool with one
obvious act (llm's query, kai_decide's decide) now declares
DEFAULT_ACTION, which the call, the advertised schema's default and its
required list all read, so it answers the same no-action call the
TypeScript and Rust runtimes do. Every other tool keeps `help`.

hanzo-tools 0.3.8.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
zooqueen
2026-10-04 13:55:31 -07:00
co-authored by Hanzo Dev
parent e86e4e8908
commit e0897353ce
3 changed files with 30 additions and 12 deletions
+16 -5
View File
@@ -156,11 +156,13 @@ class HanzoCloud:
)
return self._client
async def _request(self, method: str, path: str, **kw: Any) -> Any:
async def _request(
self, method: str, path: str, headers: dict[str, str] | None = None, **kw: Any
) -> Any:
client = await self._get_client()
try:
resp = await client.request(
method, path, headers=self._headers(), **kw
method, path, headers={**self._headers(), **(headers or {})}, **kw
)
except Exception as e: # transport/DNS/timeout — no HTTP status
raise CloudError(f"{method} {path} failed: {e}") from e
@@ -180,9 +182,18 @@ class HanzoCloud:
clean = {k: v for k, v in (params or {}).items() if v is not None}
return await self._request("GET", path, params=clean)
async def post(self, path: str, json_body: dict[str, Any] | None = None) -> Any:
"""POST json_body to path, returning parsed JSON. Raises on failure."""
return await self._request("POST", path, json=json_body or {})
async def post(
self,
path: str,
json_body: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> Any:
"""POST json_body to path, returning parsed JSON. Raises on failure.
``headers`` ride beside the auth header, for a route that reads one (Enso's
X-Max-Cost and X-Max-Latency-Ms on /v1/chat/completions).
"""
return await self._request("POST", path, headers=headers, json=json_body or {})
async def stream(
self,
+13 -6
View File
@@ -259,6 +259,11 @@ class BaseTool(_BaseToolABC):
# Version for meta envelope
VERSION: ClassVar[str] = "0.12.0"
# The action a call that names none runs. `help` unless the tool has one
# obvious act, as `llm` (query) does — the same default the TypeScript and
# Rust runtimes give that tool.
DEFAULT_ACTION: ClassVar[str] = "help"
# Param aliases for cross-implementation parity (e.g., {"path": "uri"})
PARAM_ALIASES: ClassVar[dict[str, str]] = {}
@@ -460,18 +465,20 @@ class BaseTool(_BaseToolABC):
pass
async def call(
self, ctx: MCPContext, action: str = "help", **kwargs: Any
self, ctx: MCPContext, action: str | None = None, **kwargs: Any
) -> dict[str, Any]:
"""Execute tool with action routing.
Args:
ctx: MCP context
action: Action to execute (default: "help")
action: Action to execute (default: DEFAULT_ACTION, normally "help")
**kwargs: Action parameters (flat, matching TS wire format)
Returns:
Unified response envelope
"""
action = action or self.DEFAULT_ACTION
# Unwrap kwargs wrapping from legacy clients: {"kwargs": {...}} → flat
if "kwargs" in kwargs and isinstance(kwargs["kwargs"], dict) and len(kwargs) == 1:
kwargs = kwargs["kwargs"]
@@ -524,7 +531,7 @@ class BaseTool(_BaseToolABC):
"action": {
"type": "string",
"description": "Action to perform",
"default": "help",
"default": self.DEFAULT_ACTION,
}
}
for ah in self._handlers.values():
@@ -536,14 +543,14 @@ class BaseTool(_BaseToolABC):
parameters_schema = {
"type": "object",
"properties": all_properties,
"required": ["action"],
"required": ["action"] if self.DEFAULT_ACTION == "help" else [],
"additionalProperties": True,
}
# Permissive Pydantic model that accepts any extra fields
class _FlexArgs(ArgModelBase):
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
action: str = "help"
action: str | None = None
def model_dump_one_level(self) -> dict[str, Any]:
result = super().model_dump_one_level()
@@ -554,7 +561,7 @@ class BaseTool(_BaseToolABC):
# Handler function — receives flat validated args
tool_ref = self
async def _handler(ctx: MCPContext, action: str = "help", **kwargs: Any) -> Any:
async def _handler(ctx: MCPContext, action: str | None = None, **kwargs: Any) -> Any:
result = await tool_ref.call(ctx, action=action, **kwargs)
return _result_to_mcp(result)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "hanzo-tools"
version = "0.3.7"
version = "0.3.8"
description = "Hanzo AI tools - core infrastructure and tool bundles"
readme = "README.md"
requires-python = ">=3.12"