Files
kandev/docs/specs/tasks/system-design/task-create-agent-executor-compatibility.md
T
Jonathan Marbutt 97140168d8 fix(web): keep a usable agent selected when the executor changes (#3388)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
2026-09-05 17:42:06 +00:00

10 KiB

status, system, requirements
status system requirements
current tasks
REQ-TASKS-TASK-CREATE-AGENT-COMPATIBILITY-001

Task Create Agent Compatibility Recovery System Design

Purpose and boundaries

The task-create dialog (apps/web/components/task-create-dialog*.ts and .tsx) owns agent and executor selection for new tasks. This design covers how the dialog derives an agent compatibility state, replaces an incompatible selection, and presents each state on desktop and phone.

The design uses, and does not own, two adjacent contracts:

  • The compatibility rule isAgentConfiguredOnExecutor in apps/web/lib/agent-executor-compat.ts, which reads the executor profile's remote_credentials and remote_auth_secrets configuration.
  • The remote-auth catalog served by GET /api/v1/remote-credentials and cached by useRemoteAuthSpecs in apps/web/hooks/domains/settings/use-remote-auth-specs.ts.

Requirement mapping

Requirement Design section
REQ-TASKS-TASK-CREATE-AGENT-COMPATIBILITY-001 Data and contracts, Control flow, Presentation

Components and responsibilities

  • apps/web/components/task-create-dialog-computed.ts: useExecutorProfileCompat keeps filtering agentProfiles into compatibleAgentProfiles and derives agentCompatState through a pure computeAgentCompatState helper. noCompatibleAgent remains the derived boolean that gates submission.
  • apps/web/components/task-create-dialog-autopick.ts: decideAgentProfileAutopick gains a replacement path for a selected profile that is no longer compatible. useAgentProfileAutopickEffect applies it.
  • apps/web/components/task-create-dialog-form-body.tsx: AgentColumn renders the selector, the empty state, the unavailable note, or the incompatible note per state.
  • apps/web/components/task-create-dialog-prop-builders.ts: forwards the state, the selected agent profile label, and the effective workflow name.
  • apps/web/components/task-create-dialog-footer.tsx: computeDisabledReason returns a state-specific reason key, and resolveDisabledReason fills the agent and executor names.
  • apps/web/components/task-create-dialog-setup.ts: the guarded submit handler applies the compatibility block to form, keyboard shortcut, and programmatic submissions.
  • Locale catalogs apps/web/src/locales/<locale>/task.json own the copy.

Data and contracts

export type AgentCompatState =
  | "compatible"
  | "selected-incompatible"
  | "selected-unavailable"
  | "none-compatible";

computeAgentCompatState derives the state from the selected executor profile, compatibleAgentProfiles, the effective agent profile id (the user value or the workflow override) and its store row, the workflow lock, and the dynamic routing feature flag:

  1. No executor profile selected: compatible.
  2. No effective agent id: none-compatible when compatibleAgentProfiles is empty, otherwise compatible.
  3. The effective agent id is in compatibleAgentProfiles: compatible.
  4. The selected row exists but is not selectable (disabled, or a dynamic profile while routing is off): selected-unavailable when another compatible profile exists. A locked disabled profile, or an unavailable selection with no alternative, is none-compatible. A locked disabled profile is a profile-disable outcome, not a credential problem.
  5. The workflow locks the agent: selected-incompatible, even when compatibleAgentProfiles is empty, so the locked note names the workflow and agent instead of the generic empty state.
  6. compatibleAgentProfiles is empty: none-compatible.
  7. Otherwise: selected-incompatible.

noCompatibleAgent equals agentCompatState !== "compatible", so the footer and submit gates keep their existing input.

DialogComputedValues adds agentCompatState and selectedAgentProfileName: string | null (the label of the effective agent profile). DialogFormBodyProps and CreateEditSelectorsProps add agentCompatState, selectedAgentProfileName, and effectiveWorkflowName: string | null.

The autopick decision keeps its shape and adds an optional replaces field:

{ kind: "pick"; source: "lastId" | "defId" | "first"; id: string; replaces?: string }

The replacement gate runs before the existing already-set skip. It yields a pick with replaces set to the current id when all of these hold: the dialog is open, an agent id is set, no workflow lock applies (workflowAgentProfileId is empty and the workflow has no agent), the auth catalog is loaded, an executor profile is selected, compatibleAgentProfiles is non-empty, and the current id is absent from compatibleAgentProfiles. The candidate order is unchanged: last-used, workspace default, first compatible.

Presentation

State Workflow lock Agent column
compatible any Existing selector (agent-profile-selector).
none-compatible any agent-profile-empty-state with task:noCompatibleAgentProfilesFor and the credentials link.
selected-incompatible no Selector with the compatible options plus agent-profile-incompatible-note with task:agentNotConfiguredOnExecutor and the credentials link. This state is transient until the replacement applies.
selected-incompatible yes agent-profile-incompatible-note with task:workflowAgentNotConfiguredOnExecutor (workflow, agent, executor) and the credentials link. No selector.
selected-unavailable no Selector with the compatible options plus agent-profile-unavailable-note with task:selectedAgentProfileUnavailable. This state is transient until the replacement applies.

The credentials link keeps its existing target /settings/executors/<executor-profile-id>.

Footer reason keys:

  • none-compatible: existing REASON_NO_COMPATIBLE_AGENT (task:noCompatibleAgentProfileFor).
  • selected-incompatible: new REASON_SELECTED_AGENT_INCOMPATIBLE (task:selectedAgentNotConfiguredFor, with agent and target).
  • selected-unavailable: new REASON_SELECTED_AGENT_UNAVAILABLE (task:selectedAgentProfileUnavailable, with agent).

New task namespace keys, added in en, pt-pt, zh-cn, zh-hk, zh-tw, and regenerated pseudo:

  • agentNotConfiguredOnExecutor: "{{agent}} is not configured on {{target}}."
  • workflowAgentNotConfiguredOnExecutor: "The {{workflow}} workflow uses {{agent}}, which is not configured on {{target}}."
  • selectedAgentNotConfiguredFor: "{{agent}} is not configured on {{target}}. Configure agent credentials in Settings → Executors."
  • selectedAgentProfileFallback: capitalized fallback for a sentence start.
  • selectedAgentProfileFallbackInline: lowercase fallback for the workflow sentence where the agent name is not at the start.
  • selectedAgentProfileUnavailable: "{{agent}} is unavailable. Select a compatible agent profile."

Mobile composition

The dialog renders one component tree for both viewports. The agent and executor selectors sit in a grid that collapses to one column below the sm breakpoint, so the note renders under the selector at full dialog width and wraps. No drawer, new surface, or touch control is added. The nearest shipped mobile exemplar is the create-task dialog flow in apps/web/e2e/tests/task/mobile-create-task-branch-policy.spec.ts, which already drives the executor profile selector on a phone viewport.

Control flow

  1. The user picks an executor profile. handleExecutorProfileChange sets fs.executorProfileId and queues the last-used executor. This handler is unchanged.
  2. useExecutorProfileCompat recomputes compatibleAgentProfiles and agentCompatState in the same render.
  3. useAgentProfileAutopickEffect evaluates the replacement gate. A pick is applied through setAgentProfileId on the next microtask, the same deferral the existing autopick uses. The effect does not call syncTaskCreateLastUsed, so the stored last-used agent stays untouched. The submit gate stays closed while an unavailable selection is visible.
  4. AgentColumn renders per the presentation table, and the footer resolves the disabled reason.

When the workflow locks the agent, useWorkflowAgentProfileEffect keeps setting the locked id, the replacement gate is skipped, and the locked note is shown until the user changes the workflow or configures the credentials.

Failure and recovery

  • Auth catalog not loaded: compatibleAgentProfiles equals the selectable list and the state resolves to compatible. No replacement runs. This matches the existing deferral in the autopick gate.
  • Catalog request failure: the cached specs are empty, every agent on a remote executor is blocked, and the state is none-compatible. The empty state and credentials link remain the recovery path. This behavior is unchanged.
  • Agent profiles transiently empty during a refresh: compatibleAgentProfiles is empty, so the replacement gate does not fire and a valid selection is not cleared. The existing agentProfilesLoading guard keeps the empty state hidden while profiles load.
  • An unlocked disabled or dynamic-off selection with a compatible alternative: the dialog shows the unavailable note, blocks submit, and waits for the autopick replacement. It does not show the no-compatible empty state.

Persistence

None. The replacement is dialog state only. users.settings.task_create_last_used.agent_profile_id changes only through the manual selection handler and successful task creation, as recorded in ADR 0028 and ADR 0041.

Observability

The executor-compat:autopick debug logger records the replaces id and the source of a replacement decision. The existing [executor-compat] per-agent check logs (ok, reason) remain the diagnostic for why a profile is incompatible.