mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-15 09:11:24 +00:00
fix(agent): keep project-root cwd when the default workspace is filled in
Job tools still receive the default alias. That fill-in is marked so cwd resolution can ignore it and keep the single-workspace project root at the SDK boundary. Co-authored-by: celanwang <lumworn@gmail.com>
This commit is contained in:
parent
ffda20c451
commit
f2c5b16ae3
4 changed files with 82 additions and 7 deletions
|
|
@ -70,17 +70,20 @@ class BaseAgentWrapper(BaseComponent):
|
|||
return workspaces[alias].workspace_path
|
||||
|
||||
def _call_cwd(self, kwargs: dict[str, Any]) -> Path:
|
||||
"""Working directory for this call: explicit cwd config wins, then the bound workspace.
|
||||
"""Working directory for this call.
|
||||
|
||||
A relative configured cwd resolves against the bound workspace;
|
||||
an absolute path keeps its explicit meaning.
|
||||
An explicit cwd wins. A relative one resolves against the bound
|
||||
workspace; an absolute path keeps its explicit meaning. Otherwise an
|
||||
explicitly selected workspace selects that workspace root. The default
|
||||
alias injected for job tools is not an explicit selection, so a
|
||||
single-workspace call keeps the project root.
|
||||
"""
|
||||
if self._cwd:
|
||||
cwd = Path(self._cwd)
|
||||
if cwd.is_absolute():
|
||||
return cwd
|
||||
return self._call_workspace_path(kwargs) / cwd
|
||||
if kwargs.get("workspace"):
|
||||
if kwargs.get("workspace") and kwargs.get("_workspace_explicit", True):
|
||||
return self._call_workspace_path(kwargs)
|
||||
return self.cwd
|
||||
|
||||
|
|
@ -240,10 +243,18 @@ class BaseAgentWrapper(BaseComponent):
|
|||
return resolved
|
||||
|
||||
def _merged_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Merge component defaults with call-time kwargs; call-time values win."""
|
||||
"""Merge component defaults with call-time kwargs; call-time values win.
|
||||
|
||||
An omitted workspace becomes the default alias so job tools keep an
|
||||
explicit binding. ``_workspace_explicit`` records whether the caller or
|
||||
the component config selected that alias; cwd resolution uses it to
|
||||
keep the historical project root when the alias was only filled in.
|
||||
"""
|
||||
merged = {**self.kwargs, **kwargs}
|
||||
if (workspaces := _context_workspaces(self.app_context)) and not merged.get("workspace"):
|
||||
explicit_workspace = bool(merged.get("workspace"))
|
||||
if (workspaces := _context_workspaces(self.app_context)) and not explicit_workspace:
|
||||
merged["workspace"] = next(iter(workspaces))
|
||||
merged["_workspace_explicit"] = explicit_workspace
|
||||
if "output_schema" in merged:
|
||||
merged["output_schema"] = self._normalize_output_schema(merged["output_schema"])
|
||||
return merged
|
||||
|
|
|
|||
|
|
@ -387,7 +387,11 @@ async def test_agentscope_compact_session_forces_and_persists_compression(tmp_pa
|
|||
|
||||
await wrapper.compact_session("session-1")
|
||||
|
||||
assert observed["build"] == (None, {"resume": "session-1", "workspace": "default"})
|
||||
build_inputs, build_kwargs = observed["build"]
|
||||
assert build_inputs is None
|
||||
# Default-alias fill-in is marked so cwd resolution can ignore it.
|
||||
assert build_kwargs.pop("_workspace_explicit") is False
|
||||
assert build_kwargs == {"resume": "session-1", "workspace": "default"}
|
||||
assert observed["config"].trigger_ratio == 1e-9
|
||||
assert observed["state"] is FakeAgent.state
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import pytest
|
|||
from openai_codex.generated.v2_all import TokenUsageBreakdown
|
||||
from pydantic import BaseModel
|
||||
|
||||
from reme.components import ApplicationContext
|
||||
from reme.components.agent_wrapper.codex_agent_wrapper import CodexAgentWrapper
|
||||
from reme.components.job import BackgroundJob
|
||||
from reme.components.outbound_proxy import FixedHttpOutboundProxy
|
||||
|
|
@ -721,6 +722,63 @@ async def test_reply_normalizes_schema_and_reuses_persistent_client(tmp_path, mo
|
|||
assert close_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reply_keeps_project_root_cwd_without_explicit_workspace(tmp_path, monkeypatch):
|
||||
"""A single-workspace reply delivers the project root to the SDK.
|
||||
|
||||
The default alias is still injected for job tools. Naming that alias, or
|
||||
choosing another workspace, selects that workspace directory instead.
|
||||
"""
|
||||
project = tmp_path / "project"
|
||||
workspace = project / ".reme"
|
||||
context = ApplicationContext(workspace_dir=str(workspace))
|
||||
wrapper = CodexAgentWrapper(
|
||||
app_context=context,
|
||||
project_path="..",
|
||||
auth_mode="oauth",
|
||||
release_threads=False,
|
||||
)
|
||||
observed = {"thread": [], "turn": []}
|
||||
|
||||
class FakeThread:
|
||||
id = "thread-1"
|
||||
|
||||
async def run(self, _inputs, **kwargs):
|
||||
observed["turn"].append(kwargs["cwd"])
|
||||
return _TurnResult(final_response="ok")
|
||||
|
||||
class FakeCodex:
|
||||
def __init__(self, _config):
|
||||
pass
|
||||
|
||||
async def account(self):
|
||||
return SimpleNamespace(account=SimpleNamespace())
|
||||
|
||||
async def close(self):
|
||||
return None
|
||||
|
||||
async def thread_start(self, **kwargs):
|
||||
observed["thread"].append(kwargs["cwd"])
|
||||
return FakeThread()
|
||||
|
||||
monkeypatch.setattr("reme.components.agent_wrapper.codex_agent_wrapper.AsyncCodex", FakeCodex)
|
||||
|
||||
merged = wrapper._merged_kwargs({}) # pylint: disable=protected-access
|
||||
assert merged["workspace"] == "default"
|
||||
assert wrapper._resolve_injected_job_kwargs(merged)["workspace"] == "default" # pylint: disable=protected-access
|
||||
|
||||
await wrapper.reply("hello")
|
||||
await wrapper.reply("again", workspace="default")
|
||||
await wrapper.close()
|
||||
|
||||
project_cwd = str(wrapper.project_path)
|
||||
workspace_cwd = str(context.workspaces["default"].workspace_path)
|
||||
assert wrapper.cwd == wrapper.project_path
|
||||
assert project_cwd != workspace_cwd
|
||||
assert observed["thread"] == [project_cwd, workspace_cwd]
|
||||
assert observed["turn"] == [project_cwd, workspace_cwd]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("schema", [{}, _StructuredModel])
|
||||
async def test_reply_stream_rejects_output_schema(tmp_path, schema):
|
||||
|
|
|
|||
|
|
@ -89,6 +89,8 @@ def test_call_cwd_resolves_relative_against_bound_workspace(tmp_path):
|
|||
default_wrapper = CodexAgentWrapper(app_context=context)
|
||||
assert default_wrapper._call_cwd({"workspace": "beta"}) == tmp_path / "beta"
|
||||
assert default_wrapper._call_cwd({}) != tmp_path / "beta" # project path, not a workspace
|
||||
# Filling in the default alias for job tools does not switch a relative cwd.
|
||||
assert wrapper._call_cwd(wrapper._merged_kwargs({})) == tmp_path / "alpha" / "relative" / "sub"
|
||||
|
||||
|
||||
def test_dream_integration_lock_is_workspace_scoped(tmp_path):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue