97140168d8
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
209 lines
10 KiB
Markdown
209 lines
10 KiB
Markdown
---
|
|
status: current
|
|
system: tasks
|
|
requirements:
|
|
- 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](#data-and-contracts), [Control flow](#control-flow), [Presentation](#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
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
{ 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](../../../decisions/0028-task-create-last-used-source-of-truth.md)
|
|
and [ADR 0041](../../../decisions/0041-backend-owned-portable-user-settings.md).
|
|
|
|
## 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.
|
|
|
|
## Related decisions
|
|
|
|
- [ADR 0028](../../../decisions/0028-task-create-last-used-source-of-truth.md)
|
|
- [ADR 0041](../../../decisions/0041-backend-owned-portable-user-settings.md)
|
|
- [Resolve Task Executor Policy Before Last-Used Profile](../../../decisions/2026-08-01-repository-task-executor-defaults.md)
|