|
| 1 | +# ADR-0009: Explicit handler mediation of partial results across action boundaries |
| 2 | + |
| 3 | +- **Status:** accepted |
| 4 | +- **Date:** 2026-03-24 |
| 5 | +- **Deciders:** @Aksem |
| 6 | +- **Tags:** actions, partial-results, architecture |
| 7 | + |
| 8 | +## Context |
| 9 | + |
| 10 | +FineCode supports **partial results**: incremental client-visible data that can |
| 11 | +be sent before an action fully completes. A direct action invocation can already |
| 12 | +emit partial results when the caller provides a `partial_result_token`. |
| 13 | + |
| 14 | +The gap appears when one action delegates to another. Higher-level actions such |
| 15 | +as `lint` and `format` may orchestrate more specific subactions such as |
| 16 | +`lint_files` or `format_files`. Without an explicit cross-action rule, partial |
| 17 | +results produced inside a delegated action either stop at the delegation |
| 18 | +boundary or bypass the parent action's contract entirely. |
| 19 | + |
| 20 | +That boundary matters for three reasons: |
| 21 | + |
| 22 | +1. The parent action owns the client-facing contract and may need to reshape a |
| 23 | + delegated action's partial result before exposing it. |
| 24 | +2. Orchestrator handlers should use the same logic whether partial results are |
| 25 | + enabled or not. |
| 26 | +3. The rule must work for both sequential delegation and concurrent |
| 27 | + orchestration. |
| 28 | + |
| 29 | +## Related ADRs Considered |
| 30 | + |
| 31 | +- [ADR-0007](0007-single-registration-per-action-definition.md) - requires |
| 32 | + multi-language orchestration to use explicit composition rather than duplicate |
| 33 | + registrations. This ADR defines how partial results behave across that |
| 34 | + composition boundary. |
| 35 | +- [ADR-0008](0008-explicit-specialization-metadata-for-language-actions.md) - |
| 36 | + specialization metadata for language-specific actions. Related to dispatch and |
| 37 | + orchestration, but not to partial-result flow. |
| 38 | + |
| 39 | +## Decision |
| 40 | + |
| 41 | +When one action delegates work to another, **partial results do not implicitly |
| 42 | +propagate across the action boundary**. |
| 43 | + |
| 44 | +If the parent action wants client-visible partial results while delegating, the |
| 45 | +parent handler must explicitly: |
| 46 | + |
| 47 | +- consume partial results from the delegated action |
| 48 | +- decide whether to expose them at all |
| 49 | +- map them into the parent action's client-facing result shape before |
| 50 | + re-emitting them |
| 51 | + |
| 52 | +The parent handler therefore owns the partial-result contract at its boundary in |
| 53 | +the same way it owns the final result contract. |
| 54 | + |
| 55 | +The framework must support this rule with explicit handler-facing mechanisms |
| 56 | +that allow a handler to: |
| 57 | + |
| 58 | +- consume delegated partial results incrementally |
| 59 | +- emit partial results from handler code without branching on whether partial |
| 60 | + results are active |
| 61 | +- use the same architectural rule in both sequential and concurrent |
| 62 | + orchestration patterns |
| 63 | + |
| 64 | +## Consequences |
| 65 | + |
| 66 | +- **Parent action boundaries stay explicit.** Nested actions cannot |
| 67 | + accidentally stream directly through a parent action without that parent's |
| 68 | + involvement. |
| 69 | +- **Result-shape ownership is clear.** A parent handler can translate, |
| 70 | + filter, enrich, or suppress delegated partial results before the client sees |
| 71 | + them. |
| 72 | +- **Orchestrator handlers stay mode-agnostic.** The same handler logic can run |
| 73 | + with or without partial-result support, with framework-provided no-op |
| 74 | + behavior when streaming is inactive. |
| 75 | +- **Sequential and concurrent orchestration are both supported.** The |
| 76 | + architectural rule is the same even when the concrete emission mechanism |
| 77 | + differs. |
| 78 | +- **Orchestrator APIs become slightly broader.** Handlers that compose other |
| 79 | + actions need explicit framework support for consuming and re-emitting partial |
| 80 | + results. Simple non-orchestrating handlers are unaffected. |
| 81 | + |
| 82 | +### Alternatives Considered |
| 83 | + |
| 84 | +**Implicit propagation through nested action calls.** Rejected because it lets a |
| 85 | +delegated action bypass the parent action's contract boundary. That makes |
| 86 | +result-shape mismatches and accidental exposure of subaction behavior much more |
| 87 | +likely. |
| 88 | + |
| 89 | +**Forward the token but let the subaction send results directly.** Rejected |
| 90 | +because the parent handler would lose control over client-visible partial-result |
| 91 | +shape and timing. A parent action needs to own both. |
| 92 | + |
| 93 | +**Only support manual handler-side emission.** Rejected because it makes the |
| 94 | +common sequential orchestration case more verbose than necessary. |
| 95 | + |
| 96 | +**Only support yield-style emission.** Rejected because some concurrent |
| 97 | +orchestration patterns cannot emit from the place where results become |
| 98 | +available. |
| 99 | + |
| 100 | +**Introduce a separate streaming-handler protocol now.** Deferred. Revisit if |
| 101 | +streaming handlers create recurring type-checking friction or require repeated |
| 102 | +runtime-only workarounds during handler authoring. |
| 103 | + |
| 104 | +## Implementation Notes |
| 105 | + |
| 106 | +The current implementation uses two handler-facing mechanisms: |
| 107 | + |
| 108 | +- `IActionRunner.run_action_iter()` for consuming delegated partial results |
| 109 | +- `RunActionContext.send_partial_result()` for explicit emission from handler |
| 110 | + code |
| 111 | + |
| 112 | +Async-generator handler `run()` methods are the preferred pattern for simple |
| 113 | +sequential mapping. Explicit `send_partial_result()` remains the escape hatch |
| 114 | +for concurrent patterns such as `TaskGroup`, where yielding from the point of |
| 115 | +completion is not practical. |
| 116 | + |
| 117 | +Current runtime details such as async-generator detection, queue-based bridging, |
| 118 | +and how final return values are handled after explicit partial emission are |
| 119 | +implementation choices rather than the architectural decision itself. |
0 commit comments