From 7b1daa2a5495d5d0b777d1a5eff7b7113bb5985a Mon Sep 17 00:00:00 2001 From: Manthan Thakar Date: Tue, 17 Feb 2026 22:29:37 -0800 Subject: [PATCH] Summarize large tool payloads before LLM prompts (#74) --- .../investigation-orchestrator.test.ts | 69 +++++++++++ src/agent/investigation-orchestrator.ts | 107 +++++++++++++++++- 2 files changed, 173 insertions(+), 3 deletions(-) diff --git a/src/agent/__tests__/investigation-orchestrator.test.ts b/src/agent/__tests__/investigation-orchestrator.test.ts index 92961db..cafd749 100644 --- a/src/agent/__tests__/investigation-orchestrator.test.ts +++ b/src/agent/__tests__/investigation-orchestrator.test.ts @@ -685,6 +685,75 @@ describe('InvestigationOrchestrator', () => { expect(remediationPrompt).toContain('Available Code Fix Candidates'); expect(remediationPrompt).toContain('https://github.com/acme/platform/pull/42'); }); + + it('should truncate oversized tool payloads before composing triage and evidence prompts', async () => { + let callIndex = 0; + const prompts: string[] = []; + const hugePayload = 'X'.repeat(10000); + + const complete = vi.fn().mockImplementation(async (prompt: string) => { + prompts.push(prompt); + callIndex++; + if (callIndex === 1) return mockTriageResponse; + if (callIndex === 2) return mockHypothesisResponse; + if (callIndex === 3) return mockEvidenceEvaluationConfirm; + if (callIndex === 4) return mockConclusionResponse; + if (callIndex === 5) return mockRemediationResponse; + return mockEvidenceEvaluationPrune; + }); + const llm: LLMClient = { complete }; + + const execute = vi.fn().mockImplementation(async (tool: string) => { + if (tool === 'search_knowledge') { + return { + documentCount: 1, + runbooks: [{ title: 'Large Runbook', content: hugePayload }], + }; + } + if (tool === 'cloudwatch_alarms') { + return [{ alarmName: 'HighLatency', stateValue: 'ALARM', description: hugePayload }]; + } + if (tool === 'cloudwatch_logs') { + return [{ message: hugePayload }]; + } + if (tool === 'datadog') { + return { triggeredMonitors: [{ name: 'Latency Monitor', details: hugePayload }] }; + } + if (tool === 'aws_query') { + return { + totalResources: 1, + results: { + lambda: { + count: 1, + resources: [{ id: 'fn_1', raw: { details: hugePayload } }], + }, + }, + }; + } + return { success: true }; + }); + const toolExecutor: ToolExecutor = { execute }; + + const orchestrator = createOrchestrator(llm, toolExecutor, { + availableTools: [ + 'search_knowledge', + 'cloudwatch_alarms', + 'cloudwatch_logs', + 'datadog', + 'aws_query', + ], + }); + + await orchestrator.investigate('Investigate prompt truncation behavior'); + + const triagePrompt = prompts[0] || ''; + const evidencePrompt = prompts[2] || ''; + + expect(triagePrompt).toContain('truncated'); + expect(evidencePrompt).toContain('truncated'); + expect(triagePrompt.length).toBeLessThan(12000); + expect(evidencePrompt.length).toBeLessThan(15000); + }); }); describe('log analysis', () => { diff --git a/src/agent/investigation-orchestrator.ts b/src/agent/investigation-orchestrator.ts index 665c3b0..0aa0978 100644 --- a/src/agent/investigation-orchestrator.ts +++ b/src/agent/investigation-orchestrator.ts @@ -88,6 +88,30 @@ export interface InvestigationOptions { fetchRelevantRunbooks?: (context: RemediationContext) => Promise; } +interface PromptPayloadLimits { + maxChars: number; + maxDepth: number; + maxArrayItems: number; + maxObjectKeys: number; + maxStringLength: number; +} + +const TRIAGE_PROMPT_LIMITS: PromptPayloadLimits = { + maxChars: 2800, + maxDepth: 4, + maxArrayItems: 12, + maxObjectKeys: 20, + maxStringLength: 320, +}; + +const EVIDENCE_PROMPT_LIMITS: PromptPayloadLimits = { + maxChars: 3600, + maxDepth: 5, + maxArrayItems: 15, + maxObjectKeys: 25, + maxStringLength: 400, +}; + /** * Investigation result */ @@ -627,6 +651,77 @@ export class InvestigationOrchestrator { return formatted; } + private normalizePromptPayload( + value: unknown, + limits: PromptPayloadLimits, + depth: number = 0 + ): unknown { + if (value === null || value === undefined) { + return value; + } + + if (typeof value === 'string') { + if (value.length <= limits.maxStringLength) { + return value; + } + const remaining = value.length - limits.maxStringLength; + return `${value.slice(0, limits.maxStringLength)}... [${remaining} chars truncated]`; + } + + if (typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (depth >= limits.maxDepth) { + return '[truncated: max depth reached]'; + } + + if (Array.isArray(value)) { + const normalizedItems = value + .slice(0, limits.maxArrayItems) + .map((item) => this.normalizePromptPayload(item, limits, depth + 1)); + if (value.length > limits.maxArrayItems) { + normalizedItems.push(`[... ${value.length - limits.maxArrayItems} more items omitted]`); + } + return normalizedItems; + } + + if (typeof value === 'object') { + const entries = Object.entries(value as Record); + const normalized: Record = {}; + + for (const [key, entryValue] of entries.slice(0, limits.maxObjectKeys)) { + normalized[key] = this.normalizePromptPayload(entryValue, limits, depth + 1); + } + + if (entries.length > limits.maxObjectKeys) { + normalized._truncatedKeys = `${entries.length - limits.maxObjectKeys} keys omitted`; + } + + return normalized; + } + + return String(value); + } + + private summarizePromptPayload(value: unknown, limits: PromptPayloadLimits): string { + const normalized = this.normalizePromptPayload(value, limits); + let serialized: string; + + try { + serialized = JSON.stringify(normalized, null, 2); + } catch { + serialized = String(value); + } + + if (serialized.length <= limits.maxChars) { + return serialized; + } + + const remaining = serialized.length - limits.maxChars; + return `${serialized.slice(0, limits.maxChars)}\n... [${remaining} chars truncated]`; + } + /** * Run a full investigation */ @@ -797,7 +892,9 @@ export class InvestigationOrchestrator { const result = await this.toolExecutor.execute(source.tool, source.params); this.emit({ type: 'query_complete', query: triageQuery, result }); if (result && typeof result === 'object' && !(result as Record).error) { - contextParts.push(`${source.label}: ${JSON.stringify(result)}`); + contextParts.push( + `${source.label}: ${this.summarizePromptPayload(result, TRIAGE_PROMPT_LIMITS)}` + ); break; } } catch (error) { @@ -853,7 +950,9 @@ export class InvestigationOrchestrator { this.emit({ type: 'query_complete', query: triageQuery, result }); this.updateCloudWatchHints(source.tool, result, source.params); if (result) { - contextParts.push(`${source.label}: ${JSON.stringify(result)}`); + contextParts.push( + `${source.label}: ${this.summarizePromptPayload(result, TRIAGE_PROMPT_LIMITS)}` + ); if (this.hasMeaningfulTriageSignal(source.tool, result)) { break; } @@ -1010,7 +1109,9 @@ export class InvestigationOrchestrator { // Format evidence for LLM const evidenceLines: string[] = []; for (const [queryId, result] of queryResults) { - evidenceLines.push(`Query ${queryId}:\n${JSON.stringify(result, null, 2)}`); + evidenceLines.push( + `Query ${queryId}:\n${this.summarizePromptPayload(result, EVIDENCE_PROMPT_LIMITS)}` + ); } const prompt = fillPrompt(PROMPTS.evaluateEvidence, {