mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-14 04:01:35 +00:00
fix: deliver images reliably with steer and follow-up messages
A steer or follow-up accepted while a turn is closing is now claimed by a fresh turn at the driver's clean exit instead of stranding in the inbox; cancellation and pre-step rejection still park accepted work. Continuable subagent follow-ups accept image parts: the wire is upload-shaped, the Host admits and persists each batch before inbox acceptance, and delivery is refused when the child model declines image input. The queue dock renders durable image thumbnails instead of an [image] text marker. Fixes #3186
This commit is contained in:
@@ -73,7 +73,7 @@ describe('Session queue snapshot intake', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('marks mixed-content messages non-editable while retaining their preview', () => {
|
||||
it('marks mixed-content messages non-editable and keeps image blocks out of the text preview', () => {
|
||||
const session = makeSession()
|
||||
session.handleControlFrame(queueFrame([{
|
||||
id: 'q-image',
|
||||
@@ -86,7 +86,9 @@ describe('Session queue snapshot intake', () => {
|
||||
{
|
||||
id: 'q-image', placement: 'queued',
|
||||
content: [{ type: 'text', text: 'hi' }, { type: 'image', data: 'x' }],
|
||||
preview: 'hi [image]', text: null,
|
||||
// Image blocks render as thumbnails from `content`, so the preview
|
||||
// carries only the text; non-image foreign blocks keep their marker.
|
||||
preview: 'hi', text: null,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@@ -231,6 +231,58 @@ describe('Web session model selection', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('delivers an admitted image batch through steer with the same ordered content as queue', async () => {
|
||||
const { ctx, agent, sessionId } = await harness()
|
||||
const attachments = {
|
||||
imageLimits: {
|
||||
maxImageBytes: 4,
|
||||
maxImagesPerMessage: 2,
|
||||
maxMessageImageBytes: 4,
|
||||
maxImagePixels: 4,
|
||||
maxImageDimension: 2000,
|
||||
mediaTypes: ['image/png'],
|
||||
},
|
||||
validateImage: vi.fn(() => Promise.resolve()),
|
||||
saveImage: vi.fn((input: { data: Uint8Array; mediaType: 'image/png'; name?: string }) => Promise.resolve({
|
||||
attachmentId: `att-${String(input.data[0])}`,
|
||||
mediaType: input.mediaType,
|
||||
bytes: input.data.byteLength,
|
||||
width: 1,
|
||||
height: 1,
|
||||
...input.name === undefined ? {} : { name: input.name },
|
||||
})),
|
||||
}
|
||||
ctx.provide('attachments', Object.setPrototypeOf(attachments, AttachmentStore.prototype) as never)
|
||||
const steer = vi.fn()
|
||||
const followup = vi.fn()
|
||||
Object.assign(agent, { steer, followup })
|
||||
const remote = createSessionTestRemote(ctx, {
|
||||
defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }),
|
||||
cwd: '/tmp',
|
||||
})
|
||||
|
||||
const result = await remote.prompt(promptRequest({
|
||||
sessionId,
|
||||
mode: 'steer' as const,
|
||||
content: [
|
||||
{ type: 'text' as const, text: 'look at this' },
|
||||
{ type: 'image' as const, mediaType: 'image/png' as const, data: 'AQ==', name: 'mid-turn.png' },
|
||||
],
|
||||
}))
|
||||
expect(result.ok).toBe(true)
|
||||
expect(followup).not.toHaveBeenCalled()
|
||||
expect((steer.mock.calls[0]?.[0] as UserMessage).content).toEqual([
|
||||
{ type: 'text', text: 'look at this' },
|
||||
{
|
||||
type: 'image',
|
||||
attachment: {
|
||||
attachmentId: 'att-1', mediaType: 'image/png', bytes: 1, width: 1, height: 1, name: 'mid-turn.png',
|
||||
},
|
||||
},
|
||||
])
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('allows a text-only selection while durable or pending images remain available for later models', async () => {
|
||||
const { ctx, agent, sessionId } = await harness()
|
||||
registerTextOnly(ctx)
|
||||
|
||||
@@ -281,6 +281,32 @@ describe('prompt and cancel errors', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards continuation image parts to the subagent prompt Remote unstripped', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const session = new Session(SID, fakeRemote(api), {
|
||||
address: { parentSessionId: PARENT, childSessionId: SID, mode: 'continuable' },
|
||||
parentAvailable: true,
|
||||
})
|
||||
await session.open()
|
||||
const content = [
|
||||
{ type: 'text' as const, text: '看这张图' },
|
||||
{ type: 'image' as const, mediaType: 'image/png' as const, data: 'aGk=', name: 'shot.png' },
|
||||
]
|
||||
const prompted = await session.prompt(content, 'queue')
|
||||
|
||||
expect(prompted).toEqual({ ok: true, value: { accepted: true } })
|
||||
expect(api.callsOf('subagents.prompt')).toEqual([
|
||||
{
|
||||
requestId: expect.any(String) as unknown as string,
|
||||
parentSessionId: PARENT, childSessionId: SID,
|
||||
mode: 'continuable',
|
||||
content,
|
||||
clientTimeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
])
|
||||
expect(session.getSnapshot().promptError).toBeNull()
|
||||
})
|
||||
|
||||
it('lands an interrupt business failure in promptError with op=stop', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onSubagentInterrupt = () => Promise.resolve(remoteErr({
|
||||
|
||||
Reference in New Issue
Block a user