Skip to content

@maiaos/sdk reference

Status: Source candidate. The SDK is the only product-client request boundary to Maia Node: the CLI, maia mcp and the Desktop and Expo clients use it. It transports typed operations and never owns business authority. Contracts own the operations; the package README covers install and a first call. The package rules are in packages/sdk/AGENTS.md.

Methods mirror the registered operations; many are release-disabled at the Node. The SDK never starts or stops a Node, reads Node storage or supervises processes.

Entry points

Entry pointContents
@maiaos/sdkPlatform-neutral LocalNodeClient, request options, errors, attach reducer
@maiaos/sdk/nodeconnectNodeApi, createNodeApiClient and WebSocket observations (Node.js)
@maiaos/sdk/node/effectThe same discovery and client as Effect values; connectSessionNodeApiEffect connects as one executor Session from its launch environment; openNodeBridge speaks the SSH stdio bridge over a process the caller spawned (sshCommandArguments, bridgeScript)
@maiaos/sdk/voice-orchestratorVoice orchestrator prompt and tool schemas for Realtime clients
@maiaos/sdk/voice-pi-relayVoice-to-Pi relay contracts

Browser and mobile bundles import only the root entry, so they never receive Node.js filesystem modules or the profile credential.

connectNodeApi finds the profile's private connection descriptor, reads its credential and proves the Node's identity with an authenticated node.status before it returns the client.

Client behavior

  • One typed client. LocalNodeClient has one method per operation, for example createSession, startSession, sendTurn, waitTurns, observeSessions, stopSession, resumeSession, diffSession, archiveSession, getReceipt, listConnections and showConnectionUsage. Parameters and results are the @maiaos/protocol types, validated on both sides. Human-only methods such as previewHoldPlace, confirmHoldPlace, previewHoldRelease, confirmHoldRelease and listHolds check that the Node echoes the exact binding they sent.

  • Typed call. call(method, parameters, options) serves every operation whose feature descriptor binds sdk.operation: "call" (today reference.resolve, session.rename and events.read). LOCAL_NODE_CALL_METHODS lists them from a static table the type checker holds to the descriptors, so a client does not bundle the feature registry. Parameters and results are typed from the protocol unions, the payload is validated before sending, and the answer must name the method and, for a command, carry a receipt with the command ID sent. An unconfirmed command fails outcome_unknown. Such an operation has no method of its own.

  • Request options. Every call takes { messageId, signal?, deadlineAt? }. The AbortSignal cancels the request; a mutation cancelled after dispatch reports outcome_unknown instead of guessing. deadlineAt is a bigint instant on the caller's own Clock.monotonicTimeNanos, never wall-clock time, after which an in-process requester on the same Clock stops waiting or retrying. The SDK's own requesters ignore it, so a caller that omits it sees no change.

  • Errors. Failures are MaiaClientError values with a stable code (transport_failure, cancelled, outcome_unknown, remote_error, ...) and, for Node rejections, the protocol error. Reconcile an unknown outcome with getReceipt and the same command ID; never retry with a new one. instanceof MaiaClientError also recognizes an error from another copy of the SDK in the same process, such as the one a bundled CLI carries, through a process-wide Symbol.for("@maiaos/sdk/MaiaClientError") brand. That key is the contract between copies: renaming it breaks recognition between SDK versions.

  • Errors as one value. classifyClientError turns any failure into a MaiaError (_tag: "MaiaError"); mutation: true marks a request that may have changed Node state. Its kind is cancelled, conflict, declined (a person declined the native confirmation; nothing was sent), denied, invalid, not_found, offline, outcome_unknown (a mutation's answer never arrived), refused or unavailable, plus the Node's code, retryable and, for a saturated answer of the Node only, its message (why it is full); no other code and no local failure carries one. The CLI and clients act on kind, never on a code string.

  • Session tasks. planSessionCreation splits text into tasks (one per line, or the whole text; at most 32 of at most 16 KiB each), picks the Workspace (a Project's ready Workspaces, its main checkout first, through orderExecutionWorkspaces; a choice for the caller, which the Node still validates) and mints every command ID once. createSessions runs each item, four at a time: session.create, session.start, awaitSessionRunning, then the task as the first turn.send. Each item ends sent (or started without a task), refused with its MaiaError, or unknown. Each outcome has a required receipts field with the Node's receipt of every step that committed (create, start, send; {} before any). When session.create, session.start or turn.send fails, refused or unknown, the error carries that step's commandId; only an unknown one needs reconciling with receipt.get. A session.show failure has none. A caller passing hand-built previous outcomes adds receipts: {}. Called again with previous outcomes, it replays only the unsettled items with the same command IDs and request bodies, so the Node answers with the original Sessions and turns and no turn is sent twice. The SDK composes existing operations here and owns no authority. @maiaos/sdk/node/effect has the same as planSessionCreationEffect, createSessionsEffect and awaitSessionRunningEffect.

  • Node changes. watchNodeChanges(relay, { instanceId, newId }) builds each observe.events subscribe message, checks every frame's size, schema and correlation, and yields records and resync changes. When the platform StreamRelay closes it resubscribes from the last cursor, and a gap or a cursor, policy or scope reset yields a resync. A reset the old subscription cannot recover from ends the watch with a non-retryable remote_error whose remoteError.code is epoch_changed (the Node restarted: the caller reads the Node's new instance and watches that one), policy_revoked or principal_changed (the caller authenticates again). reduceNodeChanges is the pure frame step.

  • Session attach. attachSession (@maiaos/sdk/node) yields one Session's session.attach frames; attachSessionEffect (@maiaos/sdk/node/effect) discovers the Node first. The pure reduceSessionAttach reducer in the root entry folds frames into the view every client renders, with sessionAttachCursor for a reattach.

  • Transport limits. The Node.js adapter accepts only the exact 127.0.0.1 origin, bounds bodies to 1 MiB, admits eight concurrent requests per transport and applies a five-second deadline. Three tiers, in one map in node/deadlines.ts:

    • 5 s for everything else.
    • 15 s for the operations that hash a vendor binary or launch a vendor process before they reply, about three times the slowest measured plan: connection.detect, connection.login.start, connection.system.preview, connection.system.confirm, discovery.sessions.list, integration.plan.* and integration.apply.* for Claude Code and Codex, session.adopt.preview, session.adopt.confirm, session.history.read and session.resume. A method is listed whether or not a given call reaches the slow step.
    • 50 s for connection.usage.show, which outlasts the Node's own 45 s refresh probe, so the Node answers first.

    The SSH bridge waits ten seconds longer than the request it forwards, so its deadline is never the shorter one.

    A client deadline does not cancel Node work. Handlers keep their own limits (usage refresh: 45 s) and finish after the client gives up, so a request past its deadline fails outcome_unknown. For the Codex launch paths the 15 s is a client cutoff, not a Node bound. A mutation that times out there (session.adopt.confirm, session.resume) is reconciled on a retry: session.adopt.confirm replays by its preview ID (a confirmed preview replays its receipt), and session.resume by its command ID. A retry is not free. A plan retry reuses the envelope's messageId, but resolves the target and hashes it again before it checks the stored replay. An apply replays by its command ID and plan ID. A confirm replays a recorded result, but a claim still outstanding returns storage_conflict, and a confirm that failed part way keeps its claimed confirmation until that is fixed.

Target observation client (planned)

Planned, not exported: one aggregate, transport-neutral observation client generated from the protocol.

client.observations.snapshot(query, options);
client.observations.subscribe(query, { after, signal });
client.observations.session({ agentSessionId, includeContent }, options);
client.observations.pipeline({ processorId, partitionId }, options);

subscribe opens one authorized feed for any number of Projects and Sessions with a signed, resumable cursor; slow consumers get typed gap and reset results rather than backpressuring workers. A generated, reflective operation catalog (client.operations.list, show, invoke) is also planned.

Imported from Maia OS docs at 3dc3e07