Add presentation-neutral HydraFusion progress reducer - #2447
Add presentation-neutral HydraFusion progress reducer#2447carlosscastro wants to merge 2 commits into
Conversation
Projects HydraFusion session events into a deterministic, presentation-neutral snapshot so any consumer (terminal UI, web UI, logs) can render turn progress without reimplementing the event bookkeeping. The reducer is pure and event-driven: it tolerates duplicate and out-of-order events, recovers phases whose ephemeral signals were missed, and degrades to the existing phase events on runtimes that do not yet publish the new phase plan, phase activity, or permission attribution. It never issues an RPC. It retains only event discriminants, phase kinds/roles/scopes, phase plan metadata, response byte counts, tool call IDs, permission attribution, commit IDs, and the stable terminal outcome. Phase content, verdicts, prompts, reasoning, provider error detail, and concrete model identities are never read. The newest experimental producer fields (session.fusion_resolved.data.phasePlan, assistant.fusion_phase_activity, and fusion attribution on permission events) do not have generated declarations in this repo yet, so the reducer accepts a structurally typed event input that the generated SessionEvent union satisfies. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b51c537e-a447-4a75-af56-0528910afdce
This comment has been minimized.
This comment has been minimized.
Review follow-up on two turn-identity defects in the progress reducer. alignTurn adopted a turn identity from unresolved phase, activity, tool, or permission evidence without moving the projection out of "inactive", so a consumer folding those events before session.fusion_resolved saw a turn with phases but no lifecycle. Such evidence can only come from a turn that is already executing, so adopting it now promotes the projection to "running". alignTurn also let any mismatching fusionId replace a completed or fallback projection, so a late ephemeral from a previous turn could wipe the current turn's completion, commit, plan, and phases. Only the durable session.fusion_resolved path may now establish or replace a turn identity, and a terminal projection is never resurrected by non-resolved evidence. Late prior-turn events and the first ephemerals of a next turn are ignored until it resolves. Adds regressions asserting the intermediate pre-resolution state, promotion from each unresolved evidence kind, reference-stable completion/commit/plan/phases across a full sweep of stale prior-turn events after a later turn completed, and that a fallback projection is only replaced by an authoritative resolved route. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b51c537e-a447-4a75-af56-0528910afdce
Cross-SDK Consistency Review ✅This PR adds Assessment: No consistency issuesThe PR explicitly acknowledges the intentional single-SDK scope in its description:
This is a sound rationale:
RecommendationOnce
|
Problem
HydraFusion turns emit a rich stream of routing, phase, activity, permission, and completion events, but every consumer that wants to show
what is happening right nowhas to re-derive that state itself: correlate phases, tolerate ephemeral events it may have missed, and avoid leaking phase content or concrete model identities. That bookkeeping does not belong in each UI.This is the SDK slice of a coordinated change. The producer-side contract lives in the runtime draft PR github/copilot-agent-runtime#18041, which is not merged; this PR deliberately lands independently of it and works against runtimes with or without those fields.
Solution
Adds a reusable, presentation-neutral progress reducer to the Node/TypeScript SDK (the reference SDK, where the generated session-event types live). It contains no prose, color, timers, layout, or CLI-specific concepts — consumers decide entirely how to render the projection.
Public API (
@github/copilot-sdk)Usage is a plain fold:
FusionProgressStatecarries only shared facts, leaving wording and visuals to consumers:status:inactive|routing|fallback|running|completedfusionId,turnId,turnKind,pattern,policyplan: FusionPlannedPhase[](kind,role,scope,conditional) andplanSource: "runtime" | "unavailable"phases: FusionProgressPhase[]—phaseId,kind,role,scope,status(running/succeeded/failed/cancelled),degraded/degradedToPhaseId, last safeactivity,totalResponseSizeBytes,toolCalls(ID + started/completed), andplanIndexmatching the phase to its plan entrypendingPermissions: FusionProgressPermission[]— attributed requests still awaiting a decisionpublishedCommitIdandcompletion(outcome,degraded,commitId,finalSourcePhaseId)Supporting exported types:
FusionPlannedPhase,FusionProgressPhase,FusionProgressActivity,FusionProgressToolCall,FusionProgressPermission,FusionProgressCompletion, plus theFusionProgress*string unions (each widened withstringso unknown values from newer runtimes are preserved verbatim).Compatibility
planSourcestays"unavailable", tool activity is recovered fromtool.execution_start/tool.execution_completeattribution, and permission attribution is simply absent.running— an unresolved turn never looksinactivewhile phases are in flight. Once an identity is established, or once the turn reaches a terminalcompleted/fallbackstate, only the durablesession.fusion_resolvedmay replace it: late ephemerals from a previous turn, and the first ephemerals of a next turn that has not resolved yet, are intentionally ignored (returned by reference) rather than overwriting or resurrecting the current projection.Privacy
Only event discriminants, phase kinds/roles/scopes, phase plan metadata, response byte counts, tool call IDs, permission attribution, commit IDs, and the stable terminal outcome are retained. Phase content, verdicts, prompts, reasoning, critiques, provider error messages, degradation reason strings, and concrete model identities are never read or stored — degradation is exposed as a boolean. A test asserts none of them appear in the serialized state.
Typed-declaration boundary
This repo does not yet generate declarations for
session.fusion_resolved.data.phasePlan,assistant.fusion_phase_activity, orfusionattribution on permission events (they arrive with the runtime contract). The reducer therefore takes the structurally typedFusionProgressEventInputrather than the generatedSessionEventunion, so it works with generated events, hand-built events, and raw JSON-RPC payloads alike. A compile-time assertion in the tests provesSessionEventsatisfies that input, so callers need no adapter, and the reducer can switch to the generated types later without a breaking change.Validation
npx vitest run test/fusion-progress.test.ts— 23 new tests: single/cascade/critique patterns, old-runtime plan fallback, malformed plan payload, missed ephemerals, duplicates (value and reference identity), out-of-order events, activity and tool-call tracking, tool-attribution fallback, attributed permissions, published vs. authoritative commit, routing fallback, new-turn isolation, turn-identity promotion from each unresolved evidence kind, reference-stable completion/commit/plan/phases across stale prior-turn events after a later turn completed, fallback projections that only an authoritative resolved route may replace, and privacy exclusion.npx vitest run --exclude "**/test/e2e/**"— full Node unit suite green (452 passing, 17 files).npm run buildthennpx vitest run test/cjs-compat.test.ts— the new exports are present in both ESM and CJS bundles; verified by requiringdist/cjs/index.jsand folding a sample event stream.npm run lint,npm run format:check,npm run typecheck— clean.Caveats
@experimental.plan,activity, andpendingPermissionsstay empty against current runtimes — by design, and covered by the fallback tests.session.permissions.pendingRequestsexposes the samefusionattribution shape asFusionProgressPermission; seeding from it is left to consumers so the reducer stays free of RPC calls.