Reference Implementationsbeginner
Research
document-summarizer
Summarize long documents with intelligent chunking (map-reduce pattern).
APIs Used
ctx.filesctx.llmctx.promptsctx.telemetry.emitCapabilities Required
document/summarizeWhat this demonstrates
- 1ctx.files.readText() to load large documents
- 2ctx.prompts.load() for reusable summarization prompt templates
- 3ctx.llm.complete() with map-reduce chunking for long-context documents
- 4Map-reduce pattern: chunk → summarize each → synthesize final summary
- 5ctx.telemetry.emit via emitReferenceAuthorSignal on prompt-registry fallback and multi-chunk runs
Source
View on GitHubtypescript
/** * Document Summarizer - Production Reference Agent * * Canon alignment: KB 105 * Demonstrates: ctx.files, ctx.llm, ctx.prompts, chunking strategy for long documents * * Real use case: Summarize long documents with intelligent chunking. * For documents exceeding LLM context, split into chunks, summarize each, * then synthesize a final summary. This is the pattern developers should copy. * * Updated for prompt management: * - Uses ctx.prompts.load() for governed summarization prompts (ECC-08) * - Falls back to inline system strings when prompts are not in registry (telemetry) * - Threads promptMetadata for telemetry when using managed prompts */
import { handler, withProvenanceContext } from '@human/agent-sdk';import type { ExecutionContext } from '@human/agent-sdk';import { emitReferenceAuthorSignal } from '../../lib/reference-author-telemetry.js';
export const AGENT_ID = 'document-summarizer';export const VERSION = '1.0.0';export const CAPABILITIES = ['document/summarize'];
export interface DocumentSummarizerInput { document_path: string; max_length?: number;}
export interface DocumentSummarizerOutput { success: boolean; summary: string; word_count: number; chunks_processed: number; provenance_id: string;}
/** Approximate token limit for a single LLM call context window */const CHUNK_CHAR_LIMIT = 8000;
/** * Split text into chunks at paragraph boundaries. * Avoids splitting mid-sentence for better summarization. */function chunkText(text: string, maxChars: number): string[] { if (text.length <= maxChars) return [text];
const paragraphs = text.split(/\n\n+/); const chunks: string[] = []; let current = '';
for (const para of paragraphs) { if (current.length + para.length + 2 > maxChars && current.length > 0) { chunks.push(current.trim()); current = para; } else { current += (current ? '\n\n' : '') + para; } } if (current.trim()) chunks.push(current.trim());
return chunks;}
const execute = async ( ctx: ExecutionContext, input: DocumentSummarizerInput): Promise<DocumentSummarizerOutput> => { ctx.log.info('Summarizing document', { path: input.document_path });
const content = await ctx.files.readText(input.document_path); const maxLength = input.max_length ?? 500; const chunks = chunkText(content, CHUNK_CHAR_LIMIT);
ctx.log.info('Document chunked', { totalChars: content.length, chunks: chunks.length });
let finalSummary = '';
const useGovernedPrompts = async (): Promise<boolean> => { try { const singleChunkPrompt = await ctx.prompts.load('core/agents/document-summarizer/system'); const chunkSectionPrompt = await ctx.prompts.load( 'core/agents/document-summarizer/chunk-section' ); const chunkSynthesisPrompt = await ctx.prompts.load( 'core/agents/document-summarizer/chunk-synthesis' );
if (chunks.length > 1) { await emitReferenceAuthorSignal(ctx, 'summarizer_map_reduce', { chunk_count: chunks.length, document_path: input.document_path, }); }
if (chunks.length === 1) { const result = await ctx.llm.complete({ system: singleChunkPrompt.render({ max_length: String(maxLength) }), prompt: chunks[0]!, temperature: 0.3, maxTokens: maxLength * 2, promptMetadata: singleChunkPrompt.toCallMetadata(), }); finalSummary = result.content; } else { const chunkSummaries: string[] = []; for (let i = 0; i < chunks.length; i++) { const result = await ctx.llm.complete({ system: chunkSectionPrompt.render({ part: String(i + 1), total: String(chunks.length), }), prompt: chunks[i]!, temperature: 0.3, maxTokens: 300, promptMetadata: chunkSectionPrompt.toCallMetadata(), }); chunkSummaries.push(result.content); }
const synthesisResult = await ctx.llm.complete({ system: chunkSynthesisPrompt.render({ max_length: String(maxLength) }), prompt: chunkSummaries.map((s, i) => `Section ${i + 1}: ${s}`).join('\n\n'), temperature: 0.3, maxTokens: maxLength * 2, promptMetadata: chunkSynthesisPrompt.toCallMetadata(), }); finalSummary = synthesisResult.content; } return true; } catch { await emitReferenceAuthorSignal(ctx, 'summarizer_prompt_registry_fallback', { prompt_uri: 'core/agents/document-summarizer/*', }); ctx.log.debug('Governed summarizer prompts not available, using inline fallback'); return false; } };
const governedOk = await useGovernedPrompts();
if (!governedOk) { if (chunks.length > 1) { await emitReferenceAuthorSignal(ctx, 'summarizer_map_reduce', { chunk_count: chunks.length, document_path: input.document_path, }); }
if (chunks.length === 1) { const result = await ctx.llm.complete({ system: `Summarize the document in under ${maxLength} words. Preserve key facts and structure.`, prompt: chunks[0]!, temperature: 0.3, maxTokens: maxLength * 2, }); finalSummary = result.content; } else { const chunkSummaries: string[] = []; for (let i = 0; i < chunks.length; i++) { const result = await ctx.llm.complete({ system: `Summarize this section (part ${i + 1} of ${chunks.length}) in 2-3 sentences. Focus on key points.`, prompt: chunks[i]!, temperature: 0.3, maxTokens: 300, }); chunkSummaries.push(result.content); }
const synthesisResult = await ctx.llm.complete({ system: `Synthesize these section summaries into one coherent summary of under ${maxLength} words. Maintain logical flow and highlight the most important points.`, prompt: chunkSummaries.map((s, i) => `Section ${i + 1}: ${s}`).join('\n\n'), temperature: 0.3, maxTokens: maxLength * 2, }); finalSummary = synthesisResult.content; } }
const wordCount = finalSummary.split(/\s+/).length;
const provenanceId = await ctx.provenance.log( withProvenanceContext(ctx, { action: 'document:summarized', status: 'success', input: { document_path: input.document_path }, output: { word_count: wordCount, chunks_processed: chunks.length }, }) );
return { success: true, summary: finalSummary, word_count: wordCount, chunks_processed: chunks.length, provenance_id: provenanceId, };};
export default handler({ name: AGENT_ID, id: AGENT_ID, version: VERSION, capabilities: CAPABILITIES, manifest: { operations: [ { name: 'summarize', description: 'Summarize long documents with intelligent chunking and optional managed prompts', paramsSchema: { document_path: { type: 'string', required: true, description: 'Path to document' }, max_length: { type: 'number', description: 'Max summary length in words' }, }, resultKind: 'agent.document-summarizer.result', }, ], }, execute,});Run the tests
From monorepo root
$ pnpm test:agents:reference
$ pnpm test:agents:reference:verbose
The reference suite runs all 23 agents with createMockExecutionContext(), verifying every ctx.* API call and output shape.
See Also
SDK Reference