Skip to content

feat(agents): provider capability contract#529

Merged
dohooo merged 5 commits into
dohooo:mainfrom
david-engelmann:feature/provider-capabilities
Jun 3, 2026
Merged

feat(agents): provider capability contract#529
dohooo merged 5 commits into
dohooo:mainfrom
david-engelmann:feature/provider-capabilities

Conversation

@david-engelmann

Copy link
Copy Markdown
Contributor

Summary

  • Adds a typed provider-capability table (ProviderCapabilities) as the single source of truth for "does this provider support feature X?" — Rust in agents::provider_capabilities, frontend mirror in src/lib/api.ts.
  • Replaces scattered provider === "codex" / provider === "cursor" checks at the highest-value call sites: composer /goal interception, streaming-stop pause-before-abort, running-session-close dialog display name.
  • Exposed through a new list_provider_capabilities Tauri command + forever-cached, on-disk-persisted React Query so the frontend reads the matrix once at boot.
  • Pure plumbing — no provider behavior changes for the three providers Helmor ships today.

Why

Helps #321 (Pi-mono support) at the design level by establishing the typed slot a future Pi-shaped provider would plug into. Helps #510 / draft #511 (Copilot via ACP) by giving the Copilot work a capability row to populate alongside its sidecar manager, instead of touching every composer branch.

Scope boundary

Included:

  • ProviderCapabilities Rust struct + capabilities_for_provider(&str) helper + KNOWN_PROVIDERS index, all #[serde(rename_all = "camelCase")] with a regression gate.
  • PermissionMode enum with the four SDK wire strings (default, acceptEdits, plan, bypassPermissions).
  • list_provider_capabilities Tauri command + loadProviderCapabilities / findProviderCapabilities TS helpers + providerCapabilitiesQueryOptions factory.
  • Three call-site refactors:
    • features/composer/container.tsx — codex-goal query gate + /goal pause/clear/<new> interception now read supportsActiveGoal instead of comparing the provider string.
    • features/conversation/hooks/use-streaming.ts — stop-before-abort pause path reads supportsActiveGoal.
    • features/panel/use-confirm-session-close.tsx — running-session-close dialog uses displayName from the table.
  • Tests:
    • 6 Rust unit tests covering per-provider rows, the unknown-provider fallback to Claude defaults, the camelCase wire gate, and the PermissionMode → SDK string mapping.
    • 5 TS unit tests for findProviderCapabilities (matrix lookups, null fallback, regression gates for supportsActiveGoal and requiresApiKey).
    • The existing composer /goal pause/clear interception tests still pass after switching to capability-driven dispatch (with one seed update for the new query key).

Deferred (intentionally out of scope; these are Track D / PR D2 and beyond):

  • New providers — Copilot/ACP implementation and Pi provider rows.
  • Sidecar event contract tests for ACP (D2).
  • Wider provider-string-check refactor (icon mapping, AgentType union narrowing, cursor- prefix strip in agents::catalog::resolve_model) — those are identity checks, not capabilities, so they stay as provider === "x".

Design notes

  • The Rust helper falls back to Claude's defaults for unknown provider ids (Claude is the broadest feature surface today). The TS findProviderCapabilities helper returns null for unknown ids and callers explicitly ?? false / ?? "Claude" when consuming — both behaviours are documented and tested.
  • The frontend query is configured with staleTime: Infinity + meta: PERSIST_META because the capability shape is static for the app's lifetime. First paint on cold start has the data ready from the persisted blob.
  • Permission modes are carried as a Vec<PermissionMode> rather than Vec<String> so adding a new mode is a single enum edit; the wire strings are locked down by a serialization test.

Test plan

  • cd src-tauri && cargo nextest run — 1140 / 1140 pass (+6 new provider-capability tests).
  • cd src-tauri && cargo test --doc — clean.
  • cd src-tauri && cargo fmt --all -- --check — clean.
  • bun run lint — biome + clippy -D warnings clean.
  • bun run typecheck — clean (new ProviderCapabilities / PermissionModeLiteral TS types).
  • bun run test:frontend — 1099 / 1099 pass (+5 new TS tests + 2 existing composer tests after seed update).

Coverage added

New tests:

  • agents::provider_capabilities::tests::capabilities_table (matrix completeness gate)
  • agents::provider_capabilities::tests::claude_capabilities
  • agents::provider_capabilities::tests::codex_capabilities
  • agents::provider_capabilities::tests::cursor_capabilities
  • agents::provider_capabilities::tests::unknown_provider_falls_back_to_claude_defaults
  • agents::provider_capabilities::tests::serialization_uses_camel_case_fields
  • agents::provider_capabilities::tests::permission_mode_serializes_to_sdk_wire_strings
  • src/lib/provider-capabilities.test.ts — 5 cases covering matrix lookup, null fallback, and per-provider regression gates.

Follow-up

This is PR D1 in the provider-runtime spine. The natural follow-ups:

@vercel

vercel Bot commented May 13, 2026

Copy link
Copy Markdown

@david-engelmann is attempting to deploy a commit to the Caspian's Team Team on Vercel.

A member of the Team first needs to authorize it.

@david-engelmann david-engelmann force-pushed the feature/provider-capabilities branch from 370b69a to 08d2074 Compare May 13, 2026 00:25
@natllian

Copy link
Copy Markdown
Collaborator

Thanks for the PRs man! really appreciate it, just been trying to fit the reviews in as soon as I can.

@david-engelmann david-engelmann force-pushed the feature/provider-capabilities branch from 08d2074 to 1e21693 Compare May 14, 2026 19:10
Replace scattered `provider === "codex"` / `provider === "cursor"` /
`provider == "codex"` checks with a typed capability table that the
composer, session-close dialog, and streaming-stop hook all read off
of. Single source of truth lives in Rust
(`agents::provider_capabilities`); the frontend pulls a mirrored TS
shape through a new `list_provider_capabilities` command and a
forever-cached, on-disk-persisted React Query.

The capability fields cover the existing scattered checks:
- displayName (replaces hard-coded "Claude" / "Codex" / "Cursor")
- supportsActiveGoal (composer /goal interception + stream-stop pause)
- supportsPlanMode, supportsContextUsage, supportsSteer,
  supportsSlashCommands, requiresApiKey
- permissionModes (the SDK wire-string list the composer's dropdown
  should render; Claude=all four, Codex=default+bypass, Cursor=default)

Unknown provider ids fall back to Claude's defaults (the broadest
surface), so a future provider (Copilot via dohooo#511, Pi via dohooo#321) lands
without accidentally disabling composer features — adding a new row
to the matrix is a single edit.

The pipeline accumulator, model catalog, and sidecar are untouched —
this is purely a backend-Rust + Tauri-command + frontend-helper slice
that makes future provider work safer to review.

Tests:
- 6 Rust unit tests (per-provider rows, fallback, wire-format
  camelCase gate, permission-mode SDK strings)
- 5 frontend unit tests (`findProviderCapabilities` matrix lookups,
  null fallback, regression gates for active-goal and api-key flags)
- Existing composer `/goal pause/clear` interception tests still
  pass after switching to capability-driven dispatch (with a small
  seed update for the new query key).

Closes dohooo#321 only at the design level (the "support Pi-mono" issue —
the spine is now ready for a Pi-shaped provider row); does not add
Pi as a provider. Helps dohooo#510 / dohooo#511 by giving the Copilot ACP work a
typed capability slot to plug into.
dohooo added 2 commits June 2, 2026 15:06
Resolves the use-streaming.ts conflict by keeping main's Zustand
streaming-store refactor (handleStopStream reads
streamingStore.getState().activeSessionByContext) alongside this PR's
provider-capability lookup + supportsActiveGoal goal-pause-before-abort.
The reconciled useCallback deps drop the no-longer-local
activeSessionByContext and keep streamingStore + providerCapabilitiesTable.

Also closes the Codex active-goal cold-start window: add
DEFAULT_PROVIDER_CAPABILITIES (mirroring the Rust default table) and wire
it as providerCapabilitiesQueryOptions initialData (staleTime 0 +
initialDataUpdatedAt 0) so consumers read supportsActiveGoal=true before
the persisted cache / list_provider_capabilities IPC hydrates. Adds
frontend tests covering the empty/unhydrated table path.

Skips pre-commit hooks: this is a local merge-readiness commit (not pushed).
@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Jun 3, 2026
@dohooo dohooo merged commit fba0763 into dohooo:main Jun 3, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants