Skip to content

Latest commit

 

History

History
238 lines (161 loc) · 33.9 KB

ARCHITECTURE.md

File metadata and controls

238 lines (161 loc) · 33.9 KB

Flame CLI architecture

Flame CLI consumes the public embedded and HTTP Runtime bindings. It provides one-shot Cobra commands and an interactive Oolong terminal client without creating a second product backend.

Ownership

Runtime remains authoritative for Session, Run, Segment, Item, Goal, Plan, Interrupt, execution, provider selection, persistence, compaction, and recovery.

CLI owns only command and presentation concerns:

  • process signals, streams, diagnostics, and exit status;
  • typed CLI preferences and command input;
  • terminal focus, selection, layout, navigation, overlays, and rendering;
  • drafts, attachments, prompt history, queue intent, stash, and replay intent;
  • projections used to present Runtime facts.

A Runtime event may update a preview, but completed Items and authoritative snapshots win after reconnect, gaps, and cold recovery.

Run observation retries classified transient transport failures until its caller cancels, with bounded backoff and no attempt quota. Subscription and rendering failures do not authorize a Run cancellation. Explicit user cancellation remains a separate Runtime command; permanent observation errors remain visible without manufacturing a product terminal. Mutation acknowledgement recovery queries Runtime with the original command namespace, key, and operation. Its replay retention fences command redelivery, while receipt lookup remains available after that deadline.

Ending terminal observation releases live presentation blocks while retaining the last Runtime Tool status and observed output. Only an authoritative Runtime Item updates a Tool outcome; a local observation error is displayed separately.

Ending the local start wait makes the terminal idle without creating a Run outcome. An unconfirmed start can still have been accepted; the durable outbox preserves its exact command and cancellation intent until Runtime confirms settlement.

Steer acknowledgement preserves Runtime's exact reserved User Item ID. Acceptance means the instruction is waiting to enter model context; only a completed User Item with that identity in the same Run proves application. Pending steer signals may survive an interruption and apply in a later Segment of that Run. After the Run finishes, a separate authoritative Session read settles missing Items as not applied; an unavailable read or unknown receipt remains unconfirmed. The terminal retains Item and final-read evidence when either arrives before the acknowledgement. Replayed receipts remain bound to their original Sessions. Acknowledged receipts are presentation state for the current terminal lifetime; after another restart, durable Items remain the available application evidence. These observations neither create a durable input inbox nor authorize submission to another Run.

One-shot execution reads the complete tree interruption from Conversation after the root Segment closes. Member interrupts alone do not authorize a resume; a stream lost before the root boundary must reconnect or recover the durable snapshot first.

Terminal interrupt reviews retain each pending Item's member Run identity while the resume command addresses the root. Before staging a decision, Conversation compares its reviewed interrupts with the complete authoritative waiting set. The durable workbench preserves the exact command, member identities, and answers across restart; recovery applies the same waiting-set check.

Cold recovery reads the root's authoritative status first. A running root is recovered with runs.subscribe(snapshot: true), whose material and successor tail share one Runtime observation boundary. CLI installs that material and the opaque head cursor before consuming the tail; it never replaces the material with an independent read after attachment. Waiting and finished roots install the authoritative snapshot without subscribing. Independently owned Session metadata keeps its existing stability check around the snapshot subscription, and an unstable attempt releases its tail before retrying.

Dependency direction

The CLI uses four responsibility rings:

Ring Responsibility
Domain Pure CLI-owned values and local aggregate invariants
Application CLI workflows and consumer-owned ports
Adapter Translation to Runtime and filesystem boundaries
Delivery Cobra routing, terminal interaction, and rendering

Application depends on Domain. Adapters and Delivery depend inward on the consumer contracts they satisfy. Cobra and Viper remain in command delivery; Oolong remains in terminal delivery. The concrete Runtime binding remains in adapter/runtimebinding; public Protocol values may cross consumer ports directly so CLI does not create synonymous DTOs.

The separate localruntime module owns the default local endpoint, product-root resolution and local deployment handoff paths. Settings reads its default HTTP origin. Process composition uses it to resolve FLAME_HOME before constructing the binding and authoring store; the binding adapter derives Runtime's data directory from that root. These are the only CLI consumers of that contract.

Domain contains no I/O interfaces and no context.Context. A narrow port is declared beside the Application or Delivery behavior that consumes it. Composition supplies the concrete implementation.

CLI Domain types are behavior-rich only for CLI-owned invariants. A draft, queue, replay intent, or selection owner validates its state and exposes legal transitions. Runtime snapshots, protocol values, render rows, and command inputs remain typed data; wrapping them in getters does not create a domain model.

Oolong's Editor owns completion tokens, including their line and byte range. The terminal captures those tokens for asynchronous candidates and reads the current Editor token before acceptance; moving to an identical token on another line cannot accept the earlier offer. Transcript rendering and hit testing consume Oolong's layout snapshot instead of maintaining another geometry owner.

Runtime path

The production process selects one explicit Runtime mode and fans its binding adapter into consumer-owned ports. Remote mode is the default and connects to http://127.0.0.1:17171 unless configured otherwise. Embedded mode exclusively owns the local Runtime data directory through complete shutdown. An empty or unreachable remote endpoint never selects another target. Both bindings enter the same server delivery Endpoint. The process closes the binding once; only the embedded binding owns Runtime shutdown.

CLI preferences own Runtime mode and endpoint selection, while process input owns its bearer credential. The target is immutable after composition. The remote client performs transport framing, strict decoding, and schema validation at the public Runtime boundary. CLI retains one negotiation and one immutable Profile for either binding. Invalid remote replies become permanent incompatible-Runtime errors; transport loss preserves unknown mutation acknowledgement and the existing exact replay policy.

Availability and mutation certainty are separate observations. A lost connection may be reattached or retried with the retained command identity. A malformed acknowledgement is permanent for that connection but still cannot prove that its mutation was refused: durable outboxes retain the unknown intent without retrying malformed data. Authoritative Runtime problem responses remain definitive refusals. Closing and later recovering an explicitly canceled but unacknowledged opening reuses one exact cancellation identity and payload.

The remote terminal detaches on exit without canceling an observed or newly accepted Run. An explicitly requested cancellation retains its settlement and durable recovery obligations. One-shot execution retains its separate process-interruption policy: cancel the Run that invocation started, release observations, and leave the shared Runtime alive.

runtimebinding.Connection owns binding lifecycle, capability negotiation, exact protocol translation, and safe error classification. It does not own product state. The adapter preserves exact provider/model identity and never stores credentials in CLI state, history, frames, errors, or logs.

The public Go SDK's question contract owns answer admission; Domain and terminal editing consume it without importing Runtime implementation or originating an accepted answer. The public Go SDK's exactint owns bounded counters; Frame's jsonvalue owns exact JSON values. Command adapters use the SDK's capture.Writer for bounded output and UTF-8 truncation. Filesystem adapters use filesystem.DirectoryLease for their own partition lifetime; Domain and Application do not import that I/O capability. Settings may read the independent packages/local-runtime deployment defaults; only process composition and the binding adapter use its filesystem capabilities.

The Runtime delivery endpoint is the single authority for the wire contract: both bindings validate request parameters, responses, and events with the generated protocol validators before a result reaches the CLI. The adapter therefore does not revalidate a Runtime result, and CLI projections do not restate Runtime rules such as identity syntax, closed enums, conditional field presence, numeric bounds, or lifecycle field agreement. What the adapter still checks is what Runtime cannot answer for it: a present and complete response, that the response answers the exact request it made, and that a catalog page keeps its requested filter, order, cursor continuity, and identity uniqueness. CLI-authored input is validated before it is sent, and a CLI value object validates its own construction.

The rule covers the error path too: the Runtime validates a problem before projecting it and substitutes an internal failure when it cannot, so a malformed problem is not a shape the endpoint can emit. It applies to the value, not the package — a helper in Domain or Application that takes a Runtime result and runs a generated validator on it is the same restatement as a direct call in the adapter, and is where the last of them survived. What legitimately looks similar is a CLI projection validating itself: reconstructing a wire value out of the CLI's own shape asks whether the translation was faithful, which nothing upstream can answer.

Tool presentation reads Runtime's decoded argument and result values directly. Complete JSON is encoded once for generic viewers and argument editing; it is not decoded again into a parallel material schema. Unknown tool fields remain available without having to match a built-in presentation shape.

Management, catalog, and workspace queries transfer fresh Runtime results to their consumer. The adapter does not clone them again. Synchronous calls borrow inputs; a component retaining mutable data acquires its own copy at that boundary, including immutable profiles and live event projections. Test bindings follow the same ownership contracts as Runtime.

The connection shares its immutable request metadata with synchronous binding calls. Runtime takes the snapshot retained by each operation or stream. The process owner snapshots configuration directories when it is constructed; Runtime copies them when resolving its configuration.

MCP management consumes Runtime server, tool, probe, and authorization values directly. Runtime also owns MCP error identities; the adapter applies the shared problem formatter without adding synonymous CLI errors. CLI retains form drafts and write intent and projects the server Runtime returns; the terminal formats tool schemas when building the displayed document. An editor takes ownership of its fresh server query result.

The binding adapter's immutable Profile retains the validated protocol.DiscoverResponse and client capability declaration. Command and terminal delivery declare their own read interfaces; only process composition imports the concrete binding. Runtime owns the wire constraints and feature-negotiation rule; CLI adds only its supported-surface checks and local command-replay policy. The mutation application projects the negotiated replay limits and owns the live replay clock and admission policy; Domain owns the immutable capability and guard values. Readers receive owned protocol values. Command delivery renders runtime info --json under discovery and clientCapabilities, using the Runtime field names and limit representations directly.

The terminal model catalog aggregates Runtime's per-provider discovery results. A provider discovery failure remains visible beside successfully discovered models; the CLI never invents fallback models. Cancellation, Runtime closure, and invalid protocol responses abort the aggregate read.

Commands

Each command tree is built by a factory. A command declares syntax and flags, converts input to a typed request, calls one CLI use case with cmd.Context(), and writes through Cobra streams. Runtime failures return through RunE; the process boundary chooses the exit status once.

Viper is only a configuration input. It resolves defaults, files, environment, and flags into typed CLI preferences before use-case execution. Business logic does not import Cobra or Viper.

Terminal

The terminal package owns an explicit UI state tree and cohesive feature controllers. Rendering is a projection of state and terminal dimensions; it does not repair Runtime facts or start hidden workflows.

Oolong owns terminal mode, input decoding, cell measurement, and low-level editing. Flame owns product interaction, including focus, keymaps, mouse press/release matching, overlays, composer behavior, attention signals, and stable stream presentation.

Oolong's scroll owner supplies the queue drawer's frame-local layout and commits it with the complete root frame. Flame preserves selection by the queued entry's identity when a fresh snapshot arrives and requests that entry remain visible as the drawer resizes. Queue action hit regions are projections of that layout, not a separately calculated viewport.

Oolong also owns Markdown parsing and streaming, syntax highlighting, LaTeX layout, and Mermaid's bounded browser execution. Flame supplies appearance and message lifetime: completed Markdown schedules diagram preparation, the terminal owner accepts neutral PNG results, and immutable content snapshots receive distinct image handles. A message becomes finished only after its images or diagnostics settle. Discard, session replacement, and shutdown cancel workers and release image data; the terminal transport stays open until application cleanup finishes.

/Users/tangerg/Desktop/grok-build is the visual benchmark for information hierarchy, spacing, presentation density, stable streaming, and immediate interaction feedback. Flame keeps its own vocabulary, state ownership, and Oolong primitives. Compare deterministic renders at representative terminal dimensions so visual changes have reviewable evidence instead of subjective claims.

Long-lived terminal features own their cancellation and settlement locally. The application root coordinates them but does not mirror every feature field or become a general service bag.

The application root only composes the terminal: it builds the owners below, routes input, slash commands and Runtime change events to them, and owns the view tree (transcript, header, banner, status, prompt) and attention. Each owner holds its own state and reaches others only through the collaborators it is built with:

  • sessionLifecycle owns which Session is presented and is the only writer of Session state: create, switch, fork, rename, relocate, import, roll back, refresh after Runtime changes, and install a projection with its draft. It asks the root to present a Session or transcript through sessionRoot.
  • runs owns the observed Run and is the only writer of execution state: starts and queue dispatch into admission, stream following and recovery, cancellation, interrupt resume, outbox settlement and shutdown. Others read the conversation through runs queries. It reaches the lifecycle through runSession and the root through runLink.
  • promptComposer owns the prompt being written: editor content with its attachments, draft autosave, history, stashes and external editing. promptCompletion offers commands and files for the token under the cursor. steerDesk owns steering and its receipts.
  • interruptPrompts walks a waiting Run's approvals and questions and hands the outcome to runs through interruptRun. queueEditor edits the prompt queue and asks admission through queueRun.
  • sessionBrowser is the Session center. localExtensions owns the local command extension host and the operations its commands run.
  • screens are the management surfaces, each owning one Runtime port and its state: run options (model choice, catalog, status), approval policy, workspace inspection, goals, model configuration, usage, feedback, MCP, Skills, hooks, diagnostic tools, agent documents, and installed plugins with their terminal views. A screen reaches the terminal only through screenHost; a screen whose port is absent is nil, which command availability reports.
  • taskRunner applies background results on the loop under an operation lease. readerSurface owns the reader and the query behind its document. pickerDialog pairs a searchable picker with its modal; confirmPrompt, contextEditing, liveSearch, the timeline and the shortcut guide own their modals likewise. keyRouter owns key bindings and partial sequences.

Configured product actions replace Oolong's default bindings for those actions. Input routing and help read the resulting maps; configured sequence order determines which shortcut help presents first. Oolong supplies defaults only for unconfigured editor actions.

Whether the connected Runtime offers an optional surface is the composition root's question, answered once against the negotiated Profile and expressed by leaving that consumer port unset. A binding accessor therefore returns a usable value and never reports absence with a nil pointer: assigned into a consumer's interface-typed port, a nil pointer arrives as a non-nil interface, so the consumer's own "is this wired?" check cannot fire and its first call dereferences nil. Absence is the absent field, not a present value that fails on use.

The terminal requires an explicit workbench factory and owns the returned Store until shutdown. Process composition supplies the same lazy factory to terminal startup and command-side Session deletion. It selects in-memory persistence when no state directory is configured, and otherwise opens the rooted state-file adapter. Delivery never chooses persistence or interprets a state directory. Help and completion-script generation do not invoke the factory.

Optional terminal state is asked about once, where the choice is made. A dialog, a draft writer or a pane that has not been opened is absent from the application state, and the code that reads it says so; the type itself assumes it exists rather than returning a zero answer that reads the same as "open, but empty". A presentation block that cannot render fails where it renders instead of drawing nothing.

Local authoring

Runtime workspace references and local authoring directories have separate owners. In remote mode, the Runtime validates and canonicalizes its filesystem paths, while a fixed client-local directory anchors attachment resolution, external editors, imports, and exports. Session switching never turns a remote workspace reference into a local file capability. Embedded composition may resolve the local working directory before sending it as a workspace. Recent Runtime workspace references are persisted unchanged, including foreign path syntax.

Sideloaded command protocol 2 carries both the Runtime workspace reference and the local authoring directory. The executable remains rooted in its discovered plugin directory. Manifest schema version 3 declares these semantics so an older executable is rejected before invocation; responses must also declare command protocol 2. Extension-host API version 1 is unchanged.

application/extensions owns contributed command identity, argument cardinality, availability, request snapshots, results, dependency ordering, and plugin lifetimes. Each Host creates its private command registry. A setup scope can register only slash commands and requires the declared terminal.commands capability. The Host retains command and alias ownership, preserves registration order, and releases registrations during rollback, unload, reload, and final close. Terminal cancels provider-owned executions before changing that provider's lifecycle.

adapter/sideload translates executable manifests and bounded process I/O into this command contract without importing terminal delivery. Built-in transcript and tool rendering belong directly to terminal delivery; presentation does not participate in plugin discovery or activation.

The terminal's /plugins consumes the public Runtime installation catalog through a read-only port, including exact release identity, enablement, admission and observed realization. Runtime change events refresh the open reader. It shares the command-line plugins list authority and never discovers another business plugin directory. /extensions, /reload and /unload address the local command extension host. Generic installed operations remain available through flame plugins call. Package management screens are the terminal views installed releases declare; their mutations keep the Workbench command-intent lifetime.

Remote authoring persistence is partitioned by the configured endpoint; embedded state keeps its existing directory. The terminal and command-side session deletion workflow use the same partition. Endpoint identity scopes local drafts and journals, while Runtime's advertised idempotency namespace and retention independently decide whether an exact command may replay. Neither a new process instance ID nor an endpoint spelling change authorizes migration of a pending mutation.

Start and Resume acceptance belongs to Runtime's durable command receipt. Before delivery or redelivery, the Run workflow reads runs.getAcceptance using the original namespace, key, and operation. accepted settles the local submission using the original Run, Segment, and optional input Item references; observation then restores current material through the existing coherent snapshot boundary. A later active Segment never replaces the opening Segment in the receipt. retired settles historical acceptance without subscribing to or canceling replacement material. notRecorded proves only the absence of a receipt at that read: it permits a same-key attempt only while the original replay guard still admits delivery. Expiry, a changed waiting set, a finished snapshot, and another command's receipt never authorize a new key or local settlement. Lookup errors preserve the unresolved outbox. Workbench settlement and local write retries name the acknowledged command explicitly; Run lifecycle frames advance only their projection and cannot acknowledge the FIFO head. Missing prepared content blocks redelivery, while acceptance lookup itself requires no attachment or payload reads.

Workbench owns the entire installed-command intent lifecycle: capture and persist the exact request and identity before I/O, admit retries against that retained request, decode the original owner's acknowledgement, then retire the intent. A failed write, unavailable owner, invalid acknowledgement or replay fence leaves that intent unresolved. Runtime and the installed publisher retain routing and business-outcome authority.

ExecuteInstalledCommand supplies that lifecycle to Schedule consumers. Typed Memory entries additionally admit the original decision, nullable patch, target and content before any provider preparation or dispatch. Read-only pending projections support terminal retry presentation; delivery never decodes journal input, stages or retires installed commands. Memory response identity and scope checks precede retirement. A different desired request cannot replace an unconfirmed intent, including after restart or provider replacement.

Workbench persistence contains only CLI-authored facts. The workbench Application owner owns record names, the strict current shape, and recovery semantics; its narrow persistence port carries opaque bytes while the filesystem adapter owns rooted paths, regular-file checks, and atomic replacement. Records fail closed on unknown, malformed, oversized, truncated, or trailing content.

Persistence ports and session-file publication share the SDK's pure filesystem/commit evidence value. Application consumes that contract without importing the filesystem operation package; only adapters establish visibility and durability from actual I/O.

Persistence reports uncommitted, visible, or durable writes with the original I/O error. Workbench publishes each record into memory at the visible commit boundary, including on a later directory-sync failure. A failure cannot roll back a committed record or mint a replacement identity. Unconfirmed durability fences subsequent writes and every external mutation that depends on retained intent, including retries that skip staging. Read-only Runtime receipt reconciliation remains available. Reopening first confirms the actual files, directories and ancestor links, then loads and recovers the visible records before admitting delivery. Stash compensation applies only before a step commits; a committed failure leaves the journal for forward recovery. Session tombstones hide retired state immediately, and cleanup failures remain explicit until recovery succeeds.

The authoring Domain owns prompt values, deterministic FIFO and reservation transitions, and exact replay identities and guards. Workbench owns the live queue's transaction with the durable outbox: enqueue transfers the draft before dispatch, edits restore the prior FIFO only when their write did not commit, and acknowledgement commits history and outbox retirement before releasing the opening reservation. A partial history commit is retried under the same command identity. A definitive refusal publishes its replacement identity before returning the command to the FIFO; an unconfirmed publication still fences delivery. Terminal controllers issue these Workbench commands and render detached queue snapshots; they do not persist queue ordering, restore transactional state, or settle the outbox themselves.

Drafts and queued prompts keep attachment paths editable. Before the first mutation dispatch, the binding adapter materializes those paths into Runtime content under the existing per-file size and encoding limits. Start binds that content when it leaves the queue; Steer and a Resume carrying additional input bind it before staging. Every later attempt uses the same prepared content, command identity, and replay guard without reopening the original paths. Plain text already lives completely in the immutable command and needs no external materialization. The terminal uses its existing operation owner to prepare and save content in the background. Only a current UI callback can publish the small session journal before starting delivery; cancellation or editing the source draft discards the prepared result. Input encoding and blob persistence run outside the authoring mutex. The filesystem Store serializes rooted I/O with Close so its directory lease covers active writes. A live filesystem Store stays bound to its original physical state directory: replacing that directory requires reopening the workbench as a new owner, with the original directory retained for reconciliation. Temporarily blocking the path and then restoring the same directory allows the original owner to continue.

Workbench stores prepared content in separate SHA-256-addressed files so the supported 20 MiB attachment size does not exceed the 16 MiB authoring-record budget after base64 encoding. It commits content first, then atomically publishes its digest with the command identity and replay guard in the session record. An uncommitted publication leaves the command editable and may leave an unreferenced content file; a visible publication transfers ownership even when later confirmation fails. Content-addressed files are deduplicated and retained after cancellation or retirement: opening another Store cannot prove that an unreferenced file is not awaiting publication by a live owner, so opening never collects them. Referenced files are checked against their digest before replay. Missing or damaged content and older dispatched records containing attachment paths alone remain recoverable local records with an unknown mutation outcome. They are never filled from current files, automatically reidentified, or treated as a definitive refusal. The original session must be inspected before an explicit user decision about an unavailable command. Runtime command conflicts and local pre-dispatch failures likewise cannot prove an earlier attempt failed.

JSON

The CLI speaks one JSON vocabulary, encoding/json/v2. Duplicate members, trailing documents, and unknown members are refused by the decoder itself rather than by a hand-written validating pass, and omitzero marks the fields whose absence is a fact, so a present-but-empty value survives the round trip. Bytes that are hashed, compared against a second projection, persisted, or piped to another program are encoded deterministically; only the durable workbench records also keep a nil collection as null, because a reloaded record has to distinguish an unanswered interrupt from an empty answer. The two exceptions that still decode with encoding/json decode a JSON number into an any: only that package can preserve an identifier outside float64's exact range, which is what keeps a reviewed tool argument the same value when it executes.

Package shape

A package must own a coherent CLI vocabulary, local aggregate, workflow lifecycle, external translation, or terminal mechanism. Related behavior stays in responsibility-named files inside one package. Context namespace directories exist only for several peer packages and contain no facade Go files. A package does not earn a boundary merely because its type has an interface or its workflow has one action.

Owner Package Responsibility
Local authoring domain/authoring Message and attachment values, authored options, Start and Steer intents, FIFO editing and dispatch reservations, exact replay identity and guards
Conversation domain/conversation Transcript folding, stream deduplication, interrupt review and Runtime projections
Workbench application/workbench Local authoring lifetime, durable records, draft transfers, queue transactions and outbox settlement
Mutation admission application/mutation Identity generation, replay clocks and exact acknowledgement policy shared by workflows
Agent workflows application/agent/run, application/agent/session Run observation and command workflows over consumer-owned Runtime ports
Extensions application/extensions Plugin activation, command contracts, contribution ownership and cleanup
Runtime binding adapter/runtimebinding Embedded/HTTP binding lifecycle, negotiation and projection translation
Local storage adapter/filesystem Scoped filesystem adapters for attachments, editors, session artifacts and opaque state records
Sideload execution adapter/sideload Manifest translation, confined executable invocation and command-protocol validation
User delivery delivery/cmd, delivery/terminal Cobra commands with private output renderers, Oolong interaction, rendering and view lifetimes
Process composition CLI root package Immutable Runtime target, lazy Workbench factory, port wiring and shutdown

agent, integration and filesystem are sparse namespace directories. They contain no facade package. The Workbench Store retains clocks, randomness and persistence in Application; moving its pure queue transitions to Domain does not move filesystem or process lifetime there. Conversation is a CLI projection owner and does not become a second Runtime Session or Run state machine.

Do not create packages for individual actions, interfaces, or DTOs. Merge a single-consumer forwarding package into the consumer unless the package owns an external translation or reusable technical mechanism. Do not create broad service, manager, backend, common, or helpers packages.

Verification

Use fresh-root in-memory tests for command syntax and streams, Application tests for workflow ordering, deterministic terminal state/render tests for interaction behavior, and a real PTY only for terminal mode, input decoding, resize, focus, or restoration. Architecture checks enforce dependency direction and framework isolation without freezing private fields, filenames, or package inventories.