Mobs are the multi-agent path in Meerkat. The agent-facing
delegate tool,
explicit mob_* tools, SDK Mob classes, mob/* RPC methods, public MCP mob
tools, and mobpack deployment all route through the mob system.Choose A Path
Mental Model
AMobDefinition describes profiles, limits, wiring rules, topology, and
flows. The running mob records the roster, member lifecycle, events, flow runs,
and work state. Each member is an agent session, but public mob APIs address the
member by stable AgentIdentity, not by an internal session or runtime binding.
Core Concepts
Agent Tools Vs Host APIs
Keep this distinction sharp:
Agent-side tools are late-bound through the session build path. Host APIs are
the stable control plane for applications and SDKs. Do not treat raw
mob_*
agent tools as if they were JSON-RPC methods.
Fast Path: Delegate
delegate creates an implicit session-owned mob on first use, spawns a helper,
and wires the helper to the creating session. Use it for bounded helper work
that should report back.
Define A Mob
A small mob definition has profiles and optional wiring:Read-only profiles
tools.read_only = true declares a profile whose members may execute only
tools their owning dispatcher declares read-only. It is enforcement at the
execution gate, not prompt guidance, and it is the floor: a spawn request
cannot widen it.
access_denied tool error, so the run
continues and the transcript records the refusal. The model-visible tool list is
unchanged (gating never moves the prompt-cache prefix).
What the declaration covers, and what it cannot:
Anything undeclared is denied: over-denial is honest, silent admission would be
a false guarantee. Hosts widen the read surface by implementing
AgentToolDispatcher::tool_mutation_class on their own dispatcher.
Profile model and provider fields
Beyondmodel, a profile can pin provider identity and per-member behavior:
provider_params decision. From
0.8.33 the default is automatic on every backend that supports it (Anthropic
API, Vertex, and Foundry); Bedrock and other backends that report automatic
caching as unsupported stay disabled. Releases 0.8.22 through 0.8.32 default
to disabled on every backend, so a mob that must behave the same on both
sides of that boundary sets cache_control explicitly. Opt a profile out, or
force caching on, with the nested provider_tag form:
cache_control lives inside provider_tag, not at the top of
provider_params. The flat form provider_params = { cache_control = "disabled" }
is rejected by the closed field set of ProviderParamsOverride and fails the
whole definition parse, so a mob that boots today stops booting after that
edit. The vocabulary is automatic, system_prefix,
system_and_conversation, and disabled; cache_ttl is 5m or 1h and
must be paired with an explicit non-disabled cache_control in the same tag.
Add provider_params to resume_overrides when the change must reach members
whose durable sessions already exist.
Member system prompt
peer_description is the roster blurb other members see. It becomes the
member’s PeerMeta.description, so it shows up in a peer’s peers tool output
and in the mob.peer_added lifecycle notice wired peers receive. It is never
part of this member’s own system prompt.
The member’s own prompt comes from profile.skills. Each listed id is resolved
against a [skills.<id>] table in the definition, and the resolved texts are
joined in list order into the system prompt. An id with no [skills.<id>]
table is a missing_skill_ref validation error and contributes nothing:
DurableAgentSpec.additional_instructions,
appended after the skills text and baked once at materialization, and an
AgentCustomizer whose draft.system_prompt replaces the assembled prompt.
A host that keeps its own per-profile prompt key (HomeCore’s role_summary)
parses that key itself and lowers it to additional_instructions.
There is no system_prompt key on a profile. Loading a TOML definition whose
[profiles.<name>] table carries system_prompt (or its aliases prompt and
instructions) fails with MobError::UnsupportedProfileKey, whose message
names the profile, the key, and the mechanisms above; a realm-reference table
(realm_profile = "...") is refused the same way. Any other key the table
does not declare (a host-private key such as HomeCore’s role_summary, a typo
such as comm = true under [profiles.<name>.tools], or anything but
realm_profile on a realm reference) is reported once per key as an
unknown_profile_key warning diagnostic, returned by
MobDefinition::parse_toml, and from_toml logs one warning per affected
profile when the definition loads; parsing continues, and the key is ignored.
No Meerkat surface emits that diagnostic today: a host that loads mob.toml
itself calls parse_toml to surface it, and a definition supplied as JSON is
not inspected.
Custom model registry entries
[models.<id>] tables declare uncatalogued models once, at the definition
level. One entry feeds provider inference, compaction scaling, capability
gates, and call timeouts for every profile that references the model:
[models.<id>] table shape works in the host’s config.toml under
[models], next to the per-provider default model strings.
At load time, rkat mob validate rejects a profile model that is neither
catalogued, custom-defined under [models.<id>], nor provider-annotated
(unknown_model), instead of failing at the member’s first delivery.
Create And Spawn
- JSON-RPC
- Python
- TypeScript
- Web SDK
- CLI
Autonomous members run as long-lived peers.
turn_driven members are useful
when a host wants explicit dispatch control.
Trusted Rust member build overrides
An in-process Rust host can attach a per-memberArc<dyn CompactionCurator> through
SpawnMemberSpec.compaction_curator_override. The exact curator reaches the
session build for that spawn or cold resume. It is executable process-local
behavior, not durable configuration: re-supply it from a
SpawnMemberCustomizer after restart. Remote placement rejects it before
materialization instead of dropping the override.
Identity And Respawn
Mobs separate stable member identity from runtime binding details:
Use
AgentIdentity for facts that survive respawn, such as wiring and durable
configuration. Runtime IDs and fence tokens protect the lower-level binding.
Durable role migration
A durable member may keep the same mob id,AgentIdentity, session, and
transcript while moving to a new role. This is a one-request cold Resume
contract, not a profile alias or standing migration rule.
For an explicit trusted Rust-host resume, declare the exact durable predecessor
role on the spec:
SpawnMemberCustomizer and call spec.declare_resume_from_role("domain")?
only when ctx.spawn_source == SpawnSource::Resume, after checking the exact
member identity and requested target profile. The declaration is intentionally
not persisted, so each later migration or restore must re-supply current host
intent.
The runtime enforces all of these facts:
- mob id,
AgentIdentity, and exact durable session stay fixed - the declaration must equal the one stored predecessor role
- when the stored and requested roles differ, an omitted declaration returns
MemberRoleMigrationRequired - an incorrect, inapplicable, or live-session declaration returns
MemberRoleMigrationRejected - even an idle live actor cannot be migrated; the exact session must be cold
- success restamps the current comms name, typed member binding, role/profile labels, callback context, and explicitly configured tooling
- rollback is another declared forward migration or separately versioned durable state, never an old binary silently reclaiming the predecessor role
Wire Peers
Wiring controls which members can see and message each other.- JSON-RPC
- Python
- TypeScript
Send Work
Use member send for direct content delivery. Use the work lane when the caller needs a tracked, cancellable work reference.mob/member_send, SDK Member.send, and mob/submit_work with
origin: "external" require the target’s profile to set
external_addressable: true (the default is false). The definitions above
opt in only the lead as the application’s entry point. For tracked work,
choose a member_ref belonging to an opted-in member. This opt-in is not
required for internal flow dispatch or ordinary peer messaging.
- Member send
- Tracked work
member_ref is an opaque handle returned by spawn, member list, member send,
helper spawn, fork, and respawn responses. Application code should pass it back
to work-lane APIs as-is instead of constructing it from mob_id and
agent_identity.
Flows
Flows are declarative mob workflows. They let a host dispatch repeatable work without hard-coding all member turns in application code. The classic flow shape is a flat DAG: steps declare roles, messages, dependencies, fan-out/fan-in behavior, optional conditions, and tool overlays. Frame-based flows add nestedFlowSpec.root frames and repeat_until loops.
Both are owned by the mob runtime; support modules such as flow-run projection
are not separate public machines.
Flow parameters and message templates
Flow activation parameters are available to text in step messages. Use{{ params.<path> }} for input, {{ steps.<step-id>.output.<path> }} for a
root-frame result, and
{{ loops.<loop-id>.iterations.<n>.steps.<step-id>.<path> }} for an exact
loop iteration. The .output segment on a root step is optional.
null.
Nested output lookups also depend on the producer step’s output_format:
An explicit format always wins over the schema-aware default. A profile’s
output_schema constrains the member’s structured response; it does not
override a flow step’s text mode. Asking a text-mode producer to return JSON
is therefore not enough to make steps.<step-id>.output.<field> work.
Dispatch and collection still determine the enclosing step-result shape. This
example uses one_to_one with collection_policy: { "type": "any" } so
scan exposes the selected member’s output directly instead of a member-keyed
aggregate. Add this flow to a definition containing the lead and analyst
profiles above:
Advisory nodes (failure_policy)
By default every flow node escalates: if it fails, its frame is classified
Failed and the run fails. Declare "failure_policy": "continue" on a step,
a frame node, or a repeat_until node to mark it advisory. An advisory node’s
failure is still recorded - the node stays Failed in frame state and the step
is still reported failed - but it no longer decides the frame’s terminal
classification.
continue does not keep that node’s dependents running. Dependency
resolution is unchanged by failure_policy. With depends_on_mode: "all", a
single dependency ending Failed, Skipped or Canceled sets the dependent
Skipped; with depends_on_mode: "any", the dependent is set Skipped only
when every dependency ends that way. Either way, a step whose only dependency is
an advisory node does not run when that node fails: the run can still reach
completed, but that dependent’s step status is skipped and a step_skipped
event is emitted for it. If you want work to happen after an advisory failure,
the dependent must have a dependency that can still complete.
That makes continue useful for one shape: an advisory node and a fallback node
feeding a join with depends_on_mode: "any". The advisory node’s failure is
tolerated, the fallback completes, and the join runs on the fallback’s output.
advisory-analyst fails here, baseline-analyst still completes, so
write-up is Ready rather than Skipped and the run completes with the
report written. Drop baseline-analyst and write-up is skipped instead.
Omitting failure_policy means escalate. Frame classification is owned by
MobMachine, so a tolerated failure never becomes a fabricated success: the
failed node remains visible in the frame’s typed state, the step is recorded
failed with a failure-ledger entry, and a step_failed event is emitted. Both
the ledger entry and the event carry the step’s real failure reason. If the
run’s supervisor escalation_threshold is crossed by that failure, the
supervisor is still escalated.
If that escalation cannot be delivered - no member currently holds the
supervisor role, the supervisor’s turn fails, or the escalation turn exceeds
escalation_turn_timeout_ms - the run’s terminal class is unchanged. Failing to
carry out an escalation is not a decision that the run failed, so a tolerated
failure still completes; the undelivered escalation is reported as a
supervisor_escalation_failed event carrying the escalation error, and no
supervisor_escalation event is emitted. The mob is not torn down.
Run and inspect a flow:
- JSON-RPC
- Python
- TypeScript
- CLI
Auditing a run
Every run-result surface carries anaccounting block next to the flow result:
rkat mob run --json, rkat mob attach --json (the detached-run audit path),
the mob/run_result RPC method, and the meerkat_mob_run_result MCP tool. The
two polled surfaces (RPC, MCP) attach it only once the run is terminal, so a
mid-run poll stays cheap and never reports totals for an unfinished run; the CLI
paths wait for terminality before collecting. The block is also absent when
collection failed, which the CLI reports on stderr.
attribution: session_cumulative- each number is the lifetime total of that member’s bridge session. It equals the run’s usage when the session served this run alone; on a mob that has executed several runs it includes the other turns. Nothing in the durable run projection records per-run usage.usage_totalis the sum of the readable members only. Whenmembers_usage_unavailableis non-zero (remote members, archived sessions) it is a floor, and each affected member carries ausage_unavailablereason instead of a zero. Cache and reasoning counters are summed too. Each member’s usage is normalized first, including members whose sessions were saved before 0.8.22, so on every provider cache reads plus writes stay withininput_tokensand reasoning withinoutput_tokens.membersis the mob’s current roster, not this run’s participant set: the run projection records no participation, so a member that took no part in the run is still listed and its session id still rides inmember_session_ids. Exporting the whole pointer set can pull transcripts unrelated to the run.- No monetary cost is reported. The model catalog carries no price data, so
a cost number would be invented;
unpriced_reasonsays so explicitly.
usage, cost, and transcript lines,
the last of which carries the ready-to-run rkat session export-atif <id>
command. rkat run --export-atif writes a trajectory for a single-session run.
Observe And Operate
The event log is append-only. For UI and service loops, prefer event cursors or
SDK subscriptions over repeated full snapshots.
rkat mob force-cancel <MOB_ID> <AGENT_IDENTITY> (RPC mob/force_cancel)
cooperatively cancels a member’s in-flight turn without retiring it — the
member stays in the roster and can take new turns. As the operator remedy for
a wedged member it is legal whenever the mob is running and idempotent:
cancelling a member whose runtime is no longer live, or one already retiring,
converges as a no-op success rather than an admission error. Only an identity
the roster has never seen is refused, with a typed MemberNotFound.
Persistence
Persistent mobs use SQLite/WAL-backed storage. In-memory storage is used for tests and WASM/browser-embedded paths. The mob store is realm-scoped in the runtime-backed surfaces, so a process restart can recover mob state, members, events, flow snapshots, and work records.Cold resume and cancellation
Reconstructing a stopped mob does not implicitly start its members. Explicit resume prepares the exact session attachments, commits the lifecycle transition, reconstructs members, and then reconciles readiness, topology, and operation bindings. Member construction and external reconciliation run outside the actor loop; status queries and unrelated members do not wait behind one member’s constructor. The publicrunning phase can precede completion of this sequence. Await the
resume operation’s result rather than treating that phase alone as a readiness
receipt. An observer timeout or dropped caller does not cancel process-owned
construction or prove that it did not execute.
Lifecycle cancellation retains ownership until admitted work settles.
Retirement, respawn, and registration reload wait behind the affected member’s
in-flight resume work. Cleanup failures retain exact retry authority; an
unproven cleanup is not a successful cancellation. Compensation preserves
durable session documents and refuses to remove a successor attachment.
Admission in custom Rust session services
A customMobSessionService that executes standalone turns must implement
start_turn_with_admission_notification. Send its notification only after
generated admission and command handoff; spawning a task or waiting for
terminal completion is not a substitute. The built-in ephemeral service
provides this contract. The default custom-service implementation returns
Unsupported, rather than allowing a later delivery or retirement to overtake
an unacknowledged admission.
Supervisor Rotation
mob/rotate_supervisor is a synchronous-looking view over a durable
operation. The mob records a stable operation ID and the complete target
authority before sending the one-way handoff to any member. Each member fences
the old epoch, advances the handoff independently, and exposes a durable
pending, completed, or rejected receipt for that operation ID.
A caller timeout means only that the terminal receipt was not observed before
the deadline. It does not cancel or roll back the member operation; a retry
uses the persisted operation ID and resumes observation. See
Delivery, Interaction, and Durable Operations
for the protocol boundary and recovery semantics.
Multi-Host Placement
Use a member-host daemon when the controlling mob should place and supervise members on another machine. The daemon uses its own restart-stable realm, ideally a dedicated realm or context root; realms are not distributed and do not need to match the controller’s realm.--isolated is rejected because it
would select a new throwaway realm on every restart and lose the daemon’s
durable member-host state.
advertise_tcp must be dialable from every member host. Without an explicit
controller endpoint, mixed-host route installation remains pending and fails
closed rather than publishing a process-local address.
The descriptor handoff above does not require network pairing. If you enable
the host’s pairing branch, provide its runtime-only secret through
--pairing-password-env <ENV> or --pairing-password-file <PATH> so it stays
out of process arguments. These options conflict with each other and with the
compatibility-only --pairing-password <PASSWORD> form. The secret is never
written to [mob_host] configuration and must be at least 32 bytes.
The bind report’s host id is the placement value for mob/spawn and
mob/spawn_many. Omitting placement keeps the member local:
session/* handle. Drive the member by
agent_identity, and read its transcript with mob/member_history. Generic
session history, transcript revision, and transcript edit operations are not
proxied across hosts in v1.
Placed-member portability
Remote placement compiles one digest-coveredPortableMemberSpec. Admission
rejects host-local or secret-bearing behavior before any materialization
request; it never silently strips the field and launches a weaker member.
The typed non-portable causes cover:
- Rust tool bundles, per-spawn external tools, mob-default external tools, and a default LLM client override
- an in-process compaction curator override
- a host-surface MCP allow-list, an inherited tool filter, and WorkGraph tools
- non-empty shell environment, MCP stdio environment, or MCP HTTP headers
backend or
RuntimeBinding: one member has one execution transport story.
WorkGraph commitments and stores stay on the controller, so a profile that
requests WorkGraph tools cannot be placed remotely. Schedule tools are
portable. Memory and declarative MCP servers can be portable, but only when
the bound host advertises the required capability and the MCP declaration does
not carry secret environment or header values. The same admission ladder
fails closed when the host lacks required autonomous-member, durable-session,
tracked-cancel, protocol-v4, memory-store, or MCP capability.
Operator surface boundaries are intentional:
The identity-routed mob member live family is WebSocket-only. An explicit
transport = "webrtc" request to mob/member_live_open is rejected with
LiveTransportUnsupported for both local and placed members. Use the
session-scoped live/open path for a controller-local session when WebRTC is
required; there is no controller session/* path to a placed session.
Use mob/hosts, mob/route_installs, and mob/member_history (or the
matching CLI commands) for placement diagnostics. Python and TypeScript expose
the full typed RPC family. REST and public MCP intentionally expose the three
read-only observations, while host binding, grants, hard cancel, and member
live control remain outside REST and public MCP. Host binding, grants, and
member live control also have explicit CLI verbs. On public transport
surfaces, hard cancel is RPC/SDK-only; trusted in-process MobHandle callers
also expose it as hard_cancel_member (mob/hard_cancel_member, Python
Mob.hard_cancel, and TypeScript Mob.hardCancel). rkat mob force-cancel
is the distinct cooperative boundary cancel.
Browser/WASM mobs remain single-host and reject non-local placement through
their typed capability boundary.
Embedded and explicitly delegated non-owner console principals need control
scopes. Stock v1 RPC, REST, stdio, MCP, and CLI entrypoints mint the owning
console principal; manage narrower delegated principals with rkat mob grant,
rkat mob revoke-grant, and rkat mob grants. Scope denials and
host/cursor/fence failures are typed consistently across console surfaces.
External Members
Most members are normal session-backed agents. External members are advanced: they require an external runtime binding with a concrete address and trusted peer identity so the orchestrator can route supervisor bridge traffic to the right process. This binds one already-running external member; for a managed host that can materialize multiple placed members, use the member-host flow above. For a remoterkat process, start it with the signed comms listener and
write the binding file:
--comms-binding-out writes the current external binding shape: kind: "external", advertised address, Ed25519 public identity, and typed
bootstrap_token. Current mob supervisors require that typed bootstrap token
for external bridge binding; a bare External backend tag, a raw address, or a
query-string-only bootstrap token is rejected.
Use external members only when the member must run outside the local Meerkat
runtime, such as another host, sandbox, or service process.
Live Channels
Live channels execute in the owning session. For a mob member, address the member by identity throughrkat mob live open, RPC
mob/member_live_open, Python Mob.member_live_open, or TypeScript
Mob.memberLiveOpen. That path is placement-transparent and returns the
owning host’s WebSocket endpoint.
The mob member family deliberately rejects explicit WebRTC for local and
placed members. If a controller-local member needs WebRTC, use the generic
session-scoped live/open surface with its local session id. A placed session
cannot be opened through the controller’s generic session surface.
See Live channels.
Mobpacks
A mobpack packages a mob definition and optional assets into a portable artifact:mob web build copies required prebuilt
wasm-pack output into that bundle; it does not compile wasm32.
Troubleshooting
See Also
Mob architecture
Runtime ownership, member identity, flows, persistence, and live-channel boundaries.
Mobs concept
The conceptual model behind members, profiles, wiring, and host-vs-agent surfaces.
Mobpack
Package, sign, validate, deploy, and build browser-target mob artifacts.
