diff --git a/.changeset/hook-blocked-prompt-image-gate.md b/.changeset/hook-blocked-prompt-image-gate.md new file mode 100644 index 0000000000..654a9749d9 --- /dev/null +++ b/.changeset/hook-blocked-prompt-image-gate.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix prompts blocked by a UserPromptSubmit hook keeping unsupported or malformed images in the session history unfiltered; blocked prompts now go through the same image format check as prompts that reach the model, so a rejected image becomes a text notice instead of failing later requests. diff --git a/packages/agent-core-v2/src/agent/prompt/promptService.ts b/packages/agent-core-v2/src/agent/prompt/promptService.ts index f41ded27c7..3a8c3d8067 100644 --- a/packages/agent-core-v2/src/agent/prompt/promptService.ts +++ b/packages/agent-core-v2/src/agent/prompt/promptService.ts @@ -3,12 +3,14 @@ * * Assigns prompt and message identities, serializes user prompts through an * active slot and FIFO, converts selected pending prompts into active-turn - * steers, settles lifecycle handles, and keeps system input outside the prompt - * resource model. `submit` / `submitSteer` are the wire-facing user entry - * points: they track `input_steer` through `telemetry`, persist the derived - * title/lastPrompt through `sessionMetadata` for the main agent only - * (publishing the live update through `event`), enqueue, and settle - * `{turn_id}` from the launch handle. Session tool gating is an edge + * steers, settles lifecycle handles, records hook-blocked prompts into the + * history behind the same image format gate the step requests apply, and + * keeps system input outside the prompt resource model. `submit` / + * `submitSteer` are the wire-facing user entry points: they track + * `input_steer` through `telemetry`, persist the derived title/lastPrompt + * through `sessionMetadata` for the main agent only (publishing the live + * update through `event`), enqueue, and settle `{turn_id}` from the launch + * handle. Session tool gating is an edge * concern: callers apply `IAgentToolPolicyService.setSessionDisabledTools` * before submitting, the way kap-server's prompt route composes it. * The pure-data `launching` flag is registered into @@ -23,7 +25,7 @@ import { IInstantiationService } from '#/_base/di/instantiation'; import { LifecycleScope } from '#/app/scopes'; import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { defineState } from '#/_base/state/stateRegistry'; -import { extractImageCompressionCaptions } from '#/agent/media/image-compress'; +import { extractImageCompressionCaptions, gateImageFormatParts } from '#/agent/media/image-compress'; import { userCancellationReason } from '#/_base/utils/abort'; import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'; import { newMessageId } from '#/agent/contextMemory/messageId'; @@ -332,7 +334,8 @@ export class AgentPromptService implements IAgentPromptService { ownerPromptId, }); } - if (message.content.length > 0) this.context.append({ ...message, id: ownerPromptId }); + const content = gateImageFormatParts(message.content); + if (content.length > 0) this.context.append({ ...message, id: ownerPromptId, content }); } private async deliverToolResult(ctx: ToolDidExecuteContext): Promise { const delivery = ctx.result.delivery; if (delivery === undefined) return; diff --git a/packages/agent-core-v2/test/agent/prompt/promptService.test.ts b/packages/agent-core-v2/test/agent/prompt/promptService.test.ts index 30e17a6023..25e4ecccdb 100644 --- a/packages/agent-core-v2/test/agent/prompt/promptService.test.ts +++ b/packages/agent-core-v2/test/agent/prompt/promptService.test.ts @@ -233,4 +233,33 @@ describe('AgentPromptService', () => { parts.some((part) => part.type === 'text' && part.text.includes('image/avif')), ).toBe(true); }); + + it('gates hook-blocked prompt images too', async () => { + const { prompt, context } = harness(); + prompt.hooks.onBeforeSubmitPrompt.register('block', async (ctx, next) => { ctx.block = true; await next(); }); + const avifUrl = `data:image/avif;base64,${Buffer.from([7, 8, 9]).toString('base64')}`; + const handle = await prompt.enqueue({ + id: 'prompt-blocked-img', + message: { + role: 'user', + content: [ + { type: 'text', text: 'look' }, + { type: 'image_url', imageUrl: { url: avifUrl } }, + { type: 'image_url', imageUrl: { url: 'data:image/png,not-a-base64-payload' } }, + ], + toolCalls: [], + origin: { kind: 'user' }, + }, + }); + await expect(handle.completion).resolves.toMatchObject({ state: 'blocked' }); + + const appended = context.get(); + expect(appended).toHaveLength(1); + expect(appended[0]?.id).toBe('prompt-blocked-img'); + const parts = appended[0]!.content; + expect(parts.some((part) => part.type === 'image_url')).toBe(false); + expect(parts[0]).toEqual({ type: 'text', text: 'look' }); + expect((parts[1] as { text: string }).text).toContain('image/avif'); + expect((parts[2] as { text: string }).text).toContain('not a valid data URL'); + }); });