Ver código fonte

fix(workspace): 专项 agent 产物落进本 run 工作区 — 会话 CWD 对齐 [工作目录] 提示

实锤(run_bf2405e28547,文献地图助手):literature_map.md/references.md/PROGRESS.md
落进了 agent 家目录(work_dir),而 run 查看器 + 记录看的是本 run 的 workspace_path
→ 查看器空、且跨 run 互相覆盖(每次都写同一个 work_dir)。

根因:`_execute_agent` 的会话 CWD(Bash/文件工具相对路径基准)= work_dir,但 compiler
step-0 注入的 [工作目录] 提示 = overrides["workspace_path"](本 run 目录)——两者本该
一致却分叉。模型按相对路径写 → 落 CWD(work_dir),与提示指向的 run 目录不符。

修(方案 A):抽 `_resolve_session_cwd(inplace_dir, workspace_path, work_dir)`,CWD
对齐 [工作目录] 提示——工作区模式=F;专项 agent=本 run workspace_path(per-run 隔离、
查看器可见、不覆盖);兜底=work_dir。读输入仍靠 [数据来源目录] 绝对路径(step-0 提示
已禁相对/无参读),与工作区模式一致。版本 1.4.1→1.4.2。回归 agentpaas 344(+1)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kenny67nju 2 meses atrás
pai
commit
df648aaf96

+ 1 - 1
agentpaas/pyproject.toml

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "agentpaas"
-version = "1.4.1"
+version = "1.4.2"
 description = "Agent Platform as a Service — powered by lambdagent"
 readme = "README.md"
 license = "BUSL-1.1"

+ 3 - 3
agentpaas/src/agentpaas/api/app.py

@@ -29,7 +29,7 @@ async def _lifespan(_app: FastAPI):
 app = FastAPI(
     title="AgentPaaS",
     description="Agent Platform as a Service — powered by lambdagent",
-    version="1.4.1",
+    version="1.4.2",
     docs_url="/docs",
     redoc_url="/redoc",
     lifespan=_lifespan,
@@ -309,7 +309,7 @@ async def _on_startup():
 
 @app.get("/health")
 async def health():
-    return {"status": "ok", "version": "1.4.1"}
+    return {"status": "ok", "version": "1.4.2"}
 
 # Serve built Web UI static files when webui-dist/ exists.
 #
@@ -375,7 +375,7 @@ else:
     async def root():
         return {
             "service": "AgentPaaS",
-            "version": "1.4.1",
+            "version": "1.4.2",
             "docs": "/docs",
             "ui": "Run `cd webui && npm install && npm run build` to enable the Web UI.",
             "description": "Agent Platform as a Service — Every agent is a Lambda term.",

+ 21 - 9
agentpaas/src/agentpaas/api/v1/agents.py

@@ -507,6 +507,18 @@ async def rollback_agent(
 _WORKSPACE_CONV_TEMPLATES = {"workspace.assistant"}
 
 
+def _resolve_session_cwd(inplace_dir: str, workspace_path: str, work_dir: str) -> str:
+    """会话 CWD(Bash/文件工具相对路径基准)必须 == 给模型看的 [工作目录] 提示
+    (= overrides["workspace_path"]),否则相对写入落错目录。见 _execute_agent 注释。
+    工作区模式 → F;专项 agent → 本 run 的 workspace_path;兜底 → work_dir。"""
+    import os as _os
+    if inplace_dir and _os.path.isdir(inplace_dir):
+        return _os.path.abspath(inplace_dir)
+    if workspace_path and _os.path.isdir(workspace_path):
+        return workspace_path
+    return work_dir if (work_dir and _os.path.isdir(work_dir)) else workspace_path
+
+
 def _is_native_workspace(agent, config) -> bool:
     """是否走原生 claude-code 车道:工作区对话型模板 **且** provider=claude-code。
     native 车道是 `claude -p`(只认 claude 自己的模型);切到 qwen/ollama/openai 等
@@ -2166,15 +2178,15 @@ def _execute_agent(config: dict, input_text: str, on_step=None,
     elif agent_dir:
         workspace_path = create_run_workspace(agent_dir, run_id, input_text, config)
 
-    # CRITICAL: align the Bash tool's session CWD.
-    # Shell starts in work_dir (where final outputs go) so the agent
-    # naturally reads/writes user files there. workspace_path is the
-    # log/trace dir — {workspace} variable in prompts points there —
-    # but Bash starts in work_dir.
-    # When work_dir is absent, fall back to workspace_path (existing behaviour).
-    _cwd_to_set = (
-        work_dir if (work_dir and os.path.isdir(work_dir)) else workspace_path
-    )
+    # CRITICAL: Bash/文件工具的会话 CWD 必须 == 给模型看的 [工作目录] 提示
+    # (compiler step-0 注入的 = overrides["workspace_path"])。否则模型把产物写成
+    # 相对路径时落到 CWD,而提示叫它写到另一个目录 → 产物落错地方、run 查看器空、
+    # 跨 run 互相覆盖(run_bf2405e28547 实锤:literature_map.md 落进 agent 家目录而非
+    # 本 run 工作区)。对齐规则:
+    #   · 工作区对话模式(inplace_dir=F):CWD = F(就地读写用户文件夹)。
+    #   · 专项 agent:CWD = 本 run 的 workspace_path(per-run 隔离,查看器可见,不覆盖);
+    #     读输入靠 [数据来源目录] 的绝对路径(step-0 提示已明确禁止相对/无参读)。
+    _cwd_to_set = _resolve_session_cwd(inplace_dir, workspace_path, work_dir)
     # P1 工作文件夹硬沙箱:工作区模式(inplace_dir 有值)下,把 WriteFile/EditFile
     # 的写限定在所选文件夹 F 内,越界写直接拒绝。仅工作区模式启用——专项智能体
     # 写各自 run 工作区,root=None 不受影响。finally(下方)清空,防泄漏到下个 run。

+ 1 - 1
lambdagent/pyproject.toml

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "lambdagent"
-version = "1.4.1"
+version = "1.4.2"
 description = "A Lambda Calculus Agent DSL — every agent is a function, every composition is function composition, every loop is a Y combinator."
 readme = "README.md"
 license = "BUSL-1.1"

+ 19 - 0
tests/test_workspace_guard.py

@@ -62,3 +62,22 @@ def test_native_lane_only_for_claude_code_provider():
     # 非工作区模板:永不走 native
     assert _is_native_workspace({"agent_template": "research.x"},
                                 {"model": {"provider": "claude-code"}}) is False
+
+
+from agentpaas.api.v1.agents import _resolve_session_cwd
+
+
+def test_session_cwd_aligns_with_workdir_hint(tmp_path):
+    """会话 CWD 必须 == [工作目录] 提示(overrides.workspace_path),否则相对写入落错目录
+    (run_bf2405e28547:literature_map.md 落进 agent 家目录而非本 run 工作区)。"""
+    F = tmp_path / "user_folder";  F.mkdir()
+    ws = tmp_path / "run_xxx";     ws.mkdir()
+    home = tmp_path / "agent_home"; home.mkdir()
+    # 工作区对话模式:CWD = F
+    assert _resolve_session_cwd(str(F), str(ws), str(home)) == str(F)
+    # 专项 agent(无 inplace):CWD = 本 run workspace_path(修复点,原来错误地返回 work_dir)
+    assert _resolve_session_cwd("", str(ws), str(home)) == str(ws)
+    # 兜底:无 workspace_path → work_dir
+    assert _resolve_session_cwd("", "", str(home)) == str(home)
+    # inplace 无效路径被忽略,退回 workspace_path
+    assert _resolve_session_cwd("/no/such/dir", str(ws), str(home)) == str(ws)