Files
kandev/docs/plans/dynamic-provider-options/plan.md
T
2026-08-24 11:09:03 +00:00

14 KiB

spec, decision, created, status
spec decision created status
docs/specs/agents/requirements/dynamic-provider-options.md docs/decisions/2026-08-07-model-aware-provider-capability-resolution.md 2026-08-07 complete

Implementation Plan: Dynamic Provider Model Options

Overview

Add a model-aware, provider-neutral capability resolver for settings surfaces that do not have a live ACP session. The resolver will use the existing host utility agentctl process, apply a requested model through generic ACP session model logic, and return the provider's complete normalized configuration-option snapshot. Workflow session overrides and agent profile settings will consume the same frontend hook and reconcile their draft option maps against the returned snapshot.

The existing baseline agent capability endpoint remains responsible for the model list, agent status, modes, commands, and default-session options. The live chat selector remains session-authoritative. No database migration or provider-specific frontend mapping is planned.

Architecture

Baseline versus resolved capabilities

AvailableAgent.model_config remains a fast baseline. It is used to render the model picker immediately and to preserve a usable selector while a dependent resolution is loading. It must not be treated as the final option set for a selected model.

The new resolver accepts an agent name and a canonical context containing the selected model, with optional mode and selected config options. It starts or uses the existing host utility ACP boundary, performs a model change in the probe session, waits for the provider's complete response or bounded config update notification, and normalizes the result through internal/agentctl/acpcompat. The returned option list is a full replacement for the current context, including an intentional empty list.

The provider response is authoritative. The UI renders arbitrary ACP select options and choices through ModelConfigSelector; it does not know that an option came from OpenCode or that it is called reasoning effort.

Resolution API and cache

Add POST /api/v1/agent-models/:agentName/resolve. The existing GET endpoint continues to return the baseline dynamic model response. The POST body has a required model and optional mode, config_options, and refresh fields. The response returns the agent name, requested model, status, complete normalized config_options, and a sanitized error when resolution is not authoritative.

Host utility resolution is cached in memory by agent type plus canonicalized context. Use a bounded TTL (five minutes unless existing runtime limits require a shorter value), single-flight deduplication for identical concurrent keys, and invalidation on baseline refresh. Cache failures only when they represent a stable unsupported result; transient process/timeout failures should be retryable and must not poison the cache indefinitely. The cache is runtime-only and is safe to lose on restart.

Settings behavior

The workflow rule card and profile form request resolution whenever their selected model/context changes. The hook must provide baseline options, resolved options, loading, error, and retry state while protecting against out-of-order responses. A successful snapshot replaces, rather than merges with, the previous option list. The editable config_options map drops keys not present in a successful snapshot before the next save. A failed resolution does not rewrite persisted data or silently remove saved values.

The shared selector stays generic. If a small loading/error affordance is needed, add it as an optional status prop with localized copy. The chat path is not changed because its live session already emits authoritative updates.

Probe compatibility

The utility probe must use the same client capability metadata and ACP compatibility normalization as live sessions. The model application path must support typed config-option model changes and legacy model methods through the existing sessionmodel abstraction. Provider-specific quirks remain in internal/agentctl/acpcompat; no OpenCode branch belongs in settings or the host utility cache.

Mock provider contract

Make the mock ACP agent model-dependent for E2E. Its initial session should advertise a small baseline, and applying one model should return no extra options while applying another should return an effort-like select option. This keeps tests provider-neutral while proving that settings resolve after a model change instead of accidentally relying on static AvailableAgent data.

Backend

Model-aware utility probe

Extend the utility probe request/result and ACP executor flow so a probe can carry a model-resolution context. After session/new, apply the requested model, capture the complete response config or a bounded configuration-update notification, and pass it through the existing config-option conversion and ACP dialect normalization helpers. Keep baseline probing behavior unchanged when no model context is supplied.

Add focused coverage for response snapshots, notification fallback, unsupported model application, provider normalization, and the existing client capability metadata. Reuse sessionmodel.ApplySDKWithConfigOptions where its returned config is authoritative instead of duplicating ACP request logic.

Host utility resolution service

Add typed resolution inputs/results to the host utility package, a separate runtime cache, canonical cache-key construction, TTL handling, single-flight deduplication, refresh invalidation, and a bounded context timeout. Expose a narrow public manager method for the settings controller. Do not persist resolved capabilities or attach them to a task session.

Settings HTTP contract

Add DTOs for the resolver request and response, controller validation, and the POST route beside the existing dynamic-model route. Reuse the existing agent availability and authorization checks and sanitize probe failures consistently with the current settings APIs. Preserve the baseline GET response for existing callers.

Frontend

API and hook

Extend settings-api.ts and http-agents.ts with the resolver request/response contract. Extend use-dynamic-models.ts with a reusable model-resolution hook or resolver returned by the existing hook. It must:

  • key requests by agent, model, mode, and canonical option context;
  • share in-flight and cached results across workflow/profile consumers;
  • ignore stale responses after a newer model selection;
  • replace the option snapshot atomically;
  • reconcile draft option maps only after a successful response; and
  • keep persisted values readable and retryable when discovery fails.

Add pure unit coverage for canonical context, reconciliation, stale responses, cache reuse, refresh, and error recovery.

Workflow editor

Wire WorkflowSessionConfigRuleCard to resolve options for the rule's selected model. Keep the current static model list and rule serialization. On a successful model-dependent response, render every returned select option and save only the reconciled map. Preserve existing read-only and existing-rule failure behavior.

Agent profile editor

Wire ProfileFormFields to the same resolver using the profile's current model. Keep model/mode/command discovery on the existing baseline hook. Ensure changing a profile model refreshes dependent options, removes only successfully-invalidated draft keys, and saves arbitrary resolved options.

All new loading, retry, and failure text must use i18next keys and be added to the affected locale catalogs. Keep controls within the existing mobile one-column layout and touch-target conventions.

Tests

Backend unit/integration coverage must include:

  • utility model application returning complete config snapshots;
  • config-update notification fallback and bounded timeout;
  • shared ACP dialect normalization;
  • host utility cache key, TTL, refresh invalidation, and single-flight behavior;
  • controller request validation and sanitized errors; and
  • resolver HTTP response shape while the baseline GET contract remains stable.

Frontend unit/component coverage must include:

  • resolver API request/response mapping;
  • dynamic option replacement rather than stale merging;
  • model-switch reconciliation and preservation on failure;
  • latest-request-wins behavior;
  • workflow rule rendering and serialization;
  • profile form rendering and save behavior; and
  • generic unknown option identifiers and choices.

E2E Tests and Documentation

Update the mock ACP provider and existing settings specs so the selected model, not the static boot snapshot, is what causes the dependent option to appear. Cover both desktop flows:

  • workflow session override selects a model and saves a dynamic option;
  • OpenCode/ACP profile selects a model, saves a dynamic option, reloads, and displays it.

Cover the mobile workflow path with touch interaction, one-column layout, and no document-level horizontal overflow. Keep the existing managed Playwright runner and project split; do not run the mobile spec through desktop Chromium.

Update docs/public/tasks-and-workflows.md with the provider-neutral behavior, model-dependent option discovery, and failure/retry expectations if the public documentation coverage inventory requires the change. Keep the spec and ADR as the detailed contract.

Waves

Wave 1:

Wave 2 (depends on the probe contract):

Wave 3 (depends on the HTTP contract):

Wave 4 (depends on the hook):

Wave 5 (depends on the settings behavior):

The dependency chain is complete. The implementation uses a model-only request from the current settings consumers, while the backend keeps the optional mode and option context available for providers that need it later. Save contributors remain blocked until a selected model has an authoritative resolution and any stale draft options have been reconciled. Failed discovery keeps the draft readable and exposes a localized retry action.

Verification

Targeted backend checks:

cd apps/backend && go test ./internal/agentctl/server/utility ./internal/agentctl/acpcompat ./internal/agent/hostutility ./internal/agent/settings/controller ./internal/agent/settings/handlers

Targeted frontend checks:

cd apps && pnpm --filter @kandev/web exec vitest run hooks/domains/settings/use-dynamic-models.test.ts components/settings/profile-form-fields.test.tsx components/settings/workflow-session-config-editor.test.tsx components/settings/use-workflow-draft-contributor.test.ts components/settings/model-config-resolution-status.test.tsx components/settings/profile-model-config.test.ts components/model-config-selector.test.tsx --reporter=dot
cd apps/web && pnpm run typecheck
cd apps/web && pnpm run i18n:check
cd apps/web && pnpm run i18n:ratchet

Managed desktop and mobile E2E checks:

cd apps/web && pnpm e2e:run --host --project chromium tests/settings/agent-profile-acp.spec.ts tests/workflow/workflow-settings.spec.ts
cd apps/web && pnpm e2e:run --host --project mobile-chrome tests/workflow/mobile-workflow-settings.spec.ts

Public docs checks when the public page changes:

node --test scripts/validate-public-docs.test.mjs
node scripts/validate-public-docs.mjs

Verification Results:

  • Backend build passed; focused backend packages: 307 tests passed across five packages.
  • Frontend focused unit tests: 42 tests passed across the seven listed files; typecheck, ESLint, i18n checks, and i18n ratchet passed.
  • Managed E2E passed: 19 Chromium desktop tests and 3 mobile Chromium tests across profile and workflow settings. The tests also cover startup capability revalidation, so provider registration order and probe timing are not test assumptions.
  • Public documentation checks were not required because this change updates the implementation plan and workflow spec, not docs/public/**.

Review follow-up

The implementation completed the following review work:

  • Make the current settings resolver model-only. Do not send editable option values or mode as context from the profile and workflow consumers.
  • Remove probe mutations that are applied after the returned snapshot, or implement and test final-snapshot capture for a future multi-input contract.
  • Show localized resolution loading/failure state and provide retry in both settings surfaces.
  • Test controller error sanitization and authentication status mapping.
  • Reconcile only after a user model/context change so opening a saved profile does not create unsaved changes.
  • Complete the recommended cache, context-cancellation, conversion-helper, wrapper, mock-agent, and missing probe/host-utility test cleanup.
  • Remove E2E force-click workarounds after the selector no longer remounts for each option edit, then rerun desktop and mobile E2E.
  • Block profile and workflow saves during model-option reconciliation, require a snapshot after the final ACP option mutation, isolate forced refresh generations, use typed missing-agent errors, and keep zero-baseline resolution status visible.
  • Mark ACP inference agents as dynamic before their first cache snapshot, poll pending capability probes with a bounded retry, and make settings E2E fixtures independent of provider registration order.

Risks and open questions

  • Some ACP providers may advertise model changes only through delayed notifications. The probe needs a bounded wait that is long enough for normal providers but cannot hold a settings request indefinitely.
  • Some providers may require a mode or prior option before returning the final snapshot. The API accepts those inputs now, but the first UI should only send values it can prove are current.
  • Model resolution can be expensive for large model lists. The TTL, single-flight cache, and refresh semantics should be measured with the mock provider before broadening the UI to prefetch.
  • If a provider cannot produce a complete post-model snapshot, the UI must remain safe and understandable without implying that the baseline is authoritative for the selected model.