Skip to content

F107: Single core ↔ interfaces facade #371

@pocky

Description

@pocky

F107: Single core ↔ interfaces facade

Scope

In Scope

  • Application facade port (WorkflowFacade) exposing async-first Run/Resume (returning RunSession) and sync List/Validate/Status/History
  • Canonical pack/workflow resolver consolidating CLI, TUI, HTTP, ACP, and subworkflow name resolution
  • Bidirectional RunSession with bounded replay buffer indexed by Seq, idempotent Close, and Respond() for input collection
  • Adapter composing WorkflowService, ExecutionService, HistoryService, and Resolver; sole subscriber of the F106 Recorder exchange stream (Recorder.Subscribe()) and owner of 4 edge bridges (EventPublisher, OutputWriters, DisplayRenderer, bidirectional UserInputReader)
  • Event projection derived from F106 transcript.ExchangeEvent onto neutral facade.Event, reusing the Seq already carried by the transcript
  • Provider-agnostic event contract: Claude, Gemini, Codex, Copilot, and OpenAI-compatible agents surface through one facade.Event schema by reusing F106's ContentBlock normalization — zero provider- or interface-specific branching past the edge adapters
  • 3-layer error handling with single StructuredError → ErrorCode mapping table and pure per-interface translators
  • Per-interface migration (CLI, TUI, HTTP, ACP) onto the facade, removing duplicated resolution, polling, and direct JSONStore access
  • Status/History served exclusively via facade port methods
  • Shared conformance test suite (golden) across all 4 interfaces

Out of Scope

  • Live multi-viewer fan-out of a single session to multiple concurrent consumers
  • Exposing workflows as MCP tools (possible via Run+Resolver but deferred)
  • Merging or removing the underlying application services (WorkflowService, ExecutionService, HistoryService)
  • Business logic rewrite — facade composes via delegation only
  • Honest isolation validation surfaced in ValidateWorkflow (F108 Axis A consumes the surface this feature opens)

Deferred

Item Rationale Follow-up
Live multi-viewer fan-out Adds session-level multiplexing complexity orthogonal to facade unification future
MCP tools surfacing workflows Requires MCP-side wiring distinct from interface migration scope future
Application service merger Facade delegates; collapsing services is a separate refactor future
Honest isolation validation in ValidateWorkflow Owned by F108 Axis A; F107 only opens the surface F108

User Stories

US1: Unified workflow execution entry point across all interfaces (P1 - Must Have)

As a maintainer of AWF integrating a new interface (CLI, TUI, HTTP, ACP),
I want a single WorkflowFacade port with Run/Resume returning a bidirectional RunSession,
So that I implement only the presentation edge and inherit identical resolution, execution, events, and error semantics.

Why this priority: Without a single entry point, every interface re-implements pack/workflow resolution, polling vs push divergence, status access, and error mapping — the divergence the research §1.1–§1.4 calls out as the structural debt this feature must eliminate. P1 because no downstream value (conformance, F108 Axis A) is reachable without it.

Acceptance Scenarios:

  1. Given a workflow pack/workflow exists in the repository, When any interface invokes WorkflowFacade.Run(ctx, RunRequest), Then the same canonical Resolver parses the identifier, ExecutionService runs the workflow, and a RunSession exposes Events() derived from the F106 transcript.
  2. Given a RunSession is active, When the consumer calls Close() multiple times, Then the session terminates exactly once, drains pending events, and subsequent calls are idempotent no-ops.
  3. Given CLI, TUI, HTTP, and ACP each consume Events() from the same RunRequest, When the workflow emits a step output, Then all 4 interfaces receive a facade.Event of identical kind/payload before per-interface presentation.

Independent Test: Implement a facadetest fake port producing a scripted sequence of Events; run a CLI integration test that calls Run then Drain, asserting exit code derived from the terminal event and stdout matching the scripted projection.

US2: Bidirectional input collection unifies interactive prompts (P1 - Must Have)

As a workflow author whose step requires user input mid-execution,
I want the facade to emit EventInputRequired and accept RunSession.Respond(InputResponse),
So that CLI interactive collection, ACP parking, conversational multi-turn, and missing-input recovery share one bidirectional protocol.

Why this priority: Research §1.6 identifies input collection as the most fragmented surface — ACPInputReader and TUIInputReader are interface-specific implementations of the same protocol. Without unification, the facade cannot replace them. P1 because the conformance suite must verify identical input semantics across interfaces.

Acceptance Scenarios:

  1. Given a running workflow reaches a step requiring input, When the executor calls the bridged UserInputReader, Then an Event{Kind: EventInputRequired, InputRequest} is emitted on Events() and the executor blocks until Respond is called.
  2. Given a consumer calls RunSession.Respond(InputResponse), When the response reaches the bridge, Then the blocked UserInputReader returns the response value and the workflow resumes.
  3. Given the session is closed before Respond is called, When the executor times out or context cancels, Then the executor receives an error and a terminal Event{workflow_failed} is emitted with ErrorCode mapping the cancellation cause.

Independent Test: Drive the facadetest fake to emit EventInputRequired, call Respond on a separate goroutine, and assert the fake executor observed the response value identically; repeat with a Close() before Respond and assert terminal failure event.

US3: Canonical resolver replaces 3 divergent implementations (P1 - Must Have)

As a subworkflow author or interface integrator,
I want one Resolver that parses pack/workflow, validates the manifest, and loads via PackDiscoverer.LoadWorkflow or repo.Load,
So that identifier resolution behaves identically whether reached from CLI, TUI, HTTP recomposeIdentifier, ACP :/, or SplitCallWorkflowName.

Why this priority: Three divergent resolvers (pack_resolver.go, command.go:resolvePackWorkflow, subworkflow_executor.go:SplitCallWorkflowName) produce different errors for the same identifier. P1 because every interface migration in US1 depends on a single resolution semantics.

Acceptance Scenarios:

  1. Given the identifier core/build, When any of CLI, TUI, HTTP, ACP, or subworkflow execution calls the resolver, Then the same Workflow is loaded or the same ErrorCode is returned.
  2. Given a presentation-layer wire identifier (HTTP pack.workflow, ACP pack:workflow), When the per-interface adapter transforms it to canonical pack/workflow, Then the resolver receives the canonical form and round-trips back through the adapter without loss.
  3. Given a malformed identifier missing the pack segment, When the resolver runs, Then an ErrorCode corresponding to invalid identifier is returned and mapped to interface-appropriate exit code / HTTP status / RPC code by the pure translator.

Independent Test: Table-driven test feeding the 4 wire conventions (CLI, HTTP, ACP, MCP) through their adapters into the canonical resolver and asserting round-trip equality plus identical error codes for malformed inputs. For nested call_workflow, assert the resolved subworkflow run correlates to its parent via the transcript's ParentRunID/ChildRunID (one identity model, FR-019) rather than a parallel scheme.

US4: Status, history, and resume served from the facade (P2 - Should Have)

As a CLI user running awf status or awf resume-list,
I want the commands to query WorkflowFacade.Status/History instead of JSONStore directly,
So that status access shares the same authorization, identifier resolution, and error mapping as Run.

Why this priority: Research §1.3 identifies status/history living outside the core as a coupling violation. P2 because the divergence is functional but not user-blocking — CLI works today, just inconsistently with HTTP/ACP.

Acceptance Scenarios:

  1. Given a workflow has previously executed, When awf status <workflow-id> runs, Then the CLI calls WorkflowFacade.Status(ctx, id) and renders the returned RunResult/RunStatus without touching JSONStore.
  2. Given resumable workflows exist, When awf resume-list runs, Then the CLI calls WorkflowFacade.History(ctx, HistoryFilter{Resumable: true}) and renders []RunRecord identically to HTTP /runs?resumable=true.
  3. Given awf resume <id> runs, When the facade processes the request, Then resume flows through the same Run/Resume port (via ExecutionSetup.Build()) as initial execution.

Independent Test: Replace JSONStore in the test with facadetest; assert awf status and awf resume-list produce identical output to running the equivalent HTTP endpoint against the same fake.

US5: Conformance suite proves interface parity (P2 - Should Have)

As a maintainer reviewing a PR that touches any interface,
I want a shared golden conformance suite covering CLI stdout, ACP session/update, SSE frames, and TUI tea.Msg,
So that divergence between interfaces breaks CI rather than slipping into production.

Why this priority: Without a conformance suite the facade gains can erode silently as each interface is maintained independently. P2 because it protects the investment but does not deliver the facade itself.

Acceptance Scenarios:

  1. Given a scripted facadetest event sequence, When the conformance suite projects it through all 4 interface adapters, Then the output matches golden files for CLI stdout, ACP updates, SSE frames, and TUI messages.
  2. Given a developer modifies one interface adapter to drop an event kind, When CI runs, Then the conformance suite fails with a clear diff indicating which interface diverged.

Independent Test: Run the conformance suite against the facadetest fake; deliberately mutate one adapter to skip an event kind and verify the suite fails.

Edge Cases

  • What happens when a consumer reads Events() slowly and the bounded replay buffer overflows? — The session must apply a documented backpressure or drop policy keyed by Seq so HTTP SSE replay-from-Seq remains coherent.
  • How does the system handle Close() called concurrently from multiple goroutines? — Idempotent close must use a sync.Once-style guard; subsequent calls return without error and do not panic on closed channels.
  • What is the behavior when Respond() is called after the session has terminated? — Respond returns a non-fatal error indicating the session is closed; no event is emitted, no panic.
  • What happens when the resolver receives a canonical identifier with characters reserved by a presentation layer (e.g., : for ACP)? — The CLI/canonical form accepts it; only the per-interface adapter is responsible for wire encoding/decoding; the resolver does not enforce wire constraints.
  • How does the facade handle a StructuredError not present in the mapping table? — A default ErrorCode (ErrInternal) is returned with the original error preserved for logging; the mapping table is exhaustive-checked in tests.
  • What happens when a new agent provider emits an unknown transcript.EventType or ContentBlock.BlockType? — The projection fails closed: a defined fallback EventKind is emitted with a non-fatal projection error logged; the event is never silently dropped and never panics. New provider divergence is absorbed at the F106 normalizer (FR-017), so the facade contract does not widen.
  • What happens when Respond() is called for an input the session never requested, or twice for the same request? — The bridge rejects the unexpected/duplicate response with a non-fatal error; the executor's blocking read is satisfied at most once.
  • What happens when two interfaces concurrently call Run for the same RunRequest? — Each call produces a distinct RunSession with its own ID; the facade does not deduplicate; the registry stores sessions by ID.

Requirements

Functional Requirements

  • FR-001: System MUST expose WorkflowFacade with synchronous List, Validate, Status, History and asynchronous Run, Resume returning RunSession.
  • FR-002: System MUST provide a canonical Resolver that parses pack/workflow, validates the manifest, and loads via PackDiscoverer.LoadWorkflow or repo.Load, replacing pack_resolver.go, command.go:resolvePackWorkflow, and subworkflow_executor.go:SplitCallWorkflowName.
  • FR-003: System MUST provide per-interface presentation adapters at the edge (CLI identity, HTTP recomposeIdentifier, ACP :/, MCP _/) that convert wire identifiers to canonical form before invoking the resolver.
  • FR-004: System MUST implement an Adapter composing WorkflowService, ExecutionService, HistoryService, and Resolver. The adapter MUST be the sole subscriber of the F106 Recorder exchange stream (Recorder.Subscribe() <-chan transcript.ExchangeEvent) and MUST own exactly 4 edge bridges — EventPublisher, OutputWriters, DisplayRenderer, and bidirectional UserInputReader — each with a stub-acceptable implementation for every interface so no interface is left half-wired.
  • FR-005: System MUST project facade.Event from three sources merged into one ordered stream: (1) the primary source — transcript.ExchangeEvent from Recorder.Subscribe(), reusing its ContentBlock-normalized, provider-agnostic payloads; (2) EventInputRequired, synthesized by the UserInputReader bridge (the transcript does not carry an input.required type); (3) the terminal outcome (workflow_completed/workflow_failed), derived from the synchronous StructuredError/Err() path rather than parsed from a transcript payload. The merge MUST preserve Seq monotonicity for the transcript-sourced events.
  • FR-006: System MUST implement RunSession with ID, Events, Respond, Err, and idempotent Close, backed by a bounded replay buffer keyed by ExchangeEvent.Seq — the transcript's existing sequence number MUST be reused, not regenerated, so HTTP SSE replay-from-Seq stays coherent with the on-disk transcript.
  • FR-007: System MUST emit Event{Kind: EventInputRequired} (synthesized by the UserInputReader bridge, not sourced from the transcript) and accept RunSession.Respond(InputResponse) to unify interactive input collection across CLI, ACP, conversational, and missing-input scenarios. This MUST replace and remove the interface-specific ACPInputReader and TUIInputReader.
  • FR-008: System MUST implement a single StructuredError → ErrorCode mapping table and pure per-interface translators (exitCode, httpStatus, rpcCode). Mapping MUST fail closed: an unmapped StructuredError resolves to ErrInternal (original error preserved for logging), never panics, and is caught by an exhaustive-case test (NFR-005).
  • FR-009: System MUST surface Validate results as ValidationReport, generalizing the 404-vs-422 distinction across interfaces.
  • FR-010: Users MUST be able to invoke CLI run, resume, status, resume-list through the facade with identical resolution and error semantics as HTTP and ACP.
  • FR-011: System MUST migrate TUI from 200ms polling to for e := range sess.Events().
  • FR-012: System MUST migrate HTTP to 202 + SSE replay-from-Seq + /respond + terminal event, removing the parallel eventRegistry and ad-hoc in-memory state.
  • FR-013: System MUST migrate ACP to a projector mapping facade.Event → session/update, reusing F105's toSDKUpdate.
  • FR-014: System MUST remove direct JSONStore access from CLI status and resume-list, routing through ExecutionService.ListResumable() via the facade.
  • FR-015: System MUST maintain a session registry keyed by ID for HTTP/cloud consumers and a Drain(sess) helper for CLI synchronous use.
  • FR-016: System MUST define an explicit, exhaustive transcript.EventType → facade.EventKind mapping table covering all 10 F106 event types (run.started, run.completed, step.started, step.completed, step.call_workflow.started, step.call_workflow.completed, message.user, message.assistant, tool.call, tool.result). An unknown EventType or ContentBlock.BlockType (e.g. emitted by a future provider) MUST fail closed — mapped to a defined fallback kind and surfaced as a non-fatal projection error — and MUST NOT panic or be silently dropped.
  • FR-017: facade.Event projection MUST be provider-agnostic: no branch on agent provider (Claude/Gemini/Codex/Copilot/OpenAI-compatible) is permitted past F106's ContentBlock normalization. Provider divergence MUST be absorbed at the F106 normalizer, not re-introduced in the facade.
  • FR-018: System MUST consume the masked event stream: secret masking applied by the F106 Recorder (WithMasker) MUST be preserved end-to-end, and the facade MUST NOT re-expose raw/unmasked payloads on Events(), SSE, or any interface projection.
  • FR-019: For nested call_workflow execution, the facade MUST correlate subworkflow sessions via the transcript's existing ParentRunID/ChildRunID rather than inventing a parallel correlation scheme; the canonical Resolver (FR-002) and subworkflow run correlation MUST share one identity model.

Non-Functional Requirements

  • NFR-001: Session and adapter packages MUST achieve >85% test coverage and pass make test-race with goleak deterministic verification.
  • NFR-002: Migration MUST proceed in coexistence — business services remain intact and interfaces migrate one at a time without breaking the others.
  • NFR-003: Resolver MUST round-trip all 4 wire conventions (CLI, HTTP, ACP, MCP) through wire → canonical → wire without loss.
  • NFR-004: Event projection latency from transcript.ExchangeEvent to consumer-visible facade.Event MUST not introduce additional buffering beyond the bounded replay buffer.
  • NFR-005: Error mapping MUST be exhaustive — tests MUST fail if a StructuredError variant is unmapped.
  • NFR-006: Agent uniformity — a contract test MUST assert that the same scripted execution produces an identical facade.Event sequence regardless of agent provider (Claude/Gemini/Codex/Copilot/OpenAI-compatible), proving friction between agents and the core is collapsed to F106's normalization boundary.
  • NFR-007: Defensive projection — the adapter and session MUST never panic on malformed, unknown, or out-of-order input (unknown EventType/BlockType, Respond after close, Seq gaps, slow consumers). Every such case has a defined, tested fail-closed behavior; make test-race + goleak MUST show zero leaks under these fault injections.
  • NFR-008: The WorkflowFacade/RunSession port and the EventKind mapping table MUST be covered by contract tests (recorder_contract_test-style) so any provider or interface added later is forced to satisfy the same contract rather than widening it.

Success Criteria

  • SC-001: All 4 interfaces (CLI, TUI, HTTP, ACP) consume RunSession.Events() exclusively for runtime events; the adapter is the only subscriber of the F106 Recorder stream, and zero direct event-source access (Recorder.Subscribe() or EventPublisher) remains outside the adapter.
  • SC-002: The shared conformance suite passes for CLI stdout, ACP session/update, SSE frames, and TUI tea.Msg against a single scripted facadetest sequence.
  • SC-003: Resolver code paths (pack_resolver.go, command.go:resolvePackWorkflow, subworkflow_executor.go:SplitCallWorkflowName) are removed; one Resolver remains.
  • SC-004: Session and adapter packages achieve >85% line coverage and zero goleak-detected goroutine leaks under make test-race.
  • SC-005: TUI 200ms polling loop is removed; HTTP eventRegistry parallel path is removed; CLI JSONStore direct access from status and resume-list is removed; the interface-specific ACPInputReader and TUIInputReader are removed in favor of the single EventInputRequired/Respond() protocol.
  • SC-006: A single mapping table converts StructuredError to ErrorCode; exhaustive-case test fails on any unmapped variant.
  • SC-007: A single exhaustive EventType → EventKind table covers all 10 F106 event types; the test fails if a new transcript.EventType or ContentBlock.BlockType is unmapped, and an unknown variant is proven to fail closed (fallback kind + projection error, no panic, no silent drop).
  • SC-008: The agent-uniformity contract test passes — one scripted execution yields a byte-identical facade.Event sequence across all supported providers (Claude/Gemini/Codex/Copilot/OpenAI-compatible), with no provider branch present in the adapter.

Key Entities

Entity Description Key Attributes
WorkflowFacade Application port consumed by all interfaces List, Validate, Status, History, Run, Resume
RunSession Bidirectional session for one workflow execution ID, Events(), Respond(), Err(), Close(), replay buffer indexed by Seq
RunRequest Neutral DTO carrying workflow identifier + inputs identifier (canonical pack/workflow), inputs map, options
Event Neutral runtime event projected from transcript.ExchangeEvent Seq (reused from ExchangeEvent.Seq), Kind (EventKind), payload (provider-agnostic ContentBlock), timestamp
EventKind mapping Exhaustive transcript.EventType → facade.EventKind table 10 F106 types + EventInputRequired (bridge-synthesized) + terminal; fail-closed on unknown
InputRequest / InputResponse Bidirectional input collection DTOs prompt, schema, response value
RunResult / RunStatus Terminal and intermediate execution state status, outputs, error
WorkflowSummary List/discovery DTO identifier, name, description, version
ValidationReport Validate result generalizing 404/422 findings, severity, identifier resolution status
HistoryFilter / RunRecord History query and result DTOs filter criteria (resumable, range), record fields
Resolver Canonical pack/workflow parser + loader parse, manifest validation, load via PackDiscoverer/repo
Adapter Implementation composing services + resolver sole subscriber of Recorder.Subscribe(); owns 4 bridges (EventPublisher, OutputWriters, DisplayRenderer, UserInputReader); provider-agnostic event projector
ErrorCode Enum bridging StructuredError to interface-specific codes one mapping table, pure translators per interface

Assumptions

  • F106 canonical transcript is in place and is the authoritative event source. Concretely, the adapter consumes the F106 Recorder port — Record, Subscribe() <-chan transcript.ExchangeEvent, idempotent Close — where ExchangeEvent already carries Seq, RunID, ParentRunID, ChildRunID, a 10-value EventType, and ContentBlock-normalized payloads. Secret masking is applied at record time via WithMasker. The facade reuses these (Seq, ParentRunID/ChildRunID, masking, normalization) rather than reimplementing them.
  • The transcript emits no input.required and no distinct workflow_failed type (only run.completed with a generic payload); therefore input-required and terminal-failure events are NOT transcript-sourced and are produced by the UserInputReader bridge and the synchronous StructuredError/Err() path respectively (FR-005).
  • F105 ACP migration to coder/acp-go-sdk exposes toSDKUpdate, reusable for facade.Event → session/update projection.
  • Existing application services (WorkflowService, ExecutionService, HistoryService) expose the methods the adapter delegates to; no business rewrite is needed.
  • The 4 wire conventions (CLI identity, HTTP recomposeIdentifier, ACP :/, MCP _/) are stable enough to be encoded as edge adapters without further negotiation.
  • Coexistence-style migration is acceptable — each interface migrates independently while the others continue to work against pre-facade code paths.
  • The bounded replay buffer size is configurable and the default is sufficient for typical SSE replay-from-Seq reconnect windows.

Metadata

  • Status: backlog
  • Version: v0.11.0
  • Priority: high
  • Estimation: XL

Dependencies

  • Blocked by: F106 (event source); benefits from F105 (toSDKUpdate)
  • Unblocks: F108 (Axis A surfaces honest isolation validation in ValidateWorkflow)

Clarifications

Resolved against the F106 implementation as merged (PR #370):

  • Event source: the adapter subscribes to the F106 Recorder stream (Recorder.Subscribe() <-chan transcript.ExchangeEvent), the Seq-indexed pub/sub F106 actually shipped — not the legacy EventPublisher/pluginmodel.DomainEvent bus, which is a distinct port. Earlier "sole consumer of EventPublisher" wording is superseded by FR-004/FR-005/SC-001.
  • Seq ownership: facade.Event.Seq reuses transcript.ExchangeEvent.Seq; the facade does not mint its own sequence, keeping SSE replay-from-Seq coherent with the on-disk transcript (FR-006).
  • Event stream is a 3-way merge: transcript projection + bridge-synthesized EventInputRequired + synchronous terminal outcome — because F106 emits neither input.required nor a dedicated workflow_failed type (FR-005).
  • Type name: the canonical type is transcript.ExchangeEvent (the PR summary's "Event envelope" is prose); the spec's references are correct.
  • Provider uniformity is the goal: F106 already normalizes Claude/Gemini/Codex/Copilot/OpenAI-compatible output into ContentBlock; F107 forbids re-introducing provider branching past that boundary (FR-017, NFR-006) — this is the mechanism by which agent↔core friction is reduced and uniformized.

Notes

  • Source spec: .agent/specs/2026-06-02-unified-core-facade-design.md
  • Research traceability: research-improvements.md §1 (covers §1.1 → §1.6) — §1.1 resolution → single Resolver; §1.2 entry point → single Run+Resume; §1.3 status/resume → port methods; §1.4 poll vs push → all consume Events(); §1.5 variable validation → uniform Validate/ValidationReport; §1.6 inputs → EventInputRequired+Respond().
  • Position: 5/6 in the v0.11.0 sequence (F103 → F104 → F105 → F106 → F107 → F108).
  • Test strategy: fake port facadetest (scriptable session); adapter mapping-table tests (StructuredError → ErrorCode AND EventType → EventKind, both exhaustive + fail-closed on unknown); agent-uniformity contract test (identical facade.Event sequence across Claude/Gemini/Codex/Copilot/OpenAI-compatible — NFR-006); port contract tests (recorder_contract_test-style) so future providers/interfaces satisfy rather than widen the contract; session concurrency with -race + goleak incl. fault injection (unknown event, Respond after close, Seq gaps); resolver 4-convention round-trip + subworkflow ParentRunID/ChildRunID correlation; shared golden conformance suite; per-interface integration fixtures under tests/fixtures/<interface>/.
  • Composition, not rewrite: business services intact; facade delegates.
  • Gate / DoD: >85% + test-race on sessions; conformance suite green for all 4 interfaces; business services intact; migration in coexistence, interface by interface.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureFeature specificationv0.11.0Target version

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions