Skip to content

symfonic.agent.cutover.tool_result_surfaces

tool_result_surfaces

C1-I: the tool-result lifecycle, eight rows (TA8.39).

TA8.25 recommended MIGRATE for all eight, arguing that "the fields jointly control observable result retention, compaction, offload, and recall in both routes". The second half of that sentence is the part this lane had to check, and it is wrong: not one of the eight is read on the kernel route, and the mechanism that would read them does not exist there.

The phase was declared and nothing dispatched it; TA8.50 built the seam. Phase.POST_TOOL is the sixth rung of :data:~symfonic.kernel.contracts.stages.PHASE_LADDER, and it is the only rung a tool-result policy could sit on -- compaction and offload act on a result after the tool ran and before the next round sees it. When TA8.39 scored these rows the kernel dispatched post-model and pre-tool and nothing else, so a post-tool stage compiled into the plan and was invoked in no pass. kernel/post_tool.py::run_post_tool now exists and kernel/runner.py dispatches it once per tool-bearing round, after record_tools.

That changes the seam, and it changes no row here. Seven of these eight rows are read through state['_tool_compaction'], which no kernel turn writes, and the eighth is worse than unmigrated. What is owed now is per-row parity evidence from a post-tool capability that reads each value; admitting a row because a rung it could sit on runs would score the seam, not the migration.

Where they are read instead. All three legacy bodies stamp seven of the eight into state['_tool_compaction'] -- _legacy_run_impl, _stream_impl and _stream_typed_impl each build the same dict -- and core/nodes/react.py::_maybe_compact_tool_results reads it back per iteration through core/nodes/tool_results.py::build_result_policy_service. That service writes the ledger and re-stubs the transcript, and react.py is its only caller. The eighth row is read by nothing at all; see its own entry.

The triage's capability_fragments citation is a diagnostic, not a consumer. configuration/compat/fragments.py is reached only from FrameworkConfigAdapter, whose only production caller is symfonic.diagnostics.inspection.probe. It translates a configuration into a migration report. Five C1-I rows are cited to it, and a row scored as consumed on that basis is scored on the report about the migration.

One row is worse than unmigrated, and that is the strongest reason here. tool_result_compaction_enabled already has a reachable -- and harmful -- effect on the kernel route. See its entry below.

Every override argument sits on its own row. The eight fail for related reasons but not identical ones, and this lane's acceptance forbids aggregating parity across rows that merely share a file.

tool_result_surfaces_for_subgroup

tool_result_surfaces_for_subgroup(subgroup: str) -> tuple[AdmissionSurface, ...]

Every row of one TA8.25 subgroup, in declaration order.

Per subgroup rather than per lane for the reason tools_surfaces gives: eight rows sharing a stamp is not eight rows sharing evidence.

Source code in src/symfonic/agent/cutover/tool_result_surfaces.py
def tool_result_surfaces_for_subgroup(subgroup: str) -> tuple[AdmissionSurface, ...]:
    """Every row of one TA8.25 subgroup, in declaration order.

    Per subgroup rather than per lane for the reason ``tools_surfaces`` gives:
    eight rows sharing a stamp is not eight rows sharing evidence.
    """
    return tuple(
        surface
        for surface in TOOL_RESULT_SURFACES.values()
        if surface.subgroup == subgroup
    )