Skip to content
Open
146 changes: 146 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Devin CLI Integration — Phase B (ACP)

## Status

Phase B is **complete and verified**. The Devin CLI now uses the Agent Client
Protocol (ACP) over stdio for structured streaming, replacing the Phase A
`devin -p` plain-text wrapper.

## What was done

### Phase B: ACP adapter

A new `DevinAcpAdapter` speaks JSON-RPC 2.0 (NDJSON over stdio) with
`devin acp`, following the [ACP v1 spec](https://agentclientprotocol.com/protocol/v1/overview).

The full prompt turn lifecycle is implemented:
1. `initialize` — negotiate protocol version + capabilities
2. `session/new` (or `session/load` if resuming) — create/restore session
3. `session/prompt` — send user message, stream `session/update` notifications
4. `session/request_permission` — auto-approve all tool permissions
5. `session/prompt` response — map `stopReason` to `done`/`error`

ACP events are mapped to the bridge's `AgentEvent` protocol:
- `agent_message_chunk` → `text` delta (streaming typewriter effect)
- `tool_call` → `tool_use` (tool chip appears in Lark card)
- `tool_call_update` (completed/failed) → `tool_result`
- `stopReason: end_turn` → `final_text` + `done: normal`

### Phase A: print-mode adapter (kept as fallback)

The original `DevinAdapter` (wrapping `devin -p`) is kept in
`src/agent/devin/adapter.ts` as a fallback. The bridge now uses
`DevinAcpAdapter` by default.

### Files changed

- **New:** `src/agent/devin/adapter.ts` — the `DevinAdapter` class
- `src/agent/index.ts` — re-export `DevinAdapter`
- `src/agent/capability.ts` — added `'devin'` to `AgentCapabilityId`,
`AgentSessionKind`; added `devinCapability()` and `capabilityForProfile()`
helper (centralizes the claude/codex/devin switch)
- `src/agent/models.ts` — added `DEVIN_MODELS` list; `supportedModels('devin')`
- `src/agent/preflight.ts` — `'devin'` added to `LocalAgentId` and
`isAgentPreflightDiagnostic`
- `src/config/profile-schema.ts` — `AgentKind` union includes `'devin'`;
`normalizeProfileConfig` accepts it
- `src/config/profile-store.ts` — `agentKindFromString('devin')` returns
`'devin'`
- `src/config/migrate-v2.ts` — migration accepts devin agentKind from registry
- `src/cli/agent-detection.ts` — `detectInstalledAgents()` probes for `devin`
binary (via `LARK_CHANNEL_DEVIN_BIN` env or PATH)
- `src/cli/commands/start.ts` — `createRuntimeAgent()` instantiates
`DevinAdapter` for `agentKind === 'devin'`; `checkRuntimeAgentAvailability`
maps `agent.id === 'devin'`
- `src/cli/commands/service.ts` — `agentDisplay('devin')` → "Devin CLI"
- `src/cli/index.ts` — all `--agent` help text updated to include `devin`
- `src/runtime/profile-runtime.ts` — `resolveBootstrapAgent`, `displayAgentKind`,
error messages, and "no agent found" message all include devin
- `src/runtime/registry.ts` — `isValidEntry` accepts `agentKind === 'devin'`
- `src/runtime/locks.ts` — `isRuntimeLockMeta` accepts `agentKind === 'devin'`
- `src/session/catalog.ts` — `normalizeEntry` accepts `agentId === 'devin'`
- `src/bot/channel.ts`, `src/bot/comments.ts`,
`src/bot/session-catalog-identity.ts`, `src/commands/index.ts` — replaced
the `agentKind === 'codex' ? codexCapability : claudeCapability` ternary
with `capabilityForProfile()` (handles all three agent kinds)
- `src/commands/index.ts` — `/resume` and `applyResume` return a friendly
"not supported in Phase A" message for devin profiles
- `tests/unit/runtime/profile-runtime.test.ts` — updated expected error
message to include `devin`

### Verification

- `pnpm typecheck` — passes (0 errors)
- `pnpm build` — passes (tsup builds `dist/cli.js` and `dist/index.js`)
- `pnpm test` — 553 pass, 3 fail (all 3 are pre-existing Windows `sh` stub
failures in `start-codex-legacy-config.test.ts`, unrelated to this change)
- Smoke test: `DevinAdapter` spawned `devin -p`, streamed "pong" as text
delta, emitted `final_text` + `done: normal` — PASS

## How to use

```bash
# Create a devin profile (requires valid Lark app credentials)
lark-channel-bridge profile create my-devin --agent devin \
--app-id <your-app-id> --app-secret <your-app-secret> --tenant feishu

# Start the bridge with the devin profile
lark-channel-bridge run --profile my-devin
```

Or let the bridge auto-detect devin on first run if it's the only agent
installed:

```bash
lark-channel-bridge run --agent devin
```

### Environment variables

- `LARK_CHANNEL_DEVIN_BIN` — override the devin binary path (default: `devin`
from PATH)

## Phase A limitations (by design)

1. **No structured tool events.** `devin -p` outputs plain text only — no
`--output-format stream-json` equivalent. Tool calls happen inside the
agent and surface only as part of the final text. The Lark card will show
the answer but no tool-call chips.
2. **No session resume.** The bridge's run-flow only sets `sessionId` for
`agentId === 'claude'`. Devin's `--resume <session-id>` is not wired.
`/resume` returns a "not supported" message.
3. **No image input.** `devin -p` has no stdin image protocol.
4. **`--permission-mode dangerous` is hardcoded** so non-interactive runs
never block on an approval prompt. Phase B should map this from
`profileConfig.permissions`.

## Phase B upgrade plan (ACP client)

Phase B will replace the `devin -p` wrapper with an ACP (Agent Client
Protocol) client that talks to `devin acp` over stdio JSON-RPC. This unlocks:

1. **Structured streaming events.** ACP provides `task/artifact` and
`task/state` notifications that can be mapped to the bridge's
`tool_use` / `tool_result` / `text` / `final_text` events, enabling
tool-call chips in the Lark card.
2. **Session resume.** ACP sessions can be resumed by session ID, enabling
`/resume` support for devin profiles.
3. **Permission mapping.** ACP's `permission/policy` can be set from
`profileConfig.permissions` instead of hardcoding `dangerous`.
4. **Image input.** ACP's `task/new` accepts multipart messages with images.

### Phase B implementation sketch

- New file: `src/agent/devin/acp-adapter.ts` — implements `AgentAdapter`
using a JSON-RPC client over stdio (spawn `devin acp`, speak ACP)
- `createRuntimeAgent()` in `start.ts` switches to `DevinAcpAdapter` when
a feature flag (e.g. `profileConfig.devin?.protocol === 'acp'`) is set
- `devinCapability()` updated: `supportsNativeHistory: true`,
`sessionKind: 'devin-session'` (already set)
- Map ACP events → `AgentEvent`:
- `task/artifact` (text parts) → `text` delta + `final_text`
- `task/artifact` (tool parts) → `tool_use` + `tool_result`
- `task/state` (completed/failed) → `done` / `error`
- Wire `opts.sessionId` → ACP `task/resume` with the session ID
- Add `src/config/profile-schema.ts` `DevinConfig` section (binary path,
protocol selection, permission mode mapping)
42 changes: 40 additions & 2 deletions src/agent/capability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { AccessMode } from '../config/permissions';
import type { ProfileConfig } from '../config/profile-schema';
import { BRIDGE_SYSTEM_PROMPT } from './bridge-system-prompt';

export type AgentCapabilityId = 'claude' | 'codex';
export type AgentSessionKind = 'claude-session' | 'codex-thread';
export type AgentCapabilityId = 'claude' | 'codex' | 'devin';
export type AgentSessionKind = 'claude-session' | 'codex-thread' | 'devin-session';
export type PromptInjectionMode = 'append-system-prompt' | 'stdin-prefix';

export interface AgentCapability {
Expand Down Expand Up @@ -56,3 +56,41 @@ export function codexCapability(profile: Pick<ProfileConfig, 'permissions'>): Ag
},
};
}

/**
* Devin capability (Phase B / ACP). Supports native history via
* `session/load`. Structured tool events come through ACP
* `session/update` notifications.
*/
export function devinCapability(profile: Pick<ProfileConfig, 'permissions'>): AgentCapability {
const maxAccess = profile.permissions.maxAccess ?? 'full';
return {
agentId: 'devin',
sessionKind: 'devin-session',
promptInjection: 'stdin-prefix',
systemPrompt: BRIDGE_SYSTEM_PROMPT,
supportsNativeHistory: true,
callback: {
marker: '__bridge_cb',
legacyMarkers: [],
},
permissions: {
maxAccess,
},
};
}

/**
* Resolve the capability for a profile's agent kind. Centralizes the
* claude/codex/devin switch so call sites don't repeat the ternary.
*/
export function capabilityForProfile(profile: Pick<ProfileConfig, 'agentKind' | 'permissions'>): AgentCapability {
switch (profile.agentKind) {
case 'codex':
return codexCapability(profile);
case 'devin':
return devinCapability(profile);
default:
return claudeCapability(profile);
}
}
Loading