symfonic.agent.cutover.projection¶
projection ¶
Projecting one kernel outcome onto the legacy AgentResponse/StreamChunk.
The outward half of the delegate's translation, split out of
:mod:~symfonic.agent.cutover.delegate when TA8.10 gave that module the
inbound half to carry as well. The seam is the direction of travel: this module
only ever reads a finished kernel value and writes the shape a legacy caller
already holds. It compiles nothing, calls no kernel, and holds no state.
Translation is not interpretation. Where the legacy shape has a column the
kernel path cannot fill — memory_entries_used, activation_log — the
empty value is written rather than an estimate, because the empty value is true
and an estimate is a fabrication that survives into a metric.
as_chunk ¶
Project one kernel event onto the legacy chunk shape.
error raises rather than yielding: the legacy stream contract reports
failure by raising out of the generator, and inventing a chunk kind for it
would put a failure on the success path of every existing consumer.
ask_user and interrupt fall through to None, and that is a
declared gap, not an oversight. TA8.34 made a kernel run able to pause, so
this route can now end without a done chunk where before every kernel
chunk stream ended on one. It is not a parity regression -- the legacy chunk
body has no interrupt chunk shape either, and no event_type="interrupt"
or "ask_user" exists anywhere in src/ -- but it means a chunk
consumer cannot tell a pause from a completed turn whose terminal went
missing. Giving the chunk surface a pause shape is a versioned addition to a
public wire contract and belongs with resume (TA8.35), not with the typed
projection this task scoped. Recorded in
.agent/team/framework-refactor/evidence/RET-PREP/hk1-pause-surface.md §8.
Source code in src/symfonic/agent/cutover/projection.py
as_response ¶
Project the kernel's result onto the legacy response shape.
An exhausted round budget is a failure, not an answer. The legacy body
raises GraphRecursionError out of the graph runtime when the loop runs
past config.agent.recursion_limit; projecting the kernel's
stop_reason="tool_limit" onto final_response="" would hand the
adopter an empty answer with nothing raised and no fallback recorded, so
cutover.fallbacks("invocation.run") would still report a clean
migration while the answer was wrong.
It raises :class:~symfonic.agent.cutover.errors.RecursionExhaustedError,
which is a GraphRecursionError, so the adopter's existing
except GraphRecursionError keeps catching this turn after the flip —
failing loudly with a different class would still have broken every
handler written against the documented legacy behaviour.
result.text or None reproduces the legacy tri-state rather than
narrowing it. The legacy body builds final_response=final_response or
None (symfonic.agent.engine), so an answerless turn surfaces as
None; writing "" here would be a type change visible to every
adopter whose code reads if response.final_response is None — silent,
since both are falsy, and shipped by default the day the run switch flips.