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
@@ -82,6 +82,8 @@ import com.correx.core.risk.RiskAssessor
import com.correx.core.risk.RiskContext
import com.correx.core.risk.toApprovalTier
import com.correx.core.sessions.Session
import com.correx.core.tools.compression.ToolOutputContext
import com.correx.core.tools.contract.Tool
import com.correx.core.tools.contract.ToolExecutor
import com.correx.core.tools.contract.ToolResult
import com.correx.core.tools.registry.ToolRegistry
@@ -365,7 +367,8 @@ abstract class SessionOrchestrator(
toolName = toolCall.function.name,
parameters = parameters,
)
val tier = toolRegistry?.resolve(toolCall.function.name)?.tier ?: Tier.T2
val tool = toolRegistry?.resolve(toolCall.function.name)
val tier = tool?.tier ?: Tier.T2
emit(
sessionId,
ToolInvocationRequestedEvent(
@@ -378,7 +381,7 @@ abstract class SessionOrchestrator(
),
)
val plane2Risk: RiskSummary? = runPlane2Assessment(
sessionId, stageId, invocationId, toolCall.function.name, request,
sessionId, stageId, invocationId, toolCall.function.name, request, tool,
)?.let { assessment ->
if (assessment.recommendedAction == RiskAction.BLOCK) {
emit(
@@ -571,7 +574,9 @@ abstract class SessionOrchestrator(
role = EntryRole.ASSISTANT,
)
val resultContent = when (result) {
is ToolResult.Success -> result.output
is ToolResult.Success ->
tool?.outputCompressor?.compress(result.output, ToolOutputContext(result.exitCode))
?: result.output
is ToolResult.Failure -> {
if (!result.recoverable) "FATAL: ${result.reason}"
else "ERROR: ${result.reason}"
@@ -596,10 +601,10 @@ abstract class SessionOrchestrator(
invocationId: ToolInvocationId,
toolName: String,
request: ToolRequest,
tool: Tool?,
): RiskSummary? {
val assessor = toolCallAssessor ?: return null
val policy = workspacePolicy ?: return null
val tool = toolRegistry?.resolve(toolName)
val assessment = assessor.assess(
ToolCallAssessmentInput(
request = request,