mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
fix(acp): pin the session cwd for the turn
An ACP session registered the client's cwd for the *tools*
(`_register_task_cwd` -> `register_task_env_overrides`) but never pinned it
for the *prompt*. `agent/prompt_builder.py` reports
`Current working directory: {resolve_agent_cwd()}`, and `resolve_agent_cwd()`
reads the `_SESSION_CWD` contextvar, which ACP left unset — so it fell back
to `TERMINAL_CWD` / the launch dir.
The system prompt therefore advertised one root (commonly
`~/.hermes/workspace`, or the install tree) while the tools were rooted at the
editor's project. When the model emitted a *relative* path the tools resolved
it correctly; when it emitted an *absolute* path built from the advertised
root, the write landed outside the client's workspace and the turn still
reported success.
Observed in Zed/Buzz-shaped usage: the first prompt in a fresh workspace
creates the file under `~/.hermes/workspace/` and answers "Done." The client's
directory is untouched. Later sessions on the same cwd appear to work once a
session cwd record exists, which makes it look intermittent.
`_run_agent` already calls `set_session_vars(session_key=session_id)` inside
its `contextvars.copy_context()`, and that helper's `cwd` argument exists to
"pin the logical working directory for this context". It simply was not
passed. `gateway/session_context.py` and `tui_gateway/server.py` both already
pass it; ACP was the only surface that did not.
Adds a regression test asserting that the resolved cwd *during the turn* is
the cwd the client passed to `session/new`. It fails without this change
(resolving the install tree instead of the client's project).
This commit is contained in:
parent
40eebc7d70
commit
cca2a2fc8d
2 changed files with 51 additions and 1 deletions
|
|
@ -1639,6 +1639,47 @@ class TestPrompt:
|
|||
text and "[plugin appended this]" in text for text in all_texts
|
||||
), f"expected transformed final to be delivered, got: {all_texts!r}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_pins_the_session_cwd_for_the_turn(self, agent, tmp_path):
|
||||
"""The turn must resolve the ACP client's cwd, not the Hermes workspace.
|
||||
|
||||
``resolve_agent_cwd()`` is what the system prompt reports as "Current
|
||||
working directory". When it is left unpinned it falls back to
|
||||
TERMINAL_CWD / the launch dir, so the prompt advertises one root while
|
||||
the tools are rooted at the editor's project. The model then emits
|
||||
absolute paths under the advertised root and the edit silently lands
|
||||
outside the workspace the client asked for.
|
||||
"""
|
||||
workspace = tmp_path / "project"
|
||||
workspace.mkdir()
|
||||
|
||||
new_resp = await agent.new_session(cwd=str(workspace))
|
||||
state = agent.session_manager.get_session(new_resp.session_id)
|
||||
|
||||
observed = {}
|
||||
|
||||
def capture_cwd(*args, **kwargs):
|
||||
from agent.runtime_cwd import resolve_agent_cwd
|
||||
|
||||
observed["cwd"] = str(resolve_agent_cwd())
|
||||
return {"final_response": "ok", "messages": []}
|
||||
|
||||
state.agent.run_conversation = capture_cwd
|
||||
|
||||
mock_conn = MagicMock(spec=acp.Client)
|
||||
mock_conn.session_update = AsyncMock()
|
||||
agent._conn = mock_conn
|
||||
|
||||
await agent.prompt(
|
||||
prompt=[TextContentBlock(type="text", text="hello")],
|
||||
session_id=new_resp.session_id,
|
||||
)
|
||||
|
||||
assert observed.get("cwd") == str(workspace), (
|
||||
"the turn resolved "
|
||||
f"{observed.get('cwd')!r} instead of the ACP client's cwd "
|
||||
f"{str(workspace)!r}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_auto_titles_session(self, agent):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue