From 605ae1b7fd5ca0ab67c32e429201fb8ba8685985 Mon Sep 17 00:00:00 2001 From: Darkatse <39111369+Darkatse@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:20:05 -0400 Subject: [PATCH] refactor(agent): commit live workspace without checkpoints --- docs/API/Agent.md | 74 ++------ docs/API/README.md | 4 +- docs/Agent/Handoff.md | 2 +- docs/Agent/LlmGateway.md | 2 +- docs/Agent/ProfilesAndPreset.md | 2 +- docs/Agent/README.md | 2 +- docs/Agent/RunEventJournal.md | 32 +--- docs/Agent/SubAgent.md | 2 +- docs/Agent/TestingStrategy.md | 11 +- docs/Agent/ToolSystem.md | 31 +--- docs/Agent/Workspace.md | 123 ++----------- docs/AgentArchitecture.md | 47 +++-- docs/AgentContract.md | 27 ++- docs/AgentImplementPlan.md | 24 +-- docs/CurrentState/AgentFramework.md | 25 +-- docs/CurrentState/Sync.md | 4 +- docs/FrontendHostContract.md | 2 +- .../src/app/composition/repositories.rs | 4 - .../src/app/composition/services/agent.rs | 1 - .../contract_tests/agent_runtime/execution.rs | 38 ++-- .../tauritavern/src/app/contract_tests/mod.rs | 2 - .../file_agent_repository/checkpoint_store.rs | 166 ------------------ .../repositories/file_agent_repository/mod.rs | 3 - .../file_agent_repository/paths.rs | 1 - .../file_agent_repository/tests.rs | 22 +-- .../tt-application/src/dto/agent_dto.rs | 2 - .../src/services/agent_runtime_service.rs | 4 - .../services/agent_runtime_service/commit.rs | 73 ++------ .../services/agent_runtime_service/journal.rs | 35 +--- .../agent_runtime_service/lifecycle.rs | 16 +- .../agent_runtime_service/loop_runner.rs | 113 ++++++------ .../crates/tt-domain/src/models/agent/mod.rs | 21 --- .../tt-domain/src/models/agent/storage.rs | 6 - .../src/repositories/checkpoint_repository.rs | 23 --- .../crates/tt-ports/src/repositories/mod.rs | 1 - .../main/api/agent-chat-commit-bridge.js | 7 +- src/tauri/main/api/agent-run-runtime.js | 5 +- src/tauri/main/api/agent.js | 6 - src/types.d.ts | 3 - tests/agent-api-contract.test.mjs | 16 +- tests/rust-composition-contract.test.mjs | 2 +- 41 files changed, 210 insertions(+), 774 deletions(-) delete mode 100644 src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/checkpoint_store.rs delete mode 100644 src-tauri/crates/tt-ports/src/repositories/checkpoint_repository.rs diff --git a/docs/API/Agent.md b/docs/API/Agent.md index 482f6fe3..fd9cffe1 100644 --- a/docs/API/Agent.md +++ b/docs/API/Agent.md @@ -2,7 +2,7 @@ 本文档是 Agent Host ABI 当前参考。Run 控制入口属于 Public Contract;明确标注为 Project Contract 的 UI/诊断投影可以随 Agent 实验能力演进,不等同于 Rust 内部 service/repository。 -状态:当前已实现 canonical model IR、provider native metadata 保真、provider_state continuation、上下文只读工具、Skill tools、workspace 读改工具循环、前端 dryRun adapter、Agent run history listing、Agent run retention facade / dry-run plan preview / manual apply prune / backend auto prune,以及 Agent Profile 独立 preset / 独立 model 的 Frontend PromptAssemblyBroker 链路。Agent System 扩展开关开启时,普通发送、`/trigger`、regenerate 与 overswipe 新候选生成会通过 Legacy Generate 兼容桥启动 Agent;普通切换已有 swipe 候选不启动 Agent。本文以当前已落地 Host ABI 为准,并在后续章节保留 readDiff/rollback/approval 等未来设计。 +状态:当前已实现 canonical model IR、provider native metadata 保真、provider_state continuation、上下文只读工具、Skill tools、workspace 读改工具循环、前端 dryRun adapter、Agent run history listing、Agent run retention facade / dry-run plan preview / manual apply prune / backend auto prune,以及 Agent Profile 独立 preset / 独立 model 的 Frontend PromptAssemblyBroker 链路。Agent System 扩展开关开启时,普通发送、`/trigger`、regenerate 与 overswipe 新候选生成会通过 Legacy Generate 兼容桥启动 Agent;普通切换已有 swipe 候选不启动 Agent。本文只记录当前已落地 Host ABI。 `provider_state` 是 Rust 后端内部 continuation contract,不是 Host ABI。前端/扩展不应读写 `_tauritavern_provider_state`;需要诊断时通过 run events、`modelResponsePath` 与 LLM API log 观察。 模型回合详情必须通过 `readModelTurn()` 读取后端投影 DTO;前端不解析 `model-responses/` raw JSON。 @@ -74,8 +74,6 @@ type TauriTavernAgentApi = { runs: AgentRunSummary[]; nextCursor?: { createdAt: string; runId: string }; }>; - readDiff(): never; - rollback(): never; }; ``` @@ -86,7 +84,7 @@ type TauriTavernAgentApi = { - `startRunFromLegacyGenerate()`:从当前 Legacy Generate dryRun 兼容桥启动。 - `startRunWithPromptSnapshot()`:调用方已经持有 prompt snapshot 时启动。 -`approveToolCall()`、`readDiff()`、`rollback()` 已预留名称,但当前实现会显式 throw,避免静默降级。 +`approveToolCall()` 已预留名称,但当前实现会显式 throw,避免静默降级。 ## 3. startRunFromLegacyGenerate @@ -369,7 +367,6 @@ type AgentRunTimelineDelegationEdge = { type AgentReadWorkspaceFileInput = { runId: string; path: string; - checkpointId?: string; }; type AgentWorkspaceFile = { @@ -381,8 +378,7 @@ type AgentWorkspaceFile = { }; ``` -路径必须是 workspace relative path。非法路径直接 reject。 -不传 `checkpointId` 时读取当前 run workspace;传入时读取该 run 对应 checkpoint 的不可变 UTF-8 文本。模型侧读取应使用 `workspace_read_file` 工具,前端/扩展侧读取应使用本方法。 +路径必须是 workspace relative path。非法路径直接 reject。该方法读取当前 run workspace;模型侧读取应使用 `workspace_read_file` 工具,前端/扩展侧读取应使用本方法。 ## 10. readModelTurn(Project Contract) @@ -630,50 +626,7 @@ type AgentListToolsResult = { Model alias 属于 invocation snapshot,不进入此 DTO。Profile v3 的所有 tool-keyed 字段只接受 canonical ToolId:builtin 为 `builtin:`,MCP 为 `mcp/:`;v1/v2 载入后一次性迁移并写回,不保留 shorthand 双语法。 -## 14. readDiff - -当前未实现 diff;`readDiff()` 会显式 throw。 - -```ts -type AgentReadDiffInput = { - runId: string; - fromCheckpointId?: string; - toCheckpointId?: string; - paths?: string[]; -}; - -type AgentDiff = { - runId: string; - fromCheckpointId?: string; - toCheckpointId?: string; - files: Array<{ - path: string; - status: 'added' | 'modified' | 'deleted' | 'unchanged'; - unifiedDiff?: string; - }>; -}; -``` - -第一期可以只支持文本 artifact 的 diff。 - -## 15. rollback - -当前未实现 rollback;`rollback()` 会显式 throw。 - -```ts -type AgentRollbackInput = { - runId: string; - checkpointId: string; - scope?: 'workspace' | 'committed-message'; -}; -``` - -语义: - -- `workspace`:只恢复 run workspace,不修改 chat。 -- `committed-message`:重组 artifact 并修改已提交聊天消息,必须走 chat 保存契约。 - -## 16. commit +## 14. commit ```ts type AgentCommitInput = { @@ -689,18 +642,17 @@ type AgentCommitResult = { Chat commit 不是公开 Host API 方法,而是 runtime Committer 与 host bridge 的内部握手: -- 前台 root / handoff Agent 在首次成功显式 commit 前,对 `.md`、`.markdown`、`.txt`、`.text` 文件的成功 write/patch 会自动触发 `replace` commit;路径所在 root 不参与判断。 +- 前台 root / handoff Agent 在首次成功显式 commit 前,会在每轮 tool calls 全部处理后检查该轮最后一次成功的 write/patch;若目标是 `.md`、`.markdown`、`.txt` 或 `.text` 文件,则自动触发一次 `replace` commit。每轮最多自动 commit 一次,路径所在 root 不参与判断。 - 模型也可调用 `workspace.commit`,无参数时默认 `replace output/main.md`;第一次 host-confirmed 显式 commit 后停止当前 run 的自动 commit。 -- 自动提交复用 write/patch 已创建的不可变 checkpoint,并与显式 commit 共用 event 和 host-confirmation 路径。 - 前端 host bridge 校验当前 active chat 与 run 的 `chatRef/stableChatId` 一致。 -- bridge 从 `chat_commit_requested.checkpointId` 读取并校验正文,同时通过 `readModelTurn()` 读取 root / handoff active chain 的可见 reasoning 投影。bridge 先应用上游生成输出保存前后处理,再通过上游 `saveReply()` 写入聊天、累计到标准 `extra.reasoning`,最后调用 `resolve_agent_chat_commit`。 -- Host 握手只有“已确认提交 / 未确认提交”两种结果且不设置 wall-clock timeout,移动端挂起期间可以继续等待,取消 run 仍会终止等待。未确认提交统一作为可恢复失败且不写 preservation ledger;自动提交继续运行,显式提交返回模型可见 tool error 以便重试。若 `saveReply()` 已成功改写 `chat[]`,正文与 reasoning 会保留,本地 reasoning cursor 同步推进以避免重试时重复追加。Rust 侧 checkpoint、journal、状态机等错误仍 fail-fast。 +- bridge 读取当前 workspace 文件并校验 `chat_commit_requested.sha256`,内容已变化时拒绝提交;同时通过 `readModelTurn()` 读取 root / handoff active chain 的可见 reasoning 投影。bridge 先应用上游生成输出保存前后处理,再通过上游 `saveReply()` 写入聊天、累计到标准 `extra.reasoning`,最后调用 `resolve_agent_chat_commit`。 +- Host 握手只有“已确认提交 / 未确认提交”两种结果且不设置 wall-clock timeout,移动端挂起期间可以继续等待,取消 run 仍会终止等待。未确认提交统一作为可恢复失败且不写 preservation ledger;自动提交继续运行,显式提交返回模型可见 tool error 以便重试。若 `saveReply()` 已成功改写 `chat[]`,正文与 reasoning 会保留,本地 reasoning cursor 同步推进以避免重试时重复追加。Rust 侧 journal、状态机等错误仍 fail-fast。 - `chat_commit_requested` 不携带 `persistStateId`;该字段只能在 `workspace.finish` 成功提交 persistent state 后,由 `persistent_state_metadata_update_requested` / `resolve_agent_persistent_state_metadata_update` 写回同一条 chat message。 - 首次 commit 按 generation type 创建或改写目标楼层;后续 commit 使用 `appendFinal` 写入完整 postprocessed 目标文本。`append` mode 会把本次读取到的文件文本作为 raw 追加贡献累计后再整体处理,避免片段级 regex。 - `append` 在本 run 尚无 commit 时不会报错,会创建本 run 的消息楼层。 - 自动和显式 commit 都进入 preservation ledger;任何一个成功后发生错误都会保留 chat 输出并进入 partial success。前台 run 在 `workspace.finish` 前仍必须至少成功一次显式 `workspace.commit`;后台 run 可无 chat commit 完成。 -## 17. Event Envelope +## 15. Event Envelope ```ts type AgentRunEvent = { @@ -718,7 +670,7 @@ type AgentRunEvent = { Agent event 不等同 SillyTavern `eventSource` 事件,不得伪装成 `GENERATION_*` 或 `TOOL_CALLS_*`。 -## 18. Errors +## 16. Errors 错误建议结构: @@ -748,7 +700,7 @@ commit.cursor_integrity commit.save_failed ``` -## 19. Rust Command +## 17. Rust Command ```text start_agent_run(dto) @@ -774,7 +726,7 @@ read_agent_diff(dto) rollback_agent_run(dto) ``` -## 20. Compatibility +## 18. Compatibility Agent Mode off: @@ -789,7 +741,7 @@ Agent Mode on: - dryRun 不返回 payload;Agent adapter 通过事件捕获 payload。 - Agent tool loop 不递归 `Generate()`。 -## 21. 当前工具与手动验证 +## 19. 当前工具与手动验证 当前开放十四个非 delegation 内建工具: @@ -816,7 +768,7 @@ Profile 显式选择且在 MCP Manager 中为 Ask/Allow 的 MCP 工具会加入 `AgentToolResult.structured`、`error_code` 与 `resource_refs` 只供 runtime、audit 与 Timeline UI 使用,不作为字段发送给模型。模型看到直接 Text/Markdown;可恢复错误增加 `## Tool error`。MCP text 保持原文,structured content 去重后转为 `## Details` Markdown,可操作的非文本 content diagnostic 转为 `## Notes`,metadata diagnostic 不进入模型。该 `content` 超过当前 Profile 的 `tools.mcpResultInlineCharLimit`(默认 50,000)时,完整 JSON 已先以 create-only 语义保存在内部 audit,runtime 另生成同一完整 Markdown 的行可读 `.txt` 视图。模型收到最多前 3,000 个 Unicode 字符的前缀预览、可读视图路径、字符数及 `workspace_read_file` / `workspace_search_files` 的分段读取指引,不接收 audit JSON 路径。`.txt` 视图会换行超长物理行,精确原始结果仍在 JSON audit 中。该值必须为正整数,并随 resolved Profile 固定到 invocation。`tool-results/` 是所有 invocation 可见但永远不可写的 run root。当前仍不存在 shell 或 extension bridge 工具。 -模型可修正的工具错误会作为 `is_error = true` tool result 回填下一轮。宿主级 IO、journal、checkpoint、序列化、取消和模型响应结构错误仍然让 run failed。 +模型可修正的工具错误会作为 `is_error = true` tool result 回填下一轮。宿主级 IO、journal、序列化、取消和模型响应结构错误仍然让 run failed。 推荐最小启动: diff --git a/docs/API/README.md b/docs/API/README.md index 5736581f..8f6c6e29 100644 --- a/docs/API/README.md +++ b/docs/API/README.md @@ -31,9 +31,9 @@ const api = host?.api; - 面向需要**全局持久化**的扩展(不绑定 chat)。 - 提供 Extension KV JSON + Blob 存储,支持多 table。 - `api.agent`(已落地 canonical model IR、provider_state continuation、上下文只读工具、workspace 读改工具循环与 run history listing) - - 面向 Agent Run / timeline / workspace / checkpoint / commit。 + - 面向 Agent Run / timeline / workspace / commit。 - 当前提供启动 run、列出历史 run、订阅/读取事件、取消、读取 workspace 文件、prepare/finalize/commit;模型侧内建 chat search/read、world info read、skill list/read 与 workspace list/read/write/patch/finish 工具由 Rust runtime 注册。 - - approval、readDiff、rollback 仍是后续工作;当前入口显式 throw。 + - approval 仍是后续工作;当前入口显式 throw。 - `api.llmConnections` - 面向 Agent Profile / 扩展侧的 LLM 连接定义管理。 - 当前提供 list、load、save、delete;Profile 通过 `connectionRef + modelId` 引用连接,不直接依赖 Connection Manager 的 Model Target id。 diff --git a/docs/Agent/Handoff.md b/docs/Agent/Handoff.md index 8f736a4f..0585d3a8 100644 --- a/docs/Agent/Handoff.md +++ b/docs/Agent/Handoff.md @@ -31,7 +31,7 @@ finalizer verifies, commits, finish ## 2. 核心不变量 -- Handoff 仍在同一个 `AgentRun` 内运行,共享 run workspace、journal、cancel、checkpoint、commit ledger 与 persistent projection。 +- Handoff 仍在同一个 `AgentRun` 内运行,共享 run workspace、journal、cancel、commit ledger 与 persistent projection。 - 每个 handoff stage 是独立 `AgentInvocation`,拥有自己的 provider session id、target Profile、冻结 tool snapshot/turn、Skill 解析与 prompt assembly。 - `agent.handoff` 是模型可见工具,`TransferControl` 是 runtime 内部 continuation。不要把 `continuation`、`invocationId` 或 provider state 暴露给模型参数。 - 当前 Agent 成功 handoff 后必须停止工具调用。runtime 将当前 invocation 记为 `Transferred`。 diff --git a/docs/Agent/LlmGateway.md b/docs/Agent/LlmGateway.md index a4580e50..c882b7f4 100644 --- a/docs/Agent/LlmGateway.md +++ b/docs/Agent/LlmGateway.md @@ -243,7 +243,7 @@ Provider stream 来自 ChatCompletionService/Repository 的 SSE data 或 normalized chunk。 Agent run event stream - AgentRunEvent:model_delta、tool_call_requested、checkpoint_created 等语义事件。 + AgentRunEvent:model_delta、tool_call_requested、chat_commit_requested 等语义事件。 ``` Agent UI 必须订阅 `api.agent.subscribe(runId, handler)` 的 run event,不直接消费 provider raw stream。 diff --git a/docs/Agent/ProfilesAndPreset.md b/docs/Agent/ProfilesAndPreset.md index 253b68fb..cf4b6ef7 100644 --- a/docs/Agent/ProfilesAndPreset.md +++ b/docs/Agent/ProfilesAndPreset.md @@ -311,7 +311,7 @@ Agent 可以创建和修改计划。 运行时仍必须要求: - 先产出 plan。 -- 每个阶段结束 checkpoint。 +- 每个阶段结束时记录 journal 状态。 - 完成前检查 artifact manifest。 - 不能突破全局 tool/resource/budget policy。 diff --git a/docs/Agent/README.md b/docs/Agent/README.md index 5eca43d2..0448665a 100644 --- a/docs/Agent/README.md +++ b/docs/Agent/README.md @@ -10,7 +10,7 @@ | 文档 | 内容 | | --- | --- | -| [Workspace.md](Workspace.md) | Workspace、Artifact、Checkpoint、commit/rollback 语义 | +| [Workspace.md](Workspace.md) | Workspace、Artifact 与 commit 语义 | | [RunEventJournal.md](RunEventJournal.md) | Run Event、状态机、订阅、恢复、取消与审批 | | [ProfilesAndPreset.md](ProfilesAndPreset.md) | Agent Profile、Preset agent schema、ContextFrame、Plan Policy | | [PromptAssembly.md](PromptAssembly.md) | Agent 独立 Preset / 独立 Model、FrozenRunInputSnapshot 与前后端 prompt assembly 链路 | diff --git a/docs/Agent/RunEventJournal.md b/docs/Agent/RunEventJournal.md index 46ec9cd2..6e98b3bb 100644 --- a/docs/Agent/RunEventJournal.md +++ b/docs/Agent/RunEventJournal.md @@ -10,7 +10,7 @@ Run Journal 是 Agent 系统的真相源。没有 journal 的 Agent 只是一个 2. Ordered:每个 run 内 event seq 单调递增。 3. Durable:关键副作用前后必须落 journal。 4. Replayable:UI timeline、debug、resume 应尽量从 journal 重建。 -5. User-visible:用户能看到工具调用、审批、diff、checkpoint、错误。 +5. User-visible:用户能看到工具调用、审批、workspace mutation、commit 与错误。 ## 2. 文件格式 @@ -57,7 +57,6 @@ AssemblingContext CallingModel DispatchingTool ApplyingWorkspacePatch -CreatingCheckpoint AwaitingHostCommit Finishing Completed @@ -78,7 +77,6 @@ CallingModel AwaitingApproval DispatchingTool ApplyingWorkspacePatch -CreatingCheckpoint AwaitingHostCommit Finishing Completed @@ -145,7 +143,6 @@ tool_call_completed tool_call_failed workspace_file_written workspace_patch_applied -checkpoint_created agent_loop_finished artifact_assembled commit_started @@ -269,7 +266,7 @@ run_failed } ``` -**Soft drift recovery**:在直接 fail-fast 之前,loop runner 会做"软纠正":当模型返回 0 tool_calls 且包含纯文本时,runtime 会把该文本捕获到当前 profile messageBody artifact 所在 root 的 `direct_output.md`(默认 `output/direct_output.md`),写入 `direct_output_captured` 与 checkpoint;随后把纯文本回复推进 history,再追加一条合成的 `user` 消息提醒它必须通过 Agent 工具继续。root run 如果直接输出就是目标回复,应显式 `workspace_commit` 该捕获文件,再调用 `workspace_finish`;如果需要修订已提交内容,必须先 `workspace_apply_patch` / `workspace_write_file`,再 `workspace_commit`,最后 `workspace_finish`。return-mode child 则必须用 `task_return` 结束。direct output recovery 没有独立的一次性尝试上限;只要仍有下一轮模型调用预算就继续纠偏。每次尝试都会写一条 `drift_recovery_attempted` 事件,便于宿主 UI 给用户显示"系统正在纠正…"提示: +**Soft drift recovery**:在直接 fail-fast 之前,loop runner 会做"软纠正":当模型返回 0 tool_calls 且包含纯文本时,runtime 会把该文本捕获到当前 profile messageBody artifact 所在 root 的 `direct_output.md`(默认 `output/direct_output.md`)并写入 `direct_output_captured`;随后把纯文本回复推进 history,再追加一条合成的 `user` 消息提醒它必须通过 Agent 工具继续。root run 如果直接输出就是目标回复,应显式 `workspace_commit` 该捕获文件,再调用 `workspace_finish`;如果需要修订已提交内容,必须先 `workspace_apply_patch` / `workspace_write_file`,再 `workspace_commit`,最后 `workspace_finish`。return-mode child 则必须用 `task_return` 结束。direct output recovery 没有独立的一次性尝试上限;只要仍有下一轮模型调用预算就继续纠偏。每次尝试都会写一条 `drift_recovery_attempted` 事件,便于宿主 UI 给用户显示"系统正在纠正…"提示: ```json { @@ -315,7 +312,7 @@ workspace_file_deleted workspace_rollback_completed ``` -Workspace event 不应内联大文件全文。面向 Agent/UI 的 payload 应记录 path、sha256、chars、words、patch ref;字节数仅保留在文件系统、checkpoint、hash 等内部实现边界。 +Workspace event 不应内联大文件全文。面向 Agent/UI 的 payload 应记录 path、sha256、chars、words、patch ref;字节数仅保留在文件系统与 hash 等内部实现边界。 ### 4.3 Context @@ -427,17 +424,7 @@ tool_call_failed `tool_call_started`、`tool_call_completed` 与 `tool_call_failed` 保持 `tool_call_requested` 之后的既有顺序,并携带同一 canonical `toolId` / `snapshotId`。预算耗尽属于可恢复的 `tool_call_failed`;turn/snapshot/choice violation 在 requested 后 fail-fast,不会发出 started。`tool_result_stored` 会携带同一 `round`、`toolId` 与 `path`,用于 UI 读取工具结果详情。 -### 4.6 Checkpoint / Diff - -```text -checkpoint_created -checkpoint_pruned -diff_created -``` - -Checkpoint event must include checkpoint id、reason、file count and internal storage metadata. UI-facing timeline payloads should expose text metrics as chars/words, not raw byte counts. - -### 4.7 Plan / Profile +### 4.6 Plan / Profile ```text plan_created @@ -453,20 +440,19 @@ profile_switch_denied Locked plan violation 必须失败或等待用户决策,不能静默忽略。 -### 4.8 Artifact / Commit +### 4.7 Artifact / Commit ```text artifact_assembly_started artifact_assembled artifact_assembly_failed -chat_commit_started chat_commit_requested chat_commit_completed chat_commit_failed committed_message_rollback_completed ``` -自动与显式 commit 共用同一事件序列。事件链记录 chat ref、checkpoint id、artifact path,并在 host bridge 已确认时记录 message id。`chat_commit_completed.messageId` 是提交时快照,不是对当前聊天消息位置的反向查询。Host 返回未确认时写 warning 级 `chat_commit_failed`;它不会终止 run,也不会产生 `chat_commit_recorded`。自动提交跳过本次,显式提交另写模型可见的 `tool_call_failed`。 +自动与显式 commit 共用同一事件序列。自动 commit 在一轮 tool calls 全部处理后最多发起一次。`chat_commit_requested` 记录 chat ref、artifact path 与预期 SHA;Host 重读当前 workspace,SHA 不一致时拒绝提交,并在确认后记录 message id。`chat_commit_completed.messageId` 是提交时结果,不是对当前聊天消息位置的反向查询。Host 返回未确认时写 warning 级 `chat_commit_failed`;它不会终止 run,也不会产生 `chat_commit_recorded`。自动提交跳过本次,显式提交另写模型可见的 `tool_call_failed`。 ## 5. 事件与副作用的顺序 @@ -493,7 +479,6 @@ tool_call_completed / tool_call_failed workspace_patch_requested apply patch workspace_patch_applied -checkpoint_created ``` 当前 `workspace_file_written` 同时覆盖 `workspace.write_file` 的 `replace` 与 `append` 成功结果,payload 必须携带 `mode`,便于 timeline 与恢复逻辑区分完整替换和原样追加。 @@ -525,7 +510,7 @@ readEvents(runId, { beforeSeq, limit }) ``` 移动端 timeline 不应该一次读取巨大 journal。 -默认 timeline UI 应把 raw journal 投影成用户可见操作流,并使用窗口化渲染;状态机、provider state、checkpoint 等审计事件仍保留在 journal 中,但不应因为 UI 容量限制挤掉早期操作轮次。 +默认 timeline UI 应把 raw journal 投影成用户可见操作流,并使用窗口化渲染;状态机、provider state 等审计事件仍保留在 journal 中,但不应因为 UI 容量限制挤掉早期操作轮次。 ## 8. Cancel @@ -546,7 +531,7 @@ cancel_agent_run(runId) - LLM call 必须尽量复用现有 cancellation registry 或等价 watch channel。 - Tool dispatch 需要声明是否 cancellable。 - Cancel 后不能 commit。 -- Cancel 后 workspace 与 checkpoint 保留。 +- Cancel 后 workspace 保留。 ## 9. Approval @@ -635,7 +620,6 @@ tool_call_completed tool_call_failed workspace_file_written workspace_patch_applied -checkpoint_created agent_loop_finished artifact_assembled commit_started diff --git a/docs/Agent/SubAgent.md b/docs/Agent/SubAgent.md index 736d359e..2ca5c8bb 100644 --- a/docs/Agent/SubAgent.md +++ b/docs/Agent/SubAgent.md @@ -17,7 +17,7 @@ AgentRun 这样可以保持: -- 同一个 run workspace、journal、cancel、checkpoint 与最终 commit 边界。 +- 同一个 run workspace、journal、cancel 与最终 commit 边界。 - 每个 Agent Profile 有独立 prompt、model binding、工具面与预算。 - return-mode child Agent 只产出任务结果,不直接写聊天消息。 - 主 Agent 负责整合、审稿与最终 `workspace.commit` / `workspace.finish`。 diff --git a/docs/Agent/TestingStrategy.md b/docs/Agent/TestingStrategy.md index 4975d0c5..da3de256 100644 --- a/docs/Agent/TestingStrategy.md +++ b/docs/Agent/TestingStrategy.md @@ -32,7 +32,6 @@ ToolPolicy allow/deny/approval InvocationToolSnapshot ordered binding/duplicate alias invariants ToolTurnContract all-snapshot/choice invariants Profile resolution precedence -Checkpoint metadata ``` Domain tests 不应需要 Tauri、文件系统或 HTTP。 @@ -48,7 +47,6 @@ agent loop success agent loop model failure cancel before model call cancel during model call -workspace write creates checkpoint artifact assembly success/failure commit service called through expected boundary tool loop success @@ -86,7 +84,6 @@ child tool snapshot removes commit/finish/delegation and appends task.return ```text file event journal append/read pagination -checkpoint snapshot restore workspace repository rejects symlink escape workspace repository handles unicode relative paths file sizes and retention @@ -165,6 +162,8 @@ Agent PromptManager assembly materializes a working copy and does not mutate Fro external tools/tool_choice/tool turns are rejected stream true is rejected foreground finish before workspace.commit returns recoverable tool error +foreground text mutations auto-commit only the final write/patch once per model round +chat commit rejects a workspace file whose SHA changed before Host read workspace.commit append without prior commit creates the run message subscribe polling can read events in seq order readWorkspaceFile returns UTF-8 text, chars, words, sha256 @@ -180,14 +179,14 @@ missing current-chat reads return recoverable errors and the next model turn sti externalized MCP results expose a fully line-readable text view plus the exact JSON audit workspace_write_file append creates missing files and appends existing files without a rewrite read workspace_write_file append does not auto-insert newlines and does not unlock rewrite or patch state for unread existing content -workspace_apply_patch requires full read-state and checkpoints on success +workspace_apply_patch requires full read-state chat deletion removes the corresponding Agent chat workspace and run index chat deletion fails clearly while the corresponding Agent workspace has an active run skill_search respects visible/deny policy and read budget all model-facing text read schemas expose start_line/line_count and no character range recoverable tool errors are returned to the model instead of failing the run listRuns returns paginated Agent run history summaries -future APIs approveToolCall/readDiff/rollback throw explicitly +future API approveToolCall throws explicitly run timeline/detail view switching has a single explicit state source and does not derive detailsOpen from scrollLeft run timeline main panel does not use horizontal scroll-snap or smooth scroll as a view state machine run timeline closes detail by resetting detail state and does not measure or auto-stick hidden timeline scrollers @@ -235,7 +234,6 @@ provider source denied by policy large chat history remains virtual journal pagination does not load full file workspace tree lazy read -checkpoint retention cap tool result budget truncation/summary mobile default budgets ``` @@ -259,7 +257,6 @@ fixtures/agent/ manifest_main_only.json manifest_multi_artifact.json tool_result_chat_search.json - checkpoint_snapshot/ ``` Golden fixtures 应尽量脱敏,不包含真实 API key 或私人聊天。 diff --git a/docs/Agent/ToolSystem.md b/docs/Agent/ToolSystem.md index 5803f361..12864c05 100644 --- a/docs/Agent/ToolSystem.md +++ b/docs/Agent/ToolSystem.md @@ -13,7 +13,7 @@ Agent 的能力上限很大程度由工具决定,但第一期更重要的是 - 工具调用可审计。 - 工具结果能进入 ContextFrame。 - 工具错误能被模型和用户理解。 -- 工具副作用可 checkpoint/rollback 或明确不可回滚。 +- 工具副作用使用 CAS、journal 与明确错误语义约束。 - MCP、extension 与 builtin 工具使用同一控制面契约,并保留各自 executor。 ## 2. 非目标 @@ -166,8 +166,8 @@ Agent-facing 文案必须从调用或执行 Agent 的角度描述可操作路径 | `workspace.list_files` | `workspace_list_files` | 只读列出模型可见 workspace 文件 | 已落地 | | `workspace.search_files` | `workspace_search_files` | 只读搜索模型可见 workspace UTF-8 文本文件,返回 snippet 与 ref | 已落地 | | `workspace.read_file` | `workspace_read_file` | 只读读取 UTF-8 文本或范围,完整读取记录 read-state | 已落地 | -| `workspace.write_file` | `workspace_write_file` | 写 run workspace 文件,成功后 checkpoint | 已落地 | -| `workspace.apply_patch` | `workspace_apply_patch` | 单文件精确替换,成功后 checkpoint | 已落地 | +| `workspace.write_file` | `workspace_write_file` | 写 run workspace 文件 | 已落地 | +| `workspace.apply_patch` | `workspace_apply_patch` | 单文件精确替换 | 已落地 | | `workspace.commit` | `workspace_commit` | 将 workspace 文件提交到当前 chat message | 已落地 | | `workspace.finish` | `workspace_finish` | 结束工具循环,进入 run 收尾 | 已落地 | @@ -199,11 +199,13 @@ Timeline 的隐藏、side-effect 关联与 builtin 类型判定只使用 journal Profile 的 `maxCallsPerRun` 是历史字段名,实际语义一直是 invocation 生命周期。Snapshot 使用真实名称 `maxCallsPerInvocation`;invocation-local `ToolRequestGate` 按 canonical `ToolId` 检查并预留总预算和 per-tool 预算。Gate 在当前串行 tool loop 中通过唯一 `&mut` 所有权保证一次“检查并预留”不可交错;预算从承载 workspace/skill 状态的 `AgentToolSession` 中移除。Profile 字段的纯命名迁移留到 selector schema 迁移时一次完成。 -`workspace.apply_patch` 使用 Claude Code 风格的 `old_string` / `new_string` 单文件精确替换。`old_string` 必须来自模型本 run 已读到的文本片段,或来自本 run 创建/完整替换后已经完整已知的文件;runtime 仍会读取当前完整文件检查版本与全文件唯一匹配,但不会把完整文件隐式塞回模型上下文。版本变化、匹配 0 次或多次会作为 recoverable tool error 返回模型;基于部分读取的 patch 一旦失败,同文件后续 patch 必须先完整读取,避免模型在不确定上下文上反复试错。`replace_all=true` 可能修改未读位置,因此必须在完整读取后使用。`workspace.write_file` 支持 `mode = replace | append`,默认 `replace`。`replace` 对已存在文件复用同一个 session read-state 做 CAS:模型不需要传 `expectedSha256`,schema 不暴露 overwrite policy;若文件在最近读取/写入后被其他 invocation 修改,会返回可恢复的 stale-file 工具错误,要求重新读取后再写。`append` 会把 `content` 原样追加到文件末尾,目标缺失时创建文件;不会自动补换行,模型需要新行时应把前导 `\n` 放进 `content`。`append` 工具调用本身只在新建文件或追加前文件已完整读入且版本匹配时更新完整 read-state,避免未读既有内容在同一轮内被隐式授权为后续 rewrite/patch 的依据。模型传入的非法 path、空 path、非法 mode、不可见/不可写 path 也作为可恢复工具错误回填;目标 path 实际指向目录的读写请求会作为 `workspace.path_is_directory` 业务错误回填,提示模型改用 `workspace_list_files`。repository 内部 escape/symlink/journal、checkpoint、序列化、取消和模型响应结构错误仍 fail-fast。 +`workspace.apply_patch` 使用 Claude Code 风格的 `old_string` / `new_string` 单文件精确替换。`old_string` 必须来自模型本 run 已读到的文本片段,或来自本 run 创建/完整替换后已经完整已知的文件;runtime 仍会读取当前完整文件检查版本与全文件唯一匹配,但不会把完整文件隐式塞回模型上下文。版本变化、匹配 0 次或多次会作为 recoverable tool error 返回模型;基于部分读取的 patch 一旦失败,同文件后续 patch 必须先完整读取,避免模型在不确定上下文上反复试错。`replace_all=true` 可能修改未读位置,因此必须在完整读取后使用。`workspace.write_file` 支持 `mode = replace | append`,默认 `replace`。`replace` 对已存在文件复用同一个 session read-state 做 CAS:模型不需要传 `expectedSha256`,schema 不暴露 overwrite policy;若文件在最近读取/写入后被其他 invocation 修改,会返回可恢复的 stale-file 工具错误,要求重新读取后再写。`append` 会把 `content` 原样追加到文件末尾,目标缺失时创建文件;不会自动补换行,模型需要新行时应把前导 `\n` 放进 `content`。`append` 工具调用本身只在新建文件或追加前文件已完整读入且版本匹配时更新完整 read-state,避免未读既有内容在同一轮内被隐式授权为后续 rewrite/patch 的依据。模型传入的非法 path、空 path、非法 mode、不可见/不可写 path 也作为可恢复工具错误回填;目标 path 实际指向目录的读写请求会作为 `workspace.path_is_directory` 业务错误回填,提示模型改用 `workspace_list_files`。repository 内部 escape/symlink/journal、序列化、取消和模型响应结构错误仍 fail-fast。 `workspace.commit` 与 `workspace.finish` 的契约:模型可以多次 commit;当全部修订与 commit 完成后,必须用 `workspace.finish` 收口,不能用纯文本代替最终 answer。foreground `workspace.finish` 要求同一 run 已经有至少一次成功的显式 `workspace.commit`;自动发布不满足该门槛。在 handoff 链中,这个 commit 可以来自前一个 foreground owner。`workspace.commit` 工具的返回字符串只做温和提醒,提示模型可继续修订并再次 commit,但最终不要忘记 finish。 -如果模型一回合内仍然返回 0 个 tool_calls(drift),loop runner 会做"软纠正"(issue #64):把模型的纯文本回复捕获到 workspace 的 `direct_output.md`(默认 `output/direct_output.md`,实际跟随当前 profile 的 messageBody artifact root),写 `direct_output_captured` 与 checkpoint;再把该 assistant 回复推进 history,并追加一条 `user` 角色的合成提醒,让模型在下一轮通过当前 invocation 的结束工具补回流程。root run 使用 `workspace_commit` / `workspace_finish`,return-mode child 使用 `task_return`。direct output 本身没有独立的一次性上限;只要仍有下一轮模型调用预算,就会继续纠偏。每次都会写一条 `drift_recovery_attempted` 事件。 +前台首次成功显式 commit 前,runtime 只在一轮 tool calls 全部处理后检查该轮最后一次成功的 `workspace.write_file` / `workspace.apply_patch`,并按既有文本后缀规则最多自动发布一次。各 mutation 只保留 CAS 与 journal;Host 在提交前重读当前文件并校验请求 SHA。 + +如果模型一回合内仍然返回 0 个 tool_calls(drift),loop runner 会做"软纠正"(issue #64):把模型的纯文本回复捕获到 workspace 的 `direct_output.md`(默认 `output/direct_output.md`,实际跟随当前 profile 的 messageBody artifact root),写 `direct_output_captured`;再把该 assistant 回复推进 history,并追加一条 `user` 角色的合成提醒,让模型在下一轮通过当前 invocation 的结束工具补回流程。root run 使用 `workspace_commit` / `workspace_finish`,return-mode child 使用 `task_return`。direct output 本身没有独立的一次性上限;只要仍有下一轮模型调用预算,就会继续纠偏。每次都会写一条 `drift_recovery_attempted` 事件。 - 软纠正后模型调用了 `workspace_finish`,或继续修订并再次 `workspace_commit` 后再 `workspace_finish` → run 继续,无 rollback。 - 软纠正后模型再次 0 tool_calls 且已经没有下一轮预算 → 回落到 `model.tool_call_required` 失败路径;若没有成功 chat commit,则写 `run_failed`(`userRetryable=true`);若已有成功 chat commit,则写 `run_partial_success`,保留已提交聊天输出并以 warning 暴露底层错误。 @@ -254,15 +256,7 @@ Profile 的 `maxCallsPerRun` 是历史字段名,实际语义一直是 invocati - 调用后写入 `agent-results/.json` 与 `summaries/-result.md`。 - child invocation 必须用它结束工作,不能用 `workspace.finish`。 -### 7.2 后续内置工具候选 - -后续优先补齐策略与 checkpoint 工具: - -```text -workspace.create_checkpoint -``` - -### 7.3 Workspace Tools +### 7.2 Workspace Tools `chat.search`、`workspace.search_files` 与 `skill.search` 共享纯查询规范化规则:包含字母、数字或 `_` 的 query 继续使用既有分词评分;非空 query 若分词后没有任何词项,则保留 trim 后 query,并由既有空白分词生成字面量 token。因此纯标点和符号片段可搜索,混合 query 的既有召回与排序语义不变。 @@ -281,7 +275,7 @@ workspace.create_checkpoint - 输入为 `query`,可选 `path`、`limit`、`context_lines`。 - 返回 path、score、line range、snippet 与 `workspace:#Lx-Ly` ref;sha256 只保留在内部结果与审计中。 - 0 命中是成功结果;非法 path、不可见 path、缺失 path 是 recoverable tool error。 -- 不搜索 `input/`、`model-responses/`、`checkpoints/`、`events.jsonl` 等隐藏 runtime 存储;`tool-results/` 是唯一显式可见的 runtime result root。 +- 不搜索 `input/`、`model-responses/`、`events.jsonl` 等隐藏 runtime 存储;`tool-results/` 是唯一显式可见的 runtime result root。 `workspace.read_file` @@ -298,7 +292,6 @@ workspace.create_checkpoint - 只能写 writable path;return-mode child 应把 `summaries/` / `scratch/` 用作私有笔记,只在任务要求 artifact 或 edit 时写共享 writable roots。 - `mode` 默认为 `replace`;`append` 把 `content` 原样追加到文件末尾,文件不存在时创建文件。 - `append` 不自动补换行;需要另起一行时,模型应在 `content` 开头包含 `\n`。 -- 写后应 checkpoint。 - `replace` 写已存在文件若发生并发修改,会返回 recoverable stale-file tool error;模型重新读取后再写。 `workspace.apply_patch` @@ -308,11 +301,6 @@ workspace.create_checkpoint - patch 失败返回可恢复 tool error;如果失败发生在部分读取基础上,同文件再次 patch 前必须完整读取。 - path escape 是 system failure。 -`workspace.create_checkpoint` - -- Mutating metadata。 -- 可由 runtime 自动调用,也可暴露给模型。 - `workspace.commit` - 控制/Mutating 工具。 @@ -536,7 +524,6 @@ AgentToolResult returns to backend journal - tool arguments / tool results resource refs。 - recoverable tool error 回填模型。 - workspace write/patch 成功结果只向模型回填包含目标路径的文本摘要;内部结构化元数据与 resource refs 保留给 runtime、audit 与 Timeline UI。需要完整内容时由模型显式调用 workspace_read_file。 -- workspace mutation checkpoint。 - journal events。 - MCP cached descriptor resolution、permission-aware call 与 known/unknown outcome 投影。 diff --git a/docs/Agent/Workspace.md b/docs/Agent/Workspace.md index d75d2874..d1d2da27 100644 --- a/docs/Agent/Workspace.md +++ b/docs/Agent/Workspace.md @@ -1,17 +1,16 @@ # TauriTavern Agent Workspace -本文档定义 Agent Workspace 的存储模型、路径约束、Artifact Assembly、Checkpoint 与回滚语义。 +本文档定义 Agent Workspace 的存储模型、路径约束与 Artifact Assembly 语义。 Workspace 是 Agent Mode 的中心抽象。Agent 不直接写聊天消息,而是在 workspace 中像编辑项目文件一样多轮修改输出,最后由 runtime 提交 artifact。 ## 1. 核心目标 -Workspace 解决四个问题: +Workspace 解决三个问题: 1. 多轮编辑:允许模型反复修改草稿、计划、状态栏、小剧场等文件。 -2. 可审计:每次写入、patch、checkpoint 都能追踪。 -3. 可回滚:用户不满意时回到某个 checkpoint,而不是只能重新生成。 -4. 可组合:最终聊天消息可以由多个 artifact 组装,而不是只有单个 output。 +2. 可审计:每次写入与 patch 都进入 journal。 +3. 可组合:最终聊天消息可以由多个 artifact 组装,而不是只有单个 output。 ## 2. 两级 Workspace @@ -41,7 +40,6 @@ agent-workspaces/ scratch/ summaries/ persist/ - checkpoints/ patches/ events.jsonl ``` @@ -80,10 +78,9 @@ chatWorkspaceId = "chat_" + sha256({ kind, stableChatId })[0..16] - 本次计划。 - 本次 scratch。 - 本次输出 artifact。 -- 本次 checkpoint。 - 本次 journal。 -这样不同 run 可以比较、回滚、删除或保留,不互相覆盖。 +这样不同 run 可以独立调试、删除或保留,不互相覆盖。 ## 3. Resource 类型 @@ -226,14 +223,11 @@ plan/ runtime 可检查的计划文件和用户/模型可读计划。 scratch/ - Agent 草稿;是否进入 context 由 policy 决定。前台首次显式 commit 前,匹配文本后缀的 write/patch 仍会自动发布。 + Agent 草稿;是否进入 context 由 policy 决定。前台首次显式 commit 前,每轮最后一次 write/patch 若匹配文本后缀会自动发布。 summaries/ 对历史、工具结果、前序步骤的摘要。 -checkpoints/ - checkpoint snapshot 与 manifest。 - patches/ 可选 patch 记录。第一期可以只做 snapshot。 @@ -274,9 +268,9 @@ Commit 是 workspace 到 chat 的边界。 必须: -- 在 commit 前创建 checkpoint。 - 读取 manifest。 - assemble artifacts。 +- 让 Host 按提交请求中的 SHA 校验当前 workspace 文件。 - 通过现有 chat 保存契约写入。 - 写入 agent metadata。 - journal 记录 `artifact_assembled` 与 `run_committed`。 @@ -293,11 +287,10 @@ Commit metadata 建议: ```json { "tauritavern": { - "agent": { - "runId": "run_...", - "stableChatId": "stable-chat-id", - "checkpointId": "ckpt_...", - "profileId": "writer", + "agent": { + "runId": "run_...", + "stableChatId": "stable-chat-id", + "profileId": "writer", "artifactSetId": "artifact_set_...", "artifacts": [ { "id": "main", "kind": "body", "path": "output/main.md" } @@ -307,89 +300,12 @@ Commit metadata 建议: } ``` -## 9. Checkpoint - -Checkpoint 是 run 内回滚和 commit 后追踪的基础。 - -第一期建议 snapshot,不引入 git。 - -结构: - -```text -checkpoints/ - 000001/ - checkpoint.json - manifest.json - output/ - main.md - status.md - plan/ - plan.md - summaries/ - ... -``` - -`checkpoint.json` 是内部实现元数据,不作为 Agent/UI payload;其中的 `bytes` 仅服务于存储体积、清理与完整性边界: - -```json -{ - "id": "ckpt_...", - "seq": 1, - "runId": "run_...", - "createdAt": "2026-04-26T00:00:00Z", - "reason": "after_workspace_patch", - "eventSeq": 42, - "files": [ - { "path": "output/main.md", "sha256": "...", "bytes": 1024 } - ] -} -``` - -Checkpoint 时机: - -- workspace 初始化后。 -- plan 创建后。 -- 每次 workspace-mutating tool 后。 -- 每个 plan node 完成后。 -- artifact assembly 前。 -- commit 前。 - -## 10. 回滚 - -### 10.1 Run 内回滚 - -Run 内回滚只恢复 workspace: - -```text -rollback(runId, checkpointId) - -> restore workspace files - -> append rollback event - -> status remains Running/AwaitingApproval or becomes Paused -``` - -它不修改 chat。 - -### 10.2 Commit 后回滚 - -Commit 后回滚修改 chat message: - -```text -rollbackCommittedMessage(runId, checkpointId) - -> assemble artifacts from checkpoint - -> replace/delete committed chat message - -> save through chat save contract - -> append rollback committed event -``` - -必须遵守完整 chat payload 保存串行化。 - -## 11. Retention +## 9. Retention 默认 retention 应保守: - Completed run 可以保留完整 workspace。 - Failed/Cancelled run 默认保留,便于 debug。 -- 移动端可以限制 checkpoint 数量或总大小,但删除必须明确记录。 - 用户删除聊天时,关联 chat workspace 必须随聊天生命周期清理,避免静默泄漏大量文件。 当前实现: @@ -406,7 +322,7 @@ rollbackCommittedMessage(runId, checkpointId) - 前端只通过 `api.agent.retention.readSettings()` / `updateSettings()` / `planPrune()` / `applyPrune()` facade 接触该策略;`updateSettings()` 只保存设置并唤醒后端调度器重读配置,不同步执行清理。 - `plan_agent_run_prune(dto)` 是 dry-run command:读取当前设置或调用方传入的一次性 retention override,生成候选动作、原因、文件数与字节数,不删除 run workspace 或重型 artifacts。`detailLimit` 只截断返回明细,不截断 totals;active run、缺失 terminal event、journal/storage 异常会进入 `blockedRuns`,不会被计为可执行 candidate。 - `apply_agent_run_prune(dto)` 使用同一 planner 的 execution 模式在后端重新生成全量执行计划,不信任前端 preview candidates;同一服务实例内 apply 串行执行。它执行 `slim_heavy_artifacts` / `delete_run` 后返回删除统计、`failedRuns` 和 caller `detailLimit` 下的 `afterPlan`;单个 run 删除失败会显式返回并继续后续 candidate,结构性规划错误仍 fail-fast。 -- dry-run 和 apply 都使用 Agent run storage class 判断清理范围,并与 TT-Sync 的 Agent dataset 边界保持同一套路径归属词汇。核心 history 是 `run_journal`(run 目录根级 `run.json` / `events.jsonl` 与 index run)和本地 `run_summary_projection`;`slim_heavy_artifacts` 删除 `run_context`、`run_workspace_projection`、`run_tool_io`、`workspace_outputs`、`workspace_scratch`、`tasks`、`model_responses`、`checkpoints` 以及未知 run artifact。`delete_run` 删除完整 run 目录加 index run/summary 文件。稳定 `persistent-states/` 不属于 run prune 范围。 +- dry-run 和 apply 都使用 Agent run storage class 判断清理范围,并与 TT-Sync 的 Agent dataset 边界保持同一套路径归属词汇。核心 history 是 `run_journal`(run 目录根级 `run.json` / `events.jsonl` 与 index run)和本地 `run_summary_projection`;`slim_heavy_artifacts` 删除 `run_context`、`run_workspace_projection`、`run_tool_io`、`workspace_outputs`、`workspace_scratch`、`tasks`、`model_responses` 以及未知 run artifact。`delete_run` 删除完整 run 目录加 index run/summary 文件。稳定 `persistent-states/` 不属于 run prune 范围。 run prune 由上述两个窗口推导动作: @@ -421,15 +337,14 @@ rank >= keep_recent_terminal_runs: delete terminal run history ``` -## 12. 性能约束 +## 10. 性能约束 - 不复制完整聊天历史。 - 不把大 virtual resource materialize 到每个 run。 -- checkpoint 第一版只 snapshot 小文本文件;大文件使用引用或跳过,并在 manifest 声明。 - workspace tree 展示应懒加载。 - timeline 读取 journal 应支持分页。 -## 13. 当前 Workspace +## 11. 当前 Workspace 当前 run workspace 物理布局: @@ -463,10 +378,6 @@ _tauritavern/agent-workspaces/ summaries/ persist/ - checkpoints/ - / - checkpoint.json - ``` 当前模型可见 / 可写 workspace 根: @@ -504,8 +415,6 @@ runs// manifest.json input/prompt_snapshot.json output/main.md - checkpoints/000001/checkpoint.json - checkpoints/000001/output/main.md events.jsonl ``` @@ -513,7 +422,5 @@ runs// - minimal run - journal -- checkpoint - artifact commit -- rollback 基础数据结构(当前尚未开放 rollback API) - workspace list/read/write/patch 工具循环 diff --git a/docs/AgentArchitecture.md b/docs/AgentArchitecture.md index 3ecd830b..288bdf5c 100644 --- a/docs/AgentArchitecture.md +++ b/docs/AgentArchitecture.md @@ -8,7 +8,7 @@ 2. `docs/AgentArchitecture.md`:系统边界、分层、数据流。 3. `docs/AgentContract.md`:不可破坏的不变量与 fail-fast 约束。 4. `docs/AgentImplementPlan.md`:当前实施基线、后续顺序与验收命令。 -5. `docs/Agent/Workspace.md`:Workspace、Artifact、Checkpoint 的存储语义。 +5. `docs/Agent/Workspace.md`:Workspace 与 Artifact 的存储语义。 6. `docs/Agent/RunEventJournal.md`:Run Event、状态机、恢复/取消语义。 7. `docs/Agent/ProfilesAndPreset.md`:Preset、Agent Profile、Plan Policy。 8. `docs/Agent/ToolSystem.md`:Tool Registry、Tool Result、权限与审批。 @@ -24,7 +24,7 @@ Agent Mode 的核心定义是: -> 一次生成不是 LLM 返回一段字符串,而是 Agent 对一个受策略约束的 Workspace 进行一组可审计、可回滚的编辑,最后由运行时把 Artifact 组装并提交为聊天消息。 +> 一次生成不是 LLM 返回一段字符串,而是 Agent 对一个受策略约束的 Workspace 进行一组可审计编辑,最后由运行时把 Artifact 组装并提交为聊天消息。 因此 Agent 不是更复杂的 `Generate()`,也不是 SillyTavern 工具调用递归的后端移植版。Agent 是一条新的生成路径: @@ -33,7 +33,7 @@ Legacy Generate 保持 SillyTavern 1.16.0 的事件语义、扩展兼容和 one-shot 体验 Agent Generate - Rust-owned Workspace Runtime + Profile + Plan + Tools + Journal + Checkpoint + Rust-owned Workspace Runtime + Profile + Plan + Tools + Journal MCP / Tool Direct Call 非 Agent 模式下也可显式调用 MCP 或工具,但不拥有 Agent Run @@ -56,13 +56,12 @@ MCP / Tool Direct Call ## 3. 设计目标 -Agent 系统必须同时满足五个目标: +Agent 系统必须同时满足四个目标: 1. 兼容:Agent Mode off 时,Legacy Generate 的行为、事件、扩展注入、世界书语义不变。 2. 可维护:Agent runtime 位于 Rust application layer,流程可测试,基础设施通过 trait 注入。 -3. 可审计:所有 LLM call、tool call、file write、checkpoint、profile switch、commit 都进入 append-only journal。 -4. 可回滚:Agent 修改 workspace,不直接写 chat;commit 后仍可根据 artifact/checkpoint 回滚聊天消息。 -5. 创作者自由:Preset/角色卡/扩展/Skill 能控制可见内容、工具、预算、计划模式和输出结构,但不能突破宿主安全边界。 +3. 可审计:所有 LLM call、tool call、file write、profile switch、commit 都进入 append-only journal。 +4. 创作者自由:Preset/角色卡/扩展/Skill 能控制可见内容、工具、预算、计划模式和输出结构,但不能突破宿主安全边界。 ## 4. 非目标 @@ -101,11 +100,10 @@ Workspace 初始化 ↓ ContextFrame 初步组装 ↓ -Agent Run State Machine + Agent Run State Machine ├─ model call ├─ tool call ├─ workspace patch - ├─ checkpoint ├─ optional plan/profile switch/summary └─ finish ↓ @@ -155,11 +153,11 @@ LLM Gateway / provider adapter - `workspace.write_file` / `workspace.apply_patch` 成功结果只向模型回填包含目标路径的文本摘要;内部结构化元数据与 resource refs 保留给 runtime、audit 与 Timeline UI。需要完整内容时模型必须显式调用 `workspace.read_file`。 - `chat.search` 与 `chat.read_messages` 只读取当前 run 绑定的聊天,不允许模型指定任意 chat target;message index 从 0 开始,JSONL header 不计入消息。 - `worldinfo.read_activated` 只读取本次 run prompt snapshot 中 materialized 的激活结果,不把全局 last activation 当作运行时真相。 -- 当前模型可见 / 可写 workspace 根由 run manifest roots 驱动,默认包含 `output/`、`scratch/`、`plan/`、`summaries/`、`persist/`;`persist/` 是 chat workspace 级持久 root 的 run projection,`workspace.finish` 收尾成功后 promote 回稳定 chat workspace;`input/`、`tool-args/`、`tool-results/`、`model-responses/`、`checkpoints/` 与 `events.jsonl` 不作为模型工具资源暴露。 -- 工具循环最多 80 轮,必须以 `workspace.finish` 结束;前台 run 在 finish 前必须至少成功显式 `workspace.commit` 一次(此前的文本自动发布不计),后台 run 可无 chat commit;模型直接输出文本会捕获到 workspace `direct_output.md` 并触发 soft drift recovery,只要仍有下一轮模型调用预算就继续用合成 `user` 提醒纠偏,直到恢复、取消或 `maxRounds` 边界触发 fail-fast / partial-success。 -- 模型可修正的工具错误以 `is_error = true` tool result 回填下一轮;宿主级 IO、journal、checkpoint、序列化、取消和模型响应结构错误仍 fail-fast。 +- 当前模型可见 / 可写 workspace 根由 run manifest roots 驱动,默认包含 `output/`、`scratch/`、`plan/`、`summaries/`、`persist/`;`persist/` 是 chat workspace 级持久 root 的 run projection,`workspace.finish` 收尾成功后 promote 回稳定 chat workspace;`input/`、`tool-args/`、`tool-results/`、`model-responses/` 与 `events.jsonl` 不作为模型工具资源暴露。 +- 工具循环最多 80 轮,必须以 `workspace.finish` 结束;前台 run 在一轮 tool calls 结束时最多自动发布该轮最后一次文本 mutation,但 finish 前仍必须至少成功显式 `workspace.commit` 一次,后台 run 可无 chat commit;模型直接输出文本会捕获到 workspace `direct_output.md` 并触发 soft drift recovery,只要仍有下一轮模型调用预算就继续用合成 `user` 提醒纠偏,直到恢复、取消或 `maxRounds` 边界触发 fail-fast / partial-success。 +- 模型可修正的工具错误以 `is_error = true` tool result 回填下一轮;宿主级 IO、journal、序列化、取消和模型响应结构错误仍 fail-fast。 - root、return-mode child 与 handoff invocation 各自冻结 `InvocationToolSnapshot` / `ToolTurnContract`;provider alias 只在当前 turn 内解析为 canonical `ToolInvocation`,唯一执行入口通过 invocation-local `ToolRequestGate` 检查 contract 并预留冻结预算,完整 manifest 随 run 持久化。 -- Skill profile policy、readDiff、rollback、resume-run、tool approval、profile routing、MCP、timeline UI、streaming Agent loop、主发送按钮 Agent toggle 仍未实现。 +- Skill profile policy、resume-run、tool approval、profile routing、MCP、timeline UI、streaming Agent loop、主发送按钮 Agent toggle 仍未实现。 ### 5.2 Run 与 Workspace 身份 @@ -186,7 +184,6 @@ src-tauri/ event.rs workspace.rs artifact.rs - checkpoint.rs profile.rs plan.rs policy.rs @@ -196,7 +193,6 @@ src-tauri/ tt-ports/src/repositories/ agent_run_repository.rs workspace_repository.rs - checkpoint_repository.rs skill_repository.rs mcp_repository.rs @@ -252,7 +248,7 @@ src-tauri/ - 创建 run。 - 驱动状态机。 - 追加 journal event。 -- 调用 workspace、context、tool、LLM、checkpoint、artifact、commit 服务。 +- 调用 workspace、context、tool、LLM、artifact、commit 服务。 - 管理 cancel / await approval / resume。 `AgentRunService` 不应该知道具体文件路径如何落盘,也不应该直接拼 provider-specific payload。 @@ -265,7 +261,6 @@ src-tauri/ - 管理 materialized file、virtual resource、generated artifact。 - 应用 patch/write。 - 做 path normalization 与 traversal 拒绝。 -- 生成 checkpoint snapshot/diff 所需文件视图。 详见 `docs/Agent/Workspace.md`。 @@ -315,7 +310,7 @@ Gateway 代码已拆成 `agent_model_gateway/` 模块目录:`mod.rs` 保留 tr - 根据 workspace manifest 读取一个或多个 artifact。 - 组装 message body 与 message extra。 - 通过现有 chat 保存路径提交。 -- commit 后写入 `agentRunId`、`agentCheckpointId`、`agentArtifacts` 等 metadata。 +- commit 后写入 Agent run 与 artifact metadata。 Commit 必须遵守完整 chat payload 与保存串行化契约,不能直接写 JSONL 文件。 @@ -366,7 +361,6 @@ Running ├─ AwaitingToolApproval ├─ DispatchingTool ├─ ApplyingWorkspacePatch - ├─ CreatingCheckpoint ├─ Summarizing? └─ SwitchingProfile? ├─ AwaitingHostCommit @@ -413,7 +407,7 @@ SillyTavern 上游的事件和 chat message 结构仍是兼容层的基础。Age - Workspace 不复制完整历史、完整世界书、完整记忆库。 - Context assembly 必须有 token/resource budget。 - Tool result 进入 journal 后,进入 prompt 前可以摘要、裁剪或按需读取。 -- 移动端默认更小预算、更短 checkpoint retention、更保守的并发。 +- 移动端默认更小预算与更保守的并发。 - Streaming event 应该可节流,journal 应该可 append-only 顺序写入。 ## 12. 安全策略 @@ -433,13 +427,12 @@ SillyTavern 上游的事件和 chat message 结构仍是兼容层的基础。Age 2. run workspace 与 chat 级 `persist/` projection。 3. append-only `events.jsonl`。 4. `output/main.md` artifact。 -5. checkpoint snapshot。 -6. Rust-owned model/tool loop。 -7. chat/worldinfo/workspace 内建工具。 -8. artifact commit 到 chat。 -9. Agent Mode off 行为完全不变。 -10. canonical model IR 与 `AgentModelGateway`。 -11. provider native metadata opaque 保留/回放。 +5. Rust-owned model/tool loop。 +6. chat/worldinfo/workspace 内建工具。 +7. artifact commit 到 chat。 +8. Agent Mode off 行为完全不变。 +9. canonical model IR 与 `AgentModelGateway`。 +10. provider native metadata opaque 保留/回放。 12. workspace write/patch read-state 与显式 read-before-edit 语义。 下一步的架构重点不再是证明 Agent loop 可行,而是补齐三个长期能力:更清晰的 provider adapter 模块、创作者可控的 profile/context policy、可理解的 timeline/diff/rollback UI。 diff --git a/docs/AgentContract.md b/docs/AgentContract.md index ab889a1d..15e6df83 100644 --- a/docs/AgentContract.md +++ b/docs/AgentContract.md @@ -32,21 +32,22 @@ summaries/*.md - Tool result 直接插入 chat 楼层。 - workspace-mutating tool 绕过 WorkspaceService 写文件。 -理由:只有 workspace 才能提供 checkpoint、diff、rollback、artifact assembly 与 timeline 审计。 +理由:workspace 提供受控的多轮编辑、artifact assembly 与 timeline 审计边界。 -当前前台 root / handoff Agent 在第一次成功的显式 `workspace.commit` 之前,成功执行 -`workspace.write_file` / `workspace.apply_patch` 后,如果目标路径以 `.md`、`.markdown`、 -`.txt` 或 `.text`(大小写不敏感)结尾,runtime 会把该目标文件作为一次 -`replace` commit 交给同一个 Committer。它不绕过 workspace、journal、checkpoint、 -Host bridge 或完整 chat 保存契约,也不把 Tool result 直接插入聊天。 +当前前台 root / handoff Agent 在第一次成功的显式 `workspace.commit` 之前,会在每轮 +tool calls 全部处理后检查本轮最后一次成功的 `workspace.write_file` / +`workspace.apply_patch`。如果目标路径以 `.md`、`.markdown`、`.txt` 或 `.text` +(大小写不敏感)结尾,runtime 会把该 mutation 作为一次 `replace` commit 交给同一个 +Committer。每轮最多自动 commit 一次;它不绕过 workspace、journal、Host bridge 或完整 chat +保存契约,也不把 Tool result 直接插入聊天。 第一次 host-confirmed 的显式 `workspace.commit` 成功后,当前 run 永久停止自动 commit。 自动 commit 是可保留的 chat 输出,但不能满足前台 `workspace.finish` 对显式 commit 的要求。 Host commit 不设置固定等待期限:宿主挂起时保持 pending,run cancellation 仍可中断。Host 未能确认提交时统一回到可恢复路径;只有 host-confirmed 结果才能推进 backend commit ledger。 Reasoning cursor 跟随已成功写入 `chat[]` 的内容:写入前失败不推进,写入后失败保留正文与 -reasoning 并推进 cursor,避免重试重复追加。checkpoint、journal、状态机等 runtime 不变量 -错误仍必须 fail-fast。 +reasoning 并推进 cursor,避免重试重复追加。Host 读取当前 workspace 后必须校验提交请求中的 +SHA;内容变化时拒绝提交。journal、状态机等 runtime 不变量错误仍必须 fail-fast。 ### 1.2 Run Journal 必须是真相源 @@ -57,7 +58,6 @@ reasoning 并推进 cursor,避免重试重复追加。checkpoint、journal、 - model request created / model completed / model error - tool call requested / approved / completed / failed - workspace patch applied -- checkpoint created - profile switched - summary created - artifacts assembled @@ -211,7 +211,6 @@ Domain 可以定义: - `WorkspacePath` - `WorkspaceResource` - `ArtifactSpec` -- `Checkpoint` - `AgentProfile` - `PlanPolicy` - `ToolId` @@ -237,7 +236,7 @@ Application service 可以编排: - context assembly - LLM gateway call - tool dispatch -- workspace patch/checkpoint +- workspace patch - artifact assembly - commit/rollback @@ -249,7 +248,6 @@ Infrastructure 可以实现: - file workspace repository - file event journal repository -- checkpoint snapshot/diff store - skill repository - MCP client manager - provider adapter @@ -317,7 +315,7 @@ dryRun 不能被视为纯函数。它仍会触发上游事件、prompt 组合、 - `startRunFromLegacyGenerate()` 内部可以调用 Legacy dryRun,但工具循环必须在 Rust runtime 中推进,不得递归调用 `Generate()`。 - 工具注册由 Rust runtime 独占;prompt snapshot 中不得携带 external `tools`、`tool_choice`、`role: "tool"` 或已有 `tool_calls`。 - 当前只支持非 streaming;请求 `stream: true` 必须 fail-fast。Chat commit 只能由 runtime 的 pre-explicit 文本 mutation policy 或显式 `workspace.commit` 触发,并统一通过 Committer + host bridge 写入。 -- 模型可修正的工具参数错误必须作为 `is_error = true` tool result 回填模型;宿主级 IO、journal、checkpoint、序列化、取消和模型响应结构错误必须 fail-fast。 +- 模型可修正的工具参数错误必须作为 `is_error = true` tool result 回填模型;宿主级 IO、journal、序列化、取消和模型响应结构错误必须 fail-fast。 Agent run/timeline/tool event 不得伪装成上游 `GENERATION_*` 或 `TOOL_CALLS_*` 事件。上游事件属于 Legacy Generate 兼容面。 @@ -465,7 +463,7 @@ run 失败时必须: 1. journal 追加 `run_failed`。 2. 状态进入 `Failed`。 -3. 保留 workspace 与最后 checkpoint。 +3. 保留 workspace。 4. 前端 timeline 可读到失败原因。 cancel 不是 failure,必须进入 `Cancelled`。 @@ -481,7 +479,6 @@ Agent commit 到 chat message 时,metadata 必须放入 `extra` 下的 TauriTa "agent": { "runId": "...", "stableChatId": "...", - "checkpointId": "...", "profileId": "...", "artifactSetId": "...", "artifacts": [] diff --git a/docs/AgentImplementPlan.md b/docs/AgentImplementPlan.md index 29b47b68..503435c5 100644 --- a/docs/AgentImplementPlan.md +++ b/docs/AgentImplementPlan.md @@ -8,7 +8,7 @@ 截至 2026-07-31,Agent 当前核心已经落地: -- Rust 后端拥有 Agent domain model、runtime、workspace、journal、checkpoint、commit bridge。 +- Rust 后端拥有 Agent domain model、runtime、workspace、journal 与 commit bridge。 - 聊天删除会清理对应 Agent chat workspace;active run 存在时删除 fail-fast。 - 前端 Host ABI 已挂载 `window.__TAURITAVERN__.api.agent`。 - Agent System 扩展开关开启时,前端将普通发送、regenerate 与 overswipe 新候选生成接入 Agent;普通切换已有 swipe 候选仍保持 Legacy swipe 行为。 @@ -60,8 +60,6 @@ api.agent.tools.list() ```ts approveToolCall() -readDiff() -rollback() ``` ## 3. 当前 Model Gateway @@ -165,7 +163,7 @@ Builtin 工具直接声明中性 `ToolDescriptor` 并构造 `ToolCatalog`;invo 必须区分两类错误: - Recoverable tool error:模型参数、路径字符串、可见/可写策略、文件不存在、chat message index 不存在、读取范围非法、结果超过工具预算、patch 未完整读取、sha 过期、匹配 0 次或多次等模型可修正问题。返回 `AgentToolResult { is_error: true }`,写入 `tool_call_failed` warn event,并作为 tool message 回填下一轮模型。 -- Fatal runtime error:journal 写入失败、workspace repository 内部 IO 错误、chat JSONL 损坏、manifest/checkpoint 损坏、模型响应结构不可解析、取消、序列化失败、状态机错误等宿主级问题。直接让 run 进入 failed 或 cancelled。 +- Fatal runtime error:journal 写入失败、workspace repository 内部 IO 错误、chat JSONL 损坏、manifest 损坏、模型响应结构不可解析、取消、序列化失败、状态机错误等宿主级问题。直接让 run 进入 failed 或 cancelled。 当前工具结果不做自动内容补入: @@ -204,11 +202,11 @@ model -> read-only context tools / skill tools / workspace tools -> model -> ... ↓ 工具调用参数与结果写入 workspace refs ↓ -workspace mutation 成功后 checkpoint +workspace mutation 成功后记录 CAS 结果与 journal ↓ -首次成功显式 commit 前,文本 mutation 自动复用 checkpoint 触发 Committer +首次成功显式 commit 前,每轮最后一次文本 mutation 可触发 Committer ↓ -workspace.commit 通过同一 Committer 写入 chat,并关闭后续自动 commit +自动与显式 commit 通过同一 Committer 请求 Host 校验 workspace SHA 并写入 chat;显式成功后关闭自动 commit ↓ workspace.finish 收尾并提交 persist projection ``` @@ -234,7 +232,7 @@ workspace.finish 收尾并提交 persist projection 已完成: -- Rust 后端 Agent runtime、run workspace、manifest、journal、checkpoint、commit bridge 已落地。 +- Rust 后端 Agent runtime、run workspace、manifest、journal 与 commit bridge 已落地。 - `api.agent.startRunFromLegacyGenerate()` / `startRunWithPromptSnapshot()` / subscribe / cancel / readEvents / commit bridge 已落地。 - Agent Mode off 不改变 Legacy Generate、ToolManager、事件顺序与 chat 保存语义。 @@ -252,7 +250,7 @@ workspace.finish 收尾并提交 persist projection 已完成: - `workspace.list_files`、`workspace.search_files`、`workspace.read_file`、`workspace.write_file`、`workspace.apply_patch`、`workspace.commit`、`workspace.finish` 已落地。 -- workspace mutation 后创建 checkpoint;完整读取 / 写入 read-state 约束已接入。 +- workspace mutation 使用 CAS 与 journal;Chat Commit 由 Host 重读当前文件并校验 SHA。 - `output/`、`scratch/`、`plan/`、`summaries/`、`persist/` 是当前模型可见 root。 ### 上下文只读工具 @@ -327,14 +325,8 @@ Agent tool 层已接入 Skill 可见性、deny policy 与 read budget;Skill - commit preview 与手动提交。 - cancel UI。 -### Diff / Rollback / Resume +### Run Resume -目标:让多轮创作具备可控回退能力。 - -内容: - -- `readDiff()`:基于 checkpoint 对 workspace 文本文件生成 diff。 -- `rollback()`:先只恢复 run workspace,不修改已提交聊天消息。 - `resumeRun()`:明确 run continuation 语义,避免复用已 closed run 的状态机。 ### MCP / Extension Tool diff --git a/docs/CurrentState/AgentFramework.md b/docs/CurrentState/AgentFramework.md index 8a386dde..0c028b9a 100644 --- a/docs/CurrentState/AgentFramework.md +++ b/docs/CurrentState/AgentFramework.md @@ -8,7 +8,7 @@ 截至 2026-08-03,Agent 当前基线: -- Rust 后端已有 Agent domain model、runtime、workspace、journal、checkpoint、commit bridge。 +- Rust 后端已有 Agent domain model、runtime、workspace、journal 与 commit bridge。 - 前端已挂载 `window.__TAURITAVERN__.api.agent` Host ABI。 - Agent 启动仍通过 `PromptSnapshot` 兼容桥进入;root run 已支持 Agent Profile 独立 preset 与独立 model 的 Frontend PromptAssemblyBroker 组装,`GenerationIntent + ContextFrame` 尚未完全接管上下文组装。 - LLM 调用仍复用 `ChatCompletionService::generate_exchange_with_cancel()`,不得绕过现有 provider、secret、日志、endpoint policy、iOS policy、prompt cache 或取消链路。Responses WebSocket 建连已收敛到 `HttpClientPool` 的 ChatCompletion WebSocket profile,见 `docs/CurrentState/NativeApiFormats.md`。 @@ -159,7 +159,7 @@ Agent run 创建时,Rust runtime 会冻结本 run 的输入历史前缀:`swi | `workspace.list_files` | `workspace_list_files` | read-only | 列出模型可见 workspace 文件。`path` 省略、空字符串、`.`、`./` 表示 workspace root。 | | `workspace.search_files` | `workspace_search_files` | read-only | 搜索模型可见 workspace UTF-8 文本文件;可限定 `path`,返回 snippet/ref,不搜索隐藏 runtime 存储。 | | `workspace.read_file` | `workspace_read_file` | read-only | 读取 UTF-8 文本文件并返回行号;只支持 1-based 行范围,默认全文、超限预览;完整读取会记录 read-state。 | -| `workspace.write_file` | `workspace_write_file` | mutating | 写 UTF-8 文件;`mode` 默认为完整替换,`append` 会原样追加并在缺失时创建文件;成功后按内容可知性更新 read-state 并创建 checkpoint。 | +| `workspace.write_file` | `workspace_write_file` | mutating | 写 UTF-8 文件;`mode` 默认为完整替换,`append` 会原样追加并在缺失时创建文件;成功后按内容可知性更新 read-state。 | | `workspace.apply_patch` | `workspace_apply_patch` | mutating | 单文件 `old_string` / `new_string` 精确替换;`old_string` 必须来自本 run 已读文本或本 run 创建/完整替换的文件;失败后同文件再次 patch 必须先全文读取。 | | `workspace.commit` | `workspace_commit` | control/mutating | 将可见 workspace 文件提交到当前聊天;无参数等价于 `replace output/main.md`,`append` 首次创建消息、后续追加同一消息。 | | `workspace.finish` | `workspace_finish` | control | 结束 root/active 工具循环;前台 run 必须已有至少一次 run-level 成功显式 commit,自动 commit 不满足该门槛;后台 run 可无 commit;return-mode child invocation 不可用;当前会取消 unfinished return-mode child tasks 而不阻塞完成。 | @@ -269,11 +269,10 @@ persist/ Chat commit 由 pre-explicit 文本 mutation policy 或模型显式调用 `workspace.commit` 触发,并由同一个前端 host bridge 执行: ```text -workspace.write_file / workspace.apply_patch # 首次成功显式 commit 前,匹配文本后缀 +每轮最后一次 workspace.write_file / workspace.apply_patch # 首次成功显式 commit 前,匹配文本后缀 或 workspace.commit(path?, mode?) - -> backend 以本次 mutation 内容创建 checkpoint;显式 commit 则读取文件后创建 -> chat_commit_requested event - -> Host API 读取事件绑定的 checkpoint 与 root / handoff Model Turn 的可见 reasoning + -> Host API 重读当前 workspace 文件并校验事件 SHA,同时读取 root / handoff Model Turn 的可见 reasoning -> 前端应用 Legacy generated-output postprocess -> 前端 saveReply() 写同一消息楼层与标准 extra.reasoning -> resolve_agent_chat_commit @@ -283,7 +282,7 @@ workspace.write_file / workspace.apply_patch # 首次成功显式 commit 前, -> resolve_agent_persistent_state_metadata_update ``` -自动 commit 使用大小写不敏感的固定后缀 `.md`、`.markdown`、`.txt`、`.text`,不按 artifact 或 root 过滤,固定把 mutation 后完整文件以 `replace` 发布;多文件因此在同一楼层遵循 last-write-wins。第一次 host-confirmed 的显式 `workspace.commit` 后自动触发永久停止。显式 `mode` 默认为 `replace`;`append` 在本 run 尚无 commit 时创建消息,之后多次 commit 始终更新同一个消息楼层。`append` 追加的是本次 commit 读取到的文件文本,不计算 workspace file diff;host bridge 会对累计后的 raw 目标文本做一次 Legacy output postprocess。Commit 必须遵守 SillyTavern 完整 chat payload 与保存串行化契约,不能直接写 chat JSONL。`persistStateId` 只能表示已经落盘的 durable persistent state;`chat_commit_requested` 不携带该字段,partial success 保留的聊天输出不会成为下一轮可复用 persist base。下一轮 run 的 `persistBaseStateId` 由 Rust runtime 从同一个输入历史前缀内解析,前端不再负责扫描聊天历史来决定 base state。 +自动 commit 只在一轮 tool calls 全部处理后检查该轮最后一次成功的 write/patch,每轮最多一次。它使用大小写不敏感的固定后缀 `.md`、`.markdown`、`.txt`、`.text`,不按 artifact 或 root 过滤,并把该 mutation 的完整文件以 `replace` 发布;跨轮多次发布仍在同一楼层遵循 last-write-wins。第一次 host-confirmed 的显式 `workspace.commit` 后自动触发永久停止。显式 `mode` 默认为 `replace`;`append` 在本 run 尚无 commit 时创建消息,之后多次 commit 始终更新同一个消息楼层。`append` 追加的是本次 commit 读取到的文件文本,不计算 workspace file diff;host bridge 会对累计后的 raw 目标文本做一次 Legacy output postprocess。Commit 必须遵守 SillyTavern 完整 chat payload 与保存串行化契约,不能直接写 chat JSONL。`persistStateId` 只能表示已经落盘的 durable persistent state;`chat_commit_requested` 不携带该字段,partial success 保留的聊天输出不会成为下一轮可复用 persist base。下一轮 run 的 `persistBaseStateId` 由 Rust runtime 从同一个输入历史前缀内解析,前端不再负责扫描聊天历史来决定 base state。 聊天删除现在会联动清理对应的 Agent chat workspace: @@ -320,14 +319,14 @@ prepare_agent_tool_request 从该 turn 生成 AgentModelRequest 与 request-scop ↓ model -> tool -> model -> ... -> workspace.commit? -> workspace.finish ↓ -workspace mutation 成功后 checkpoint;符合 pre-explicit policy 时立即进入 Committer +workspace mutation 成功后记录 CAS 结果与 journal;每轮结束时由最后一次 mutation 检查 pre-explicit policy ↓ workspace.commit 成功后关闭自动 commit;host 继续写入同一条 chat message ↓ workspace.finish 结束 run,并提交 persist projection ``` -工具循环轮数来自 `profile.tools.maxRounds`。超过后以 `agent.max_tool_rounds_exceeded` 失败。模型直接输出文本且不调用工具会触发 soft drift recovery:runtime 将直接文本捕获到当前 messageBody artifact root 下的 `direct_output.md`(默认 `output/direct_output.md`),记录 `direct_output_captured` 与 checkpoint,然后提醒模型通过当前 invocation 的 Agent 工具提交/结束;该 runtime capture 本身不触发自动 commit。direct output recovery 不再有独立的一次性上限;只要仍有下一轮模型调用预算就继续纠偏,直到恢复、取消或在 `maxRounds` 边界以 `model.tool_call_required` 失败 / `run_partial_success` 收口。前台 run 在 `workspace.finish` 前必须至少成功一次显式 `workspace.commit`;后台 run 可以无 chat commit 结束。 +工具循环轮数来自 `profile.tools.maxRounds`。超过后以 `agent.max_tool_rounds_exceeded` 失败。模型直接输出文本且不调用工具会触发 soft drift recovery:runtime 将直接文本捕获到当前 messageBody artifact root 下的 `direct_output.md`(默认 `output/direct_output.md`),记录 `direct_output_captured`,然后提醒模型通过当前 invocation 的 Agent 工具提交/结束;该 runtime capture 本身不触发自动 commit。direct output recovery 不再有独立的一次性上限;只要仍有下一轮模型调用预算就继续纠偏,直到恢复、取消或在 `maxRounds` 边界以 `model.tool_call_required` 失败 / `run_partial_success` 收口。前台 run 在 `workspace.finish` 前必须至少成功一次显式 `workspace.commit`;后台 run 可以无 chat commit 结束。 Return-mode SubAgent 当前流程: @@ -384,11 +383,9 @@ tool_result_stored tool_call_completed / tool_call_failed workspace_file_written workspace_patch_applied -checkpoint_created provider_state_updated model_response_stored agent_loop_finished -chat_commit_started chat_commit_requested chat_commit_completed / chat_commit_failed chat_commit_recorded @@ -456,10 +453,6 @@ _tauritavern/agent-workspaces/ / result.md persist/ - checkpoints/ - / - checkpoint.json - _tauritavern/skills/ installed/ / @@ -474,7 +467,7 @@ _tauritavern/agent-profiles/ .staging/ ``` -Workspace path 必须是相对路径。绝对路径、Windows drive prefix、NUL、`..` 会被拒绝。工具参数层可修正的问题返回 recoverable tool error;repository 内部 IO、journal、checkpoint、chat JSONL 损坏、序列化、取消和模型响应结构错误是 fatal runtime error。 +Workspace path 必须是相对路径。绝对路径、Windows drive prefix、NUL、`..` 会被拒绝。工具参数层可修正的问题返回 recoverable tool error;repository 内部 IO、journal、chat JSONL 损坏、序列化、取消和模型响应结构错误是 fatal runtime error。 工具参数与结果的审计文件名使用 provider `tool_call_id` 的 SHA-256 前 8 字节 hex 派生;原始 `tool_call_id` 仍保存在 event payload、审计 JSON 内容与下一轮模型 tool result 中,不能被截断、清洗或替换。 @@ -530,7 +523,7 @@ const stop = agent.subscribe(run.runId, event => console.log(event)); - 继续完善运行中 prompt assembly 的恢复/诊断体验;当前 child / handoff 已有独立 provider_state、model binding 与 host bridge prompt assembly handshake。 - 实现模型可见 task cancel 与更完整的 scheduler policy。 - 明确多 Agent provider/model switch policy;root run 的 `connectionRef` 模型绑定已经可用。 -- 实现 readDiff、rollback、resume-run、streaming 的明确策略。 +- 实现 resume-run、streaming 的明确策略。 ## 每次 Agent 相关变更必须更新 diff --git a/docs/CurrentState/Sync.md b/docs/CurrentState/Sync.md index 24a66bed..f54b9f94 100644 --- a/docs/CurrentState/Sync.md +++ b/docs/CurrentState/Sync.md @@ -53,7 +53,7 @@ Mirror 删除计划文件后,文件系统适配器会继续清理因此变成 - `_tauritavern/agent-workspaces/index/runs/**` - `_tauritavern/agent-workspaces/chats/**/runs/*/{run.json,events.jsonl}` -`default-user/secrets.json`、Agent `model-responses/`、`checkpoints/`、`backups/`、`vectors/`、`thumbnails/` 被保留为独立数据集,不在 TauriTavern 推荐默认同步中;用户可以在 Sync Panel 的“Sync content”范围选择弹窗里显式勾选。 +`default-user/secrets.json`、Agent `model-responses/`、`backups/`、`vectors/`、`thumbnails/` 被保留为独立数据集,不在 TauriTavern 推荐默认同步中;用户可以在 Sync Panel 的“Sync content”范围选择弹窗里显式勾选。 Agent run history 只同步终态运行。扫描器会读取 `run.json` / run index JSON 的 `status`,仅纳入 `completed`、`partial_success`、`cancelled`、`failed`;运行中的 `calling_model` 等状态不会进入 manifest。 @@ -285,7 +285,7 @@ wire framing(见 TT-Sync 的 `ttsync_core::bundle` / `ttsync_client::bundle` 4. **不要改动事件语义**:阶段划分与完成/错误时序对前端是契约。 5. **不要把 iOS policy 本地缓存纳入 scope**:`_tauritavern/.ios-policy.json` 属于 iOS-only 宿主本地状态,用于避免同步覆盖 `tauritavern-settings.json` 时丢失已解锁的 policy。 6. **不要在 v2 链路重新引入手写 scope 数组**:新增同步目录必须先进入 TT-Sync `DatasetPolicy`,再由 LAN Sync/TT-Sync v2 消费。 -7. **不要把敏感/重型 Agent 数据默认并入无选择同步**:`model-responses/`、`checkpoints/` 与密钥文件需要保持独立数据集。 +7. **不要把敏感/重型 Agent 数据默认并入无选择同步**:`model-responses/` 与密钥文件需要保持独立数据集。 8. **不要绕过 Sync Panel 的持久化 selection**:前端显示、保存、命令参数必须围绕 `DatasetSelection`;不要在 UI 中复制路径规则或用 manifest omission 伪装范围选择。 9. **不要把覆盖策略移回服务端配置**:它是逻辑发起方的作业输入;LAN push 必须透传发起方策略,同时保留目标端对 Sync mode 的所有权。 10. **不要绕过 Dataset boundary 清理目录**:Mirror 删除只能清理 Dataset catalog 为 delete path 推导的 boundary 内的 fileless 祖先树;不得改用物理 data root、手写路径表或 `remove_dir_all`。 diff --git a/docs/FrontendHostContract.md b/docs/FrontendHostContract.md index 8174229f..98514a57 100644 --- a/docs/FrontendHostContract.md +++ b/docs/FrontendHostContract.md @@ -169,7 +169,7 @@ - `api.agent`:TauriTavern Agent Run API。用于启动 Agent Run、订阅 run event、取消、审批工具、读取 workspace 文件/diff、rollback。 - 详细参考见:`docs/API/Agent.md`。 - 当前已落地 Host ABI:`startRunFromLegacyGenerate()`、`startRunWithPromptSnapshot()`、`cancel()`、`readEvents()`、`readWorkspaceFile()`、`readModelTurn()`、`subscribe()`。 - - Chat commit 由 pre-explicit 文本文件 mutation policy 或模型显式调用 `workspace.commit` 触发;两者产生相同的 `chat_commit_requested`,前端内部 host bridge 读取事件绑定的不可变 checkpoint 与对应 Model Turn 的可见 reasoning,先按完整 raw 目标文本应用上游生成输出保存前后处理,再通过上游 `saveReply()` 写同一消息楼层、把累计 reasoning 写入标准 `extra.reasoning`,最后调用 `resolve_agent_chat_commit`。 + - Chat commit 由每轮结束时最多一次的 pre-explicit 文本 mutation policy 或模型显式调用 `workspace.commit` 触发;两者产生相同的 `chat_commit_requested`,前端内部 host bridge 重读当前 workspace 文件并校验事件 SHA,同时读取对应 Model Turn 的可见 reasoning,先按完整 raw 目标文本应用上游生成输出保存前后处理,再通过上游 `saveReply()` 写同一消息楼层、把累计 reasoning 写入标准 `extra.reasoning`,最后调用 `resolve_agent_chat_commit`。 - `persistStateId` 只在 persistent state 已经落盘后写入 chat metadata;host bridge 响应 `persistent_state_metadata_update_requested` 后调用 `resolve_agent_persistent_state_metadata_update`。 - `startRunFromLegacyGenerate()` 是当前兼容入口:使用 Legacy dryRun 生成 `promptSnapshot`,再进入 Rust-owned Agent loop。 - `startRunWithPromptSnapshot()` 必须在调用 backend 前解析 `stableChatId`;`workspaceId` 由 `kind + stableChatId` 派生,`runId` 仍表示单次执行。 diff --git a/src-tauri/crates/tauritavern/src/app/composition/repositories.rs b/src-tauri/crates/tauritavern/src/app/composition/repositories.rs index 744111f8..c31bf890 100644 --- a/src-tauri/crates/tauritavern/src/app/composition/repositories.rs +++ b/src-tauri/crates/tauritavern/src/app/composition/repositories.rs @@ -46,7 +46,6 @@ use tt_ports::repositories::character_repository::CharacterRepository; use tt_ports::repositories::chat_completion_repository::ChatCompletionRepository; use tt_ports::repositories::chat_payload_commit_repository::ChatPayloadCommitRepository; use tt_ports::repositories::chat_repository::ChatRepository; -use tt_ports::repositories::checkpoint_repository::CheckpointRepository; use tt_ports::repositories::content_repository::ContentRepository; use tt_ports::repositories::extension_repository::ExtensionRepository; use tt_ports::repositories::extension_store_repository::ExtensionStoreRepository; @@ -116,7 +115,6 @@ pub(in crate::app::composition) struct AppRepositories { Arc, pub(in crate::app::composition) mcp_server_repository: Arc, pub(in crate::app::composition) workspace_repository: Arc, - pub(in crate::app::composition) checkpoint_repository: Arc, pub(in crate::app::composition) chat_completion_repository: Arc, pub(in crate::app::composition) provider_metadata_repository: Arc, @@ -278,7 +276,6 @@ pub(super) async fn build( let agent_invocation_repository: Arc = file_agent_repository.clone(); let workspace_repository: Arc = file_agent_repository.clone(); - let checkpoint_repository: Arc = file_agent_repository.clone(); let agent_workspace_lifecycle_repository: Arc = file_agent_repository; @@ -358,7 +355,6 @@ pub(super) async fn build( local_endpoint_grant_repository, mcp_server_repository, workspace_repository, - checkpoint_repository, chat_completion_repository, provider_metadata_repository, tokenizer_repository, diff --git a/src-tauri/crates/tauritavern/src/app/composition/services/agent.rs b/src-tauri/crates/tauritavern/src/app/composition/services/agent.rs index 518ac18e..410be739 100644 --- a/src-tauri/crates/tauritavern/src/app/composition/services/agent.rs +++ b/src-tauri/crates/tauritavern/src/app/composition/services/agent.rs @@ -53,7 +53,6 @@ pub(super) fn build( repositories.agent_run_repository.clone(), repositories.agent_invocation_repository.clone(), repositories.workspace_repository.clone(), - repositories.checkpoint_repository.clone(), repositories.chat_repository.clone(), repositories.group_chat_repository.clone(), skill_service, diff --git a/src-tauri/crates/tauritavern/src/app/contract_tests/agent_runtime/execution.rs b/src-tauri/crates/tauritavern/src/app/contract_tests/agent_runtime/execution.rs index b0244882..42b46188 100644 --- a/src-tauri/crates/tauritavern/src/app/contract_tests/agent_runtime/execution.rs +++ b/src-tauri/crates/tauritavern/src/app/contract_tests/agent_runtime/execution.rs @@ -467,7 +467,7 @@ async fn agent_runtime_agent_list_discovers_callable_profiles_with_real_reposito } #[tokio::test] -async fn agent_runtime_foreground_auto_commits_until_explicit_commit() { +async fn agent_runtime_foreground_auto_commits_once_per_round_until_explicit_commit() { let root = temp_root("agent-foreground"); let fixture = agent_runtime_fixture_with_responses( &root, @@ -517,7 +517,7 @@ async fn agent_runtime_foreground_auto_commits_until_explicit_commit() { let request = chat_request("write, revise, commit, and finish"); let prompt_snapshot = json!({ "chatCompletionPayload": request.payload.clone() }); let (_cancel_sender, mut cancel_receiver) = watch::channel(false); - let rejected_commit_calls = ["call_write", "call_commit"]; + let rejected_commit_calls = ["call_commit"]; execute_agent_loop_with_host_resolver( fixture.service.clone(), @@ -560,15 +560,22 @@ async fn agent_runtime_foreground_auto_commits_until_explicit_commit() { .iter() .filter(|event| event.event_type == "chat_commit_requested") .collect::>(); - assert_eq!(commit_requests.len(), 4); - for request in &commit_requests[..2] { - assert_eq!(request.payload["path"], "output/main.md"); - assert_eq!(request.payload["mode"], "replace"); - assert!(request.payload["sha256"].as_str().is_some()); - } - assert_ne!( - commit_requests[0].payload["sha256"], - commit_requests[1].payload["sha256"] + assert_eq!(commit_requests.len(), 3); + let automatic_commit_request = commit_requests + .iter() + .find(|event| event.payload["callId"] == "call_patch") + .expect("the round must auto-commit only its final text mutation"); + assert_eq!(automatic_commit_request.payload["path"], "output/main.md"); + assert_eq!(automatic_commit_request.payload["mode"], "replace"); + assert!( + automatic_commit_request.payload["sha256"] + .as_str() + .is_some() + ); + assert!( + commit_requests + .iter() + .all(|event| event.payload["callId"] != "call_write") ); let final_commit_request = commit_requests.last().unwrap(); assert_eq!(final_commit_request.payload["runId"], run.id); @@ -585,7 +592,7 @@ async fn agent_runtime_foreground_auto_commits_until_explicit_commit() { .iter() .filter(|event| event.event_type == "chat_commit_failed") .collect::>(); - assert_eq!(commit_failures.len(), 2); + assert_eq!(commit_failures.len(), 1); assert!( commit_failures .iter() @@ -601,13 +608,6 @@ async fn agent_runtime_foreground_auto_commits_until_explicit_commit() { explicit_commit_failure.payload["errorCode"], "agent.chat_commit_rejected" ); - assert_eq!( - events - .iter() - .filter(|event| event.event_type == "checkpoint_created") - .count(), - 5 - ); assert_eq!( events .iter() diff --git a/src-tauri/crates/tauritavern/src/app/contract_tests/mod.rs b/src-tauri/crates/tauritavern/src/app/contract_tests/mod.rs index 482006c5..19fcdd3f 100644 --- a/src-tauri/crates/tauritavern/src/app/contract_tests/mod.rs +++ b/src-tauri/crates/tauritavern/src/app/contract_tests/mod.rs @@ -75,7 +75,6 @@ use tt_ports::repositories::agent_profile_storage_health_repository::AgentProfil use tt_ports::repositories::agent_run_repository::{AgentRunEventReadQuery, AgentRunRepository}; use tt_ports::repositories::agent_workspace_lifecycle_repository::AgentWorkspaceLifecycleRepository; use tt_ports::repositories::chat_repository::ChatRepository; -use tt_ports::repositories::checkpoint_repository::CheckpointRepository; use tt_ports::repositories::group_chat_repository::GroupChatRepository; use tt_ports::repositories::preset_repository::PresetRepository; use tt_ports::repositories::workspace_repository::WorkspaceRepository; @@ -225,7 +224,6 @@ fn agent_runtime_fixture_with_results( agent_repository.clone() as Arc, agent_repository.clone() as Arc, agent_repository.clone() as Arc, - agent_repository.clone() as Arc, chat_file_repository.clone() as Arc, chat_file_repository.clone() as Arc, skill_service, diff --git a/src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/checkpoint_store.rs b/src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/checkpoint_store.rs deleted file mode 100644 index d0f7b1da..00000000 --- a/src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/checkpoint_store.rs +++ /dev/null @@ -1,166 +0,0 @@ -use async_trait::async_trait; -use chrono::Utc; -use tokio::fs; - -use super::FileAgentRepository; -use super::fs_tree::{sha256_hex, workspace_file_from_text}; -use super::paths::validate_segment; -use tt_domain::errors::DomainError; -use tt_domain::models::agent::{Checkpoint, CheckpointFile, WorkspacePath}; -use tt_ports::repositories::checkpoint_repository::CheckpointRepository; -use tt_ports::repositories::workspace_repository::WorkspaceFile; - -#[async_trait] -impl CheckpointRepository for FileAgentRepository { - async fn create_checkpoint( - &self, - run_id: &str, - reason: &str, - event_seq: u64, - snapshot_files: &[WorkspaceFile], - ) -> Result { - let _guard = self.checkpoint_lock.lock().await; - let run_dir = self.load_run_dir(run_id).await?; - let checkpoints_dir = run_dir.join("checkpoints"); - fs::create_dir_all(&checkpoints_dir) - .await - .map_err(|error| { - DomainError::InternalError(format!( - "Failed to create checkpoint directory {}: {}", - checkpoints_dir.display(), - error - )) - })?; - - let mut next_seq = 1_u64; - let mut entries = fs::read_dir(&checkpoints_dir).await.map_err(|error| { - DomainError::InternalError(format!( - "Failed to read checkpoint directory {}: {}", - checkpoints_dir.display(), - error - )) - })?; - while let Some(entry) = entries.next_entry().await.map_err(|error| { - DomainError::InternalError(format!( - "Failed to read checkpoint directory entry {}: {}", - checkpoints_dir.display(), - error - )) - })? { - if entry - .file_type() - .await - .map(|file_type| file_type.is_dir()) - .unwrap_or(false) - { - next_seq += 1; - } - } - - let checkpoint_id = format!("cp_{next_seq:06}"); - let checkpoint_dir = checkpoints_dir.join(&checkpoint_id); - fs::create_dir_all(&checkpoint_dir).await.map_err(|error| { - DomainError::InternalError(format!( - "Failed to create checkpoint {}: {}", - checkpoint_dir.display(), - error - )) - })?; - - let mut files = Vec::new(); - for file in snapshot_files { - let bytes = file.text.as_bytes(); - let target = checkpoint_dir.join(file.path.as_str()); - if let Some(parent) = target.parent() { - fs::create_dir_all(parent).await.map_err(|error| { - DomainError::InternalError(format!( - "Failed to create checkpoint file parent {}: {}", - parent.display(), - error - )) - })?; - } - fs::write(&target, &bytes).await.map_err(|error| { - DomainError::InternalError(format!( - "Failed to write checkpoint file {}: {}", - target.display(), - error - )) - })?; - - files.push(CheckpointFile { - path: file.path.as_str().to_string(), - sha256: sha256_hex(bytes), - bytes: bytes.len() as u64, - }); - } - - let checkpoint = Checkpoint { - id: checkpoint_id, - seq: next_seq, - run_id: run_id.to_string(), - created_at: Utc::now(), - reason: reason.to_string(), - event_seq, - files, - }; - Self::write_json_atomic(&checkpoint_dir.join("checkpoint.json"), &checkpoint).await?; - - Ok(checkpoint) - } - - async fn read_checkpoint_text( - &self, - run_id: &str, - checkpoint_id: &str, - path: &WorkspacePath, - ) -> Result { - validate_segment(checkpoint_id, "checkpoint_id")?; - let checkpoint_dir = self - .load_run_dir(run_id) - .await? - .join("checkpoints") - .join(checkpoint_id); - let checkpoint: Checkpoint = - Self::read_json(&checkpoint_dir.join("checkpoint.json")).await?; - if checkpoint.run_id != run_id { - return Err(DomainError::InvalidData(format!( - "Checkpoint `{checkpoint_id}` belongs to another run" - ))); - } - let expected = checkpoint - .files - .iter() - .find(|file| file.path == path.as_str()) - .ok_or_else(|| { - DomainError::NotFound(format!( - "Checkpoint file not found: {checkpoint_id}/{}", - path.as_str() - )) - })?; - let target = checkpoint_dir.join(path.as_str()); - let text = fs::read_to_string(&target).await.map_err(|error| { - if error.kind() == std::io::ErrorKind::NotFound { - DomainError::NotFound(format!( - "Checkpoint file not found: {checkpoint_id}/{}", - path.as_str() - )) - } else if error.kind() == std::io::ErrorKind::InvalidData { - DomainError::workspace_file_not_text(path.as_str()) - } else { - DomainError::InternalError(format!( - "Failed to read checkpoint file {}: {error}", - target.display() - )) - } - })?; - let file = workspace_file_from_text(path.clone(), text)?; - if file.sha256 != expected.sha256 { - return Err(DomainError::InvalidData(format!( - "Checkpoint file hash mismatch: {checkpoint_id}/{}", - path.as_str() - ))); - } - Ok(file) - } -} diff --git a/src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/mod.rs b/src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/mod.rs index 7a861b33..cadc9306 100644 --- a/src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/mod.rs +++ b/src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/mod.rs @@ -1,4 +1,3 @@ -mod checkpoint_store; mod event_journal; mod fs_tree; mod invocation_store; @@ -28,7 +27,6 @@ use tt_domain::errors::DomainError; pub struct FileAgentRepository { pub(super) root: PathBuf, pub(super) event_lock: Arc>, - pub(super) checkpoint_lock: Arc>, pub(super) persist_lock: Arc>, pub(super) workspace_write_locks: Arc>>>>, } @@ -38,7 +36,6 @@ impl FileAgentRepository { Self { root, event_lock: Arc::new(Mutex::new(())), - checkpoint_lock: Arc::new(Mutex::new(())), persist_lock: Arc::new(Mutex::new(())), workspace_write_locks: Arc::new(Mutex::new(HashMap::new())), } diff --git a/src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/paths.rs b/src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/paths.rs index 337300e9..ebc467fb 100644 --- a/src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/paths.rs +++ b/src-tauri/crates/tt-adapter-storage-userdata/src/repositories/file_agent_repository/paths.rs @@ -173,7 +173,6 @@ pub(super) fn validate_workspace_root_path(value: &str) -> Result PathBuf { std::env::temp_dir().join(format!("tauritavern-agent-repo-{}", Uuid::new_v4())) @@ -187,7 +186,7 @@ fn sample_resolved_profile(manifest: &WorkspaceManifest) -> ResolvedAgentProfile } #[tokio::test] -async fn repository_round_trips_run_workspace_event_and_checkpoint() { +async fn repository_round_trips_run_workspace_and_event() { let root = temp_root(); let repository = FileAgentRepository::new(root.clone()); let run = sample_run(); @@ -240,23 +239,8 @@ async fn repository_round_trips_run_workspace_event_and_checkpoint() { let file = repository .read_text(&run.id, &path) .await - .expect("read checkpoint source"); - let checkpoint = repository - .create_checkpoint(&run.id, "test", event.seq, std::slice::from_ref(&file)) - .await - .expect("checkpoint"); - assert_eq!(checkpoint.files[0].bytes, 5); - - repository - .write_text(&run.id, &path, "newer") - .await - .expect("replace live workspace file"); - let snapshot = repository - .read_checkpoint_text(&run.id, &checkpoint.id, &path) - .await - .expect("read checkpoint text"); - assert_eq!(snapshot.text, "hello"); - assert_eq!(snapshot.sha256, checkpoint.files[0].sha256); + .expect("read workspace file"); + assert_eq!(file.text, "hello"); fs::remove_dir_all(root).await.expect("cleanup"); } diff --git a/src-tauri/crates/tt-application/src/dto/agent_dto.rs b/src-tauri/crates/tt-application/src/dto/agent_dto.rs index 6decea9b..976ecefd 100644 --- a/src-tauri/crates/tt-application/src/dto/agent_dto.rs +++ b/src-tauri/crates/tt-application/src/dto/agent_dto.rs @@ -595,8 +595,6 @@ pub struct AgentRunTimelineDelegationEdgeDto { pub struct AgentReadWorkspaceFileDto { pub run_id: String, pub path: String, - #[serde(default)] - pub checkpoint_id: Option, } #[derive(Debug, Clone, Deserialize)] diff --git a/src-tauri/crates/tt-application/src/services/agent_runtime_service.rs b/src-tauri/crates/tt-application/src/services/agent_runtime_service.rs index 6f449831..94162699 100644 --- a/src-tauri/crates/tt-application/src/services/agent_runtime_service.rs +++ b/src-tauri/crates/tt-application/src/services/agent_runtime_service.rs @@ -31,7 +31,6 @@ use tt_domain::models::tool::{ use tt_ports::repositories::agent_invocation_repository::AgentInvocationRepository; use tt_ports::repositories::agent_run_repository::AgentRunRepository; use tt_ports::repositories::chat_repository::ChatRepository; -use tt_ports::repositories::checkpoint_repository::CheckpointRepository; use tt_ports::repositories::group_chat_repository::GroupChatRepository; use tt_ports::repositories::workspace_repository::WorkspaceRepository; @@ -115,7 +114,6 @@ pub struct AgentRuntimeService { run_repository: Arc, invocation_repository: Arc, workspace_repository: Arc, - checkpoint_repository: Arc, chat_repository: Arc, group_chat_repository: Arc, model_gateway: Arc, @@ -142,7 +140,6 @@ impl AgentRuntimeService { run_repository: Arc, invocation_repository: Arc, workspace_repository: Arc, - checkpoint_repository: Arc, chat_repository: Arc, group_chat_repository: Arc, skill_service: Arc, @@ -164,7 +161,6 @@ impl AgentRuntimeService { run_repository, invocation_repository, workspace_repository, - checkpoint_repository, chat_repository, group_chat_repository, model_gateway, diff --git a/src-tauri/crates/tt-application/src/services/agent_runtime_service/commit.rs b/src-tauri/crates/tt-application/src/services/agent_runtime_service/commit.rs index 8975bf39..22628a31 100644 --- a/src-tauri/crates/tt-application/src/services/agent_runtime_service/commit.rs +++ b/src-tauri/crates/tt-application/src/services/agent_runtime_service/commit.rs @@ -19,7 +19,7 @@ use crate::services::agent_tools::{ use tt_domain::errors::DomainError; use tt_domain::models::agent::{ AgentChatCommitMode, AgentInvocationStatus, AgentRun, AgentRunEventLevel, AgentRunStatus, - AgentToolResult, ArtifactTarget, Checkpoint, WorkspacePath, WorkspacePersistentChangeSet, + AgentToolResult, ArtifactTarget, WorkspacePath, WorkspacePersistentChangeSet, }; use tt_domain::models::tool::ToolInvocation; use tt_domain::text_metrics::TextMetrics; @@ -36,9 +36,9 @@ enum HostChatCommitOutcome { } struct HostChatCommit<'a> { - call: &'a ToolInvocation, + call_id: &'a str, file: &'a WorkspaceFile, - checkpoint: Option, + is_explicit: bool, mode: AgentChatCommitMode, reason: Option, round: usize, @@ -180,9 +180,9 @@ impl AgentRuntimeService { .perform_host_chat_commit( run_id, HostChatCommit { - call, + call_id: call.call_id.as_str(), file: &file, - checkpoint: None, + is_explicit: true, mode, reason, round, @@ -239,14 +239,13 @@ impl AgentRuntimeService { #[expect( clippy::too_many_arguments, - reason = "automatic commit forwards one immutable mutation to the shared host boundary" + reason = "automatic commit forwards the round's final immutable mutation to the shared host boundary" )] pub(super) async fn auto_commit_text_file_if_eligible( &self, run_id: &str, - call: &ToolInvocation, + call_id: &str, file: &WorkspaceFile, - checkpoint: Checkpoint, round: usize, invocation_id: &str, commit_ledger: &mut RunCommitLedger, @@ -262,9 +261,9 @@ impl AgentRuntimeService { self.perform_host_chat_commit( run_id, HostChatCommit { - call, + call_id, file, - checkpoint: Some(checkpoint), + is_explicit: false, mode: AgentChatCommitMode::Replace, reason: None, round, @@ -299,59 +298,16 @@ impl AgentRuntimeService { cancel: &mut AgentCancelReceiver, ) -> Result { let HostChatCommit { - call, + call_id, file, - checkpoint, + is_explicit, mode, reason, round, invocation_id, } = commit; - let is_explicit = checkpoint.is_none(); let run = self.run_repository.load_run(run_id).await?; let commit_id = format!("commit_{}", Uuid::new_v4().simple()); - let started = self - .event( - run_id, - AgentRunEventLevel::Info, - "chat_commit_started", - json!({ - "commitId": commit_id.as_str(), - "invocationId": invocation_id, - "callId": call.call_id.as_str(), - "path": file.path.as_str(), - "mode": mode, - "reason": reason.as_deref(), - }), - ) - .await?; - let checkpoint = match checkpoint { - Some(checkpoint) => checkpoint, - None => { - self.transition_status(run_id, AgentRunStatus::CreatingCheckpoint) - .await?; - let checkpoint = self - .checkpoint_repository - .create_checkpoint( - run_id, - "chat_commit", - started.seq, - std::slice::from_ref(file), - ) - .await?; - self.event( - run_id, - AgentRunEventLevel::Info, - "checkpoint_created", - json!({ - "checkpointId": checkpoint.id.as_str(), - "reason": "chat_commit", - }), - ) - .await?; - checkpoint - } - }; let (sender, receiver) = oneshot::channel(); let previous = self.active_chat_commits.write().await.insert( @@ -377,7 +333,7 @@ impl AgentRuntimeService { json!({ "commitId": commit_id, "invocationId": invocation_id, - "callId": call.call_id.as_str(), + "callId": call_id, "runId": run.id.as_str(), "workspaceId": run.workspace_id.as_str(), "stableChatId": run.stable_chat_id.as_str(), @@ -391,7 +347,6 @@ impl AgentRuntimeService { "chars": file_metrics.chars, "words": file_metrics.words, "sha256": file.sha256.as_str(), - "checkpointId": checkpoint.id.as_str(), }), ) .await?; @@ -430,7 +385,7 @@ impl AgentRuntimeService { json!({ "commitId": commit_id, "invocationId": invocation_id, - "callId": call.call_id.as_str(), + "callId": call_id, "path": file.path.as_str(), "mode": mode, "messageId": result.message_id.as_deref(), @@ -467,7 +422,7 @@ impl AgentRuntimeService { json!({ "commitId": commit_id, "invocationId": invocation_id, - "callId": call.call_id.as_str(), + "callId": call_id, "path": file.path.as_str(), "mode": mode, "message": message.as_str(), diff --git a/src-tauri/crates/tt-application/src/services/agent_runtime_service/journal.rs b/src-tauri/crates/tt-application/src/services/agent_runtime_service/journal.rs index 0a0c7e6b..d2763b2a 100644 --- a/src-tauri/crates/tt-application/src/services/agent_runtime_service/journal.rs +++ b/src-tauri/crates/tt-application/src/services/agent_runtime_service/journal.rs @@ -4,10 +4,7 @@ use serde_json::{Map, Value, json}; use super::{AgentCancelReceiver, AgentRuntimeService}; use crate::errors::ApplicationError; use tt_domain::errors::DomainError; -use tt_domain::models::agent::{ - AgentRun, AgentRunEvent, AgentRunEventLevel, AgentRunStatus, Checkpoint, -}; -use tt_ports::repositories::workspace_repository::WorkspaceFile; +use tt_domain::models::agent::{AgentRun, AgentRunEvent, AgentRunEventLevel, AgentRunStatus}; impl AgentRuntimeService { pub(super) async fn transition_status( @@ -52,36 +49,6 @@ impl AgentRuntimeService { } Ok(()) } - - pub(super) async fn checkpoint_workspace_file( - &self, - run_id: &str, - update_run_status: bool, - reason: &str, - event_type: &str, - payload: Value, - file: &WorkspaceFile, - ) -> Result { - if update_run_status { - self.transition_status(run_id, AgentRunStatus::CreatingCheckpoint) - .await?; - } - let event = self - .event(run_id, AgentRunEventLevel::Info, event_type, payload) - .await?; - let checkpoint = self - .checkpoint_repository - .create_checkpoint(run_id, reason, event.seq, std::slice::from_ref(file)) - .await?; - self.event( - run_id, - AgentRunEventLevel::Info, - "checkpoint_created", - json!({ "checkpointId": checkpoint.id, "reason": reason }), - ) - .await?; - Ok(checkpoint) - } } fn with_canonical_event_scope(event_type: &str, payload: Value) -> Result { diff --git a/src-tauri/crates/tt-application/src/services/agent_runtime_service/lifecycle.rs b/src-tauri/crates/tt-application/src/services/agent_runtime_service/lifecycle.rs index 8de516e5..e41c9c9c 100644 --- a/src-tauri/crates/tt-application/src/services/agent_runtime_service/lifecycle.rs +++ b/src-tauri/crates/tt-application/src/services/agent_runtime_service/lifecycle.rs @@ -325,18 +325,10 @@ impl AgentRuntimeService { dto: AgentReadWorkspaceFileDto, ) -> Result { let path = WorkspacePath::parse(dto.path)?; - let file = match dto.checkpoint_id.as_deref() { - Some(checkpoint_id) => { - self.checkpoint_repository - .read_checkpoint_text(&dto.run_id, checkpoint_id, &path) - .await? - } - None => { - self.workspace_repository - .read_text(&dto.run_id, &path) - .await? - } - }; + let file = self + .workspace_repository + .read_text(&dto.run_id, &path) + .await?; let metrics = TextMetrics::from_text(&file.text); Ok(AgentWorkspaceFileDto { diff --git a/src-tauri/crates/tt-application/src/services/agent_runtime_service/loop_runner.rs b/src-tauri/crates/tt-application/src/services/agent_runtime_service/loop_runner.rs index c9313223..d248b6bb 100644 --- a/src-tauri/crates/tt-application/src/services/agent_runtime_service/loop_runner.rs +++ b/src-tauri/crates/tt-application/src/services/agent_runtime_service/loop_runner.rs @@ -144,7 +144,6 @@ impl AgentRuntimeService { let direct_output_path = self .capture_direct_output( run_id, - updates_run_status, round, model_response_path.as_str(), &response, @@ -199,6 +198,7 @@ impl AgentRuntimeService { let mut handoff = None; let tool_call_count = tool_calls.len(); let completion_tool = completion_tool_name(exit_policy, &prepared.tool_turn); + let mut text_mutation = None; for (index, call) in tool_calls.into_iter().enumerate() { if finished { @@ -217,28 +217,25 @@ impl AgentRuntimeService { cancel, ) .await?; - let mut text_mutation = None; - match &outcome.effect { + let result = outcome.result; + match outcome.effect { AgentToolEffect::WorkspaceFileWritten { file, mode } => { let metrics = TextMetrics::from_text(&file.text); - let checkpoint = self - .checkpoint_workspace_file( - run_id, - updates_run_status, - "tool_workspace_write", - "workspace_file_written", - json!({ - "invocationId": invocation_id, - "path": file.path.as_str(), - "mode": mode, - "chars": metrics.chars, - "words": metrics.words, - "sha256": file.sha256.as_str(), - }), - file, - ) - .await?; - text_mutation = Some((file, checkpoint)); + self.event( + run_id, + AgentRunEventLevel::Info, + "workspace_file_written", + json!({ + "invocationId": invocation_id, + "path": file.path.as_str(), + "mode": mode, + "chars": metrics.chars, + "words": metrics.words, + "sha256": file.sha256.as_str(), + }), + ) + .await?; + text_mutation = Some((call.call_id, file)); } AgentToolEffect::WorkspaceFilePatched { file, @@ -250,25 +247,22 @@ impl AgentRuntimeService { .await?; } let metrics = TextMetrics::from_text(&file.text); - let checkpoint = self - .checkpoint_workspace_file( - run_id, - updates_run_status, - "tool_workspace_patch", - "workspace_patch_applied", - json!({ - "invocationId": invocation_id, - "path": file.path.as_str(), - "chars": metrics.chars, - "words": metrics.words, - "oldSha256": old_sha256, - "sha256": file.sha256.as_str(), - "replacements": replacements, - }), - file, - ) - .await?; - text_mutation = Some((file, checkpoint)); + self.event( + run_id, + AgentRunEventLevel::Info, + "workspace_patch_applied", + json!({ + "invocationId": invocation_id, + "path": file.path.as_str(), + "chars": metrics.chars, + "words": metrics.words, + "oldSha256": old_sha256, + "sha256": file.sha256.as_str(), + "replacements": replacements, + }), + ) + .await?; + text_mutation = Some((call.call_id, file)); } AgentToolEffect::ChatCommitRequested { .. } => {} AgentToolEffect::Finish => { @@ -279,7 +273,7 @@ impl AgentRuntimeService { result_ref, summary, } => { - let metrics = TextMetrics::from_text(summary); + let metrics = TextMetrics::from_text(&summary); self.event( run_id, AgentRunEventLevel::Info, @@ -300,7 +294,7 @@ impl AgentRuntimeService { new_invocation_id, .. } => { - handoff = Some((task_id.clone(), new_invocation_id.clone())); + handoff = Some((task_id, new_invocation_id)); self.finish_invocation( run_id, invocation_id, @@ -311,24 +305,24 @@ impl AgentRuntimeService { } AgentToolEffect::None => {} } - if auto_commit_text_mutations && let Some((file, checkpoint)) = text_mutation { - self.auto_commit_text_file_if_eligible( - run_id, - &call, - file, - checkpoint, - round, - invocation_id, - commit_ledger, - cancel, - ) - .await?; - } - tool_results.push(outcome.result); + tool_results.push(result); self.ensure_not_cancelled(cancel)?; } + if auto_commit_text_mutations && let Some((call_id, file)) = text_mutation { + self.auto_commit_text_file_if_eligible( + run_id, + &call_id, + &file, + round, + invocation_id, + commit_ledger, + cancel, + ) + .await?; + } + if finished { self.event( run_id, @@ -377,7 +371,6 @@ impl AgentRuntimeService { async fn capture_direct_output( &self, run_id: &str, - update_run_status: bool, round: usize, model_response_path: &str, response: &AgentModelResponse, @@ -394,10 +387,9 @@ impl AgentRuntimeService { .write_text(run_id, &path, text) .await?; let metrics = TextMetrics::from_text(&file.text); - self.checkpoint_workspace_file( + self.event( run_id, - update_run_status, - "direct_output_capture", + AgentRunEventLevel::Info, "direct_output_captured", json!({ "round": round, @@ -407,7 +399,6 @@ impl AgentRuntimeService { "sha256": file.sha256.as_str(), "modelResponsePath": model_response_path, }), - &file, ) .await?; diff --git a/src-tauri/crates/tt-domain/src/models/agent/mod.rs b/src-tauri/crates/tt-domain/src/models/agent/mod.rs index 1dff9912..20f5aea1 100644 --- a/src-tauri/crates/tt-domain/src/models/agent/mod.rs +++ b/src-tauri/crates/tt-domain/src/models/agent/mod.rs @@ -40,7 +40,6 @@ pub enum AgentRunStatus { CallingModel, DispatchingTool, ApplyingWorkspacePatch, - CreatingCheckpoint, AwaitingHostCommit, Finishing, Completed, @@ -504,26 +503,6 @@ pub struct CommitPolicy { pub store_artifacts_in_extra: bool, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Checkpoint { - pub id: String, - pub seq: u64, - pub run_id: String, - pub created_at: DateTime, - pub reason: String, - pub event_seq: u64, - pub files: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CheckpointFile { - pub path: String, - pub sha256: String, - pub bytes: u64, -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct WorkspacePersistentChangeSet { diff --git a/src-tauri/crates/tt-domain/src/models/agent/storage.rs b/src-tauri/crates/tt-domain/src/models/agent/storage.rs index 7196afd9..f5800531 100644 --- a/src-tauri/crates/tt-domain/src/models/agent/storage.rs +++ b/src-tauri/crates/tt-domain/src/models/agent/storage.rs @@ -9,7 +9,6 @@ pub enum AgentRunStorageClass { WorkspaceScratch, Tasks, ModelResponses, - Checkpoints, OtherRunArtifact, } @@ -33,7 +32,6 @@ impl AgentRunStorageClass { "scratch" => Self::WorkspaceScratch, "tasks" => Self::Tasks, "model-responses" => Self::ModelResponses, - "checkpoints" => Self::Checkpoints, _ => Self::OtherRunArtifact, } } @@ -125,10 +123,6 @@ mod tests { AgentRunStorageClass::from_run_relative_path("model-responses/round-001.json"), AgentRunStorageClass::ModelResponses ); - assert_eq!( - AgentRunStorageClass::from_run_relative_path("checkpoints/cp-1.json"), - AgentRunStorageClass::Checkpoints - ); } #[test] diff --git a/src-tauri/crates/tt-ports/src/repositories/checkpoint_repository.rs b/src-tauri/crates/tt-ports/src/repositories/checkpoint_repository.rs deleted file mode 100644 index 48d738d6..00000000 --- a/src-tauri/crates/tt-ports/src/repositories/checkpoint_repository.rs +++ /dev/null @@ -1,23 +0,0 @@ -use async_trait::async_trait; - -use super::workspace_repository::WorkspaceFile; -use tt_domain::errors::DomainError; -use tt_domain::models::agent::{Checkpoint, WorkspacePath}; - -#[async_trait] -pub trait CheckpointRepository: Send + Sync { - async fn create_checkpoint( - &self, - run_id: &str, - reason: &str, - event_seq: u64, - files: &[WorkspaceFile], - ) -> Result; - - async fn read_checkpoint_text( - &self, - run_id: &str, - checkpoint_id: &str, - path: &WorkspacePath, - ) -> Result; -} diff --git a/src-tauri/crates/tt-ports/src/repositories/mod.rs b/src-tauri/crates/tt-ports/src/repositories/mod.rs index 19dfd3e6..181f788b 100644 --- a/src-tauri/crates/tt-ports/src/repositories/mod.rs +++ b/src-tauri/crates/tt-ports/src/repositories/mod.rs @@ -12,7 +12,6 @@ pub mod chat_completion_repository; pub mod chat_payload_commit_repository; pub mod chat_repository; pub mod chat_types; -pub mod checkpoint_repository; pub mod content_repository; pub mod extension_repository; pub mod extension_store_repository; diff --git a/src/tauri/main/api/agent-chat-commit-bridge.js b/src/tauri/main/api/agent-chat-commit-bridge.js index 84078a88..7517b24a 100644 --- a/src/tauri/main/api/agent-chat-commit-bridge.js +++ b/src/tauri/main/api/agent-chat-commit-bridge.js @@ -102,11 +102,10 @@ async function handleChatCommitRequested({ state, event, safeInvoke, readWorkspa try { await assertCurrentChat(payload.chatRef, payload.stableChatId); const path = requirePayloadString(payload, 'path'); - const checkpointId = requirePayloadString(payload, 'checkpointId'); const mode = normalizeCommitMode(payload.mode); - const file = await readWorkspaceFile({ runId: state.runId, checkpointId, path }); + const file = await readWorkspaceFile({ runId: state.runId, path }); if (file?.sha256 !== requirePayloadString(payload, 'sha256')) { - throw new Error('agent.chat_commit_checkpoint_mismatch: checkpoint content does not match commit request'); + throw new Error('agent.chat_commit_workspace_changed: workspace content changed before commit'); } const reasoning = await prepareCommitReasoning(state.reasoning, state.runId, readModelTurn); const script = await state.loadScript(); @@ -314,7 +313,6 @@ function mergeAgentCommitExtraIntoMessage(chat, messageId, payload, file, commit const commit = { seq: commitSeq, commitId: payload.commitId, - checkpointId: payload.checkpointId, path: file.path, mode: normalizeCommitMode(payload.mode), reason: typeof payload.reason === 'string' ? payload.reason : undefined, @@ -342,7 +340,6 @@ function mergeAgentCommitExtraIntoMessage(chat, messageId, payload, file, commit profileId: payload.profileId ?? null, persistBaseStateId: payload.persistBaseStateId ?? null, persistStateStatus: 'not_committed', - checkpointId: payload.checkpointId, commitId: payload.commitId, commitSeq, commits: [...previousCommits, commit], diff --git a/src/tauri/main/api/agent-run-runtime.js b/src/tauri/main/api/agent-run-runtime.js index e641729c..ad4a908e 100644 --- a/src/tauri/main/api/agent-run-runtime.js +++ b/src/tauri/main/api/agent-run-runtime.js @@ -12,7 +12,6 @@ const AGENT_RUN_STATUSES = new Set([ 'calling_model', 'dispatching_tool', 'applying_workspace_patch', - 'creating_checkpoint', 'awaiting_host_commit', 'finishing', 'completed', @@ -139,9 +138,7 @@ export function createAgentRunRuntimeApi({ safeInvoke }) { const runId = requireRunId(input?.runId); const path = String(input?.path || '').trim(); if (!path) throw new Error('path is required'); - const checkpointId = normalizeOptionalString(input?.checkpointId); - if (input?.checkpointId != null && !checkpointId) throw new Error('checkpointId cannot be empty'); - return safeInvoke('read_agent_workspace_file', { dto: { runId, path, ...(checkpointId ? { checkpointId } : {}) } }); + return safeInvoke('read_agent_workspace_file', { dto: { runId, path } }); } async function readModelTurn(input) { diff --git a/src/tauri/main/api/agent.js b/src/tauri/main/api/agent.js index 7e345fde..fd1698e4 100644 --- a/src/tauri/main/api/agent.js +++ b/src/tauri/main/api/agent.js @@ -115,12 +115,6 @@ function createAgentApi({ safeInvoke }) { throw new Error('approveToolCall is not implemented'); }, listRuns: runtime.listRuns, - readDiff() { - throw new Error('readDiff is not implemented'); - }, - rollback() { - throw new Error('rollback is not implemented'); - }, }; } diff --git a/src/types.d.ts b/src/types.d.ts index 09498694..4b8fd29e 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -77,7 +77,6 @@ type TauriTavernAgentRunStatus = | 'calling_model' | 'dispatching_tool' | 'applying_workspace_patch' - | 'creating_checkpoint' | 'awaiting_host_commit' | 'finishing' | 'completed' @@ -585,8 +584,6 @@ type TauriTavernAgentApi = { runs: TauriTavernAgentRunSummary[]; nextCursor?: TauriTavernAgentRunListCursor; }>; - readDiff: () => never; - rollback: () => never; }; type TauriTavernLlmConnectionSummary = { diff --git a/tests/agent-api-contract.test.mjs b/tests/agent-api-contract.test.mjs index f430f82c..f083f76f 100644 --- a/tests/agent-api-contract.test.mjs +++ b/tests/agent-api-contract.test.mjs @@ -81,7 +81,6 @@ function agentCommitPayload(chatRef, overrides = {}) { persistBaseStateId: null, path: 'output/main.md', mode: 'replace', - checkpointId: 'checkpoint-1', sha256: 'sha-19', ...overrides, }; @@ -744,14 +743,14 @@ test('api.agent.retention fails fast on invalid retention inputs', async () => { test('api.agent read projections forward camelCase DTOs and reject invalid input', async () => { const { calls, agent } = await installHarness(); - await agent.readWorkspaceFile({ runId: 'run-1', path: 'plan/plan.md', checkpointId: ' cp_000001 ' }); + await agent.readWorkspaceFile({ runId: 'run-1', path: 'plan/plan.md' }); await agent.readModelTurn({ runId: 'run-1', invocationId: 'inv_child', round: 2, maxChars: 12000 }); await agent.readModelTurn({ runId: 'run-1', round: 3 }); assert.deepEqual(calls, [ { command: 'read_agent_workspace_file', - args: { dto: { runId: 'run-1', path: 'plan/plan.md', checkpointId: 'cp_000001' } }, + args: { dto: { runId: 'run-1', path: 'plan/plan.md' } }, }, { command: 'read_agent_model_turn', @@ -775,10 +774,6 @@ test('api.agent read projections forward camelCase DTOs and reject invalid input () => agent.readModelTurn({ runId: 'run-1', round: 1, maxChars: 0 }), /maxChars must be a positive integer/, ); - await assert.rejects( - () => agent.readWorkspaceFile({ runId: 'run-1', path: 'plan/plan.md', checkpointId: ' ' }), - /checkpointId cannot be empty/, - ); }); test('api.agent.pruneChatPersistentStates forwards explicit candidate state ids', async () => { @@ -908,14 +903,13 @@ test('agent chat commit bridge runs generated output cleanup before saving', asy }]); assert.deepEqual(workspaceReads, [{ runId: 'run-commit-cleanup', - checkpointId: 'checkpoint-1', path: 'output/main.md', }]); assert.deepEqual(saveCalls, [{ type: 'normal', getMessage: 'real', reasoning: '' }]); assert.equal(script.chat[0].mes, 'real'); }); -test('agent chat commit bridge retries failure resolution', async () => { +test('agent chat commit bridge retries failure resolution after workspace SHA mismatch', async () => { const moduleUrl = pathToFileURL(path.join(REPO_ROOT, 'src/tauri/main/api/agent-chat-commit-bridge.js')); moduleUrl.search = `?case=commit-recoverable-${Date.now()}`; const { attachHostCommitBridge } = await import(moduleUrl.href); @@ -934,7 +928,7 @@ test('agent chat commit bridge retries failure resolution', async () => { resolveCommit(args); return {}; }, - readWorkspaceFile: async () => workspaceFile('debug real'), + readWorkspaceFile: async () => workspaceFile('changed'), readModelTurn: async () => { throw new Error('reasoning projection unavailable'); }, subscribe(_runId, handler) { listener = handler; @@ -954,7 +948,7 @@ test('agent chat commit bridge retries failure resolution', async () => { const result = await resolved; assert.equal(resolveAttempt, 2); - assert.match(result.dto.error, /reasoning projection unavailable/); + assert.match(result.dto.error, /workspace content changed before commit/); }); test('agent chat commit bridge preserves applied reasoning across a persistence retry', async () => { diff --git a/tests/rust-composition-contract.test.mjs b/tests/rust-composition-contract.test.mjs index 91cf5182..f516b863 100644 --- a/tests/rust-composition-contract.test.mjs +++ b/tests/rust-composition-contract.test.mjs @@ -27,7 +27,7 @@ test('Rust composition keeps repository sharing explicit', async () => { ); assert.match( source, - /let file_agent_repository = Arc::new\(FileAgentRepository::new\([\s\S]*?let agent_run_repository: Arc = file_agent_repository\.clone\(\);[\s\S]*?let agent_invocation_repository: Arc =\s*file_agent_repository\.clone\(\);[\s\S]*?let workspace_repository: Arc = file_agent_repository\.clone\(\);[\s\S]*?let checkpoint_repository: Arc = file_agent_repository\.clone\(\);[\s\S]*?let agent_workspace_lifecycle_repository: Arc =\s*file_agent_repository;/, + /let file_agent_repository = Arc::new\(FileAgentRepository::new\([\s\S]*?let agent_run_repository: Arc = file_agent_repository\.clone\(\);[\s\S]*?let agent_invocation_repository: Arc =\s*file_agent_repository\.clone\(\);[\s\S]*?let workspace_repository: Arc = file_agent_repository\.clone\(\);[\s\S]*?let agent_workspace_lifecycle_repository: Arc =\s*file_agent_repository;/, ); });