Coverage for src / harness_utils / compaction / truncation.py: 96%
49 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-31 13:48 -0600
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-31 13:48 -0600
1"""Tier 1: Output truncation at tool execution boundary.
3Prevents large outputs from entering context by truncating at source.
4Cost: Free, Latency: 0ms.
5"""
7from dataclasses import dataclass
8from typing import Literal
10from harness_utils.config import TruncationConfig
13@dataclass
14class TruncationResult:
15 """Result of truncation operation."""
17 content: str
18 truncated: bool
19 output_path: str | None = None
20 bytes_removed: int = 0
23def truncate_output(
24 output: str,
25 config: TruncationConfig,
26 output_id: str | None = None,
27) -> TruncationResult:
28 """Truncate tool output if it exceeds limits.
30 Args:
31 output: The tool output to potentially truncate
32 config: Truncation configuration
33 output_id: ID for saving full output (if None, full output not saved)
35 Returns:
36 TruncationResult with content and metadata
37 """
38 lines = output.split("\n")
39 total_bytes = len(output.encode("utf-8"))
41 if len(lines) <= config.max_lines and total_bytes <= config.max_bytes:
42 return TruncationResult(
43 content=output,
44 truncated=False,
45 )
47 preview_lines: list[str] = []
48 bytes_accumulated = 0
50 if config.direction == "head":
51 for i, line in enumerate(lines):
52 if i >= config.max_lines:
53 break
54 line_bytes = len(line.encode("utf-8")) + 1 # +1 for newline
55 if bytes_accumulated + line_bytes > config.max_bytes:
56 break
57 preview_lines.append(line)
58 bytes_accumulated += line_bytes
59 else: # tail
60 for i in range(len(lines) - 1, -1, -1):
61 if len(preview_lines) >= config.max_lines:
62 break
63 line = lines[i]
64 line_bytes = len(line.encode("utf-8")) + 1 # +1 for newline
65 if bytes_accumulated + line_bytes > config.max_bytes:
66 break
67 preview_lines.insert(0, line)
68 bytes_accumulated += line_bytes
70 preview = "\n".join(preview_lines)
71 bytes_removed = total_bytes - bytes_accumulated
73 message = _format_truncated_message(
74 preview,
75 bytes_removed,
76 output_id,
77 config.direction,
78 )
80 return TruncationResult(
81 content=message,
82 truncated=True,
83 output_path=output_id,
84 bytes_removed=bytes_removed,
85 )
88def _format_truncated_message(
89 preview: str,
90 bytes_removed: int,
91 output_path: str | None,
92 direction: Literal["head", "tail"],
93) -> str:
94 """Format the truncated output message.
96 Args:
97 preview: Preview content (head or tail)
98 bytes_removed: Number of bytes that were removed
99 output_path: Path where full output was saved
100 direction: Direction of truncation
102 Returns:
103 Formatted message string
104 """
105 parts = [preview]
107 if bytes_removed > 0:
108 parts.append("")
109 parts.append(f"...{bytes_removed} bytes truncated...")
110 parts.append("")
112 if output_path:
113 parts.append(f"Full output saved to: {output_path}")
114 parts.append("Use search tools to query the full content or read specific sections.")
115 parts.append("Delegate large file processing to specialized exploration agents.")
117 return "\n".join(parts)