From 5ef44f5a5e8bc75cd93908bba386d221370f6ecd Mon Sep 17 00:00:00 2001 From: mahaichuan-qq <2805100453@qq.com> Date: Thu, 5 Mar 2026 10:11:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E8=83=BDcheckpoint=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=EF=BC=8C=E5=BD=93=E5=89=8D=E4=BB=85stm=EF=BC=8C=E5=8F=AF?= =?UTF-8?q?=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + dare_framework/agent/react_agent.py | 3 +- dare_framework/checkpoint/PLAN.md | 166 -------- dare_framework/checkpoint/__init__.py | 48 +-- .../checkpoint/_internal/__init__.py | 1 - .../_internal/contributors/__init__.py | 1 - .../contributors/session_contributor.py | 69 --- .../_internal/contributors/stm_contributor.py | 41 -- .../contributors/workspace_git_contributor.py | 34 -- .../checkpoint/_internal/git_runner.py | 94 ----- .../checkpoint/_internal/memory_store.py | 25 -- .../checkpoint/_internal/save_restore.py | 64 --- dare_framework/checkpoint/factory.py | 39 -- dare_framework/checkpoint/interfaces.py | 74 ---- dare_framework/checkpoint/kernel.py | 399 ++++++++++++++++++ dare_framework/checkpoint/types.py | 104 ----- dare_framework/plan_v2/prompts.py | 19 +- .../plan_claude_code.py | 125 ++++-- .../skills/code-recon/SKILL.md | 3 +- 19 files changed, 511 insertions(+), 799 deletions(-) delete mode 100644 dare_framework/checkpoint/PLAN.md delete mode 100644 dare_framework/checkpoint/_internal/__init__.py delete mode 100644 dare_framework/checkpoint/_internal/contributors/__init__.py delete mode 100644 dare_framework/checkpoint/_internal/contributors/session_contributor.py delete mode 100644 dare_framework/checkpoint/_internal/contributors/stm_contributor.py delete mode 100644 dare_framework/checkpoint/_internal/contributors/workspace_git_contributor.py delete mode 100644 dare_framework/checkpoint/_internal/git_runner.py delete mode 100644 dare_framework/checkpoint/_internal/memory_store.py delete mode 100644 dare_framework/checkpoint/_internal/save_restore.py delete mode 100644 dare_framework/checkpoint/factory.py delete mode 100644 dare_framework/checkpoint/interfaces.py create mode 100644 dare_framework/checkpoint/kernel.py delete mode 100644 dare_framework/checkpoint/types.py diff --git a/.gitignore b/.gitignore index b0ebf02e..dce4383a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ examples/**/workspace/ workspace/ .cursorrules agentscope/ +openjiuwen/ # Environment variables and secrets .env diff --git a/dare_framework/agent/react_agent.py b/dare_framework/agent/react_agent.py index 64a6e09d..c12d33ef 100644 --- a/dare_framework/agent/react_agent.py +++ b/dare_framework/agent/react_agent.py @@ -278,7 +278,8 @@ async def _execute_basic( print(f"[{self.name}] 工具调用: {name} | id={tool_call_id or '-'} | args={params_preview}", flush=True) try: - result = await gateway.invoke(name, envelope=envelope, **params) + # 关键修复:将当前 Context 传递给 ToolGateway,使工具能看到 config/workspace_dir + result = await gateway.invoke(name, envelope=envelope, context=self._context, **params) except Exception as exc: await self._emit_transport_error( transport=transport, diff --git a/dare_framework/checkpoint/PLAN.md b/dare_framework/checkpoint/PLAN.md deleted file mode 100644 index ea484915..00000000 --- a/dare_framework/checkpoint/PLAN.md +++ /dev/null @@ -1,166 +0,0 @@ -# Checkpoint 现场保存与恢复 — 完整计划 - -本文档记录 DARE 框架内 Checkpoint 能力的完整设计:支持现场保存、现场恢复,可指定保存/恢复对象,并可对「Agent 操作对象(工作区文件)」用独立 Git 仓库管理,与用户 .git 完全解耦。 - ---- - -## 一、目标与范围 - -### 1.1 能力目标 - -- **现场保存**:将当前运行现场(或选定子集)保存为 Checkpoint,得到唯一 `checkpoint_id`,可持久化或仅内存。 -- **现场恢复**:根据 `checkpoint_id` 将已保存的现场写回当前运行时,使执行可从该现场继续。 -- **可指定对象**:调用方指定哪些「组件」参与本次保存、哪些参与本次恢复(白名单)。 -- **按对象开关**:每个组件可独立设置「是否参与保存」「是否参与恢复」(如只保存 STM+工作区文件、只恢复 STM)。 - -### 1.2 与现有能力的关系 - -- **IPlanAttemptSandbox**:保留,仍负责里程碑内 STM 快照/回滚/提交,不替代。 -- **IExecutionControl.checkpoint(label, payload)**:保留,用于 HITL 暂停点;Checkpoint 可在 pause 时额外做一次「现场保存」并记入 event log。 -- **Task.resume_from_checkpoint**:恢复时由调用方传入 `resume_from_checkpoint=checkpoint_id`,Session 初始化后先 `restore(checkpoint_id, scope)` 再进入循环。 - ---- - -## 二、参与保存/恢复的「对象」(组件键) - -可参与保存/恢复的组件用**组件键**表示,例如: - -| 组件键 | 含义 | -|--------|------| -| `stm` | 短期记忆(对话流等) | -| `session_state` | 会话状态(milestone 列表、当前索引等) | -| `session_context` | 会话上下文(config 快照、session_id 等) | -| `config` | 有效配置快照 | -| `plan_state` | 当前 ValidatedPlan / 当前 milestone 索引等 | -| `budget` | 预算使用情况 | -| `workspace_files` | 工作区中由 Agent 修改/新增的文件(见下文「工作区文件」) | - -**CheckpointScope** 为每个组件键提供: - -- `include_in_save`:该对象是否参与本次 save。 -- `include_in_restore`:该对象是否参与本次 restore。 - -支持预设(如 all、stm_only、stm_and_session、stm_and_workspace)和自定义组合。 - ---- - -## 三、工作区文件(Agent 操作对象)— 用 Git 管理 - -### 3.1 原则 - -- 不拷贝文件、不建「DARE 专用工作目录」再同步。 -- Agent 使用**独立 Git 仓库**,与用户 `.git` **完全解耦**;所有 Git 操作由框架自动完成,用户无感。 - -### 3.2 核心:仓库与工作区解耦 - -Git 的**仓库(--git-dir)**和**工作区(--work-tree)**是解耦的: - -- **仓库**:版本数据库,可放在任意路径(此处为 `.dare/agent.git`)。 -- **工作区**:要管理的文件所在目录,由执行命令时的 `--work-tree` 指定(此处为工程根 `.`)。 -- **管理范围**与「仓库所在目录层级」无关,只与 `--git-dir`、`--work-tree` 有关。因此 **`.dare/agent.git` 作为子目录,可以管理上级工程根目录的所有文件**。 - -### 3.3 目录与约定 - -- **用户仓库**:`<工程根>/.git`,用户照常使用,框架不触碰。 -- **Agent 仓库**:`<工程根>/.dare/agent.git`,裸仓(`git init --bare`),仅用于 Checkpoint。 -- **工作区**:两者都视「工程根」为工作区;Agent 通过每次命令显式指定 `--work-tree=.` 绑定工程根,无需移动用户 `.git`,也无须 worktree add。 - -### 3.4 排除规则(避免循环与误跟踪) - -在 Agent 仓中通过 **excludesfile**(或等价 .gitignore)排除: - -- `.git/` — 用户的 Git,不纳入 Agent 版本。 -- `.dare/` — 框架目录(含 agent.git 自身),不纳入,避免循环。 - -仅跟踪工程根下的业务文件(src/、config/、requirements.txt 等)。 - -### 3.5 关键命令(均由框架封装) - -**保存 Checkpoint(工作区文件部分)** - -```bash -git --git-dir=.dare/agent.git --work-tree=. add -A -git --git-dir=.dare/agent.git --work-tree=. commit -m "DARE Checkpoint: " -# 将返回的 commit_sha 记入 checkpoint 的 workspace_files 段 -``` - -**恢复 Checkpoint(工作区文件部分)** - -```bash -git --git-dir=.dare/agent.git --work-tree=. checkout -f . -``` - -- ``:checkpoint 中保存的 commit。 -- 最后的 `.` 表示将历史快照覆盖到工作区当前目录(工程根);`-f` 强制覆盖。 -- 用户的 `.git` 未被 Agent 跟踪,checkout 不会动它。 - -**清理未跟踪文件(可选)** - -```bash -git --git-dir=.dare/agent.git --work-tree=. clean -fdq -e .git -e .dare -``` - -- 删除 Agent 新增的、未跟踪的临时文件;`-e .git -e .dare` 排除用户仓和框架目录。 - -### 3.6 初始化(框架自动) - -- 若不存在 `.dare/agent.git`:创建 `.dare/`,执行 `git --git-dir=.dare/agent.git init --bare`,配置 excludesfile 排除 `.git/`、`.dare/`,可选配置 `user.name`/`user.email` 为框架标识。 - ---- - -## 四、接口与类型(设计要点) - -### 4.1 CheckpointScope - -- 表达「本次 save/restore 涉及哪些组件」及每个组件的 `include_in_save` / `include_in_restore`。 -- 支持预设(如 `ScopePresets.ALL`、`ScopePresets.STM_AND_WORKSPACE`)和按组件键自定义。 - -### 4.2 ICheckpointSaveRestore - -- `save(scope: CheckpointScope, ..., context: 运行时上下文) -> str` - 按 scope 采集选定组件状态,序列化为 Checkpoint,写入 Store(若配置),返回 `checkpoint_id`。 - 若 scope 含 `workspace_files`,则在工程根执行上述 Git 命令,将 commit_sha 写入 Checkpoint。 -- `restore(checkpoint_id: str, scope: CheckpointScope, ..., context: 运行时上下文) -> None` - 从 Store 加载 Checkpoint,按 scope 的恢复范围写回各组件。 - 若 scope 含 `workspace_files`,则从 Checkpoint 取 commit_sha,在工程根执行 checkout。 - -### 4.3 ICheckpointContributor - -- 每个参与组件实现一个 Contributor: - - **serialize**:从当前运行时采集该组件数据,返回可序列化结构。 - - **deserialize_and_apply**:从 Checkpoint 中该组件的数据写回当前运行时。 -- 引擎按 scope 调用对应 Contributor;`workspace_files` 的 Contributor 内部通过 Git 命令(或封装好的 Git 客户端)完成 save/restore,不落盘文件内容到 Checkpoint payload(只存 commit_sha)。 - -### 4.4 ICheckpointStore(可选) - -- 持久化抽象:`put(checkpoint_id, payload)`、`get(checkpoint_id)`、`delete(checkpoint_id)`。 -- 默认实现:内存 dict;可选实现:文件系统(如 `.dare/checkpoints/.json` 或目录)。 -- `workspace_files` 不把文件内容存 Store,只存 `git_commit`;Store 中 Checkpoint 的 payload 仅含各组件序列化结果及 `workspace_files: { "git_commit": "" }`。 - ---- - -## 五、与运行时的集成 - -- **Builder**:可注入 `ICheckpointSaveRestore`、`CheckpointScope` 预设、可选 `ICheckpointStore`;若启用 `workspace_files`,确保工程根可访问且可初始化 `.dare/agent.git`。 -- **Agent 编排层**:在适当时机调用 save(如显式 API、或 pause 时可选自动 save);恢复时若 `Task.resume_from_checkpoint` 存在则调用 `restore(checkpoint_id, scope)` 再进入 Session/Milestone 循环。 -- **workspace_dir**:从 Config 或 SessionContext 获取,作为 `--work-tree` 的基准(通常为 `.` 时的当前工作目录)。 - ---- - -## 六、实现任务概要 - -1. **类型与接口**:定义 `CheckpointScope`、`ICheckpointSaveRestore`、`ICheckpointContributor`、`ICheckpointStore`。 -2. **默认实现**:STM、SessionState、SessionContext(及可选 config、plan_state、budget)的 Contributor;默认 SaveRestore 按 scope 调用 Contributor 并写入 Store;内存 Store 与可选文件 Store。 -3. **workspace_files**:实现基于 Git 的 Contributor(封装 `--git-dir=.dare/agent.git --work-tree=.` 的 add/commit/checkout);Agent 仓初始化与 excludesfile 配置。 -4. **集成**:Builder 注入、编排层 save/restore 调用、`resume_from_checkpoint` 处理。 -5. **文档与测试**:单元测试(save 后 restore 状态一致;workspace_files 回滚后文件内容一致)。 - ---- - -## 七、总结 - -- **现场保存/恢复**:通过 CheckpointScope 指定对象,SaveRestore + Contributor + Store 完成采集与写回。 -- **工作区文件**:用独立 Git 仓 `.dare/agent.git` + `--work-tree=.` 直接管理工程根,不拷贝、不碰用户 `.git`,用户无感;所有 Git 操作由框架封装。 -- **两套 .git 各管各的**:用户用 `.git`,Agent 用 `.dare/agent.git`,通过 excludesfile 和显式 `--work-tree` 实现完全解耦。 - -以上为讨论后的完整计划,实现时以本目录下此文档为准。 diff --git a/dare_framework/checkpoint/__init__.py b/dare_framework/checkpoint/__init__.py index a6e5432d..6ef0f3de 100644 --- a/dare_framework/checkpoint/__init__.py +++ b/dare_framework/checkpoint/__init__.py @@ -1,36 +1,22 @@ -"""Checkpoint domain facade.""" +"""checkpoint - AgentState checkpoint 门面(当前仅包含 STM)。 -from dare_framework.checkpoint.interfaces import ( - ICheckpointContributor, - ICheckpointSaveRestore, - ICheckpointStore, -) -from dare_framework.checkpoint.types import ( - CheckpointContext, - CheckpointScope, - ScopePresets, -) -from dare_framework.checkpoint.factory import create_default_save_restore -from dare_framework.checkpoint._internal.memory_store import MemoryCheckpointStore -from dare_framework.checkpoint._internal.save_restore import DefaultCheckpointSaveRestore -from dare_framework.checkpoint._internal.contributors.stm_contributor import StmContributor -from dare_framework.checkpoint._internal.contributors.workspace_git_contributor import ( - WorkspaceGitContributor, -) -from dare_framework.checkpoint._internal.contributors.session_contributor import ( - SessionStateContributor, - SessionContextContributor, -) +当前版本: +- 定义通用 `AgentState` 抽象(目前只有 `stm` 字段); +- 提供可配置后端的 agent_state checkpoint 管理器(默认使用文件持久化 backend="file"); + +后续可以在不破坏接口形状的前提下,为 `AgentState` 增加更多字段。 +""" +from dare_framework.checkpoint.kernel import ( + AgentState, + AgentStateCheckpoint, + AgentStateCheckpointer, + CheckpointId, +) __all__ = [ - "ICheckpointContributor", - "ICheckpointSaveRestore", - "ICheckpointStore", - "CheckpointContext", - "CheckpointScope", - "ScopePresets", - "MemoryCheckpointStore", - "DefaultCheckpointSaveRestore", - "create_default_save_restore", + "CheckpointId", + "AgentState", + "AgentStateCheckpoint", + "AgentStateCheckpointer", ] diff --git a/dare_framework/checkpoint/_internal/__init__.py b/dare_framework/checkpoint/_internal/__init__.py deleted file mode 100644 index 05efbee1..00000000 --- a/dare_framework/checkpoint/_internal/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Checkpoint internal implementations.""" diff --git a/dare_framework/checkpoint/_internal/contributors/__init__.py b/dare_framework/checkpoint/_internal/contributors/__init__.py deleted file mode 100644 index 7112463b..00000000 --- a/dare_framework/checkpoint/_internal/contributors/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Checkpoint contributor implementations.""" diff --git a/dare_framework/checkpoint/_internal/contributors/session_contributor.py b/dare_framework/checkpoint/_internal/contributors/session_contributor.py deleted file mode 100644 index 2e81c307..00000000 --- a/dare_framework/checkpoint/_internal/contributors/session_contributor.py +++ /dev/null @@ -1,69 +0,0 @@ -"""session_state / session_context 组件贡献者:序列化会话状态与上下文.""" - -from __future__ import annotations - -from dataclasses import asdict -from typing import Any - -from dare_framework.checkpoint.interfaces import ICheckpointContributor -from dare_framework.checkpoint.types import ( - CheckpointContext, - SESSION_CONTEXT, - SESSION_STATE, -) - - -def _serialize_session_state(state: Any) -> dict | None: - if state is None: - return None - try: - return asdict(state) - except Exception: - return {"task_id": getattr(state, "task_id", None), "run_id": getattr(state, "run_id", None)} - - -def _serialize_session_context(sc: Any) -> dict | None: - if sc is None: - return None - try: - d = asdict(sc) - # Config 可能不可 asdict,简化为 None 或跳过 - if "config" in d and d["config"] is not None: - try: - d["config"] = asdict(d["config"]) - except Exception: - d["config"] = None - return d - except Exception: - return {"session_id": getattr(sc, "session_id", None), "task_id": getattr(sc, "task_id", None)} - - -class SessionStateContributor(ICheckpointContributor): - @property - def component_key(self) -> str: - return SESSION_STATE - - def serialize(self, ctx: CheckpointContext) -> dict | None: - return _serialize_session_state(ctx.session_state) - - def deserialize_and_apply(self, payload: Any, ctx: CheckpointContext) -> None: - if payload is None or ctx.session_state is None: - return - # 最小恢复:仅恢复可写字段,避免重建复杂嵌套类型 - if isinstance(payload, dict): - if "current_milestone_idx" in payload: - ctx.session_state.current_milestone_idx = payload["current_milestone_idx"] - - -class SessionContextContributor(ICheckpointContributor): - @property - def component_key(self) -> str: - return SESSION_CONTEXT - - def serialize(self, ctx: CheckpointContext) -> dict | None: - return _serialize_session_context(ctx.session_context) - - def deserialize_and_apply(self, payload: Any, ctx: CheckpointContext) -> None: - # SessionContext 多为只读/构造时确定,此处仅做序列化留存;恢复不改写 - if payload is None or ctx.session_context is None: - return diff --git a/dare_framework/checkpoint/_internal/contributors/stm_contributor.py b/dare_framework/checkpoint/_internal/contributors/stm_contributor.py deleted file mode 100644 index 69040777..00000000 --- a/dare_framework/checkpoint/_internal/contributors/stm_contributor.py +++ /dev/null @@ -1,41 +0,0 @@ -"""STM 组件贡献者:序列化/恢复短期记忆消息列表.""" - -from __future__ import annotations - -from dare_framework.checkpoint.interfaces import ICheckpointContributor -from dare_framework.checkpoint.types import CheckpointContext, STM -from dare_framework.context.types import Message - - -class StmContributor(ICheckpointContributor): - @property - def component_key(self) -> str: - return STM - - def serialize(self, ctx: CheckpointContext) -> list[dict]: - if ctx.context is None: - return [] - messages = ctx.context.stm_get() - return [ - { - "role": m.role, - "content": m.content, - "name": m.name, - "metadata": dict(m.metadata), - } - for m in messages - ] - - def deserialize_and_apply(self, payload: list, ctx: CheckpointContext) -> None: - if ctx.context is None: - return - ctx.context.stm_clear() - for item in payload or []: - if isinstance(item, dict): - msg = Message( - role=item.get("role", "user"), - content=item.get("content", ""), - name=item.get("name"), - metadata=dict(item.get("metadata") or {}), - ) - ctx.context.stm_add(msg) diff --git a/dare_framework/checkpoint/_internal/contributors/workspace_git_contributor.py b/dare_framework/checkpoint/_internal/contributors/workspace_git_contributor.py deleted file mode 100644 index 6afa7234..00000000 --- a/dare_framework/checkpoint/_internal/contributors/workspace_git_contributor.py +++ /dev/null @@ -1,34 +0,0 @@ -"""workspace_files 组件贡献者:通过 Agent 裸仓 Git 保存/恢复工作区文件.""" - -from __future__ import annotations - -from dare_framework.checkpoint._internal.git_runner import ( - add_and_commit, - checkout_commit, - ensure_agent_repo, -) -from dare_framework.checkpoint.interfaces import ICheckpointContributor -from dare_framework.checkpoint.types import CheckpointContext, WORKSPACE_FILES - - -class WorkspaceGitContributor(ICheckpointContributor): - """使用 .dare/agent.git + --work-tree 管理工程根;payload 仅存 git_commit.""" - - @property - def component_key(self) -> str: - return WORKSPACE_FILES - - def serialize(self, ctx: CheckpointContext) -> dict: - """执行 add+commit,返回 {"git_commit": ""}.""" - workspace_dir = ctx.workspace_dir or "." - # checkpoint_id 在 SaveRestore 层才生成,这里用占位;commit message 仅做标识 - sha = add_and_commit(workspace_dir, "DARE Checkpoint: workspace snapshot") - return {"git_commit": sha} - - def deserialize_and_apply(self, payload: dict, ctx: CheckpointContext) -> None: - """根据 git_commit checkout 工作区.""" - if not payload or "git_commit" not in payload: - return - workspace_dir = ctx.workspace_dir or "." - ensure_agent_repo(workspace_dir) - checkout_commit(workspace_dir, payload["git_commit"]) diff --git a/dare_framework/checkpoint/_internal/git_runner.py b/dare_framework/checkpoint/_internal/git_runner.py deleted file mode 100644 index 9f92c1ed..00000000 --- a/dare_framework/checkpoint/_internal/git_runner.py +++ /dev/null @@ -1,94 +0,0 @@ -"""封装 Agent 裸仓 Git 操作:--git-dir=.dare/agent.git --work-tree=.""" - -from __future__ import annotations - -import os -import subprocess -from pathlib import Path - - -# Agent 仓相对工作区根目录的路径 -AGENT_GIT_DIR = ".dare/agent.git" - -# excludesfile 内容:排除用户 .git 和 .dare,避免循环跟踪 -EXCLUDES_CONTENT = """# DARE checkpoint agent repo: do not track user .git or self -.git/ -.dare/ -""" - - -def _run_git( - workspace_dir: str, - args: list[str], - env_extra: dict[str, str] | None = None, -) -> subprocess.CompletedProcess[str]: - """在 workspace_dir 下执行 git,使用 Agent 裸仓与当前工作区.""" - env = os.environ.copy() - env["GIT_DIR"] = str(Path(workspace_dir).resolve() / AGENT_GIT_DIR) - env["GIT_WORK_TREE"] = str(Path(workspace_dir).resolve()) - if env_extra: - env.update(env_extra) - return subprocess.run( - ["git"] + args, - cwd=workspace_dir, - env=env, - capture_output=True, - text=True, - timeout=60, - ) - - -def ensure_agent_repo(workspace_dir: str) -> None: - """若不存在则创建 .dare/agent.git 裸仓并配置 excludesfile.""" - root = Path(workspace_dir).resolve() - git_dir = root / AGENT_GIT_DIR - if (git_dir / "HEAD").exists(): - return - git_dir.mkdir(parents=True, exist_ok=True) - # init --bare - r = subprocess.run( - ["git", "init", "--bare"], - cwd=str(root), - env={**os.environ, "GIT_DIR": str(git_dir)}, - capture_output=True, - text=True, - timeout=10, - ) - r.check_returncode() - # excludesfile - excludes_path = git_dir / "info" / "exclude" - excludes_path.parent.mkdir(parents=True, exist_ok=True) - excludes_path.write_text(EXCLUDES_CONTENT, encoding="utf-8") - # 可选:配置 user 以便 commit - _run_git(workspace_dir, ["config", "user.name", "DARE Checkpoint"]) - _run_git(workspace_dir, ["config", "user.email", "checkpoint@dare.local"]) - - -def add_and_commit(workspace_dir: str, message: str) -> str: - """add -A 并 commit,返回 commit SHA(40 字符)。无变更时返回当前 HEAD.""" - ensure_agent_repo(workspace_dir) - _run_git(workspace_dir, ["add", "-A"]) - r = _run_git(workspace_dir, ["commit", "-m", message]) - if r.returncode != 0 and "nothing to commit" in (r.stderr or ""): - # 无变更,当前状态已与上次 commit 一致,返回当前 HEAD - pass - elif r.returncode != 0: - r.check_returncode() - rev = _run_git(workspace_dir, ["rev-parse", "HEAD"]) - if rev.returncode != 0: - # 可能尚无任何 commit,做一次空提交 - _run_git(workspace_dir, ["commit", "--allow-empty", "-m", message]) - rev = _run_git(workspace_dir, ["rev-parse", "HEAD"]) - rev.check_returncode() - return rev.stdout.strip()[:40] - - -def checkout_commit(workspace_dir: str, commit_sha: str) -> None: - """强制将工作区恢复到指定 commit(不移动 HEAD 分支,只覆盖工作区文件)。""" - r = _run_git(workspace_dir, ["checkout", "-f", commit_sha, "--", "."]) - r.check_returncode() - - -def clean_untracked(workspace_dir: str) -> None: - """删除未跟踪的文件/目录,排除 .git 和 .dare.""" - _run_git(workspace_dir, ["clean", "-fdq", "-e", ".git", "-e", ".dare"]) diff --git a/dare_framework/checkpoint/_internal/memory_store.py b/dare_framework/checkpoint/_internal/memory_store.py deleted file mode 100644 index 1d613ae6..00000000 --- a/dare_framework/checkpoint/_internal/memory_store.py +++ /dev/null @@ -1,25 +0,0 @@ -"""In-memory checkpoint store.""" - -from __future__ import annotations - -from dare_framework.checkpoint.interfaces import ICheckpointStore -from dare_framework.checkpoint.types import CheckpointPayload - - -class MemoryCheckpointStore(ICheckpointStore): - """内存存储,进程内有效.""" - - def __init__(self) -> None: - self._store: dict[str, CheckpointPayload] = {} - - def put(self, checkpoint_id: str, payload: CheckpointPayload) -> None: - self._store[checkpoint_id] = dict(payload) - - def get(self, checkpoint_id: str) -> CheckpointPayload | None: - return dict(self._store[checkpoint_id]) if checkpoint_id in self._store else None - - def delete(self, checkpoint_id: str) -> bool: - if checkpoint_id in self._store: - del self._store[checkpoint_id] - return True - return False diff --git a/dare_framework/checkpoint/_internal/save_restore.py b/dare_framework/checkpoint/_internal/save_restore.py deleted file mode 100644 index 15481949..00000000 --- a/dare_framework/checkpoint/_internal/save_restore.py +++ /dev/null @@ -1,64 +0,0 @@ -"""默认 Checkpoint 保存/恢复实现:按 scope 调用各 Contributor 并读写 Store.""" - -from __future__ import annotations - -from uuid import uuid4 - -from dare_framework.checkpoint.interfaces import ( - ICheckpointContributor, - ICheckpointSaveRestore, - ICheckpointStore, -) -from dare_framework.checkpoint.types import ( - CheckpointContext, - CheckpointPayload, - CheckpointScope, -) - - -class DefaultCheckpointSaveRestore(ICheckpointSaveRestore): - def __init__( - self, - store: ICheckpointStore, - contributors: list[ICheckpointContributor], - ) -> None: - self._store = store - self._contributors = {c.component_key: c for c in contributors} - - def save( - self, - scope: CheckpointScope, - ctx: CheckpointContext, - ) -> str: - payload: CheckpointPayload = {} - for key in scope.keys_for_save(): - contrib = self._contributors.get(key) - if contrib is None: - continue - try: - payload[key] = contrib.serialize(ctx) - except Exception as e: - raise RuntimeError(f"Checkpoint save failed for component {key!r}") from e - checkpoint_id = uuid4().hex[:16] - self._store.put(checkpoint_id, payload) - return checkpoint_id - - def restore( - self, - checkpoint_id: str, - scope: CheckpointScope, - ctx: CheckpointContext, - ) -> None: - payload = self._store.get(checkpoint_id) - if payload is None: - raise LookupError(f"Checkpoint not found: {checkpoint_id!r}") - for key in scope.keys_for_restore(): - if key not in payload: - continue - contrib = self._contributors.get(key) - if contrib is None: - continue - try: - contrib.deserialize_and_apply(payload[key], ctx) - except Exception as e: - raise RuntimeError(f"Checkpoint restore failed for component {key!r}") from e diff --git a/dare_framework/checkpoint/factory.py b/dare_framework/checkpoint/factory.py deleted file mode 100644 index 75561c0f..00000000 --- a/dare_framework/checkpoint/factory.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Checkpoint factory helpers.""" - -from __future__ import annotations - -from dare_framework.checkpoint.interfaces import ( - ICheckpointContributor, - ICheckpointSaveRestore, - ICheckpointStore, -) -from dare_framework.checkpoint._internal.contributors.session_contributor import ( - SessionContextContributor, - SessionStateContributor, -) -from dare_framework.checkpoint._internal.contributors.stm_contributor import StmContributor -from dare_framework.checkpoint._internal.contributors.workspace_git_contributor import ( - WorkspaceGitContributor, -) -from dare_framework.checkpoint._internal.memory_store import MemoryCheckpointStore -from dare_framework.checkpoint._internal.save_restore import DefaultCheckpointSaveRestore - - -def create_default_save_restore( - store: ICheckpointStore | None = None, - *, - include_session_contributors: bool = True, -) -> ICheckpointSaveRestore: - """Create default save/restore with STM and workspace contributors.""" - - checkpoint_store = store if store is not None else MemoryCheckpointStore() - contributors: list[ICheckpointContributor] = [ - StmContributor(), - WorkspaceGitContributor(), - ] - if include_session_contributors: - contributors.extend([SessionStateContributor(), SessionContextContributor()]) - return DefaultCheckpointSaveRestore(store=checkpoint_store, contributors=contributors) - - -__all__ = ["create_default_save_restore"] diff --git a/dare_framework/checkpoint/interfaces.py b/dare_framework/checkpoint/interfaces.py deleted file mode 100644 index 140698ad..00000000 --- a/dare_framework/checkpoint/interfaces.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Checkpoint domain interfaces.""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Any - -from dare_framework.checkpoint.types import ( - CheckpointContext, - CheckpointPayload, - CheckpointScope, -) - - -class ICheckpointStore(ABC): - """Checkpoint 存储抽象:put/get/delete.""" - - @abstractmethod - def put(self, checkpoint_id: str, payload: CheckpointPayload) -> None: - """写入 checkpoint payload.""" - ... - - @abstractmethod - def get(self, checkpoint_id: str) -> CheckpointPayload | None: - """读取 checkpoint payload,不存在返回 None.""" - ... - - @abstractmethod - def delete(self, checkpoint_id: str) -> bool: - """删除指定 checkpoint,返回是否曾存在.""" - ... - - -class ICheckpointContributor(ABC): - """单个组件的序列化/反序列化贡献者.""" - - @property - @abstractmethod - def component_key(self) -> str: - """组件键,与 types.COMPONENT_KEYS 一致.""" - ... - - def serialize(self, ctx: CheckpointContext) -> Any: - """从运行时采集该组件数据,返回可序列化结构(可 JSON 化).""" - ... - - def deserialize_and_apply( - self, payload: Any, ctx: CheckpointContext - ) -> None: - """从 payload 写回该组件到运行时.""" - ... - - -class ICheckpointSaveRestore(ABC): - """现场保存与恢复入口.""" - - @abstractmethod - def save( - self, - scope: CheckpointScope, - ctx: CheckpointContext, - ) -> str: - """按 scope 采集状态并保存,返回 checkpoint_id.""" - ... - - @abstractmethod - def restore( - self, - checkpoint_id: str, - scope: CheckpointScope, - ctx: CheckpointContext, - ) -> None: - """从 checkpoint_id 按 scope 恢复状态.""" - ... diff --git a/dare_framework/checkpoint/kernel.py b/dare_framework/checkpoint/kernel.py new file mode 100644 index 00000000..ed01a59d --- /dev/null +++ b/dare_framework/checkpoint/kernel.py @@ -0,0 +1,399 @@ +"""checkpoint: agent_state checkpoint manager(当前仅包含 STM). + +设计目标: +- 抽象出一个可扩展的 `AgentState`,用于保存/恢复 Agent 运行时状态; +- 当前实现里 `AgentState` 只包含 STM(短期记忆),后续可以逐步加入 session_state、 + plan_state、workspace 快照等,而不破坏 checkpoint 的接口形状; +- 仍然保持 API 轻量:内存实现 + `save` / `restore` / `delete` / `list`, + 方便在任务过程中自动保存,并支持 `/resume ` 回滚到指定状态。 +""" + +from __future__ import annotations + +import json +import time +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, List +from uuid import uuid4 + +from dare_framework.context.types import Message + +if TYPE_CHECKING: + from dare_framework.context.kernel import IContext + + +CheckpointId = str + +# 单用户/单目录下最多保留的 checkpoint 数量,超出时删除最老的 +CHECKPOINT_LIMIT = 100 + + +@dataclass(frozen=True) +class AgentState: + """Agent 的运行时状态快照(当前仅包含 STM,预留扩展位)。""" + + stm: List["Message"] + # 未来可扩展字段示例(目前暂不启用,避免破坏最小实现): + # session_state: Any | None = None + # plan_state: Any | None = None + # workspace_diff: dict[str, Any] | None = None + + +@dataclass(frozen=True) +class AgentStateCheckpoint: + """单个 agent_state checkpoint 快照. + + Attributes: + checkpoint_id: 唯一 ID(十六进制字符串),用于 `/resume ` 等外部控制命令。 + created_at: 创建时间(time.time() 秒)。 + state: 当时的 AgentState 快照(当前仅 STM)。 + """ + + checkpoint_id: CheckpointId + created_at: float + state: AgentState + # 简短描述当时的现场(例如最近一条用户指令),用于 CLI/UI 展示与选择。 + summary: str = "" + + +class AgentStateCheckpointManager: + """管理 agent_state 的 checkpoint 管理器,支持多种持久化后端。 + + 后端通过构造参数指定: + - backend="memory":只在进程内内存中保存 checkpoint; + - backend="file"(默认):保存到指定目录的 JSON 文件,支持跨进程/重启恢复; + - backend="sqlite" 等其它值暂未实现,传入会抛出 ValueError。 + + 典型用法:: + + mgr = AgentStateCheckpointManager() + + cp_id = mgr.save(ctx) + ... + mgr.restore(cp_id, ctx) + """ + + def __init__( + self, + *, + backend: str = "file", + checkpoint_dir: str | Path = ".dare/agent_state_checkpoints", + max_checkpoints: int = CHECKPOINT_LIMIT, + ) -> None: + normalized = (backend or "memory").strip().lower() + if normalized not in {"memory", "file"}: + raise ValueError(f"unsupported agent_state checkpoint backend: {backend!r}") + if max_checkpoints < 1: + raise ValueError("max_checkpoints must be >= 1") + self._backend = normalized + self._max_checkpoints = max_checkpoints + self._checkpoints: Dict[CheckpointId, AgentStateCheckpoint] = {} + if self._backend == "file": + self._checkpoint_dir = Path(checkpoint_dir) + self._checkpoint_dir.mkdir(parents=True, exist_ok=True) + + # ------------------------------------------------------------------ + # 公共 API:save / restore / delete / get / list / clear + # ------------------------------------------------------------------ + + def save(self, ctx: "IContext") -> CheckpointId: + """保存当前 agent_state(当前仅 STM)状态,返回 checkpoint_id.""" + + checkpoint_id: CheckpointId = uuid4().hex[:16] + state = AgentState(stm=list(ctx.stm_get())) + checkpoint = AgentStateCheckpoint( + checkpoint_id=checkpoint_id, + created_at=time.time(), + state=state, + summary=_summarize_state(state), + ) + + if self._backend == "memory": + self._checkpoints[checkpoint_id] = checkpoint + else: # file backend + self._write_checkpoint(checkpoint) + self._trim_to_limit() + return checkpoint_id + + def restore(self, checkpoint_id: CheckpointId, ctx: "IContext") -> None: + """将 agent_state(当前仅 STM)回滚到指定 checkpoint_id. + + Raises: + KeyError: 当 checkpoint_id 不存在时。 + """ + + checkpoint = self.get(checkpoint_id) + if checkpoint is None: + raise KeyError(f"AgentState checkpoint not found: {checkpoint_id!r}") + + ctx.stm_clear() + for msg in checkpoint.state.stm: + ctx.stm_add(msg) + + def delete(self, checkpoint_id: CheckpointId) -> bool: + """删除指定 checkpoint,返回是否存在过.""" + + if self._backend == "memory": + return self._checkpoints.pop(checkpoint_id, None) is not None + + path = self._path_for(checkpoint_id) + if not path.exists(): + return False + try: + path.unlink() + except OSError: + return False + return True + + def get(self, checkpoint_id: CheckpointId) -> AgentStateCheckpoint | None: + """获取指定 checkpoint 元信息(不修改运行中上下文).""" + + if self._backend == "memory": + return self._checkpoints.get(checkpoint_id) + + path = self._path_for(checkpoint_id) + if not path.exists(): + return None + try: + raw = path.read_text(encoding="utf-8") + data = json.loads(raw) + except Exception: + return None + return self._deserialize_checkpoint(data) + + def list(self) -> list[AgentStateCheckpoint]: + """按创建时间升序返回所有 checkpoint.""" + + if self._backend == "memory": + return sorted(self._checkpoints.values(), key=lambda cp: cp.created_at) + + checkpoints: list[AgentStateCheckpoint] = [] + for file in sorted(self._checkpoint_dir.glob("*.json")): + try: + raw = file.read_text(encoding="utf-8") + data = json.loads(raw) + except Exception: + continue + checkpoint = self._deserialize_checkpoint(data) + if checkpoint is not None: + checkpoints.append(checkpoint) + return sorted(checkpoints, key=lambda cp: cp.created_at) + + def clear(self) -> None: + """清空所有 checkpoint(不修改当前 STM).""" + + if self._backend == "memory": + self._checkpoints.clear() + return + + for file in self._checkpoint_dir.glob("*.json"): + try: + file.unlink() + except OSError: + continue + + def _trim_to_limit(self) -> None: + """保留最近 _max_checkpoints 条,超出则按 created_at 删除最老的。""" + all_cp = self.list() + if len(all_cp) <= self._max_checkpoints: + return + to_remove = all_cp[: len(all_cp) - self._max_checkpoints] + for cp in to_remove: + self.delete(cp.checkpoint_id) + + # ------------------------------------------------------------------ + # 文件后端工具方法(backend == "file" 时使用) + # ------------------------------------------------------------------ + + def _path_for(self, checkpoint_id: CheckpointId) -> Path: + return self._checkpoint_dir / f"{checkpoint_id}.json" + + def _write_checkpoint(self, checkpoint: AgentStateCheckpoint) -> None: + payload = { + "checkpoint_id": checkpoint.checkpoint_id, + "created_at": checkpoint.created_at, + "state": self._serialize_state(checkpoint.state), + "summary": checkpoint.summary, + } + path = self._path_for(checkpoint.checkpoint_id) + path.write_text( + json.dumps(payload, ensure_ascii=False, sort_keys=True), + encoding="utf-8", + ) + + @staticmethod + def _serialize_state(state: AgentState) -> dict[str, Any]: + """将 AgentState 序列化为 JSON 友好的 dict.""" + + return { + "stm": [ + { + "role": m.role, + "content": m.content, + "name": m.name, + "metadata": dict(getattr(m, "metadata", {}) or {}), + } + for m in state.stm + ], + } + + @staticmethod + def _deserialize_state(data: dict[str, Any]) -> AgentState: + """从 dict 反序列化为 AgentState.""" + + stm_items = data.get("stm") or [] + messages: list[Message] = [] + for item in stm_items: + if not isinstance(item, dict): + continue + msg = Message( + role=item.get("role", "user"), + content=item.get("content", ""), + name=item.get("name"), + metadata=dict(item.get("metadata") or {}), + ) + messages.append(msg) + return AgentState(stm=messages) + + def _deserialize_checkpoint(self, data: dict[str, Any]) -> AgentStateCheckpoint | None: + checkpoint_id = data.get("checkpoint_id") + if not isinstance(checkpoint_id, str) or not checkpoint_id: + return None + created_at_raw = data.get("created_at", 0.0) + try: + created_at = float(created_at_raw) + except (TypeError, ValueError): + created_at = 0.0 + state_data = data.get("state") + if not isinstance(state_data, dict): + state = AgentState(stm=[]) + else: + state = self._deserialize_state(state_data) + raw_summary = data.get("summary") + summary = str(raw_summary).strip() if isinstance(raw_summary, str) else "" + if not summary: + summary = _summarize_state(state) + return AgentStateCheckpoint( + checkpoint_id=checkpoint_id, + created_at=created_at, + state=state, + summary=summary, + ) + + +def _summarize_state(state: AgentState) -> str: + """为 AgentState 生成简短描述(当前基于 STM 消息)。 + + 优先策略: + - 最近一条 role == "user" 且有内容的消息; + - 否则最后一条消息; + - 否则 "(无对话内容)"。 + 最长保留 60 个字符,多余部分使用 "..." 截断。 + """ + + messages = getattr(state, "stm", []) or [] + summary = "" + # 优先最近 user 消息 + for m in reversed(messages): + role = getattr(m, "role", "") + content = getattr(m, "content", "") or "" + if role == "user" and content.strip(): + summary = content.strip() + break + # 退而求其次:最后一条消息 + if not summary and messages: + summary = (messages[-1].content or "").strip() + if not summary: + summary = "(无对话内容)" + if len(summary) > 60: + summary = summary[:60] + "..." + return summary + + +class AgentStateCheckpointer: + """agent_state checkpoint 的单一入口,支持多后端与便捷构造。 + + 构造参数: + - backend: "memory" 仅进程内存;"file" 持久化到目录(默认)。 + - checkpoint_dir: 存储目录(file 后端时使用);与 user_dir 二选一。 + - user_dir: 用户目录,未传 checkpoint_dir 时使用 user_dir/.dare/agent_state_checkpoints; + 两者都不传时使用 Path.home()/.dare/agent_state_checkpoints。 + + 示例:: + + checkpointer = AgentStateCheckpointer(user_dir=str(Path.home())) + cp_id = checkpointer.save(ctx) + checkpointer.restore(cp_id, ctx) + checkpointer.list(print_to_stdout=True) + """ + + def __init__( + self, + *, + backend: str = "file", + checkpoint_dir: str | Path | None = None, + user_dir: str | Path | None = None, + ) -> None: + normalized = (backend or "file").strip().lower() + if normalized not in {"memory", "file"}: + raise ValueError(f"unsupported agent_state checkpoint backend: {backend!r}") + self._backend = normalized + if checkpoint_dir is not None: + self._dir = Path(checkpoint_dir) + elif user_dir is not None: + self._dir = Path(user_dir) / ".dare" / "agent_state_checkpoints" + else: + self._dir = Path.home() / ".dare" / "agent_state_checkpoints" + self._mgr = AgentStateCheckpointManager( + backend=normalized, + checkpoint_dir=self._dir, + ) + + @property + def checkpoint_dir(self) -> Path: + """当前 checkpoint 存储目录(file 后端时有效)。""" + return self._dir + + def save(self, ctx: "IContext") -> CheckpointId: + """保存当前 agent_state(当前仅 STM),返回 checkpoint_id。""" + return self._mgr.save(ctx) + + def restore(self, checkpoint_id: CheckpointId, ctx: "IContext") -> None: + """将 agent_state 回滚到指定 checkpoint_id。""" + self._mgr.restore(checkpoint_id, ctx) + + def get(self, checkpoint_id: CheckpointId) -> AgentStateCheckpoint | None: + """获取指定 checkpoint 元信息(不修改运行中上下文)。""" + return self._mgr.get(checkpoint_id) + + def delete(self, checkpoint_id: CheckpointId) -> bool: + """删除指定 checkpoint,返回是否曾存在。""" + return self._mgr.delete(checkpoint_id) + + def list(self, *, print_to_stdout: bool = False) -> list[AgentStateCheckpoint]: + """返回所有 checkpoint;print_to_stdout=True 时同时打印到 stdout。""" + checkpoints = self._mgr.list() + if print_to_stdout: + if not checkpoints: + print("[checkpoint] 当前没有可恢复的 checkpoint。", flush=True) + else: + print("\n[checkpoint] 可用 checkpoint(最新在下方):", flush=True) + for idx, cp in enumerate(checkpoints, 1): + ts = datetime.fromtimestamp(cp.created_at).strftime("%Y-%m-%d %H:%M:%S") + summary = (cp.summary or "").strip() or "(无对话内容)" + print(f" [{idx}] {cp.checkpoint_id} | {ts} | {summary}", flush=True) + return checkpoints + + def clear(self) -> None: + """清空所有 checkpoint(不修改当前 STM)。""" + self._mgr.clear() + + +__all__ = [ + "CheckpointId", + "AgentState", + "AgentStateCheckpoint", + "AgentStateCheckpointer", +] diff --git a/dare_framework/checkpoint/types.py b/dare_framework/checkpoint/types.py deleted file mode 100644 index 3fe7bed9..00000000 --- a/dare_framework/checkpoint/types.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Checkpoint domain types: scope, component keys, payload, runtime context.""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any - -# 组件键:与 PLAN.md 一致 -STM = "stm" -SESSION_STATE = "session_state" -SESSION_CONTEXT = "session_context" -CONFIG = "config" -PLAN_STATE = "plan_state" -BUDGET = "budget" -WORKSPACE_FILES = "workspace_files" - -COMPONENT_KEYS = ( - STM, - SESSION_STATE, - SESSION_CONTEXT, - CONFIG, - PLAN_STATE, - BUDGET, - WORKSPACE_FILES, -) - - -@dataclass -class ComponentScope: - """单个组件在 save/restore 中是否参与.""" - - include_in_save: bool = True - include_in_restore: bool = True - - -@dataclass -class CheckpointScope: - """本次 save/restore 涉及哪些组件及各自开关.""" - - components: dict[str, ComponentScope] = field(default_factory=dict) - - def include_in_save(self, key: str) -> bool: - c = self.components.get(key) - return c.include_in_save if c else False - - def include_in_restore(self, key: str) -> bool: - c = self.components.get(key) - return c.include_in_restore if c else False - - def keys_for_save(self) -> list[str]: - return [k for k in self.components if self.include_in_save(k)] - - def keys_for_restore(self) -> list[str]: - return [k for k in self.components if self.include_in_restore(k)] - - -class ScopePresets: - """预设的 CheckpointScope(每次返回新实例,避免共享可变状态).""" - - @staticmethod - def all() -> CheckpointScope: - return CheckpointScope( - components={k: ComponentScope(True, True) for k in COMPONENT_KEYS} - ) - - @staticmethod - def stm_only() -> CheckpointScope: - return CheckpointScope(components={STM: ComponentScope(True, True)}) - - @staticmethod - def stm_and_session() -> CheckpointScope: - return CheckpointScope( - components={ - STM: ComponentScope(True, True), - SESSION_STATE: ComponentScope(True, True), - SESSION_CONTEXT: ComponentScope(True, True), - } - ) - - @staticmethod - def stm_and_workspace() -> CheckpointScope: - return CheckpointScope( - components={ - STM: ComponentScope(True, True), - WORKSPACE_FILES: ComponentScope(True, True), - } - ) - - -# Checkpoint payload:各组件键 -> 可序列化 dict(JSON 友好) -CheckpointPayload = dict[str, Any] - - -@dataclass -class CheckpointContext: - """保存/恢复时传入的运行时上下文,供各 Contributor 读写.""" - - # 工程根目录,Git --work-tree 的基准 - workspace_dir: str = "." - # 以下为可选引用,Contributor 按需使用 - context: Any = None # IContext - session_state: Any = None # SessionState | None - session_context: Any = None # SessionContext | None - config: Any = None # Config | None diff --git a/dare_framework/plan_v2/prompts.py b/dare_framework/plan_v2/prompts.py index 49b05232..0664e014 100644 --- a/dare_framework/plan_v2/prompts.py +++ b/dare_framework/plan_v2/prompts.py @@ -1,25 +1,20 @@ """Plan Agent and sub-agent prompts.""" -PLAN_AGENT_SYSTEM_PROMPT = """You are a planning agent. You have tools to create, validate, revise, and finish a plan, and to delegate steps to sub-agents. Each sub-agent (e.g. sub_agent_general) is a tool: pass the step description as "task" and the step identifier as "step_id". -Before every action, check the [Plan State] block: it shows Plan Status, step statuses, Completed, Pending, and **NEXT**. Follow **NEXT** strictly; do not repeat completed steps. -When the user gives a task: -1. Call create_plan exactly once with plan_description (short summary) and steps: a list of objects, each with step_id (e.g. "step1"), description (what to do), and optional params (dict). Do NOT call create_plan again after success. -2. Call validate_plan(success=True) to confirm the plan. -3. For each step, call the matching sub-agent tool (e.g. sub_agent_general) with task= and step_id=. Delegate each step exactly once; do not repeat a completed step. Check the tool result's "progress" field: only delegate steps that are in "Pending", never repeat steps in "Completed". Execute steps in order. -4. If plan content must change, call revise_current_plan and then validate_plan(success=True) again. -5. When all steps are terminal, call finish_plan(target_state="done" or "abandoned") to close the plan. -Keep steps concise and ordered. Use sub-agent tools (sub_agent_general, sub_agent_special_*) to execute steps; do not execute steps yourself. +PLAN_AGENT_SYSTEM_PROMPT = """你是一个main_Agent。你可以自己来做任务,也可以把任务或者部分任务委托给sub_agent。 +每个sub_agent(例如 sub_agent_xxx)都通过工具暴露:若将任务委托给sub_agent,请将任务描述作为 "task",将步骤标识作为 "step_id" 传入。 ## 委托原则 -委托任务时task 只写:任务目标、交付件(绝对路径)、目标工程路径。**禁止**写执行步骤、指定具体文件。执行由 sub-agent 自决。 -审核交付件时请自己亲自阅读交付件审视结果。 +- 委托任务时 task 只写:任务目标、交付件、目标工程路径。**禁止**写执行步骤、指定具体文件。执行由 sub-agent 自决。 +- 审核交付件时请自己亲自阅读交付件并审视结果。 +- 所有调用的输入和输出中涉及到路径的一律用绝对路径!!! """ -SUB_AGENT_TASK_PROMPT = """你收到 Plan Agent 下发的任务(任务目标 + 交付件 + 目标路径)。按 skill 和工具自主执行,返回清晰结果。 +SUB_AGENT_TASK_PROMPT = """你收到 main_Agent 下发的任务(任务目标 + 交付件 + 目标路径)。按 skill 和工具自主执行,返回清晰结果。 ## 交付件 - 若指定了文件路径 → 必须 write_file 写入,不得只展示;同时还要自然语言对外说明白交付件位置 - 若交付是纯自然语言描述 → 在回复中产出,无需写文件 +- 所有调用的输入和输出中涉及到路径的一律用绝对路径!!! """ __all__ = ["PLAN_AGENT_SYSTEM_PROMPT", "SUB_AGENT_TASK_PROMPT"] diff --git a/examples/10-react-agent-code-recon/plan_claude_code.py b/examples/10-react-agent-code-recon/plan_claude_code.py index 1a621da3..11e106d2 100644 --- a/examples/10-react-agent-code-recon/plan_claude_code.py +++ b/examples/10-react-agent-code-recon/plan_claude_code.py @@ -5,6 +5,7 @@ import os import sys from dataclasses import replace +from datetime import datetime from enum import Enum from pathlib import Path @@ -24,6 +25,7 @@ PlannerState, SubAgentRegistry, ) +from dare_framework.checkpoint import AgentStateCheckpointer from dare_framework.tool._internal.tools import ( EditLineTool, ReadFileTool, @@ -33,9 +35,6 @@ WriteFileTool, ) -EXAMPLE_DIR = Path(__file__).resolve().parent -WORKSPACE_DIR = EXAMPLE_DIR / "workspace" - def _parse_args() -> argparse.Namespace: """解析命令行:目标工程路径。""" @@ -44,9 +43,7 @@ def _parse_args() -> argparse.Namespace: formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 示例: - python plan_claude_code.py # 目标工程 = 当前目录 - python plan_claude_code.py . # 同上 - python plan_claude_code.py D:/Agent/realesrgan/Real-ESRGAN + python plan_claude_code.py """, ) parser.add_argument( @@ -102,9 +99,9 @@ def _build_plan_prompt(workspace_dir: str, project_path: str) -> str: """构建主 Agent prompt(意图驱动),注入路径与 sub-agent 说明。""" return PLAN_AGENT_SYSTEM_PROMPT + f""" -【路径 - 全部用绝对路径】 -- 目标工程(只读): {project_path} -- 产出目录(可写): {workspace_dir} +【路径根目录】 +- 目标工程目录: {project_path} +- 产出内容目录: {workspace_dir} 【可用 sub-agent】委托格式见上方【委托原则】。 - sub_agent_recon:侦察。理解代码、回答问题、搜索、生成报告(可写 workspace)。 @@ -125,13 +122,20 @@ def _ensure_run_alias(agent: BaseAgent) -> BaseAgent: async def main() -> None: - args = _parse_args() - project_path = Path(args.project).resolve() - if not project_path.exists(): - print(f"Error: 目标工程路径不存在: {project_path}") - sys.exit(1) + # 路径约定(简化版,只有一个 workspace_dir): + # - workspace_dir = D:\Agent 作为工具沙箱根目录 + # - 目标工程目录 project_path_abs = 绝对路径(只读) + # - 交付产物目录 output_dir_abs = 绝对路径(可写) + # + # 所有工具调用一律使用绝对路径,由调用者自行区分「读项目」和「写产物」。 + + project_path_abs = "D:\\Agent\\realesrgan\\Real-ESRGAN\\realesrgan\\archs" + output_dir_abs = "D:\\Agent\\realesrgan\\Real-ESRGAN\\realesrgan\\dare" + workspace_dir_abs = "D:\\Agent\\realesrgan\\Real-ESRGAN\\realesrgan" # 工具沙箱根目录,必须同时包含 project / output + + Path(output_dir_abs).mkdir(parents=True, exist_ok=True) - api_key = os.getenv("OPENROUTER_API_KEY", "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") + api_key = os.getenv("OPENROUTER_API_KEY", "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") if not api_key: print("Error: OPENROUTER_API_KEY environment variable not set") sys.exit(1) @@ -139,8 +143,6 @@ async def main() -> None: model_name = os.getenv("OPENROUTER_MODEL", "moonshotai/kimi-k2.5") max_tokens = int(os.getenv("OPENROUTER_MAX_TOKENS", "4096")) - WORKSPACE_DIR.mkdir(parents=True, exist_ok=True) - model = OpenRouterModelAdapter( model=model_name, api_key=api_key, @@ -151,26 +153,12 @@ async def main() -> None: }, ) - # 全部用绝对路径,禁止相对路径 - workspace_dir_abs = str(WORKSPACE_DIR.resolve()) - project_path_abs = str(project_path.resolve()) - roots = [workspace_dir_abs, project_path_abs] - - class _ConfigWithRoots: - """包装 Config,添加 workspace_roots 供 file_utils 使用。""" - - def __init__(self, base: Config, workspace_roots: list[str]) -> None: - object.__setattr__(self, "_base", base) - object.__setattr__(self, "workspace_roots", workspace_roots) - - def __getattr__(self, name: str) -> object: - return getattr(self._base, name) - + # 单一 workspace_dir,所有工具只检查「路径是否在 workspace_dir_abs 之下」 base_config = Config( - workspace_dir=str(PROJECT_ROOT), + workspace_dir=workspace_dir_abs, user_dir=str(Path.home()), ) - base_config = _ConfigWithRoots(base_config, roots) + checkpointer = AgentStateCheckpointer(user_dir=base_config.user_dir) def _build_sub_prompt() -> str: return SUB_AGENT_TASK_PROMPT @@ -184,7 +172,9 @@ def _build_sub_prompt() -> str: ) # sub_agent_recon:只读 + write_file,加载 code-recon skill - _code_recon_skill_dir = EXAMPLE_DIR / "skills" / "code-recon" + _code_recon_skill_dir = Path( + "D:\\Agent\\darev0.1\\Deterministic-Agent-Runtime-Engine\\examples\\10-react-agent-code-recon\\skills\\code-recon" + ) _code_recon_skills = FileSystemSkillLoader(_code_recon_skill_dir).load() _code_recon_skill = _code_recon_skills[0] if _code_recon_skills else None @@ -192,7 +182,7 @@ def _build_sub_prompt() -> str: BaseAgent.react_agent_builder("sub_agent_recon") .with_model(model) .with_config(base_config) - .with_context_strategy("smart") + .with_context_strategy("basic") .with_prompt(sub_prompt) .with_sys_skill(_code_recon_skill) .with_skill_tool(False) # 使用固定 code-recon skill,不启用 search_skill @@ -207,7 +197,7 @@ def _build_sub_prompt() -> str: BaseAgent.react_agent_builder("sub_agent_coder") .with_model(model) .with_config(base_config) - .with_context_strategy("smart") + .with_context_strategy("basic") .with_prompt(sub_prompt) .add_tools(ReadFileTool(), WriteFileTool(), SearchCodeTool(), EditLineTool()) .build() @@ -219,7 +209,7 @@ def _build_sub_prompt() -> str: BaseAgent.react_agent_builder("sub_agent_runner") .with_model(model) .with_config(base_config) - .with_context_strategy("smart") + .with_context_strategy("basic") .with_prompt(sub_prompt) .add_tools(RunCommandTool(), ReadFileTool()) .build() @@ -249,17 +239,18 @@ def _build_sub_prompt() -> str: plan_prompt = Prompt( prompt_id="plan-agent.system", role="system", - content=_build_plan_prompt(workspace_dir_abs, project_path_abs), + content=_build_plan_prompt(output_dir_abs, project_path_abs), supported_models=[], order=0, ) - plan_config = _ConfigWithRoots(replace(base_config._base, mcp_paths=[]), roots) + # plan-agent 使用与 sub-agents 相同的 workspace 规则 + plan_config = replace(base_config, mcp_paths=[]) plan_agent = await ( BaseAgent.react_agent_builder("plan-agent") .with_config(plan_config) .with_model(model) - .with_context_strategy("smart") + .with_context_strategy("basic") .with_prompt(plan_prompt) .with_plan_provider(planner) .add_tools(ReadFileTool()) @@ -272,7 +263,7 @@ def _build_sub_prompt() -> str: print(" Sub-agents: sub_agent_recon (只读) | sub_agent_coder (可写) | sub_agent_runner (可执行)") print(f" Model: {model_name}") print(f" 目标工程: {project_path_abs}") - print(f" Workspace: {workspace_dir_abs}") + print(f" 产物路径: {output_dir_abs}") _print_help(project_path_abs) print("-" * 60) @@ -286,6 +277,49 @@ def _build_sub_prompt() -> str: if not raw: continue + # 优先处理 /resume 命令:基于 checkpoint 恢复 agent_state(当前仅 STM) + if raw.startswith("/resume"): + parts = raw.split(maxsplit=1) + checkpoint_id: str | None = None + + # /resume(无参数):先列出可选 checkpoint,再由用户选择 + if len(parts) < 2 or not parts[1].strip(): + checkpoints = checkpointer.list(print_to_stdout=True) + if not checkpoints: + continue + choice = input("请输入要恢复的 checkpoint 序号或 ID(回车取消): ").strip() + if not choice: + continue + + # 支持按序号选择 + if choice.isdigit(): + index = int(choice) + if 1 <= index <= len(checkpoints): + checkpoint_id = checkpoints[index - 1].checkpoint_id + # 或者按 ID / 前缀选择 + if checkpoint_id is None: + for cp in checkpoints: + if cp.checkpoint_id.startswith(choice): + checkpoint_id = cp.checkpoint_id + break + if checkpoint_id is None: + print("未找到匹配的 checkpoint。", flush=True) + continue + + # /resume :直接按提供的 ID 恢复 + if checkpoint_id is None: + checkpoint_id = parts[1].strip() + + try: + checkpointer.restore(checkpoint_id, plan_agent.context) + except KeyError: + print(f"Checkpoint 不存在: {checkpoint_id}", flush=True) + except Exception as exc: + print(f"恢复 checkpoint 失败: {exc}", flush=True) + else: + print(f"已从 checkpoint {checkpoint_id} 恢复 agent_state(当前仅 STM)。", flush=True) + continue + parsed = _parse_command(raw) if isinstance(parsed, CommandType): if parsed is CommandType.QUIT: @@ -306,6 +340,13 @@ def _build_sub_prompt() -> str: if result.errors: print(f"Errors: {result.errors}", flush=True) + # 每轮任务结束后自动保存一次 agent_state checkpoint(当前仅 STM),便于后续 /resume 回到本轮结束现场 + try: + checkpoint_id = checkpointer.save(plan_agent.context) + print(f"[checkpoint] 本轮任务结束后已保存 agent_state checkpoint: {checkpoint_id}", flush=True) + except Exception as exc: + print(f"[checkpoint] 自动保存 checkpoint 失败: {exc}", flush=True) + if __name__ == "__main__": asyncio.run(main()) diff --git a/examples/10-react-agent-code-recon/skills/code-recon/SKILL.md b/examples/10-react-agent-code-recon/skills/code-recon/SKILL.md index bc3a95df..86478d97 100644 --- a/examples/10-react-agent-code-recon/skills/code-recon/SKILL.md +++ b/examples/10-react-agent-code-recon/skills/code-recon/SKILL.md @@ -7,7 +7,8 @@ description: 代码侦查流程。对目标工程做结构化探索、读取关 ## 工作流程 -1. **search_file**:对目标目录调用一次,pattern `*` 或 `**/*`,得到 paths。paths 中没有的即项目不存在。 +0. 所有调用的输入和输出中涉及到路径的一律用绝对路径!!! +1. **search_file**:对目标目录执行工具得到 paths。paths 中没有的即项目不存在。特别重要,只对目标目录及其子目录进行侦查,不允许看其他目录。 2. 从 paths 中选关键文件读取,path 从 paths 原样复制。 3. **循环**:读一批代码文件 → 读入当前交付件 → 综合分析 → 更新交付件写入 → 读下一批 → 更新交付件写入。完成交付件结束。 4. 任务指定了报告路径时,必须 write_file 落盘。