mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-04 03:29:33 +00:00
feat: integrate Pi backend and Pi Web
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
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/);
|
||||
});
|
||||
Reference in New Issue
Block a user