避免不同接口字段差异导致思考链丢失或采样参数被忽略。 Co-authored-by: Cursor <cursoragent@cursor.com>
134 lines
3.7 KiB
TypeScript
134 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
createMockToolCall,
|
|
MockLlmProvider,
|
|
} from "../src/llm/client.js";
|
|
import { createLocalLlmDriver } from "../src/runtime/driver.js";
|
|
|
|
describe("local LLM driver", () => {
|
|
it("continues while tools return results, then stops on text", async () => {
|
|
const llm = new MockLlmProvider([
|
|
createMockToolCall("lookup", { q: "a" }, "c1"),
|
|
"done",
|
|
]);
|
|
const driver = createLocalLlmDriver(llm);
|
|
const seen: string[] = [];
|
|
|
|
const run = await driver.run({
|
|
system: "sys",
|
|
messages: [{ role: "user", content: "go" }],
|
|
tools: [
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "lookup",
|
|
description: "lookup",
|
|
parameters: { type: "object", properties: {} },
|
|
},
|
|
},
|
|
],
|
|
handleStep: (calls) => {
|
|
seen.push(...calls.map((c) => c.name));
|
|
return {
|
|
kind: "continue",
|
|
results: calls.map((c) => ({ callId: c.id, content: "ok" })),
|
|
};
|
|
},
|
|
});
|
|
|
|
expect(seen).toEqual(["lookup"]);
|
|
expect(run.iterations).toBe(2);
|
|
expect(run.stop).toEqual({ kind: "text", content: "done" });
|
|
});
|
|
|
|
it("uses reasoning as the story when tools return no content", async () => {
|
|
const llm = {
|
|
complete: async () => ({ content: "" }),
|
|
completeWithTools: async () => ({
|
|
content: null,
|
|
toolCalls: [],
|
|
reasoning: "门开了。她把耳机摘下,问:「你在干什么?」",
|
|
}),
|
|
};
|
|
const driver = createLocalLlmDriver(llm as never);
|
|
const run = await driver.run({
|
|
system: "sys",
|
|
messages: [{ role: "user", content: "go" }],
|
|
tools: [
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "chance",
|
|
description: "roll",
|
|
parameters: { type: "object", properties: {} },
|
|
},
|
|
},
|
|
],
|
|
handleStep: () => ({ kind: "continue", results: [] }),
|
|
});
|
|
expect(run.stop).toEqual({
|
|
kind: "text",
|
|
content: "门开了。她把耳机摘下,问:「你在干什么?」",
|
|
});
|
|
});
|
|
|
|
it("stops when handleStep concludes, without another model call", async () => {
|
|
const llm = new MockLlmProvider([
|
|
createMockToolCall("run_worker", { workerId: "x" }, "t1"),
|
|
"should-not-be-used",
|
|
]);
|
|
const driver = createLocalLlmDriver(llm);
|
|
|
|
const run = await driver.run({
|
|
system: "sys",
|
|
messages: [{ role: "user", content: "go" }],
|
|
tools: [
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "run_worker",
|
|
description: "run",
|
|
parameters: { type: "object", properties: {} },
|
|
},
|
|
},
|
|
],
|
|
handleStep: (calls) => ({ kind: "conclude", call: calls[0]! }),
|
|
});
|
|
|
|
expect(run.iterations).toBe(1);
|
|
expect(run.stop.kind).toBe("conclude");
|
|
if (run.stop.kind === "conclude") {
|
|
expect(run.stop.call.name).toBe("run_worker");
|
|
}
|
|
});
|
|
|
|
it("throws when the iteration cap is hit", async () => {
|
|
const llm = new MockLlmProvider([
|
|
createMockToolCall("ping", {}, "p1"),
|
|
]);
|
|
const driver = createLocalLlmDriver(llm);
|
|
|
|
await expect(
|
|
driver.run({
|
|
system: "sys",
|
|
messages: [{ role: "user", content: "go" }],
|
|
tools: [
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "ping",
|
|
description: "ping",
|
|
parameters: { type: "object", properties: {} },
|
|
},
|
|
},
|
|
],
|
|
maxIterations: 2,
|
|
handleStep: (calls) => ({
|
|
kind: "continue",
|
|
results: calls.map((c) => ({ callId: c.id, content: "pong" })),
|
|
}),
|
|
}),
|
|
).rejects.toThrow(/exceeded 2 iterations/);
|
|
});
|
|
});
|