Reference Implementations

Research

document-summarizer

beginner

Summarize long documents with intelligent chunking (map-reduce pattern).

APIs Used

ctx.filesctx.llmctx.promptsctx.telemetry.emit

Capabilities Required

document/summarize

What 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
typescript
/**
* 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