65 lines
2.4 KiB
C++
65 lines
2.4 KiB
C++
// clean_history must never hand a provider a tool result whose tool call was not issued: devplace answers
|
|||
|
|
// that with HTTP 400 "No function call found for function call output with call_id ..." and the whole turn
|
||
|
|
// fails (everythingwrong.md; reproduced against the live endpoint with an orphan result).
|
||
|
|
#include <cstdio>
|
||
|
|
#include "helpers.h"
|
||
|
|
#include "json.h"
|
||
|
|
using detz::json::Value;
|
||
|
|
static int g_fail = 0;
|
||
|
|
#define CHECK(cond) do { if (!(cond)) { g_fail++; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } } while (0)
|
||
|
|
|
||
|
|
static Value msg(const char* role, const char* content) {
|
||
|
|
Value m = Value::object();
|
||
|
|
m.set("role", role);
|
||
|
|
m.set("content", content);
|
||
|
|
return m;
|
||
|
|
}
|
||
|
|
static Value assistant_calling(const char* id) {
|
||
|
|
Value fn = Value::object();
|
||
|
|
fn.set("name", "fork");
|
||
|
|
fn.set("arguments", "{}");
|
||
|
|
Value call = Value::object();
|
||
|
|
call.set("id", id);
|
||
|
|
call.set("type", "function");
|
||
|
|
call.set("function", fn);
|
||
|
|
Value calls = Value::array();
|
||
|
|
calls.push(call);
|
||
|
|
Value m = Value::object();
|
||
|
|
m.set("role", "assistant");
|
||
|
|
m.set("content", Value());
|
||
|
|
m.set("tool_calls", calls);
|
||
|
|
return m;
|
||
|
|
}
|
||
|
|
static Value tool_result(const char* id) {
|
||
|
|
Value m = msg("tool", "done");
|
||
|
|
m.set("tool_call_id", id);
|
||
|
|
return m;
|
||
|
|
}
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
// a window that starts on a tool result (compaction cut its assistant message away)
|
||
|
|
Value cut = Value::array();
|
||
|
|
cut.push(msg("system", "s"));
|
||
|
|
cut.push(tool_result("call_orphan"));
|
||
|
|
cut.push(msg("user", "go"));
|
||
|
|
cut.push(assistant_calling("call_a"));
|
||
|
|
cut.push(tool_result("call_a"));
|
||
|
|
Value cleaned = detz::llm::drop_orphan_tool_results(detz::llm::clean_history(cut));
|
||
|
|
CHECK(cleaned.size() == 4);
|
||
|
|
bool orphan_left = false;
|
||
|
|
for (const auto& m : cleaned.items()) {
|
||
|
|
if (m.get("role").str() == "tool" && m.get("tool_call_id").str() == "call_orphan") orphan_left = true;
|
||
|
|
}
|
||
|
|
CHECK(!orphan_left);
|
||
|
|
// a healthy exchange is untouched
|
||
|
|
Value ok = Value::array();
|
||
|
|
ok.push(msg("user", "go"));
|
||
|
|
ok.push(assistant_calling("call_b"));
|
||
|
|
ok.push(tool_result("call_b"));
|
||
|
|
CHECK(detz::llm::drop_orphan_tool_results(detz::llm::clean_history(ok)).size() == 3);
|
||
|
|
// clean_history itself stays byte-for-byte tai.py behaviour: it keeps the orphan (agent_diff.py compares it)
|
||
|
|
CHECK(detz::llm::clean_history(cut).size() == 5);
|
||
|
|
std::printf(g_fail ? "llm_history_test: %d FAILED\n" : "llm_history_test: ok\n", g_fail);
|
||
|
|
return g_fail ? 1 : 0;
|
||
|
|
}
|