Cap get_api_docs, break duplicate tool rounds, force a reply after successful writes, and log each tool call so limit hits are diagnosable.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
fingerprintToolCalls,
|
||||
isSuccessfulWrite,
|
||||
summarizeToolTrace,
|
||||
truncateToolResult,
|
||||
} from "../../src/modules/agent/server/loop-guards";
|
||||
import { runAgentChat } from "../../src/modules/agent/server/run";
|
||||
import type {
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionResult,
|
||||
LlmClient,
|
||||
} from "../../src/lib/llm/types";
|
||||
|
||||
describe("loop-guards", () => {
|
||||
it("fingerprints tool calls stably regardless of key order", () => {
|
||||
const a = fingerprintToolCalls([
|
||||
{ name: "create_event", arguments: '{"title":"Dentist","calendarName":"Family"}' },
|
||||
]);
|
||||
const b = fingerprintToolCalls([
|
||||
{ name: "create_event", arguments: '{"calendarName":"Family","title":"Dentist"}' },
|
||||
]);
|
||||
assert.equal(a, b);
|
||||
});
|
||||
|
||||
it("treats create_event 201 as a successful write", () => {
|
||||
assert.equal(isSuccessfulWrite("create_event", 201), true);
|
||||
assert.equal(isSuccessfulWrite("list_calendars", 200), false);
|
||||
assert.equal(
|
||||
isSuccessfulWrite("call_api", 201, '{"method":"POST","path":"/api/v1/events"}'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isSuccessfulWrite("call_api", 200, '{"method":"GET","path":"/api/v1/events"}'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("truncates oversized tool results", () => {
|
||||
const result = truncateToolResult("x".repeat(7000), 100);
|
||||
assert.ok(result.length < 200);
|
||||
assert.match(result, /truncated/);
|
||||
});
|
||||
|
||||
it("summarizes tool traces", () => {
|
||||
assert.equal(
|
||||
summarizeToolTrace([
|
||||
{ name: "list_calendars", status: 200 },
|
||||
{ name: "create_event", status: 201 },
|
||||
]),
|
||||
"list_calendars→200, create_event→201",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runAgentChat loop guards", () => {
|
||||
it("stops after a successful write instead of looping", async () => {
|
||||
let calls = 0;
|
||||
const llm: LlmClient = {
|
||||
async chatCompletion(request: ChatCompletionRequest): Promise<ChatCompletionResult> {
|
||||
calls += 1;
|
||||
if (calls === 1) {
|
||||
return {
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: "1",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_event",
|
||||
arguments: JSON.stringify({
|
||||
title: "Dentist",
|
||||
startAt: "2026-07-10T15:00:00.000Z",
|
||||
endAt: "2026-07-10T16:00:00.000Z",
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finishReason: "tool_calls",
|
||||
};
|
||||
}
|
||||
|
||||
assert.equal(request.tools, undefined);
|
||||
return {
|
||||
message: { role: "assistant", content: "Added Dentist to your calendar." },
|
||||
finishReason: "stop",
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const result = await runAgentChat({
|
||||
messages: [{ role: "user", content: "add dentist tomorrow at 10" }],
|
||||
request: new Request("http://localhost:3000/api/agent/chat"),
|
||||
llm,
|
||||
executeTool: async () =>
|
||||
JSON.stringify({ status: 201, body: { id: "evt-1", title: "Dentist" } }),
|
||||
});
|
||||
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(result.toolCalls.length, 1);
|
||||
assert.equal(result.toolCalls[0]?.name, "create_event");
|
||||
assert.match(result.message.content, /Dentist/);
|
||||
});
|
||||
|
||||
it("breaks duplicate identical tool rounds", async () => {
|
||||
let calls = 0;
|
||||
const llm: LlmClient = {
|
||||
async chatCompletion(): Promise<ChatCompletionResult> {
|
||||
calls += 1;
|
||||
if (calls <= 2) {
|
||||
return {
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: String(calls),
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_calendars",
|
||||
arguments: "{}",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finishReason: "tool_calls",
|
||||
};
|
||||
}
|
||||
return {
|
||||
message: { role: "assistant", content: "You have one Family calendar." },
|
||||
finishReason: "stop",
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const result = await runAgentChat({
|
||||
messages: [{ role: "user", content: "what calendars do I have?" }],
|
||||
request: new Request("http://localhost:3000/api/agent/chat"),
|
||||
llm,
|
||||
executeTool: async () =>
|
||||
JSON.stringify({ status: 200, body: [{ id: "cal-1", name: "Family" }] }),
|
||||
});
|
||||
|
||||
assert.equal(calls, 3);
|
||||
assert.equal(result.toolCalls.length, 1);
|
||||
assert.match(result.message.content, /Family/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user