- Updated the demotion logic to treat complete, signed thinking blocks as stable content that terminates an interrupted stream. - Protected signed thinking runs from being stripped when processing interrupted agent messages.
126 lines
3.8 KiB
TypeScript
126 lines
3.8 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
import {
|
|
type CustomMessage,
|
|
convertToLlm,
|
|
INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
SKILL_PROMPT_MESSAGE_TYPE,
|
|
type SkillPromptDetails,
|
|
} from "./messages";
|
|
|
|
function customMessage(customType: string, attribution: "agent" | "user"): CustomMessage<SkillPromptDetails> {
|
|
return {
|
|
role: "custom",
|
|
customType,
|
|
content: "Use this skill.",
|
|
display: true,
|
|
details: { name: "atomic-commit", path: "/tmp/SKILL.md", lineCount: 1 },
|
|
attribution,
|
|
timestamp: 1,
|
|
};
|
|
}
|
|
|
|
const interruptedUsage: AssistantMessage["usage"] = {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 0,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
};
|
|
|
|
function abortedAssistant(content: AssistantMessage["content"]): AssistantMessage {
|
|
return {
|
|
role: "assistant",
|
|
content,
|
|
api: "anthropic-messages",
|
|
provider: "anthropic",
|
|
model: "claude-sonnet-4-5",
|
|
usage: interruptedUsage,
|
|
stopReason: "aborted",
|
|
timestamp: 1,
|
|
};
|
|
}
|
|
|
|
function interruptedThinkingContinuity(): CustomMessage {
|
|
return {
|
|
role: "custom",
|
|
customType: INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
content: "preserved reasoning",
|
|
display: false,
|
|
attribution: "agent",
|
|
timestamp: 2,
|
|
};
|
|
}
|
|
|
|
describe("convertToLlm", () => {
|
|
it("presents user-invoked skill prompts as user turns", () => {
|
|
const [message] = convertToLlm([customMessage(SKILL_PROMPT_MESSAGE_TYPE, "user")]);
|
|
|
|
expect(message?.role).toBe("user");
|
|
if (message?.role !== "user") {
|
|
throw new Error(`Expected user role, received ${message?.role ?? "none"}`);
|
|
}
|
|
expect(message.attribution).toBe("user");
|
|
});
|
|
|
|
it("keeps auto-applied skill prompts and other custom messages as developer turns", () => {
|
|
const [autoSkill, otherCustom] = convertToLlm([
|
|
customMessage(SKILL_PROMPT_MESSAGE_TYPE, "agent"),
|
|
customMessage("extension-note", "user"),
|
|
]);
|
|
|
|
expect(autoSkill?.role).toBe("developer");
|
|
expect(otherCustom?.role).toBe("developer");
|
|
});
|
|
|
|
it("strips the demoted trailing thinking run from the assistant LLM view when its continuity message follows", () => {
|
|
const messages: AgentMessage[] = [
|
|
abortedAssistant([
|
|
{ type: "text", text: "partial answer" },
|
|
{ type: "thinking", thinking: "interrupted reasoning" },
|
|
]),
|
|
interruptedThinkingContinuity(),
|
|
];
|
|
|
|
const llm = convertToLlm(messages);
|
|
const assistant = llm.find(entry => entry.role === "assistant");
|
|
expect(Array.isArray(assistant?.content) && assistant.content.map(block => block.type)).toEqual(["text"]);
|
|
expect(llm.some(entry => entry.role === "developer")).toBe(true);
|
|
});
|
|
|
|
it("keeps trailing thinking on the assistant LLM view when no continuity message follows", () => {
|
|
const messages: AgentMessage[] = [
|
|
abortedAssistant([
|
|
{ type: "text", text: "partial answer" },
|
|
{ type: "thinking", thinking: "interrupted reasoning" },
|
|
]),
|
|
];
|
|
|
|
const llm = convertToLlm(messages);
|
|
const assistant = llm.find(entry => entry.role === "assistant");
|
|
expect(Array.isArray(assistant?.content) && assistant.content.map(block => block.type)).toEqual([
|
|
"text",
|
|
"thinking",
|
|
]);
|
|
});
|
|
|
|
it("keeps a signed (complete) trailing thinking block in the assistant LLM view even with a continuity message", () => {
|
|
const messages: AgentMessage[] = [
|
|
abortedAssistant([
|
|
{ type: "text", text: "partial answer" },
|
|
{ type: "thinking", thinking: "complete reasoning", thinkingSignature: "sig" },
|
|
]),
|
|
interruptedThinkingContinuity(),
|
|
];
|
|
|
|
const llm = convertToLlm(messages);
|
|
const assistant = llm.find(entry => entry.role === "assistant");
|
|
expect(Array.isArray(assistant?.content) && assistant.content.map(block => block.type)).toEqual([
|
|
"text",
|
|
"thinking",
|
|
]);
|
|
});
|
|
});
|