Choosing the Boundary: Python SDK, ACP, or Both

Agentao offers two stable ways to place the runtime beside a host: import it into a Python process, or run it as an ACP subprocess that speaks JSON over standard input and output. The important choice is not which example is shorter. It is which process owns memory, which language owns the UI, who handles approval messages, and what survives a crash. This chapter compares those boundaries so a host can choose an integration shape without confusing low transport overhead with low operational responsibility.

Two ways to place the runtime

The Python SDK puts Agentao in the same process as the application. The host calls Agentao(...), invokes chat() or its asynchronous counterpart, and receives native Python events and return values. There is no protocol framing between the host and runtime, so a Python service can inspect state with ordinary debugging tools and connect callbacks directly to its UI or business logic. For a Python SaaS backend that already has a session model and needs low-latency access to the agent, this is the natural starting point.

The cost of that convenience is shared fate. The runtime shares the host’s memory space, dependency environment, process lifetime, and crash boundary. A library-level failure can affect the service that imported it, and a dependency choice made by the host can collide with the runtime’s expectations. The host also has to be disciplined about instance ownership: one Agentao object represents one session, and concurrent turns on it must be serialized. The SDK removes protocol work; it does not remove lifecycle work.

ACP, the Agent Client Protocol, moves Agentao into a subprocess. The host starts agentao --acp --stdio and exchanges newline-delimited JSON—one JSON-RPC 2.0 object per line—over standard input and output. This creates a language-neutral boundary. A TypeScript editor extension, a Go service, or a Rust desktop application can drive Agentao without embedding Python. More importantly, process isolation can keep a child-process crash from directly taking down the host, while allowing the host to restart or sandbox the subprocess independently of its own UI process.

ACP introduces its own costs. Messages must be serialized, parsed, correlated, and dispatched. The host no longer receives Python objects or calls a confirmation function directly; it must translate runtime messages into application events and translate user choices back into protocol responses. Debugging also moves from a shared call stack to wire traces, subprocess logs, and restart behavior. These are not flaws in ACP. They are the price of making the boundary explicit and portable.

The comparison is therefore conditional rather than hierarchical. A Python host with no strong isolation requirement may gain more from the SDK’s directness than from a subprocess. A non-Python host has little choice but to use ACP. An IDE, a multi-tenant service that wants a smaller crash blast radius, or a product that runs agents with different dependency environments may accept protocol overhead in exchange for process separation. The project’s own guidance treats the latency figures in its comparison as illustrative; the durable decision is about ownership, isolation, and failure handling.

The same product request makes the distinction concrete. In an in-process SaaS assistant, a web request can acquire a session lock, call chat() in a worker thread, stream Transport events into a browser queue, and release the lock when the turn ends. A crash in the agent code is part of the service’s failure domain, and the service’s deployment must decide how to recover the session. With ACP, the web service can keep its request handling and UI process separate from Agentao. It must now read session/update messages, answer permission requests, detect a dead child, and reconnect or load the session. The agent may be easier to isolate, but the host has more protocol states to represent.

The boundary also affects debugging. SDK embedding makes it straightforward to inspect a Python stack and the live object, but it can tempt a host to reach into private state or let application code share mutable objects. ACP makes the wire visible and therefore easier to capture as a protocol trace, but a trace alone may not explain a child-side error; the host also needs the subprocess log and exit status. In both cases, the durable public contract matters more than a convenient internal object. The host should prefer documented construction, session, event, and loading surfaces over coupling itself to implementation details that a release can change.

ACP is a conversation between two event loops

ACP’s wire protocol is host-driven. The connection begins with initialize, where the client and server negotiate a protocol version and capabilities. The host then sends session/new with a working directory and optional session-scoped configuration. A user turn travels through session/prompt. The final response tells the host why the turn stopped, but it is not the only traffic on the connection.

While the prompt is running, Agentao sends session/update notifications. These can carry streamed assistant text, thinking, tool-call status, and tool progress. A notification has no JSON-RPC id and must not receive a response. That detail matters because a host that waits for one large final message will lose the interactive behavior, while a host that replies to notifications violates the message pattern.

Approval is different. When a tool requires permission, Agentao sends session/request_permission as a server-to-client request with an id. The host must answer that request with an allowed outcome, a rejection, or a cancellation. From Agentao’s point of view, the request is synchronous: the tool cannot proceed until the host responds. From the host’s point of view, the answer may require an asynchronous operation such as opening an editor modal, asking a user to review a command, or applying an unattended policy. The client must bridge those two timing models without blocking its reader.

Cancellation is a host request of its own. A Stop button can send session/cancel for the active session, and the prompt eventually completes with a cancelled stop reason while the connection remains available for another turn. This is more precise than killing the subprocess for every interruption. It gives the host a chance to preserve the session and distinguish an intentional cancellation from a process failure.

flowchart TD Q[Choose the host runtime boundary] --> P{Is the host Python?} P -->|No| A[Use ACP subprocess] P -->|Yes| I{Is strong process isolation needed?} I -->|No| S[Use Python SDK] I -->|Yes| A P -->|Yes| L{Need long-lived low-latency in-process state?} L -->|Yes| S L -->|No or mixed| A S --> H[Host still owns persistence policy, permissions, cleanup] A --> H S --> Y[Optional hybrid: delegate specialist work through ACP] A --> Y

The protocol also has a security boundary that readers should not overstate. The documented ACP capability block has no protocol-level authentication methods; credentials are supplied through the process environment. Stdio avoids opening an inbound network port, but it does not authenticate the host or constrain what the child process can reach. The host still has to protect the environment, choose the working directory, and apply the filesystem and network controls appropriate to its deployment.

The host work moves; it does not disappear

A production ACP client needs three cooperating loops, whether they are threads, coroutines, or equivalent components. A reader continuously parses stdout and dispatches incoming messages. A writer serializes outgoing requests so two callers do not interleave bytes. Application logic correlates responses with request ids, renders notifications, and routes approval requests to the user or policy engine.

Incoming JSON-RPC messages have three useful shapes. A message with an id but no method is a response to something the host sent, so it completes a pending request. A message with a method but no id is a notification, such as session/update, so it goes to an event handler and receives no reply. A message with both a method and an id is a request from Agentao to the host, such as a permission or user question, so it must eventually receive a response. Dispatching by these shapes is more reliable than assuming that every incoming line is a response to the last prompt.

The stream boundary has a practical hygiene rule: stdout must remain protocol data. Diagnostic logging belongs on stderr or in the Agentao log, not in the same stream the client parses. A single stray debug print can turn a valid NDJSON connection into a parse failure. The host should also impose request deadlines and close out pending calls when it observes end-of-file. Otherwise an unavailable child process can leave application futures, UI spinners, or queue jobs waiting forever.

The separation is not aesthetic. If the reader pauses while a UI callback performs slow work, more notifications can accumulate in the pipe and the agent may appear hung. If the host allows multiple writers to write directly to stdin, two JSON objects can be interleaved and corrupt the framing. If a permission request is placed on a UI queue but no response is guaranteed, the Agentao subprocess can remain blocked indefinitely. The protocol boundary turns these into host responsibilities that an in-process callback arrangement can hide.

The same applies to failure. The host should notice a non-zero process exit, stdout end-of-file, malformed JSON, and request timeouts as different signals. Pending response futures need to be failed when the process disappears; otherwise an application can retain promises that will never resolve. A restart can create a fresh ACP connection, but it does not magically restore the old in-memory conversation. The host must use session loading or its own persisted history if continuity matters.

Permission UI is the clearest example of responsibility moving outward. With the SDK, confirm_tool() can call a host callback directly, although an asynchronous UI still needs a cross-thread bridge and a finite timeout. With ACP, the same decision becomes a server-to-client JSON-RPC request. The host must show enough tool information for a meaningful decision, respect the options Agentao offers, and answer when the user closes the dialog. The fact that the message is standardized does not make the decision automatic; it makes the decision observable and portable.

ACP also helps when the host wants a specialist agent without sharing its process. Agentao’s ACP client side can start another ACP-capable runtime, send a bounded prompt, stream notifications, and stop the child afterward or keep it warm. A search specialist, document crawler, or code reviewer can have a different dependency set or resource profile. The isolation is useful when a crash or heavy task should not take down the main process. It is unnecessary complexity when the supposed specialist is simply a local function that could be a normal Tool.

For unattended work, the project describes ACPManager’s one-shot prompt_once() path as a predictable entry point. It uses fail-fast concurrency behavior: if a server already has an active turn, the caller receives a busy condition rather than an invisible queue. That distinction matters for schedulers and workers. The host can decide whether to retry, route elsewhere, or record the job as deferred. “Headless runtime” is not a third protocol; it is ACP operated without a person to answer questions, which makes interaction policy and admission control more explicit.

A hybrid design can make this division useful rather than merely complicated. A Python Agentao session can keep the user’s long-lived conversation and expose a local Tool whose implementation delegates one narrow task to an ACP specialist. The main agent receives the specialist’s result as tool evidence; the specialist remains isolated and can have its own working directory, credentials, or dependency set. If the specialist is busy, times out, or enters a fatal transport state, the main agent’s Tool can return a bounded error and let the host decide whether to retry or explain the limitation. The failure is visible at the capability boundary instead of becoming an unstructured exception somewhere inside the main process.

There is a cost to every extra boundary. The host now owns two session lifecycles, two sets of logs, and a policy decision about whether the specialist can ask for human interaction. A one-shot, read-only specialist may be a good fit for an ephemeral subprocess; a frequently used conversational specialist may justify a warm long-lived process. If the task is local and deterministic, a normal Tool remains simpler. The point of ACP is not to turn every function into a subprocess, but to make isolation and interoperability available where their benefits exceed their coordination cost.

When no single option dominates

The SDK is a strong fit when the host is Python, sessions are naturally managed inside the application, and direct debugging and low call overhead matter more than process isolation. It works well for an in-product assistant whose session pool, business tools, and UI already live in one Python service. The host should still isolate working directories, serialize turns, persist the state it needs, and close each instance.

ACP is a strong fit when the host is written in another language, when an IDE wants an external agent, or when process failure and dependency separation deserve a hard boundary. It is also a useful choice for a tenant or specialist workload that should be independently restarted or sandboxed. The host should budget for framing, a reader/writer dispatcher, protocol timeouts, permission responses, restart logic, and session restoration.

A hybrid arrangement is often the most honest design. Keep long-lived application state and ordinary business interactions in the Python SDK, then delegate a narrow task to an ACP subprocess when isolation or language interoperability is the reason for the delegation. The hybrid path does not create a new Agentao object model. It puts different jobs behind different boundaries and makes the handoff explicit.

The decision should therefore be made by asking what responsibility the host is willing to own. Importing Agentao keeps responsibility close but shares fate. Spawning Agentao separates fate but requires protocol machinery. Both paths expose the same governed-runtime question: which capabilities cross the boundary, who approves their use, where session state lives, and how the host proves what happened. Once that boundary is chosen, the next question is what belongs inside it—an executable Tool, an instructional Skill, an MCP connection, or a lifecycle hook.

References