Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions packages/pi-plugin/src/issue-785-request-controls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { expect, test } from "bun:test";
import {
piPreparedStructure,
piPreparedStructureMismatch,
} from "./pi-dispatch-structure";

// Issue 785: Oh My Pi's Anthropic provider appends request-control system
// messages (tool_addition/tool_removal blocks, or an empty effort-only control)
// after the context pass. They are not prepared turns, so the dispatch check
// must not count them as conversation rows.

const anthropic = {
api: "anthropic-messages",
compat: { supportsMidConversationSystem: true },
input: ["text", "image"],
};
const head = { role: "user", content: "managed history", timestamp: 1 };
const prompt = { role: "user", content: "current request", timestamp: 4 };
const prepared = piPreparedStructure(
[
head,
{
role: "assistant",
content: [{ type: "toolCall", id: "one", name: "read", arguments: {} }],
timestamp: 2,
},
{
role: "toolResult",
toolCallId: "one",
toolName: "read",
content: [{ type: "text", text: "body" }],
timestamp: 3,
},
prompt,
],
anthropic,
);
const wire = [
head,
{
role: "assistant",
content: [{ type: "tool_use", id: "one", name: "read", input: {} }],
},
{
role: "user",
content: [{ type: "tool_result", tool_use_id: "one", content: "body" }],
},
prompt,
];
const control = (type: string, name: string) => ({
type,
tool: { type: "tool_reference", name },
});

test("issue 785: trailing request-control system rows are not prepared turns", () => {
expect(
piPreparedStructureMismatch({ messages: wire }, prepared),
).toBeUndefined();
for (const content of [
[control("tool_addition", "x")],
[control("tool_addition", "x"), control("tool_removal", "y")],
[],
])
expect(
piPreparedStructureMismatch(
{ messages: [...wire, { role: "system", content }] },
prepared,
),
).toBeUndefined();
});

test("issue 785: unprepared system text rows still refuse", () => {
for (const content of [
"unprepared instructions",
[{ type: "text", text: "unprepared instructions" }],
[control("tool_addition", "x"), { type: "text", text: "unprepared" }],
]) {
expect(
piPreparedStructureMismatch(
{ messages: [...wire, { role: "system", content }] },
prepared,
),
).toContain("comparison=count");
expect(
piPreparedStructureMismatch(
{
messages: [
...wire.slice(0, 3),
{ role: "system", content },
...wire.slice(3),
],
},
prepared,
),
).toContain("comparison=");
}
});
13 changes: 13 additions & 0 deletions packages/pi-plugin/src/pi-dispatch-structure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,19 @@ function payloadStructure(
(message.role === "system" || message.role === "developer")
)
continue;
// Oh My Pi's Anthropic provider appends request-control system messages
// (only tool_addition/tool_removal blocks, or empty effort-only controls)
// while building the request, after the context pass. They are not
// prepared turns. @oh-my-pi/pi-ai/src/providers/anthropic.ts
// insertAnthropicControlMarkers (18.12.6).
if (
message.role === "system" &&
Array.isArray(message.content) &&
message.content.every((block) =>
["tool_addition", "tool_removal"].includes(String(object(block)?.type)),
)
)
continue;
// Responses emits reasoning/text/calls as separate items for one assistant
// turn. Only its call/result IDs survive every supported replay mode.
if (responses && message.type === "reasoning") continue;
Expand Down
Loading