Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/hook-blocked-prompt-image-gate.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 11 additions & 8 deletions packages/agent-core-v2/src/agent/prompt/promptService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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';
Expand Down Expand Up @@ -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<void> {
const delivery = ctx.result.delivery; if (delivery === undefined) return;
Expand Down
29 changes: 29 additions & 0 deletions packages/agent-core-v2/test/agent/prompt/promptService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});