Multiplayer API
The classes behind Multiplayer, with their methods and what they return. For how to use them together, follow Build a Multiplayer Agent.
| Import |
Classes |
jarviscore.room |
Person, Account, Identities, IdentityError, Room, AskResult, Proposal, Head, FileChange, RoomFeed, FeedItem, RoomAgent, RoomSubAgent, Turn, RoomConversation, ChatSurface, ACCEPT, SHOW_CHANGE, RELEASE, DISCARD, CONFIRM, CANCEL, ROLES, VISIBILITIES, RoomConflict, RoomPermissionError |
jarviscore.room.slack |
SlackApp, SlackApi, SlackSurface, SlackError |
jarviscore.room.work |
resume, finish, unfinished |
jarviscore.human |
HumanAgent, HumanChannel, TerminalChannel |
jarviscore.hitl |
HITLQueue |
jarviscore.testing.rooms |
MemorySurface, ScriptedRoomAgent |
jarviscore.testing.slack |
FakeSlack |
People
| Member |
|
Person(tenant, id, name="") |
A person in a tenant. .key is "tenant/id": what credentials and audit records are keyed by. .display is the name others see |
Account(tenant, issuer, subject, name="") |
An authenticated account on a surface, such as issuer="slack:T0123" |
Identities(redis_store) |
Accounts, people, links and removals, in the mesh's Redis |
people.person_for(account, *, auto_provision=True) |
The person an account belongs to; a new account becomes a new person unless auto_provision=False. Raises IdentityError for removed people |
people.register(person) |
Record a person your application manages |
people.get(tenant, id) |
A person, or None if unknown or removed |
people.begin_link(person) / people.complete_link(account, code) |
A one-time code (10 minutes) that makes another account the same person |
people.accounts(person), people.unlink(account) |
A person's accounts; let an account be someone else |
people.deprovision(person) |
Remove someone: their roles stop applying and their accounts are refused |
people.log(tenant, since=0) |
Who was added, linked, unlinked or removed, oldest first |
Spending
from jarviscore.auth.spending import Spending, SpendCapReached
| Member |
|
Spending(redis_store) |
Per-person ledgers and caps, in the mesh's Redis |
spending.set_caps(tenant, *, person="", tokens_per_day=, tokens_per_month=, usd_per_day=, usd_per_month=) |
The tenant's caps, or one person's (which override); None clears one |
spending.caps_for("tenant/person"), spending.usage("tenant/person") |
The caps that apply; tokens, USD and calls this UTC day and month |
spending.check("tenant/person") |
Raises SpendCapReached if a cap is reached |
SpendCapReached |
.person, .period; a model call over a cap raises it before it is made |
Room
Opening
| Member |
|
Room.for_mesh(mesh, room_id, *, tenant) |
A room on a started mesh; it can ask the mesh's agents |
Room.open(room_id, *, tenant, redis_url=None) |
The same room without a mesh (no ask) |
await room.ensure(files=None, *, by) |
Create it with these files, owned by by, or return its head |
await room.ensure_from(snapshot, *, by) |
Create it from a captured source snapshot |
room.exists(), room.head() |
Whether it exists; its current Head (version, updated_by, proposal_id) |
room.id, room.tenant, room.key |
Its id, tenant, and "tenant/id" |
Reading
Reading methods are for trusted code; check room.require(person, "viewer", ...) before reading for a person.
| Member |
|
await room.read(path, version=None) |
A file's bytes, or None |
await room.files(version=None), await room.snapshot(version=None) |
All files; the snapshot |
await room.diff(proposal_id) |
FileChanges, each with .unified() |
room.proposal(id), room.proposals(status=None) |
A Proposal; all, or those in one status |
room.events(after=""), room.progress(workflow_id, after="") |
The room's history; a piece of work's ledger |
Work and proposals
| Member |
|
await room.ask(goal, *, asked_by, revision_of="", timeout=None, plan_template=None, workflow_id="") |
Run a goal on the room as asked_by; returns AskResult(workflow_id, status, proposal, summary, result, held_for, sources) |
await room.propose(changes, *, by, author="", title="", base_version=None, revision_of="") |
Propose content per path (None deletes); None if nothing changes |
await room.accept(proposal_id, *, by) |
Merge it in; returns the new Head. Raises RoomConflict (with .paths) |
await room.reject(proposal_id, *, by, reason) |
Turn it down |
room.release(proposal_id, *, by) |
Show held work to the room (only the person it is held for) |
await room.checkout(directory, *, by), await room.push(directory, *, by, title="") |
Work by hand |
room.private_sources(workflow_id) |
The connected systems a piece of work used that the room does not share |
room.audit(since=0, until=None) |
Records of everything done: at, by, by_name, action, target, detail, held_for; for work, workflow_id, sources and usage |
room.memory |
The room's team memory (RoomMemory) when Athena is configured; assignable |
room.delete(*, by) |
Delete it (owners) |
A Proposal has id, title, author, asked_by, base_version, changes, paths, status (open, accepted, rejected, conflicted), revision_of, decided_by, reason, conflicts, held_for and sources.
Who can do what
| Member |
|
room.role_of(person) |
viewer, editor, owner, or None |
room.can(person, role), room.require(person, role, action) |
Check a role; require raises RoomPermissionError |
room.visible_to(person, proposal) |
Whether they may see its contents (held work is for its asker) |
room.members(), room.name_of(person_id), room.settings() |
Person id to role; a name; visibility, tenant_role, shared_connectors, created_by |
room.set_role(person, role_or_None, *, by) |
Owners; a room keeps one owner |
room.set_visibility("members" \| "tenant", *, by, tenant_role="editor") |
Owners |
room.set_shared_connectors(providers, *, by) |
Owners: systems everyone in the room may see |
History
| Event |
Fields |
room.created |
by, by_name, files, source |
ask.started |
by, by_name, goal, workflow_id, version, revision_of |
ask.finished, ask.failed |
status, summary, proposal, held_for; error |
proposal.opened, proposal.released |
proposal, author, title, paths, base_version, revision_of, workflow_id, held_for |
proposal.accepted, proposal.rejected, proposal.conflicted |
proposal, by, by_name, version, merged_over, line_merged; reason; paths |
member.changed, room.visibility, room.connectors |
person, role; visibility, tenant_role; shared |
RoomMemory
from jarviscore.room.memory import RoomMemory
| Member |
|
RoomMemory(room, athena_client), RoomMemory.from_env(room) |
A room's team memory in Athena (tenant: the room's; scope: the room; session agent room) |
await memory.said(person, text, *, thread=""), await memory.replied(text, *, thread="") |
Keep a public message, or the agent's public reply |
await memory.sync() |
Keep what the room recorded since the last sync, once per room (RoomConversation.follow calls it) |
RoomFeed
| Member |
|
RoomFeed(room, state=None) |
Reads the room's history and its work's progress as lines |
feed.poll() |
New FeedItems: text, kind, at, workflow_id, proposal, detail, held_for, withheld, cursor |
feed.state(), feed.skip_history() |
Where it is, to resume from; start from now |
RoomAgent
An AutoAgent with a room Kernel role. See Build a Multiplayer Agent.
| Member |
|
await agent.converse(room, person, message, **turn) |
Handle one message from person; returns a Turn with replies and decisions |
agent.prepare(turn, action, target, reason="") |
Check and describe a decision for the person to confirm |
agent.running_work(room), agent.stop(room, workflow_id, *, by, reason) |
Work in progress; stop it (its asker, or an owner) |
agent.resume_work(room) |
Follow work started before this process to its end |
system_prompt, ask_options, subagent_class |
How it speaks; options for room.ask; its harness (RoomSubAgent) |
Its tools: room_status, running_work, room_history, read_file, show_change, ask_agents, request_changes, propose_version, prepare_decision, answer_question, stay_quiet.
RoomConversation and ChatSurface
| Member |
|
RoomConversation(room, agent, surface, *, poll_interval=1.0, decision_seconds=600, retention_days=90, audience_seconds=30.0) |
One room's conversation on one surface |
await conversation.start() |
Resume saved state, unhandled messages and unfinished work |
await conversation.follow() |
Post updates until cancelled (one process at a time) |
conversation.receive(person, text, *, message_id, thread=None, addressed=False) |
Record a message once; False for a redelivery |
await conversation.handle(message_id), await conversation.hear(...) |
Handle a received message; both at once |
await conversation.press(person, action, target, thread) |
A button |
conversation.recheck_audience(), conversation.can_see(person, item) |
Check who can see the surface before the next post; per-person visibility |
ChatSurface: key, say, notice, show_proposal, confirm, audience, history. See Chat Surfaces.
SlackApp
| Member |
|
SlackApp(agent, *, tenant, api=None, identify=None, poll_interval=1.0) |
Serves a tenant's rooms in its Slack workspace |
slack.connect(room, channel) |
Show a room in a channel (one room per channel) |
await slack.start(), await slack.run() |
Connect; serve until cancelled |
await SlackApi.through_nexus(mesh=None, *, api_url=...) |
Slack's Web API and Socket Mode through the slack and slack_app Nexus connections |
jarviscore room slack ROOM --channel ID --tenant T runs it from the terminal. See Chat Surfaces.
HumanAgent
A person or team on the mesh. See People as Peers.
| Member |
|
HumanAgent(name=None, capabilities=None, *, channel=TerminalChannel(), description=None, capability_descriptions=None, answer_timeout=None, agent_id=None) |
channel=None leaves work in the HITL inbox |
agent.answer(item_id, decision, response="", by="") |
decision: answered, approved, rejected or declined |
agent.waiting() |
The person's open items |
answer_timeout, poll_interval |
Seconds before unanswered work fails (3600), and between checks for an answer (1) |
A HumanChannel implements any of start(agent), show(item), withdraw(item_id), notify(sender, message) and stop().
HITLQueue methods for people's work
| Method |
|
queue.assign(title, content, *, targets, context=None, urgency="normal") |
File work routed to a person (input_request): no escalation category needed |
queue.inbox(target=None, *, tenant=None) |
Pending items for a person or role, from every host, oldest first; only one tenant's with tenant |
queue.resolve(request_id, decision, reason="", by="") |
Answer an item, once; resumes the mesh step an escalation paused. False if it was not pending |
queue.expire(request_id) |
Close an unanswered item so it leaves every inbox |
Items record the mesh step that raised them (context["mesh_workflow_id"], context["mesh_step_id"]) and the tenant of the person the work runs for (context["tenant"]). Escalations (queue.request(...)) are covered in HITL Escalation.
Testing helpers
| Helper |
|
MemorySurface() |
A ChatSurface that keeps what is posted: posts, confirmations, thread(id), texts(id), seen_by(person), public(); set audience_problem or fail_next |
ScriptedRoomAgent(*scripts) |
The real room tools driven by scripts, async (tools, turn) -> reply, one per message |
FakeSlack(users=..., channels=...) |
Slack's Web API and Socket Mode on a local port; people play with say, click, join and leave |