fix(coding-agent): preserved queued skill images
Kept image content attached to compaction-queued skill prompts when they are rebuilt as custom messages.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import { type CustomMessage, SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../session/messages";
|
||||
import type { InteractiveModeContext } from "./types";
|
||||
|
||||
@@ -8,7 +9,7 @@ type SkillPromptMessage = Pick<
|
||||
"customType" | "content" | "display" | "details" | "attribution"
|
||||
> & {
|
||||
customType: typeof SKILL_PROMPT_MESSAGE_TYPE;
|
||||
content: string;
|
||||
content: string | (TextContent | ImageContent)[];
|
||||
display: true;
|
||||
details: SkillPromptDetails;
|
||||
attribution: "user";
|
||||
@@ -27,6 +28,7 @@ interface ParsedSkillCommand {
|
||||
interface InvokeSkillCommandOptions {
|
||||
propagateErrors?: boolean;
|
||||
queueOnly?: boolean;
|
||||
images?: ImageContent[];
|
||||
}
|
||||
|
||||
/** Built custom-message payload and delivery options for a `/skill:` command. */
|
||||
@@ -55,6 +57,7 @@ export async function buildSkillCommandPrompt(
|
||||
ctx: SkillCommandHost,
|
||||
text: string,
|
||||
streamingBehavior: "steer" | "followUp",
|
||||
images?: ImageContent[],
|
||||
): Promise<BuiltSkillCommandPrompt | undefined> {
|
||||
const parsed = parseSkillCommand(text);
|
||||
if (!parsed) return undefined;
|
||||
@@ -68,6 +71,8 @@ export async function buildSkillCommandPrompt(
|
||||
metaLines.push(`User: ${parsed.args}`);
|
||||
}
|
||||
const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
|
||||
const textBlock: TextContent = { type: "text", text: message };
|
||||
const promptContent = images && images.length > 0 ? [textBlock, ...images] : message;
|
||||
const skillName = parsed.commandName.slice("skill:".length);
|
||||
const details: SkillPromptDetails = {
|
||||
name: skillName || parsed.commandName,
|
||||
@@ -79,7 +84,7 @@ export async function buildSkillCommandPrompt(
|
||||
return {
|
||||
message: {
|
||||
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
||||
content: message,
|
||||
content: promptContent,
|
||||
display: true,
|
||||
details,
|
||||
attribution: "user",
|
||||
@@ -96,7 +101,7 @@ export async function invokeSkillCommandFromText(
|
||||
options?: InvokeSkillCommandOptions,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const built = await buildSkillCommandPrompt(ctx, text, streamingBehavior);
|
||||
const built = await buildSkillCommandPrompt(ctx, text, streamingBehavior, options?.images);
|
||||
if (!built) return false;
|
||||
const promptOptions = options?.queueOnly ? { ...built.options, queueOnly: true } : built.options;
|
||||
await ctx.session.promptCustomMessage(built.message, promptOptions);
|
||||
|
||||
@@ -672,6 +672,7 @@ export class UiHelpers {
|
||||
await invokeSkillCommandFromText(this.ctx, message.text, message.mode, {
|
||||
propagateErrors: true,
|
||||
queueOnly: true,
|
||||
images: message.images,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
@@ -770,7 +771,12 @@ export class UiHelpers {
|
||||
// is not sent as a literal prompt after compaction.
|
||||
let promptPromise: Promise<unknown>;
|
||||
if (isKnownSkillCommand(this.ctx, firstPrompt.text)) {
|
||||
const built = await buildSkillCommandPrompt(this.ctx, firstPrompt.text, firstPrompt.mode);
|
||||
const built = await buildSkillCommandPrompt(
|
||||
this.ctx,
|
||||
firstPrompt.text,
|
||||
firstPrompt.mode,
|
||||
firstPrompt.images,
|
||||
);
|
||||
promptPromise = built
|
||||
? this.ctx.session.promptCustomMessage(built.message, built.options).catch(restoreQueue)
|
||||
: Promise.resolve();
|
||||
|
||||
@@ -268,8 +268,9 @@ describe("compaction skill re-invocation", () => {
|
||||
});
|
||||
|
||||
it("re-invokes a queued skill as a user-attributed skill prompt", async () => {
|
||||
const image: ImageContent = { type: "image", data: "aGVsbG8=", mimeType: "image/png" };
|
||||
const { ctx, promptCustomMessage, promptCustomMessageCalled, prompt, steer, followUp } =
|
||||
createCompactionDrainContext([{ text: "/skill:test-skill arg1 arg2", mode: "followUp" }]);
|
||||
createCompactionDrainContext([{ text: "/skill:test-skill arg1 arg2", mode: "followUp", images: [image] }]);
|
||||
const uiHelpers = new UiHelpers(ctx);
|
||||
|
||||
await uiHelpers.flushCompactionQueue({ willRetry: false });
|
||||
@@ -278,7 +279,11 @@ describe("compaction skill re-invocation", () => {
|
||||
const [message, options] = firstPromptCustomCall(promptCustomMessage);
|
||||
expect(message.customType).toBe(SKILL_PROMPT_MESSAGE_TYPE);
|
||||
expect(message.attribution).toBe("user");
|
||||
expect(message.content).toContain("Do the thing.");
|
||||
if (!Array.isArray(message.content)) {
|
||||
throw new Error("expected queued skill prompt to preserve image content blocks");
|
||||
}
|
||||
expect(message.content[0]).toMatchObject({ type: "text", text: expect.stringContaining("Do the thing.") });
|
||||
expect(message.content[1]).toEqual(image);
|
||||
expect(message.details).toMatchObject({ name: "test-skill", args: "arg1 arg2", lineCount: 1 });
|
||||
expect(options).toEqual({
|
||||
streamingBehavior: "followUp",
|
||||
@@ -292,13 +297,22 @@ describe("compaction skill re-invocation", () => {
|
||||
it("queues retry-drained skills without appending them to session history", async () => {
|
||||
const fixture = await createRealSession();
|
||||
try {
|
||||
const { ctx } = createCompactionDrainContext([{ text: "/skill:test-skill retry args", mode: "followUp" }]);
|
||||
const image: ImageContent = { type: "image", data: "cmV0cnk=", mimeType: "image/png" };
|
||||
const { ctx } = createCompactionDrainContext([
|
||||
{ text: "/skill:test-skill retry args", mode: "followUp", images: [image] },
|
||||
]);
|
||||
ctx.session = fixture.session;
|
||||
const uiHelpers = new UiHelpers(ctx);
|
||||
|
||||
await uiHelpers.flushCompactionQueue({ willRetry: true });
|
||||
|
||||
expect(fixture.session.getQueuedMessages().followUp).toEqual(["/skill:test-skill retry args"]);
|
||||
const queued = fixture.session.agent.peekFollowUpQueue()[0];
|
||||
if (queued?.role !== "custom" || !Array.isArray(queued.content)) {
|
||||
throw new Error("expected retry-drained skill to be queued as image-bearing custom content");
|
||||
}
|
||||
expect(queued.customType).toBe(SKILL_PROMPT_MESSAGE_TYPE);
|
||||
expect(queued.content[1]).toEqual(image);
|
||||
expect(fixture.session.messages).toEqual([]);
|
||||
} finally {
|
||||
await fixture.session.dispose();
|
||||
|
||||
Reference in New Issue
Block a user