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:
+8
@@ -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
|
||||
|
||||
+9
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -2,6 +2,11 @@ package com.correx.infrastructure.tools.shell
|
||||
|
||||
import com.correx.core.approvals.Tier
|
||||
import com.correx.core.events.events.ToolRequest
|
||||
import com.correx.core.tools.compression.CompressionRule.HeadTail
|
||||
import com.correx.core.tools.compression.CompressionRule.StripBlankLines
|
||||
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
|
||||
@@ -44,6 +49,9 @@ class ShellTool(
|
||||
override val requiredCapabilities: Set<ToolCapability> =
|
||||
setOf(ToolCapability.SHELL_EXEC, ToolCapability.PROCESS_SPAWN)
|
||||
override val paramRoles: Map<String, ParamRole> = mapOf("argv" to ParamRole.EXEC_COMMAND)
|
||||
override val outputCompressor: ToolOutputCompressor = DeclarativeCompressor(
|
||||
OutputCompressionSpec(listOf(StripBlankLines, HeadTail(HEAD_LINES, TAIL_LINES)), bypassOnError = true),
|
||||
)
|
||||
|
||||
override fun validateRequest(request: ToolRequest): ValidationResult {
|
||||
val argv = when (val raw = request.parameters["argv"]) {
|
||||
@@ -103,6 +111,11 @@ class ShellTool(
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val HEAD_LINES = 40
|
||||
const val TAIL_LINES = 40
|
||||
}
|
||||
|
||||
private suspend fun runCmd(request: ToolRequest, process: Process): ToolResult = withTimeout(timeoutMs) {
|
||||
val stdoutDeferred = async { process.inputStream.bufferedReader().use { it.readText() } }
|
||||
val stderrDeferred = async { process.errorStream.bufferedReader().use { it.readText() } }
|
||||
|
||||
+22
@@ -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
|
||||
@@ -112,4 +113,25 @@ class ShellToolTest {
|
||||
assertEquals("Process timed out after 100ms", failure.reason)
|
||||
assertFalse(failure.recoverable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `outputCompressor strips blanks and head-tail elides long output`() {
|
||||
val tool = ShellTool()
|
||||
val body = (1..100).joinToString("\n") { "line$it" }
|
||||
val raw = "\n$body\n\n"
|
||||
val compressed = tool.outputCompressor.compress(raw, ToolOutputContext(exitCode = 0))
|
||||
val lines = compressed.split("\n")
|
||||
assertEquals("line1", lines.first())
|
||||
assertEquals("line100", lines.last())
|
||||
assertEquals(81, lines.size) // 40 head + 1 marker + 40 tail
|
||||
assertTrue(compressed.contains("lines elided"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `outputCompressor passes raw through on non-zero exit`() {
|
||||
val tool = ShellTool()
|
||||
val raw = "a\n\nb\n\nc"
|
||||
val compressed = tool.outputCompressor.compress(raw, ToolOutputContext(exitCode = 1))
|
||||
assertEquals(raw, compressed)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user