refactor: identify and freeze messages at creation

This commit is contained in:
_Kerman
2026-07-28 13:55:59 +08:00
parent c49c0ba497
commit fbf87e660c
345 changed files with 5220 additions and 2901 deletions

View File

@@ -216,7 +216,8 @@ function validatedSessionEvent(value: unknown): SessionEvent {
// kind-tagged content blocks; other variants pass through under their
// envelope shape.
if (value.type === 'assistant/message') {
const content = isRecord(value.data) ? value.data.content : undefined
const message = isRecord(value.data) ? value.data.message : undefined
const content = isRecord(message) ? message.content : undefined
if (!Array.isArray(content) || !content.every(block => isRecord(block) && typeof block.type === 'string')) {
throw new SdkProtocolError(`assistant/message event carried malformed content: ${JSON.stringify(value)}`)
}
@@ -242,7 +243,7 @@ export function finalResponse(events: SessionEvent[]): string {
for (let index = events.length - 1; index >= 0; index--) {
const event = events[index]
if (event?.type !== 'assistant/message') continue
return event.data.content
return event.data.message.content
.filter((block): block is ContentBlock & { type: 'text' } => block.type === 'text')
.map(block => block.text)
.join('')

View File

@@ -95,7 +95,16 @@ function runTurn(sessionId: string): void {
event(sessionId, 'turn/start', { turn: 0 })
event(sessionId, 'assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text } })
if (env.FAKE_MALFORMED_MESSAGE !== undefined) {
event(sessionId, 'assistant/message', { turn: 0, step: 0, content: 'not-an-array' })
event(sessionId, 'assistant/message', {
turn: 0,
step: 0,
message: {
id: 'fake-malformed-message',
role: 'assistant',
content: 'not-an-array',
source: { kind: 'model', provider: 'fake', model: 'fake' },
},
})
return
}
if (env.FAKE_MESSAGE_WITHOUT_DATA !== undefined) {
@@ -105,8 +114,12 @@ function runTurn(sessionId: string): void {
event(sessionId, 'assistant/message', {
turn: 0,
step: 0,
content: [{ type: 'text', text }],
provenance: { provider: 'fake', model: 'fake' },
message: {
id: `fake-assistant-${seq}`,
role: 'assistant',
content: [{ type: 'text', text }],
source: { kind: 'model', provider: 'fake', model: 'fake' },
},
})
const reasonKind = env.FAKE_REASON_KIND ?? 'completed'
event(sessionId, 'turn/end', { turn: 0, reason: { kind: reasonKind } })

View File

@@ -85,7 +85,8 @@ describe('DeepSeekHarness', () => {
expect(childEvents.length).toBeGreaterThan(0)
// Child events do not count as the parent's own turn events.
expect(result.events.every(event => event.type !== 'assistant/message'
|| (event.data as { content: { type: string; text?: string }[] }).content[0]?.text !== 'child says hi')).toBe(true)
|| event.data.message.content[0]?.type !== 'text'
|| event.data.message.content[0].text !== 'child says hi')).toBe(true)
await harness.close()
})
@@ -471,8 +472,8 @@ describe('pure helpers', () => {
expect(finalResponse([])).toBe('')
expect(finalResponse([{ type: 'turn/start', seq: 0, time: 0, data: { turn: 0 } } as never])).toBe('')
expect(finalResponse([
{ type: 'assistant/message', seq: 0, time: 0, data: { content: [{ type: 'text', text: 'first' }] } } as never,
{ type: 'assistant/message', seq: 1, time: 0, data: { content: [{ type: 'text', text: 'a' }, { type: 'tool-call' }, { type: 'text', text: 'b' }] } } as never,
{ type: 'assistant/message', seq: 0, time: 0, data: { message: { content: [{ type: 'text', text: 'first' }] } } } as never,
{ type: 'assistant/message', seq: 1, time: 0, data: { message: { content: [{ type: 'text', text: 'a' }, { type: 'tool-call' }, { type: 'text', text: 'b' }] } } } as never,
])).toBe('ab')
})
})