npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gomomento-poc/mo-sdk

v0.4.0

Published

TypeScript SDK for mo's session protocol (mo-session v1) — spawn a mo session-host, drive turns, and intercept tool calls. Thin by rule: never agent logic.

Readme

@gomomento-poc/mo-sdk

The programmable surface for mo, Momento's coding agent, over the mo-session protocol. Spawn a mo session-host process, drive turns, and intercept every tool call it makes.

Thin by rule. The SDK owns the wire — framing, request correlation, and the always-answer discipline every host round-trip depends on — and nothing else. Agent behaviour lives in mo, so an embedder and the CLI cannot drift apart.

Requirements

  • Node ≥ 20 — the version CI validates the package against. The await using form in the examples additionally needs a runtime that provides Symbol.asyncDispose; where it is missing, call close() instead and everything else is unchanged.
  • The mo binary on PATH, or pass binary to createAgentSession
  • Gateway credentials in the environment, exactly as the CLI reads them

Install

npm install @gomomento-poc/mo-sdk

Quickstart

import { createAgentSession } from "@gomomento-poc/mo-sdk";

await using session = await createAgentSession({
  model: "momento/zai-org/GLM-5.2",
  cwd: process.cwd(),
  context: { preamble: "You are a release-notes assistant. Be terse." },
  maxIterations: 20,
  compactionThresholdPercent: 90,
});

session.on("assistant", ({ text }) => process.stdout.write(text));

const turn = await session.prompt("Summarise the last five commits.");
console.log(turn.stop_reason, turn.usage);

const result = await session.close();
console.log(result.outcome, session.sessionId);

await using closes the session at scope exit; using kills it outright. Without either, call close() (clean, resolves on the terminal result) or kill().

session.sessionId is the durable resume target. The host owns the journal; the SDK deliberately does not expose its filesystem path.

What you can control

| Capability | Surface | |---|---| | System prompt | context: { preamble } — see Context sections | | Model & effort | model, strongModel, models, effort | | Sampling | providerParams: { temperature, max_tokens } | | Context budget | compactionThresholdPercent — a percent of the ROUTE's window, resolved host-side | | Iteration cap | maxIterations | | Tool allow-list | allowTools | | Static permissions | permissions: { allow, deny } — rule-level, evaluated before any prompt | | Dynamic approval | onApproval(request) → { choiceId } or { choiceId: "deny", denyReason } | | Custom tools | tools: [{ name, description, inputSchema, execute }] | | Rewriting a call | onToolCallMutate({ tool, tool_call_id, arguments }) → the new arguments | | Rewriting a result | onToolResultRewrite({ tool, tool_call_id, result }) → the new result string | | Cost attribution | taskId | | Durability & resume | session: { mode, resume } | | Peer plane | intercom: { name, mode, groups, transport } — see Intercom | | A presence, no model | openIntercom({ name }) — see A presence on the plane | | Plane readiness | peerReadyTimeoutMs — the arming budget, from the configure reply | | Startup fan-out | configureSdk({ spawnConcurrency }) — process-wide, see Lifecycle | | Lifecycle | session.on(kind, handler) — see Events |

Context sections

context overrides mo's own assembled prompt per section: a string replaces the named section, an explicit null suppresses it, and an absent key leaves the host's assembly alone. The sections are preamble, agents_md, memory, repo_map, skills; the host rejects any other name.

context: {
  preamble: "You are the TRIAGE role. …",  // replace
  memory: null,                            // suppress
}                                          // agents_md, repo_map, skills: host's own

It is per-configure, not persisted — a resume that wants the same overrides passes them again.

Custom tools

execute returns a ToolResult or a bare string. A rejection fails the call closed as a model-visible error.

tools: [
  {
    name: "emit_decision",
    description: "Report the decision. Call exactly once.",
    inputSchema: { type: "object", properties: { kind: { type: "string" } }, required: ["kind"] },
    terminal: true,   // this call ends the session as `completed` (structured-output shutdown)
    execute: async (args, { toolCallId }) => {
      record(args);
      return { content: "recorded" };
    },
  },
]

ToolResult also carries details (echoed on the tool_result frame, never model-visible), isError, and pause — which completes the call and then ends the session as paused, the pause-for-human disposition. Model-visible content is capped at 256 KiB by the host.

The two interception hooks

Both are async, and both return the replacement value itself — not a wrapper object. Returning undefined means "leave it unchanged".

onToolCallMutate: async ({ tool, arguments: args }) => {
  if (tool !== "read_file") return undefined;                 // unchanged
  return { ...(args as Record<string, unknown>), path: SAFE }; // the new arguments
},

toolResultRewritePolicy: "required",
onToolResultRewrite: async ({ result }) => redactSecrets(result),

onToolCallMutate runs before validation, classification and approval, so policy judges — and any approval prompt displays — the arguments that will actually execute. A registered hook that does not answer declines that call: an unexamined call must not run when you asked to examine calls.

onToolResultRewrite runs at the single chokepoint the rendered output, model context, and durable journal all funnel through, so a redaction cannot land in one and miss another. Its failure direction is toolResultRewritePolicy, and setting the handler without it is an error:

  • best_effort — a hook that fails leaves the original result standing; the turn continues.
  • required — a hook that fails withholds the result and ends the session as error.

Events

session.on(kind, handler) returns an unsubscribe. The kinds:

session_start, user, assistant, reasoning, tool_call, tool_result, iteration_end, phase, notice, approval_declined, session_shutdown, peer_ready, peer_message, peer_turn_started, peer_turn_complete, result, mo/error.

There is no text or turn_end kind — model output arrives as assistant, and turn completion is the resolved value of prompt(), not an event. session_start is a one-shot opening fact emitted while the session is still being created, so a subscriber added afterwards still receives it.

A handler that throws — or an async one that rejects — is contained: the remaining subscribers for that event still run, and the failure is reported as an mo/error with code subscriber_failed. Subscribe to mo/error if you want to see your own handler bugs — with one gap by construction, a failing mo/error handler, whose failure is dropped rather than routed back through itself.

Intercom

A session can join the peer plane, so other mo sessions under the same login address it by name. Two things happen at open: the SDK declares the capability at initialize, and arms the plane at configure. createAgentSession does not resolve until the host reports the plane live, so a session you hold is a session peers can reach.

import { createAgentSession } from "@gomomento-poc/mo-sdk";

await using session = await createAgentSession({
  model: "momento/zai-org/GLM-5.2",
  intercom: { name: "planner" },
});

console.log(session.intercom?.peerId);          // the id peers address

session.on("peer_message", ({ handle, sender_name, text, attested }) => {
  console.log(`${sender_name ?? "unknown"}: ${text}`);
});

const { messageId } = await session.intercom!.send("scout", "claim WR Smith");
const peers = await session.intercom!.peers();  // [{ sessionId, name?, departed, paired }]

name is what peers see: printable ASCII, no spaces, at most 64 characters, and never beginning with #. That last one is not arbitrary — a send target written #<name> is always a group, and a roster offers names as targets, so a session calling itself #ops would put a target on every peer's roster that publishes to the group ops instead of reaching it. The SDK refuses all of that before opening.

Sending. target is a peer's announced name, its exact session id, or an id prefix of at least six characters. A target that is not exactly one live peer is refused rather than guessed at, and that refusal arrives as a PeerUnresolvedError whose candidates lists what the host would have had to choose between (empty when the target was simply unknown or departed). replyTo takes the handle of a message you received, and then target must be its sender.

import { createAgentSession, PeerUnresolvedError } from "@gomomento-poc/mo-sdk";

const session = await createAgentSession({ model: "momento/zai-org/GLM-5.2", intercom: { name: "planner" } });
try {
  await session.intercom!.send("sc", "hello");
} catch (error) {
  if (error instanceof PeerUnresolvedError) console.log(error.candidates); // ["scout", "scribe"]
}

Two vocabularies meet here, as everywhere in this SDK: what you pass in is camelCase (replyTo), and what the host hands you keeps the wire's own names. So peers() reads sessionId — the SDK built that list — while the peer_message event reads sender_name, because that payload is the host's, delivered as it arrived.

Hearing. peer_message is observational: the SDK hands you the message and inserts nothing into the model on its own account. Everything on it except attested is the sender's own claim, including senderName — treat the text as content from a process nobody authenticated.

Opening. Everything the host emits between peer_ready and the moment createAgentSession returns is held for you, and delivered to the first subscriber for that kind. A message that arrives in that window has no subscriber it could have reached, and for a receive-only session hearing is the whole job. The plane gets its own peerReadyTimeoutMs (default 30s, the host's own deadline) measured from the configure reply, so a slow handshake does not eat the arming budget.

Receive-only. A session that only listens may be opened without a model, and the host refuses prompt on it. It is the one session kind where model is optional.

import { createAgentSession } from "@gomomento-poc/mo-sdk";

await using listener = await createAgentSession({
  intercom: { name: "scoreboard", mode: "receive_only" },
});

Admission. intercom.onMessage decides whether one inbound message reaches the model, and the host asks per message within a five-second window.

import { createAgentSession } from "@gomomento-poc/mo-sdk";

const session = await createAgentSession({
  model: "momento/zai-org/GLM-5.2",
  intercom: {
    name: "planner",
    onMessage: async ({ sender_id, attested, text }) => (attested && known(sender_id) ? "admit" : "refuse"),
  },
});

It fails closed in every direction. No handler refuses, a handler that throws refuses, and one that outruns the window refuses, because the host has already decided by then. A session that declared it would decide and then did not has consented to nothing.

The handler and the peer_message event are different things, and the difference is easy to miss. The event is observational and fires for every arrival, including one a standing refusal already dropped. The handler is the blocking request that gates the model. Subscribe to the event without registering the handler and you will see every message and admit none of them.

The answer is a string rather than a boolean because the set is expected to grow, and a host that meets a decision it does not know refuses that message rather than guessing.

Lifecycle

createAgentSession hands back a session that is ready to use. It resolves after the handshake, and — when intercom is declared — after the host reports the peer plane live. It rejects rather than handing back a half-open session: a host on another protocol version, a rejected configure, a plane that cannot be armed, or a host that dies mid-handshake. A rejected open leaves no child process behind.

Two deadlines bound it, and they are separate because the host counts them separately. handshakeTimeoutMs (default 30s) covers initialize and configure; peerReadyTimeoutMs (default 30s) starts at the configure reply and covers arming, which is when the host starts its own clock. A slow handshake therefore does not eat the arming budget.

close() sends shutdown and waits. It owns no ordering of its own: the host stops admission, settles what it owes, tears down the plane, and answers — close() resolves on the terminal result that follows. It resolves immediately if the session already ended on its own (a pause disposition, a terminal tool), because there is nothing left to shut down.

How long it waits for that answer is closeGraceMs. The default depends on what the host has to do: 5s for a session with no peer plane, and 20s for an armed one, whose shutdown is a whole ordered sequence the host bounds at 15 seconds. Killing at 5 would preempt a host doing exactly what it promised.

A shutdown is legal only between turns, so a host that is not between turns refuses it and the SDK raises SessionBusyError. The refusal is not a death: the session is still usable and its host was not killed. The error's message is the host's own and says which case it is — a turn running, or a close already under way.

interrupt() is a notification, so it returns before the turn has actually ended. A retry that follows it immediately can meet the same refusal; wait for the turn you interrupted to settle first, which is the prompt() promise you are already holding.

import { createAgentSession, SessionBusyError } from "@gomomento-poc/mo-sdk";

const session = await createAgentSession({ model: "momento/zai-org/GLM-5.2" });
const turn = session.prompt("Summarise the last five commits.");
try {
  await session.close();
} catch (error) {
  if (!(error instanceof SessionBusyError)) throw error;
  session.interrupt();
  await turn.catch(() => undefined);   // let the turn end before asking again
  await session.close();
}

Calling close() twice does not send a second shutdown: the second call joins the first. A close that was refused stays retryable, which is what makes the sequence above work.

A forced end is not a failure. If the host's ordered shutdown runs past its budget it settles everything it still owes and answers the outstanding requests — including the shutdown itself — with SessionShutdownTimeoutError. close() still resolves, with result { outcome: "interrupted" } after session_shutdown { reason: "forced" }, because the session is ending cleanly rather than breaking.

The order matters if you read frames yourself: the active turn's own completion comes first, carrying interrupted and no usage, then the other outstanding requests, then the shutdown reply, then the terminal pair. A program that stops reading at the shutdown reply misses the turn report it is entitled to.

SessionShutdownTimeoutError says the request did not complete. It does not say the request failed on its own terms, and it says nothing either way about whether its effect happened — a peer_send answered this way may or may not have published.

await using performs that clean close at scope exit; using kills the host outright. Without either, call close() or kill() yourself — a session left open is a child process left running.

Turns you did not ask for

An admitted peer message can open a turn. The host brackets that turn with peer_turn_started and peer_turn_complete, which is how a program learns it happened at all — nothing requested it, so there is no promise resolving to report it.

import { createAgentSession } from "@gomomento-poc/mo-sdk";

const session = await createAgentSession({
  model: "momento/zai-org/GLM-5.2",
  intercom: { name: "planner", onMessage: async () => "admit" },
});

session.on("peer_turn_started", ({ turn_id }) => console.log(`${turn_id} began`));
session.on("peer_turn_complete", ({ turn_id, outcome, usage }) => {
  console.log(turn_id, outcome, usage?.prompt_tokens ?? "usage not reported");
});

Two things here are easy to get wrong, and both fail quietly.

The pair brackets only turns nobody requested. A prompt turn is bracketed by neither, because the promise prompt() returns is that turn's report. Counting both would count one turn twice.

Absent means unmetered; zeros are a measurement. undefined is "nobody measured this turn", never "the turn was free", so do not default it to zero. A turn genuinely metered at zero reports zeros, so those you may read as measured. A failed turn reports absent rather than zero, because the model may well have run.

The outcome vocabulary is the same one prompt() resolves with, so a program that classifies outcomes does it once. The host owns that vocabulary and may grow it, so a word this version does not know reaches you as itself — switch with a default arm.

Correlation is yours. The SDK hands over the frames and the turn_id that pairs them, and keeps no ledger: a completion with no start, a repeated start, or a second completion all reach you unchallenged. That is deliberate, because these are observations and an orphan can legitimately follow a frame that could not be sent or a subscription added late. Deduplicate on turn_id if it matters to you.

Two diagnostics arrive on mo/error and mean different things. peer_turn_failed says a turn an admitted message opened could not run. peer_turn_unreported says one of these lifecycle frames could not be sent — they ride the same budget as model output, so an embedder that stops draining can fill it — and the turn itself ran or is running. Treating the second as the first has a program conclude a turn that happened never did.

Groups

A session hears a group only if it declared one at open. The model has no action that joins one, so what a session hears is your decision, and changing it means opening a new session.

import { createAgentSession } from "@gomomento-poc/mo-sdk";

const session = await createAgentSession({
  model: "momento/zai-org/GLM-5.2",
  intercom: { name: "planner", groups: ["round-1", "scores"] },
});

// What the host made ready, which may be less than was asked for.
console.log(session.intercom?.groups);

await session.intercom?.send("#round-1", "the board is up");

Names are lowercase ASCII letters, digits and ._-, one to thirty-two characters. At most eight groups, counted after duplicates collapse: naming one twice declares one, so lists that overlap are not over the bound just because they overlap. There is no normalization, so two different spellings are two different groups. The character set excludes every separator the wire uses, so a name cannot splice into a neighbouring field.

The SDK checks all of that before sending configure, because the host's refusal is fatal: it ends the session rather than the call.

Compare intercom.groups against what you declared. It lists what came up ready, not what was asked for, and a group missing from it is one you cannot address.

A # target is only ever a group. An undeclared one is refused by name rather than searched for among the peers, so the PeerUnresolvedError carries no candidates. A name outside the grammar is an ordinary SessionRequestError with the host's own code, not an unresolved target: the name was never addressable in the first place.

transport picks which topics group traffic rides, and defaults to peer-topic, which needs no grant beyond the peer one. group-topics is refused by the current host build — at configure and again before the vend — so send peer-topic or omit the field. That is deliberate rather than an oversight: a session that believed it had per-group topics while riding the peer topic is a rollout reporting itself complete when it is not, so the stage is refused rather than quietly served by the one below it.

replyTo and a #group target do not go together, and the host refuses the pair: a reply is addressed to whoever sent the message you are answering, which is a peer and not a group. A group send is also unattested and carries no delivery receipt.

Opening many at once

Each session is a child process, and its startup — loading the binary, the handshake, reading a workspace — is the expensive part. What the bound covers is concurrent handshakes: a slot frees when the handshake settles, so a host being torn down after a failed one does not hold the fleet behind it.

A Promise.all over a fleet would otherwise land every one of those in the same instant, so the SDK bounds how many may be starting at once. Nothing is dropped, only queued: twelve sessions opened together all open.

The bound defaults to the parallelism available to the process, capped at 4. It is process-wide rather than per call, because a per-call option would let two parts of one program each assume they were the only ones spawning.

import { configureSdk } from "@gomomento-poc/mo-sdk";

configureSdk({ spawnConcurrency: 8 });   // once, at startup

It bounds startup, not how many sessions may be open at a time.

A presence on the plane

A program that wants to be addressable — a scoreboard, a referee, the thing that collects what the agents say — does not want an agent. openIntercom gives it the plane and nothing else: a receive-only session, which is the one kind the host runs without a model and never starts a turn for.

import { openIntercom } from "@gomomento-poc/mo-sdk";

await using stage = await openIntercom({ name: "scoreboard" });

stage.onMessage(({ sender_name, text }) => console.log(`${sender_name ?? "?"}: ${text}`));
await stage.send("planner", "the board is up");

const peers = await stage.peers();
await stage.state.set("board:round", { round: 1 });

It resolves once the plane is live, so a handle you hold is one peers can reach. peerId is the id they address. There is no prompt: a presence has no model, and a method whose only outcome is the host refusing it is not worth having.

onMessage is observational and that is all it needs to be. Admission decides whether a message reaches a model, and a presence has none — so every message the plane delivers reaches your handler. Pass it as an option to have it registered as early as the SDK can; call it on the handle to register later. The first handler also receives what arrived while the session was still opening, which the session holds for it — up to 64 messages, after which the rest are dropped and onError says so. A presence never hears its own sends: the plane's router drops a sender's own frames.

onError is worth subscribing to. It is the only channel some things have: the held groups above, a handler of your own that threw, a plane that stopped being reachable after opening. A listener that never subscribes can go quiet without being told why.

state is a StateHandle — a fresh in-process one by default, or pass your own to share a record with the rest of the program. See State.

groups declares what this presence hears, and stage.groups is what the host actually made ready. See Groups.

Opening several sessions

There is no openSessions helper, deliberately. A loop is shorter than the options bag it would take, and it leaves the failure policy where it belongs — with the program, which is the only thing that knows whether a missing member means carry on or give up.

import { createAgentSession, type AgentSession } from "@gomomento-poc/mo-sdk";

declare const roles: Array<{ name: string; preamble: string }>;

// All or nothing: one failure rejects, and the rest are closed rather than left running.
const opening = roles.map((role) =>
  createAgentSession({
    model: "momento/zai-org/GLM-5.2",
    context: { preamble: role.preamble },
    intercom: { name: role.name },
  }),
);
const settled = await Promise.allSettled(opening);
const open: AgentSession[] = settled.flatMap((one) => (one.status === "fulfilled" ? [one.value] : []));
if (open.length !== roles.length) {
  await Promise.all(open.map((session) => session.close().catch(() => undefined)));
  throw new Error(`only ${open.length} of ${roles.length} roles opened`);
}

Opening them together is safe: the SDK bounds how many hosts may be starting at once, so the fleet queues rather than storming. See Opening many at once.

Context budget

compactionThresholdPercent is a percentage, not a token count, because both inputs are the host's: the route's served context window, and the correction for the token estimator's measured under-count on anthropic/* routes — which a caller cannot compute at all. Omit it and the session runs uncompacted, which is right for a short scripted leg and wrong for a long role, since nothing else bounds the message list.

State

StateHandle is the shared-state revision contract as an interface: a key-value record several sessions on one login read and write, with compare-and-set so two cooperative writers cannot both succeed against the same revision.

Two implementations behind the one interface. memoryState() is in process and private to it. state: true on a session asks the host for the record other sessions on this login can see, and puts the same interface on session.state. Moving between them is a change of handle, not of code.

import { createAgentSession, memoryState, type StateHandle } from "@gomomento-poc/mo-sdk";

// In process, private to this program:
let state: StateHandle = memoryState();

// Or the record the rest of the login shares. `session.state` is optional because most sessions
// never ask for one, so this narrows the type — it is not a guard: an open that asked for state and
// returned has it.
const session = await createAgentSession({ model: "momento/…", state: true });
state = session.state ?? state;

// Everything below is the same code either way, which is the point of the interface.

const created = await state.set("league:table", { round: 1 }, { ifAbsent: true });
if (!created.ok) console.log(created.reason);       // "exists"

const current = await state.get("league:table");
if (current.ok) {
  const won = await state.cas("league:table", current.rev, { round: 2 });
  if (!won.ok) console.log(won.reason);             // "stale" | "missing" | "unknown"
}

const bumped = await state.update("league:table", (value) => ({
  round: ((value as { round: number } | undefined)?.round ?? 0) + 1,
}));
if (!bumped.ok) console.log(bumped.reason);         // "exhausted" | "missing" | "unknown"

Every method resolves to { ok: true, … } or { ok: false, reason }, with the contract's own name for each reason. unknown is a write the host sent and got no answer to: it may or may not have landed, and nothing retries it. A refusal throws a StateError whose code is also the contract's name — bad_key, bad_ttl, bad_rev, too_large, too_deep, bad_schema (with a reason) for a stored record the reader cannot present as data, and the host-only rate_limited, denied, unavailable and error.

Keys are [a-z0-9._:-]{1,128}. Data is any JSON value, at most 64 KiB serialized and 32 levels deep. Every write carries a TTL in seconds (1 to 86400, default 3600); a record can expire between a read and its cas, and the answer is stale. update reads, applies a pure function (which receives undefined for an absent key), writes, and retries a lost race up to maxAttempts (default 8, at most 32), so the function may run more than once. It does not retry a deletion: that answers missing.

memoryState(store) shares one map between handles, which is how a test models several sessions on one login, or the contract's direct writer: a record placed in the store by hand is read with the same checks a host applies.

session.state is undefined unless the session asked for state, so which sessions hold a record is a question the type answers rather than one every call discovers. Asking for it and being refused fails the open instead, because a session that cannot hold the record it asked for should say so before a program has decided it has one. A session holding no grant answers every call unavailable. openIntercom({ state: true }) gives a presence the same handle, which is what a scoreboard is.

Entry points

| Import | Contents | |---|---| | @gomomento-poc/mo-sdk | createAgentSession, openIntercom, configureSdk, memoryState and StateHandle, the protocol types, the codec, the transport | | @gomomento-poc/mo-sdk/protocol | Wire types alone | | @gomomento-poc/mo-sdk/codec | decodeLine / encodeLine / LineDecoder, for a host you transport yourself | | @gomomento-poc/mo-sdk/transport | spawnTransport, for a process you supervise yourself |

What the handshake settles

session.hostVersion is the host binary's version as it announced itself — informational, never something to branch on, but the first thing a version-skew report needs. session.maxFrameBytes is the per-frame byte budget in force: what the host advertised, capped at 16 MiB. Sending a frame over it throws rather than writing a line the host would reject, which is worth knowing before you put a large result in a custom tool.

A resolver that takes longer than the host's window loses the decision: the host has already failed that call closed, so the SDK stops holding the slot and does not send the late answer. It reports an mo/error with code callback_expired when that happens.

Errors

Three, and which one you caught tells you what to do next. SessionRequestError is the host declining something on purpose — it carries the JSON-RPC code, and the session stays usable. SessionProtocolError is a host reply this SDK could not read — the host is not speaking the protocol you compiled against, so check its version. From prompt the session survives it; from createAgentSession the handshake failed and the child is already stopped. SessionClosedError is the session being over, and every promise still pending rejects with it.

Building a worker

Everything a containerised multi-role worker needs is above: a role is a set of session options, a structured decision is a terminal custom tool, pausing is a tool result with pause: true plus session.resume in the next process, and policy the model must not negotiate goes in permissions and the two hooks. Ask your Momento contact for the integration guide if you want those assembled into a worked example.

License

Apache-2.0