diff --git a/.opencode/plugins/analysis-reminder.mjs b/.opencode/plugins/analysis-reminder.mjs index f18a437..38e534b 100644 --- a/.opencode/plugins/analysis-reminder.mjs +++ b/.opencode/plugins/analysis-reminder.mjs @@ -33,7 +33,7 @@ function describeError(stage, error) { return `${stage}:${status ? `HTTP_${status}:` : ""}${reason}:${message}`; } -/** 使用当前进程的 SDK transport 读取累计金额,每个 attempt 最多提交一次 steer 输入。 */ +/** 使用当前进程的 SDK transport 读取累计金额,每个 attempt 最多保存一次用户提醒。 */ export default async function analysisReminder({ client }, options) { const { attempt_id, policy, socket_path, session_id } = options ?? {}; if (!attempt_id || !socket_path || !policy || policy.version !== 1) { @@ -64,21 +64,23 @@ export default async function analysisReminder({ client }, options) { let checking = false; let submissionStarted = false; return { - "experimental.chat.system.transform": async (input) => { - // prompt admission 可能引起 hook 重入;重入立即返回,不等待当前提交。 + "experimental.chat.messages.transform": async (_input, output) => { + // 提醒保存期间的并发 hook 立即返回,不等待当前提交。 if (checking || submissionStarted) return; - if (state.session_id && input.sessionID !== state.session_id) return; + const user = output.messages.findLast(message => message.info.role === "user")?.info; + const sessionID = user?.sessionID; + if (state.session_id && sessionID !== state.session_id) return; checking = true; let stage = "analysis_reminder_session_read"; try { try { - if (!input.sessionID) throw new Error("analysis_reminder_session_missing"); + if (!sessionID) throw new Error("analysis_reminder_session_missing"); const response = await sdk.v2.session.get( - { sessionID: input.sessionID }, { throwOnError: true }, + { sessionID }, { throwOnError: true }, ); const session = response.data.data; if (session.parentID) return; - if (session.id !== input.sessionID) throw new Error("analysis_reminder_session_mismatch"); + if (session.id !== sessionID) throw new Error("analysis_reminder_session_mismatch"); state.session_id = session.id; if (typeof session.cost !== "number" || !Number.isFinite(session.cost) || session.cost < 0) { throw new Error("analysis_reminder_cost_invalid"); @@ -87,15 +89,23 @@ export default async function analysisReminder({ client }, options) { // 响应丢失时无法确认服务端是否已接收,因此本 attempt 不重复提交。 submissionStarted = true; stage = "analysis_reminder_prompt_submit"; - const admitted = await sdk.v2.session.prompt( - { sessionID: session.id, prompt: { text: reminder }, delivery: "steer" }, + const saved = await sdk.session.prompt( + { + sessionID, noReply: true, parts: [{ type: "text", text: reminder }], + agent: user.agent, model: user.model, variant: user.model.variant, + tools: user.tools, system: user.system, format: user.format, + }, { throwOnError: true }, ); + if (saved.data?.info?.role !== "user" || saved.data.info.sessionID !== sessionID) { + throw new Error("analysis_reminder_message_invalid"); + } + // hook 的消息快照早于保存操作;加入真实消息后,本次模型请求即可读取提醒。 + output.messages.push(saved.data); state.reminder_sent = true; console.info(JSON.stringify({ - event: "analysis_reminder_admitted", attempt_id, - session_id: session.id, message_id: admitted.data.data.id, - admitted_seq: admitted.data.data.admittedSeq, + event: "analysis_reminder_message_included", attempt_id, + session_id: session.id, message_id: saved.data.info.id, })); } state.error = null; diff --git a/openspec/changes/add-model-specific-agent-turn-limits/design.md b/openspec/changes/add-model-specific-agent-turn-limits/design.md index fb35490..bf267a1 100644 --- a/openspec/changes/add-model-specific-agent-turn-limits/design.md +++ b/openspec/changes/add-model-specific-agent-turn-limits/design.md @@ -46,13 +46,13 @@ Server 初始策略如下: ### 3. 插件读取主 session 用量并发送一次提醒 -插件使用 `experimental.chat.system.transform`,从当前请求取得 `sessionID`,通过 OpenCode SDK 读取冻结主 session 已完成请求的累计金额。插件不自行计算 token 价格,也不汇总子 session 或其他 session。 +插件使用 `experimental.chat.messages.transform`,从当前用户消息取得 `sessionID`,通过 OpenCode SDK 读取冻结主 session 已完成请求的累计金额。插件不自行计算 token 价格,也不汇总子 session 或其他 session。 -累计金额达到 `reminder_cost` 后,插件调用 `sdk.v2.session.prompt({ sessionID, prompt: { text }, delivery: "steer" }, { throwOnError: true })`,省略 `resume`,文字为“请尽快完成必要工作、验证结果并生成报告。”。提醒作为当前主 session 的用户输入被持久化,后续 Agent 回合按照 OpenCode 调度读取。 +累计金额达到 `reminder_cost` 后,插件调用 `sdk.session.prompt()`,设置 `noReply: true`,文字为“请尽快完成必要工作、验证结果并生成报告。”,沿用当前用户消息的 agent、model、variant、tools、system 和 format。OpenCode 保存用户消息后立即返回,TUI 通过同一消息接口读取提醒。插件将返回的真实消息加入当前 hook 的 messages,使即将发送的模型请求包含提醒。 -插件在异步读取前设置处理中状态,重入 hook 直接返回;提交前设置本 attempt 已尝试提交,成功收到 admission 后才设置 `reminder_sent=true`。通过 Executor 创建的 Unix socket 报告状态,日志记录消息 ID 和 admission 序号。请求失败或响应丢失时保留阶段、错误类别和消息,本 attempt 不自动重复提交。请求重试、上下文压缩和后续调用均不重复提醒。恢复产生的新 attempt 拥有独立提醒状态,并继续读取同一 session 的累计金额。 +插件在异步读取前设置处理中状态,重入 hook 直接返回;提交前设置本 attempt 已尝试提交,保存成功且加入当前消息后才设置 `reminder_sent=true`。通过 Executor 创建的 Unix socket 报告状态,日志记录消息 ID。请求失败或响应丢失时保留阶段、错误类别和消息,本 attempt 不自动重复提交。请求重试、上下文压缩和后续调用均不重复提醒。恢复产生的新 attempt 拥有独立提醒状态,并继续读取同一 session 的累计金额。 -例如提醒金额为 0.10 美元、主 session 已用 0.11 美元时,插件提交一条 steer 输入。admission 成功只表示已接收;验收另外检查后续模型输入包含提醒、assistant 响应以及最终 attempt 状态。 +例如提醒金额为 0.10 美元、主 session 已用 0.11 美元时,插件保存一条用户提醒并加入本次模型输入。验收检查消息持久化、TUI 消息接口、模型处理证据及最终 attempt 状态;提醒状态不代表任务完成。 插件状态绑定 attempt 标识、策略版本、插件版本和 session。运行期间无法读取用量或追加提示时,插件记录明确错误并允许任务继续;Executor 的终止判断不读取插件提醒结果。 diff --git a/openspec/changes/add-model-specific-agent-turn-limits/proposal.md b/openspec/changes/add-model-specific-agent-turn-limits/proposal.md index 5ad8381..9f09203 100644 --- a/openspec/changes/add-model-specific-agent-turn-limits/proposal.md +++ b/openspec/changes/add-model-specific-agent-turn-limits/proposal.md @@ -8,7 +8,7 @@ Agent 任务只有总超时,无法按模型等级控制一次分析允许消 - Server 保留三等级目录并显式启用:`model_free` 提醒 0.05 美元、终止 0.10 美元;`model_standard` 提醒 0.10 美元、终止 0.20 美元;`model_plus` 提醒 0.15 美元、终止 0.30 美元。公开模型选择响应不返回内部金额阈值。 - 配置工具在未单独指定终止限额时使用提醒限额的两倍;冻结策略保存两个明确值,允许各等级配置不同关系。 - 任务创建时由可信配置按照模型目录键冻结实际模型、变体、启用状态和金额策略。恢复任务沿用原策略和同一 session 的累计金额。 -- OpenCode 插件读取当前主 session 已完成请求的累计金额,达到提醒限额后通过 v2 SDK 向当前 session 最多提交一次 `delivery: "steer"` 用户输入;成功接收后记录 admission。 +- OpenCode 插件读取当前主 session 已完成请求的累计金额,达到提醒限额后通过 `sdk.session.prompt()` 和 `noReply: true` 向当前 session 最多保存一次用户提醒,并加入本次模型输入;TUI 通过同一消息接口读取提醒。 - Executor 从任务 `OPENCODE_DB` 读取冻结主 session 的 `session.cost`,达到终止限额后复用现有进程组终止和回收流程;允许检查间隔内已完成调用造成金额超过限额。 - 启用策略的 Agent 不依赖 bubblewrap 或 Linux namespace;Executor 使用任务专属 scratch、数据库、候选 manifest 和 OpenCode 文件权限规则,容器继续提供运行时边界。 - 因金额达到限额而停止且确认进程已回收时,公开错误为 `agent_maximum_analysis_depth_exceeded`,显示“超过最大分析程度”。受保护诊断保存观测金额、策略阈值、检查阶段、终止信号和回收结果。 diff --git a/openspec/changes/add-model-specific-agent-turn-limits/specs/agent-turn-limits/spec.md b/openspec/changes/add-model-specific-agent-turn-limits/specs/agent-turn-limits/spec.md index 16b348f..0da0744 100644 --- a/openspec/changes/add-model-specific-agent-turn-limits/specs/agent-turn-limits/spec.md +++ b/openspec/changes/add-model-specific-agent-turn-limits/specs/agent-turn-limits/spec.md @@ -48,11 +48,11 @@ ### Requirement: 插件必须在达到提醒限额后尽力发送一次收尾提醒 -系统 MUST 使用 OpenCode 现有插件接口读取当前 attempt 主 session 已完成请求的累计金额。插件 MUST 只统计冻结的主 session。当累计金额达到或超过 `reminder_cost` 时,插件 MUST 调用 `sdk.v2.session.prompt()`,请求只包含 `sessionID`、`prompt.text` 和 `delivery: "steer"`,省略 `resume`。文字 MUST 为“请尽快完成必要工作、验证结果并生成报告。”。每个 attempt 最多提交一次提醒,恢复产生的新 attempt 拥有独立提醒状态并沿用同一 session 的累计金额。提醒作为当前 session 的用户输入持久化。插件 MUST 在异步检查前设置防重入状态,并在提交前设置已尝试提交;重入 hook 直接返回。成功返回 admission 后才设置 `reminder_sent=true`,通过现有 Unix socket 报告状态并记录 admission 标识。插件异常 MUST 保留阶段、错误类别和消息,Executor 独立执行金额终止。 +系统 MUST 使用 `experimental.chat.messages.transform` 读取当前 attempt 主 session 已完成请求的累计金额。插件 MUST 只统计冻结的主 session。当累计金额达到或超过 `reminder_cost` 时,插件 MUST 调用 `sdk.session.prompt()`,设置 `noReply: true`,沿用当前用户消息的 agent、model、variant、tools、system 和 format。文字 MUST 为“请尽快完成必要工作、验证结果并生成报告。”。每个 attempt 最多提交一次提醒,恢复产生的新 attempt 拥有独立提醒状态并沿用同一 session 的累计金额。提醒 MUST 保存为 TUI 消息接口可以读取的用户消息,并将返回的消息加入本次 hook 的 messages。插件 MUST 在异步检查前设置防重入状态,并在提交前设置已尝试提交;重入 hook 直接返回。保存成功且加入当前消息后才设置 `reminder_sent=true`,通过现有 Unix socket 报告状态并记录消息 ID。插件异常 MUST 保留阶段、错误类别和消息,Executor 独立执行金额终止。 #### Scenario: 达到提醒限额 - **WHEN** 某 attempt 的提醒金额限额为 0.10 美元,插件在下一次模型请求前确认主 session 已完成用量为 0.11 美元且尚未提醒 -- **THEN** 插件向主 session 提交一次 steer 输入,成功返回 admission 后记录该 attempt 已提醒 +- **THEN** 插件向主 session 保存一次用户提醒,加入本次模型输入后记录该 attempt 已提醒 #### Scenario: 提醒不重复发送 - **WHEN** 已提醒的 attempt 因模型重试、上下文压缩或后续请求再次满足提醒条件 @@ -62,9 +62,9 @@ - **WHEN** 提醒提交发生网络错误、session 不存在或请求冲突 - **THEN** 保留实际错误类别和消息,`reminder_sent` 保持 false,本 attempt 不自动重复提交 -#### Scenario: admission 与后续处理分别验收 -- **WHEN** OpenCode 返回提醒的消息 ID 和 admission 序号 -- **THEN** 该结果仅证明已接收输入;验收另外检查后续模型输入包含提醒、assistant 响应及最终 attempt 状态 +#### Scenario: 消息与任务结果分别验收 +- **WHEN** OpenCode 返回保存后的提醒消息 +- **THEN** 验收确认 TUI 消息接口可以读取提醒、本次模型输入包含提醒,并检查 assistant 响应及最终 attempt 状态 #### Scenario: 子 session 不计入累计用量 - **WHEN** OpenCode 历史中存在不属于冻结主 session 的子 session 或其他 session diff --git a/openspec/changes/add-model-specific-agent-turn-limits/tasks.md b/openspec/changes/add-model-specific-agent-turn-limits/tasks.md index 5c31c4d..63f255e 100644 --- a/openspec/changes/add-model-specific-agent-turn-limits/tasks.md +++ b/openspec/changes/add-model-specific-agent-turn-limits/tasks.md @@ -11,7 +11,7 @@ ## 2. OpenCode 提醒插件 - [x] 2.1 编写与生产 OpenCode `1.18.18` 匹配的本地 JavaScript 插件,通过当前 OpenCode 进程的 session SDK 投影读取冻结主 session 已完成请求的累计金额,不递归汇总子 session,也不自行重新计算 token 价格 -- [ ] 2.2 在金额检查 hook 中通过 v2 SDK 提交 `delivery: "steer"` 用户输入,省略 `resume`,每个 attempt 最多提交一次“请尽快完成必要工作、验证结果并生成报告。”;验证重入、admission 状态和后续模型处理 +- [ ] 2.2 在 messages hook 中通过 `sdk.session.prompt()` 和 `noReply: true` 保存用户消息并加入本次模型输入,每个 attempt 最多提交一次“请尽快完成必要工作、验证结果并生成报告。”;验证消息持久化、TUI 消息接口和后续模型处理 - [x] 2.3 以 attempt 状态记录提醒结果,覆盖该 attempt 内的请求重试、上下文压缩和后续模型调用;恢复产生的新 attempt 可以再次提醒一次 - [x] 2.4 对 session 缺失、用量读取失败和提示注入失败记录明确错误;运行期提醒失败允许任务继续,插件不调用终止流程 - [x] 2.5 让任务配置显式引用镜像内只读插件并传入冻结策略、目标模型、模型等级、attempt 标识和策略版本