Agent Execution Redux — State Management
Routing — read the companion that matches your task
Architecture, execution flow, design rules, and the where-does-state-belong table below apply to every run. Branch material lives beside this file:
- Adding a field, action, or selector to a slice → read extending-slices.md (the 5-step process + worked example).
- Firing / engaging / triggering a shortcut from product code (button, context menu, mounted component, programmatic — including
createInstanceFromShortcut) → read triggering-shortcuts.md before writing the call site.
- Reading or changing
instanceUIState fields or actions → read instance-ui-state-reference.md.
Architecture
Four strict layers. Every slice, selector, and thunk belongs to exactly one.
Layer 1 — Agent Source (features/agents/redux/)
Static definitions. What agents and shortcuts exist.
| Redux Key | Directory | Owns |
|---|
agentDefinition | agent-definition/ | All agent records (live + version snapshots). Dirty tracking, field-level undo, progressive fetch status, access metadata. |
agentShortcut | agent-shortcuts/ | Pre-configured launch configs: agentId + scope mappings + display prefs + variable overrides. |
agentConsumers | agent-consumers/ | Per-UI-instance filter/sort/search for agent list views. Multiple consumers browse independently. |
Layer 2 — App Context (lib/redux/slices/appContextSlice.ts)
| Redux Key | Owns |
|---|
appContext | Org, workspace, project, task IDs. Injected into every API call by assembleRequest(). |
Layer 3 — Per-conversation slices (under execution-system/)
Ephemeral runtime state. Each conversation is self-contained.
Rename landed (2026-08): the "execution instance" is now the conversation. Every
per-conversation slice keys on byConversationId: Record<string, T> — byInstanceId no longer
exists anywhere in execution-system/. The shell slice is conversations
(conversations/conversations.slice.ts, "formerly 'execution instance'");
generateInstanceId survives only as a @deprecated alias of generateConversationId
(utils/ids.ts). The family has also grown well past the ten slices this doc originally
listed — the table below shows the core request-assembly set; see the execution-system/
directory listing for the full current set (messages, observability, run-sets,
instance-working-document, inbox, durable-runs, …).
Core invariant: agentId is read exactly ONCE at conversation creation. After that, the conversation owns all its data. The agent definition can change or be deleted — the running conversation is unaffected.
| Redux Key | Sent to API? | What it owns |
|---|
conversations | No | Shell: agentId, origin, status (draft → ready → running → streaming → paused → complete → error) |
instanceModelOverrides | config_overrides | Base LLM settings snapshot + user deltas (only deltas sent) |
instanceVariableValues | variables | Definitions snapshot + three-tier resolution: defaults → scope → user |
instanceResources | merged into user_input | Attached files/content with status tracking |
instanceContext | context | Slot-matched + ad-hoc context entries |
instanceUserInput | user_input | Text + multimodal content blocks |
instanceClientTools | client_tools | Client-side tool IDs |
instanceUIState | Never | Display mode, panels, variable focus, creator flags — purely visual |
messages | No | The committed transcript (turn history) |
activeRequests | No | Per-request stream state: accumulated text, data payloads, pending tool calls, client metrics |
Layer 4 — Thunks + Cross-Cutting Selectors
| File | Role |
|---|
thunks/create-instance.thunk.ts | Conversation factory. 5 creation paths. Reads agent once, dispatches init* to every sibling per-conversation slice. |
thunks/execute-instance.thunk.ts | Convergence point. assembleRequest() reads all per-conversation slices → fetch → NDJSON stream → dispatches to activeRequests + messages. |
selectors/aggregate.selectors.ts | Cross-cutting: conversationId → latest request → derived state (executing, streaming, text, errors, tools). |
Execution Flow
createManualInstance(agentId)
├── readAgentSnapshot() — reads agentDefinition ONCE
├── createInstance() — shell in `conversations`
└── init* dispatches to every sibling per-conversation slice
│
▼
executeInstance(conversationId)
├── assembleRequest(state, conversationId)
│ reads: instanceUserInput, instanceResources,
│ instanceVariableValues, instanceModelOverrides,
│ instanceContext, instanceClientTools, appContext
│ → snake_case payload
│
├── Routing: no conversationId → POST /api/ai/agents/{agentId}
│ has conversationId → POST /api/ai/conversations/{id}
│
├── addUserTurn → optimistic UI (message appears immediately)
├── createRequest → row in activeRequests
│
└── NDJSON stream loop:
chunk → appendChunk (accumulated text)
tool_delegated → addPendingToolCall (instance pauses)
completion → stats captured
error → status "error"
end → commitAssistantTurn to conversation history
Design Rules
Snapshot isolation — Instances never read back from agentDefinition. The creation thunk copies what it needs; execution only touches instance slices.
Request ≠ Conversation — One conversation, multiple requests (multi-turn). activeRequests keyed by requestId, reverse-indexed by conversationId. Components only know conversationId — aggregate selectors bridge.
Cleanup — activeRequests, messages, and instanceUIState listen to destroyInstance via extraReducers. Other slices rely on the creation thunk for init and the shell for truth.
Progressive fetch — AgentFetchStatus: list < execution < customExecution < full < versionSnapshot. Never downgrades. shouldUpgradeFetchStatus() enforces this.
Factory selectors — makeSelect* creates per-component memoized instances. Use useMemo(makeSelectFoo, []) in components. See the redux-selector-rules skill for full selector guidelines.
How to Extend This System
Decision: where does new state belong?
| If the state describes... | It belongs in... |
|---|
| How something is displayed (layout, panels, expanded, focus) | instanceUIState |
| What the user typed or attached | instanceUserInput / instanceResources |
| Agent config for this run (model, temperature, tokens) | instanceModelOverrides |
| Variable values for this run | instanceVariableValues |
| Contextual data sent to the agent | instanceContext / instanceClientTools |
| What happened during execution (stream, chunks, tools) | activeRequests |
| Conversation record (turns, mode) | conversations / messages |
| Agent definition itself (permanent, not per-run) | agentDefinition |
| How a list of agents is filtered/sorted | agentConsumers |
| Pre-configured launch config | agentShortcut |
What you must never do
- Local state for shared concerns.
useState for something that belongs in a slice means other components can't see it and shortcuts can't configure it.
- Parallel systems. Don't build a new store because the existing one "doesn't have what I need yet." Extend it.
- Shape hacks. Don't force data into a shape that works for your component but breaks the contract.
- Abandon architecture. A missing piece is a cue to strengthen the system, not bypass it.
Adding new state → read extending-slices.md for the 5-step process (field → init* default → setter → selector → barrel) and the variable-display-layout worked example.
Triggering Shortcuts (Consumption Side)
Engaging a stored shortcut from product code: Rule Zero (read the working example first), the direct vs non-direct modes, the Golden Rules, the four trigger APIs, the shortcut registry, warm-up, applicationScope keys, resolution order, jsonExtraction, both examples, cleanup, and common mistakes.
Firing any shortcut → read triggering-shortcuts.md before writing the call site.
Reference: Instance UI State
InstanceUIState fields, types, defaults, and available actions.
Touching instanceUIState → read instance-ui-state-reference.md.
For the full developer guide with pipeline details, see features/agents/redux/AGENTS_OVERVIEW.MD.