Files
deepseek-harness/packages/compact/compact/src/render.ts

96 lines
3.3 KiB
TypeScript

/**
* Pure shared transcript projection for summarization and recall, so both
* render the same log span byte-for-byte under replay.
* @module @deepseek-ai/dsh-compact/render
*/
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
/**
* Render text directly, reasoning as a tagged span, and every other block as a
* type-tagged placeholder. Tool results recurse into nested content; empty
* blocks contribute nothing and rendered blocks join with newlines.
*
* @param blocks - the content blocks to render.
* @returns the newline-joined plain-text rendering; empty string when nothing renders.
*/
export function renderContentBlocks(blocks: readonly ContentBlock[]): string {
const parts: string[] = []
for (const block of blocks) {
switch (block.type) {
case 'text':
if (block.text) parts.push(block.text)
break
case 'reasoning':
if (block.text) parts.push(`[reasoning: ${block.text}]`)
break
case 'tool-call':
parts.push(`[tool-call: ${block.name}(${block.arguments})]`)
break
case 'tool-result': {
const inner = renderContentBlocks(block.content)
parts.push(inner ? `[tool-result: ${inner}]` : '[tool-result]')
break
}
// ContentBlockMap is merge-extensible — render an unknown block as a
// bare type-tagged placeholder so a plugin-added block type is still
// signalled to the reader rather than dropped.
default:
parts.push(`[${(block as ContentBlock).type}]`)
}
}
return parts.join('\n')
}
/**
* Render message-producing events as a role-labeled transcript. `seqs` are
* walked in caller-supplied surface order, which may differ from numeric log
* order after replacement; non-surface and unknown merged events are skipped.
*
* @param events - the session log the seqs index into (`session.events`).
* @param seqs - the surface-node seqs to render, in surface order.
* @returns the transcript, entries joined by blank lines; empty string when nothing renders.
*/
export function renderTranscript(events: readonly SessionEvent[], seqs: readonly number[]): string {
const lines: string[] = []
for (const seq of seqs) {
const event = events[seq]
if (!event) continue
switch (event.type) {
case 'user/message': {
const text = renderContentBlocks(event.data.content)
if (text) lines.push(`User: ${text}`)
break
}
case 'assistant/message': {
const text = renderContentBlocks(event.data.content)
if (text) lines.push(`Assistant: ${text}`)
break
}
case 'tool/result': {
const text = renderContentBlocks(event.data.content)
const label = event.data.isError ? 'Tool error' : 'Tool result'
if (text) lines.push(`${label} (call ${event.data.callId}): ${text}`)
break
}
case 'context/message': {
const text = renderContentBlocks(event.data.content)
if (text) lines.push(`[Context: ${text}]`)
break
}
case 'steering/message': {
const text = renderContentBlocks(event.data.content)
if (text) lines.push(`[Steering: ${text}]`)
break
}
default:
break
}
}
return lines.join('\n\n')
}