W20: Tool Calling 闭环 + Stream+Tools + 回归测试 + session auto-save + ASan CI (W20.1-W20.6)
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled
CI / Sanitizer (ASan+UBSan) / ubuntu-24.04 (push) Has been cancelled

- W20.1: CLI tool_calls→execute→result→re-call 循环(5轮上限)
- W20.2: deepseek 流式 tool_calls 增量解析(configure 缓存,无 ABI break)
- W20.3: plugin_loader 回归测试 5 块 32 断言(路径/原子性/mock 日志)
- W20.4: plugin_loader ABI 契约校验(name/version/on_init 字段验证)
- W20.5: ASan/UBSan CMake preset + CI sanitizer job(PR-only Linux)
- W20.6: session auto-save(on_shutdown 写 %APPDATA%/dstalk/session.json)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 20:15:00 +08:00
parent 3250b5a8bf
commit 20ead86e88
14 changed files with 730 additions and 21 deletions

View File

@@ -10,6 +10,8 @@ set_target_properties(dstalk-cli PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
find_package(Boost REQUIRED CONFIG)
target_link_libraries(dstalk-cli
PRIVATE dstalk
PRIVATE dstalk boost::boost dstalk_boost_config
)

View File

@@ -14,6 +14,9 @@
#include <system_error>
#include <vector>
#include <boost/json.hpp>
#include <boost/json/src.hpp>
#ifdef _WIN32
#include <windows.h>
#include <io.h>
@@ -45,6 +48,7 @@
static const dstalk_ai_service_t* g_ai = nullptr;
static const dstalk_session_service_t* g_session = nullptr;
static const dstalk_file_io_service_t* g_file_io = nullptr;
static const dstalk_tools_service_t* g_tools = nullptr;
// ---- 运行时状态 ----
static std::string g_current_model;
@@ -454,6 +458,7 @@ int main(int argc, char* argv[])
g_ai = static_cast<const dstalk_ai_service_t*>(dstalk_service_query(ai_provider, 1));
g_session = static_cast<const dstalk_session_service_t*>(dstalk_service_query("session", 1));
g_file_io = static_cast<const dstalk_file_io_service_t*>(dstalk_service_query("file_io", 1));
g_tools = static_cast<const dstalk_tools_service_t*>(dstalk_service_query("tools", 1));
if (!g_ai) {
std::fprintf(stderr, CLR_RED "[dstalk] AI 服务未找到(请检查插件目录)\n" CLR_RESET);
@@ -572,6 +577,102 @@ int main(int argc, char* argv[])
g_session->add(&user_msg);
dstalk_message_t ai_msg = {"assistant", result.content, nullptr, result.tool_calls_json};
g_session->add(&ai_msg);
// W20.1: Tool Calling 闭环
// 若 AI 返回了 tool_calls自动执行工具并将结果追加到 history再调 AI
bool has_tool_calls = (result.tool_calls_json && result.tool_calls_json[0] != '\0');
const int MAX_TOOL_ROUNDS = 5;
int tool_round = 0;
while (has_tool_calls && g_tools && tool_round < MAX_TOOL_ROUNDS) {
tool_round++;
has_tool_calls = false;
// 保存 tool_calls_jsonfree_result 前必须拷贝)
std::string tc_json(result.tool_calls_json);
// 解析 [{"id":"...", "function":{"name":"...", "arguments":"..."}}]
boost::system::error_code ec;
auto tc_val = boost::json::parse(tc_json, ec);
if (ec.failed() || !tc_val.is_array()) break;
const auto& tc_array = tc_val.as_array();
if (tc_array.empty()) break; // 空数组 → 终止
bool any_executed = false;
for (const auto& tc : tc_array) {
if (!tc.is_object()) continue;
const auto& obj = tc.as_object();
const auto* func_j = obj.if_contains("function");
if (!func_j || !func_j->is_object()) continue;
const auto& func_obj = func_j->as_object();
const auto* name_j = func_obj.if_contains("name");
const auto* args_j = func_obj.if_contains("arguments");
if (!name_j || !name_j->is_string()) continue;
std::string tool_name = boost::json::value_to<std::string>(*name_j);
std::string tool_args = (args_j && args_j->is_string())
? boost::json::value_to<std::string>(*args_j) : "{}";
const auto* id_j = obj.if_contains("id");
std::string call_id = (id_j && id_j->is_string())
? boost::json::value_to<std::string>(*id_j) : "";
// 执行工具
char* exec_result = g_tools->execute(tool_name.c_str(), tool_args.c_str());
if (exec_result) {
dstalk_message_t tool_msg = {
"tool",
exec_result,
call_id.empty() ? nullptr : call_id.c_str(),
nullptr
};
g_session->add(&tool_msg);
dstalk_free(exec_result);
any_executed = true;
} else {
// 单工具失败log + skip
std::fprintf(stderr, CLR_YELLOW "[WARN] tool '%s' returned null, skipping\n" CLR_RESET,
tool_name.c_str());
}
}
if (!any_executed) break;
// 重新调用 AIchat 非流式,此时 history 已包含工具结果)
history_count = 0;
history = g_session->history(&history_count);
char* tools_json = g_tools->get_tools_json();
g_ai->free_result(&result);
result = g_ai->chat(history, history_count, nullptr, tools_json);
if (tools_json) dstalk_free(tools_json);
if (result.ok) {
if (result.content && result.content[0]) {
std::printf("%s\n", result.content);
}
dstalk_message_t ai_followup = {
"assistant",
result.content,
nullptr,
result.tool_calls_json
};
g_session->add(&ai_followup);
has_tool_calls = (result.tool_calls_json && result.tool_calls_json[0] != '\0');
} else {
std::printf(CLR_RED "[ERROR] AI 调用失败: %s\n" CLR_RESET,
result.error ? result.error : "unknown error");
break;
}
}
if (tool_round >= MAX_TOOL_ROUNDS && has_tool_calls) {
std::fprintf(stderr, CLR_YELLOW "[WARN] 已达最大工具调用轮次(%d),停止\n" CLR_RESET, MAX_TOOL_ROUNDS);
}
} else {
// A3: error 路径下需 NULL 保护;当前只取 result.errorcontent 未涉及
std::printf(CLR_RESET "\n" CLR_RED "[ERROR] AI 调用失败: %s\n" CLR_RESET,