Skip to content

feat: integrate Devin CLI as a third agent kind (Phase A) - #200

Open
cweaty wants to merge 10 commits into
zarazhangrui:mainfrom
cweaty:feat/devin-cli-adapter
Open

feat: integrate Devin CLI as a third agent kind (Phase A)#200
cweaty wants to merge 10 commits into
zarazhangrui:mainfrom
cweaty:feat/devin-cli-adapter

Conversation

@cweaty

@cweaty cweaty commented Jul 18, 2026

Copy link
Copy Markdown

Summary

  • 新增 DevinAdapter,包装 devin -p --prompt-file 非交互模式,将 stdout 流式输出为 text delta + final_text + done 事件,完全匹配 bridge 的 AgentEvent 协议
  • 将所有 agent-kind 相关的类型联合、校验器、运行时、CLI、能力解析、会话目录等触点从 'claude' | 'codex' 扩展为 'claude' | 'codex' | 'devin'
  • 新增 capabilityForProfile() 辅助函数,集中处理三种 agent 的能力解析,替换了 4 处重复的三元表达式
  • /resume 对 devin profile 返回友好的"Phase A 暂不支持"提示

Phase A 设计限制

  1. 无结构化工具事件(devin -p 只输出纯文本)
  2. 无会话恢复(--resume 未接入)
  3. 无图片输入
  4. --permission-mode dangerous 硬编码

Phase B 将切换到 devin acp(JSON-RPC over stdio)以解锁结构化事件流、会话恢复、权限映射和图片输入。详见 AGENTS.md

使用方式

lark-channel-bridge profile create my-devin --agent devin \
  --app-id <id> --app-secret <secret> --tenant feishu
lark-channel-bridge run --profile my-devin

改动文件

  • 新增: src/agent/devin/adapter.ts, AGENTS.md
  • 修改: 20 个文件(类型定义、校验器、运行时、CLI、能力解析、会话目录、测试)

Test plan

  • pnpm typecheck — 0 错误
  • pnpm build — 成功
  • pnpm test — 553 通过,3 失败(均为预先存在的 Windows sh stub 问题,与本次改动无关)
  • 适配器冒烟测试 — devin -p 启动,流式输出 "pong",发出 final_text + done: normal
  • agentKindFromString('devin') 返回 'devin'
  • capabilityForProfile({ agentKind: 'devin' }) 返回 devin capability
  • supportedModels('devin') 返回正确的模型列表
  • CLI --help 显示 devin 选项

Generated with Devin

Add DevinAdapter wrapping `devin -p --prompt-file` to stream stdout as
text deltas + final_text + done, matching the bridge's AgentEvent
protocol. Extends all agent-kind touch points (types, validators,
runtime, CLI, capability resolution, session catalog) to support
'devin' alongside 'claude' and 'codex'.

Phase A limitations (by design): no structured tool events, no session
resume, no image input, hardcoded --permission-mode dangerous. Phase B
will switch to `devin acp` (JSON-RPC over stdio) for structured events.

Verified: typecheck 0 errors, build success, 553/556 tests pass (3
pre-existing Windows failures), adapter smoke test passes.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 18, 2026 12:19

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds Devin CLI as a third supported agentKind (Phase A) by introducing a DevinAdapter wrapper around devin -p --prompt-file and wiring 'devin' through the bridge’s type unions, validation, CLI UX strings, capability resolution, model picker, and session catalog metadata.

Changes:

  • Added DevinAdapter (Phase A) that spawns devin -p and streams stdout into bridge AgentEvents.
  • Extended agent-kind unions/validators/runtime registry/session catalog to include 'devin'.
  • Centralized capability selection via capabilityForProfile() and updated call sites; /resume now returns a friendly “not supported” message for Devin in Phase A.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/unit/runtime/profile-runtime.test.ts Updates expected CLI help/error text to include devin.
src/session/catalog.ts Allows session catalog entries to normalize agentId: 'devin'.
src/runtime/registry.ts Accepts 'devin' in persisted process registry validation.
src/runtime/profile-runtime.ts Updates bootstrap selection + user-facing messages to include devin.
src/runtime/locks.ts Accepts 'devin' in runtime lock metadata validation.
src/config/profile-store.ts Allows parsing agentKindFromString('devin').
src/config/profile-schema.ts Extends AgentKind union + schema validation to include devin.
src/config/migrate-v2.ts Migration logic accepts registry entries with agentKind: 'devin'.
src/commands/index.ts Adds Devin-aware /resume behavior and uses capabilityForProfile().
src/cli/index.ts Updates CLI --agent help text to include devin.
src/cli/commands/start.ts Instantiates DevinAdapter and updates availability diagnostics.
src/cli/commands/service.ts Displays “Devin CLI” for devin processes.
src/cli/agent-detection.ts Detects Devin CLI via LARK_CHANNEL_DEVIN_BIN or PATH.
src/bot/session-catalog-identity.ts Uses centralized capabilityForProfile() for policy evaluation context.
src/bot/comments.ts Uses centralized capabilityForProfile() when handling comment mentions.
src/bot/channel.ts Uses centralized capabilityForProfile() in run flow.
src/agent/preflight.ts Extends LocalAgentId/diagnostic validation to include devin.
src/agent/models.ts Adds DEVIN_MODELS and routes supportedModels('devin').
src/agent/index.ts Re-exports DevinAdapter.
src/agent/devin/adapter.ts New Phase A adapter implementing devin -p streaming into AgentEvents.
src/agent/capability.ts Adds devin capability + capabilityForProfile() helper.
AGENTS.md Documents Phase A behavior/limitations and Phase B ACP upgrade plan.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +6 to +15
import { mergeProcessEnv, spawnProcess, type SpawnedProcessByStdio } from '../../platform/spawn';
import { buildLarkChannelEnv, type LarkChannelEnvContext } from '../lark-channel-env';
import { checkAgentAvailability, type AgentAvailability } from '../preflight';
import type {
AgentAdapter,
AgentBotIdentity,
AgentEvent,
AgentRun,
AgentRunOptions,
} from '../types';
// Pass the prompt via a temp file (--prompt-file) so no special
// characters ever reach the Windows cmd.exe shim — same reason the
// Claude adapter avoids argv for the prompt.
const promptFile = writePromptFile(opts.prompt);
Comment on lines +254 to +257
if (accumulated.trim()) {
yield { type: 'final_text', content: accumulated };
}
yield { type: 'done', terminationReason: 'normal' };
cweaty and others added 9 commits July 18, 2026 20:49
Replace the `devin -p` plain-text wrapper with `DevinAcpAdapter` that
speaks JSON-RPC 2.0 (NDJSON over stdio) with `devin acp`, following the
Agent Client Protocol v1 spec.

ACP events mapped to the bridge's AgentEvent protocol:
- agent_message_chunk → text delta (streaming)
- tool_call → tool_use (tool chips in Lark card)
- tool_call_update → tool_result
- stopReason → done/error

Implements the full ACP lifecycle: initialize → session/new (or
session/load for resume) → session/prompt → session/update streaming →
session/request_permission (auto-approve) → done.

New files:
- src/agent/devin/acp-client.ts — minimal NDJSON JSON-RPC 2.0 client
- src/agent/devin/acp-adapter.ts — DevinAcpAdapter (ACP → AgentEvent)

Updated:
- start.ts now uses DevinAcpAdapter instead of DevinAdapter
- capability.ts: devin supportsNativeHistory = true (ACP session/load)
- AGENTS.md updated with Phase B status

Verified: typecheck 0 errors, build success, ACP smoke tests pass
(text streaming + tool_call/tool_result events), 552/556 tests pass
(4 pre-existing Windows failures).

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Previously each message spawned a new `devin acp` process with a fresh
session — no conversation history carried over. Now the adapter emits a
`system` event with the ACP sessionId after session/new, which the bridge
records in the session catalog. On the next message, the catalog entry
is looked up and the sessionId is passed to the adapter, which calls
`session/load` to resume the conversation.

Changes:
- acp-adapter.ts: emit `system` event with sessionId after session setup
- run-flow.ts: handle devin in session catalog lookup (same as claude)
- run-flow.ts: recordRunSessionEvent handles devin sessionId
- comments.ts: session resume for devin in doc-comment flows
- catalog.ts: isValidAgentEntry + assertAgentIdentity handle devin
  (uses sessionId like claude, not threadId like codex)

Verified: two-message smoke test confirms session resume — agent
remembers "42" from the first message when asked in the second.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
On Windows, `child.kill('SIGTERM')` only kills the direct child process,
not the entire process tree. `devin acp` spawns child processes (MCP
servers, telemetry, etc.) that inherit the stdout pipe and keep the
Node.js event loop alive even after the parent exits — so Ctrl+C hangs.

Fix:
- AcpClient.kill(): use `taskkill /PID <pid> /T /F` on Windows to kill
  the entire process tree; fall back to SIGTERM/SIGKILL on Unix
- DevinAcpAdapter.stop(): same tree-kill approach for the stop() path
- start.ts stop(): add 10s force-exit safety net so Ctrl+C always
  terminates even if bridge.disconnect() hangs

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Root cause: On Windows, `process.on('SIGINT')` does not reliably fire
when the process is launched via npm's .cmd/.ps1 wrappers in PowerShell.
The readline interface's 'SIGINT' event, however, fires reliably — but
only when created with `terminal: true`.

Fix: replace the `await new Promise(() => {})` keep-alive with a
readline interface in terminal mode that listens for 'SIGINT'. This
keeps the event loop alive AND reliably catches Ctrl+C on Windows.

Also added diagnostic logging in stop() and a 10s force-exit safety net
in case bridge.disconnect() hangs.

Verified: Ctrl+C now prints "收到 SIGINT,正在关闭..." and exits cleanly.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Two features were missing for the Devin agent that Claude and Codex
already had:

1. /resume session restore — was explicitly blocked with a placeholder
   message. Now wired up:
   - handleResume: lists Devin sessions from the session catalog
     (filtered by scopeId+agentId+cwd, sorted by recency)
   - applyResume: upserts the selected sessionId into the catalog
     (same path as Claude, since both use sessionId not threadId)
   - consumeResumeCandidate: validates devin candidates (must have
     sessionId, same as claude)

2. Image input — run-flow.ts only passed images to codex. Now devin
   also gets image paths. The ACP adapter reads image files, base64-
   encodes them, and sends them as ACP image content blocks in the
   session/prompt request.

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

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

1. post-done-exit-timeout warning: after the event stream generator
   ended, client.kill() fired taskkill via spawn (async, not awaited).
   The generator returned immediately, and the run-executor's
   waitForExit(2000) would fire before the process actually died,
   producing a spurious warning.

2. session/load failure ("Session already open in another..."): the
   next run's devin acp process tried to load the same session before
   the previous process had fully exited and released the session lock,
   causing a fallback to a new session (losing conversation context).

Fix: after client.kill(), await waitForChildExit(child, 10_000) before
the generator returns. This ensures the OS has reaped the process and
released any session locks before the next run starts.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The EventFanout pump breaks on terminal events (done/error), which
calls generator.return(). This throws at the current yield point,
skipping any code after the while loop — including the client.kill()
and waitForChildExit() cleanup.

Moving the kill + waitForChildExit to a finally block ensures they
run even when generator.return() is called. This fixes:
- post-done-exit-timeout warning (child wasn't killed before
  run-executor's waitForExit grace period expired)
- session/load failures on subsequent runs (previous process hadn't
  released the session lock)

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Newer lark-cli (>=1.0.47) auto-detects the --source from environment
variables (LARK_CHANNEL_HOME / HERMES_HOME / OPENCLAW_HOME) and rejects
an explicit --source that doesn't match the detected environment.

When bridge runs inside a Hermes/Devin environment, HERMES_HOME is set,
so lark-cli detects source=hermes. But bridge hardcoded
--source lark-channel, causing a validation error:
  "--source lark-channel does not match detected Agent environment (hermes)"

Fix: try bind without --source first (let lark-cli auto-detect). Only
fall back to explicit --source lark-channel for older lark-cli builds
that don't support auto-detection.

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

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

When the bridge runs inside a Hermes/Devin environment, HERMES_HOME is
set in the outer process.env. lark-cli auto-detects the source from
env vars, and HERMES_HOME takes precedence over LARK_CHANNEL_HOME,
causing lark-cli to bind to the hermes workspace (wrong app) instead
of the lark-channel workspace (bridge's app).

Fix: in buildLarkChannelEnv, when LARK_CHANNEL_HOME is set, explicitly
set HERMES_HOME and OPENCLAW_HOME to undefined. mergeProcessEnv handles
undefined values by deleting the key from the merged env, so the
devin acp process and lark-cli commands will not see HERMES_HOME,
and lark-cli will auto-detect lark-channel as the source.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants