Skip to content

Add presentation-neutral HydraFusion progress reducer - #2447

Draft
carlosscastro wants to merge 2 commits into
mainfrom
carlosscastro-automatic-dollop
Draft

Add presentation-neutral HydraFusion progress reducer#2447
carlosscastro wants to merge 2 commits into
mainfrom
carlosscastro-automatic-dollop

Conversation

@carlosscastro

@carlosscastro carlosscastro commented Aug 31, 2026

Copy link
Copy Markdown

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 now has 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)

function initialFusionProgressState(): FusionProgressState;
function reduceFusionProgress(
    state: FusionProgressState,
    event: FusionProgressEventInput
): FusionProgressState;

interface FusionProgressEventInput { readonly type: string; readonly data?: unknown }

Usage is a plain fold:

const state = events.reduce(reduceFusionProgress, initialFusionProgressState());

FusionProgressState carries only shared facts, leaving wording and visuals to consumers:

  • status: inactive | routing | fallback | running | completed
  • fusionId, turnId, turnKind, pattern, policy
  • plan: FusionPlannedPhase[] (kind, role, scope, conditional) and planSource: "runtime" | "unavailable"
  • phases: FusionProgressPhase[]phaseId, kind, role, scope, status (running/succeeded/failed/cancelled), degraded/degradedToPhaseId, last safe activity, totalResponseSizeBytes, toolCalls (ID + started/completed), and planIndex matching the phase to its plan entry
  • pendingPermissions: FusionProgressPermission[] — attributed requests still awaiting a decision
  • publishedCommitId and completion (outcome, degraded, commitId, finalSourcePhaseId)

Supporting exported types: FusionPlannedPhase, FusionProgressPhase, FusionProgressActivity, FusionProgressToolCall, FusionProgressPermission, FusionProgressCompletion, plus the FusionProgress* string unions (each widened with string so unknown values from newer runtimes are preserved verbatim).

Compatibility

  • Additive and optional. Every new producer field is read defensively. Against an older runtime the projection degrades to the existing phase events: planSource stays "unavailable", tool activity is recovered from tool.execution_start/tool.execution_complete attribution, and permission attribution is simply absent.
  • No snapshot RPC. The reducer is pure and event-driven; a resumed session is rebuilt by replaying its durable events.
  • Duplicates are idempotent (repeat events return the same state, by reference where possible), and out-of-order events never regress a terminal phase or tool call.
  • Missed ephemerals recover: a phase first seen on its durable completion still appears.
  • Turn identity is authoritative. Evidence that arrives before the turn resolves (phase, activity, tool, or permission events) is adopted only while no turn identity exists yet, and it promotes the projection to running — an unresolved turn never looks inactive while phases are in flight. Once an identity is established, or once the turn reaches a terminal completed/fallback state, only the durable session.fusion_resolved may 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, or fusion attribution on permission events (they arrive with the runtime contract). The reducer therefore takes the structurally typed FusionProgressEventInput rather than the generated SessionEvent union, so it works with generated events, hand-built events, and raw JSON-RPC payloads alike. A compile-time assertion in the tests proves SessionEvent satisfies 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 build then npx vitest run test/cjs-compat.test.ts — the new exports are present in both ESM and CJS bundles; verified by requiring dist/cjs/index.js and folding a sample event stream.
  • npm run lint, npm run format:check, npm run typecheck — clean.

Caveats

  • The underlying HydraFusion contract is experimental; the helper is marked @experimental.
  • Only the Node/TypeScript SDK is implemented here, as the single canonical core. Behavior like this has no generator in this repo, so porting it to Python/Go/.NET/Rust/Java before the producer contract settles would create parallel hand-written implementations to keep in sync. Idiomatic ports can follow once github/copilot-agent-runtime#18041 lands and the fields are generated.
  • Until those fields ship, plan, activity, and pendingPermissions stay empty against current runtimes — by design, and covered by the fallback tests.
  • session.permissions.pendingRequests exposes the same fusion attribution shape as FusionProgressPermission; seeding from it is left to consumers so the reducer stays free of RPC calls.

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
@github-actions

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
@github-actions

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅

This PR adds reduceFusionProgress and initialFusionProgressState — a presentation-neutral HydraFusion progress reducer — exclusively to the Node.js/TypeScript SDK.

Assessment: No consistency issues

The PR explicitly acknowledges the intentional single-SDK scope in its description:

"Only the Node/TypeScript SDK is implemented here, as the single canonical core. Behavior like this has no generator in this repo, so porting it to Python/Go/.NET/Rust/Java before the producer contract settles would create parallel hand-written implementations to keep in sync. Idiomatic ports can follow once github/copilot-agent-runtime#18041 lands and the fields are generated."

This is a sound rationale:

  • The underlying producer-side contract (runtime PR #18041) is not yet merged
  • The feature is marked @experimental
  • The Node.js SDK is the canonical reference SDK where generated session-event types live
  • Porting to 5 other languages before the API stabilizes would create maintenance burden with no immediate benefit

Recommendation

Once github/copilot-agent-runtime#18041 lands and the fields are generated, consider tracking follow-up work to add idiomatic ports to Python, Go, .NET, Java, and Rust. The public API surface is well-defined in this PR, which will make porting straightforward.

Generated by SDK Consistency Review Agent for #2447 · sonnet46 19.7 AIC · ⌖ 5.44 AIC · ⊞ 6.6K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant