feat: tool-declared output compression (Tier A)

Tools can declare how their output is compressed before it enters the
context pack, replacing uniform handling. Open-threads 6.3 / ADR-0003.

- core:tools/compression: ToolOutputCompressor interface,
  IdentityCompressor default, declarative OutputCompressionSpec +
  CompressionRule (StripBlankLines, StripLeadingWhitespace,
  DropMatching, HeadTail), and the pure DeclarativeCompressor engine
  (regexes compiled at construction; bypassOnError passes raw on
  non-zero exit; never throws on input).
- Tool gains `outputCompressor` (default IdentityCompressor). FileReadTool
  strips blanks + leading whitespace; ShellTool strips blanks + head/tail.
- SessionOrchestrator applies the resolved tool's compressor on the
  ToolResult -> ContextEntry path only. Raw output stays authoritative
  in the event log (invariant #6); compression is a pure function of
  (raw, exitCode, spec), so no new event and replay stays deterministic.
- Spec is @Serializable/config-shaped (JSON round-trip tested). No
  custom-tool-from-config path exists today, so first-party tools wire
  in code; TOML-declared compression rides along when that path lands.

Tier B (format-aware parsers: failures-only test output, git-log oneline)
is a documented per-tool extension behind the interface, not built here.
This commit is contained in:
2026-05-31 21:20:16 +04:00
parent 7fa1db8345
commit d03479cea7
11 changed files with 342 additions and 9 deletions
@@ -2,6 +2,11 @@ package com.correx.infrastructure.tools.filesystem
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ToolRequest
import com.correx.core.tools.compression.CompressionRule.StripBlankLines
import com.correx.core.tools.compression.CompressionRule.StripLeadingWhitespace
import com.correx.core.tools.compression.DeclarativeCompressor
import com.correx.core.tools.compression.OutputCompressionSpec
import com.correx.core.tools.compression.ToolOutputCompressor
import com.correx.core.tools.contract.ParamRole
import com.correx.core.tools.contract.Tool
import com.correx.core.tools.contract.ToolCapability
@@ -49,6 +54,9 @@ class FileReadTool(
override val tier: Tier = Tier.T1
override val requiredCapabilities: Set<ToolCapability> = setOf(ToolCapability.FILE_READ)
override val paramRoles: Map<String, ParamRole> = mapOf("path" to ParamRole.PATH)
override val outputCompressor: ToolOutputCompressor = DeclarativeCompressor(
OutputCompressionSpec(listOf(StripBlankLines, StripLeadingWhitespace)),
)
override fun validateRequest(request: ToolRequest): ValidationResult {
val pathString = request.parameters["path"] as? String
@@ -4,6 +4,7 @@ import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.tools.compression.ToolOutputContext
import com.correx.core.tools.contract.ToolResult
import com.correx.core.tools.contract.ValidationResult
import kotlinx.coroutines.runBlocking
@@ -109,4 +110,12 @@ class FileReadToolTest {
val failure = result as ToolResult.Failure
assertTrue(failure.reason.contains("is not in the allowed list"))
}
@Test
fun `outputCompressor strips blank lines and leading whitespace`() {
val tool = FileReadTool()
val raw = " fun main() {\n\n println()\n }\n"
val compressed = tool.outputCompressor.compress(raw, ToolOutputContext(exitCode = 0))
assertEquals("fun main() {\nprintln()\n}", compressed)
}
}