mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
feat: 路径约定体系 & 自优化管道落地
This commit is contained in:
parent
08df5d580a
commit
df30b5902f
37 changed files with 239 additions and 324 deletions
|
|
@ -43,5 +43,5 @@ This project exposes local atomic capabilities as shared `public` tools for all
|
|||
|
||||
## Path behavior defaults
|
||||
|
||||
- `run_command`: when `cwd` is omitted, it runs in `data/workspace`.
|
||||
- `run_command`: when `cwd` is omitted, it runs in workspace root.
|
||||
- `write_file`: absolute path is used directly; relative path is written under `data/workspace`.
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ def _sniff_image_mime(data: bytes) -> str | None:
|
|||
|
||||
def _ffprobe_exists() -> bool:
|
||||
try:
|
||||
p = subprocess.run(["ffprobe", "-version"], capture_output=True, text=True, timeout=3)
|
||||
p = subprocess.run(["ffprobe", "-version"], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=3)
|
||||
return p.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
|
@ -115,6 +115,8 @@ def _ffprobe_video_meta(path: Path) -> dict[str, Any]:
|
|||
["ffprobe", "-v", "error", "-print_format", "json", "-show_format", "-show_streams", str(path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=8,
|
||||
)
|
||||
if p.returncode != 0:
|
||||
|
|
|
|||
|
|
@ -81,5 +81,7 @@ def run_cc_mini_cli(
|
|||
env=merged,
|
||||
check=False,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
**kwargs,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ from oclaw.runtime.session_auto_title import (
|
|||
finalize_auto_title,
|
||||
)
|
||||
from oclaw.runtime.tools.base import ToolRegistry
|
||||
from oclaw.runtime.tools.local_sdk import local_adapter_startup_self_check
|
||||
from oclaw.runtime.tools.public.local_sdk import local_adapter_startup_self_check
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -154,6 +154,8 @@ def _run_dep_install(command: list[str], *, cwd: Path) -> tuple[bool, str]:
|
|||
cwd=str(cwd),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=600,
|
||||
check=False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,12 +42,13 @@ def _session_bootstrap_content_signature(*, skill_binding_role: str | None, work
|
|||
|
||||
targets: list[Path] = []
|
||||
# session-bootstrap static source files
|
||||
targets.append((repo / "runtime" / "skills" / "session-bootstrap" / "SOUL.md").resolve())
|
||||
targets.append((repo / "runtime" / "skills" / "session-bootstrap" / "IDENTITY.md").resolve())
|
||||
targets.append((repo / "skills" / "session-bootstrap" / "SOUL.md").resolve())
|
||||
targets.append((repo / "skills" / "session-bootstrap" / "IDENTITY.md").resolve())
|
||||
# wiki global memory/improvement sources
|
||||
targets.append((repo / "data" / "wiki" / "improvement" / "learnings.md").resolve())
|
||||
targets.append((repo / "data" / "wiki" / "improvement" / "errors.md").resolve())
|
||||
targets.append((repo / "data" / "wiki" / "improvement" / "feature-requests.md").resolve())
|
||||
targets.append((repo / "data" / "wiki" / "users" / "current.md").resolve())
|
||||
|
||||
core_dir = (repo / "data" / "wiki" / "core").resolve()
|
||||
if core_dir.exists() and core_dir.is_dir():
|
||||
|
|
@ -99,6 +100,7 @@ def _unified_skill_policy_guidance() -> str:
|
|||
# Global policy for all agents (including dynamic/ephemeral) — appended to base_system in build_executor_system_prompt.
|
||||
return (
|
||||
"## 技能使用政策(Skill Usage Policy):\n"
|
||||
"- 每次会话启动后,在进行文件/路径相关操作前,必须先读取 `skills/_workspace/public/path-convention/SKILL.md` 了解当前路径规范。\n"
|
||||
"- 如果用户问你有哪些技能(skill/技能),请直接根据已注入的技能目录及其 description 回答。\n"
|
||||
"- 不要为了“列出技能”而去读取 SKILL.md。只有在你确实需要某个技能的详细使用说明时,才读取对应 SKILL.md。\n"
|
||||
"- 当你需要技能细节时,请按目录中给出的 path 读取对应的 SKILL.md。\n"
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
# Canonical memory expert tools live under this directory.
|
||||
from .wiki_tools import (
|
||||
memory_wiki_apply_tool,
|
||||
memory_wiki_get_tool,
|
||||
memory_wiki_lint_tool,
|
||||
memory_wiki_search_tool,
|
||||
memory_wiki_status_tool,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"memory_wiki_apply_tool",
|
||||
"memory_wiki_get_tool",
|
||||
"memory_wiki_lint_tool",
|
||||
"memory_wiki_search_tool",
|
||||
"memory_wiki_status_tool",
|
||||
]
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from .adapter import LocalAdapter, LocalAdapterError, get_local_adapter, local_adapter_startup_self_check
|
||||
|
||||
__all__ = ["LocalAdapter", "LocalAdapterError", "get_local_adapter", "local_adapter_startup_self_check"]
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def cd_tool() -> ToolSpec:
|
||||
def _handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
cwd = str(args.get("cwd") or "").strip()
|
||||
return get_local_adapter().cd(cwd=cwd)
|
||||
|
||||
return ToolSpec(
|
||||
name="cd",
|
||||
description="Change current working directory for local tools (best-effort, per-process).",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {"cwd": {"type": "string", "description": "Directory to change into."}},
|
||||
"required": ["cwd"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=_handler,
|
||||
tags=frozenset({"public", "local", "write"}),
|
||||
risk_level="high",
|
||||
timeout_s=5.0,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["cd_tool"]
|
||||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def delete_file_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def edit_file_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def get_cwd_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def get_env_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ def _git(command: str, *, cwd: str) -> dict[str, Any]:
|
|||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=60.0,
|
||||
)
|
||||
out = (cp.stdout or "") + (("\n" + cp.stderr) if cp.stderr else "")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def kill_process_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def list_directory_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def list_processes_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ class LocalAdapter:
|
|||
|
||||
def get_cwd(self) -> dict[str, Any]:
|
||||
try:
|
||||
p = resolve_workspace_path(self._cwd or ".")
|
||||
return {"ok": True, "cwd": str(p)}
|
||||
root = resolve_workspace_path(".")
|
||||
return {"ok": True, "cwd": str(root)}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error_code": "local_execution_error", "error": f"{type(exc).__name__}: {exc}"}
|
||||
|
||||
|
|
@ -105,8 +105,8 @@ class LocalAdapter:
|
|||
try:
|
||||
timeout_s = max(1, min(int(timeout or 300), 600))
|
||||
# run_command never follows adapter cd state.
|
||||
# It only uses explicit cwd; otherwise defaults to data/workspace.
|
||||
workdir = str(resolve_workspace_path(cwd or "data/workspace"))
|
||||
# It only uses explicit cwd; otherwise defaults to workspace root.
|
||||
workdir = str(resolve_workspace_path(cwd or "."))
|
||||
Path(workdir).mkdir(parents=True, exist_ok=True)
|
||||
# Use UTF-8 for decoded streams: Windows defaults (e.g. GBK) break on UTF-8-only bytes from
|
||||
# curl/wttr.in, git, ripgrep, etc.; subprocess may leave stdout None after UnicodeDecodeError.
|
||||
|
|
@ -370,6 +370,8 @@ class LocalAdapter:
|
|||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=10.0,
|
||||
)
|
||||
out = str(cp.stdout or "")
|
||||
|
|
@ -393,6 +395,8 @@ class LocalAdapter:
|
|||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=10.0,
|
||||
)
|
||||
for line in str(cp.stdout or "").splitlines():
|
||||
|
|
@ -418,7 +422,7 @@ class LocalAdapter:
|
|||
try:
|
||||
if os.name == "nt":
|
||||
cmd = f"taskkill /PID {p} " + ("/F" if force else "")
|
||||
cp = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10.0)
|
||||
cp = subprocess.run(cmd, shell=True, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10.0)
|
||||
ok = cp.returncode == 0
|
||||
return {"ok": ok, "pid": p, "stdout": str(cp.stdout or ""), "stderr": str(cp.stderr or ""), "exit_code": int(cp.returncode)}
|
||||
import signal
|
||||
|
|
@ -462,5 +466,3 @@ def local_adapter_startup_self_check() -> dict[str, Any]:
|
|||
"error": f"{type(exc).__name__}: {exc}",
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["LocalAdapter", "LocalAdapterError", "get_local_adapter", "local_adapter_startup_self_check"]
|
||||
|
|
@ -31,8 +31,6 @@ def _wiki_handlers() -> dict[str, Callable[[dict[str, Any]], dict[str, Any]]]:
|
|||
if spec is None or spec.loader is None:
|
||||
return {}
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
# Register module before exec so decorators (e.g., @dataclass) can resolve
|
||||
# cls.__module__ via sys.modules during import-time processing.
|
||||
sys.modules[spec.name] = mod
|
||||
spec.loader.exec_module(mod) # type: ignore[assignment]
|
||||
fn = getattr(mod, "build_wiki_tool_specs", None)
|
||||
|
|
@ -67,7 +65,8 @@ def _status_tool(public_name: str, desc: str) -> ToolSpec:
|
|||
description=desc,
|
||||
parameters={"type": "object", "properties": {}, "required": [], "additionalProperties": False},
|
||||
handler=lambda args: _delegate("wiki_status", args),
|
||||
tags=frozenset({"memory", "wiki", "curator"}),
|
||||
tags=frozenset({"wiki", "curator"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
|
@ -87,7 +86,8 @@ def _get_tool(public_name: str, desc: str) -> ToolSpec:
|
|||
"additionalProperties": False,
|
||||
},
|
||||
handler=lambda args: _delegate("wiki_get", args),
|
||||
tags=frozenset({"memory", "wiki", "curator"}),
|
||||
tags=frozenset({"wiki", "curator"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
|
@ -108,7 +108,8 @@ def _search_tool(public_name: str, desc: str) -> ToolSpec:
|
|||
"additionalProperties": False,
|
||||
},
|
||||
handler=lambda args: _delegate("wiki_search", args),
|
||||
tags=frozenset({"memory", "wiki", "curator"}),
|
||||
tags=frozenset({"wiki", "curator"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
|
@ -124,7 +125,8 @@ def _lint_tool(public_name: str, desc: str) -> ToolSpec:
|
|||
"additionalProperties": False,
|
||||
},
|
||||
handler=lambda args: _delegate("wiki_lint", args),
|
||||
tags=frozenset({"memory", "wiki", "curator"}),
|
||||
tags=frozenset({"wiki", "curator"}),
|
||||
risk_level="low",
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
|
|
@ -144,8 +146,8 @@ def _apply_tool(public_name: str, desc: str) -> ToolSpec:
|
|||
"additionalProperties": False,
|
||||
},
|
||||
handler=lambda args: _delegate("wiki_apply", args),
|
||||
tags=frozenset({"memory", "wiki", "curator", "write"}),
|
||||
risk_level="high",
|
||||
tags=frozenset({"wiki", "curator", "write"}),
|
||||
risk_level="low",
|
||||
read_only=False,
|
||||
)
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def mkdir_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def move_file_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from oclaw.runtime.tools.base import ToolSpec
|
|||
|
||||
def _ffmpeg_exists() -> bool:
|
||||
try:
|
||||
p = subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True, timeout=3)
|
||||
p = subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=3)
|
||||
return p.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
|
@ -41,6 +41,8 @@ def _ffprobe_json(path: Path) -> dict[str, Any] | None:
|
|||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=8,
|
||||
)
|
||||
if p.returncode != 0:
|
||||
|
|
@ -179,6 +181,8 @@ def query_video_attachment_tool() -> ToolSpec:
|
|||
["ffmpeg", "-y", "-i", str(p), "-vn", "-ac", "1", "-ar", "16000", str(wav)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=60,
|
||||
)
|
||||
if not wav.exists() or wav.stat().st_size <= 0:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def run_command_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def search_files_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.local_sdk import get_local_adapter
|
||||
from oclaw.runtime.tools.public.local_sdk import get_local_adapter
|
||||
|
||||
|
||||
def set_env_tool() -> ToolSpec:
|
||||
|
|
|
|||
|
|
@ -156,6 +156,8 @@ def _run(argv: list[str], *, cwd: Path, stdin_json: dict[str, Any], timeout_s: f
|
|||
argv,
|
||||
input=json.dumps(stdin_json, ensure_ascii=False),
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
capture_output=True,
|
||||
cwd=str(cwd),
|
||||
timeout=max(1, int(timeout_s)),
|
||||
|
|
|
|||
31
skills/_workspace/public/path-convention/SKILL.md
Normal file
31
skills/_workspace/public/path-convention/SKILL.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
name: path-convention
|
||||
description: "Oclaw 项目路径规范。定义了项目根目录、用户数据区、技能目录、Wiki 记忆四者之间的路径关系。适用于:新建文件、读取文件、运行脚本、创建目录等所有涉及文件路径的操作之前。"
|
||||
---
|
||||
|
||||
# 路径规范 —— Path Convention
|
||||
|
||||
## 根目录
|
||||
|
||||
项目根 = 当前仓库根目录(workspace root)
|
||||
|
||||
所有 `read_file`、`write_file`、`delete_file`、`move_file`、`mkdir` 等工具的 `path` 参数支持两种写法:
|
||||
- 相对路径:相对于**项目根目录**(workspace root)解析,例如 `data/workspace/a.txt`
|
||||
- 绝对路径:按系统绝对路径解析(Windows 如 `D:\...`,Linux/macOS 如 `/...`),但仍受工作区路径访问策略约束
|
||||
|
||||
## 核心目录
|
||||
|
||||
| 用途 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| **用户数据** | `data/workspace/` | 所有用户生成的文件放这里:脚本、文档、工作流、图片生成产物、测试文件等 |
|
||||
| **技能文件** | `skills/` | 系统技能和用户安装的技能 |
|
||||
| **Wiki 记忆** | `data/wiki/` | 长期记忆、Learnings、用户偏好、项目背景 |
|
||||
| **临时文件** | `data/workspace/tmp/` | 临时中间文件 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `get_cwd()` 返回项目根目录(workspace root)
|
||||
- `run_command` 未传 `cwd` 时默认在项目根目录执行;需要在子目录执行时请显式传 `cwd`
|
||||
- 用户数据只放在 `data/workspace/` 下,不要直接散落在根目录
|
||||
- `data/wiki/` 仅存放结构化记忆,不要放脚本或工作流文件
|
||||
- 技能安装路径见 `skill_auto_install` 工具,勿手动写入 `skills/` 以外的技能目录
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
/**
|
||||
* Oclaw 自我改进 Hook
|
||||
*
|
||||
* 在 agent:bootstrap 阶段注入学习沉淀提醒。
|
||||
*/
|
||||
|
||||
const REMINDER_NAME = 'SELF_IMPROVEMENT_REMINDER.md';
|
||||
const REMINDER_PATH = REMINDER_NAME;
|
||||
|
||||
const REMINDER_CONTENT = `
|
||||
## 自我改进提醒
|
||||
|
||||
任务完成后,请评估是否产生可沉淀学习。
|
||||
|
||||
仅在当前仓库/工作区启用 self-improvement 技能时记录。
|
||||
|
||||
记录前:
|
||||
- 使用 memory_wiki_* 工具写入 \`improvement/\` 下的 Wiki 笔记
|
||||
- 不记录密钥、令牌、私钥、环境变量或原始对话全文
|
||||
- 优先使用简短摘要或脱敏片段,避免完整命令输出
|
||||
|
||||
**以下情况应记录:**
|
||||
- 用户纠正你 → \`improvement/learnings.md\`
|
||||
- 命令/操作失败 → \`improvement/errors.md\`
|
||||
- 用户提出缺失能力 → \`improvement/feature-requests.md\`
|
||||
- 发现认知错误 → \`improvement/learnings.md\`
|
||||
- 发现更优做法 → \`improvement/learnings.md\`
|
||||
|
||||
**当模式被验证后进行提升:**
|
||||
- 行为模式 → \`SOUL.md\`
|
||||
- 工作流改进 → \`AGENTS.md\`
|
||||
- 工具易错点 → \`TOOLS.md\`
|
||||
|
||||
条目保持简洁:时间、标题、发生了什么、后续应如何做。
|
||||
`.trim();
|
||||
|
||||
function isObject(value) {
|
||||
return !!value && typeof value === 'object';
|
||||
}
|
||||
|
||||
function isInjectedReminderFile(value) {
|
||||
if (!isObject(value) || value.path !== REMINDER_PATH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
value.virtual === true ||
|
||||
value.content === REMINDER_CONTENT
|
||||
);
|
||||
}
|
||||
|
||||
const handler = async (event) => {
|
||||
// 事件结构安全检查
|
||||
if (!event || typeof event !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅处理 agent:bootstrap 事件
|
||||
if (event.type !== 'agent' || event.action !== 'bootstrap') {
|
||||
return;
|
||||
}
|
||||
|
||||
// context 安全检查
|
||||
if (!event.context || typeof event.context !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳过子代理会话,避免引导污染
|
||||
const sessionKey = event.sessionKey || '';
|
||||
if (sessionKey.includes(':subagent:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 以虚拟 bootstrap 文件注入提醒
|
||||
if (Array.isArray(event.context.bootstrapFiles)) {
|
||||
const occupiedByOtherFile = event.context.bootstrapFiles.some(
|
||||
(file) => isObject(file) && file.path === REMINDER_PATH && !isInjectedReminderFile(file),
|
||||
);
|
||||
if (occupiedByOtherFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanedBootstrapFiles = event.context.bootstrapFiles.filter(
|
||||
(file, index, files) =>
|
||||
!isInjectedReminderFile(file) ||
|
||||
files.findIndex((candidate) => isInjectedReminderFile(candidate)) === index,
|
||||
);
|
||||
|
||||
const reminderFile = {
|
||||
name: REMINDER_NAME,
|
||||
path: REMINDER_PATH,
|
||||
content: REMINDER_CONTENT,
|
||||
missing: false,
|
||||
virtual: true,
|
||||
};
|
||||
|
||||
const existingIndex = cleanedBootstrapFiles.findIndex((file) => isInjectedReminderFile(file));
|
||||
if (existingIndex === -1) {
|
||||
cleanedBootstrapFiles.push(reminderFile);
|
||||
} else {
|
||||
cleanedBootstrapFiles[existingIndex] = reminderFile;
|
||||
}
|
||||
|
||||
event.context.bootstrapFiles = cleanedBootstrapFiles;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = handler;
|
||||
module.exports.default = handler;
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
/**
|
||||
* Oclaw 自我改进 Hook
|
||||
*
|
||||
* 在 agent:bootstrap 阶段注入学习沉淀提醒。
|
||||
*/
|
||||
|
||||
import type { HookHandler } from 'oclaw/hooks';
|
||||
|
||||
const REMINDER_NAME = 'SELF_IMPROVEMENT_REMINDER.md';
|
||||
const REMINDER_PATH = REMINDER_NAME;
|
||||
|
||||
const REMINDER_CONTENT = `## 自我改进提醒
|
||||
|
||||
任务完成后,请评估是否产生可沉淀学习。
|
||||
|
||||
仅在当前仓库/工作区启用 self-improvement 技能时记录。
|
||||
|
||||
记录前:
|
||||
- 使用 memory_wiki_* 工具写入 \`improvement/\` 下的 Wiki 笔记
|
||||
- 不记录密钥、令牌、私钥、环境变量或原始对话全文
|
||||
- 优先使用简短摘要或脱敏片段,避免完整命令输出
|
||||
|
||||
**以下情况应记录:**
|
||||
- 用户纠正你 → \`improvement/learnings.md\`
|
||||
- 命令/操作失败 → \`improvement/errors.md\`
|
||||
- 用户提出缺失能力 → \`improvement/feature-requests.md\`
|
||||
- 发现认知错误 → \`improvement/learnings.md\`
|
||||
- 发现更优做法 → \`improvement/learnings.md\`
|
||||
|
||||
**当模式被验证后进行提升:**
|
||||
- 行为模式 → \`SOUL.md\`
|
||||
- 工作流改进 → \`AGENTS.md\`
|
||||
- 工具易错点 → \`TOOLS.md\`
|
||||
|
||||
条目保持简洁:时间、标题、发生了什么、后续应如何做。`;
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object';
|
||||
}
|
||||
|
||||
function isInjectedReminderFile(value: unknown): boolean {
|
||||
if (!isObject(value) || value.path !== REMINDER_PATH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
value.virtual === true ||
|
||||
value.content === REMINDER_CONTENT
|
||||
);
|
||||
}
|
||||
|
||||
const handler: HookHandler = async (event) => {
|
||||
// 事件结构安全检查
|
||||
if (!event || typeof event !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅处理 agent:bootstrap 事件
|
||||
if (event.type !== 'agent' || event.action !== 'bootstrap') {
|
||||
return;
|
||||
}
|
||||
|
||||
// context 安全检查
|
||||
if (!event.context || typeof event.context !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳过子代理会话,避免引导污染
|
||||
const sessionKey = event.sessionKey || '';
|
||||
if (sessionKey.includes(':subagent:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 以虚拟 bootstrap 文件注入提醒
|
||||
if (Array.isArray(event.context.bootstrapFiles)) {
|
||||
const occupiedByOtherFile = event.context.bootstrapFiles.some(
|
||||
(file) => isObject(file) && file.path === REMINDER_PATH && !isInjectedReminderFile(file),
|
||||
);
|
||||
if (occupiedByOtherFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanedBootstrapFiles = event.context.bootstrapFiles.filter(
|
||||
(file, index, files) =>
|
||||
!isInjectedReminderFile(file) ||
|
||||
files.findIndex((candidate) => isInjectedReminderFile(candidate)) === index,
|
||||
);
|
||||
|
||||
const reminderFile = {
|
||||
name: REMINDER_NAME,
|
||||
path: REMINDER_PATH,
|
||||
content: REMINDER_CONTENT,
|
||||
missing: false,
|
||||
virtual: true,
|
||||
};
|
||||
|
||||
const existingIndex = cleanedBootstrapFiles.findIndex((file) => isInjectedReminderFile(file));
|
||||
if (existingIndex === -1) {
|
||||
cleanedBootstrapFiles.push(reminderFile);
|
||||
} else {
|
||||
cleanedBootstrapFiles[existingIndex] = reminderFile;
|
||||
}
|
||||
|
||||
event.context.bootstrapFiles = cleanedBootstrapFiles;
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
|
@ -23,7 +23,7 @@ Oclaw uses workspace-based prompt injection combined with event-driven hooks. Co
|
|||
└── hooks/ # Custom hooks
|
||||
└── <hook-name>/
|
||||
├── HOOK.md
|
||||
└── handler.ts
|
||||
└── handler.py
|
||||
```
|
||||
|
||||
## 快速配置
|
||||
|
|
|
|||
|
|
@ -128,13 +128,35 @@ def _extract_recent_topic(memory_text: str) -> str:
|
|||
return _last_nonempty_line(memory_text)
|
||||
|
||||
|
||||
def _extract_developer_name(raw_text: str) -> str:
|
||||
lines = [ln.strip() for ln in str(raw_text or "").splitlines() if ln.strip()]
|
||||
for ln in lines:
|
||||
low = ln.lower()
|
||||
if low.startswith("#"):
|
||||
continue
|
||||
if low.startswith("name:") or low.startswith("姓名:"):
|
||||
name = ln.split(":", 1)[1].strip()
|
||||
if name:
|
||||
return name[:60]
|
||||
if low.startswith("- name:") or low.startswith("- 姓名:"):
|
||||
name = ln.split(":", 1)[1].strip()
|
||||
if name:
|
||||
return name[:60]
|
||||
if ln.startswith("- "):
|
||||
val = ln[2:].strip()
|
||||
if val:
|
||||
return val[:60]
|
||||
return ln[:60]
|
||||
return ""
|
||||
|
||||
|
||||
def _build_bootstrap_content(event: Any) -> str:
|
||||
ws_dir = _workspace_dir(event)
|
||||
repo = _repo_root()
|
||||
wiki = _wiki_root(repo)
|
||||
|
||||
soul = repo / "runtime" / "skills" / "session-bootstrap" / "SOUL.md"
|
||||
ident = repo / "runtime" / "skills" / "session-bootstrap" / "IDENTITY.md"
|
||||
soul = repo / "skills" / "session-bootstrap" / "SOUL.md"
|
||||
ident = repo / "skills" / "session-bootstrap" / "IDENTITY.md"
|
||||
mem_file = _latest_memory_file(ws_dir)
|
||||
|
||||
role_id = _agent_role(event)
|
||||
|
|
@ -158,10 +180,12 @@ def _build_bootstrap_content(event: Any) -> str:
|
|||
role_lines.append(f"### {p.name}\n{snap or '(缺失)'}")
|
||||
role_txt = "\n\n".join(role_lines).strip()
|
||||
lrn_txt = _safe_read(learnings, max_chars=900)
|
||||
user_current = wiki / "users" / "current.md"
|
||||
user_txt = _safe_read(user_current, max_chars=400)
|
||||
|
||||
topic = _extract_recent_topic(mem_txt) or "近期项目上下文"
|
||||
learning = _extract_recent_learning(lrn_txt) or "待补充新的关键学习"
|
||||
developer = "开发者"
|
||||
developer = _extract_developer_name(user_txt) or "开发者"
|
||||
welcome = f"欢迎回来,{developer}。上次我们聊了{topic},我学到了{learning}。"
|
||||
|
||||
mem_path = str(mem_file) if mem_file else "(无)"
|
||||
|
|
@ -184,6 +208,7 @@ def _build_bootstrap_content(event: Any) -> str:
|
|||
f"- 当前角色: {role_id or '(未知)'}\n"
|
||||
f"- Wiki core 规则文件: {core_sources}\n"
|
||||
f"- Wiki 角色规则文件: {role_sources}\n"
|
||||
f"- Wiki 当前用户: {user_current}\n"
|
||||
f"- Wiki 学习记录: {learnings}\n"
|
||||
f"- Wiki 错误记录: {errors}\n"
|
||||
f"- Wiki 需求记录: {feats}\n\n"
|
||||
|
|
@ -223,6 +248,13 @@ def handle(event: object) -> None:
|
|||
return
|
||||
|
||||
content = _build_bootstrap_content(event)
|
||||
try:
|
||||
repo = _repo_root()
|
||||
soul_ok = bool((repo / "skills" / "session-bootstrap" / "SOUL.md").exists())
|
||||
ident_ok = bool((repo / "skills" / "session-bootstrap" / "IDENTITY.md").exists())
|
||||
ctx["sessionBootstrapDiag"] = {"soul_found": soul_ok, "identity_found": ident_ok}
|
||||
except Exception:
|
||||
pass
|
||||
occupied = any(_is_record(f) and str(f.get("path")) == BOOT_PATH and not _is_bootstrap_file(f) for f in files)
|
||||
if occupied:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ Set in OpenClaw config:
|
|||
## Script Location
|
||||
|
||||
```bash
|
||||
python3 skills/tavily/lib/tavily_search.py <command> "query" [options]
|
||||
python3 skills/tavily-search-pro/lib/tavily_search.py <command> "query" [options]
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -347,5 +347,5 @@ Sources:
|
|||
## Install
|
||||
|
||||
```bash
|
||||
bash skills/tavily/install.sh
|
||||
bash skills/tavily-search-pro/install.sh
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name: weather
|
||||
description: Get current weather and forecasts (no API key required).
|
||||
homepage: https://wttr.in/:help
|
||||
metadata: {"clawdbot":{"emoji":"🌤️","requires":{"bins":["curl"]}}}
|
||||
metadata: {"clawdbot":{"emoji":"🌤️"}}
|
||||
---
|
||||
|
||||
# Weather
|
||||
|
|
@ -17,6 +17,11 @@ curl -s "wttr.in/London?format=3"
|
|||
# Output: London: ⛅️ +8°C
|
||||
```
|
||||
|
||||
Tool fallback (no curl required):
|
||||
```text
|
||||
web_fetch_clean url="https://wttr.in/London?format=3"
|
||||
```
|
||||
|
||||
Compact format:
|
||||
```bash
|
||||
curl -s "wttr.in/London?format=%l:+%c+%t+%h+%w"
|
||||
|
|
@ -36,6 +41,7 @@ Tips:
|
|||
- Units: `?m` (metric) `?u` (USCS)
|
||||
- Today only: `?1` · Current only: `?0`
|
||||
- PNG: `curl -s "wttr.in/Berlin.png" -o /tmp/weather.png`
|
||||
- If `curl` is unavailable, prefer `web_fetch_clean` for text responses
|
||||
|
||||
## Open-Meteo (fallback, JSON)
|
||||
|
||||
|
|
@ -44,6 +50,11 @@ Free, no key, good for programmatic use:
|
|||
curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12¤t_weather=true"
|
||||
```
|
||||
|
||||
Tool fallback:
|
||||
```text
|
||||
web_fetch_clean url="https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12¤t_weather=true"
|
||||
```
|
||||
|
||||
Find coordinates for a city, then query. Returns JSON with temp, windspeed, weathercode.
|
||||
|
||||
Docs: https://open-meteo.com/en/docs
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import tempfile
|
|||
from pathlib import Path
|
||||
|
||||
from oclaw.runtime.tools.catalog import default_registry
|
||||
from oclaw.runtime.tools.local_sdk.adapter import LocalAdapter
|
||||
from oclaw.runtime.tools.public.local_sdk import LocalAdapter
|
||||
from oclaw.runtime.tools.public.edit_file_tool import edit_file_tool
|
||||
from oclaw.runtime.tools.public.run_command_tool import run_command_tool
|
||||
from oclaw.runtime.tools.public.read_file_tool import read_file_tool
|
||||
|
|
@ -13,7 +13,6 @@ from oclaw.runtime.tools.public_registry import clear_public_tool_cache
|
|||
from oclaw.runtime.tools.public.list_directory_tool import list_directory_tool
|
||||
from oclaw.runtime.tools.public.search_files_tool import search_files_tool
|
||||
from oclaw.runtime.tools.public.get_cwd_tool import get_cwd_tool
|
||||
from oclaw.runtime.tools.public.cd_tool import cd_tool
|
||||
from oclaw.runtime.tools.public.get_env_tool import get_env_tool
|
||||
from oclaw.runtime.tools.public.set_env_tool import set_env_tool
|
||||
|
||||
|
|
@ -33,7 +32,6 @@ def test_local_public_read_tool_visible_by_default() -> None:
|
|||
assert "mkdir" not in names
|
||||
assert "delete_file" not in names
|
||||
assert "move_file" not in names
|
||||
assert "cd" not in names
|
||||
assert "set_env" not in names
|
||||
assert "kill_process" not in names
|
||||
|
||||
|
|
@ -54,7 +52,6 @@ def test_local_public_high_risk_tools_visible_when_enabled(monkeypatch) -> None:
|
|||
assert "mkdir" in names
|
||||
assert "delete_file" in names
|
||||
assert "move_file" in names
|
||||
assert "cd" in names
|
||||
assert "set_env" in names
|
||||
assert "kill_process" in names
|
||||
|
||||
|
|
@ -160,14 +157,10 @@ def test_p1_p2_read_tools_smoke(tmp_path: Path, monkeypatch) -> None:
|
|||
out_s = search_files_tool().handler({"pattern": "hell", "root": "d", "regex": False})
|
||||
assert out_s.get("ok") is True
|
||||
assert (out_s.get("count") or 0) >= 1
|
||||
# get_cwd / cd
|
||||
# get_cwd
|
||||
out_cwd0 = get_cwd_tool().handler({})
|
||||
assert out_cwd0.get("ok") is True
|
||||
out_cd = cd_tool().handler({"cwd": "d"})
|
||||
assert out_cd.get("ok") is True
|
||||
out_cwd1 = get_cwd_tool().handler({})
|
||||
assert out_cwd1.get("ok") is True
|
||||
assert str(out_cwd1.get("cwd") or "").replace("\\", "/").endswith("/d")
|
||||
assert str(out_cwd0.get("cwd") or "").replace("\\", "/").rstrip("/") == str(tmp_path).replace("\\", "/").rstrip("/")
|
||||
# get_env / set_env
|
||||
out_get0 = get_env_tool().handler({"key": "LOCAL_PUBLIC_TOOLS_TEST_KEY", "default": "x"})
|
||||
assert out_get0.get("ok") is True
|
||||
|
|
@ -178,6 +171,34 @@ def test_p1_p2_read_tools_smoke(tmp_path: Path, monkeypatch) -> None:
|
|||
assert out_get1.get("value") == "y"
|
||||
|
||||
|
||||
def test_get_cwd_returns_workspace_root_when_at_data_workspace(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("OPS_WORKSPACE_ROOT", str(tmp_path))
|
||||
(tmp_path / "data" / "workspace").mkdir(parents=True, exist_ok=True)
|
||||
adapter = LocalAdapter()
|
||||
|
||||
out_cd = adapter.cd(cwd="data/workspace")
|
||||
assert out_cd.get("ok") is True
|
||||
|
||||
out_cwd = adapter.get_cwd()
|
||||
assert out_cwd.get("ok") is True
|
||||
norm = str(out_cwd.get("cwd") or "").replace("\\", "/").rstrip("/")
|
||||
assert norm == str(tmp_path).replace("\\", "/").rstrip("/")
|
||||
|
||||
|
||||
def test_get_cwd_returns_workspace_root_even_after_cd(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("OPS_WORKSPACE_ROOT", str(tmp_path))
|
||||
(tmp_path / "subdir").mkdir(parents=True, exist_ok=True)
|
||||
adapter = LocalAdapter()
|
||||
|
||||
out_cd = adapter.cd(cwd="subdir")
|
||||
assert out_cd.get("ok") is True
|
||||
|
||||
out_cwd = adapter.get_cwd()
|
||||
assert out_cwd.get("ok") is True
|
||||
norm = str(out_cwd.get("cwd") or "").replace("\\", "/").rstrip("/")
|
||||
assert norm == str(tmp_path).replace("\\", "/").rstrip("/")
|
||||
|
||||
|
||||
def test_run_command_does_not_follow_cd_state(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("OPS_WORKSPACE_ROOT", str(tmp_path))
|
||||
monkeypatch.setenv("AIA_ENABLE_RUN_COMMAND", "1")
|
||||
|
|
@ -194,8 +215,8 @@ def test_run_command_does_not_follow_cd_state(tmp_path: Path, monkeypatch) -> No
|
|||
|
||||
out_run = adapter.run_command(command='python -c "import os; print(os.path.basename(os.getcwd()))"', timeout=20)
|
||||
assert out_run.get("ok") is True, out_run
|
||||
# If run_command follows cd state this would be "subdir"; default should be data/workspace.
|
||||
assert str(out_run.get("cwd") or "").replace("\\", "/").rstrip("/").endswith("/data/workspace")
|
||||
# If run_command follows cd state this would be "subdir"; default should be workspace root.
|
||||
assert str(out_run.get("cwd") or "").replace("\\", "/").rstrip("/") == str(tmp_path).replace("\\", "/").rstrip("/")
|
||||
|
||||
|
||||
def test_run_command_reads_db_toggle_without_ops_db_env(tmp_path: Path, monkeypatch) -> None:
|
||||
|
|
|
|||
|
|
@ -14,3 +14,13 @@ def test_memory_tools_only_expose_canonical_names() -> None:
|
|||
assert all(not str(x).startswith("memory_curator_") for x in names)
|
||||
|
||||
|
||||
def test_generalist_also_exposes_memory_wiki_tools() -> None:
|
||||
specs = materialize_tool_specs(expert="generalist")
|
||||
names = {str(x.name or "") for x in specs}
|
||||
assert "memory_wiki_status" in names
|
||||
assert "memory_wiki_get" in names
|
||||
assert "memory_wiki_search" in names
|
||||
assert "memory_wiki_lint" in names
|
||||
assert "memory_wiki_apply" in names
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
|
||||
from oclaw.platform.config.paths import PROJECT_ROOT
|
||||
from oclaw.runtime.tools.experts.memory.wiki_tools import (
|
||||
from oclaw.runtime.tools.public.memory_wiki_tools import (
|
||||
memory_wiki_apply_tool,
|
||||
memory_wiki_get_tool,
|
||||
memory_wiki_lint_tool,
|
||||
|
|
|
|||
61
tests/test_session_bootstrap_handler.py
Normal file
61
tests/test_session_bootstrap_handler.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
def _load_handler_module():
|
||||
module_path = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "skills"
|
||||
/ "session-bootstrap"
|
||||
/ "hooks"
|
||||
/ "runtime"
|
||||
/ "handler.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("test_session_bootstrap_handler", str(module_path))
|
||||
assert spec is not None and spec.loader is not None
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def test_build_bootstrap_reads_skills_root_and_user_name(tmp_path: Path) -> None:
|
||||
mod = _load_handler_module()
|
||||
repo = tmp_path / "repo"
|
||||
ws = repo / "workspace"
|
||||
(repo / "skills" / "session-bootstrap").mkdir(parents=True)
|
||||
(repo / "data" / "wiki" / "users").mkdir(parents=True)
|
||||
ws.mkdir(parents=True)
|
||||
|
||||
(repo / "skills" / "session-bootstrap" / "SOUL.md").write_text("SOUL_OK", encoding="utf-8")
|
||||
(repo / "skills" / "session-bootstrap" / "IDENTITY.md").write_text("IDENT_OK", encoding="utf-8")
|
||||
(repo / "data" / "wiki" / "users" / "current.md").write_text("name: Alice", encoding="utf-8")
|
||||
|
||||
mod._repo_root = lambda: repo # type: ignore[attr-defined]
|
||||
event = SimpleNamespace(context={"workspaceDir": str(ws), "agentId": "generalist"})
|
||||
content = mod._build_bootstrap_content(event) # type: ignore[attr-defined]
|
||||
assert "SOUL_OK" in content
|
||||
assert "IDENT_OK" in content
|
||||
assert "欢迎回来,Alice" in content
|
||||
|
||||
|
||||
def test_handle_sets_bootstrap_diag(tmp_path: Path) -> None:
|
||||
mod = _load_handler_module()
|
||||
repo = tmp_path / "repo"
|
||||
(repo / "skills" / "session-bootstrap").mkdir(parents=True)
|
||||
(repo / "skills" / "session-bootstrap" / "SOUL.md").write_text("x", encoding="utf-8")
|
||||
(repo / "skills" / "session-bootstrap" / "IDENTITY.md").write_text("y", encoding="utf-8")
|
||||
mod._repo_root = lambda: repo # type: ignore[attr-defined]
|
||||
event = SimpleNamespace(
|
||||
type="agent",
|
||||
action="bootstrap",
|
||||
sessionKey="agent:main:main",
|
||||
context={"workspaceDir": str(tmp_path / "ws"), "bootstrapFiles": [], "agentId": "generalist"},
|
||||
)
|
||||
mod.handle(event)
|
||||
diag = event.context.get("sessionBootstrapDiag")
|
||||
assert isinstance(diag, dict)
|
||||
assert diag.get("soul_found") is True
|
||||
assert diag.get("identity_found") is True
|
||||
Loading…
Add table
Add a link
Reference in a new issue