diff --git a/src/agents/pi-embedded-runner/run/attempt.empty-prompt.test.ts b/src/agents/pi-embedded-runner/run/attempt.empty-prompt.test.ts deleted file mode 100644 index 097547a66f5..00000000000 --- a/src/agents/pi-embedded-runner/run/attempt.empty-prompt.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { AgentMessage } from "@mariozechner/pi-agent-core"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - cleanupTempPaths, - createContextEngineAttemptRunner, - createContextEngineBootstrapAndAssemble, - resetEmbeddedAttemptHarness, -} from "./attempt.spawn-workspace.test-support.js"; - -describe("runEmbeddedAttempt empty prompt guard", () => { - const tempPaths: string[] = []; - - beforeEach(() => { - resetEmbeddedAttemptHarness(); - }); - - afterEach(async () => { - await cleanupTempPaths(tempPaths); - vi.restoreAllMocks(); - }); - - it("skips provider submission when prompt, history, and images are empty", async () => { - const sessionPrompt = vi.fn(async () => {}); - const { assemble } = createContextEngineBootstrapAndAssemble(); - - const result = await createContextEngineAttemptRunner({ - contextEngine: { assemble }, - sessionKey: "agent:main:guildchat:dm:empty-prompt", - tempPaths, - sessionMessages: [], - sessionPrompt, - attemptOverrides: { - prompt: " ", - }, - }); - - expect(sessionPrompt).not.toHaveBeenCalled(); - expect(result.promptError).toBeNull(); - expect(result.finalPromptText).toBeUndefined(); - expect(result.messagesSnapshot).toEqual([]); - expect(result.assistantTexts).toEqual([]); - }); - - it("still submits a blank prompt when replay history has content", async () => { - const sessionPrompt = vi.fn(async () => {}); - const { assemble } = createContextEngineBootstrapAndAssemble(); - const sessionMessages = [ - { role: "user", content: "previous turn", timestamp: 1 }, - ] as AgentMessage[]; - - await createContextEngineAttemptRunner({ - contextEngine: { assemble }, - sessionKey: "agent:main:guildchat:dm:empty-prompt-with-history", - tempPaths, - sessionMessages, - sessionPrompt, - attemptOverrides: { - prompt: " ", - }, - }); - - expect(sessionPrompt).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts b/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts index c6915703687..55c2556d416 100644 --- a/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts @@ -11,7 +11,10 @@ const videoGenerationTaskStatusMocks = vi.hoisted(() => ({ vi.mock("../../music-generation-task-status.js", () => musicGenerationTaskStatusMocks); vi.mock("../../video-generation-task-status.js", () => videoGenerationTaskStatusMocks); -import { resolveAttemptPrependSystemContext } from "./attempt.prompt-helpers.js"; +import { + hasPromptSubmissionContent, + resolveAttemptPrependSystemContext, +} from "./attempt.prompt-helpers.js"; describe("resolveAttemptPrependSystemContext", () => { it("prepends active video task guidance ahead of hook system context", () => { @@ -62,3 +65,42 @@ describe("resolveAttemptPrependSystemContext", () => { expect(result).toBe("Hook system context"); }); }); + +describe("hasPromptSubmissionContent", () => { + it("rejects empty prompt submissions without history or images", () => { + expect( + hasPromptSubmissionContent({ + prompt: " ", + messages: [], + imageCount: 0, + }), + ).toBe(false); + }); + + it("allows blank prompt submissions when replay history has content", () => { + expect( + hasPromptSubmissionContent({ + prompt: " ", + messages: [{ role: "user", content: "previous turn", timestamp: 1 }], + imageCount: 0, + }), + ).toBe(true); + }); + + it("allows text or image prompt submissions", () => { + expect( + hasPromptSubmissionContent({ + prompt: "hello", + messages: [], + imageCount: 0, + }), + ).toBe(true); + expect( + hasPromptSubmissionContent({ + prompt: " ", + messages: [], + imageCount: 1, + }), + ).toBe(true); + }); +}); diff --git a/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts b/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts index 200153f1ed0..ea7a1214aa2 100644 --- a/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts +++ b/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts @@ -122,6 +122,14 @@ export function shouldWarnOnOrphanedUserRepair( return trigger === "user" || trigger === "manual"; } +export function hasPromptSubmissionContent(params: { + prompt: string; + messages: readonly unknown[]; + imageCount: number; +}): boolean { + return params.prompt.trim().length > 0 || params.messages.length > 0 || params.imageCount > 0; +} + const QUEUED_USER_MESSAGE_MARKER = "[Queued user message that arrived while the previous turn was still active]"; const MAX_STRUCTURED_MEDIA_REF_CHARS = 300; diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts index 012e23b60ee..4fbf273db79 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts @@ -18,7 +18,6 @@ import { import { cleanupTempPaths, createContextEngineBootstrapAndAssemble, - createContextEngineAttemptRunner, expectCalledWithSessionKey, getHoisted, resetEmbeddedAttemptHarness, @@ -277,81 +276,6 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { expect(params.sessionKey).toBe(sessionKey); }); - it("prechecks unwindowed context before submitting a windowed context-engine prompt", async () => { - const sessionPrompt = vi.fn(async () => {}); - const fullHistory = [ - { - role: "assistant", - content: [{ type: "text", text: "large historical context ".repeat(600) }], - timestamp: 1, - }, - ] as AgentMessage[]; - const windowedMessages = [ - { role: "assistant", content: [{ type: "text", text: "small window" }], timestamp: 2 }, - ] as AgentMessage[]; - const assemble = vi.fn(async () => ({ - messages: windowedMessages, - estimatedTokens: 3, - })); - - const result = await createContextEngineAttemptRunner({ - contextEngine: { assemble }, - sessionKey, - tempPaths, - sessionMessages: fullHistory, - sessionPrompt, - attemptOverrides: { - contextTokenBudget: 512, - }, - }); - - expect(assemble).toHaveBeenCalledWith( - expect.objectContaining({ - messages: fullHistory, - }), - ); - expect(sessionPrompt).not.toHaveBeenCalled(); - expect(result.promptErrorSource).toBe("precheck"); - expect(result.preflightRecovery).toEqual({ route: "compact_only" }); - }); - - it("keeps preflight overflow checks active for engines that own compaction", async () => { - const sessionPrompt = vi.fn(async () => {}); - const fullHistory = [ - { - role: "assistant", - content: [{ type: "text", text: "engine-owned large historical context ".repeat(600) }], - timestamp: 1, - }, - ] as AgentMessage[]; - const assemble = vi.fn(async () => ({ - messages: [ - { role: "assistant", content: [{ type: "text", text: "small window" }], timestamp: 2 }, - ] as AgentMessage[], - estimatedTokens: 3, - })); - - const result = await createContextEngineAttemptRunner({ - contextEngine: { - assemble, - info: { - ownsCompaction: true, - }, - }, - sessionKey, - tempPaths, - sessionMessages: fullHistory, - sessionPrompt, - attemptOverrides: { - contextTokenBudget: 512, - }, - }); - - expect(sessionPrompt).not.toHaveBeenCalled(); - expect(result.promptErrorSource).toBe("precheck"); - expect(result.preflightRecovery).toEqual({ route: "compact_only" }); - }); - it("skips maintenance when afterTurn fails", async () => { const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble(); const afterTurn = vi.fn(async () => { diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 60f51f76e19..5bb325ed5c0 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -252,6 +252,7 @@ import { resolveAttemptPrependSystemContext, resolvePromptBuildHookResult, resolvePromptModeForSession, + hasPromptSubmissionContent, shouldWarnOnOrphanedUserRepair, shouldInjectHeartbeatPrompt, } from "./attempt.prompt-helpers.js"; @@ -453,14 +454,6 @@ function summarizeSessionContext(messages: AgentMessage[]): { }; } -function hasPromptSubmissionContent(params: { - prompt: string; - messages: readonly AgentMessage[]; - imageCount: number; -}): boolean { - return params.prompt.trim().length > 0 || params.messages.length > 0 || params.imageCount > 0; -} - export function applyEmbeddedAttemptToolsAllow( tools: T[], toolsAllow?: string[],