Writing your own agent for Devin Desktop looks like a big task - like embedding a full product into someone else's editor, with its menus, panels and rendering rules. ACP removes most of that weight, but in exchange it demands that you understand the contract: what exactly Desktop expects from a process to treat it as an agent at all. Until the contract is in your head, the work goes blind, and any pause looks like a breakage.
It is naive to picture the integration as a REST API with endpoints or as a plugin called through functions that answers with a return value. Hence the expectation of heavy plumbing: capability registration, request schemas, callbacks on every event. That image frightens before the work has begun and pushes you to look for complexity where there is none.
In reality the model is simpler and stricter. Desktop launches the agent as a local subprocess and talks to it over JSON-RPC on stdio - the same transport by which ACP standardizes the link between an editor and an agent: an agent is a process reading stdin and writing stdout, not a server on a port. The minimal contract is four methods. initialize negotiates the protocol version and advertises capabilities. session/new creates a session for a working directory. session/prompt takes a message and drives the turn. session/cancel aborts work in flight. Everything else is built on top of these four.
The most important thing happens inside the turn, and it is not one answer but a stream. While the agent works, it streams notifications back: session/update with chunks of assistant text as it is generated, tool_call and tool_call_update to display what the tools are doing, session/request_permission for sensitive actions. The turn ends with a stopReason - end_turn on a normal finish, cancelled on a cancel, max_tokens on a spent limit. The client does not poll the agent in a loop, it listens to its stream and reacts to each event.
Why this way and not request-response. An agent is not a short transaction but an ongoing process with intermediate results, permissions and the option to cancel halfway. The stream of notifications is precisely how the human is shown what happens while the turn is not yet finished: which tools were called, what needs approval right now, along which plan the agent moves toward the goal. Collapsing this into one final answer would hide the whole middle of the work - exactly the part where the human makes the decisions.
Developing your own agent is a cycle of edits, and Desktop accounts for it. An entry in the local registry describes the launch, as covered in the previous chapter, and after rebuilding the binary you do not need to restart the whole editor with all its open sessions: the Reload ACP Connections command in the palette brings only the ACP connections back up. A small thing in appearance, but across a debugging session it saves dozens of full restarts and keeps the rest of the context intact.
The integration has documented limitations, and knowing them is cheaper than hitting them blind. Desktop does not surface ACP session modes directly in the interface - modes are implemented not as a separate UI but as session config with a "mode" category. Terminal creation is also not advertised: the agent gets no terminal from Desktop but runs commands itself as a subprocess and returns their output through tool call updates. These are not bugs or temporary gaps but a shape of the contract: what is not in it cannot be bolted on from the side, and trying to go around the boundary only wastes time.
A custom ACP agent is justified when you need logic or an ecosystem that Devin Local lacks and you are ready to carry the process, its permissions and its updates yourself. For a one-off edit it is plainly overkill; for a durable, repeating scenario it is a way to fit someone else's executor into the same Desktop surfaces without breaking their shared model of permissions and display. The boundary is simple: one-off by the standard means, durable through the contract.
You should check readiness by the contract, not by the feeling that "it seems to have launched". Does the agent answer initialize with a negotiated protocol version. Does session/new create a session for the right directory specifically. Do meaningful session/update and tool_call flow during session/prompt, rather than silence to the very end of the turn. Does request_permission arrive where an action is genuinely sensitive. Does a clear stopReason come back rather than a cutoff. The five points of the contract are checked one by one, and if one is silent, the defect is localized in it, not smeared across the whole agent.
The typical failures grow from misunderstanding the transport. "The agent hung" - the turn did not return a stopReason, and the client waits for it forever, though the work ended long ago. "Nothing is visible" - work went on, but notifications were not streamed, and the human is deprived of the middle by which they judge the progress. "The modes disappeared" - they were expected in the UI instead of session config with a mode category. "The terminal did not open" - there will be none, commands go by subprocess and show as tool call updates. The sign is the same: the agent was written as a server obliged to return an answer rather than as a process obliged to keep a stream. Keep the stream of notifications and the terminating stopReason in mind, and the contract stops being a riddle.