fix(attachment): distinguish admission from storage failures

This commit is contained in:
Tianyi Cui
2026-08-11 17:45:09 +08:00
parent 32c584561a
commit 57fc6bc539
10 changed files with 80 additions and 13 deletions

View File

@@ -18,6 +18,7 @@ import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import type { Context } from '@deepseek-ai/cordis'
import { isImageAdmissionError } from '@deepseek-ai/dsh-attachment'
import type { AttachmentStore, ImageAttachmentRef, ImageMediaType, SaveImageAttachment } from '@deepseek-ai/dsh-attachment'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { ToolDefinition, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
@@ -474,10 +475,13 @@ async function prepareImageProjection(
type: 'image',
attachment: byIndex.get(index) as ImageAttachmentRef,
}))
} catch {
} catch (error: unknown) {
const reason = isImageAdmissionError(error)
? `image admission rejected the result: ${error.message}`
: 'durable image storage rejected the result'
return projectContent(content, toolName, block => ({
type: 'text',
text: imageDiagnostic(block, 'durable image storage rejected the result'),
text: imageDiagnostic(block, reason),
}))
}
}

View File

@@ -2,7 +2,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
import { Context } from '@deepseek-ai/cordis'
import AttachmentStore, { AttachmentId } from '@deepseek-ai/dsh-attachment'
import AttachmentStore, { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@deepseek-ai/dsh-attachment'
import { CallId, LlmAdapter, LlmService } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
@@ -676,6 +676,29 @@ describe('tool execution', () => {
expect(textAt(result.content)).toContain('durable image storage rejected the result')
})
it('reports attachment policy rejection as image admission rather than storage failure', async () => {
const rich = await mountRichRegistry()
vi.spyOn(rich.attachments, 'saveImages').mockRejectedValueOnce(
new AttachmentError('too many images', 'TOO_MANY_IMAGES'),
)
const client = createMockClient(
[{ name: 'img', inputSchema: { type: 'object' } }],
{ content: [{ type: 'image', mimeType: 'image/png', data: 'AQ==' }] },
)
await syncTools(client as never, rich.ctx, defaultOpts, new Map())
const result = await rich.ctx.tools.execute({
signal: testToolSignal,
callId: CallId('policy-rejected'),
name: 'mcp__srv__img',
arguments: {},
agent: agentOn() as never,
})
expect(textAt(result.content)).toContain('image admission rejected the result: too many images')
expect(textAt(result.content)).not.toContain('storage rejected')
})
it('lets post-execute replacement win over a prepared image projection', async () => {
const rich = await mountRichRegistry()
rich.ctx.on('tools/post-execute', async (): Promise<PostToolDecision> => ({