@acpjs/protocol
v0.4.0
Published
acpjs normalized event model, SessionState model, pure reducer and Transport envelope types.
Readme
@acpjs/protocol
The foundation layer of acpjs: a normalized event model, the SessionState
model, a pure reduce reducer, and Transport envelope types for the Agent
Client Protocol (ACP).
This package is types + pure functions only. It is environment-neutral: it
references no Node built-ins and has a type-only dependency on
@agentclientprotocol/sdk, so it runs unchanged in Node, browsers, and jsdom.
Installation
pnpm add @acpjs/protocolESM-only. Requires node >= 24.
Quick start
reduce(state, event) is a pure function. Starting from
createInitialSessionState(sessionId), fold events in seq order; the same
event sequence yields a field-for-field identical SessionState in any
environment.
import {
type AcpEvent,
createInitialSessionState,
reduce,
} from '@acpjs/protocol'
const events: AcpEvent[] = [
{
sessionId: 'sess-1',
seq: 1,
ts: 0,
type: 'agent-message-chunk',
payload: { content: { type: 'text', text: 'Hel' }, messageId: 'm1' },
},
{
sessionId: 'sess-1',
seq: 2,
ts: 0,
type: 'agent-message-chunk',
payload: { content: { type: 'text', text: 'lo' }, messageId: 'm1' },
},
]
let state = createInitialSessionState('sess-1')
for (const event of events) state = reduce(state, event)
// state.messages === [
// {
// kind: 'agent',
// messageId: 'm1',
// content: [{ type: 'text', text: 'Hel' }, { type: 'text', text: 'lo' }],
// seq: 1,
// },
// ]reduce never mutates its input; it returns a new SessionState (or the same
reference unchanged for events it does not reduce).
Public API
Event model
AcpEvent— the closed discriminated union of every event, equal toAcpSessionEvent | AcpHostEvent(19 session events + 6 host events).- Member interfaces, e.g.
UserMessageChunkEvent,AgentMessageChunkEvent,AgentThoughtChunkEvent,ToolCallEvent,ToolCallUpdateEvent,PlanEvent,AvailableCommandsUpdateEvent,CurrentModeUpdateEvent,SessionConfigInitEvent,ConfigOptionsUpdateEvent,SessionInfoUpdateEvent,UsageUpdateEvent,PromptFinishedEvent,SessionStatusChangeEvent,SessionResetEvent,PermissionRequestCreatedEvent,PermissionRequestResolvedEvent,TerminalOutputEvent,UnrecognizedUpdateEvent,AgentUpdatedEvent,AgentRemovedEvent,SessionUpdatedEvent,PermissionUpdatedEvent,InstallProgressEvent,DiagnosticEvent. - Payload types:
SessionConfigInitPayload,PromptFinishedPayload,SessionStatusChangePayload,SessionResetPayload,PermissionRequestCreatedPayload,PermissionRequestResolvedPayload,HostPermissionSnapshot,TerminalOutputPayload,InstallProgressPayload,DiagnosticPayload,UnrecognizedUpdatePayload. - Enums and aliases:
SessionStatus,AgentStatus,AgentExitReason,InstallStage,DiagnosticLevel,AcpEventExtensions,AcpHostProjectionEvent,AcpHostTelemetryEvent.
Payloads reuse SDK protocol types. The top-level _meta field is removed at the
type level (see implementation-defined notes); it surfaces on the envelope as
extensions instead.
State model
SessionState— the derived per-session state.- Supporting types:
SessionMessage,MessageKind,ToolCallState,TerminalOutputState,SessionUsageState,SessionConnectionState,PendingPermissionRequest,ResolvedPermissionRequest,PromptErrorState. createInitialSessionState(sessionId)— builds the empty initial state.reduce(state, event)— the pure reducer.
Terminal output helper
truncateUtf8Tail(output, limitBytes)— trims a string to at mostlimitBytesbytes from the tail along UTF-8 character boundaries, returning{ output, truncated }. The reducer uses it to bound terminal buffers; it is exported for callers that need the same byte-bounded, tail- preserving truncation.
Transport envelopes
RpcRequest,RpcResponse,InboundRequest,InboundResponse,ErrorObject.InboundRequest.kindis an open string with one known member,'permission'(typed as'permission' | (string & {})).ACP_ERROR_CODES— frozen error-code constants in theacpjs/*namespace (8 codes:config-invalid,prompt-in-flight,already-answered,session-closed,agent-exited,capability-unsupported,agent-error,transport-closed), plus theAcpErrorCodetype and theisAcpErrorCode(value)guard.
Transport contract
Transport—connect(handlers),request(request),subscribe(params, onEvent),respondInbound(response),close().TransportHandlers—onInboundRequest+onLifecycle+ optionalonSubscriptionError(params, error).TransportLifecycleEvent—connecting → connected → closed, with an optionalerroron the terminating path.TransportSubscribeParams— optionalsessionId+ requiredfromSeq.TransportConnectionStatus,TransportUnsubscribe.EnvelopeEndpoint— the host-side contract endpoint (request + event subscription + reverse request +respondInbound), with no connection lifecycle.
While connected, envelope delivery is order-preserving. Reconnection is not a
transport obligation; a new connection backfills using fromSeq.
Wire contract
Shared by @acpjs/core and @acpjs/client to avoid duplicated literals.
ACPJS_HOST_RPC_METHODS— frozen RPC method-name constants (agents/spawn|list|dispose,sessions/create|load|list|resume|delete|prompt|cancel|close|setMode|setConfigOption|getAll|restore), pinned by tests, plus theAcpRpcMethodtype.agents/disposecarriesdisposeAgent(agentId)across the envelope (the cross-process counterpart of the host method /client.agents.dispose).- Shared payload shapes:
AgentDefinition,SessionConfigValue,CreateOrLoadSessionParams,ResumeSessionParams,CreateSessionResult,AgentCapabilitiesWire,AgentSnapshotWire,SessionSnapshotWire.
SDK protocol re-exports
Type-only, for contract consumers: AgentCapabilities, AuthMethod,
ContentBlock, ListSessionsResponse, McpServer,
RequestPermissionOutcome, SessionConfigOption.
Event types (closed union)
Session events carry sessionId and an in-session seq:
user-message-chunk, agent-message-chunk, agent-thought-chunk,
tool-call, tool-call-update, plan, available-commands-update,
current-mode-update, session-config-init, config-options-update,
session-info-update, usage-update, prompt-finished,
session-status-change, session-reset, permission-request-created,
permission-request-resolved, terminal-output, unrecognized-update.
Host events carry a host-level seq. AcpHostProjectionEvent is the product
state projection subset:
agent-updated— top-levelagentId; payload is the fullAgentSnapshotWire.agent-removed—AgentRemovedEvent, payload{ agentId }; emitted whendisposeAgenttears an agent down and removes it from the host registry (distinct from anexitedtombstone, which stays in the registry). Added as a new union variant — a non-breaking addition, since consumers already tolerate unknown/new variants.session-updated— fullSessionSnapshotWiresession projection.permission-updated— host-level permission pending/answered/superseded projection.
AcpHostTelemetryEvent is the non-state telemetry subset:
install-progress— top-levelagentId.diagnostic—agentIdoptional (registry-scope diagnostics have no owning agent).
Implementation-defined notes
- Event type names use kebab-case; the canonical name
unrecognized-updateis preserved verbatim. - Payloads that reuse SDK protocol types are
Omit<…, '_meta'>, enforcing at the type level the rule that top-level_metamoves into the envelope'sextensions. Nested structures (e.g.ContentBlock,PlanEntry) keep their native_meta. SessionStateis derived state; absent values are placed explicitly asnull(unlike the "omit absent keys" rule for events/protocol payloads), so cross- environment deep equality is assertable.- Message chunk merging: when
messageIdis non-null, the reducer scans from the tail for a message matching(kind, messageId)(same id is merged even across intervening messages). WhenmessageIdis absent, a chunk merges only with the immediately preceding message if it shares the samekindand also has nomessageId. tool-call-update:nulland absent keys are both treated as "no change" (core normalizesnullaway; the reducer handles it defensively).content/locationsare replaced wholesale. Updates to an unknowntoolCallIdleave state unchanged.ToolCallState.extensions?: AcpEventExtensionscarries a tool call's_meta/extensions throughreduceverbatim (e.g.extensions._meta.subagent_session_info); acpjs interprets no keys.session-info-update: tri-state semantics follow the SDK — an absent key means no change, an explicitnullclears the field, a string replaces it.prompt-finisheddoes not change connection status (returning toactiveis expressed separately by core via asession-status-changeevent). Whenusageis absent,lastTurnUsageis cleared (it reflects only the most recent turn).session-status-change: theresumedflag is sticky until the nextdisconnected/closed/deleted, which resets it.AgentCapabilitiesWireis an explicit stable ACP capability projection used by acpjs. It intentionally does not mirror SDK experimental/auth/provider fields that are outside the acpjs product contract. The agent's advertised auth methods are surfaced separately on the snapshot asAgentSnapshotWire.authMethods(AuthMethod[], theinitializeresponse's methods verbatim) — acpjs implements no authenticate flow, so this is the data integrators read to drive out-of-band login.current-mode-updatearriving before any mode state synthesizes{ currentModeId, availableModes: [] }.diagnosticagent attribution is expressed solely by the envelopeagentId(which may be absent — registry-scope diagnostics have no owning agent; the payload does not duplicate it). The payload'ssessionIdprovides session context.InboundRequest.kindis an open string with one known member,'permission'(the only inbound request category today); new kinds may be added as they stabilize.- The reducer returns the input state reference unchanged for
unrecognized-update,diagnostic, and all host events (they do not participate in reduction and never throw). - The error-code namespace extends the base error codes with two
contract-level codes:
acpjs/agent-error(an agent-side JSON-RPC error tunneled across the envelope, with the original error indata) andacpjs/transport-closed(a call rejected after the transport closed;retryable: true). - Terminal accumulation cap:
reduceenforces a hard 1 MiB byte limit on a single terminal'soutput. On overflow it discards from the oldest end (the head of the string) along UTF-8 character boundaries viatruncateUtf8Tail, preserving the newest tail and settingtruncated: true. resolvedPermissionRequestscap:reducekeeps at most the 100 most recent resolved-permission records (FIFO); on overflow it drops the oldest entries.
