Skip to content

Commit 7608ba9

Browse files
committed
Merge branch 'main' into feat/google-scopes
2 parents f5f3396 + b14b8f8 commit 7608ba9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+5586
-465
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,11 @@ jobs:
6262
uv run pytest tests/ -v
6363
6464
test-tools:
65-
name: Test Tools
66-
runs-on: ubuntu-latest
65+
name: Test Tools (${{ matrix.os }})
66+
runs-on: ${{ matrix.os }}
67+
strategy:
68+
matrix:
69+
os: [ubuntu-latest, windows-latest]
6770
steps:
6871
- uses: actions/checkout@v4
6972

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Use Hive when you need:
8282

8383
- Python 3.11+ for agent development
8484
- An LLM provider that powers the agents
85+
- **ripgrep (optional, recommended on Windows):** The `search_files` tool uses ripgrep for faster file search. If not installed, a Python fallback is used. On Windows: `winget install BurntSushi.ripgrep` or `scoop install ripgrep`
8586

8687
> **Note for Windows Users:** It is strongly recommended to use **WSL (Windows Subsystem for Linux)** or **Git Bash** to run this framework. Some core automation scripts may not execute correctly in standard Command Prompt or PowerShell.
8788

core/framework/agents/hive_coder/nodes/__init__.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def _build_appendices() -> str:
4646
"read_file",
4747
"write_file",
4848
"edit_file",
49+
"hashline_edit",
4950
"list_directory",
5051
"search_files",
5152
"run_command",
@@ -55,8 +56,6 @@ def _build_appendices() -> str:
5556
"validate_agent_tools",
5657
"list_agents",
5758
"list_agent_sessions",
58-
"get_agent_session_state",
59-
"get_agent_session_memory",
6059
"list_agent_checkpoints",
6160
"get_agent_checkpoint",
6261
"run_agent_tests",
@@ -131,12 +130,23 @@ def _build_appendices() -> str:
131130
132131
# Tools
133132
133+
## Paths (MANDATORY)
134+
**Always use RELATIVE paths**
135+
(e.g. `exports/agent_name/config.py`, `exports/agent_name/nodes/__init__.py`).
136+
**Never use absolute paths** like `/mnt/data/...` or `/workspace/...` — they fail.
137+
The project root is implicit.
138+
134139
## File I/O
135-
- read_file(path, offset?, limit?) — read with line numbers
140+
- read_file(path, offset?, limit?, hashline?) — read with line numbers; \
141+
hashline=True for N:hhhh|content anchors (use with hashline_edit)
136142
- write_file(path, content) — create/overwrite, auto-mkdir
137143
- edit_file(path, old_text, new_text, replace_all?) — fuzzy-match edit
144+
- hashline_edit(path, edits, auto_cleanup?, encoding?) — anchor-based \
145+
editing using N:hhhh refs from read_file(hashline=True). Ops: set_line, \
146+
replace_lines, insert_after, insert_before, replace, append
138147
- list_directory(path, recursive?) — list contents
139-
- search_files(pattern, path?, include?) — regex search
148+
- search_files(pattern, path?, include?, hashline?) — regex search; \
149+
hashline=True for anchors in results
140150
- run_command(command, cwd?, timeout?) — shell execution
141151
- undo_changes(path?) — restore from git snapshot
142152
@@ -149,8 +159,6 @@ def _build_appendices() -> str:
149159
in an agent's nodes actually exist. Call after building.
150160
- list_agents() — list all agent packages in exports/ with session counts
151161
- list_agent_sessions(agent_name, status?, limit?) — list sessions
152-
- get_agent_session_state(agent_name, session_id) — full session state
153-
- get_agent_session_memory(agent_name, session_id, key?) — memory data
154162
- list_agent_checkpoints(agent_name, session_id) — list checkpoints
155163
- get_agent_checkpoint(agent_name, session_id, checkpoint_id?) — load checkpoint
156164
- run_agent_tests(agent_name, test_types?, fail_fast?) — run pytest with parsing
@@ -185,8 +193,7 @@ def _build_appendices() -> str:
185193
## Debugging Built Agents
186194
When a user says "my agent is failing" or "debug this agent":
187195
1. list_agent_sessions("{agent_name}") — find the session
188-
2. get_agent_session_state("{agent_name}", "{session_id}") — see status
189-
3. get_agent_session_memory("{agent_name}", "{session_id}") — inspect data
196+
2. get_worker_status
190197
4. list_agent_checkpoints / get_agent_checkpoint — trace execution
191198
192199
# Agent Building Workflow
@@ -608,7 +615,7 @@ def _build_appendices() -> str:
608615
- File I/O: read_file, write_file, edit_file, list_directory, search_files, \
609616
run_command, undo_changes
610617
- Meta-agent: list_agent_tools, validate_agent_tools, \
611-
list_agents, list_agent_sessions, get_agent_session_state, get_agent_session_memory, \
618+
list_agents, list_agent_sessions, \
612619
list_agent_checkpoints, get_agent_checkpoint, run_agent_tests
613620
- load_built_agent(agent_path) — Load the agent and switch to STAGING mode
614621
- list_credentials(credential_id?) — List authorized credentials

core/framework/graph/executor.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,11 +621,14 @@ async def execute(
621621
# node doesn't restore a filled OutputAccumulator from the previous
622622
# webhook run (which would cause the judge to accept immediately).
623623
# The conversation history is preserved (continuous memory).
624+
# Exclude cold restores — those need to continue the conversation
625+
# naturally without a "start fresh" marker.
624626
_is_fresh_shared = bool(
625627
session_state
626628
and session_state.get("resume_session_id")
627629
and not session_state.get("paused_at")
628630
and not session_state.get("resume_from_checkpoint")
631+
and not session_state.get("cold_restore")
629632
)
630633
if _is_fresh_shared and is_continuous and self._storage_path:
631634
try:

core/framework/mcp/agent_builder_server.py

Lines changed: 2 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2899,6 +2899,7 @@ def run_tests(
28992899
text=True,
29002900
timeout=600, # 10 minute timeout
29012901
env=env,
2902+
stdin=subprocess.DEVNULL,
29022903
)
29032904
except subprocess.TimeoutExpired:
29042905
return json.dumps(
@@ -3091,6 +3092,7 @@ def debug_test(
30913092
text=True,
30923093
timeout=120, # 2 minute timeout for single test
30933094
env=env,
3095+
stdin=subprocess.DEVNULL,
30943096
)
30953097
except subprocess.TimeoutExpired:
30963098
return json.dumps(
@@ -3714,82 +3716,6 @@ def list_agent_sessions(
37143716
)
37153717

37163718

3717-
@mcp.tool()
3718-
def get_agent_session_state(
3719-
agent_work_dir: Annotated[str, "Path to the agent's working directory"],
3720-
session_id: Annotated[str, "The session ID (e.g., 'session_20260208_143022_abc12345')"],
3721-
) -> str:
3722-
"""
3723-
Load full session state for a specific session.
3724-
3725-
Returns complete session data including status, progress, result,
3726-
metrics, and checkpoint info. Memory values are excluded to prevent
3727-
context bloat -- use get_agent_session_memory to retrieve memory contents.
3728-
"""
3729-
state_path = Path(agent_work_dir) / "sessions" / session_id / "state.json"
3730-
data = _read_session_json(state_path)
3731-
if data is None:
3732-
return json.dumps({"error": f"Session not found: {session_id}"})
3733-
3734-
memory = data.get("memory", {})
3735-
data["memory_keys"] = list(memory.keys()) if isinstance(memory, dict) else []
3736-
data["memory_size"] = len(memory) if isinstance(memory, dict) else 0
3737-
data.pop("memory", None)
3738-
3739-
return json.dumps(data, indent=2, default=str)
3740-
3741-
3742-
@mcp.tool()
3743-
def get_agent_session_memory(
3744-
agent_work_dir: Annotated[str, "Path to the agent's working directory"],
3745-
session_id: Annotated[str, "The session ID"],
3746-
key: Annotated[str, "Specific memory key to retrieve. Empty for all."] = "",
3747-
) -> str:
3748-
"""
3749-
Get memory contents from a session.
3750-
3751-
Memory stores intermediate results passed between nodes. Use this
3752-
to inspect what data was produced during execution.
3753-
3754-
If key is provided, returns only that memory key's value.
3755-
If key is empty, returns all memory keys and their values.
3756-
"""
3757-
state_path = Path(agent_work_dir) / "sessions" / session_id / "state.json"
3758-
data = _read_session_json(state_path)
3759-
if data is None:
3760-
return json.dumps({"error": f"Session not found: {session_id}"})
3761-
3762-
memory = data.get("memory", {})
3763-
if not isinstance(memory, dict):
3764-
memory = {}
3765-
3766-
if key:
3767-
if key not in memory:
3768-
return json.dumps(
3769-
{
3770-
"error": f"Memory key not found: '{key}'",
3771-
"available_keys": list(memory.keys()),
3772-
}
3773-
)
3774-
value = memory[key]
3775-
return json.dumps(
3776-
{
3777-
"session_id": session_id,
3778-
"key": key,
3779-
"value": value,
3780-
"value_type": type(value).__name__,
3781-
},
3782-
indent=2,
3783-
default=str,
3784-
)
3785-
3786-
return json.dumps(
3787-
{"session_id": session_id, "memory": memory, "total_keys": len(memory)},
3788-
indent=2,
3789-
default=str,
3790-
)
3791-
3792-
37933719
@mcp.tool()
37943720
def list_agent_checkpoints(
37953721
agent_work_dir: Annotated[str, "Path to the agent's working directory"],

core/framework/runner/mcp_client.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import asyncio
88
import logging
99
import os
10+
import sys
11+
import threading
1012
from dataclasses import dataclass, field
1113
from typing import Any, Literal
1214

@@ -73,6 +75,8 @@ def __init__(self, config: MCPServerConfig):
7375
# Background event loop for persistent STDIO connection
7476
self._loop = None
7577
self._loop_thread = None
78+
# Serialize STDIO tool calls (avoids races, helps on Windows)
79+
self._stdio_call_lock = threading.Lock()
7680

7781
def _run_async(self, coro):
7882
"""
@@ -156,11 +160,19 @@ def _connect_stdio(self) -> None:
156160
# Create server parameters
157161
# Always inherit parent environment and merge with any custom env vars
158162
merged_env = {**os.environ, **(self.config.env or {})}
163+
# On Windows, passing cwd can cause WinError 267 ("invalid directory name").
164+
# tool_registry passes cwd=None and uses absolute script paths when applicable.
165+
cwd = self.config.cwd
166+
if os.name == "nt" and cwd is not None:
167+
# Avoid passing cwd on Windows; tool_registry should have set cwd=None
168+
# and absolute script paths for tools-dir servers. If cwd is still set,
169+
# pass None to prevent WinError 267 (caller should use absolute paths).
170+
cwd = None
159171
server_params = StdioServerParameters(
160172
command=self.config.command,
161173
args=self.config.args,
162174
env=merged_env,
163-
cwd=self.config.cwd,
175+
cwd=cwd,
164176
)
165177

166178
# Store for later use
@@ -184,10 +196,12 @@ async def init_connection():
184196
from mcp.client.stdio import stdio_client
185197

186198
# Create persistent stdio client context.
187-
# Redirect server stderr to devnull to prevent raw
188-
# output from leaking behind the TUI.
189-
devnull = open(os.devnull, "w") # noqa: SIM115
190-
self._stdio_context = stdio_client(server_params, errlog=devnull)
199+
# On Windows, use stderr so subprocess startup errors are visible.
200+
if os.name == "nt":
201+
errlog = sys.stderr
202+
else:
203+
errlog = open(os.devnull, "w") # noqa: SIM115
204+
self._stdio_context = stdio_client(server_params, errlog=errlog)
191205
(
192206
self._read_stream,
193207
self._write_stream,
@@ -353,7 +367,8 @@ def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
353367
raise ValueError(f"Unknown tool: {tool_name}")
354368

355369
if self.config.transport == "stdio":
356-
return self._run_async(self._call_tool_stdio_async(tool_name, arguments))
370+
with self._stdio_call_lock:
371+
return self._run_async(self._call_tool_stdio_async(tool_name, arguments))
357372
else:
358373
return self._call_tool_http(tool_name, arguments)
359374

@@ -448,11 +463,15 @@ async def _cleanup_stdio_async(self) -> None:
448463
if self._stdio_context:
449464
await self._stdio_context.__aexit__(None, None, None)
450465
except asyncio.CancelledError:
451-
logger.warning(
466+
logger.debug(
452467
"STDIO context cleanup was cancelled; proceeding with best-effort shutdown"
453468
)
454469
except Exception as e:
455-
logger.warning(f"Error closing STDIO context: {e}")
470+
msg = str(e).lower()
471+
if "cancel scope" in msg or "different task" in msg:
472+
logger.debug("STDIO context teardown (known anyio quirk): %s", e)
473+
else:
474+
logger.warning(f"Error closing STDIO context: {e}")
456475
finally:
457476
self._stdio_context = None
458477

0 commit comments

Comments
 (0)