226 KiB
Agent GUI Node Architecture
Status: current implemented architecture
This document defines the durable architecture of the Agent GUI system: ownership, dependency direction, core entities, critical flows, and change-routing rules. It is not an implementation plan, feature inventory, or troubleshooting log.
Scope:
packages/agent/host: provider-neutral Agent application corepackages/agent/store-sqliteandstore-sqlite/canonical: canonical contracts and transactional local storagepackages/agent/daemon: provider runtimes, adapters, and registryservices/tuttid: HTTP, queries, product policy, and Host adapterspackages/agent/activity-core: frontend workspace enginepackages/agent/gui: Agent GUI, Message Center, and conversation presentationapps/desktop: Electron, Workbench, transport, and concrete host capabilitiesapps/mobile: React Native presentation, DeviceLink transport adapter, and mobile lifecycle integration
Implementation progress belongs in Git history or an active spec. Debugging procedures belong in Agent Runtime Troubleshooting.
1. Architectural taste
1.1 One fact, one owner
- durable lifecycle:
packages/agent/host - canonical vocabulary:
packages/agent/store-sqlite/canonical - canonical frontend state: workspace
AgentSessionEngine - DOM, focus, scroll, menus, and temporary disclosure: UI only
Do not solve cross-layer coordination by copying state. Consumers read projections/selectors and write semantic commands.
Device-global quick prompts are not Session or Turn state. AgentGUI consumes
the optional AgentHostApi.quickPrompts capability, preserves the host's
canonical list order, and delegates create/update/delete/move effects to the
host adapter. Reordering is presentation plus intent only: AgentGUI emits a
moved prompt, nullable beforePromptId anchor, and moved version; Desktop may
show an optimistic projection, while tuttid remains the durable order owner.
Hosts that omit the capability must hide the entire entry rather than expose a
partial or disabled library.
Desktop AgentGUI reports quick-prompt engagement through the typed panel
engagement sink. Opening the picker from the Composer emits
agent.quick_prompt_engagement with action=panel_opened and
source=composer_input; successfully inserting a saved prompt or a saved
recommended template into the Composer emits the same event with
action=prompt_used and the corresponding prompt_type. Engagement is
buffered until the containing panel satisfies its exposure gate. The
action-discriminated contract forbids prompt_type on panel-open events.
Events carry only stable AgentGUI context and never prompt titles or prompt
bodies, while the action keeps picker exposure distinct from successful use.
The native Mobile composer consumes the same device-global list through its
authenticated Desktop connection rather than creating Mobile-owned prompt
state. Its authenticated-device service reads the canonical quick-prompt list
through the generated tuttid client. The Mobile + menu
offers a searchable, read-only selector; choosing a prompt adds its text at the
current plain-text input position without replacing the existing draft,
restores input focus, and never sends automatically. Create, edit, delete, and
reorder remain Desktop management actions. DeviceLink permits only exact
GET /v1/agent-quick-prompts reads for this flow; mutation and per-prompt
routes remain blocked.
The native Mobile new-conversation Composer presents Agent Target and working
directory as peer context selectors directly above the input. The Agent Target
catalog remains the launch authority. The working-directory selector reads the
daemon-owned registered project catalog through the exact read-only
GET /v1/user-projects DeviceLink route and also offers an explicit no-project
choice; Mobile does not browse the Desktop filesystem, accept arbitrary paths,
or mutate the registered project catalog. The authenticated-device scope keeps
the last loaded project metadata across workspace activity replacement, while
the selected path remains process-only draft state and disappears after
activation. Changing either selector reloads Composer options for the exact
Target and optional cwd; creation waits while those exact options are loading
and submits only sparse user-selected settings rather than cached effective
defaults. An outcome-unknown activation locks both context selectors so an
idempotent retry cannot change its launch identity. Activation with a
registered project carries both its path as cwd and its versioned canonical
project railPlacement; no-project activation omits cwd so the daemon
allocates an isolated Session directory and carries the canonical
conversations placement. Existing Sessions never expose these selectors as
mutable settings.
Use the closed-surface test when assigning ownership: if state must survive or continue progressing after every Agent GUI surface closes, it belongs to Host/store or the workspace engine. State that should disappear with the surface belongs to UI.
1.2 Semantics before screens
Session, Turn, Interaction, Goal, and operation are domain facts. Rail, timeline, dock, toast, and Message Center are projections of those facts; they do not define lifecycle.
Transcript is historical presentation. It is not authoritative for approvals, questions, Turn state, or submit availability.
1.3 Ports and adapters
Core layers declare narrow contracts and ports. HTTP, Electron, filesystem, provider wire, authorization, VM, and process details stay in adapters.
A reusable boundary needs a real responsibility and consumer. Do not create vague common, utils, or shared core modules merely to look reusable.
1.4 Provider-neutral does not mean provider-blind
A provider adapter may understand its own wire protocol. Shared business code reads descriptors, strategies, capabilities, and canonical payloads.
AgentGUI, Message Center, composer, and shared services must not choose behavior by names such as Codex, Claude Code, Cursor, or OpenCode.
1.5 Events are hints; canonical reads reconcile
Realtime events reduce latency but are not automatically complete truth:
- normalized provider text/reasoning streams and explicitly appendable textual
tool output arrive as optimistic
message_deltapayloads on the/v1/events/wsbusiness-event WebSocket - canonical tool
output/errorbodies bound eachtext,stdout, andstderrfield to 1 MiB, then fit formal output streams into a 768 KiB aggregate durable payload budget, including nested tool steps, by retaining a valid UTF-8 prefix and the fixed[Output truncated]marker - terminal command snapshots may omit
textonly when it exactly equals the trimmedstdoutorstderr; Reporter resolves the alias before independent field truncation and clears an earlier runningtext, while the raw stream remains canonical and explicit non-command tools retain provider-neutraltext - continuous, version-complete
message_updateevents may merge inline - terminal
message_updateis the durable confirmation; message version gaps, invalid/unanchored deltas, nonterminal deltas after known terminal message truth, reconnects, Turn, Interaction, and state changes trigger authoritative reconciliation - a focused conversation holds a synchronization lease. A terminal initial reconcile failure is observable through that lease and receives one bounded retry; releasing the lease prevents further focus-owned recovery work
- event publication or observer failure cannot roll back a committed canonical transaction
- message projections preserve the original textual content, including leading and trailing whitespace; trimmed or whitespace-collapsed copies may decide emptiness, synthetic-control suppression, or duplicate identity, but must not replace the body passed to renderers. Markdown renderers continue to own soft-break presentation.
1.6 Identity and correlation are explicit
Cross-boundary work uses stable identifiers:
- workspace:
workspaceId - session:
agentSessionId - Turn:
turnId - Interaction:
requestId - submit:
clientSubmitId - UI Agent:
agentTargetId
Never infer identity from titles, timestamps, array positions, provider names, the latest transcript row, or runtime instance IDs.
1.7 Fail closed
When authoritative identity, Turn, or Interaction is missing, return unsupported/loading/error. Capability presence is explicit: a null session capability snapshot means no authoritative session snapshot is available (not yet reported or legacy-ambiguous), while a non-null snapshot is complete and may explicitly contain no supported capabilities. Presentation may fall back from a null session snapshot to the authoritative provider composer descriptor, but an explicit session false always wins. Do not choose the first provider, manufacture a Turn, treat an ambiguous legacy empty array as loaded, or hide contract drift behind an ad hoc UI fallback.
Compatibility paths require evidence of existing data or a release window. Keep them isolated from canonical writes.
1.8 Contract first
Change OpenAPI before HTTP contracts, then generate Go and TypeScript types. Internal domain types cross layers through explicit projections; do not maintain handwritten transport mirrors.
Identity, time, and state use canonical representations. Unknown enum values produce an explicit unsupported/error path; widening them to arbitrary strings is not compatibility.
2. System shape
2.1 Command path
AgentGUI / Message Center / host surface
-> semantic AgentSessionEngine operation or typed intent
-> workspace AgentSessionEngine
-> shared typed lifecycle effect projection
-> Desktop or Mobile AgentSessionEffectPort
(host-only commands use the narrow EngineExtensionCommand adapter)
-> tuttid HTTP and product adapter
-> packages/agent/host
-> canonical store transaction + provider runtime port
2.2 Observation path
provider runtime observation
-> packages/agent/host + store-sqlite canonical transaction
-> CommittedDelta / CommitObserver
-> tuttid ActivityProjection and event publication
-> Desktop business-event bridge or Mobile DeviceLink live lane
-> workspace AgentSessionEngine reducer
-> memoized AgentActivitySnapshot
-> selectors / pure projections
-> AgentGUI / Message Center / host chrome
2.3 Session capability snapshots
Provider adapters report canonical session capabilities through a typed,
complete snapshot. They do not place capability ids in provider-private
RuntimeContext. A missing snapshot means no authoritative session snapshot
is available because it has not been reported yet or legacy data is ambiguous;
an empty reported snapshot means no canonical session capabilities are
supported. Partial state reports omit the snapshot and preserve the last
reported value.
Persistence uses a private JSON compatibility carrier for capability ids plus
explicit report presence, then exposes only the typed optional snapshot at the
store boundary. Legacy non-empty lists are reported snapshots; legacy empty
lists without presence evidence remain unknown. The daemon API projects
unknown as null and reported snapshots as a closed boolean record.
This does not require a database schema migration: the presence marker lives
inside the existing metadata JSON column, and decoding normalizes old rows.
activity-core owns the presentation resolution rule: a reported session
value overrides composer options, while a null session snapshot falls back to
the authoritative provider composer descriptor. AgentGUI consumes that result
and does not add provider-specific or control-specific fallback logic. Host and
runtime operation handlers remain authoritative when an action is submitted.
Runtime context transport also distinguishes a complete snapshot from an
explicit top-level set/unset patch. Omission preserves existing context;
patches may update only provider-private keys and must never replace unrelated
session metadata.
New-session launch settings
Remembered composer defaults are target-scoped preferences, not active Session
state. AgentGUI reads them only while composing a new Session and sends the
resolved sparse settings through the normal activation command. A host-owned
entry capability may additionally hide or disable an experimental control; the
activation boundary must fail closed as well, so a remembered true value
cannot outlive a disabled host entry. Provider support comes from the resolved
composer descriptor rather than provider-name checks in shared UI code.
Provider and extension model catalogs can change independently of target-scoped
remembered defaults. On Create, the daemon treats such a default as a fallback
preference and resolves an obsolete value to the current catalog default from
the same target. Because AgentGUI also sends its presented effective value, the
activation request carries explicit provenance bits for model and reasoning;
Engine and the tuttid adapter must preserve them through the HTTP boundary.
Legacy direct Create callers that omit provenance retain the compatibility rule
that a supplied non-empty value is explicit. Non-explicit model-dependent settings, such as reasoning
effort, resolve against that effective model rather than remaining bound to the
obsolete preference. A strict per-model catalog never forwards an inherited
value that the target model did not advertise. A model or dependent setting
explicitly supplied by the caller remains strict. If the runtime rejects an
explicit model selection, startup fails rather than continuing with an
undisclosed provider default.
Create resolves and freezes the effective launch cwd before the first cwd-sensitive model-catalog lookup. A no-project session therefore queries the catalog from its allocated session directory, which is also passed to runtime; it must not inherit the tuttid process cwd. Worktree creation intentionally queries from the selected source checkout before creating the isolated checkout. The no-project directory remains a provisional allocation until Host creates the Session (or delivery becomes uncertain); any earlier catalog, plan, reasoning, preparation, or runtime failure releases it so failed requests cannot consume the allocator's daily name space.
Composer model catalogs are daemon-owned snapshots. A provider auth/config change invalidates the snapshot and closes its provider app-server session, but the daemon may return the last known list as stale while refreshing it in the background. AgentGUI may render that stale projection with its loading state; model validation must wait for an authoritative snapshot. The daemon shares concurrent catalog loads, reuses a warm app-server session, and publishes the existing catalog invalidation hint after a successful refresh so open composers can converge without owning provider or cache policy.
Connector capability catalogs follow the same projection boundary. Each host
reads its authoritative local Connector Market snapshot, then uses
packages/agent/daemon/composercatalog to map manifest identity, icon,
installation, compatibility, and authorization state into the closed Composer
capability contract. Host transports only serialize that projection and route
semantic open intents; they must not duplicate Connector readiness rules or
derive Composer entries from renderer-local market state. The snapshot uses the
host's current account scope so Composer status and prompt admission include the
same authorization overlay as Connector Market management. Connector Market
change events invalidate retained Composer options, while a menu open may still
force an authoritative reread as a recovery path.
Hosts may project a Connector catalog as inspectable but read-only. In that mode AgentGUI keeps the menu and host-provided readiness descriptions visible, but suppresses selection, setup or authorization opens, selected chips, and the optional management footer. Read-only presentation never becomes execution authority; the Host remains responsible for Connector admission at submit or runtime preparation.
Settings that affect provider preparation are immutable after launch. The daemon validates them against current product policy and resolved provider capability before runtime preparation; an active Session cannot reinterpret them through an in-place settings update.
RTK saver mode follows this launch-only path for every resolved Agent provider.
The remembered composer value is only an opt-in; provider-neutral runtime
preparation resolves the pinned Tutti-bundled or managed-runtime rtk
executable, copies it and the canonical RTK.md into the exact Session runtime,
places an absolute reference to that file first in session-private AGENTS.md
providers, retains the inline RTK rule as a compatibility fallback through the
provider's existing instruction channel, installs provider-native rewrite hooks
or plugins where available, and prepends only the private binary directory to
the Session environment. Tutti never runs an RTK package manager or global
installer. RTK usage data, tee output, and telemetry policy are also
Session-scoped, so enabling the mode cannot change another Agent's instructions,
executable search path, or tracking state. The independent
rtkSaverMode property carries this provider-neutral setting, while the
existing codexSaverMode property remains Codex-only and continues to control
the Luna subagent workflow. Tutti terminals receive the bundled
RTK directory in their child PATH for explicit use, while the user-global PATH
and external Agent processes remain unchanged.
2.4 Ownership map
| Layer | Owns | Must not own |
|---|---|---|
store-sqlite/canonical |
canonical phase, outcome, origin, Interaction, capability vocabulary, and pure projections | HTTP, provider processes, React |
store-sqlite |
canonical transactions, SQLite repositories, durable tombstones/outbox participation | product UI, transport policy |
packages/agent/host |
create/resume/send/cancel/fork, lineage, Interaction, Goal, operation, and recovery lifecycle | HTTP DTOs, Electron, concrete provider wire |
packages/agent/daemon |
provider registry, runtime mechanics, wire normalization | AgentGUI policy, cross-provider UI branches |
services/tuttid/service/agent |
Host adapters, HTTP/query/composer/product policy, provider preparation | reimplementation of Host lifecycle |
tuttid ActivityProjection |
canonical read projection, commit observation, event publication/repair | lifecycle decisions, React state |
agent-activity-core |
workspace engine, canonical frontend entities, pending intents, queue, selectors | HTTP, Electron, React |
agent-gui |
runtime contract, projections, controllers, views, UI-local state | daemon truth, a second session store |
apps/desktop |
tuttid client, business-event WebSocket, preload, Workbench, windows, file/OS capabilities | a second Agent business core |
apps/mobile |
DeviceLink adapter, Native renderer, app lifecycle, navigation and drafts | a second Agent business core or Session DTO cache |
services/tuttid/api/openapi/tuttid.v1.yaml is authoritative for HTTP request/response contracts. It projects the canonical domain; it does not replace store-sqlite/canonical.
2.5 Ephemeral observation gaps
A Host may expose an ephemeral observation gap for one exact Session and Turn when its caller-side projection may be stale. The gap is presentation-only: AgentGUI pauses the processing animation, keeps recovery chrome visible, and blocks commands that require current state until the Host removes the gap. A Host may additionally classify the gap as peer-offline or synchronizing; while that classification is present, AgentGUI replaces the numeric live duration with the matching status copy. An older Host that omits the classification retains the frozen-duration fallback.
An observation gap does not settle, interrupt, or otherwise rewrite the canonical Turn. The Host owns reconnect and catch-up fencing and must remove the gap only after the same Turn is authoritative again. When the capability is absent, AgentGUI preserves its existing lifecycle presentation.
2.5.1 Live Side transient lane
Side is a provider-neutral, surface-owned live conversation fork. It is intentionally separate from the durable command and observation paths:
AgentGUI Side pane
-> AgentSideConversationRuntime port
-> Desktop generated tuttid client adapter
-> tuttid Side HTTP handlers
-> packages/agent/host Side lifecycle
-> packages/agent/daemon provider Side adapter
provider Side runtime observation
-> SideStreamObserver
-> tuttid agent.side.updated publisher
-> Desktop business-event adapter
-> AgentGUI Side event normalizer
-> activity-core ephemeral conversation projector
-> shared AgentConversationVM projection
-> Side timeline + composer instances
The runtime SideStreamObserver seam includes both event observation and
explicit ephemeral-identity cleanup. Host calls the cleanup operation when a
Side closes, is removed, or expires, so bridge-local ordering state cannot
outlive the Side identity.
The provider adapter owns provider-specific context snapshot mechanics. Host
owns Side identity, idempotent open, exact-Turn commands, scope isolation, and
cleanup. tuttid projects those commands through generated OpenAPI contracts and
publishes only the transient agent.side.updated envelope. Desktop is a thin
transport adapter. AgentGUI owns capability gating, the transient event
projection, and presentation; it contains no provider-name branches.
Side has no persistent button in the main composer. Desktop exposes Side and
the transcript text-selection action toolbar only after the user enables the
default-off lab.agentSideConversation developer preference. The host passes
that preference as an explicit AgentGUI capability and omits the Side runtime
while it is disabled; AgentGUI therefore does not resolve provider support,
show the toolbar or slash entry, or intercept typed /side input. After the
host opt-in, AgentGUI injects the provider-neutral /side entry into the slash
command palette with a local submitImmediate selection effect only when the
exact source Session reports every required Side capability. Selecting the
entry immediately submits the local /side intent, ensures the source runtime
is available, and opens Side; typed /side <prompt> opens it and sends the
prompt in one action.
After capability discovery has enabled /side, submission creates the local
opening projection immediately and renders the Side shell plus its empty
temporary-conversation explanation before the provider fork finishes. It does
not perform another renderer capability request on the open path; Host and
Runtime remain the authoritative live-source validators.
Capability resolution is fail-closed: pending, unavailable, and unsupported
providers do not show /side, including when a provider advertises a command
with that name. A typed /side invocation remains a Side-only intent in those
states: AgentGUI reports the unavailable operation and never forwards its text
through the main conversation submit path.
A released canonical provider connection may be resumed from
the source Session before capability resolution; the runtime remains the final
authority for whether the operation can open.
Desktop supplies a Side runtime factory rather than a workspace singleton. Each mounted embedded or detached AgentGUI surface creates and disposes its own runtime instance, so transient identity and cleanup ownership cannot race between windows.
The Side controller is not an AgentSessionEngine and must never dispatch Side
events into the durable workspace engine. AgentGUI normalizes the transient
envelope into the React-free activity-core ephemeral conversation projector,
which emits the same AgentActivitySnapshot/Turn vocabulary consumed by the
shared conversation projection. This reuse is projection-only: it does not
load, retain, reconcile, or persist a workspace Session.
The controller retains only the selected surface's active Side identity, ephemeral projection, pending Interaction, sequence, and error. Embedded AgentGUI keeps Side attached to its source identity when the selected source changes and shows it again when that source is reselected. A host that injects the optional external Side presentation bridge opts into fail-closed source switching: changing the selected canonical Session closes the old Side. The owning AgentGUI surface otherwise closes it only through the explicit Side close action or when the surface-owned runtime is destroyed. Its external-store subscription owns the transient event and connection subscriptions. Once the last surface subscriber is gone, those subscriptions are released. Disconnect, source-identity mismatch, or a sequence gap expires local state and prevents further rendering; no canonical reconciliation fallback exists for this ephemeral lane.
The shared Side surface creates a second instance of the conversation timeline
and composer. Embedded AgentGUI wraps it as a docked sibling of the main detail
surface, not an overlay or nested durable Session route. Standalone AgentGUI
publishes the exact surface projection through an optional presentation-only
external store; Desktop maps that projection to a contextual side tab in the
shared right tool sidebar and suppresses the inline wrapper. The bridge carries
only exact source/Side identity, surface props, and the close intent. It owns no
Side lifecycle or durable state. Each surface instance owns its own draft,
scroll-follow state, focus/shortcut scope, Turn gate, and interrupt action. The
Side composer inherits the provider/model/cwd boundary captured at open time
and presents those settings read-only. A normal main-composer submission always
stays on the main Session; only the explicit /side command opens or targets
the Side lane.
The transcript selection toolbar stages selected text as a removable quote in the target composer. Add to conversation updates only the main draft. Ask in Side opens an empty Side when needed and updates only its draft; it never creates a Turn until the user explicitly submits that composer. Quote blocks are UI-local draft state and materialize as Markdown blockquote text at the existing submit boundary, so the Side transport remains text-only. When a typed question accompanies those quotes, Side keeps the blockquotes in provider content but projects only the typed question as the visible user message; a legacy quote-only submission keeps the existing blockquote display fallback; only a future explicit selected-text content kind can make that ambiguous shape safe to render as a chip. Focus ownership follows the currently visible Side pane: an unmounted or source-mismatched Side cannot keep the main composer inactive, and pane cleanup must release its focus scope.
Side capability is enabled only when the exact selected source runtime, after
any required canonical-session resume, reports all mandatory facts: native
support, active-source-Turn snapshot support, ephemeral lifetime, hidden
inherited history, and an injected model boundary. The Side may be opened after
the source has started and may continue after the source Turn settles; the
source Turn only determines whether the provider has live context to snapshot,
not the lifetime of an already-open Side. Attachments
are rejected at the AgentGUI controller and Desktop adapter until the
transport contract can preserve them without degradation. Approval and
question state remains Side-local and is answered through the exact
(workspaceId, sideAgentSessionId, turnId, requestId) command.
2.5.2 Interaction admission and execution presentation
An exact pending Interaction is a separate admission scope. When its Host supplies interaction readiness, that exact result owns transport presentation and early write admission for the pending card. An observation gap may still govern the active Turn before or after that Interaction, but it cannot override the exact ready or blocked readiness result while the card is presented.
When an exact Interaction is blocked, AgentGUI keeps the pending action visible but disables its controls. The disabled action group remains focusable and hoverable so the same readiness reason is discoverable in the conversation chrome and Message Center; this is presentation only and does not change the Host-owned admission decision.
Message Center and attention-card consumers preserve this separation in their
presentation contract: isSubmitting describes an in-flight response, while
an independently supplied interaction-disabled state blocks the nested prompt
controls without hiding the pending card or its conversation navigation. A
host-provided blocked reason is exposed through a focusable, hoverable
description so a blocked card remains understandable without making Tooltip
the command authority. If the Host omits the reason, the disabled wrapper stays
out of the tab order and does not create an empty Tooltip. The final semantic
command admission check remains in the consumer/controller path.
Submission and execution presentation
Submission acknowledgment and execution are separate presentation signals:
isSubmitting, an unconfirmed submit, and a viable initial launch that expects a Turn drive anisAwaitingTurnStartComposer spinner immediately because they prove local submission intent or expected launch work- a reconnect-only
activatingstate belongs to connection chrome and command admission; without submission intent it must not create a sending spinner,workingstatus, Stop target, or queue eligibility - Conversation
workingstatus and queue eligibility require canonical execution evidence: the fenced Engine display status or a non-settled active Turn; pending activation or submission alone must remainready, both before and after canonical Session confirmation - canonical completed, failed, or canceled activity stops the execution indicator even if a lower-level runtime signal has not caught up yet
- transport recovery chrome, such as connecting or unavailable, takes priority over normal working copy
- Stop is available before Turn identity only when the Engine exposes an exact pending-submit stop target; reconnect and other connection state never invent one
This keeps feedback optimistic for responsiveness while keeping claims about Agent work grounded in the canonical Turn/runtime projection.
The Composer also hands the exact draft used to build a submission to the AgentGUI controller. The controller snapshots that handoff for conditional post-submit clearing; it must clear only when the current draft still matches the submitted snapshot, so a queue admission or accepted send cannot erase a newer edit made while the request is in flight. For image blocks, provider locators and preview/upload-progress metadata may settle asynchronously and do not count as user edits; upload errors and unknown fields remain part of the comparison.
2.6 On-demand status
AgentGUI owns one provider-neutral AgentStatusController for /status, Agent
Info, and Agent Config. These surfaces are explicit bounded reads; mounting an
AgentGUI node must not start background status polling.
The host injects an AgentStatusSource. AgentGUI treats scopeKey as opaque
and never resolves provider, account, local-vs-remote transport, or owner
identity. The controller owns only loading/ready/error presentation,
30-second request cancellation, a one-hour retained UI snapshot, 5-second
manual-refresh debounce, and fencing callbacks from closed or replaced
requests. Opening any status surface uses the same controller snapshot so the
three views cannot drift into separate state machines.
Every production host, including Tutti Desktop, injects this controller. A host adapter resolves the exact Agent Target to its provider, verifies an optional Session belongs to the same workspace/target/provider, and performs the bounded status read. AgentGUI has no legacy probe-backed status state or provider-derived fallback. The active conversation id is the request Session identity because it exists before detail hydration; raw Session chrome remains presentation data.
Tutti Desktop creates one status source per workspace renderer and injects it
into every AgentGUI surface in that workspace. The source shares the one-hour
Agent Target snapshot, five-second refresh debounce, and in-flight status probe
by exact agentTargetId, while each surface keeps its own controller and therefore its own
query, loading, close, and stale-response state. Sharing the controller itself
is invalid because one surface could replace or close another surface's active
request. Standalone renderer processes have their own source; the Electron main
process remains the cross-window short-lived exact-target cache.
Account-usage probing for an Agent Extension is an optional, versioned,
target-scoped capability declared by the signed Extension. The provider-owned
Helper owns provider config, credentials, trusted origins, private endpoints,
and response parsing; tuttid runs only the exact companion script installed
and fingerprinted in that Target's independent companion runtime through a
separately verified Node interpreter. Companion availability never changes ACP
readiness. A daemon-owned reconciler installs the companion outside setup
actions, wakes after runtime or Extension activation, retries failures with
bounded backoff, and rechecks persisted activation on restart. Account-usage
requests are joined and cached for a short TTL by exact agentTargetId; the TTL
starts when execution completes, and Node identity derivation is reused while
the executable file identity is unchanged. The daemon accepts existing v1
helper output and returns the normalized provider-neutral
tutti.agent.account-usage.v2 discriminated result. Desktop validates the
schema and echoed Target/provider identity, then projects billing identity,
quota completeness, and optional exact Provider-neutral amounts without
changing ACP readiness. A complete quota set is distinct from a known account
whose quota is unavailable and from API billing where quota is not applicable;
only the complete state may carry rows. Presentation maps API not_applicable
to a resolved empty limits row (—), while unavailable uses localized
account-quota-unavailable copy; neither becomes a refresh failure. Missing
profiles and older Extensions are unsupported; unknown schemas, enums,
shapes, partial exact amounts, or a claimed complete result without quotas fail
closed as parse_failed. No provider message, path, endpoint, response body,
credential, account ID, or raw account record crosses the port or enters logs.
A source emits at most one cached snapshot followed by at most one
refreshed value, then completes. Backend probing may continue independently
to fill a host-owned cache after the presentation request is canceled; late
frames from the canceled request must not mutate AgentGUI. Errors crossing the
port are structured codes, never provider stderr, account material, endpoints,
or transport diagnostics. The limits projection preserves stable codes such as
auth_required, session_expired, and subscription_required through
AgentStatusValue.limitsErrorCode; AgentGUI owns their localized presentation
and maps unknown codes to one generic failure label.
A provider quota may carry an optional exact provider-neutral amount and unit when a percentage alone would hide useful account information. The host owns the provider request, credential handling, package aggregation, and percentage normalization; AgentGUI only renders the projected amount and uses the optional percentage for progress presentation. Raw account and package records never cross this boundary.
An explicit unsupported usage probe is a successful bounded read with no
quotas and limitsState: unavailable; it must not become a refresh failure.
Only real authentication, transport, parsing, timeout, or execution failures
project limitsState: error.
A Custom Agent bound to a Model Plan does not use the Agent Runtime provider's
native account for model access, so provider-native account usage is not
applicable to that exact target. Desktop projects
providerAccountUsageApplicable: false from the Workspace Agent binding;
AgentGUI preserves the optional capability through its target projection. The
Desktop status source then completes the bounded read with unavailable limits
without calling the provider usage probe, and AgentGUI disables manual usage
refresh for that target. An absent capability keeps the existing provider
account behavior for other hosts and ordinary Runtime targets.
Account-config refresh placement is also host-scoped presentation. Tutti
Desktop opts into accountUsageRefreshInline so the refresh control shares the
limits header row. The capability defaults to false; TSH/VN and other hosts that
omit it retain the established standalone menu row.
Closing /status, Agent Info, or Agent Config cancels only the request owned
by that surface. Replaced requests remain fenced. A stream that completes
without a frame is a failed refresh: a retained value may remain visible, but
the UI must show the refresh failure rather than treating the old value as a
new success.
Desktop availability analytics reads the renderer's loaded managed-provider snapshot and must not turn a pageview or focus event into a daemon request. A stale visibility snapshot may reconcile through a non-forced tuttid status read; tuttid owns cache validity, credential-fingerprint checks, and single-flight probing. Forced detection is reserved for explicit provider refresh and the provider-scoped confirmation after an install, login, or update action. A confirmed new-conversation creation failure is the only analytics exception: Desktop force-refreshes only the exact target's provider, then reports the fresh availability snapshot with a failure-specific trigger.
Cold workspace initialization may request all managed providers together so the first useful snapshot arrives quickly. tuttid may detect different providers in parallel, but it applies one process-wide limit to auth, version, and adapter probe subprocesses because a single provider can otherwise fan out into several CLI processes. Later visible-window stale reconciliation is best-effort background work and checks providers serially; another focus cannot overlap the same reconciliation run.
Provider detection separates volatile readiness from stable executable facts.
An ordinary read may reuse the full provider snapshot. A forced read bypasses
that snapshot and rechecks current authentication/readiness, including the
provider credential marker, but may reuse a successful CLI version or adapter
launch result while the resolved executable fingerprint is unchanged. Failed
version reads and failed adapter launches are never cached. OpenCode and
Tutti Agent use their validated local credential files as the primary auth
signal; malformed files may fall back to one CLI check. Cursor's single
about --format json result supplies both auth and version when available.
Authentication status is evidence-based and provider-neutral. A local status
command, marker, token, or API key proves only configured. A provider-backed
account probe or successful agent request proves authenticated. An explicit
remote authentication failure proves required and outranks stale local
credentials until credentials change or a later provider request succeeds.
Conversation failures may update this provider-global state only for a
provider-native runtime and only from the typed completed root-provider Turn:
auth_required records required, while a completed outcome records success.
Model Plan and Agent Extension failures are connection-scoped and never mutate
the provider-global login state. Shared layers do not infer authentication from
provider prose, HTTP 403, gateway timeouts, billing, quota, or model errors.
packages/agent/daemon/providerstatus owns evidence reduction; tuttid owns
the live outcome store, cache invalidation, and public status projection. Tutti
Desktop management surfaces and AgentGUI consume that same status. They may
keep a configured provider launchable when availability is ready, but must
not relabel it as connected or authenticated.
For subscription OAuth, provider descriptors select the remote-auth strategy.
Claude Code uses GET https://api.anthropic.com/api/oauth/usage; Codex uses the
provider runtime's account/rateLimits/read. These checks are attempted on
ordinary status detection and become stale after 15 minutes. Visible, focused
Desktop windows reconcile stale providers on focus/visibility activation and
on the 15-minute poll. API-key configuration skips subscription OAuth checks.
Explicit authentication rejection is authoritative; rate limits, server
errors, and network failures leave the weaker configured state intact.
Conversation error adapters preserve typed provider code, HTTP status,
retryability, origin, and sanitized upstream detail. AgentGUI shows sanitized
provider-origin detail directly; Tutti-owned transport and lifecycle failures
continue to use localized product copy. Secrets are redacted before logging,
persistence, or presentation.
These checks validate current access but do not rotate refresh tokens; refresh
ownership requires a separate serialized credential lifecycle that can gate on
live provider processes.
2.6 Developer cassette replay
The developer-only agent.sessionRecording desktop preference defaults off.
When enabled, Desktop injects its recording and replay controls through generic
AgentGUI render slots. AgentGUI contains no recording/replay API, controller,
state, provider branch, component, or copy.
The provider-neutral renderer contract is published separately as
@tutti-os/agent-session-replay. It owns the portable activity event type and
interaction contract shared by Desktop and TSH; product adapters still own
scope mapping, persistence, HTTP/Electron integration, replay runners, and
provider/runtime setup.
packages/agent/session-replay owns the provider-neutral Recording/Cassette
workflow, status transitions, portable contracts, and validation policy.
services/tuttid/service/agentsessionreplay is Tutti's HTTP/product adapter and
composition surface; Desktop is the Electron/renderer adapter and composition
surface. Desktop reads the authoritative recording list after mounting and
projects commands; React state is never the source of an active Recording.
packages/agent/daemon/runtime retains only concrete recording and scripted
replay transport mechanics.
The default-off preference is a startup composition gate, not only a UI gate.
The renderer awaits initial persisted preference hydration before it constructs
the Workspace service container, so composition never reads transient in-memory
defaults. The preferences module owns this readiness seam; Replay remains a
consumer of the hydrated feature flag and does not add a parallel event center.
The main-process Replay composition module owns manager/access/control creation
and all Replay IPC bindings; general runtime IPC supplies only Electron and
daemon adapters. When disabled, Desktop main does not create the Replay process
manager, access adapters, control writer, or Replay IPC handlers, and the
renderer keeps the replay activity bridge inert: it creates no recording
binding, recorder map, or Engine observer. When enabled, the
agent-session-replay feature-local activity bridge owns the recording binding,
recorder map, and Engine observer fan-out; WorkspaceAgentActivityService
remains the Activity Engine/reconcile facade and delegates that replay boundary
to the bridge. The renderer creates recorder state only for the lifetime of an
active Recording and mounts isolated Replay observers only inside the Replay
runtime. Changing the preference does not claim to recompose a running daemon
or renderer; the next process composition applies the new value.
A Recording captures a time window over the root SessionGraph. Root Turn
settlement, child creation, and Goal continuation do not complete it. Explicit
stop waits for a stable boundary, detaches every graph connection, and exports
expected graph state. Daemon restart recovery marks an interrupted capture
incomplete and exposes it through the same recording list.
The cassette runner drives these controls through a real Electron AgentGUI.
Replay mode keeps the current provider adapter but replaces only its Agent
SessionGraph ProcessConnection set; current adapter outbound bytes must match
the recorded Session/provider/launch-ordinal connection before inbound frames
are released.
The Desktop feature
apps/desktop/src/renderer/src/features/agent-session-replay owns the recording
toolbar, recording list, replay-window controls, feature gating, product copy,
and daemon client adapter. Recording names default to their UTC creation
timestamp. A completed row supports inline rename; the daemon writes the name
into cassette.json, recalculates the manifest hash, and commits matching
Recording and Cassette metadata. The list header can import one or more portable
Cassette directories through a native Desktop file selection. tuttid
validates every manifest, file inventory, hash, portable state, identity, and
size limit before copying the batch into daemon-owned storage and projecting
each valid Cassette as a completed Recording in the current Workspace. Invalid
or conflicting members report per-Cassette failures without rolling back other
successful imports; source directories are never mutated. The Desktop Replay
feature service owns the native import adapter call, loading and error state,
and the authoritative Recording refresh after any successful member. React
dispatches that semantic command and renders its localized outcome toast; it
does not compose preload and refresh operations. A completed recording exposes
Play directly in its list row. When at
least two distinct root Sessions are available, the
list may enter temporary batch-selection mode and submit their exact Cassette
IDs in one Replay Workspace launch. Selecting one Recording disables other
Recordings for the same root Session; this selection disappears when the list
closes and is never Recording or Cassette domain state. The primary workspace
recording list exposes Delete for inactive rows and requires a
portaled destructive confirmation dialog before removing the Recording and its
Cassette. Active rows remain protected by the daemon workflow. The primary
workspace window never renders Replay pause or checkpoint controls. Those
controls belong only to the isolated Replay Electron surface and become enabled
only when the daemon core exposes the corresponding stable commands; the UI
must not simulate them by dropping chunks or changing a local cursor.
While a Recording is active, Desktop observes the workspace
AgentSessionEngine boundary rather than AgentGUI. Public replayable intents
enter a synchronous renderer buffer; matching external command settlements
become effect events linked to the intent that caused them. Reducer-generated
follow-up intents are not recorded a second time, and effects without a
recorded cause are discarded. Desktop flushes this ordered stream through the
recording API before asking the daemon to complete the Recording, and drops the
buffer after a successful cancellation. AgentGUI remains unaware of recording
and replay contracts.
The isolated Replay Workspace reuses the Workspace window composition and
mounts one AgentGUI Workbench Node per Replay Cassette. Desktop installs one
Workspace replay binding, not one binding per Node. The binding maps generated
Engine commandId values to the exact Cassette; command results never fall back to
recorded correlation IDs across Cassettes.
Desktop mounts Cassette-scoped status observation and playback controls through
each AgentGUI Node's composer footer slot, while Replay runtime state keeps
them empty in normal windows. AgentGUI lays this accessory slot out as a
full-width wrapping row below the primary composer controls so Replay transport
does not compete with handoff, model, or reasoning controls in narrow Nodes.
Replay speed is a daemon-owned command. The
isolated Node resolves its Cassette from exact Node identity before reading or
updating playback through Desktop IPC.
Replay status remains visible independently of provider transport readiness:
the footer renders with disabled transport controls until playback becomes
active, and terminal completion produces a success toast in the isolated
Electron window.
The primary recording list asks for automatic or manual playback when Play is
selected. Automatic mode preserves continuous playback. Manual mode enters the
isolated runtime paused at the first inspectable checkpoint, so no initial
Activity interval can race a later renderer Pause command.
Pause, resume, and checkpoint movement use a versioned
replay-control.json handoff at
TUTTI_AGENT_SESSION_REPLAY_CONTROL_PATH, because pausing only the provider
transport would still allow the runner to dispatch later business stimuli.
The runner publishes daemon-confirmed semantic checkpoint state and playback
mode beside phase in replay-status.json; Desktop observes that file but does
not manufacture playback state in React. A checkpoint becomes inspectable only
after the exact Node is mounted, its logical Session is selected, current
detail is hydrated, and the renderer has observed the satisfying canonical
version. Controls remain forward-only;
Desktop does not expose previous-checkpoint or restart.
The primary window owns only Replay launch feedback. Main supervises the
runner by (launchId, cassetteId) and keeps Cassette milestones in memory plus
the temporary Surface-status handoff. It does not persist playback execution,
checkpoint, completion, failure, or cancellation into the primary daemon. The
cassette runner writes Cassette-scoped replay, verification, success, or failure
events, and a failure in one Surface keeps the Replay Workspace open while
other Cassettes continue.
The runner uses isolated daemon state and Electron userData. CDP opens
AgentGUI, selects the provider and exact Session, and verifies the rendered
result. Recorded business stimuli are sent through the isolated daemon HTTP
contract; they are not reconstructed from composer clicks. Cassette transport
selection is daemon composition and does not change Session, Turn, Interaction,
Goal, or runtime-operation lifecycle ownership in Host. The semantic Replay
runtime creates the isolated Workbench snapshot with onboarding already
auto-opened and no restored nodes, so the runner can open only AgentGUI.
Create-session stimuli persist the
effective launch settings returned by the recorded Session; Replay must not
recompute model, reasoning, permission, or speed defaults from the current
machine. The runner preserves lifecycle ordering by waiting for the canonical
Session to become idle before dispatching each recorded session.send; HTTP
status retries are not lifecycle authority.
Managed Replay launch has two Cassette-scoped milestones. surface-ready requires
the exact Node to be mounted, its root Session selected, and that Session's
detail hydrated; it closes launch feedback but does not complete the Cassette.
replay-complete is emitted only after that Cassette's stimuli, transport check,
and expected-state check succeed, and only that milestone resolves the
Cassette's local completion as complete. A create-session Replay restores its
recorded rail Project, waits
for the create command, and selects the exact canonical Session without
reloading the page.
3. Domain model
3.1 Session
A Session holds identity, target, provider metadata, cwd, title, settings, resume information, a Goal reference, and the current active Turn reference.
Session title has an explicit ownership rule in the runtime. A user title (explicit rename) is immutable from the turn-execution path: a running turn may fold provider/event titles as candidates, but the controller's current Session is the only accepted state, and the turn-completion commit merges only the turn-owned lifecycle/status fields back. A title the user set is never overwritten by a late provider title or by a stale turn-completion snapshot, and neither the stream projection nor the durable report may carry a stale provider title over an established user title. On resume the runtime fails closed and treats a persisted title as user-established. Metadata commands issued while a new Session is still activating wait for the canonical Session; an activation failure or bounded wait timeout is an explicit user-visible error and never a silent no-op.
A Session does not copy Turn phase/outcome, own pending Interactions, or persist lifecycle inferred from transcript.
If provider-native compaction fails because the current context is already over
its hard limit, the provider adapter projects one typed
context-handoff-required system notice with error severity. AgentGUI localizes
the failure and tells the user to start a new conversation and mention the
exhausted Session there. Tutti does not replace the provider session or
automatically redispatch the user's next message: the fresh root Session and
its canonical agent-session mention make the handoff explicit and preserve
the user's control over what continues.
Provider-native subagents use child Sessions:
rootAgentSessionId/rootTurnId: root executionparentAgentSessionId/parentTurnId: direct parentparentToolCallId: delegation card correlation- child messages, Turns, and Interactions retain the child owner
A provider adapter must normalize explicit spawn evidence into both the parent
delegation call and the canonical child Session/Turn. AgentGUI attaches the
child lane only through the immutable parentToolCallId; it must not parse
provider-native spawn events, invent a missing parent card, or create a
presentation-only child lane.
The parent-side child lane preserves the latest canonical assistant message as complete Markdown both while it streams and after the child settles. The same safe streaming Markdown/link boundary used by ordinary assistant messages owns that presentation. Bounded plain-text activity is only a fallback before the child emits assistant content; character-level activity truncation must never corrupt a generated-file link or drop earlier assistant context.
User-initiated Fork creates a new root Session rather than a provider-native
subagent. The child records durable lineage to the source Session and inclusive
boundary Turn, but receives a caller-reserved canonical Session id and a
provider-created Session id. Canonical Session, Turn, Message, and Interaction
identities are session-scoped and are remapped during the atomic clone. Fork
titles are allocated across the complete lineage family, so a source titled
Session produces Session (2), Session (3), and so on even when a later
Fork starts from an earlier child.
3.1.1 Session Fork
throughTurn means the boundary Turn and all earlier canonical history are
included. AgentGUI emits only the canonical Turn id. Host resolves its durable
provider root Turn id and invokes the exact provider adapter selected by the
runtime registry; shared UI and Host code never branch on provider names.
The projected Session capability is provider/runtime-level:
provider registry declares native Session Fork
AND the exact adapter/version attests throughTurn support
AND provider state has an explicit host-copy or provider-owned binding mode
Only that conjunction projects lifecycleCapabilities.forkThroughTurn=true.
Projection consumes a live observation or persisted driver attestation only;
it does not prepare or launch a runtime. The full preparation and exact driver
revalidation happen after the user requests Fork.
Turn-level eligibility is the single
providerForkBindingAvailable fact projected from a non-empty canonical
rootProviderTurnId, plus the canonical Turn being settled. Host eligibility
remains optimistic and verifies only those selected-Turn facts. AgentGUI
exposes Fork only for settled, provider-bound Turns. This per-Turn presentation
rule is shared by every Agent provider. Source activity, pending Interactions,
descendants, and historical prefix provenance do not hide Fork actions for
earlier settled Turns. Only an in-flight Fork for that exact canonical Turn
disables its button.
Session Fork is available whenever the exact provider/runtime and selected Turn project the capabilities described above. Desktop exposes that capability directly, and tuttid verifies the same provider and Turn facts when executing a new Fork write; no separate feature preference gates the action.
Execution accepts a worktree-isolated source. A provider-native Fork retains the provider cwd, and Tutti freezes the same cwd and isolation coordinates into the target snapshot without creating or transferring worktree ownership. Other runtime facts are frozen into the target snapshot as well. Only attachments referenced by that snapshot are staged into the target namespace; the source attachment directory is never copied wholesale.
Fork is a durable Host-owned saga:
prepared -> dispatching -> provider_accepted -> committed
\-> unknown
\-------------------------> failed
requestId is the caller-stable replay identity and
targetAgentSessionId is reserved at prepare. Prepare freezes the complete
canonical snapshot through the selected Turn, the selected provider binding,
driver kind/version, runtime context, settings, and a deterministic attachment
manifest. Referenced attachments are staged into the target namespace before
provider dispatch. The source remains writable; only physical deletion retains
the frozen resources. A second explicit Fork from the same boundary is valid.
The provider call begins only after the dispatching marker commits.
The provider adapter verifies only that the selected provider Turn exists in
the provider source. Tutti deliberately trusts earlier provider and canonical
history to represent the same conversation. Provider acceptance and the child
provider Session id are persisted before host-copy binding and canonical
materialization. A binding or materialization failure therefore leaves
provider_accepted; retry continues only local work and never reissues the
provider mutation.
prepared recovery safely continues dispatch because the durable marker proves
the provider call has not begun. A crash in dispatching becomes unknown and
is never automatically redispatched. provider_accepted recovery retries only
local binding and materialization. The canonical commit consumes the frozen
snapshot without re-proving the live prefix, remaps all session-scoped ids,
normalizes a live boundary to settled/interrupted, supersedes copied pending
Interactions, persists lineage, and emits the complete committed delta.
3.2 Turn
One user submission or provider continuation belongs to one canonical Turn.
submitted -> running -> waiting -> running -> settling -> settled
Terminal outcome is independent from phase:
completed | failed | canceled | interrupted
Cancellation targets an exact Turn. cancel_requested, provider confirmation, and canonical settlement are distinct facts; UI must not manufacture an early terminal outcome.
3.3 Interaction
An Interaction represents an approval, question, or plan confirmation that requires user handling:
pending -> answered | superseded
Actionable UI reads canonical pending Interactions only. A transcript tool row showing waiting_input does not create answerable state.
A provider adapter must publish interaction.requested with the complete
immutable input required by the action surface. When a provider splits one
request across ordered wire frames—for example permission identity and options
first, then the matching question body in a tool update—the adapter correlates
them by exact Turn and tool-call identity and delays Interaction publication
until the input is complete. A later transcript tool update cannot repair an
already-persisted incomplete Interaction. The adapter must also validate that
the provider wire can represent every answer allowed by the published surface;
an unsupported shape fails before publication instead of exposing a lossy
Interaction. This publication gate applies equally to emitted events and
SessionState.PendingInteractive; a snapshot must not leak the incomplete
request while the adapter waits for the later frame.
Approval detail presentation is provider-neutral. The daemon carries generic
url/uri and prompt/instruction display fields into the canonical
toolCall.input when a provider reports them only at the tool-call root, and
AgentGUI renders those fields from either canonical nested input or compatible
root input. Shared and local surfaces use this same projection; they must not
branch on a provider or tool name to recover web-action details.
A child Interaction may appear in the root conversation, but submission carries the exact (agentSessionId, turnId, requestId) tuple.
Every AgentGUI, Message Center, Desktop notification, and Mobile action submits
that tuple plus the user's current answer through
engine.submitInteractionResponse. The Engine allocates command identity,
applies the shared 30-second timeout, rejects non-pending or in-flight targets,
and recognizes an explicit exact repeat after a confirmed failure. It does not
silently reuse the previous answer when a caller omits fields, and it does not
retry delivery-unknown responses. Plan-implementation actions remain their
specialized plan-decision workflow rather than masquerading as ordinary
Interaction responses.
Free-text approval feedback is a non-terminal rejection. When an approval
offers both deny and a terminal deny_and_stop/abort option, the feedback
surface submits the non-terminal deny option and preserves the full feedback
payload; abort remains a separate stop action. A provider adapter first
acknowledges that exact approval response, then carries the feedback through
the provider's active-turn guidance protocol using the exact provider thread
and Turn identities. Waiting for that guidance acknowledgement must not turn a
successfully answered Interaction into a failed runtime operation.
3.4 Goal and operations
Goal is a Session-level durable entity, not a Turn command. It owns desired/observed state, revision, and an independent operation.
A Goal operation may produce zero or more provider Turns, but it cannot reserve or fabricate Turn IDs. Goal control bypasses the prompt pipeline and does not create a user transcript Turn message. AgentGUI dispatches goal/controlRequested; the workspace Engine owns the provider-neutral goal/control command, typed effect execution, and exact Session-scoped result reconciliation. AgentGUI may project its durable session audit as a dedicated goal-control timeline row; that row has no Turn ID and does not participate in Turn counts, processing ownership, cancellation, or settlement. When the audit carries a user-visible command body, the dedicated row also uses the shared user-message action surface, including copy.
When a session-level timeline row occurs chronologically between two rows from the same Turn, transcript presentation keeps one Turn group and renders the session-level row as an interstitial item. This presentation grouping does not assign the row a Turn ID or make it lifecycle-owned by that Turn.
Host owns recovery for runtime operations, Goal operations, and the reconcile inbox. An adapter must not start a second worker or state machine.
On daemon restart, Host recovery first restores durable operations, then settles unrecoverable active Turns as settled/interrupted and supersedes pending Interactions.
Codex's restored Full access warning is presentation-only, device-local safety chrome. Show it only when an empty home composer restores an unacknowledged Full access target default; do not show it for another provider or permission mode, an active or historical Session, or while defaults are loading. Explicit Full access confirmation and “Don't show again” persist the same browser-local acknowledgement, while the close action affects only the current mount. This acknowledgement must not enter Session lifecycle, target defaults, Workbench node data, or AgentGUIRuntime state.
3.5 Edit retry and effective history
Edit retry is a Host-owned lifecycle operation, not a transcript mutation and not a GUI-orchestrated pair of provider calls. It is available only for the latest settled root user Turn when the provider exposes authoritative effective history and the Session has no active or child work. The command carries the exact Turn id, expected history revision, edited text, and a caller-stable operation id.
AgentGUI edit intent
-> agent-activity-core typed command + stable client operation id
-> Desktop transport adapter
-> tuttid HTTP adapter
-> Host durable edit-retry operation
-> provider rollback/read/start capability
-> SQLite effective-history transaction
-> committed activity invalidation
-> Desktop semantic event normalization
-> agent-activity-core state-and-message reconcile
-> AgentGUI canonical projection
Host checkpoints before provider mutation and treats provider thread/read as
the authority after an unknown result. Confirmed rollback retracts one complete
Turn from effective history but retains its audit row and file-change record.
The replacement reuses the persisted structured submission and changes only
its first text block, preserving attachments and other non-text input. Rollback
is never represented as natural-language model input.
AgentGUI projects only three presentation states: ready, processing, and
needs-action. It may expose edit controls only on the exact eligible Turn.
The workspace AgentSessionEngine owns command identity, pending/failure
state, recovery-action dispatch, and command-result reconciliation; React keeps
only the unsent editor draft and awaits the engine-owned command settlement.
An accepted transport result enters an engine-owned reconciling state and
does not overwrite canonical availability. Editing stays blocked until a
session-detail read confirms the returned history revision and recovery state;
Desktop must not manufacture recovery actions from the command response.
resend_pending and recovery_required are explicit recovery states rather
than indefinite loading. GUI recovery commands delegate to Host and never
choose whether rollback or replacement should be repeated. Completion and
terminal Turn events request both state and message reconciliation because
events are latency hints; the authoritative detail and message reads repair
missed WebSocket delivery without requiring a Session switch.
3.6 Messages and ordering
A durable message has two independent ordering values:
sequence: presentation order assigned at creation; streaming updates do not change itversion: per-session mutable change cursor used for incremental updates and gap detection
Lifecycle timestamps describe occurrence time; they do not replace durable sequence. A live message with unknown Turn ownership must be completed or rejected at the boundary, never assigned an owner in GUI. Optimistic prompt and Goal-control echoes occupy a separate overlay ordering domain. They remain after all currently durable messages until canonical confirmation replaces them; their earlier submission timestamps must not move them back through durable transcript history during message projection or timeline merging.
Neither value is a history-coverage signal. A newest-to-oldest message read
projects an explicit Session message window into the frontend engine:
oldestLoadedVersion is the next paging cursor and the response's hasMore
becomes hasOlderMessages. AgentGUI may request an older page only from that
authoritative window. It must not infer older history from a non-one minimum
version, a version gap, sequence, timestamps, or transcript shape; streaming
updates can raise a message version without creating any older row.
The same rule applies to imported transcripts. When the provider transcript contains trustworthy user-message boundaries, the import adapter persists stable settled Turn identities before the data reaches AgentGUI. A page that starts midway through a long imported Turn must therefore retain that Turn id; AgentGUI must not depend on the leading user message being present in the currently loaded window. Imported content before the first trustworthy boundary remains explicitly session-scoped.
4. Workspace frontend engine
One (workspaceId, runtime origin) maps to one AgentSessionEngine. Panel unmount, Workbench node reconstruction, and standalone window switching must not change its lifecycle.
The engine owns:
- canonical Session, Turn, Interaction, and Message indexes
- pending activation/submit intents and optimistic projections
- prompt queue, send-now, and cancel-then-send coordination
- session mutation, Goal Control, settings, composer options, and operation state
- workspace/session reconciliation state
- ephemeral per-Session runtime command availability projected by the host
- attention/read state and cross-surface selectors
The engine does not own daemon persistence, provider transport, DOM, or permanent UI layout.
When a model change invalidates canonical context-window usage, AgentGUI keeps the last rendered usage for that exact Session visible until a fresh canonical usage value arrives. This retained value is presentation-only: it is keyed by Session, never crosses into another conversation, and is never written back to the workspace engine.
Runtime command availability is session-scoped whenever one workspace engine
can contain Sessions backed by different transports or access policies. The
host projects available, transport/capability blocking, or the explicit
agent_sharing_revoked state with the owner display label. The engine uses that
single fact to gate sends, cancellation, settings, and Interaction or plan
responses. AgentGUI preserves the composer draft content but disables editing
and runtime-dependent actions. A revoked shared Session keeps its history and
renders the package-owned owner-specific notice; it becomes available again
only when the host projects the sharing relationship as active. It must not
reuse the engine-wide connection state for this case, because one remote
Session losing its owner or access must not disable Local Agent or another
remote Session.
Mobile projects pending Interactions from the root conversation plus its child
Sessions and reads each exact Engine response record for submitting/failure
state. Native cards call engine.submitInteractionResponse; they do not
construct response intents, recover previous answer payloads, or keep a
parallel Promise lifecycle. After a confirmed failure, Mobile keeps the
canonical prompt and its explicit answer controls visible instead of replacing
them with a payload-free retry button. Missing provider-authored Plan options
fail closed, and runtime unavailability disables the exact response without
discarding composer drafts or Interaction identity.
Device connection presentation is target-scoped rather than Session-scoped.
The host exposes a target connection source keyed by agentTargetId with the
current status and retry attempt, and AgentGUI reads the active conversation
target or the selected Home target. This lets a new-conversation composer show
and enforce connection state before any Session exists. Session runtime
availability remains the independent command safety gate for existing
Sessions; it is not the source of device connection presentation.
Sharing eligibility is a parent gate for device connection. When an owner has
revoked a Session's shared Agent relationship, the host projects
agent_sharing_revoked and does not manufacture a target transport state for
that relationship. Device reconnect and automatic retry presentation begins
only while the sharing relationship is active.
Outside an exact pending Interaction, AgentGUI projects a blocked target
connection through the chrome above the composer and gives it precedence over
other non-interaction recovery notices because ordinary Composer writes cannot
complete while the target is blocked. An explicitly terminal unavailable
state appears immediately. Initial connecting appears only after a
300-millisecond controller delay so short background connections do not flash.
A recoverable host retry, including a dormant low-frequency retry, remains a
neutral connecting presentation and updates the visible retry attempt without
restarting the delay. During the initial delay, the raw target state already
blocks commands, but AgentGUI keeps the existing non-interaction recovery
chrome visible until the connection notice replaces it. Recovery removes the
notice without a success banner. The notice does not offer a manual retry
because transport recovery is host-owned.
When the Host supplies readiness for the exact approval or interactive prompt being presented, that readiness result is the sole transport-chrome and early write-admission authority for the card. Target connection and exact-Turn observation gaps continue to gate ordinary Composer commands, but neither may hide the card or override an exact ready or blocked interaction result.
Session presentation derives one canonical Composer gate from target
connection, Session runtime availability, provider readiness, ownership,
activation, Interaction, and busy/queue facts. The gate is one atomic
projection with separate editor, submission, and runtime-command decisions;
AgentGUI must not place canSubmit, target-connection blocking, or
Session-runtime blocking in independent memoized view slices and recombine them
later. The editor, send action, keyboard submit paths, Stop availability, and
Interaction submission consume that same gate snapshot. Busy work may project
queue submission while keeping the editor editable. Draft emptiness, upload
progress/failure, project existence, and other draft-local conditions may
disable submission, but must not change editor editability.
Engine submitting and unconfirmed-submit selectors remain Turn-admission facts
after a canonical Session first appears. Session existence or an available
runtime must not create an idle frame in the Composer spinner before the exact
Turn claims the submission, but these facts do not create execution busy,
Conversation working, or queue eligibility. A viable new-Session activation
with initialTurnExpected remains the same Turn-start bridge while no canonical
latest Turn exists. Goal-only activation deliberately does not set
initialTurnExpected, because Goal Control does not synchronously create a
Turn. A viable initial Goal set whose projected Goal is still active remains a
Turn-start bridge only while the Goal is optimistic or the host supplies an
exact pending/applying/unknown goalSyncState with a pending operation identity.
A synced Goal proves only that the Goal mutation converged; it does not prove
that a future Turn will exist. The first canonical Turn, a synced or non-active
Goal, failed/diverged synchronization,
pending/applying/unknown without an operation identity, or a
canceled/failed activation releases the bridge; initial Goal clear never
creates it. A host that omits goalSyncState cannot prove post-create execution
and therefore fails closed to the canonical availability projection instead of
keeping the Composer spinner active indefinitely. When a provider exposes an exact
session-level running/idle observation before Turn identity, the daemon
projects that
typed, non-persistent runtime activity through agent.activity.updated after
the associated state report wins canonical ordering. Desktop and Mobile Live
consume the same event variant. The workspace Engine uses its occurrence time
to reject reordering and prevents an older running observation from
overriding a settled Turn. A disconnect clears the ephemeral observation,
while canonical Turn state remains authoritative once it exists. AgentGUI
consumes the Engine's fenced Session display status after the canonical
Session exists; it must not recompute Composer busy state from the raw runtime
activity flag, because that would let an older running observation outlive a
completed or failed latest Turn. Raw runtime activity may bridge presentation
only before that canonical Session projection exists.
4.1 Read/write rules
- reads use exported selectors or memoized
AgentActivitySnapshot - an engine subscriber notification is an invalidation signal, not a render command. Concurrent AgentGUI surfaces subscribe through exact Session-family or target selectors; a selector preserves its selected reference when another root Session changes
- AgentGUI transcript presentation subscribes to projected messages for only
the selected root Session and its current child Sessions. This projection
includes optimistic
message_deltatext before durable confirmation, while preserving message-array references for every unrelated Session - whole-workspace
AgentActivitySnapshotprojections remain valid for bounded aggregate reads, but do not belong in high-frequency AgentGUI render paths. Event callbacks that need current canonical data read the engine snapshot at event time instead of retaining a whole-workspace render snapshot - optimistic
message_deltainvalidations are coalesced on the host-injected scheduler; the Desktop bridge queues matching session-message events and flushes them at the same boundary so the rendered snapshot and event fan-out cannot observe different prefixes. The external-storesubscribeandgetSnapshotcallbacks must remain referentially stable across renders - lifecycle writes use semantic Engine operations or typed intents/commands
- composer-option reads use
engine.loadComposerOptions; the Engine owns request identity, signature-aware cache reuse, identical in-flight joining, supersession, exact settlement, caller abort, and disposal. Desktop and Mobile retain transport execution in theirEngineExtensionCommandadapters and delegate pure generated-DTO projection to@tutti-os/agent-activity-tuttid-adapter - A composer-option failure with no signature-matching cached value remains an
explicit Engine
errorstate. AgentGUI renders a compact failure and retry action in the existing options slot instead of presenting an empty list or a stale/default selection; a last valid cached value remains renderable while its load status is reconciled. The retry is the same semanticengine.loadComposerOptionsoperation withforceleft false, so request identity, cache reuse, and identical in-flight joining remain Engine-owned; a view must not call the transport adapter directly. - an acknowledged home Composer default remains an optimistic draft until a later authoritative Composer-options response reports the same effective field value. A successful read alone must not retire the draft because a slow or overlapping provider discovery may still return an older default. When a settled response omits that field, AgentGUI keeps the optimistic intent but releases its confirmation marker so providers that cannot project the field do not remain on permanent forced, uncached discovery. Concrete conflicting authority is retried only for a bounded number of confirmation reads; if a provider keeps normalizing, rejecting, or overriding the default, AgentGUI protects the optimistic intent from generic option sanitization only while that confirmation marker remains active. Releasing the marker lets later settled options apply normal sanitization and return to the Engine's signature-aware cache
- the Engine alone translates shared activation, prompt send, settings update,
Goal Control, turn cancel, Interaction response, rename, pin, and
batch-delete commands into
AgentSessionEffectPortcalls. Desktop and Mobile effect ports retain transport, product integration, and host-specific DTO mapping. Their create/send request and Turn response projections delegate to@tutti-os/agent-activity-tuttid-adapter; they must not duplicate a command-type switch for these shared effects. Host activity facades callengine.activateSession,engine.submitPrompt,engine.controlGoal,engine.stopSession,engine.updateSessionSettings,engine.renameSession,engine.setSessionPinned, andengine.deleteSessions; these deep methods own the applicable workspace projection, protocol or mutation identity, timeout, cancellation, settlement, and canonical result projection. Settings update is fire-and-observe intent admission rather than a settlement Promise: the existing settings-operation selector remains the source of pending, failed, and unknown state. Hosts must not reconstruct mutation protocol with reducer intents and snapshot reads or construct rawsession/settingsUpdateRequestedfields for an existing Session. Session stop is also fire-and-observe admission: the Engine owns its command identity, 30-second cancellation timeout, duplicate fence, and 30-second first-Turn waiting window. Desktop AgentGUI and Native Mobile callengine.stopSessionand never construct rawsession/stopRequestedfields. Existing-Session Goal Control is also fire-and-observe admission. The caller proposes one stableclientSubmitId; the Engine admission returns the effective identity used for settlement. The Engine owns workspace scope, command identity, the 30-second transport timeout, one in-flight operation per Session, optimistic Goal projection, typed result validation, and delivery-unknown reconciliation. AgentGUI readsselectSessionGoalControlPresentationand never stores a parallel optimistic Goal or settles the transport Promise itself. Desktop and Mobile effects send the request and map the authoritative Session/Goal/operation evidence only. Session activation enters throughengine.activateSession. Desktop AgentGUI and Native Mobile keep product-specific target selection, placement, initial content/settings, and stable request identities, while the Engine owns workspace scope, timestamps, construction ofactivation/requested, the accepted result, and one 120-second confirmation window. The confirmation deadline is later than the 90-second new-Session command timeout so a valid slow runtime startup cannot expire while the command may still succeed. The typed activation effect returns either the created authoritative Session or the resumed Session's authoritative detail aggregate. The Engine validates the versionedactivation-v1result contract, result scope, mode, and every nested Session/Turn/Interaction entity, then projects the aggregate in its own drain. Desktop and Mobile effects retain transport, DTO mapping, and product-local integration/observability concerns, but must not pre-dispatch those projections. Canonical monotonicity guards prevent a late activation response from regressing newer realtime state. A failed new-Session activation is a rollback signal only when the Engine's canonical Session selector confirms that no Session entity exists. If the Session exists but runtime startup or the initial Goal failed, the Session remains selectable and the failure is rendered as Session detail state. An explicit historical-row selection has higher priority than stale activation failure metadata; it first requests detail reconciliation and only returns home after an authoritative not-found result. Surfaces clear a new-Session draft only after activation admission succeeds. If an admitted new-Session activation is canceled before canonical Session confirmation, the surface restores the submitted draft only while the current draft is still empty, releases the pendingclientSubmitId, and allocates a fresh identity for the next explicit submission. Existing-Session Prompt submission enters throughengine.submitPrompt. The surface keeps the stableclientSubmitIdused by its draft-recovery and idempotent-retry bookkeeping; the Engine owns workspace scope, timestamps, routing protocol, the shared 120-second confirmation window, and the accepted/queued admission result. Desktop AgentGUI and Native Mobile clear a submitted draft only after that result confirms admission, and never construct rawsubmit/requestedfields or rebuild admission from selectors. New-Session initial content continues to travel with activation. Provider acceptance protects provider-turn identity and fork safety, but it is not the prompt's durability boundary. If runtime delivery is explicitly rejected or times out with an unknown outcome, Host persists the submitted content andclientSubmitIdindependently of the canceled request. Existing sessions then receive a terminal failed Turn or an uncertain-delivery claim; only an unobserved provisional activation may be compensated away. Platform-only commands remain in each host'sEngineExtensionCommandadapter. Every effect propagates its typed Engine origin and Engine-owned AbortSignal to its transport. Rename, pin, and delete settle through the shared Session-mutation state. Rename and pin may update canonical Session state only from a validated authoritative Session result. Delete may remove canonical Sessions only from a validatedSessionDeleteMutationResult, projected assession/removedtombstone intents. Caller cancellation aborts the host effect, but an already accepted write remains delivery-unknown until later canonical reconciliation; it is never converted into a confirmed failure. Direct settings changes, post-activation persistence, and prompt-required settings share one per-Session Engine lane. Owner boundaries are serialization barriers rather than coalescing opportunities. A validated precondition updates canonical Session state before the Engine starts send, while a failed or timed-out precondition prevents delivery. A timed-out settings write remains delivery-unknown and does not release queued writes automatically. A fresh explicit settings selection is the user's retry:engine.updateSessionSettingsrecognizes the exact Engine settings-operation state, so Desktop AgentGUI and Native Mobile do not derive retry flags independently. Ordinary approval and question answers similarly enter throughengine.submitInteractionResponse; the Engine owns exact Interaction identity, command identity, timeout, deduplication, and confirmed-failure retry admission. Surfaces submit the current explicit answer and never dispatchinteraction/responseRequestedor reconstruct a previous answer themselves - consumers do not read reducer maps directly
- consumers do not create canonical session/message mirrors
- optimistic records define confirmation, rejection, timeout, and uncertain-delivery paths
- business command completion returns to the engine as a result intent; controllers do not rebuild lifecycle with Promise/effect chains
Edit-and-retry availability is an exact SessionEngine projection, not a fact inferred from transcript order. AgentGUI edits only the authoritative eligible latest Turn, preserves attachment and non-text blocks through the Host-owned submission envelope, and never optimistically splices the transcript. After the command is accepted, Desktop applies effective history through the composite authoritative snapshot; AgentGUI only renders that canonical result. An authoritatively retracted initial optimistic prompt is marked on its pending activation so it cannot be materialized again while activation metadata and turnless controls remain intact.
4.2 Historical pull and realtime push
- list/history reads use
session/snapshotReceivedand do not create unread completion - newest-to-oldest reads attach their authoritative message-window coverage to the same snapshot intent; incremental/realtime updates preserve that coverage
- realtime authoritative entities use upsert intents
- optimistic
message_deltaupdates invalidate the exact Session projection; they do not write an unconfirmed message into canonical Engine state. Terminalmessage_updatereconciliation replaces that optimistic projection - an authoritative Session detail result enters through
session/detailSnapshotReceived;agent-activity-coreexpands the root Session, Turns, child Sessions, and optional message coverage in one engine drain and one subscriber notification. Desktop and Mobile use the same@tutti-os/agent-activity-tuttid-adapteraggregate mapper and must not dispatch each entity independently - Desktop and Mobile execute Engine-owned
session/reconcilecommands through the same activity-core Session reconcile executor. The executor owns scope, cursor/window, pagination, double-detail race closure, cancellation/deletion fences, and atomic application; hosts own transport, DTO mapping, diagnostics, polling, and presentation side effects. A shared-agent detail open also re-materializes a durable binding into the local Engine when its cached binding exists but the local runtime projection is absent; this repairs local state and does not mutate historical data. - every daemon Session response carries the required
messageVersionhigh-water cursor. Daemon and renderer ship as one protocol unit, so the shared adapter rejects a missing or invalid cursor instead of fabricating zero or entering a compatibility read path - Desktop and Mobile use the same host-neutral event-observation helper to decide whether normalized entities can apply inline and whether the exact Session needs an authoritative state or message reconcile; partial parsing, identity mismatch, count mismatch, or cursor disagreement always reconciles
- message updates fold inline only when unseen versions are continuous
- version gaps and reconnects trigger incremental message reconciliation for hydrated Sessions
- Turn, Interaction, and legacy state invalidation trigger authoritative Session reconciliation
- realtime provenance survives until the authoritative result reaches the engine; fetch failure must not downgrade it to historical
4.3 Root and child hydration
Workspace lists show root Sessions only. A root detail read also returns nested child Sessions; the engine stores every entity, Rail selects roots, and timeline/Message Center selectors aggregate descendants.
Child Session discovery does not require every host to prefetch child transcript pages. A surface that does not render child conversations keeps the hierarchy and pending Interaction state canonical without paying for unused child message history.
The shared reconcile executor exposes an explicit message-hydration policy.
Desktop selects Session-hierarchy hydration for its aggregate projections;
Mobile selects requested-Session hydration while it does not render child
transcripts. The first read remains a bounded newest-first page. Later detail
reconciliation compares each hydrated Session's required messageVersion with
the largest locally cached durable message version; an unchanged child performs
no message request. Newly discovered children use the bounded newest-first
read, while already hydrated children that advanced use incremental reads. A
known empty child window is an authoritative durable cursor at zero, so its
first later messages are drained incrementally from afterVersion=0; it is not
treated as an unknown window. The second detail read catches a root or child
that advances while the first message pass is in flight. Transient optimistic
rows never advance this cursor. This child policy is separate from the root
conversation's user-boundary repair policy, which may intentionally restart an
incremental read at zero.
The discovery detail request uses the daemon's messageHydration projection.
That projection retains the hierarchy and messageVersion cursors needed by
the shared executor but does not resolve provider-backed lifecycle
capabilities. The final detail request uses the full projection and is the only
read in one combined reconcile that may perform a historical provider
capability probe. Desktop and Mobile derive this choice from the same
activity-core request purpose; host adapters must not infer it from timing or
add a TTL cache for capability results. The detail response echoes the selected
projection and an explicit lifecycle-capability projection flag. The shared
tuttid client rejects mismatched responses, and consumers must not treat
false-valued capabilities from an unresolved hydration projection as
authoritative.
A waiting Turn does not imply user action. Only a pending Interaction produces approval/question attention.
4.4 Prompt queue
The busy-session prompt queue is ephemeral durable-intent coordination in the workspace engine. It is neither a daemon queue nor component state.
- a normal prompt waits for canonical availability
- a queued-prompt
Send nextaction uses native guidance when the provider supports it, even when the provider also supports interruption; guidance stays on the same canonical Turn and follows the provider's native semantics - when an active-turn
Send nowarrives before the Session has an authoritative capability snapshot, the workspace Engine retains the exact prompt and send-now decision without dispatching guidance or cancellation. A later authoritative snapshot resolves that same intent to native guidance or cancel-then-send. An explicit complete snapshot that supports neither keeps the prompt in the ordinary queue until canonical availability returns; a missing snapshot must never discard the prompt or be treated as unsupported. Each queued prompt owns its deferred decision and exact target Turn; a later Turn must not inherit guidance or cancellation intended for its predecessor - Claude SDK guidance acknowledges delivery only after its SDK interrupt has succeeded and the guidance prompt has been enqueued; a failed interrupt is a failed guidance request and must not allow the old response to keep running. The confirmed interruption settles the old response's streaming projections while the canonical Turn remains active for the guidance response
- Codex/Tutti Agent guidance sends
turn/steerwith the exact active provider Turn identity. Steering inserts the guidance into the current provider Turn; it does not interrupt the response or start a replacement provider Turn. If the provider response has already ended while the canonical Turn remains active for child work, guidance may start a provider continuation withturn/starton that same canonical Turn - guidance captures the canonical
activeTurnIdat the interaction boundary and carries it through the queue, activity adapter, daemon API, Agent Host, and runtime Controller;turnIdis required for every cross-process guidance request and is never inferred from the latest Session snapshot - Host and the runtime Controller compare that target with the live active
Turn while holding the lifecycle admission lock. A mismatch is a typed
pre-provider rejection (
NotDispatched), and a prepared submit claim is removed so the failed guidance cannot strand the queue or be redirected to a newer Turn - otherwise send-now performs exact cancel-then-send
- user Stop pauses the queue; cancellation must not leak the next prompt
- a durable pending-submit record does not by itself make queued backlog stoppable. A queued prompt without a Turn becomes a pending-submit Stop target only while that exact queue prompt is in flight or has uncertain delivery; visible or suspended backlog must not keep the Composer Stop spinner alive
- a prompt settings precondition is an explicit preparation stage, not a nested host effect. It serializes with direct and post-activation settings writes, updates the canonical Session on success, starts send before releasing later settings writes, and fails the logical prompt without delivery when the settings result is not valid
- a visible failed queue entry continues to own its submitted content for retry; draft settlement must not duplicate that content back into the composer
- uncertain delivery reconciles by
clientSubmitIdand exactturnId; it never resends merely because the Session appears idle - editing a queued prompt restores its stable attachment references, then rehydrates missing image previews through
AgentGUIRuntimewith the exact workspace and Session identity; renderer-inaccessible paths never become image URLs, and late reads may update only the matching restored draft image - rebuilding a Composer draft from queued canonical Markdown resolves mention labels and icons in each
AgentMentionNodeViewthrough the nearest workspace-scopedRichTextMentionService; every NodeView subscribes only to its canonical mention identity and keeps presentation out of the Tiptap document, while missing, failed, or removed services fall back to the canonical label and semantic icon. Agent target catalog presentation remains authoritative over service fallbacks - the delivery barrier serializes new-Turn sends only; a guidance head steering the running barrier Turn is exempt and may steer it repeatedly, while in-flight, uncertain-delivery, suspension, and failed-head blockers still gate guidance sends
- drain readiness is one pure decision over the queue record and canonical availability; a new blocker joins that single decision with an explicit priority against every existing blocker, never as another independent pre-check in the drain path
4.5 Rail query and presentation state
The headless AgentGUIConversationRailQueryController is the single
cross-platform owner of Rail query scope, first-page refresh, cursor
pagination, stale-request fences, membership reconciliation, and Engine
ingestion. Desktop and Native Mobile both construct it through
createAgentGUIConversationRailQueryController, the canonical factory
exported by @tutti-os/agent-gui/conversation-rail-controller; a host must not
instantiate the internal implementation or recreate that state machine in its
app layer.
The Rail query cache stores section metadata, ordered Session IDs, cursors, and totals only. Each first-page or pagination response passes Session DTOs transiently through the host mapper into Engine upserts, while the Rail snapshot retains only memberships, ordered IDs, cursors, totals, loading, and failure state. Refreshing a bounded Rail page is not deletion evidence and must not replace or prune canonical Engine entities.
The public headless snapshot contains query and membership state only.
Desktop-localized conversation summaries are projected from the snapshot plus
canonical Engine state outside the controller, so the Native entrypoint does
not depend on Desktop presentation or locale bundles. The public factory
accepts only the Engine, active-conversation identity getter, canonical runtime
queries, workspace identity, and small scheduling/page-size ports; cache
records, diagnostic trackers, and request-generation seams remain package
internals. Surface identity such as a Desktop AgentGUI nodeId is
adapter-owned diagnostic context: the Desktop runtime adapter enriches
diagnostic payloads instead of passing it into the headless controller
interface.
The full AgentGUI surface reads the optional
AgentGUIRuntime.conversationRailQueryLimits.sectionRefreshLimitMax backend
contract and passes it to that same controller. A host whose section endpoint
accepts less than AgentGUI's default refresh limit must declare its positive
maximum instead of relying on transport rejection or adapter-side truncation.
Resolved query results may be reused from the workspace cache. In-flight
first-page entity payloads are controller-generation scoped and must not be
shared across mounted controllers: detach, pause, or a scope change must fence
both Engine ingestion and cache writes from the obsolete request.
The canonical factory owns one resolved-query cache per workspace Engine, so
Desktop and Mobile receive the same remount semantics without exposing cache
access through AgentGUIRuntime or a host adapter.
Hosts own the transport adapter, DTO mapping, runtime-availability policy, and surface lifecycle. For example, Mobile owns disconnected polling and foreground/background pause-resume around the shared controller. Native hosts also own their renderer, localized status projection, and interaction layout; those host concerns must not leak back into the shared query controller.
Mobile DeviceLink recovery is application-scoped rather than screen-scoped.
MobileApplicationService is the single owner of the paired-device connection
phase (idle, reconnecting, synchronizing, connected, or failed).
Native stops Agent Live immediately on background entry while retaining the
underlying DeviceLink for its background grace interval. Every Native-to-JS
live delivery carries the caller-owned subscription generation; the bridge and
workspace live lane reject deliveries from a closed generation before they can
reach the coordinator. JavaScript records the background entry time and uses
elapsed time on foreground instead of relying on a timer that the runtime may
suspend. Foreground resume queues canonical workspace and selected-Session
reconciliation before opening the replacement live lane. An unexpected
live-lane disconnect first allows one bounded stream retry, then rebuilds
DeviceLink if the stream remains offline. Runtime commands stay blocked until
the replacement live lane reports ready.
Recovery retains the current device, workspace, navigation, and drafts behind a global blocking presentation. A replacement workspace scope is started and authoritatively hydrated before it atomically replaces the paused scope; a failed candidate cannot clear the visible conversation. The App-root overlay only projects the service phase and exposes retry or explicit return-to-device commands. It does not own timers, transport checks, retry policy, or copied connection state.
Cross-platform hosts may reuse the DOM-free canonical Rail summary projection
from @tutti-os/agent-gui/conversation-rail-projection. They must still obtain
ordered membership, project labels, totals, and cursors from the authoritative
section query and join those IDs to canonical engine Sessions. Native hosts own
their renderer and interaction layout; they must not import Desktop or Web
components, infer project membership from cwd, or create a second Session
lifecycle store.
AgentGUI's existing-Session project projection uses the authoritative
railSectionKey as its only join key: it matches that value exactly against a
registered user project's sectionKey. The key is immutable under runtime
reports, cwd changes, and user-project list updates. The conversations key,
a missing key, or an unknown key fails closed to no project
label even when the Session cwd equals or sits below a registered project
path. Conversely, a recognized project key keeps its project identity when the
runtime cwd is an isolated worktree or another detached execution directory.
Path matching is reserved for resolving the user's explicit project selection
before a new activation; it is not an existing-Session compatibility fallback.
The Remove project action delegates one awaited operation to the daemon. The
daemon deletes every unpinned root through the canonical Host batch path, keeps
pinned Session trees, atomically rehomes retained rows to Chats, and only then
removes the user-project row. Failure keeps the project visible for retry; the
renderer does not compose candidate-query, Session-delete, and metadata-delete
requests itself.
Native Mobile applies the same rule to Activity labels and Rail placement.
Section sessionIds remain query membership, page-boundary, initial-order, and
hydration data. After joining those memberships to current Engine entities,
hosts sort loaded ordinary sections by the canonical conversation sort key:
the latest Turn start time, falling back to Session creation time. Streaming
freshness, status, and completion updates do not change that key, so parallel
running Turns do not churn Rail positions. Pinned sections preserve the query's
pinned order. If a section disagrees with a Session's canonical
railSectionKey, the host rehomes the row by the Session key instead of
accepting the stale membership; presentation reordering must not mutate cached
memberships or pagination cursors.
Cross-platform hosts may also reuse the DOM-free canonical transcript
projection from @tutti-os/agent-gui/conversation-projection. It accepts the
canonical activity snapshot, selected Session id, and known Turns. The
projection derives both the Session and its messages from that one snapshot,
hides intermediate AgentGUI activity-card/timeline-item construction, then
returns the same AgentConversationVM that AgentGUI renders for message
merging, thinking, tool groups, processing, notices, and turn summaries. Its
portable navigation resolver covers only external URLs and Agent Session
mentions and depends on focused host-neutral parsing primitives shared with the
broader Workspace resolver; file, local-asset, app, issue, and custom-mention
actions remain host capabilities. Projection code in this entry must depend on
focused pure helpers for persisted pasted-text presentation and file-path
validation; importing the interactive Composer model or the broad Workspace
link resolver would pull Web-only editor, UI System, issue, and file-manager
types into Native consumers. Native hosts own their renderer, localized
fallback copy, scroll position, and temporary disclosure state; they do not
reinterpret raw message kinds or import the DOM transcript. Approvals, Plan
choices, and questions remain canonical pending Interactions, are projected
through the shared Interaction-to-Prompt seam, and continue to submit exact
option ids through semantic Engine commands rather than through transcript
rows.
Cross-platform hosts may reuse the DOM-free Composer policy from
@tutti-os/agent-gui/composer-projection. Composer-option loading remains an
AgentSessionEngine semantic operation keyed by the exact Agent Target. Both
Desktop and Mobile call engine.loadComposerOptions; the generated tuttid DTO
is mapped once by @tutti-os/agent-activity-tuttid-adapter into
AgentActivityComposerOptions, while the host extension adapter remains the
transport seam. Hosts render provider-authored options and use the shared
support projection, but keep Native/DOM menus and temporary open state local.
Existing-Session setting changes enter through
engine.updateSessionSettings. The Engine allocates command identity, fixes
timeout and retry policy, and translates the semantic call to its internal
session/settingsUpdateRequested intent. New-Session draft settings travel on
the activation intent instead; provider-independent draft projection and
Desktop-persisted defaults remain surface policy until activation. A renderer
must not call the settings endpoint from a component or invent a
provider-specific settings schema. Desktop and Mobile project the broader
Engine settings through one generated-contract allowlist before composer-option
or existing-Session settings requests. Both preserve supported fields such as
browserUse; neither sends computerUse until OpenAPI adds that request field.
Existing-Session Prompt sends similarly enter through engine.submitPrompt;
Desktop and Mobile provide content plus a stable client submit identity, while
the Engine owns common routing, confirmation expiry, and admission projection.
New- and existing-Session activation enters through engine.activateSession.
The surfaces retain target selection, placement, initial draft projection, and
stable request identity; the Engine derives workspace and timing fields,
projects the typed intent, and reports whether it was admitted. Actual Session
creation, resume, and initial Turn lifecycle remain owned by Agent Host behind
the host effect port.
An activation intent's shared Session settings are not an HTTP create-field
allowlist. Each host must construct a typed
CreateWorkspaceAgentSessionRequest and forward only fields present in the
generated contract. Both hosts preserve browserUse, which is a supported
create field. In contrast, computerUse is a default-on runtime setting but is
not currently a create-request field; neither host may add it as an extra
property. Supporting an explicit first-Turn opt-out requires changing OpenAPI
and the create adapters first.
Desktop and Mobile construct the headless controller through
@tutti-os/agent-gui/conversation-rail-controller and supply its narrow
query/diagnostic runtime port. The shared factory owns the workspace-scoped
cache lifetime while transport adapters own only protocol mapping,
authorization, and host-specific diagnostic context. Hosts that expose the
full AgentGUI mutation surface additionally install the complete query/mutation
cohort from @tutti-os/agent-gui/conversation-rail-runtime. Batch deletion
requires both authoritative section candidate lookup and the batch mutation.
AgentGUI fails that paired capability closed when either method is absent, so
the view cannot expose an action that will resolve to an empty optional-method
path.
Conversation deletion is transactional from the renderer's perspective. The
canonical delete command must succeed before AgentGUI clears the active
Session, changes Rail selection, unactivates runtime state, removes rows, or
discards local drafts. The same rule applies to batch deletion: a protected
Tutti execution conflict leaves the whole selection and local collection
unchanged. The conflict parser recognizes only the daemon's typed
tutti_execution_active payload; archive/view commands remain an optional
host capability until the generated execution adapter is present.
Conversation Rail Activity View
AgentGUIRuntime.conversationActivityViewEnabled is the fail-closed host
boundary for the Conversation Rail Activity View. Desktop opts in explicitly;
external hosts that omit or disable it retain the ordinary Rail unchanged.
The view is a presentation-only activation over the current workspace Engine:
it reads visible root Session summaries, each root Session's own lifecycle
status, root-conversation attention/read state, and already-cached Session
messages. Descendant lifecycle activity remains available in the conversation
detail and interaction surfaces, but it does not change the root row's Rail
status. A pending descendant Interaction still contributes canonical
root-conversation attention so an approval or question cannot disappear with
the filtered child row; submission retains the exact child identity. Opening
Activity View must not call list pagination or transcript hydration.
Its membership input is the canonical mounted Engine summary collection, never
the ordinary Rail's transient runtimeRailConversations overlay; stale page
projections therefore cannot leak into Activity View.
Membership and recency are snapshotted when the view opens; subsequent Engine
pushes reconcile incrementally, while deletion removes a member immediately.
The Activity controller owns that activation and its retained row cache;
React render only consumes the controller snapshot and projects live row facts.
Existing Priority member IDs and their relative order survive ordinary Rail
refreshes that temporarily omit a summary; the controller uses the last known
row summary until the next toggle or an explicit canonical tombstone. Selecting
a historical Session that is temporarily injected into the visible summary
list does not make an idle Session a newly discovered Activity task; only its
live waiting, unread, or active facts can admit it to Priority.
The Engine preserves this distinction at the Turn boundary: turn/upserted
uses live provenance. Historical detail hydration writes canonical Turns with
live: false and cannot create new unread attention; realtime projections and
live reconcile writes use live: true. Omitted live remains compatible with
older hosts and is treated as live, while historical producers must pass
false. Existing durable read and unread markers remain authoritative during
hydration; only a read marker synthesized by the current historical observation
may be upgraded by a later live observation for the same completion.
The activation is scoped by the workspace, authenticated user, rail filter,
AgentGUI node, and Engine identity, not by the currently selected Session's
provider or target; selecting a row must not rebuild a cross-provider Activity
queue.
Search temporarily takes over the content area and clearing search restores the
same activation. Closing the view discards the activation and its retained row
cache. Existing workspace, authenticated-user,
rail-filter, AgentGUI-node, and Engine identities fence the activation
internally; the selected Session's provider and target do not. No Activity
filter or additional host scope contract is introduced. Disabled hosts use an
empty Activity selector and do not scan root Sessions. Activity rows omit the
minute-clock subscription together with their hidden timestamp. The full
product contract is recorded in
docs/specs/2026-08-05-agent-conversation-activity-view-prd.md.
The full first-page query is the only Rail read that resolves a navigation scope and clears its pending state. Targeted section refresh and pagination may update only an already-resolved matching scope. A subordinate result must not cancel the full query, publish partial membership for an unresolved scope, or unlock Rail interactions.
When Activity View has a non-null in-memory projection, that projection remains visible while the unrelated initial Rail membership query is pending; Rail loading, empty, or failure placeholders must not replace it. A refresh or page failure after Rail rows already exist preserves those rows and reports a non-blocking error through the host toast capability, falling back to the UI System toast when that optional capability is absent. An unresolved empty Rail scope retains the centered error state and its retry action.
AgentGUI owns one bounded retry for first-page and targeted membership reads so
every host receives the same recovery behavior across daemon restarts, transient
upstream failures, and transport loss. A fast transient failure retries once
after a short deterministic jitter while publication remains pending. A request
that already reached its timeout publishes the retained or empty failure state,
releases the interaction lock, and performs its one retry after a longer
background delay. Cancellation and permanent authorization, parameter, or other
non-retryable client failures never retry. Scope replacement and surface detach
abort both the active read and any scheduled retry; hosts must propagate the
controller-owned AbortSignal while retaining their own request timeout.
Presentation-invisible Sessions remain canonical engine entities and stay available through exact Session selectors for trusted open, reconcile, and command flows. Plural consumer selectors exclude them before Rail and Message Center collection projection; a hidden Session must not become a list row just because it is resumable or receives later canonical updates.
When runtime sections are enabled, projection unions IDs from the current
section, search, reconciliation, and every in-progress root Session in the
exact target scope, then joins canonical Sessions. In-progress Sessions remain
display overlays until the bounded section query contains them; overlay rows
do not alter cursors, hasMore, or server totals. Unchanged summaries preserve
structural sharing so unrelated engine updates do not rebuild the whole Rail
snapshot.
A locally initiated Session creation projects its exact target identity as
pending-creation reconciliation evidence until the canonical Session arrives.
This covers both new-conversation activation and Session fork, and the marker
is independent from any optimistic or runtime-overlay render source. When that
identity becomes canonical, the Rail controller refreshes the exact persisted
railSectionKey even if the new Session is currently selected. The selected
historical-Session fast path remains valid only when no pending-creation
evidence exists. An active or running overlay must never mask a missing
authoritative section membership that would disappear after the Turn settles.
Scroll, section collapse, visible limits, and search query belong to mounted view scope. Non-search state is isolated by workspaceId + agentTargetId/all; search creates a temporary navigation scope. activeConversationId expresses selection only. Scrolling requires an explicit reveal intent.
On the Home composer, a single-Agent Rail filter follows the effective composer
Agent Target whether the change originates inside AgentGUI or from host-owned
node data. The all filter remains broad, an open Session keeps its current
Rail filter, and unresolved/loading targets neither rewrite presentation state
nor expose placeholder target labels in Home chrome.
Rail scroll memory is captured by scroll events and explicit navigation. Effect cleanup must not synchronously read scrollTop: React may already have dirtied the document, turning that read into a full layout inside the interaction task.
An empty bounded Rail result must not unactivate an active or persisted Session. Only an explicit Agent target selection may move the detail to that target's Home composer.
Contain selection and presentation identity at the Rail boundary. Each section receives the active ID only when it owns the canonical or overlay row; unrelated sections receive null so their memoized props remain equal. Rail pane, section, and row receive a dedicated Rail-label projection whose identity changes for locale changes, not provider-specific detail copy. Event handlers shared by every section keep stable identities and read the current scope and lock state when invoked.
Keep section header/action chrome independent from changing item collections. A memoized header receives scalar presentation fields and stable event-time actions; it must not receive the section object or rebuild project/session semantics. Split the header into narrow render islands. Frequently changing derived booleans such as project drag disabled, project action locked, and batch deletion disabled may cross the Section presentation boundary through separate primitive Context projections. The Rail pane owns those providers outside the memoized Section so a projection-only update does not execute item projection; only the frame, forwarded-ref button leaf, or open menu content that renders the value may consume it. Do not combine those values into one Context object or copy them into persistent state. Menu disclosure is view-local state. A conversation row keeps its context-menu root mounted so right-click remains immediate, but may defer its normally hidden direct actions and dropdown root until the row is first hovered, focused, or opened by context menu. Once activated, those controls stay mounted for stable focus and keyboard behavior. Portaled menu content exists only while that menu is open, and a closed menu has no availability-state consumer. The project header remains the native drag source, each project section updates the insertion position across its full area, and the Rail scroll viewport owns the final drop so section gaps cannot discard an already visible insertion target. This is a presentation boundary, not a second Rail or lifecycle store; stable event-time guards remain authoritative for action delivery.
Relative time uses one renderer-realm minute clock. Timestamp leaves subscribe directly; do not thread a tick prop through Rail pane/section/row and rerender the interactive subtree every minute. Fully occluded AgentGUI surfaces do not subscribe to that minute cadence. They retain the last clock snapshot while hidden and refresh from current time when visual exposure resumes.
4.6 Detail and transcript
Rail selection, detail hydration, older-page loading, and transcript projection are separate states.
Desktop and Mobile use the same renderer-neutral AgentGUI conversation-message controller for focused detail message queries. Initial hydration and latest refresh enter the Engine as Session reconcile intents; explicit older-page loads read only the Engine's authoritative message window, share one in-flight/retry/stale-request fence, and apply mapped durable pages back to the same Engine. Switching the focused Session or pausing the host aborts and generation-fences the obsolete older-page request. A failed request remains retryable at the same cursor.
The host owns mapped transport, foreground/background and disconnected-polling triggers, diagnostics enrichment, scrolling, and rendering. Canonical messages, Turns, Interactions, and optimistic prompts still come from the Engine; neither Desktop view state nor Mobile services keep an older-message entity store. An empty message list means neither hydrated nor not-found. Selecting an already hydrated Session must not start another detail reconcile; the selection controller alone decides whether hydration is missing. Composer option synchronization does not own Session detail reloads. On Desktop the selection controller also owns activation guards and Rail projection coordination before requesting idempotent initial hydration. A repeated automatic selection restoration must not become a forced refresh or append pending demand to the in-flight reconcile; explicit user refresh is a separate intent. The message paging adapter owns only focused message-controller lifetime, transport mapping, diagnostics, and initial/older commands; it does not call back into selection or Rail state. There is no separate messages-only Engine reconcile helper.
Timeline projection is pure, deterministic, and provider-neutral. React views render rows/cards and dispatch actions.
Transcript Turn membership and order come only from timeline items in the
hydrated message window. Session-wide canonical Turn metadata may enrich an
already projected Turn by exact turnId—for example, by adding a view-only
terminal error when the provider emitted no error message—but it must not
create a transcript Turn that has no item in that window. The missing item
normally belongs to an older page; the projection adds the error when that
page is hydrated. A user_prompt Turn that still has no transcript item after
authoritative full hydration is an invariant or data-repair case, not an empty
row to manufacture. Goal and provider-initiated activity without transcript
items use their dedicated presentation surfaces.
Canonical reducers preserve entity and collection references when an accepted
Session snapshot is semantically unchanged, while still merging changed Turns
and Interactions at the same Session version. The legacy activity snapshot
projector preserves each unchanged domain-slice projection so reconcile
bookkeeping or composer-option updates do not rebuild transcript inputs.
Turn elapsed-time presentation starts at the client submission timestamp carried by the canonical user-message payload, not at provider runtime start. Historical activity without that timestamp falls back to the leading user-message timestamp.
Completed Turn disclosure collapses only process-oriented rows such as thinking, tool groups, progress, turn-boundary messages, and transient processing. Ordinary assistant content, user messages, and the response-tail file summary remain visible. The file summary owns the diff panel and stays at the end of its canonical Turn after the final assistant reply.
High-frequency transcript updates must not pair DOM mutation with unconditional synchronous reads of the timeline's full scroll geometry. Conversation switches, explicit submit-to-bottom requests, skeleton transitions, and older-page prepend restoration may perform pre-paint scroll correction.
Transcript end-following is one UI-local state machine shared by DOM, TanStack
Virtual, and React Native adapters. It has only following and detached
modes. User scroll-away intent detaches synchronously, before the first scroll
frame. A mounted detail view retains the scroll anchor and follow-end mode for
each exact Agent Session it visits. First selection follows the end; returning
to a detached Session restores its retained position, while returning to a
following Session stays at the end. This mounted-view memory retains at most 64
recently used Sessions and evicts the least recently used entry beyond that
limit. It expires with the mounted view and never enters navigation, Engine, or
Session state. Prompt submission, an explicit scroll-to-end request, or the
user actually reaching the end may reattach. Content growth, layout effects,
observers, virtualizer geometry, and near-end thresholds are sensors or
executors only; they must not transition the mode.
Turn-level virtualization has one geometry owner. When the transcript is
virtualized and the state machine is following, TanStack Virtual owns append
following, streaming size adjustments, prepend anchoring, end detection, list
height, and item transforms. While detached, AgentGUI disables append
following so TanStack cannot bypass the shared intent owner, but retains
stable-item mutation anchoring so prepending an older page preserves the
visible viewport without reattaching to the end. AgentGUI retains Session
selection, explicit user intent, top-page loading, bottom-dock safe-area
measurement, and the non-virtualized short-transcript branch. It must not apply
native scrollHeight-delta prepend compensation or a content-resize bottom
write after the virtualizer accepts ownership.
The detail view reaches that owner through a narrow controller containing the
exact agentSessionId, isAtEnd, and scrollToEnd. Every read or write checks
the current Session identity; a stale controller is inert. Controller readiness
is reported back to the detail scroll controller so initial end positioning
still runs when the virtualizer acquires its scroll element after the first
detail layout. Virtual measurement keys also include the exact Agent Session
id, so replacing a conversation cannot reuse another Session's measured Turn
height. The virtual list measures its offset from the timeline scroll origin as
scrollMargin, including changes caused by the older-page loading indicator.
Direct DOM transform mode owns the virtual sizer height and item transforms;
React keeps only row content, measurement refs, cross-axis sizing, and
disclosure spacing.
Virtualizer- or layout-driven scroll events do not change the end-following mode or trigger older-page loading without explicit user scroll-away intent. A settled timeline that is too short to fill its viewport may still request older pages.
Composer draft updates cross the view boundary through the stable shell,
rail, detail, composer, interaction, readiness, and operations
projections rather than one aggregate Detail prop. The active Timeline
ScrollArea is a memoized render island and does not consume draft state.
TipTap treats a controlled value as a local acknowledgement only when its
draft scope and local edit revision match; a scope change or other external
replacement may rebuild the document.
A virtualized transcript derives message-locator selection from the virtualizer's measured turn positions and explicit transcript identity. The currently mounted DOM window is rendering output, not a selection source; range changes must not make the locator temporarily select a neighboring message. Recent wheel and keyboard direction fences locator selection from reversing because of estimate-to-measurement scroll compensation or an item-list identity shift; the previous stable locator index is reconciled in a layout effect before paint. A genuine opposite input replaces that intent.
Historical rich text renders from the canonical Tiptap document through a static schema renderer. Only interactive composer surfaces own a Tiptap Editor/ProseMirror EditorView; read-only transcript surfaces reuse the same mention/token presentation without mounting editor lifecycle. Settled transcript messages reuse a bounded cache of pure Markdown ASTs and Tiptap JSON documents keyed by message identity and exact parser input; rendered React elements are never cached, and streaming Markdown bypasses this cache. Conversation titles are a separate plain-text projection: Markdown mention links are normalized to their @label text and never render mention SVGs or interactive rich-text tokens.
Fenced Markdown blocks whose exact language is mermaid use the bundled
Mermaid renderer only in non-inline transcript surfaces. While the owning
message is streaming, AgentGUI renders a stable diagram frame and placeholder;
it does not expose the growing source or repeatedly lay out the diagram. Once
the message settles, AgentGUI renders with Mermaid's strict security mode and
bounded text and edge limits. Active Turn lifecycle is authoritative over an
individual message or tool status: a completed tool must not settle a Mermaid
block while the owning Turn is still active. Oversized, invalid, or failed
diagrams render a compact failure state with an explicit source-copy action;
they do not automatically expose the source in the transcript. A rendered
diagram may open a presentation-local viewer with zoom,
reset, wheel input, and Space-modified pointer panning. Viewer transform,
disclosure, and theme-refresh state are UI-only and must not enter Message,
Turn, Session, activity-runtime, or workspace-engine state. The renderer is
packaged with AgentGUI and performs no runtime network fetch.
Attachment-only fallback labels such as [Image] may provide title or summary
text, but they are not an additional transcript text block when the canonical
structured content already renders the same image. Explicit display prompts
remain transcript content and continue to replace expanded rich prompt text,
except when the structured content carries the composer’s selected-text
reference blocks. In that case the conversation projection preserves ordinary
structured text and images, groups selected-text blocks into one leading typed
compact reference part, and uses displayPrompt only as a compatibility
fallback; the renderer owns the chip presentation. The selected reference is
presentation-only, so copy/edit actions remain attached to the ordinary typed
prompt part. This keeps the durable prompt and editing source unchanged while
preventing a display-only flattened prompt from discarding the reference
boundary.
Structured user-prompt transcript images keep resource acquisition and browser
image decoding as separate presentation states. A URL or hydrated data source
is not proof that pixels are ready: AgentGUI reserves the final thumbnail
geometry and shows its loading treatment until the exact image element reports
load. error replaces that slot with the retryable failure treatment. A retry
of the same URL or a replacement effective source starts a new decode attempt
instead of reusing stale loaded or failed state. These loading and retry states
are UI-local and never enter canonical Message or workspace-engine state.
Standalone hosts may opt a transcript into participant avatars through the
agent-conversation entrypoint's explicit presentation contract. Omitted or
disabled presentation preserves the existing transcript DOM. Enabled
presentation has distinct loading and ready states, so the renderer never
infers identity readiness from a missing image URL. The host supplies user and
Agent names and optional avatar URLs; AgentGUI owns the UI System Avatar,
fixed-size loading slot, fallback initial, and user-right/Agent-left layout.
Participant-header placement is projected from presentation turns rather than
individual message, tool-progress, or canonical Turn rows. A presentation turn
starts at a user message and continues until the next user message, so recovery
or provider continuation may span canonical Turns without duplicating the
participant header. Each speaker renders at most one header per presentation
turn, and a collapsed completed canonical Turn keeps the Agent header on visible
reply content.
This presentation input is view data only and must not enter canonical Session,
Turn, Message, activity-runtime, or workspace-engine state.
Standalone hosts rendering AgentConversationFlow may also pass the current
Agent target presentation catalog through the agent-conversation entrypoint.
The flow scopes that catalog to all historical rich-text rows, so agent-target
and agent-session mentions use the same provider icon and identity
presentation as the full AgentGUI node without copying the catalog into
canonical activity data. Omitting the catalog preserves an inherited AgentGUI
presentation context. The standalone flow also owns its visibility-aware
conversation clock, so a hidden host does not keep elapsed-time presentation
timers active.
Hosts whose action directories omit valid identities supply the complete
presentation-only catalog through mentionAgentDirectory. It retains exact
target IDs and unavailable Agents without making them launchable. Hosts that
omit this capability keep the existing union of provider-rail and handoff
targets. Interactive composer mentions, readonly rich text, and Markdown links
must resolve presentation through the shared target-presentation resolver.
They must not infer icons from target-id formats such as local:*; serialized
mention presentation is only the fallback when the current catalog no longer
contains a historical target.
5. Agent identity and provider architecture
5.1 agentTargetId is UI identity
Use agentTargetId for:
- Agent selection and Rail filtering
- composer-options cache
- Workbench node state
- new-session launch
- Agent mentions and handoff targets
provider is execution metadata, not UI identity. Multiple Agents may share a provider; UI must not group, deduplicate, cache, or fall back by provider.
Ordinary Session selection reads composer options through the existing
target/cwd/settings request cache. It does not force a refresh; force is
reserved for explicit invalidation, completed Session creation, and documented
provider prewarm behavior.
For an active Session, the composer treats agentSessionId, agentTargetId,
and provider as one Engine-owned identity projection. It must not combine a
target id from the canonical Session with a provider from lagging Workbench
node data. If the Engine target is not yet available or belongs to a different
selected Session, composer-option loading waits instead of issuing a guessed
request. The home composer continues to use its selected Agent Target as one
atomic projection.
Trusted host/daemon code resolves a target-backed request through agent_targets, then derives provider and runtime reference. If a client supplies both target and provider, daemon rejects a mismatch.
5.2 Provider strategy
provider ID
-> daemon providerregistry descriptor
-> typed strategy / capability
-> provider-neutral consumer
An unknown provider produces explicit unsupported behavior. Provider adapters normalize their own wire; shared renderers consume canonical message/tool/notice contracts only.
Desktop managed-provider setup reads the generated provider catalog's
statusKind strategy before requesting a provider-specific runtime candidate
catalog. The setup service and view must not keep a second provider-id list or
branch directly on provider names.
Skill invocation follows the same boundary. Filesystem and runtime adapters
discover skill identity, source, and plugin ownership; providerregistry
projects the provider-authored trigger and invocation strategy. Composer and
host adapters consume that projection and must not rebuild $ versus /,
plugin namespaces, or prompt-item versus text-trigger behavior from provider
names.
For an active Session, composer Skill discovery also scans that Session's
isolated provider home. Opening slash search explicitly refreshes this catalog
so newly created Skills appear without restarting the Session; runtime-managed
Skills remain hidden through their .tutti-managed-skill marker.
App-server-backed skill discovery follows the descriptor boundary. Tutti Agent
requests only skills/list and retains the ordinary Skill projection through
the shared app-server transport, capability contract, cache, and structured
prompt-item submission path.
Model-only app-server catalog probes are a separate read path: they request
model/list without skills/list and pass the runtime launch preparation
contract's SkipSkills flag. These probes do not create a live Session and
must not resolve or materialize provider Skill directories. A Composer-options
read that returns both models and Skills keeps the normal Skill preparation
path.
Tutti Desktop's slash connector section is a local catalog projection,
not a Provider connector catalog. services/tuttid/service/agent reads the
daemon-owned connector-market application through its read-only snapshot port;
the Agent service never opens or receives the underlying repository directly.
It projects manifest presentation (including the catalog icon) plus installation,
authorization, and compatibility into a provider-neutral capability option, and
replaces any Provider-reported connector capabilities before returning composer
options. This keeps installable connectors visible in the slash menu while
ensuring that only installed and authorized connectors can be invoked. AgentGUI
does not scan external MCP, plugin, or package-manager configuration and does not
infer installation from a remote market response.
Authorization and draft selection are separate facts. Selected connectors are
stored as semantic draft blocks, rendered as removable chips, and submitted as
structured prompt content without synthesizing slash text. The compact Composer
trigger previews installed and authorized connectors independently from draft
selection, while preserving selected connectors first in that bounded preview.
A matching send on a session-scoped draft keeps those connector blocks and
clears the rest of the draft; home and other non-session scopes clear the
full draft. /clear keeps the session enable set. A new conversation starts
empty. The GUI submits the full enable set every turn; the daemon remembers
the last announced set and injects only the delta into the provider turn.
The device-global lab.connectors UI-preference flag controls whether that
projection is returned. The daemon fails closed when the preference is absent
or unreadable and removes Provider-reported connector entries as well as local
ones. Desktop invalidates cached Composer Options when the preference changes,
publishes the same invalidation through the AgentGUI host event bus, and
projects the host capability into the palette so stale cached connector rows
are hidden immediately. Workspace and standalone AgentGUI surfaces therefore
converge on the next canonical read without exposing stale entries. The flag
does not uninstall connectors, stop their runtimes, or reject an already
structured connector prompt.
Desktop also projects these flags through AgentGUI's existing host-owned
capability-menu state. The footer renders the Connectors menu only when
lab.connectors is on. The independent lab.tuttiMode flag defaults off; when
enabled, the same footer renders the Tutti Mode activation switch and the slash
palette exposes /tutti, including when Connectors are also enabled. When the
flag is off, both Tutti Mode entry points are omitted. The same footer serves
both the home hero and existing-session dock, so the two AgentGUI contexts
cannot drift. The one-time Desktop Preferences migration resets any persisted
lab.tuttiMode value to false; its durable migration marker preserves later
user opt-ins instead of resetting the preference on every launch.
The menu, selection-chip, and Palette-item implementations plus their neutral
item contracts belong to @tutti-os/connector-renderer/ui. AgentGUI owns only
the React-free capability projection under integrations/connector, Composer
placement, draft/prompt semantics, canonical option refresh request, and Tutti
Mode fallback. Its existing onCapabilitySettingsRequest host port emits the
exact connector key; the host delegates that intent to Connector Market's
shared open use case and mounts one window-level Connector Market dialog host.
AgentGUI does not load Connector Market state, construct its Root, or implement
Connector-specific visuals.
5.3 Agent Directory and setup
The host provides a complete, ordered Agent Directory with this load lifecycle:
idle | loading | ready | error
ready may contain an authoritative empty list. error may retain the last successful snapshot. Components must not infer loading from agents.length.
One-shot Desktop surfaces that emit Agent identity, such as outcome notifications, start and await their own Agent Directory load attempt before resolving presentation. They must not depend on an AgentGUI or Workbench React effect to initialize the directory.
The provider descriptor's target sort order owns the default built-in Agent order. Tutti Agent is the first built-in target; an explicit device-local Provider Rail reorder remains a presentation preference and takes precedence over that default.
Tutti Agent is also the first Desktop default Provider for ambiguous new entries. An exact persisted Agent Target or an explicit launch request still takes precedence. AgentGUI and launch surfaces that require readiness fall back through the authoritative ready Agent Directory when that default is unavailable.
The directory owns Agent presentation. agents[].iconUrl is the primary
presentation asset used by conversation identity, Message Center, mentions,
and the empty-home carousel and Provider Rail. It is decorative metadata:
an Agent with an exact agentTargetId and name remains selectable when the
icon is absent. maskIconUrl may supply the monochrome conversation-row glyph.
Desktop Workspace Agent projections first inherit the resolved icon of their
Harness target by exact target ID, then use the provider/icon catalog fallback.
Host projections preserve these roles independently and do not create
provider-specific renderer catalogs.
AgentGUI accepts separate directory projections for separate responsibilities:
agentDirectory owns the current runtime rail, handoffAgentDirectory owns
launch choices, and optional mentionAgentDirectory owns exact rendered
identity. The mention directory may include offline or otherwise unavailable
Agents and must not participate in selection, admission, setup, or handoff.
This keeps availability as action state instead of deleting identity metadata.
Agent presentation images decoded for a canvas-backed texture must use
anonymous CORS loading before assigning src. Any host-owned custom protocol
that serves those images must return an Access-Control-Allow-Origin response
header so the decoded image cannot taint the texture canvas.
When the Desktop host projects built-in Agent mentions into a workspace app, it replaces host-local file URLs with bounded 64px WebP data URLs. The external bridge is the serialization owner: workspace apps must not read host paths, register an Electron-only asset protocol, or re-encode the icon. Remote and already-inline extension icons retain their authoritative URL.
Handoff target menus are an AgentGUI presentation contract. The shared
AgentHandoffMenu renders exact agentTargetId rows, ownership metadata, and
optional host-resolved ownerDeviceLabel metadata directly from the same
target, plus temporary disclosure/icon-motion state; a host supplies its
authoritative ready target projection and retains launch orchestration in
onSelect. Host surfaces must not reconstruct a second handoff row model,
observe the portaled menu DOM, or infer target identity from provider or
visible text. The current conversation's composer input availability is
independent from this launch surface: a target connection may disable input
while Handoff remains available when the host supplies the launch callback.
Handoff preserves canonical Rail placement: a source in the Chats section
omits its runtime cwd from the destination project selection, while a source
in a project section carries that project's path.
AgentGUI DOM surfaces render the target's owner badge through the shared UI
System Avatar primitive. A failed owner image falls back to the supplied owner
label initial instead of exposing a browser broken-image glyph.
For a signed Agent Extension, package icon is the primary identity and
optional package maskIcon is the conversation-row glyph. All assets remain
pinned to the verified active installation.
Target-managed setup uses exact agentTargetId; daemon persists its state and actions. Setup gates only the empty new-conversation surface. Active/history conversations follow host-projected Session runtime availability for exact-target capability and transport reachability. A blocked Session runtime disables both composer editing and submit until the host reports the Session available again.
For a built-in Runtime that is not installed or still requires authentication,
the Desktop readiness projection may additionally declare the host-owned
onModelPlanSetup action. AgentGUI then keeps the ordinary install/login
action and offers a secondary route to the host's Model Plan settings. Desktop
derives the capability from the provider descriptor's Model Plan protocol and
injects its managed-model settings route; shared AgentGUI does not name or open
that product destination. Unknown or unsupported providers omit it and keep the
single setup action. The capability is fail-closed: non-Tutti hosts, including
TSH/VN, omit it and never render the Model Plan route when they consume the
shared AgentGUI package. This route is navigation only: it does not rewrite
readiness, create a Session, or bypass activation-time Model Plan validation.
Provider-declared terminal authentication remains a Host capability, not React
or Session lifecycle. AgentGUI's target-setup controller owns the local
idle/waiting/error projection and terminal handle, while the Desktop host
launches the workspace terminal and monitors the authoritative target-setup
watch until it reports ready or the bounded wait expires. The view only renders
that controller state and dispatches start/cancel intent; it must not add its
own effect or timer polling loop, raise the AgentGUI degradation baseline, or
infer authentication from terminal output.
The built-in managed-environment wizard and Agent Extension setup have different owners. Shared UI must not combine their lifecycles by provider name.
See Agent Extensions for the detailed setup contract.
6. Agent GUI composition
6.1 UI chain
AgentGUI
-> AgentGUINode shell
-> useAgentGUINodeController
-> { viewModel, actions }
-> AgentGUINodeView
-> shared conversation components
Code uses stable horizontal layers and behavior-oriented vertical modules:
- shell: host/runtime/i18n/layout composition
- controller: selector binding, UI-local state, typed command dispatch
- model/projection: pure derivation
- view: DOM, focus, scroll, animation, event wiring
- vertical module: navigation, composer, timeline, Interaction, readiness, Goal, files/mentions
A controller may compose flows but cannot become a second lifecycle state machine. Extract complete behavior first; do not scatter it into a pile of domainless helpers.
Activation and existing-Session submit share a canonical prompt envelope. Submit eligibility includes text and renderable structured content; an individual composer does not redefine it.
@tutti-os/agent-gui/quick-composer is the draft-only public entry for
launch surfaces that need the canonical DOM Composer without a Rail or
timeline. It may present rich text, image draft blocks, and an exact Agent
Target selector, but it owns no Session, Turn, option-loading, or recovery
state. The host receives its typed prompt envelope and must route new Session
creation through the workspace's existing AgentSessionEngine; the entry must
never construct a second Engine or call a lifecycle transport.
The embedding host owns target readiness and capability loading. It passes the
canonical agentTargetId plus a capability snapshot for each selectable
target. Quick Composer resolves only the exact identifier and fails closed for
unknown, disabled, or capability-less targets; it never falls back to array
position, legacy targetId, or an inferred provider. Structured image content
is accepted only when the selected target declares image support. Submit emits
the resolved target identity with the prompt envelope, preserving the target
the user actually selected across the host activation boundary.
The Quick Composer public target omits legacy targetId and AgentGUI-internal
ref; the adapter derives both from canonical agentTargetId for the shared
Composer VM. Provider-menu selection therefore cannot cross or collide with a
second identifier namespace.
When an embedding host supports pre-session settings, it passes one explicit
settings capability containing authoritative AgentActivityComposerOptions,
a controlled sparse settings draft, its change callback, and option-loading
state for the exact target and project. Omitting any persistence path means
omitting the whole capability, so Quick Composer fails closed instead of
rendering interactive no-op controls. It projects supported values into the
same model, reasoning, speed, permission, plan, and browser controls rendered
by the full Composer; it does not maintain a second menu implementation or
fetch options itself. computerUse and gated codexSaverMode remain hidden
until an embedding activation seam preserves and authorizes them end to end. A
settings change returns a typed patch to the host, which refreshes authority
and forwards the settled draft through its existing activation command. Only
the very first options load for a target locks settings controls and submit;
once a good options snapshot exists, background refreshes keep every control
interactive and a failed refresh keeps the last good snapshot instead of
blanking the menus.
@tutti-os/agent-gui/composer-settings-core is the host-agnostic policy for
that settings capability. ComposerSettingsCore is a headless controller
(subscribe/getSnapshot, no React, no Engine dependency) that owns the
sparse settings draft, a revision-fenced options lifecycle with last-good
retention, defaults seeding via the daemon's defaults-merged
effectiveSettings, and trailing-edge write-back of explicit picks into the
canonical per-target composer-defaults ledger through injected ports
(fetchOptions, rememberDefaults). resolveSubmitSettings() returns the
exact values the composer displays; hosts must submit them verbatim so the
daemon never re-interprets empty fields against another surface's memory. A
Quick Composer host embeds this core instead of hand-rolling fencing,
failure semantics, or a parallel settings store; the desktop capture window
is the first adopter.
The Quick Composer uses the Composer's embedded layout contract. Unlike
dock, which intentionally grows attachments and long drafts upward over a
conversation timeline, embedded keeps the entire draft in normal document
flow. Compact host surfaces must select that layout instead of compensating for
dock overhang with consumer-specific offsets or clipping.
Embedding hosts may inject the same RichTextMentionService and workspace
reference-picker callback used by a full AgentGUI surface. Quick Composer wraps
those inputs in the canonical mention-service boundary and delegates reference
insertion to AgentComposer; it does not own a parallel mention catalog or
picker. A constrained host may also declare a top viewport inset for portaled
menus. Provider Select collision padding and mention-palette geometry must
honor that inset so host chrome is never treated as usable menu space.
An embedding host may also opt into the canonical project selector with a
controlled selected path and an explicit WorkspaceUserProjectApi. That
capability supplies the real registered-project catalog, selection preparation,
native directory choice, and project registration. A native directory callback
alone is insufficient: Quick Composer does not invent a project registry or
Session state, so it hides the project selector when no real project capability
is available. The host carries the resulting path through its existing
activation command as cwd.
AgentGUI's standalone locale runtime includes the scoped defaults of package
surfaces it mounts, including workspace-user-project. A host-supplied app
runtime may override those keys, but a standalone Quick Composer must never
render an unresolved scoped key as user-visible copy.
Quick Composer also establishes the canonical AgentGUI semantic-token scope,
so embedded placeholder, foreground, border, and menu colors never fall back
to inherited host text styles. Hosts may provide a compact action accessory;
AgentComposer renders it immediately before the primary send/stop action in
non-hero layouts instead of requiring consumer CSS to position controls over
the Composer DOM. An embedding host with a definite block size may opt into
fillAvailableHeight; the embedded Composer then gives its input group and
each rich-text editor wrapper the remaining height while keeping attachment and
footer rows intrinsic. The entire assigned editor block remains a native
contenteditable hit target instead of relying on a host-level click-to-focus
proxy. This remains explicit so ordinary launch surfaces keep the default
content-sized behavior.
An embedded host may also choose composerActionPlacement="footer". In that
mode the same primary action cluster (including an optional host accessory)
joins the canonical Composer footer instead of the prompt row. This keeps the
reference controls, Agent selector, host modifier, and send button on one
alignment baseline without host positioning CSS; the default remains input
for existing consumers.
The canonical Composer gate belongs to the Session-presentation projection and travels through the Composer view-model slice as one object. View-local transition or workflow locks may layer on top as explicit presentation locks; they do not copy or reinterpret runtime, connection, provider, ownership, or queue readiness.
The conversation composer area is a stable AgentComposerRegion with explicit
floating-control, lifted-interaction, accessory, and primary-composer slots.
Workflow features compose through the accessory slot instead of adding another
bottom-dock layout or reaching into composer DOM. A disclosure accessory owns
only presentation and expanded state; its domain controller owns phase
selection and commands.
Tutti plan review and Issue execution project into one
composer-anchored TuttiWorkflowDock. The same banner shell expands upward and
moves through review, UI-local materializing, execution, or error;
there is no top-level Plan/Task tab and no duplicate full plan or Issue card in
the conversation timeline. Accepting a plan stores only the checkpoint identity
and title needed to bridge the read-model handoff, not an accepted-plan copy.
That UI-local marker is scoped by exact Session, workflow, and checkpoint, and
is cleared when the matching Issue or materialization failure arrives, the
decision fails, or a newer actionable checkpoint supersedes it. It must not
survive a completed handoff and later turn an ordinary Session re-entry into a
false materializing phase. During a genuine handoff the draft remains editable
while submit is disabled. The arrival of a newer actionable plan takes
presentation priority over an existing Issue. Each newly identified actionable
review starts expanded once; an explicit user collapse remains authoritative
for that review, while the disclosure's current expanded state survives
materialization and execution handoffs so the UI does not jump. The expanded
plan panel starts with the plan title and body; it does not repeat mode,
review-kind, or pending-state badges already communicated by the workflow
banner.
The reviewed document's effect and speed are immutable plan inputs, while the
Composer preferences remain mutable for the next Turn. A change is proven only
when the document carries both explicit frozen values and either differs from
the current Composer value; legacy documents with a missing pair never infer a
change from reasoning or orchestration fields. With a proven change, the empty
Composer shows Request changes beside Accept; without one, it shows only
Accept. Accept always targets the checkpoint bound to the currently projected
revision and therefore executes exactly the visible plan. The daemon rejects a
checkpoint that is no longer bound to the workflow's current revision, so a
stale render cannot execute replacement content. Typed feedback continues to
request changes and includes the current preferences when they diverge.
Task-assignment directories and target option catalogs are workspace query projections, not Plan, Session, or Turn state. The Desktop assignment source retains them in the shared bounded workspace query cache: directories are keyed by workspace, while target options use exact workspace, Agent Target, and provider identity. AgentGUI may synchronously reuse the last successful value, including a stale value, while the source performs a deduplicated refresh. Provider catalog and workspace model-configuration events invalidate only the affected target entries; a generation fence prevents a pre-invalidation response from becoming current. Composer options remain a separate cwd/settings-sensitive projection and do not share this cache instance.
The materialized Issue is also a Tutti-owned aggregate-work projection for the source composer. While dispatch is not paused and any task is nonterminal, an empty composer shows the normal running Stop control even when the source Session has no active Turn; this is presentation state and must not manufacture an Agent Turn or change Host submit availability. Typed input replaces that aggregate Stop with Send. Sending from the ordinary composer always uses the normal source-conversation submit path, including while the Issue and source Turn are active; canonical busy-session availability therefore keeps the prompt in the normal queue. Provider guidance capability only enables an explicit guidance action and never changes ordinary Send semantics. Stop durably pauses Issue dispatch and cancels its running task Sessions, and also sends the ordinary source-Session stop only when that Session has stoppable work. The two idempotent paths are independent because canceling an idle Session merely to trigger the Issue cascade could capture a later Turn.
Task-level accept and rework controls in that projection prepare localized instructions in the exact source Session composer; they preserve any existing draft and never send automatically. They do not call generic Issue Task mutations or impersonate the source Agent's CLI authority. Once the user sends the draft, the normal source-conversation submit path applies and the Agent inspects the canonical Tutti execution before issuing checkpoint- and revision-fenced commands. Other plan-panel interactions should use this prompt-action pattern only when their intended effect requires source-Agent judgment; navigation and existing daemon-owned commands remain direct semantic actions.
An independent-review failure is a distinct workflow presentation, not a generic retry loop. The dock may offer an explicitly user-triggered self-review fallback through an optional host command and shows its pending, success, or failure result together with the audit identifier. It never switches review mode automatically.
An active Tutti Mode composer badge is a preference-settings entry, not a
destructive toggle. Clicking it opens the UI-local TuttiBudgetPopover, seeded
from the engine-projected effect and speed preferences. Every slider
movement sends its selected value directly through the existing Tutti Mode
activation command -- there is no draft/confirm step. Turning Tutti Mode off
remains a separate adjacent action, and all controls stay disabled while an
activation update is unresolved. The Desktop command host and HTTP adapter must
preserve the optimistic CAS revision and both optional preferences; dropping
any field turns a valid UI intent into a stale or semantically mismatched
response. Tutti Desktop advertises the Tutti Mode host capability only while
the default-off lab.tuttiMode preference is enabled. Toggling the preference
changes presentation without mutating an existing session's durable activation
or workflow state.
The preference popup uses two independent 0-100 sliders. effect raises the
minimum model capability and task-verification breadth. speed asks the
planning Agent to choose the fastest model that still satisfies that effect
floor and maps into an upper parallel target: 0-24 -> 1, 25-49 -> 2,
50-74 -> 3, and 75-100 -> 4. They are combined, never averaged: high effect
plus high speed means "fastest suitable powerful model" with a target of up to
four parallel Agents, not a mid-tier model.
The popup previews a short model strategy and that parallel target. The target is not a concurrency promise: the planning Agent may shape real independent workstreams toward it, while actual scheduling remains bounded by dependencies, ownership, safe isolation, ready work, budget, and the workspace-wide four-Run ceiling. Exact models still come from the live composer catalog. Effect-scaled verification remains planning policy but is not shown as a primary preview metric.
Plan review reads the additive execution.effect and execution.speed
snapshots from current tutti-mode-plan/v1 documents. A legacy v1 document
without those fields maps its single orchestrationIntensity value to effect
and uses the balanced speed default. The existing execution fields keep their
original Issue semantics; AgentGUI must not reinterpret
orchestrationIntensity as speed.
Home-composer project state distinguishes an unresolved durable default from an explicit selection whose path may be null. The project selector may apply the durable default only while that intent is unresolved. Entering the unscoped conversation section resolves the intent to no project, so remounting the hero composer or refreshing the project list cannot restore a previous project.
New-Session worktree launch is a fail-closed Host capability, not Session
lifecycle state in React. The host opts in with
sessionWorktreeEnabled, projects a launch preference map for the current
workspace keyed by the canonical project sectionKey, persists explicit
changes through onSessionLaunchModePreferenceChange, and supplies the exact
project-path probe through
AgentHostApi.workspace.resolveSessionWorktreeSupport. AgentGUI renders the
selector only when all four pieces exist, the selected target is explicitly
self-owned, a real project path and section key are selected, and the probe
succeeds. Existing Sessions and shared/remote targets never expose it.
Tutti Desktop carries the exact agentTargetId and project path through the
generated daemon support contract. Tuttid resolves that target through its
local launch authority and fails closed for shared-Agent identities. The
Session create boundary repeats the same target admission before allocating a
worktree and also requires a project railPlacement with a non-empty logical
project path. An unscoped conversations Session therefore cannot acquire
worktree isolation even through a direct API request, so React visibility is
presentation defense rather than the business authorization boundary.
The adapter records the selected repository-relative cwd and remaps it under
the isolated checkout, so a registered project below the repository root keeps
the same working-directory scope. A retry with the same Workspace, Session,
repository, and relative cwd reuses its exact metadata-backed checkout before
entering Host idempotency; a mismatched identity fails closed.
The stored local | worktree value records future launch intent; canonical
Session isolation records what actually launched. Probe failure, temporary
repository incompatibility, or a missing host capability makes the effective
mode local without overwriting a saved worktree intent. Visiting an old
Session also cannot write that preference. Tutti Desktop persists the nested
workspaceId -> project.sectionKey -> mode map in daemon-backed desktop
preferences and enables the capability. Each change uses the daemon-owned
preferences.agent.session.launch.mode.patch.requested workspace/project patch;
its SQLite transaction merges against the latest map, and full
desktop-preference updates preserve that field, so concurrent windows cannot
replace one another's choices. Renderer write failures roll back only
the still-current optimistic value and are consumed through the Workbench
diagnostic path. Published AgentGUI defaults the capability off so other hosts
may opt in independently. The composer only adds isolation: "worktree" to the
admitted activation input. Tutti's daemon adapter owns the filesystem checkout
and provider-preparation cwd, while Host continues to own the idempotent Session
create. The canonical Session projection later carries the created worktree
path, branch, and base commit. Rail rows render the worktree glyph from that
canonical isolation projection next to relative time in the unhovered row and
never infer it from cwd.
A locked Session cwd existence check is UI-local observation, not Session truth. AgentGUI starts it only after pending creation has resolved, scopes its result to the normalized selected path, and discards callbacks from a previous path. Switching between Sessions that share the same cwd must not repeat the same mounted probe. A host probe failure leaves existence unknown; only a successful check that confirms absence may render missing-project chrome.
The empty-home carousel may measure its placeholder synchronously when live alignment first activates. Later React updates coalesce alignment into the next animation frame; ResizeObserver and MutationObserver keep layout roots current.
Composer text transactions may publish the current draft, but the draft value must not drive synchronous pre-paint geometry reads or an urgent AgentGUI tree render. The rich-text editor DOM owns the urgent input transaction. The latest prompt ref updates synchronously for submit and attachment reconciliation, while palette and controlled-draft projections publish in a React transition. The editor recognizes stale controlled echoes from that transition so an older projection cannot overwrite newer local input; a value not emitted locally remains an authoritative external replacement.
An existing-Session composer derives input history from that Session's
canonical user-message projection and its turnless Goal-control audit entries;
it does not persist a second history store. These sources are merged by their
timeline timestamps so Goal commands participate in the same Up/Down sequence
as ordinary prompts.
The host capability remains explicit so unsupported hosts can fail closed, but
Tutti Desktop always supplies sessionInputHistoryEnabled: true; historical
lab.agentInputHistory preference values do not hide or disable the feature.
Composer model recents and favorites are separate browser-local menu chrome.
The Composer presentation projects the exact Agent Target identity together
with narrow provider-native catalog testimony, and one focused controller owns
storage reads, writes, cross-window refresh, and reconciliation. An unresolved
active-Session target disables history instead of reading or writing a shared
fallback bucket. Only an authoritative, settled native catalog may retire a
recent model; loading, empty catalogs, requested-origin entries, and
selected-model-only echoes remain unverifiable. Favorites preserve explicit
user intent even when a model is currently unavailable. The legacy shared
default bucket migrates lazily to the first exact target: recents pass through
current authoritative testimony before migration, while favorites migrate
without availability filtering.
Quick Composer projects the same history identity and testimony from its exact
selected target and host-owned options capability. Host-level option loading
keeps that testimony unsettled, so a retained last-good catalog cannot retire
history while the embedding host is refreshing or changing targets.
Bare Up/Down recalls older/newer structured drafts only from an empty composer or an unchanged recalled entry, and only when the collapsed caret is at a whole-document boundary. Palette handling and IME composition take precedence, while editing a recalled draft exits history navigation. Moving past the newest entry clears the composer. Adjacent equivalent submissions collapse, persisted attachments are restored with exact workspace and Session identity, and reaching the oldest loaded entry requests the existing authoritative older-page read while preserving the timeline prepend scroll anchor. The history cursor is UI-local and resets when the draft Session scope changes; Home has no Session history.
The dock observes geometry through one coalesced animation-frame measurement
entry point. Editor document updates, attachment membership or intrinsic
attachment size changes, and changes to the stable input-shell width may
invalidate that entry point.
ResizeObserver must not observe the animated input area or editor block size:
their height transition is an output of measurement and must never feed back
into another measurement cycle. Width observations compare inline size before
invalidating, while attachment observers cover asynchronous chip and preview
sizes without a duplicate global resize listener.
The measurement pass is read-only. It derives natural text height from the editor document blocks, reads attachment heights, and publishes one atomic composer-metrics snapshot only when the snapshot changes. It must not temporarily collapse or restyle either the editor or transitioned input container. The dock input area establishes a local layout-containment boundary so a composer read cannot invalidate the conversation root. Composer paragraphs and the viewport calculation share the same line-height token so the 3.5-line cap remains exact. Regression coverage must expand across explicit newline rows, delete back to one row, and verify stable action-button placement.
Dynamic Agent surfaces must not use :has() on .workbench-window, the
timeline, or transcript rows. Composer and streaming DOM mutations would make
those relational subjects candidates for subtree style invalidation. Window
header layout and render-error state are projected onto the owning window or
header; message footer, speaker, thinking-edge, and row-kind state are projected
onto the owning message flow or transcript row. Small, self-contained controls
may still use local relational selectors when their subject and mutation scope
are bounded.
Composer mention providers own entity presentation metadata, including an
optional iconUrl, through their insertion result. AgentGUI mention
projections must preserve that metadata for every supported entity kind, and
the shared mention row renders the icon only when supplied. A provider-level
icon assertion is not sufficient coverage: presentation changes require both a
projection assertion and a consuming-row DOM assertion so an intermediate
view-model cannot silently discard the icon.
Composer copy and cut write both the canonical prompt Markdown as
text/plain and schema-serialized mention markup as text/html. Pasting that
markup reparses it through the AgentGUI editor schema, so built-in and
currently registered custom mentions retain their canonical href and chip
identity without a host DOM event interceptor.
External OS file paste and drop enter one host-injected classification boundary before draft attachment creation. The synchronous resolveExternalPromptEntries port classifies each source index as a live WorkspaceFileReference or a snapshot requiring preparation. AgentGUI owns ordered mention insertion and draft reconciliation: references become ordinary file/folder mentions and never consume prompt-asset slots, while only prepare entries create pending attachment state and enter prepareExternalPromptFiles. A host without the resolver prepares every external entry. The preparer owns native-path or byte lookup, size enforcement, persistence, and remote transport; each prepared input has one sourceIndex result, one failure must not fail siblings, successful results include a provider-readable path or url, and failures carry typed error codes. Hosts that classify path-backed entries as references must reject any such entry that unexpectedly reaches preparation, so classification failure cannot silently create a duplicate snapshot.
Plain-text absolute path paste may enter an optional host-owned
workspace.resolvePastedPath boundary. AgentGUI applies only a strict sync
candidate gate (one trimmed absolute path with no whitespace or quotes). The
host returns a WorkspaceFileReference to insert as a file/folder mention, or
null/rejection to fall back to plain text. Hosts that omit the callback keep
the previous plain-text paste path; existence checks, host-namespace
projection, and Shared-Agent policy remain host-owned.
Workspace picker results and internal workspace-reference drags remain live references. They enter the rich-text document as mentions and never pass through external-file preparation. A picker source whose selected locator is not yet consumer-readable may perform source-owned confirmation preparation before the mention is inserted; the picker waits in a loading state, publishes no partial result on failure, and remains open for retry. This confirmation transaction belongs to the reference source contract and is distinct from the external OS file preparation pipeline. Removing an inline external-file mention removes its draft intent; a later async result must not revive it or lose its error reason when the draft is in another scope.
A host may map a reference-source content error to a labeled recovery action through the optional workspace contract. AgentGUI passes that policy through to the shared picker without copying error state. The picker owns centered error presentation and retries the failed browse or search; the host remains the authority for deciding which structured errors can request authorization or otherwise recover interactively.
6.2 Public node contract
AgentGUINodeProps groups fields by semantic responsibility:
| Object | Responsibility |
|---|---|
identity |
node, workspace, user, title identity |
workspace |
path, reference, project, Agent settings |
frame |
position, size, visibility, embedding |
state |
persisted Agent GUI node data |
runtimeRequests |
focus, launch, prefill, probe requests |
hostCapabilities |
host catalog, readiness, menus, icons |
hostActions |
host mutations, Workbench/window actions |
renderSlots |
narrow product-neutral presentation slots |
AgentToolBrowserPanel owns its BrowserNode feature, surface identity, and
tab state. It also owns the single browser chrome instance for that surface.
Its controller may activate an existing page by URL inside that one surface;
the product Host remains responsible for choosing among Browser surfaces and
focusing the owning top-level node or window.
Desktop mode adapters register the same semantic Terminal, Browser, and Files
launch coordinators in each renderer. A Workspace adapter presents through its
Workbench host; a standalone Agent adapter presents through the right-side tool
sidebar and waits for the exact mounted controller when necessary. The
standalone AgentGUI surface.host represents the Agent node and must not be
used as a tool-launch host. After a canonical Turn is created, its originating
renderer claims the Browser presentation role through Electron main; Browser
automation then selects or creates that exact Agent or Workspace surface.
Automation may wait briefly when a provider tool request races ahead of that
claim. Unclaimed legacy Turns retain the user/Workspace fallback. Host
readiness must not be used to infer Turn origin. These tools remain Desktop
chrome and do not enter AgentGUI lifecycle state.
Hosts compose window controls into the tab strip through defaultActions,
pass draggable-header semantics through dragHandleProps, and use
navigationActions for address-row actions. A host must not wrap the panel in
a second visible title bar or recreate the browser header outside the panel.
Host-issued runtimeRequests.composerAppend values are one-shot requests. An
append may carry prompt text, prepared files, or one semantic connector key.
AgentGUI waits until the exact requested Session is the active conversation,
applies the append once to the structured draft, and then calls
hostActions.onComposerAppendHandled(sequence). A Host that retains routed
requests must clear only the acknowledged sequence; it must not let an older
open-Session append mask a newer request.
Collaborative Hosts may supply
hostCapabilities.interactionReadinessSource for exact pending Interaction
write admission. The source is keyed by
(workspaceId, agentSessionId, turnId, requestId) and exposes only
ready or blocked(synchronizing | owner_offline | binding_revoked).
AgentGUI does not compose presence, transport, or lifecycle facts. When the
capability exists, a missing exact record fails closed as synchronizing;
omitting the capability preserves ordinary local-Host behavior. AgentGUI reads
the same source for presentation and again at the event-time submission
boundary. This readiness is ephemeral Host policy and must not enter canonical
Session, Turn, Interaction, or Engine state.
While an exact approval or server-projected interactive prompt is presented, this capability owns its transport chrome. Target connection and observation gap sources still govern target discovery, ordinary composer writes, and sessions without an exact Interaction, but they must not override an exact ready or blocked result. Synchronizing and owner-offline results keep the pending card visible while disabling its actions; binding revocation is the terminal exception.
If an approval and a server-projected prompt coexist, AgentGUI reads readiness for both exact identities. Each surface consumes only its own admission result; selecting one as the active prompt must not reuse its readiness for the sibling Interaction.
Do not restore flat compatibility props or hide workflow inside a render slot.
The optional renderSlots.projectDirectoryPickerHeaderActions slot is limited
to host presentation beside the directory picker's title. AgentGUI owns picker
state and supplies refresh plus source-located target selection after host
mutations; the host owns the action's side effects and must not duplicate
picker navigation or selection state.
Hosts that render capabilities owned by another device set
hostCapabilities.capabilityControlsReadOnly; AgentGUI keeps owner-supported
Browser/Computer entries visible but disables their mutation and setup actions.
Unsupported capabilities remain absent according to the authoritative composer
capability descriptor. A caller host must not open its local device settings as
a fallback for a remote owner.
Account and Commerce UI is Host chrome rendered through
@tutti-os/commerce/react; it is not an AgentGUI node capability. AgentGUI
accepts only generic hostCapabilities.visibleErrorPresentationOverrides for
structured product errors. The host owns Account/Commerce requests,
login/logout, external navigation, reward receipt persistence, clipboard
writes, notifications, localization, feature gating, and menu lifecycle.
Neither AgentGUI nor the frontend Commerce package may receive a Cookie or
start a Commerce request.
For a shared Agent conversation, the Host sets the presentation-only
hostCapabilities.visibleErrorPresentationScope to shared_caller.
AgentGUI preserves the canonical structured reason and existing diagnostic
disclosure, substitutes caller-safe contact guidance only when the structured
code identifies an Owner-remediable failure, and suppresses current-device and
current-account remediation actions. Transport and ambiguous failure copy stays
neutral and retry-oriented. Omitting the scope preserves local_owner
behavior. The scope must not enter Turn, activity, Engine, event, persistence,
or provider contracts.
The optional renderSlots.agentConfigAccount is a presentation-only Host
chrome seam for the exact selected Agent Target. Its paired
hostActions.onAgentConfigMenuOpen notification lets the Host refresh account
state without hiding workflow in the render slot. Returning no renderable
content keeps the provider account and quota block unchanged. Returning the
package-owned AgentGUIConfigAccountFallbackSuppressed marker suppresses those
generic fallback rows without adding Host content.
Tutti Desktop fills this seam only for its self-owned local Tutti Agent target.
The Desktop Account service remains the source of account, membership, credit,
and Commerce-link state; opening the target menu asks that service to refresh,
and the render slot stays request-free. Its compact Agent menu omits the
redundant Tutti account identity row while retaining credit, membership, and
account-center actions. A signed-out local Tutti Agent returns the suppression
marker because the generic provider-account and quota rows do not describe its
Host-owned account model. Other providers return null and retain AgentGUI's
default provider-account and quota presentation.
The optional renderSlots.agentTargetInfo seam enriches the exact target icon
in the provider Rail and Conversation Rail. The same
AgentGUIAgentTargetInfoRenderer may be passed to
AgentGuiWorkbenchHeader.renderAgentTargetInfo with its exact
conversationAgentTarget. AgentGUI owns Tooltip disclosure, positioning,
focus behavior, and the built-in label fallback; the Host renderer receives
only { target, surface }, remains presentation-only, and is invoked lazily
while the Tooltip content is mounted. Conversation rows resolve the target by
canonical agentTargetId from the current Host directory and fail closed when
it is absent. They do not copy owner, device, availability, or other target
metadata into Session state.
Unavailable Agent targets retain the standard empty-home identity. AgentGUI
projects offline transport and revoked-sharing states through the same Home
status chrome used by other runtime failures, immediately above the composer;
it does not expose a second Host-owned availability container. The canonical
composer gate blocks both editing and submission until the selected target is
available.
Host chrome that aligns to AgentGUI's internal layout must consume explicit
package signals such as hostActions.onConversationRailLayoutChange; it must
not observe package DOM, CSS variables, or class names with
MutationObserver. Composer affordances belong in AgentGUI itself or a
narrow renderSlots contract, not in host-owned portals inserted into package
DOM. During a Conversation Rail drag, AgentGUI emits this signal with
resizing: true; the Workbench and standalone desktop headers apply that
ephemeral width only to their own grid alignment, while the existing
onUpdateNode path remains the sole persistent width write. Hosts must not
write node state for each pointer movement. AgentGUI updates both its grid
track (--agent-gui-conversation-rail-width) and the Rail content width
(--agent-gui-conversation-rail-content-width) in that same pointer move;
updating only one leaves blank space when expanding or overflows detail content
when shrinking.
6.3 AgentGUIRuntime and AgentHostApi
AgentGUIRuntime is the runtime surface accepted by AgentGUI. It owns reads,
Rail queries, file/upload capabilities, subscriptions, diagnostics, and the
workspace AgentSessionEngine lookup. Shared lifecycle writes enter through
semantic Engine methods or typed intents; AgentGUI does not require duplicate
activateSession, createSession, goalControl, sendInput,
submitInteractive, unactivateSession, updateSessionSettings, or
updateTuttiModeActivation callbacks.
AgentGUIRuntime is the sole AgentGUI host contract. Hosts must not recreate
the removed lifecycle callbacks as no-op or forwarding adapters. Metadata
actions that AgentGUI still reads directly remain on this surface until their
Engine migration preserves all host observability and product integration.
AgentGUI performance correlation is package-owned through
createAgentGUIPerformanceMonitor. A host supplies the workspace Engine, the
existing Session-event subscription, a clock, and a product-neutral event
sink. The monitor joins AgentGUI's preserved submittedAtUnixMs and queue
diagnostics to exact Session and Turn identities, then emits activation
settlement, Prompt admission, first-token receipt, Turn settlement, and
Composer-options load facts. trackComposerOptionsLoad emits a start fact
before calling either the runtime getComposerOptions or exposed Engine
loadComposerOptions path, then emits a correlated completed or failed fact
with exact duration. An unmatched start therefore remains observable when a
Provider options request never settles. The facts distinguish runtime from
session-Engine entry, force refresh, directory presence, bounded error
category, section/stage, and model count. Settled Composer events may include
a bounded, deduplicated, control-character-cleaned allowlist of model
identifiers; paths, settings, prompts, credentials, and error messages remain
excluded.
Hosts map those facts to their own analytics catalogs; they must not copy the
Turn-binding, early-event buffering, duration-bucket, or token-classification
logic.
First-token time ends when the renderer receives the first non-empty Agent
content message_delta; its bounded kind is text, reasoning, plan, or other.
It deliberately does not use the producer timestamp, infer a latest Turn,
count tool output, or inspect Prompt or response content. Early deltas are
retained only until the Engine supplies the exact Turn binding, duplicate
deltas emit once, and bounded caches prevent an abandoned Prompt from growing
runtime memory without limit. Exact durationMs accompanies the stable
buckets lt_1s, 1s_to_3s, 3s_to_10s, 10s_to_30s, 30s_to_60s, and
gte_60s.
AgentHostApi supplies host capabilities only: files, clipboard, project/account lookup, Agent Target setup/probes, diagnostics, and OS/Workbench helpers. It must not become a Session, Turn, timeline, or write source again.
Terminal authentication is one such Host capability. AgentGUI receives an
optional atomic terminalStartupAction containing a safe slash-command name
and literal readiness marker, then returns it unchanged through
terminalLogin.run; neither layer accepts or synthesizes raw terminal input.
Hosts must explicitly list supported startup action types; an older Host that
omits that capability may still expose the manual command fallback but cannot
silently launch a typed action it does not implement.
The Desktop Workbench presenter owns node launch, exact-session readiness
matching, slash submission, diagnostics, and close cleanup. Its startup result
must settle before the setup-readiness monitor starts, so timeout, cancellation,
or transport failure cannot leave a second polling lifecycle behind.
The optional quick-prompt library follows that host-capability boundary. Tutti
Desktop projects the device-global tuttid quick-prompt CRUD service through
AgentHostApi.quickPrompts; AgentGUI owns only the picker/editor presentation
and inserts a selected prompt into the current TipTap selection without
submitting it. The library snapshot and cross-window invalidation are not
Session or Turn state and must not enter AgentGUIRuntime or the workspace
engine. Hosts that omit the capability render no quick-prompt composer entry.
AgentGUI may also present a small, localized set
of recommended templates; those only prefill the existing editor and remain
client-local until the user explicitly saves them through the CRUD capability.
6.4 Multiple surfaces
AgentGUI, Message Center, dock/header, workspace window, and standalone Agent window consume the same workspace engine.
Opening a panel/window creates presentation state only. It does not clone a Session, copy engine entities, or start another event stream. Standalone tools are Desktop chrome, not AgentGUI lifecycle.
Workbench previews must not mount a second AgentGUI tree. Genie capture prefers
the host-provided native image and clones the visible node DOM into a texture
only after native capture fails or exceeds its bounded wait. Electron hosts
pass the sanitized node region directly to webContents.capturePage, retain
the returned region for Genie, and resize a copy for the Dock. The Genie path
must not reuse an undersized Dock image. Dock popup
cards and minimized slots use the bounded image and its cache. A background or
minimized popup node must not request a fresh DOM snapshot: AgentGUI may be
unhydrated, and surface.isVisible=false deactivates imperative resources such
as the hero Canvas. Those nodes reuse only a previously successful image. If no
captured image exists, the Dock shows a static terminal placeholder rather than
an in-flight skeleton; it does not mount AgentGUI or render a conversation
summary as a fallback. Capture failure is not cached across popup lifetimes, so
a later foreground attempt may still populate the image. AgentGUI therefore
has no preview-mode rendering contract.
When a minimized node has a cached Genie texture, Workbench completes the
restore animation before launching the host node, then replaces the final
texture frame only after launch settles. The expensive AgentGUI reconstruction
therefore stays outside the animation. Workbench also prepares the Genie canvas
during browser idle time with representative scanline, scaling, and glow
operations, and commits the final minimize state in a later task instead of
extending the last animation frame. A later minimize may start from that
retained texture while the host refreshes its native capture asynchronously;
a native image replaces the retained Genie texture only when it can cover the
current window without upscaling. Low-resolution thumbnails update only the
Dock preview cache. A late native result updates that Dock cache immediately,
but Workbench decodes its full-size image only after the active Genie animation
settles and the browser is idle. Without a reusable full-resolution texture,
Workbench falls back to the visible DOM capture. The retained Canvas cache is bounded by
both entry count and estimated RGBA bytes. Restored nodes keep their texture for
the next minimize; controller state changes remove textures only after their
nodes are closed. Eviction leaves the separate Dock image cache intact.
Genie node hiding uses a per-node operation token. A newer operation for the same node may supersede an older reveal, but another node's animation may not leave the first node hidden. The global animation generation controls only the shared Genie Canvas.
The empty AgentGUI Hero renders its existing DOM player before initializing the
optional WebGL carousel. A host projects whether the normal window presentation
is currently visible; the carousel waits for that visibility and then browser
idle time before creating its WebGL renderer. This keeps Genie restore work out
of the WebGL creation task without exposing Genie state to AgentGUI. A fully
occluded presentation releases its scene, decoded images, observer, and wheel
listener; exposure restores them after the reveal interaction settles. Once
initialized, WebGL renders only for texture or size changes and carousel
selection changes; the spring stops requesting frames after it settles. The DOM
turntable record uses a compositor animation only in the active visible
AgentGUI, and composer prompt tips follow the same active-surface rule.
Empty-Hero entry animations may fill backwards across their delay, but must
release their final identity transform after completion so every mounted
AgentGUI does not retain decorative compositor layers. An embedded AgentGUI
skips those internal entry animations because the Workbench shell already owns
its appearance transition. Background Workbench AgentGUI bodies remain mounted
so drafts and local conversation presentation survive focus changes. Focus does
not determine visibility. Workbench frame geometry and z-order classify bodies
as fully occluded or visually exposed; partial exposure counts as visible.
While Genie replaces a real node with Canvas output, or scale minimize marks
the node as pending, Workbench excludes that departing node from geometric
occlusion. Covered windows therefore resume presentation during minimize; a
restoring Genie node stays non-occluding until its real node is revealed.
Scale restore, shell frame transitions, and onboarding entry keep the moving
node's own presentation visible while temporarily excluding it as an occluder;
DOM transition and animation completion release that state. Portal-backed
dialog-popover nodes sort above default-layer nodes before Workbench applies
normal stack order.
Only fully occluded bodies pause descendant animations and use
content-visibility: hidden. Visible Empty-Hero surfaces may own carousel
images, alignment observers, wheel input, and a Three.js/WebGL scene.
Occlusion releases those resources; exposure restores the DOM presentation
immediately and defers WebGL reconstruction until after the reveal interaction
settles. Detail timelines follow the same geometric boundary: fully occluded
surfaces disconnect dock/timeline resize observation and scroll listeners,
while exposure reattaches them and resynchronizes the retained bottom lock.
Elapsed Turn, compaction, subagent, and Goal labels subscribe to one shared
renderer-realm second clock only while their containing presentation is
visible; exposure catches the displayed value up from canonical timestamps.
Focus alone must not stop either behavior because multiple exposed AgentGUI
windows remain live at the same time.
On workspace restore, Desktop mounts the focused AgentGUI body immediately and
hydrates inactive bodies sequentially after browser idle, one animation frame
at a time. Window shells and persisted geometry remain synchronous; this
staging is Desktop presentation work and does not enter engine or Session
lifecycle state.
WebGL scene readiness remains local presentation state; it must not enter
AgentGUIRuntime, the workspace engine, or Workbench node state.
The shared Workbench Header owns conversation-identity visibility. When no Conversation exists, it ignores conversation titles, Agent titles, primary icons, exact Agent targets, target-information renderers, and fallback icons even if a host supplies them. When target information is enabled, only the session icon becomes a no-drag Tooltip trigger; the surrounding title remains owned by the Header's existing drag and menu behavior.
The reusable tool-sidebar contract lives in
packages/agent/gui/workbench/tool-sidebar. Hosts provide the supported panel
catalog and render adapters; the shared component owns tab selection, picker,
sizing, toolbar mechanics, and a structured AgentToolSidebarHeaderLayout.
Standalone Desktop registers Side only while the exact external projection
exists, opens the matching source-Session tab, and uses the existing Side width
bounds (440px default, 360px minimum, 600px maximum). Collapsing the
sidebar retains the Side tab/runtime and releases Side focus; closing the exact
tab invokes the projected close intent. Stream updates notify only the mounted
Side panel, while the sidebar subscribes to a stable identity projection so it
does not rerender for each delta. Other hosts keep explicit panel catalogs and
do not receive Side implicitly.
Hosts may hide the shared toolbar toggle through the explicit
showToggleButton presentation input; omission preserves the standard visible
entry. Hiding the entry does not duplicate or override sidebar behavior in the
host.
That layout carries the actions, open state, and reserved width into the one
authoritative AgentGuiWorkbenchHeader.
The standalone Files tool reuses the Desktop file-manager pane and its complete context menu. Its Open With submenu keeps Tutti's file viewer and in-app browser alongside system applications, default-browser handling, and the system application picker. Double-clicking a file still delegates to the desktop host's system-default opener; directories continue to navigate inside Files.
Header ownership is explicit. A native standalone window selects the window-owned contract, so the shared sidebar composes the Workbench Header and body frame. An embedded Workbench selects the host-owned contract and projects the same header layout into its existing Header. The sidebar never creates a second visible panel Header. Interactive controls stop host drag gestures, while blank host-owned Header space bubbles to the host drag owner; native window ownership alone uses Electron app-region dragging.
Header layout is independent from ownership. overlay means the Header is
layered above a full-height body, so open tool-panel content reserves the shared
Header height and the Header stacks above --z-panel. stacked means the host
has already placed the body below the Header and no panel spacer is added.
The package owns one responsive Rail presentation policy for every surface.
resolveAgentGUIConversationRailPresentation derives the effective Rail width,
automatic collapse, and combined collapsed state from container width plus the
persisted user preference. Hosts must not supply an alternative breakpoint,
copy Rail dimension constants, or restyle AgentGuiWorkbenchHeader to move its
controls. The shared Workbench contribution selects an overlay Header with no
bottom border, and native standalone composition projects that same explicit
window presentation state. Standalone identity remains relevant only to native
window ownership such as Electron drag regions and full-viewport sizing; it is
not a Header layout or separator switch.
7. Key flows
7.1 New conversation
home composer submit
-> engine pending activation + optimistic Session/message
(including the resolved immutable railSectionKey)
-> Host CreateSession(initial content, clientSubmitId)
-> provisional runtime + canonical transaction
-> first Turn accepted
-> authoritative Session/Turn replaces optimistic projection
Initial content is one user-owned submit flow, but provider acceptance is not the prompt's durability boundary. Once the submitted Turn and prompt are durably recorded, a deterministic provider rejection keeps the visible Session, failed Turn, and user prompt so the failure can be rendered as history. Host then discards the startup runtime without publishing canonical completion. AgentGUI may offer provider login for an authentication failure, but it does not offer manual activation retry for failed or canceled Sessions; the user starts a new conversation instead. Restarted Sessions already project their canonical canceled state, so this presentation rule does not require a separate provider-history query. Only a pre-dispatch startup/validation failure may compensate an empty provisional shell; an outcome-unknown delivery keeps its recovery claim instead of guessing whether the provider ran.
The rejected submit claim is terminal and remains bound to that failed Turn, so
replaying the same clientSubmitId is an idempotent read of the persisted
failure and never dispatches a second provider Turn.
The initiating composer snapshots Tutti activation plus effect and speed with
that submit. An explicit active or inactive submit snapshot is authoritative
over a later read of mutable home-draft state; non-composer callers may fall
back to the engine draft when no snapshot exists. capabilityRefs remain
independent audit provenance and must never substitute for
initialTuttiModeActivation.
An activation may instead carry initialGoalControl. In that branch the engine
and runtime adapter preserve the structured {action, objective} command.
Desktop and Mobile map it to the typed initialGoalControl Create field and
send an empty initialContent; the daemon delegates that field to Agent Host,
which creates a non-provisional Session and the durable Goal operation without
manufacturing a Turn. Typed initial Goal and non-empty initial content are
mutually exclusive. The structured field is authoritative; integrations must
not reparse the display prompt or forward /goal ... as backend command text
to recover Goal semantics. Host retains text parsing only for compatibility
callers that omit the structured field. AgentGUI represents the pending control
and its durable audit with the same client-submit presentation identity, so
canonical replacement does not remove and recreate the visible goal-control
row.
Hosts that can observe the durable Goal saga may additionally project the
optional Session goalSyncState (revision, syncStatus, and
pendingOperationId, plus optional executionPending). It is read evidence
owned by the Host, not Session-owned Goal lifecycle state. AgentGUI uses an
identified pending, applying, or unknown operation while the exact initial
Goal mutation remains pending. After mutation convergence, synced retains the
bridge only when the Host explicitly reports executionPending=true. The Host
clears that proof on the first canonical Turn with exact Goal provenance or on
terminal, diverged, failed, or non-active Goal evidence. Omission fails closed,
including across mixed-version hosts; AgentGUI never infers future execution
from synced alone or uses a UI timeout.
The pending activation carries the same resolved project section key as the
create command. Exact rail projection therefore shows the conversation as soon
as the intent is accepted; it does not wait for provider startup or invent a
temporary catch-all section.
AgentGUI may select that optimistic conversation in mounted UI immediately,
but it must not persist the provisional Session ID into Workbench navigation
state. The selection becomes durable only after the workspace engine confirms
the activation from an authoritative Session snapshot. On restart, absence
from a bounded Rail page is not deletion evidence. A host that proves the
selected Session is absent returns typed session.not_found; the activity
engine retains that code, and AgentGUI clears only the matching global and
per-target navigation memories before returning Home. Timeout, transport, and
other reconcile failures preserve the remembered selection.
The controller's new-conversation command must distinguish rail placement from
the active Session's runtime working directory before entering the home
composer. A Session in the Chats section may have a generated cwd, but that
path is not a selected user project and must be cleared. A Session in a
canonical project section resolves that section key back to the registered
project root; its runtime cwd may instead be a nested directory or isolated
worktree and is never reused as project identity. “Continue in new
conversation” uses the same resolution before moving the mention draft to
Home. A command already on the home composer preserves the user's explicit
project selection. Views only forward new-conversation intent; unresolved
active rail membership fails closed rather than guessing from composer
presentation fields.
For delegated/shared execution, the initiating caller remains the placement
authority: the adapter forwards that caller-selected RailPlacement through
the binding to the owner Host. The owner persists the same section key and does
not recompute it from the owner's user-project list.
The Agent CLI handoff adapter also inherits the caller Session's runtime cwd
so the delegate starts inside the same checkout or linked worktree. If a
project-backed caller has no runtime cwd, its canonical project path is the
fallback. A supplied caller Session ID that cannot be resolved fails the start
instead of creating a detached Session in an allocator directory.
7.2 Existing conversation submit
composer submit
-> engine pending submit / queue
-> Host SendInput(clientSubmitId)
-> durable submit claim
-> provider execution
-> exact authoritative Turn acknowledgement
-> event/reconcile confirmation
A successful response includes the exact Turn. Clients must not repair a missing Turn by polling, sleeping, or synthesizing an entity.
AgentGUI-generated Prompt clientSubmitId values are canonical UUID v4
identities. They are opaque correlation and idempotency values because a host
may promote the same identity into a cross-device or shared-execution command.
Business meaning must not be encoded in or recovered from the UUID. In
particular, Plan feedback records its exact turnId and requestId in the
pending submit's typed source; Message Center reads that source through the
activity-core selector. Activity Core continues to accept caller-supplied
submit identities as opaque strings and does not reinterpret or rewrite them.
7.2.1 Existing conversation Goal Control
AgentGUI goal action or structured /goal submit
-> engine.controlGoal(action, clientSubmitId)
-> Engine Goal operation + optimistic Goal projection
-> Desktop or Mobile AgentSessionEffectPort.controlGoal
-> tuttid HTTP request carrying the same clientSubmitId
-> Agent Host durable Goal saga
-> typed Session/Goal/operation result
-> Engine validates Goal state and applies the returned canonical Session
Every admitted action reaches Host, even when the projected Goal already has
the requested value, because Host owns the durable revision, operation, and
audit. A typed response with pending or applying Goal state is accepted;
synced is succeeded; explicit failed, diverged, and unknown states
remain distinct. Only definitive protocol rejections become frontend
failed. Timeout, transport loss, or a malformed typed success remains
unknown; generic Session reconciliation does
not prove the outcome of a particular Goal operation. Retrying the same action
from unknown reuses its original clientSubmitId, so Host resolves the same
durable operation instead of creating a second one. A canonical Session Goal
cannot settle the operation by value equality because it carries no operation
identity. At most one operation record is retained per Session and Session
removal clears it. The package root exposes presentation and settlement
selectors, and the public Engine snapshot contains only their derived state,
not the raw Goal operation ledger. These maps are sparse and update only the
Session IDs whose Goal, Goal operation, or Goal-bearing activation changed, so
Turn streaming and unrelated Session changes preserve the Goal branch and
unaffected presentation references. The response goal is Host's durable
desired projection; provider observation remains in Goal state and may be
empty without clearing the visible Goal. Only a durable tombstone produces
goal: null. A definitive failure releases the old identity; only an unknown
result retains it for an explicit retry. Goal control enters only through the
workspace Engine; AgentGUI has no parallel runtime callback.
7.3 Interaction response
canonical Interaction(pending)
-> selector projection
-> inline / Message Center / toast surface
-> exact interaction response command
-> Host idempotent transition
-> answered or superseded projection
Every surface shares the exact interaction identity
(workspaceId, agentSessionId, turnId, requestId) and submitting state.
Collapsing an ask-user surface back into the conversation is presentation-only:
the canonical Interaction remains pending, no response or cancellation is
dispatched, and the mounted answer flow retains its in-memory draft for that
surface. The collapsed surface keeps a visible resume affordance. Hosts expose
this action only when their ordinary Composer can answer the exact pending
Interaction; Side and other prompt-only composers do not infer support from a
full-size layout. In the supported full conversation surface, a question with
structured options does not add a second
free-text answer field; users give a new direction through the ordinary
composer instead. When the active root Turn is waiting on exactly one pending
question whose normalized fields all accept free text, a text-only ordinary
composer submit is converted to the canonical answer payload and sent through
the existing Engine Interaction response operation for that exact Turn and
request. This resolves the provider's pending request so the same Turn can
continue; it does not cancel the Turn, enqueue a second prompt, or branch on
provider names. The composer draft clears only after Engine admits the response
and is restored if the response later fails. Fixed-choice questions and
non-text composer content remain on their existing paths.
Provider request ids remain unchanged and may repeat across Turns; no adapter
may recover a missing Turn by scanning for a session-wide request-id match.
An optional Host interaction-readiness capability may block an owner-dependent
response while preserving the canonical pending Interaction. Synchronizing
uses the existing device-connecting presentation and disables mutation; it is
not a terminal Interaction or Session outcome. Host submission adapters must
recheck their authoritative projection immediately before transport dispatch;
AgentGUI's early check does not replace Host or provider admission.
An interactive provider callback must not block the transport's message reader
while waiting for user input. If the provider can emit follow-up frames during
that wait, the adapter keeps reading and joins them before publishing the
canonical Interaction. When the answer is delivered, local call resolution is
serialized ahead of provider terminal messages caused by that answer.
Codex app-server requests are classified explicitly against the generated
server-request surface. A request being schema-known does not make it a
background request that may be declined silently. Message-only MCP form
elicitations reuse the canonical approval Interaction and preserve the exact
accept, decline, cancel, and advertised persistence semantics in the
app-server response. Field-bearing forms, URL elicitations, and other request
shapes that AgentGUI cannot represent losslessly fail closed before publishing
an Interaction; adapters must not coerce them into a partial approval or
question surface.
When a standard ACP provider bridges a structured question through
one session/request_permission as the complete question transaction, one
selected permission option is that bridge's entire response capacity. The
one-shot adapter therefore publishes only an exact single-question,
single-select mapping whose question labels correspond one-to-one with the
provider's non-rejection options. It marks that surface as option-only,
rejects multi-question, multi-select, free-text, duplicate, or
malformed/mismatched shapes before canonical Interaction publication, and
never records them as answered. On submission, only the single scalar value
in answersByQuestionId is authoritative; the flat answers list is display
data and must not select a provider outcome. The adapter preserves the
accepted canonical answer payload for local projection but translates its
chosen label back to the provider's opaque option ID. The wire response
remains ACP-native (outcome=selected plus optionId); renderer actions such
as submit are not provider outcome values. A provider may model richer
questions as a correlated sequence of permission requests, but Tutti must
implement that sequence as an explicit transaction before publishing the
richer surface; it must not batch answers into request ids that have not
arrived.
Non-DOM hosts such as React Native reuse the pure
agent-conversation/interactive-answer entrypoint for canonical ask-user
payload construction and own-property-safe question-id access. Presentation
components remain platform-specific, but they must not copy or reinterpret
this cross-provider answer contract.
A synthesized plan decision uses a durable plan_decision operation. A provider-native plan Interaction continues through interactive_response. Similar UI does not justify merging their write paths.
7.4 Resume
select/open existing Session
-> engine session reconcile
-> Host GetSession / EnsureRuntimeSession
-> canonical state + optional live observation
-> messages/detail hydration
If resume is unavailable, return an explicit state. Do not create a shadow Session.
7.5 Conversation actions and copy
rail row More menu / row context menu / workbench header menu
-> one shared action-group contract (AgentGUIConversationActionsMenu)
-> rename | copy as reference | copy as Markdown | open window | mark unread
All three surfaces render the same action groups; the header dispatches
one discriminated AgentGuiWorkbenchCommand protocol and the node resolves the
target session against canonical rail entities under the rail interaction
lock. While either row
menu is open the row keeps its hover layout (short title truncation, actions
visible) so titles cannot overlap the action cluster.
The existing package-owned AgentGUI external-request controller is the only
consumer of Workbench commands. It routes by exact Workbench instance and
executes new-conversation and Session actions directly, without a host hook,
React request state, or sequence projection. Product hosts provide only the
Workbench instance identity and an optional host-owned Rail response callback.
The reusable Workbench node-state source remains the only embedded Rail state
writer; its callback may record a host device preference but must not write the
same node state again. A standalone surface may use the callback as its sole
Rail state writer because it has no Workbench node-state source. The callback
contract contains no Tutti Desktop preference or product policy, so other hosts
such as TSH may persist their own Rail state without adopting Desktop behavior.
Attention state is unread-only: a live completion or explicit user unread
request creates a durable unread completion, and real user exposure removes it.
Absence of an unread completion means read; no separate durable read marker is
authoritative. Historical list/detail reads own canonical content but never
create, remove, or reconcile attention. Hydration restores only
completed.unreadIds and failed.unreadIds; legacy readIds are ignored on
read and every write payload explicitly empties both read buckets so stale read
markers cannot survive migration.
Selection is node-local while attention state is shared. An active Session may
remove unread attention only while its AgentGUI surface is host-focused,
host-visible, and the renderer document is visible and focused. A retained
hidden, occluded, unfocused, or backgrounded node must preserve unread
attention even when its local activeConversationId still names the Session.
The read-decision diagnostic records the exact node, completion, host focus,
document exposure, visibility, and decision reason without logging on stream
updates.
Read-only host surfaces reuse the complete workbench header and declare the
session actions they support. Omitting that capability list preserves the full
rename-and-copy menu; a copy-only surface does not render rename or an empty
separator. Hosts that already own a complete canonical message projection may
reuse the pure transcript serializer exported by the agent-conversation
entrypoint, while clipboard access, toasts, and session loading remain
host-owned capabilities. Window-level Agent chrome applies only when that Header
is rendered through the Workbench window's own header slot; a complete Header
nested in another host window's body remains ordinary embedded content and must
not alter the outer window's layout or drag layer.
Copy as reference copies the session-mention markdown link the @ panel
produces, so pasting into any composer reconstructs the session chip; it is
synchronous and only requires a writable host clipboard. Copy as Markdown
loads every canonical message page and serializes a lean transcript: user
inputs blockquoted in full, per-turn final agent replies plain, interim
narration collapsed in a <details> block; tool payloads (except image
outputs), thinking, agent_system_notice messages, and JSON fallbacks are
dropped. The clipboard write is dual-format: text/plain keeps short image
references and never carries base64, while text/html embeds images as
data URIs hydrated from inline data, the attachment store, and host
workspace.readFile — verified empirically: rich-paste targets (Feishu
docs) consume data URIs and re-upload them, but never fetch local paths or
localhost URLs. Images over the per-image embed cap, or with failed reads,
keep the lean reference and surface a toast that counts the omissions and
points at per-image copy. Because history loading and image hydration take
a noticeable moment on long conversations, selecting copy as Markdown opens
one toast immediately (AgentHostToastApi.loading): it shows busy with a
spinner and never auto-dismisses, and the handle it returns settles that
same toast in place to success, the omitted-images info tone, or failure —
at which point it starts auto-dismissing like any other toast. This is one
continuous toast, not a loading toast followed by a separate result toast.
Hosts without the loading capability get the prior plain info toast
instead, and the result lands as an ordinary second toast.
8. Change routing
Answer before editing:
| Question | Owner |
|---|---|
| Change when Session/Turn/Goal/operation is created, sent, terminated, or recovered? | packages/agent/host; add a conformance scenario first |
| Change canonical phase/outcome/Interaction vocabulary? | store-sqlite/canonical |
| Change an HTTP request/response? | OpenAPI first, then generated clients |
| Change provider wire normalization? | provider-owned daemon adapter |
| Change cross-provider behavior? | registry descriptor/strategy/capability |
| Change frontend async, optimistic, queue, or reconciliation semantics? | agent-activity-core engine |
| Change projection, interaction, or loading behavior? | focused AgentGUI model/controller |
| Change DOM, focus, scroll, or animation? | view/UI-local hook |
| Change Electron, Workbench, OS, file, or window capability? | Desktop host adapter |
Diagnose in owner order:
- Did the canonical command accept and commit?
- Did Host produce the correct lifecycle result/
CommittedDelta? - Do tuttid events match the authoritative read?
- Did Desktop reconciliation emit the correct engine intent?
- Did the engine reducer/selector derive the correct state?
- Did projection/view render only its input?
Do not start by adding a fallback to the visible component.
8.1 Agent settings surface
The desktop settings panel's Agent section has General Settings, Agent Runtime,
and Custom Agents available by default; Automation remains independently
gated. The Agent Runtime tab renders built-in provider rows from the
authoritative identity catalog plus the live IAgentProviderStatusService.
Stable Agent Extension maturity is declared separately from Early Access
activation flags, and those rows consume their live IAgentsService Targets
and package-provided identity assets rather than becoming built-in provider
descriptors. Its Enable/Disable control reads all Agent Targets from
IAgentsService and persists the daemon-owned Agent Target enabled field.
Tutti Agent is the built-in exception: its target is always enabled, its row
cannot be disabled, and it has no separate developer visibility switch.
Other disabled targets remain in this settings control plane so they can be
re-enabled, but they are excluded from the AgentGUI agent projection and from
CLI discovery and launch. The device-global provider-rail preferences remain
presentation-only (ordering and optional sidebar personalization); they do not
authorize an Agent Target or replace daemon enablement. Staged
(Beta/Preview/in-progress) rows are gated by the lab.previewAgents switch via
the provider-neutral agentGuiWorkbenchPreviewProviders predicate; stable
built-in and Agent Extension rows always show in settings and launch surfaces.
Deep links publish the existing
openWorkspaceSettingsPanel intent with optional pane/provider; the
Desktop Settings service is the single adapter that resolves legacy aliases
and current destinations for workspace and standalone windows. An Agent
Runtime destination also bumps agentFocus to scroll and briefly highlight the row;
a link to a hidden preview agent surfaces an "enable Preview Agents" hint rather
than failing silently. This is a settings surface, not a second Agent Target
state store.
New Custom Agent drafts derive an untouched display name from the selected
Model Plan and Agent Runtime, joined as Model Plan - Runtime when both are
present. Runtime or plan changes may refresh that suggestion only until the
user edits the name; after the first edit, the draft preserves the user's value
across later selection changes. Description, instructions, and call conditions
remain draft fields but are grouped under a collapsed-by-default Advanced
options disclosure.
8.2 Deleted-conversation settings surface
Deleted conversations are maintained by a top-level desktop Settings section immediately before About; they do not belong to the Agent subsection. The page is scoped to the current Workspace, while its 15/30-day automatic-cleanup preference is device-global. Its fixed header owns the title-only search, project filter, retention selector, and destructive “delete all” action so the controls remain discoverable while the list scrolls.
Each row represents one topmost deleted Session component—a canonical root or
a child whose parent is not deleted—and reuses the two-line Activity View
summary convention: title first, then original project identity and the
pre-delete updated_at. The row body is not navigation. Restore and
permanent delete are the only row actions; restore has no confirmation and does
not navigate, while permanent delete uses the ordinary destructive
confirmation. Legacy lossy tombstones explain why restore is unavailable.
“Delete all” ignores search/project filters, confirms against the Workspace
component count with a typed phrase, and remains subject to the daemon idle gate.
The list uses stable cursor paging plus virtual scrolling and automatically
loads near its end. Project options include the unscoped case and original
paths whose project registration has since disappeared. Renderer state may
optimistically remove a row only after a successful restore/purge response; it
does not copy lifecycle semantics out of Host or trigger provider resume. The
canonical restore commit emits session_restored; the workspace engine clears
only that Session's deletion tombstone and performs an authoritative detail
reconcile before AgentGUI renders it again. Generic activity events remain
unable to resurrect a deleted Session.
9. Folder guide
| Path | Responsibility |
|---|---|
packages/agent/host/** |
provider-neutral lifecycle application core |
packages/agent/store-sqlite/** |
canonical SQLite transactions/repositories |
packages/agent/store-sqlite/canonical/** |
canonical vocabulary and projection contracts |
packages/agent/daemon/** |
provider runtime, registry, wire adapters |
services/tuttid/service/agent/** |
Host adapters, queries, HTTP/product preparation |
services/tuttid/api/openapi/tuttid.v1.yaml |
daemon HTTP contract |
packages/agent/activity-core/src/engine/** |
frontend workspace engine |
packages/agent/gui/agentActivityRuntime.tsx |
AgentGUI runtime interface |
packages/agent/gui/agentGUIPerformanceMonitor.ts |
product-neutral AgentGUI latency correlation |
packages/agent/gui/agent-gui/agentGuiNode/controller/** |
focused controller modules |
packages/agent/gui/agent-gui/agentGuiNode/model/** |
pure node projection/policy |
packages/agent/gui/shared/agentConversation/** |
reusable transcript projections/components |
packages/agent/gui/agent-message-center/** |
Message Center projection/presentation |
apps/desktop/**/workspace-agent/** |
desktop activity service, adapter, host integration |
10. Validation
Follow the repository Validation Selection. The Agent architecture boundary commands available to that workflow are:
pnpm check:agent-host-boundary
pnpm check:agent-activity-runtime-boundaries
pnpm check:agent-provider-strategy-boundaries
pnpm check:agent-gui-degradation
pnpm check:renderer-boundaries
check:agent-gui-degradation is executable architecture. Its business-file 800-line limit and budgets for effects, memoization, render-mirror refs, provider branches, timers, component stores, and module globals may only stay level or decrease. Tighten the baseline when a metric drops; never raise it to merge new drift.
Any change to an owner, data flow, public contract, or recurring trap requires documentation impact:
- durable architecture rules update this or an adjacent architecture document
- implementation plans belong in
docs/specsordocs/plans - symptoms and investigation steps belong in troubleshooting
- historical migration records do not return to this document