Skip to content

feat(providers): add Devin CLI provider via ACP - #11239

Closed
adampeterhiggins wants to merge 7 commits into
pingdotgg:mainfrom
adampeterhiggins:devin-provider
Closed

adampeterhiggins wants to merge 7 commits into
pingdotgg:mainfrom
adampeterhiggins:devin-provider

Conversation

@adampeterhiggins

@adampeterhiggins adampeterhiggins commented Sep 11, 2026 •

Copy link
Copy Markdown

Summary

  • Adds a Devin provider (devin acp over Agent Client Protocol, same architecture as the Cursor provider): sessions, streaming, steering, interrupt/stop, permission requests, elicitation handling, and text generation.
  • Devin's CLI encodes reasoning effort, speed tier, and context window inside the model uid (claude-opus-5-high-fast, glm-5-2-max-1m, MODEL_GPT_5_2_LOW), so devinModelCatalog groups devin models list --format json into one picker row per family with effort / speed / context option descriptors. Families whose variant uids share no base (MODEL_PRIVATE_*) are grouped via label-derived dims, with the concrete uid carried in the option choice. At dispatch the adapter resolves { model, effort, speed, context } back to a concrete advertised uid by parsed-dims matching — irregular uids and missing combinations relax instead of failing the turn.
  • Provider registry treats Devin like the other catalog-discovered providers: a successful probe replaces the model inventory so retired/renamed variants don't linger.
  • Settings contract (providers.devin, opt-in like Cursor/Grok), onboarding wizard entry with install/sign-in actions, official Devin mark on web and mobile, docs page.

Details

  • Spawn: devin --model <uid> --permission-mode <mode> acp; mode picks the closest advertised ACP mode (accept-edits / smart / ask / plan / bypass).
  • Auth: probes devin --version + devin auth status; ACP advertise devin-browser auth.
  • Permissions: session/request_permission maps to request.opened with risk classification; session/elicitation maps to user-input.requested (URL-mode elicitations are cancelled — chat can't answer them).
  • Text generation (commit messages, PR content, thread titles) uses ask mode with swe-2 family default.
  • Backward compat: stored flat variant selections (swe-2-high) resolve to grouped rows via aliases.

Test plan

  • apps/server focused tests: catalog grouping/resolution, ACP support (spawn args, mode mapping, model selection), adapter prompt flow, provider snapshot/auth parsing — 39+ tests
  • ProviderRegistry merge tests for stale Devin rows
  • Typecheck: contracts, server, web, mobile
  • Verified live against devin acp: session start, model apply, streaming deltas, permission prompt
  • CI

Generated with Devin

Summary by CodeRabbit

  • New Features

    • Added Devin as an Early Access provider for agent sessions and text generation.
    • Added Devin setup, authentication, enablement, and provider status support.
    • Added Devin model discovery with Adaptive defaults and configurable effort, speed, and context options.
    • Added Devin branding across provider lists, settings, onboarding, and chat interfaces.
    • Added permission-mode mappings, plan-mode support, session handling, and approval prompts.
  • Documentation

    • Added Devin installation, setup, model, and permission-mode guidance.
    • Updated provider documentation links.

adampeterhiggins and others added 7 commits September 11, 2026 11:06
Add a first-class Devin provider so T3 Code can run Devin's SWE models
through the local `devin` CLI's ACP server.

- Contracts: `DevinSettings` (enabled, binaryPath, customModels), provider
  kind, "adaptive" default model, "swe-1-6-fast" text-generation default.
- `DevinAcpSupport` builds `devin --permission-mode <mode> acp` spawn args,
  maps T3 runtime modes onto Devin's session modes (accept-edits, smart,
  ask, plan, bypass), and applies model selection through the session's
  `model` config option.
- `DevinAdapter` runs sessions over `AcpSessionRuntime`: prompt streaming,
  steering, interrupt/stop, permission requests via `request.opened`,
  and `session/elicitation` mapped to user-input questions (form mode only;
  URL-mode elicitations are cancelled).
- `DevinProvider` probes `devin --version` / `auth status` and discovers
  models via `devin models list --format json` with a static SWE fallback
  catalog; `devin update` provides maintenance.
- `DevinDriver` registers the provider (multi-instance capable) and
  `DevinTextGeneration` reuses the ACP runtime in `ask` mode for commit
  messages, PR content, branch names, and thread titles.
- Web/mobile: provider icon, driver metadata, settings search, composer
  draft persistence, and status ordering. Docs: providers-devin page.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The "Your agents" step only showed Claude Code and Codex. Add Devin as a
primary agent card with its install command (cli.devin.ai install.sh /
setup.ps1) and `devin auth login` for the sign-in terminal. Since Devin is
opt-in, a disabled card now offers an Enable button that flips the
providerInstances envelope instead of a dead "Disabled" label.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Replace the placeholder "D" letterform with the official Devin mark
(three interlocking hexagons, 24x24, currentColor) sourced from the
published vector used by @lobehub/icons.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The picker's display-name helper strips a leading subProvider qualifier
from the model name. Devin variant labels are already fully qualified
("Inkling High", "SWE-2 Max"), so tagging them with the family slug made
rows render as just the effort suffix ("High") with "Devin · inkling" as
the subtitle. Drop subProvider from the catalog rows.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… options

Devin encodes effort, speed, and context in the model uid itself
(`claude-opus-5-high-fast`, `glm-5-2-max-1m`), so the picker showed one
row per variant — ~215 entries — instead of one row per model with
selectable axes like other providers.

`devinModelCatalog` now parses each variant uid right-to-left (context,
speed, effort suffixes, `-`/`_` separators) and groups variants by base
into a single `ServerProviderModel` per family with `effort`/`speed`/
`context` option descriptors. Bare variants contribute a "Default"
effort choice; variant uids are kept as aliases so stored flat
selections still resolve. The adapter folds dim selections back into a
concrete advertised uid by parsed-dims matching rather than string
composition, so irregular uids (`MODEL_GPT_5_2_LOW`, `claude-5-fable-*`)
and missing combinations relax instead of failing the turn.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The registry's snapshot merge retained all previously seen models for
every driver except codex/antigravity/opencode, so Devin's old per-variant
rows survived each probe alongside the grouped family rows. Give Devin
the same rule: a ready probe's discovered list replaces the inventory,
while warning/error/disabled/unauthenticated snapshots keep the last good
catalog.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Families like gpt-5.1 and claude-sonnet-4.5 carry their effort tiers in
uids that share no base (`MODEL_PRIVATE_12..15`, `MODEL_PRIVATE_2/3`), so
uid-suffix parsing left them as separate rows. Variants in one family are
the same model by definition — when the uids can't prove it, parse the
label remainder ("GPT-5.1 Low Thinking" → low) and emit a single effort
select whose choices carry the concrete uid. The resolver now passes a
dim selection that is itself an advertised model value straight through,
so opaque families resolve without a shared base.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 11, 2026
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: cc3f69aa-84d7-466a-b400-ddb5fc23eac3

📥 Commits

Reviewing files that changed from the base of the PR and between 211618f and a485b51.

📒 Files selected for processing (34)
  • apps/mobile/src/components/ProviderIcon.tsx
  • apps/mobile/src/lib/modelOptions.ts
  • apps/server/src/provider/Drivers/DevinDriver.ts
  • apps/server/src/provider/Layers/DevinAdapter.test.ts
  • apps/server/src/provider/Layers/DevinAdapter.ts
  • apps/server/src/provider/Layers/DevinProvider.test.ts
  • apps/server/src/provider/Layers/DevinProvider.ts
  • apps/server/src/provider/Layers/ProviderRegistry.test.ts
  • apps/server/src/provider/Layers/ProviderRegistry.ts
  • apps/server/src/provider/Services/DevinAdapter.ts
  • apps/server/src/provider/acp/DevinAcpSupport.test.ts
  • apps/server/src/provider/acp/DevinAcpSupport.ts
  • apps/server/src/provider/builtInDrivers.ts
  • apps/server/src/provider/devinModelCatalog.test.ts
  • apps/server/src/provider/devinModelCatalog.ts
  • apps/server/src/provider/providerStatusCache.ts
  • apps/server/src/serverSettings.test.ts
  • apps/server/src/serverSettings.ts
  • apps/server/src/textGeneration/DevinTextGeneration.ts
  • apps/server/src/textGeneration/TextGeneration.ts
  • apps/web/src/components/Icons.tsx
  • apps/web/src/components/chat/providerIconUtils.ts
  • apps/web/src/components/onboarding/WelcomeWizard.tsx
  • apps/web/src/components/settings/ProviderModelsSection.tsx
  • apps/web/src/components/settings/providerDriverMeta.ts
  • apps/web/src/components/settings/settingsSearch.ts
  • apps/web/src/composerDraftStore.ts
  • apps/web/src/onboarding/providerReadiness.logic.ts
  • docs/README.md
  • docs/user/install.md
  • docs/user/permission-modes.md
  • docs/user/providers-devin.md
  • packages/contracts/src/model.ts
  • packages/contracts/src/settings.ts

📝 Walkthrough

Walkthrough

This PR adds Devin as a built-in provider. It adds settings, model catalogs, ACP sessions, provider discovery, text generation, onboarding support, icons, persistence, tests, and documentation.

Changes

Devin provider integration

Layer / File(s) Summary
Contracts, model catalog, and ACP mapping
packages/contracts/src/*, apps/server/src/provider/devinModelCatalog.ts, apps/server/src/provider/acp/*
Adds Devin settings and defaults, groups model variants, resolves concrete model UIDs, and maps runtime permission and session modes to Devin ACP configuration.
Provider discovery and server registration
apps/server/src/provider/Layers/DevinProvider.ts, apps/server/src/provider/Drivers/DevinDriver.ts, apps/server/src/provider/Layers/ProviderRegistry.ts, apps/server/src/serverSettings.ts, apps/server/src/provider/builtInDrivers.ts
Adds CLI probes, provider snapshots, maintenance handling, degraded model retention, persisted settings, and built-in driver registration.
ACP adapter and session lifecycle
apps/server/src/provider/Layers/DevinAdapter.ts, apps/server/src/provider/Services/DevinAdapter.ts, apps/server/src/provider/Layers/DevinAdapter.test.ts
Adds session start and stop, turn steering, event streaming, image handling, permissions, elicitation, resume cursors, interruption, and cleanup.
Devin text generation
apps/server/src/textGeneration/DevinTextGeneration.ts, apps/server/src/textGeneration/TextGeneration.ts
Adds structured Devin generation for commit messages, pull request content, branch names, and thread titles.
Client integration and documentation
apps/web/src/components/*, apps/web/src/onboarding/*, apps/web/src/composerDraftStore.ts, apps/mobile/src/*, docs/user/*, docs/README.md
Adds Devin icons, provider settings metadata, onboarding enablement, model-option handling, install and login commands, mobile labels, and provider documentation.

Estimated code review effort: 5 (Critical) | ~90 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant T3Code
  participant DevinDriver
  participant DevinProvider
  participant DevinAdapter
  participant DevinCLI
  T3Code->>DevinDriver: Create Devin provider instance
  DevinDriver->>DevinProvider: Check CLI status and models
  DevinProvider->>DevinCLI: Run version, auth, and model commands
  DevinProvider-->>DevinDriver: Return provider snapshot
  T3Code->>DevinAdapter: Start ACP session
  DevinAdapter->>DevinCLI: Spawn devin acp
  DevinCLI-->>DevinAdapter: Stream ACP events
  DevinAdapter-->>T3Code: Emit turns, content, and approval events
Loading

Suggested reviewers: t3dotgg, juliusmarminge, maria-rcks

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

for (const selection of input.selections ?? []) {
if (isDevinModelDimOptionId(selection.id)) continue;
const option = configOptions.find((candidate) => candidate.id === selection.id);
if (!option || option.id === "model") continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High acp/DevinAcpSupport.ts:249

An approval-required turn can run with bypass instead of requiring approval when selections contain { id: "mode", value: "bypass" }. applyDevinAcpModelSelection forwards mode through setConfigOption after spawning with normal, while resolveDevinModeId does not replace an already-writable mode; exclude mode here so the runtime-mode resolver remains its sole owner.

-      if (!option || option.id === "model") continue;
+      if (!option || option.id === "model" || option.id === "mode") continue;
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/DevinAcpSupport.ts around line 249:

An approval-required turn can run with `bypass` instead of requiring approval when selections contain `{ id: "mode", value: "bypass" }`. `applyDevinAcpModelSelection` forwards `mode` through `setConfigOption` after spawning with `normal`, while `resolveDevinModeId` does not replace an already-writable mode; exclude `mode` here so the runtime-mode resolver remains its sole owner.

];
const { enabled: _legacyEnabled, ...legacyConfigRest } = legacyConfig ?? {};
updateSettings({
providerInstances: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium onboarding/WelcomeWizard.tsx:702

Enabling two disabled agents before the first settings update is reflected causes the later update to replace providerInstances with a stale snapshot and silently remove the earlier enabled envelope. enableProvider spreads settings.providerInstances captured during render, while updateSettings replaces the entire map; merge each entry against the latest settings or serialize these updates so concurrent enables are preserved.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/onboarding/WelcomeWizard.tsx around line 702:

Enabling two disabled agents before the first settings update is reflected causes the later update to replace `providerInstances` with a stale snapshot and silently remove the earlier enabled envelope. `enableProvider` spreads `settings.providerInstances` captured during render, while `updateSettings` replaces the entire map; merge each entry against the latest settings or serialize these updates so concurrent enables are preserved.

Comment on lines +504 to +522
if (!wanted.hasAny && input.advertisedValues.includes(model)) return model;

const wantedBase = normalizeDevinModelId(model);
const candidates = input.advertisedValues.filter(
(uid) => parseDevinModelUid(uid).base === wantedBase,
);
if (candidates.length === 0) return model;

if (!wanted.hasAny) {
// No dims selected: keep the session's current value when it already
// belongs to this family, else the bare variant, else first advertised.
if (input.currentValue && candidates.includes(input.currentValue)) {
return input.currentValue;
}
const bare = candidates.find((uid) => {
const dims = parseDevinModelUid(uid);
return dims.effort === undefined && dims.speed === undefined && dims.context === undefined;
});
return bare ?? candidates[0]!;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium provider/devinModelCatalog.ts:504

When no non-default dimensions are selected, resolveDevinModelUid resets a session from currentValue such as glm-5-2-max to the grouped model slug glm-5-2. The early exact-match return at line 504 prevents the later currentValue preservation branch from running; defer the model pass-through until after checking whether currentValue belongs to the family.

-  if (!wanted.hasAny && input.advertisedValues.includes(model)) return model;
-
   const wantedBase = normalizeDevinModelId(model);
@@
     if (input.currentValue && candidates.includes(input.currentValue)) {
       return input.currentValue;
     }
+    if (input.advertisedValues.includes(model)) return model;
     const bare = candidates.find((uid) => {
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/devinModelCatalog.ts around lines 504-522:

When no non-default dimensions are selected, `resolveDevinModelUid` resets a session from `currentValue` such as `glm-5-2-max` to the grouped `model` slug `glm-5-2`. The early exact-match return at line 504 prevents the later `currentValue` preservation branch from running; defer the model pass-through until after checking whether `currentValue` belongs to the family.

});

const DEVIN_PLAN_MODE_ALIASES = ["plan", "architect"];
const DEVIN_READ_ONLY_MODE_IDS = new Set(["plan", "ask"]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High acp/DevinAcpSupport.ts:121

An approval-required turn remains in read-only architect mode after a plan turn, so normal editing requests cannot proceed. architect is accepted by DEVIN_PLAN_MODE_ALIASES but missing from DEVIN_READ_ONLY_MODE_IDS, preventing the restore path from selecting a writable mode; add it to the read-only IDs.

Suggested change
const DEVIN_READ_ONLY_MODE_IDS = new Set(["plan", "ask"]);
const DEVIN_READ_ONLY_MODE_IDS = new Set(["plan", "ask", "architect"]);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/DevinAcpSupport.ts around line 121:

An approval-required turn remains in read-only `architect` mode after a plan turn, so normal editing requests cannot proceed. `architect` is accepted by `DEVIN_PLAN_MODE_ALIASES` but missing from `DEVIN_READ_ONLY_MODE_IDS`, preventing the restore path from selecting a writable mode; add it to the read-only IDs.

// A sendTurn while a prompt is in flight is a steer: the agent folds
// the new prompt into the ongoing work, so the active turn id is
// reused instead of opening a new turn.
const steeringTurnId = ctx.promptsInFlight > 0 ? ctx.activeTurnId : undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High Layers/DevinAdapter.ts:807

A concurrent second sendTurn creates a separate turnId and emits another turn.started, leaving the first turn without a terminal event and attributing streamed output to the wrong turn. ctx.promptsInFlight is incremented before ctx.activeTurnId is assigned, so the second call sees an active prompt but no active ID; reserve ctx.activeTurnId immediately after choosing turnId.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/DevinAdapter.ts around line 807:

A concurrent second `sendTurn` creates a separate `turnId` and emits another `turn.started`, leaving the first turn without a terminal event and attributing streamed output to the wrong turn. `ctx.promptsInFlight` is incremented before `ctx.activeTurnId` is assigned, so the second call sees an active prompt but no active ID; reserve `ctx.activeTurnId` immediately after choosing `turnId`.

@macroscopeapp

macroscopeapp Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This 4,038-line change introduces a new production ACP provider with external process execution, authentication, permissions, sessions, model discovery, onboarding, and text generation, along with new product defaults. It also adds a static-analysis suppression and retains unresolved Medium/High findings involving permission enforcement, concurrency, model selection, and settings updates.

Not approved because:

  • 5 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant