mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 11:09:34 +00:00
242 lines
7.9 KiB
JavaScript
242 lines
7.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
import { createJiti } from "jiti";
|
|
|
|
const jiti = createJiti(import.meta.url);
|
|
const {
|
|
appendTitleRequestToTrailingUser,
|
|
buildSessionTitleAgentOptions,
|
|
generateSessionTitle,
|
|
parseGeneratedSessionTitle,
|
|
sanitizeTitleMessages,
|
|
} = await jiti.import("./session-title.ts");
|
|
|
|
function assistantMessage(text) {
|
|
return {
|
|
role: "assistant",
|
|
content: [{ type: "text", text }],
|
|
api: "test",
|
|
provider: "test",
|
|
model: "test-model",
|
|
usage: {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 0,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
},
|
|
stopReason: "stop",
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|
|
|
|
test("cleans common session title response wrappers", () => {
|
|
assert.equal(parseGeneratedSessionTitle("标题:修复 SSE 重连。"), "修复 SSE 重连");
|
|
assert.equal(parseGeneratedSessionTitle('```json\n{"title":"整理 Session 文件夹"}\n```'), "整理 Session 文件夹");
|
|
assert.equal(parseGeneratedSessionTitle('"Improve worktree session grouping"'), "Improve worktree session grouping");
|
|
});
|
|
|
|
test("rejects responses without a usable title", () => {
|
|
assert.throws(() => parseGeneratedSessionTitle("```\n---\n```"), /usable session title/);
|
|
});
|
|
|
|
test("folds the title request into a trailing user message without mutating the source", () => {
|
|
const source = [
|
|
{ role: "assistant", content: [], timestamp: 1 },
|
|
{ role: "user", content: [{ type: "text", text: "Fix the running-session race" }], timestamp: 2 },
|
|
];
|
|
|
|
const prepared = appendTitleRequestToTrailingUser(source);
|
|
|
|
assert.deepEqual(prepared.map((message) => message.role), ["assistant", "user"]);
|
|
assert.match(prepared[1].content.at(-1).text, /Create a concise title/);
|
|
assert.equal(source[1].content.length, 1);
|
|
assert.notEqual(prepared[1], source[1]);
|
|
});
|
|
|
|
test("leaves a completed conversation unchanged before adding the title turn", () => {
|
|
const source = [
|
|
{ role: "user", content: "Fix it", timestamp: 1 },
|
|
{ role: "assistant", content: [], timestamp: 2 },
|
|
];
|
|
|
|
assert.equal(appendTitleRequestToTrailingUser(source), source);
|
|
});
|
|
|
|
test("waits for the source reply before sending the title prompt", async () => {
|
|
let sourceReplyFinished = false;
|
|
let providerRoles;
|
|
const sourceAgent = {
|
|
state: {
|
|
systemPrompt: "system",
|
|
model: { provider: "test", id: "test-model" },
|
|
thinkingLevel: "off",
|
|
tools: [],
|
|
messages: [{ role: "user", content: "Implement auto name", timestamp: 1 }],
|
|
},
|
|
waitForIdle: async () => {
|
|
sourceAgent.state.messages.push(assistantMessage("The implementation is complete"));
|
|
sourceReplyFinished = true;
|
|
},
|
|
convertToLlm: (messages) => messages,
|
|
streamFunction: (_model, context) => {
|
|
assert.equal(sourceReplyFinished, true);
|
|
providerRoles = context.messages.map((message) => message.role);
|
|
const stream = createAssistantMessageEventStream();
|
|
queueMicrotask(() => {
|
|
stream.push({
|
|
type: "done",
|
|
reason: "stop",
|
|
message: assistantMessage("Wait for Complete Agent Reply"),
|
|
});
|
|
});
|
|
return stream;
|
|
},
|
|
sessionId: "source-session-id",
|
|
};
|
|
|
|
const result = await generateSessionTitle({ agent: sourceAgent });
|
|
|
|
assert.equal(result.title, "Wait for Complete Agent Reply");
|
|
assert.deepEqual(providerRoles, ["user", "assistant", "user"]);
|
|
});
|
|
|
|
test("temporary title agent preserves the provider-facing prefix", async () => {
|
|
const model = { provider: "test", id: "cached-model" };
|
|
const messages = [{ role: "user", content: [{ type: "text", text: "Fix it" }] }];
|
|
const originalExecute = async () => ({ content: [], details: {} });
|
|
const tools = [{
|
|
name: "read",
|
|
label: "read",
|
|
description: "Read a file",
|
|
parameters: { type: "object", properties: {} },
|
|
execute: originalExecute,
|
|
}];
|
|
const convertToLlm = (value) => value;
|
|
const transformContext = async (value) => value;
|
|
const streamFunction = () => { throw new Error("not called"); };
|
|
const source = {
|
|
state: {
|
|
systemPrompt: "cached system prompt",
|
|
model,
|
|
thinkingLevel: "high",
|
|
tools,
|
|
messages,
|
|
},
|
|
convertToLlm,
|
|
transformContext,
|
|
streamFunction,
|
|
steeringMode: "one-at-a-time",
|
|
followUpMode: "one-at-a-time",
|
|
sessionId: "source-session-id",
|
|
transport: "sse",
|
|
toolExecution: "parallel",
|
|
};
|
|
|
|
const options = buildSessionTitleAgentOptions(source);
|
|
|
|
assert.equal(options.initialState.systemPrompt, source.state.systemPrompt);
|
|
assert.equal(options.initialState.model, model);
|
|
assert.equal(options.initialState.thinkingLevel, "high");
|
|
assert.equal(options.initialState.messages, messages);
|
|
assert.equal(options.convertToLlm, convertToLlm);
|
|
assert.equal(options.transformContext, transformContext);
|
|
assert.equal(options.streamFn, streamFunction);
|
|
assert.equal(options.sessionId, "source-session-id");
|
|
const withoutExecute = (tool) => Object.fromEntries(
|
|
Object.entries(tool).filter(([key]) => key !== "execute"),
|
|
);
|
|
assert.deepEqual(
|
|
options.initialState.tools.map(withoutExecute),
|
|
tools.map(withoutExecute),
|
|
);
|
|
assert.notEqual(options.initialState.tools[0].execute, originalExecute);
|
|
await assert.rejects(
|
|
options.initialState.tools[0].execute("call", {}, undefined, undefined),
|
|
/cannot be executed/,
|
|
);
|
|
});
|
|
|
|
test("keeps only tool calls with adjacent matching results", () => {
|
|
const messages = [
|
|
{ role: "user", content: "inspect both files", timestamp: 1 },
|
|
{
|
|
...assistantMessage("Inspecting files"),
|
|
content: [
|
|
{ type: "text", text: "Inspecting files" },
|
|
{ type: "toolCall", id: "call-complete", name: "read", arguments: { path: "a.txt" } },
|
|
{ type: "toolCall", id: "call-incomplete", name: "read", arguments: { path: "b.txt" } },
|
|
],
|
|
stopReason: "toolUse",
|
|
},
|
|
{
|
|
role: "toolResult",
|
|
toolCallId: "call-complete",
|
|
toolName: "read",
|
|
content: [{ type: "text", text: "file contents" }],
|
|
isError: false,
|
|
timestamp: 2,
|
|
},
|
|
];
|
|
|
|
const sanitized = sanitizeTitleMessages(messages);
|
|
|
|
assert.deepEqual(
|
|
sanitized[1].content.filter((block) => block.type === "toolCall").map((block) => block.id),
|
|
["call-complete"],
|
|
);
|
|
assert.equal(sanitized[2], messages[2]);
|
|
assert.equal(messages[1].content.length, 3);
|
|
});
|
|
|
|
test("removes incomplete tool calls before invoking the title provider", async () => {
|
|
let providerMessages;
|
|
const sourceAgent = {
|
|
state: {
|
|
systemPrompt: "system",
|
|
model: { provider: "test", id: "test-model" },
|
|
thinkingLevel: "off",
|
|
tools: [],
|
|
messages: [
|
|
{ role: "user", content: "run a command", timestamp: 1 },
|
|
{
|
|
...assistantMessage(""),
|
|
content: [{
|
|
type: "toolCall",
|
|
id: "call-incomplete",
|
|
name: "bash",
|
|
arguments: { command: "sleep 10" },
|
|
}],
|
|
stopReason: "toolUse",
|
|
},
|
|
],
|
|
},
|
|
waitForIdle: async () => {},
|
|
convertToLlm: (messages) => messages,
|
|
streamFunction: (_model, context) => {
|
|
providerMessages = context.messages.map((message) => ({
|
|
role: message.role,
|
|
content: message.content,
|
|
}));
|
|
const stream = createAssistantMessageEventStream();
|
|
queueMicrotask(() => {
|
|
stream.push({
|
|
type: "done",
|
|
reason: "stop",
|
|
message: assistantMessage("Sanitized Tool Call History"),
|
|
});
|
|
});
|
|
return stream;
|
|
},
|
|
sessionId: "source-session-id",
|
|
};
|
|
|
|
const result = await generateSessionTitle({ agent: sourceAgent });
|
|
|
|
assert.equal(result.title, "Sanitized Tool Call History");
|
|
assert.deepEqual(providerMessages.map((message) => message.role), ["user"]);
|
|
assert.match(providerMessages[0].content, /Create a concise title/);
|
|
});
|