feat: 原生函数调用 + 多工具串行批次执行(tool execution 重构) - #211
Merged
Conversation
- LLMClient 请求体发 tools 数组(fancy/openai/cloudflare + gpt-oss Responses 格式),400/422 自动去 tools 降级重试 - StreamingHandler 累加流式 delta.tool_calls(跨 delta 归并参数片段) - ResponseParser 解析 OpenAI message.tool_calls 与 Cloudflare function_call 项 - 新增 ToolRegistry:按权限门控组装 tools、tool_call 桥接回 #tool 文本复用 handler、模型黑名单 - 修复 #edit_memory 前缀劫持 bug(KNOWN_TOOLS 长名在前) - 串行批量:一次响应多个 tool_calls 逐个执行,结果合并一次回灌模型(批次安全集 + 60s 超时兜底) - config.yml 新增 native_tool_calling 开关(默认关,保持原文本协议) - 380 测试全绿 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CLIManager 的 API 异常 catch 块此前只向玩家发送简略报错、不打印控制台日志, 导致 API 失败(如 401/429/模型不支持)无法在服务端诊断。 为全部 API 相关错误路径补 `[CLI] ...` warning 日志(含玩家名 + 完整异常堆栈): - processAIMessage 主入口与重试的 IOException/Throwable catch - continueGeneration 的 IOException catch - feedbackToAI 的 IOException/Throwable catch - processStreamingMessage 与 feedbackToAI 的流式 onErrorCallback 玩家聊天界面不变,仅服务端控制台可诊断。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
buildToolsArray 生成的 parameters 直接是 {"query": {...}},最外层缺
{"type":"object","properties":{...}}。OpenAI 兼容 API 要求 parameters 是
合法 JSON Schema,DeepSeek 返回 400: Invalid schema ... got 'type: null'。
addTool 统一把传入的 properties 内容包一层 object schema。新增测试断言
parameters 形状。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
obj("type", str("string","array")) 生成 {"type":{"type":"string","description":"array"},...},
type 字段被塞进一个对象而非字符串,不是合法 JSON Schema。DeepSeek 校验严格,
二次请求 400 直接拒绝,模型无法再给草方块。新增 arr() 辅助生成
{"type":"array","description":...,"items":{"type":...}},ask.options 与 todo.todos 走它。
同时给原生 tool_calls 分发与批次执行加诊断日志,区分"模型只返回一个调用"
与"多调用被单工具路径静默丢弃"两种情况。新增测试断言数组参数 schema 合法。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
根因:提示词 [Single Tool Call] 规定每次响应只能一个工具调用, deepseek 严格遵守,导致"给我苹果和草方块"只返回一个 run。 native 开启时:Single Tool Call → Multiple Tool Calls(可一次返回多个 独立函数调用,串行执行、结果一次性回灌);#tool 文本降级为"仅原生不可用 才兜底";Usage Guide 的 todo 后禁止其他工具也放开。文本协议分支原样保留。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
根因:hasPreloadedSession 遍历 30 分钟内的所有会话文件,只要找到一个 非 explicitExit 的就返回 true;而 loadLatestPlayerSession 只取最新文件, 最新文件 explicitExit 时返回 null。两处判定不一致导致: 1. 之前某次在 CLI 中重载 → shutdown 保存了无 explicitExit 的旧文件 A; 2. 本次退出 CLI → 最新文件 B 被标记 explicitExit; 3. 重载后 hasPreloadedSession 扫到 A → 误判 true → 进 CLI; 4. loadLatestPlayerSession 取最新的 B → 恢复失败 → 空会话进入。 修复:hasPreloadedSession 改为与 loadLatestPlayerSession 一致—— 只检查最新会话文件是否 explicitExit。显式退出后重载不再自动进入 CLI。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
问题:退出 CLI 后深度重载仍会误入 CLI。之前尝试用 explicitExit 标记修复, 但根因是恢复的会话可能无用户消息(hasUserMessage=false)导致标记逻辑被跳过, 残留旧文件也会被当作可恢复。 新方案:完全禁用自动恢复—— - 注释 FancyHelper.checkOnlinePlayersForPreloadedSessions 调用 - 注释 ChatListener.onPlayerJoin 的预加载自动进入逻辑 - 插件关闭时 sendUnloadMessage 改为"会话已保存并退出,点击恢复或输入 /cli resume",带可点击按钮(三语言文案) - 手动 /cli resume 不受 explicitExit 限制,列表照常显示所有会话 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sendUnloadMessage 改为接收当前会话 UUID,提示"会话已保存并退出。使用 /cli resume <uuid> 恢复",恢复命令整条蓝色下划线可点击(RUN_COMMAND), 三语言文案同步。无 UUID 时退化为通用 /cli resume。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
/cli resume <uuid> 会把 uuid 当页码解析并显示列表,不恢复会话。 显示保持 /cli resume <uuid>,点击实际执行 /cli resume_confirm <uuid> 直接恢复该会话。hover 如实显示实际执行命令。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
之前多个 run 一起返回时,非 YOLO 模式因确认按钮卡批次屏障而只执行 第一个。现放行 run 在所有模式进批次:逐个确认→执行→捕获输出暂存→ 下一项→全部完成合并一次发回 AI。 - isBatchSafeTool 放行 run(NORMAL/SMART/PLAN/YOLO) - 新增 batchInProgress 标记,让批次最后一项的异步反馈也能被屏障 拦截合并,不再漏结果 - 取消/拒绝在批次中回灌 #error 结果并推进下一工具,避免永久卡死 - handleConfirm 重置超时起点,防止确认按钮停留过久被 60s 兜底误杀 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
原实现固定 1s/5s 延迟:spark 这类打完命令后 4-5s 才有后续输出的 命令会被提前截断,秒回命令又白白等 5s。改为捕获缓冲长度增长判定: - 有新输出 → 重置静默计时;有输出后安静 2s 即收尾 - 从未有输出 → 等待 5s 首次输出宽限后走控制台日志兜底 - 绝对上限 15s 防止持续吐输出的命令无限等待 - 命令失败立即收尾;captureLength/stopCapture 与 netty 监听器 共用锁,保证主线程读到最新写入 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
两个问题:
1. native 模式下 todo 工具 schema 是 {todos:[...]},模型返回该对象经
bridgeToText 原样透传,但 updateTodos 只接受裸数组,导致报"必须数组
格式"。现兼容 {todos:[...]} 与裸数组 [...] 两种形状。
2. todo 解析失败(多为模型格式问题)的详细错误文案此前直接 sendMessage
展示给玩家。这是内部细节,改为仅 feedbackToAI 让 AI 自行修正,
控制台保留 warning 日志。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cloudflare Workers AI(gemma 等)缺少 parallel_tool_calls 时会退化到 单工具调用,导致"请同时给我苹果和木头"只执行一个命令。加上该字段后 实测稳定返回多个 tool_calls;stripTools 降级时一并移除配对字段。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
gemma 在完整系统提示词下退回文本协议并输出多个 #run,原 extractToolCall 只取第一个导致'苹果和木头'只执行一个。改为 extractToolCalls 收集全部 合法 #tool,dispatchTextTools 复用原生 FC 批次屏障串行执行。
完整系统提示词的 #tool 语法说明会诱导 gemma 放弃原生 function calling, 改用文本 #run 输出。nativeTools 分支改为只声明'全部工具由函数提供, 绝不输出 #tool 文本',并清理所有 # 文本示例。同时把缺失的 #end 注册为 原生 end 工具,保证删除文本列表后任务完成仍可触发。
CF gemma 并行 tool_calls 共享同一 index 且参数分片不带 id,原按 index 归并会把两个调用的参数拼进一个 #run,导致'苹果和木棍'只执行一条 报错命令。改为:带 id 的 delta 按 id 归并并设为当前活跃调用,无 id 的参数分片顺序路由到当前活跃调用;无 id 带 name 的退化为按 index。
模型执行 #run 时若首词不在命令表,但存在 0/1/2 个斜杠变体命中
(如 /menu ↔ menu、pos1 ↔ /pos1),则拦截不执行,feedback 警告进
上下文并附带 force 示例。原生 run 调用 JSON 键 {"force": true} 跳过
此校验(不注入提示词)。命令表为空(懒注册)时放行避免误杀,
校验只查表绝不执行命令。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
问题 1:批次超时兜底复用 generationStartTimes,基准会被 setGenerating(EXECUTING_TOOL)、 handleConfirm 等反复重置,导致异步慢工具(#webfetch 45s timeout、#mcp 网络延迟) 的合法执行等待被误判为批次卡死而强制终结。改为在 DialogueSession 中维护独立的 batchLastFeedbackTime(setBatchInProgress 启动、addPendingToolResult 推进),超时检查 仅关注"批次中没有任何工具反馈超过 BATCH_TIMEOUT_MS",不再与单工具执行计时耦合。 问题 2:玩家在批次中 #ask 等待期间执行 /cli stop,pendingCommands 被清除但 isBatchInProgress 仍为 true。后续交互中 feedbackToAI 命中批次拦截器,工具反馈被 吞入已死批次的幽灵队列。在 /cli stop 分支中增加 clearBatchState() 清理。
- CLIManager.executeTool 同步异常兜底:report+反馈 AI,单路径不再卡 EXECUTING_TOOL - executeNativeBatch 合并处删除按结果文本的启发式二次计数(含 _error 字样会被误判失败) - handleAskTool 解析失败/缺 question 时反馈 #ask_error,不再无声等待 - handleSearchTool/handleMcpTool 异步 lambda 加 try-catch,异常反馈 AI 不丢 - parseToolCall 加 null 防御 - 注释:force 键刻意不进 schema 的设计意图(ToolRegistry x2 + handleRunTool javadoc)
clim.agree.prompt 文案宣称可发 /cli 退出,但 exitCLI 开头对非 activeCLI 玩家直接 return, 待同意玩家被困在协议提示循环。现在 exitCLI 会清除待同意状态并发送退出消息。
- LLMClient: 请求消息 system 后首条非 user 时垫 user 占位消息,修复 reka 等 严格网关 400(进入 CLI 的欢迎语以 assistant 开头产生非法序列) - FileWatcherManager: 补 ENTRY_CREATE/DELETE 监听 + mtime 轮询兜底,修复 Git Bash sed -i 等替换式保存不触发热重载(Windows WatchService 不报 rename) - PromptManager: 文本协议提示词改为多工具引导,修复"要求多工具只执行部分"漏执行
- 离线断线保存会话改用玩家名缓存(enterCLI 记录),不再依赖在线玩家对象, 修复异步保存时 Bukkit.getPlayer 返回 null 导致会话历史不保存 - 压缩输入不再截断 500 字符,上下文完整传入压缩模型 + 新增逐条诊断日志 (定位工具调用消息是否算进压缩/被截断)
- #edit/#write 支持 JSON 行参数(标准 JSON 转义,消灭 | 分隔符与 \n 三层转义冲突),旧格式兼容 - ToolRegistry bridgeToText 原生通道输出 JSON 行,与文本协议统一 - config fancy.temperature 主对话采样温度(null=跟随模型默认,0=完全确定),6 处主对话请求挂载 - 会话保存改原子写入(临时文件+ATOMIC_MOVE),避免中断留下半截 JSON 导致 resume 扫描报错 - 新增 ToolExecutorFileFormatTest(JSON 行+旧格式 10 例),更新 ToolRegistryTest bridge 断言
YOLO 分支 setPendingCommand 后直接执行但未清除,玩家后续所有消息被 pendingCommands 拦截走"请确认 [Y/N]"分支静默吞掉。修复:YOLO 免确认 不设置 pendingCommand,与 #run 的 YOLO 分支一致。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
重构工具调用体系:原生函数调用门控 + 文本协议多工具串行批次 + 文件工具 JSON 行格式。
核心改动(32 commits)
工具调用
native_tool_calling):OpenAI 兼容 tool_calls + Responses API(gpt-oss),显式声明parallel_tool_calls/tool_choice#tool,串行执行、逐条反馈、60s 超时兜底force键|分隔符冲突与\n三层转义;原生通道 bridgeToText 统一输出 JSON 行会话与稳定性
fancy.temperature(null=模型默认,0=完全确定),6 处主对话请求挂载测试
|的 edit/write、compact、YOLO 链路