fix: preserve interaction replies across chat recovery
This commit is contained in:
@@ -32,7 +32,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"id": "tjwater.tools",
|
||||
"description": "注册 20 个回答、展示、调查写入、通信、记忆/技能管理及待办工具;安装会话 Shell 沙箱、原生 MCP 连接和权限策略。查询由 MCP 提供,公开网页搜索使用原生 websearch。",
|
||||
"description": "注册 21 个回答、展示、调查写入、通信、记忆/技能管理及待办工具;安装会话 Shell 沙箱、原生 MCP 连接和权限策略。查询由 MCP 提供,公开网页搜索使用原生 websearch。",
|
||||
"package": "tjwater-plugin",
|
||||
"entrypoint": "tjwater-plugin/index.ts",
|
||||
"implementation": "plugins/tjwater-tools.ts",
|
||||
@@ -41,6 +41,7 @@
|
||||
"description": "本地交互式回答与完整最终答复;不代表后台任务完成。",
|
||||
"tools": [
|
||||
"present_answer",
|
||||
"ask_user",
|
||||
"final_answer"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -86,7 +86,7 @@ temperature: 0.2
|
||||
|
||||
一个界面块发布后可以继续查询或解释,再发布下一块;相同 block_id 更新本轮同一块。不要重复在文字中抄写整张表或图。每个 Chart 和 Metric 保留单位或来源;查询事实、用户事实、推断和演练数据分别标注。MapLocation 仅提供用户主动点击后的定位入口。FollowUpForm 只收集输入,用户提交后形成新的对话消息,不能在未提交时当作已确认事实。后台 task_execution 使用原任务进度与展示渠道,不调用 present_answer。
|
||||
|
||||
当前步骤确实依赖用户才能决定的范围、参数或取舍时,普通聊天与任务协作主动使用 `question`,简要说明该选择如何影响后续执行,并合并相关问题。可查询的事实、已确认的参数和工具 schema 已有的默认值先自行处理;工具权限审批仍走宿主流程。等待真实回答后接着执行,不先提交 `final_answer`;取消提问不表示确认任何选项。`question` 不可用时用简短文字提问,说明哪一步等待回答。FollowUpForm 用于最终报告后可选的资料补充,不代替执行中阻塞的提问;同一问题只使用一种收集方式。
|
||||
当前步骤确实依赖用户才能决定的范围、参数或取舍时,普通聊天与任务协作优先使用当前目录提供的 `ask_user`。先完成其他工具,再单独提出最多三个问题,简要说明选择的影响;定义稳定的问题与选项 ID。一般澄清问题优先设置 allow_custom=true,允许用户直接填写回答或补充已选内容;只有确实必须限定为已有选项时才关闭。宿主确认问题已保存后,本轮结束,不继续工具、不猜测默认答案、不调用 `final_answer`。用户回答会以原会话新的用户输入送达,再继续执行。旧客户端没有 `ask_user` 时才使用可用的原生 `question`;二者都不可用时用简短文字提问。可查询的事实、已确认的参数和工具 schema 已有的默认值先自行处理;工具权限审批仍走宿主流程。取消或跳过不表示确认任何选项。FollowUpForm 用于最终报告后可选的资料补充,不代替执行中阻塞的提问;同一问题只使用一种收集方式。
|
||||
|
||||
核对目标完成情况或真实阻塞,并完成所有已启动工具、必要写回和界面更新后,普通聊天与任务协作调用一次 `final_answer` 结束本轮。阶段说明、等待回答和可继续执行的下一步留在过程里;最终答复后不继续查询、重复改写报告或调用 `present_answer`。后台调查执行按追加的调查指令和原有任务运行接口收尾。
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Plugin } from "@opencode/plugin";
|
||||
import { getInternalBridgeConfig } from "./internalBridge.js";
|
||||
|
||||
export async function hostQuestionBridge(operation: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {
|
||||
const { baseUrl, internalToken } = getInternalBridgeConfig();
|
||||
const response = await fetch(`${baseUrl}/internal/chat-interactions/${operation}`, {
|
||||
method: "POST", headers: { "content-type": "application/json", "x-agent-internal-token": internalToken },
|
||||
body: JSON.stringify(body), signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10000)]) : AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!response.ok) throw new Error("Interaction gate rejected the operation. Finish existing tools before asking, then wait for the user.");
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function registerHostQuestionPolicy(ctx: Parameters<Plugin.Plugin["setup"]>[0]) {
|
||||
await ctx.session.hook("context", async (event) => {
|
||||
const policy = await hostQuestionBridge("policy", { session_id: event.sessionID }) as { enabled: boolean; waiting: boolean };
|
||||
if (!policy.enabled) delete event.tools.ask_user;
|
||||
else {
|
||||
delete event.tools.question;
|
||||
if (policy.waiting) for (const name of Object.keys(event.tools)) delete event.tools[name];
|
||||
}
|
||||
});
|
||||
// Wrap native, MCP and custom executors, including calls via Code Mode.
|
||||
await ctx.tool.transform((editor) => {
|
||||
for (const definition of editor.list()) editor.update(definition.id, (draft) => {
|
||||
const execute = draft.execute;
|
||||
draft.execute = async (input, context) => {
|
||||
const identity = { session_id: context.sessionID, call_id: context.id, tool: definition.id };
|
||||
await hostQuestionBridge("enter", identity, context.signal);
|
||||
try { return await execute(input, context); }
|
||||
finally { await hostQuestionBridge("leave", identity).catch(() => undefined); }
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import ask_user from "../tools/ask_user.js";
|
||||
import { registerHostQuestionPolicy } from "../host-question-policy.js";
|
||||
import final_answer from "../tools/final_answer.js";
|
||||
import present_answer from "../tools/present_answer.js";
|
||||
import locate_features from "../tools/locate_features.js";
|
||||
@@ -29,6 +31,7 @@ import send_task_message from "../tools/send_task_message.js";
|
||||
import reply_agent_message from "../tools/reply_agent_message.js";
|
||||
|
||||
const businessTools = {
|
||||
ask_user,
|
||||
final_answer,
|
||||
present_answer,
|
||||
locate_features,
|
||||
@@ -57,7 +60,7 @@ export default Plugin.define({
|
||||
await ctx.tool.transform((editor) => {
|
||||
for (const [name, definition] of Object.entries(businessTools)) {
|
||||
const object = z.object(definition.args);
|
||||
const input = (AGENT_MESSAGE_TOOLS as readonly string[]).includes(name) ? object.strict() : object;
|
||||
const input = name === "ask_user" || (AGENT_MESSAGE_TOOLS as readonly string[]).includes(name) ? object.strict() : object;
|
||||
editor.add({
|
||||
name,
|
||||
description: definition.description,
|
||||
@@ -99,6 +102,7 @@ export default Plugin.define({
|
||||
const mcpReport = await registerMcpPolicy(ctx);
|
||||
await registerTaskAgentPolicy(ctx);
|
||||
await registerAgentMessagePolicy(ctx);
|
||||
await registerHostQuestionPolicy(ctx);
|
||||
await ctx.rpc.register(Rpc.define({
|
||||
id: "tjwater.tools",
|
||||
methods: {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { tool } from "../tool.js";
|
||||
import { interactionQuestionsSchema } from "../../src/chat/interaction-request-contract.js";
|
||||
import { hostQuestionBridge } from "../host-question-policy.js";
|
||||
|
||||
export default tool({
|
||||
description: "向用户提出最多三个澄清问题。先完成其他工具,再单独调用;问题由宿主持久保存,调用成功后本轮结束,等待用户回答。不要猜测答案,不要继续调用工具,不代替业务审批。选项使用稳定 id;支持单选、多选和自定义文字。",
|
||||
args: { questions: tool.schema.array(tool.schema.object({
|
||||
question_id: tool.schema.string().min(1).max(128), title: tool.schema.string().min(1).max(200), body: tool.schema.string().min(1).max(2000),
|
||||
options: tool.schema.array(tool.schema.object({ id: tool.schema.string().min(1).max(128), label: tool.schema.string().min(1).max(200) }).strict()).max(12),
|
||||
mode: tool.schema.enum(["single", "multiple"]), allow_custom: tool.schema.boolean(), required: tool.schema.boolean(),
|
||||
}).strict()).min(1).max(3) },
|
||||
async execute(args, context) {
|
||||
if (!context.callID) throw new Error("Question requires a trusted tool call ID");
|
||||
return JSON.stringify(await hostQuestionBridge("create", {
|
||||
session_id: context.sessionID, call_id: context.callID, tool: "ask_user", questions: interactionQuestionsSchema.parse(args.questions),
|
||||
}, context.signal));
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user