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() to load summarization prompt (when available) * - Falls back to inline prompt when prompt registry doesn't have the prompt * - 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: string;
// Try to load managed prompt for summarization (falls back to inline) let systemPrompt = `Summarize the document in under ${maxLength} words. Preserve key facts and structure.`; let promptMetadata; try { const managedPrompt = await ctx.prompts.load('task-summarize'); systemPrompt = managedPrompt.render({ content: '{{content}}', // Will be replaced per chunk format: `concise summary under ${maxLength} words`, }); promptMetadata = managedPrompt.toCallMetadata(); ctx.log.info('Using managed prompt', { uri: managedPrompt.uri }); } catch { await emitReferenceAuthorSignal(ctx, 'summarizer_prompt_registry_fallback', { prompt_uri: 'task-summarize', }); // Prompt not in registry -- use inline (acceptable for backward compat) ctx.log.debug('Managed prompt not available, using inline'); }
if (chunks.length > 1) { await emitReferenceAuthorSignal(ctx, 'summarizer_map_reduce', { chunk_count: chunks.length, document_path: input.document_path, }); }
if (chunks.length === 1) { // Single chunk: direct summarization const result = await ctx.llm.complete({ prompt: [ { role: 'system', content: systemPrompt }, { role: 'user', content: chunks[0]! }, ], temperature: 0.3, maxTokens: maxLength * 2, promptMetadata, }); finalSummary = result.content; } else { // Multi-chunk: map-reduce summarization // Step 1: Summarize each chunk const chunkSummaries: string[] = []; for (let i = 0; i < chunks.length; i++) { const result = await ctx.llm.complete({ prompt: [ { role: 'system', content: `Summarize this section (part ${i + 1} of ${chunks.length}) in 2-3 sentences. Focus on key points.`, }, { role: 'user', content: chunks[i]! }, ], temperature: 0.3, maxTokens: 300, }); chunkSummaries.push(result.content); }
// Step 2: Synthesize chunk summaries into final summary const synthesisResult = await ctx.llm.complete({ prompt: [ { role: 'system', content: `Synthesize these section summaries into one coherent summary of under ${maxLength} words. Maintain logical flow and highlight the most important points.`, }, { role: 'user', content: 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