mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
166 lines
6.6 KiB
TypeScript
166 lines
6.6 KiB
TypeScript
/**
|
|
* Default one-shot summarization and durable checkpoint framing.
|
|
*
|
|
* @module @deepseek-ai/dsh-compact-basic/summarizer
|
|
*/
|
|
|
|
import type { Context } from 'cordis'
|
|
import { BlockAssembler } from '@deepseek-ai/dsh-llm'
|
|
import type { ContentBlock, FinishReason, GenerateOptions } from '@deepseek-ai/dsh-llm'
|
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
import type { ResolvedConfig } from './types.ts'
|
|
|
|
/** Tags wrapping the structured summary inside the landed checkpoint node. */
|
|
const SUMMARY_OPEN_TAG = '<compacted-summary>'
|
|
const SUMMARY_CLOSE_TAG = '</compacted-summary>'
|
|
|
|
/** Fixed structure required from the auxiliary summarization call. */
|
|
const SUMMARIZE_SYSTEM_PROMPT = [
|
|
'You are a compaction engine for an AI coding assistant. Condense the conversation transcript into a structured checkpoint that lets another model resume the work with no loss of essential context.',
|
|
'',
|
|
'Output EXACTLY the Markdown structure below: keep every section, in order. Use terse bullets, not prose paragraphs. Write "(none)" for an empty section — never drop a section.',
|
|
'',
|
|
'## Primary Request and Intent',
|
|
"- [the user's original and evolving goals; quote verbatim where the exact wording matters]",
|
|
'',
|
|
'## Key Technical Concepts',
|
|
'- [technologies, frameworks, patterns, and conventions in play]',
|
|
'',
|
|
'## Files and Code',
|
|
'- [exact path: why it matters, key changes or snippets]',
|
|
'',
|
|
'## Errors and Fixes',
|
|
'- [error: how it was resolved, plus any related user feedback]',
|
|
'',
|
|
'## Pending Tasks',
|
|
'- [explicitly requested work not yet completed]',
|
|
'',
|
|
'## Current Work',
|
|
'- [precisely what was in progress at this checkpoint]',
|
|
'',
|
|
'## Next Step',
|
|
'- [the single next action, directly in line with the most recent request, or "(none)"]',
|
|
'',
|
|
'## Critical Context',
|
|
'- [decisions and their rationale, constraints, user preferences, open questions, data needed to continue]',
|
|
'',
|
|
'Rules:',
|
|
'- Preserve exact file paths, commands, error strings, identifiers, and function signatures.',
|
|
'- Capture user feedback and explicit instructions faithfully, especially corrections.',
|
|
'- Do NOT mention this summarization process or that the context was compacted.',
|
|
`- If the transcript already contains a ${SUMMARY_OPEN_TAG} block, it is a PRIOR checkpoint. Do not copy it forward verbatim: preserve still-true facts, drop stale ones, and merge newer information into a single consolidated summary under the same structure.`,
|
|
].join('\n')
|
|
|
|
/** Framing that makes the replacement user message established context. */
|
|
const CHECKPOINT_PREAMBLE =
|
|
'This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.'
|
|
|
|
/** Safe summary content plus the exact auxiliary call envelope recorded in provenance. */
|
|
export interface SummaryResult {
|
|
summary: ContentBlock[]
|
|
provider: string
|
|
model: string
|
|
maxTokens?: number
|
|
}
|
|
|
|
/**
|
|
* Run the default direct `ctx.llm.stream()` summarization call.
|
|
* @param ctx - context providing the LLM service.
|
|
* @param config - resolved backend configuration.
|
|
* @param text - rendered transcript region to summarize.
|
|
* @param agent - supplies routed-model history, fallback model, and session id.
|
|
* @param signal - optional cancellation forwarded to the adapter.
|
|
* @returns safe text-only summary blocks and exact call provenance.
|
|
*/
|
|
export async function summarizeWithLlm(
|
|
ctx: Context,
|
|
config: ResolvedConfig,
|
|
text: string,
|
|
agent: Agent,
|
|
signal?: AbortSignal,
|
|
): Promise<SummaryResult> {
|
|
const latest = agent.session.requestHeader()?.config
|
|
const configured = config.summarizationProvider.length === 0
|
|
? undefined
|
|
: { provider: config.summarizationProvider, model: config.summarizationModel }
|
|
const agentTarget = agent.options.provider !== undefined
|
|
&& agent.options.provider.length > 0
|
|
&& agent.options.model !== undefined
|
|
&& agent.options.model.length > 0
|
|
? { provider: agent.options.provider, model: agent.options.model }
|
|
: undefined
|
|
const target = configured ?? latest ?? agentTarget
|
|
if (target === undefined) {
|
|
throw new Error(
|
|
'no provider/model available for summarization: set both BasicCompactConfig summarization fields, route one request, or set both AgentOptions fields',
|
|
)
|
|
}
|
|
|
|
const assembler = new BlockAssembler()
|
|
const options: GenerateOptions = {
|
|
provider: target.provider,
|
|
model: target.model,
|
|
messages: [{
|
|
role: 'user',
|
|
content: [{ type: 'text', text: `Summarize this conversation history:\n\n${text}\n\nSummary:` }],
|
|
}],
|
|
system: SUMMARIZE_SYSTEM_PROMPT,
|
|
maxTokens: config.maxTokens,
|
|
sessionId: agent.session.id,
|
|
...signal === undefined ? {} : { signal },
|
|
}
|
|
for await (const chunk of ctx.llm.stream(options)) assembler.push(chunk)
|
|
const error = finishError(assembler.finish)
|
|
if (error !== undefined) throw error
|
|
|
|
const summary = textOnly(assembler.message().content)
|
|
if (!summary.some(block => block.text.trim().length > 0)) {
|
|
throw new Error('summarization produced no text summary content')
|
|
}
|
|
return {
|
|
summary,
|
|
provider: options.provider,
|
|
model: options.model,
|
|
maxTokens: config.maxTokens,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrap raw summary blocks in the durable checkpoint framing.
|
|
* @param summary - safe text-only model output.
|
|
* @returns content for the synthesized replacement user message.
|
|
*/
|
|
export function frameSummary(summary: readonly ContentBlock[]): ContentBlock[] {
|
|
return [
|
|
{ type: 'text', text: `${CHECKPOINT_PREAMBLE}\n\n${SUMMARY_OPEN_TAG}` },
|
|
...summary,
|
|
{ type: 'text', text: SUMMARY_CLOSE_TAG },
|
|
]
|
|
}
|
|
|
|
/** Map a terminal summarization finish to its fail-closed error. */
|
|
function finishError(finish: FinishReason): Error | undefined {
|
|
switch (finish.kind) {
|
|
case 'error':
|
|
case 'aborted': {
|
|
const error = new Error(finish.failure.message) as Error & { code?: string }
|
|
error.code = finish.failure.code
|
|
return error
|
|
}
|
|
case 'max-tokens': {
|
|
const error = new Error('summarization truncated at the token cap (incomplete checkpoint)') as Error & { code?: string }
|
|
error.code = 'MAX_TOKENS'
|
|
return error
|
|
}
|
|
default:
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
/** Keep only text blocks before synthesizing a user message. */
|
|
function textOnly(
|
|
blocks: readonly ContentBlock[],
|
|
): Array<Extract<ContentBlock, { type: 'text' }>> {
|
|
return blocks.filter((block): block is Extract<ContentBlock, { type: 'text' }> => block.type === 'text')
|
|
}
|