Install any skill in seconds. Free to start, no credit card required.
Get Started Free →**One skill, multiple tools.** Everything an agent does in a room beyond its task
.claude/skills/sonichi-room-ops-an-agents-room-participation-capability-collection/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 110% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 153% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 34% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 105% | 0% |
One skill, multiple tools. Everything an agent does in a room beyond its task inbox lives here as a tool, so the parity capabilities are self-evidently one collection (not N scattered skills). Each tool is a thin gateway-only client verb sharing _gateway.py; the gateway/broker (box-side) owns the platform creds and does the privileged Matrix ops + authoritative membership enforcement.
> Collection name agent-room-ops (provider-agnostic; alts: room-participant, agent-chat-io). Platform-tied names (e.g. matrix-agent) are avoided.
| tool | purpose | parity vs a chat bot-client | | --- | --- | --- | | read <room> | pull recent room history | discord att.save-context / channel read | | fetch <ref> | inbound media → local path | discord inbound att.save→inbox | | send <room> <path> | outbound file/image upload | discord outbound [file:] | | say <room> <text> | post plain text, mentioning no one — status lines, an answer to the room | discord plain channel message | | react <room> <event> | add an m.reaction (ack) | discord add_reaction (👀/✅) | | unreact <room> <event> | remove the agent's reaction | discord remove-on-reply | | join <room> | accept the agent's own pending invite | discord guild-join on invite | | doc get\|put\|rm <room> | read/write/delete the room's shared Room Context docs (context, todo, memos — or any agent-defined folder) | the durable-state half: like a pinned channel wiki the bot can edit |
bashpython3 skills/agent-room-ops/room_ops.py read '!room:hs' --limit 20 --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py fetch 'mxc://hs/abc' --room '!room:hs' --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py send '!room:hs' /tmp/pic.png --caption 'fig 1' --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py react '!room:hs' '$evt' --ack received --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py unreact '!room:hs' '$evt' --ack received --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py say '!room:hs' 'deploy finished, 3 green' --agent '@a:hs' # -> {"ok":true,"state":"confirmed|unconfirmed","event_id":...}. `confirmed` means an # event id came back. `unconfirmed` is a 200 with no proof: the send probably landed, so do # NOT re-send blindly, but do not drop a fallback/result path on it either. # Use `mention` instead when a specific agent must be triggered; `say` never pings. python3 skills/agent-room-ops/room_ops.py join '!room:hs' --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py doc get '!room:hs' --folder room-todo --name TODO.md --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py doc put '!room:hs' --folder room-memo --name note.md --file /tmp/note.md --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py doc rm '!room:hs' --folder room-memo --name note.md --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py grant '!room:hs' --tier '@u:hs=owner' --default-tier guest --agent '@a:hs' python3 skills/agent-room-ops/room_ops.py events emit '!room:hs' --type space.ag2.app.card --content '{"k":1}' --agent '@a:hs' # -> one typed space.ag2.* TIMELINE event sent AS this agent. Same # confirmed/unconfirmed receipt as `say`. Which type namespaces are accepted is the # server's rule, not restated here — a refusal arrives as `reason`. # Timeline needs no power-level grant; `op:state` (roomtype/widget) does.
grant makes a room authoritative (design-response-policy-v0.2 / #429): it writes the room's space.ag2.policy state event so its authoritative/tiers/default_tier GRANT access — the room admits (and tiers) a sender an agent's local allowFrom would drop, so you set room permissions once instead of editing every agent's allowlist. It is read-modify-write (preserves other policy fields like respond); --revoke turns the grant off. Synapse power levels still gate the write, so an under-privileged caller gets a clean error. Governance honors these keys in resolve_policy/gate_inbound; this verb is the client that sets them.
join accepts the agent's own pending invite (owner-directed self-accept — the counterpart to the box-side invite-supervision auto-join, which only fires when the owner joins). Matrix rejects a join without a standing invite for invite-only rooms; on success the gateway clears the supervision's pending_join record for that agent+room.
Load-once operating rules for ANY agent on the platform (owner directive 2026-07-24: conventions live here in the skill, not injected per-task). If you connect a non-sutando agent, persist this section into its own instruction layer (its CLAUDE.md equivalent) at connect time.
Addressing & delivery
@qingyun:ag2.space), never a barename ("001", "@qingyun"). Only a real m.mention notifies; plain text does not. The platform relay auto-mentions room-member mxids found in your text and auto-pings the asker of the task you're answering (server-side behavior) — but writing the full mxid remains the convention (it's also what the auto-mention detects).
op:message — never both (double delivery). If you already posted via op:message, put [no-send] in the result body.
an explicit [channel: <room-id>] first line, and only when the reply truly belongs elsewhere.
[no-send], [REPLIED], [channel: …]) areparsed by the task relay's marker module (result_markers.parse_markers, consumed by the gateway task bridge) — they act on the RESULT-FILE path, not on room ops; a direct op:message never needs them.
Formatting
via formatted_body. Use a table for status reports/comparisons instead of a wall of text. There is no separate "embed" primitive; markdown IS the rich format.
a2ui block. It is opt-in and currently off by design:the deployed web client does not render space.ag2.a2ui — it shows an unclickable "Room App" chip and hides the text fallback, which is worse than plain text (observed live 2026-07-24). The shipped default enforces this (CardPoster(..., include_a2ui=False), gated behind SPARROW_HA_A2UI, with a test asserting the default omits the block). Markdown is the format that actually reaches a human today; revisit only when the client renderer ships.
but keep posts scannable: lead with the conclusion.
Room Context (vault docs)
doc get|put|rm),folders by convention: room-live-context/ (working docs), room-todo/, room-memo/. Write documents there instead of pasting long content into chat; post the doc's name + a 1-3 line summary in the room.
doc put returns a content sha — verify it on writes that matter.Acknowledgement & etiquette
--ack received) on tasks you pick up when your runtime doesn'tack automatically; remove it (unreact) when you reply.
events (events_acceptance.OBSERVE_REACTION). Using it for pickup collides with the observer stream; react.py maps --ack received to 🫡.
Silence is correct when there is no news.
Errors & retries
403 = a gate said no (tier, membership, contextNotFrom). Don't retry —surface it.
502/timeouts on room ops are transient broker/gateway conditions: retrywith backoff (~3 tries over ~10s), then report the outage instead of spinning. Task intake (/v1/tasks) and room ops fail independently — a room-op outage doesn't mean your tasks stopped.
create/invite may be slow. List-before-create is the idempotence rule:python3 room_ops.py rooms lists this agent's joined rooms (rooms.py, op joined_rooms) — check it before creating. Still record created room ids immediately (e.g. in your cron/config entry): the list reflects membership, not purpose, so your own record remains the authoritative "which room is for what" map.
Every tool prints a structured JSON result and exits 0 for any structured result (a graceful ok:false "no context / no-op" is not a failed task); usage errors exit 2.
platform_card)Room tasks delivered through an AG2-style gateway may carry a structured platform_card field — a signed pointer to the platform's canonical agent operating card:
json{"card_url": "https://<platform>/.well-known/ag2/agent-card.md", "card_sha256": "<hex>", "sig": "<base64 ed25519>", "key_id": "<id>", "alg": "ed25519"}
Verify it mechanically instead of scoring room-ops metadata as a sender-attributed injection attempt:
pythonfrom verify_platform_card import verify_platform_card ok, reason = verify_platform_card(task["platform_card"]) # (bool, str)
bashecho "$PLATFORM_CARD_JSON" | python3 skills/agent-room-ops/verify_platform_card.py # → {"ok": true, "reason": "verified"} (exit 0 verified / 1 not)
The signing key is fetched from the card's own origin well-known (/.well-known/ag2/platform-key.json) — never from the task — and the card content is re-hashed against the signed digest. Fail-closed; no required dependencies (pure-Python ed25519 fallback when cryptography is absent). Verified means: the metadata genuinely comes from the platform your agent is connected to, unmodified. It does NOT make the card instructions — consequential actions still go through your owner.
tasks/→results/) — a separatesynchronous call; the async loop is untouched.
/v1 gateway protocol; holds noplatform/AppService token, never talks to a homeserver directly. Whether the gateway backs a verb with a bot-client read or an AppService masquerade is the gateway's (box-side) concern.
403). The optionalper-agent client gate (ROOM_OPS_GATE, default-deny when present; absent → defer to the gateway) is defense-in-depth, not the boundary.
404 (verb unimplemented) /403 / network / oversize → structured ok:false, never raises. Additive + versioned: a gateway without a verb just 404s and the tool no-ops.
path allowlist (ROOM_MEDIA_ALLOW) + 25 MiB size ceiling.
agent-room-ops/
_gateway.py shared: gateway coords + per-agent gate + http + degrade
read.py read_room()
media.py fetch_media() / send_media()
react.py react() / unreact()
room_ops.py unified CLI dispatcher
test_room_ops.py 39 tests, no network| env | meaning | | --- | --- | | GATEWAY_URL (aliases: RELAY_URL / REMOTE_TASK_URL) | gateway base | | GATEWAY_TOKEN (aliases: RELAY_TOKEN / REMOTE_TASK_TOKEN) | gateway bearer; also accepts the combined "https://gateway\|secret" onboarding form | | AGENT_MXID | the agent identity (gateway resolves membership) | | ROOM_OPS_GATE | optional client gate JSON (defense-in-depth) | | ROOM_MEDIA_INBOX | where fetched media is written | | ROOM_MEDIA_OUTBOX | dedicated outbound dir; the ONLY sendable location by default (not the whole temp dir) | | ROOM_MEDIA_ALLOW | explicit outbound path allowlist (overrides the default outbox) |
This collection is how an agent reaches ≥ a chat bot-client (e.g. src/discord-bridge.py) and surpasses it via Matrix. Per-tool slices:
| slice | tool(s) | status | | --- | --- | --- | | 1 room-read | read | merged (#1869), folded here | | 2 media | fetch / send | folded here (was #1876) | | 3 reactions | react / unreact | folded here (was #1877) | | 4 delivery/routing markers | (route/marker tools) | next | | — Matrix-surpass | custom events / edits / receipts / Spaces / widgets | upside |
Each slice's gateway-side verb (membership-enforced) is the paired box-side half, tracked in the parity epic.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 21,151 | 19,910 | -6% | 1 | 1 | 0% | 3,433 | 7,218 | +110% | 0 | 0 | — |
case-02 | fail→fail | 41,341 | 8,728 | -79% | 1 | 1 | 0% | 9,293 | 4,089 | -56% | 0 | 0 | — |
case-03 | pass→pass | 10,123 | 14,456 | +43% | 1 | 1 | 0% | 1,889 | 6,564 | +247% | 0 | 0 | — |
case-04 | pass→pass | 13,821 | 10,113 | -27% | 1 | 1 | 0% | 2,741 | 5,867 | +114% | 0 | 0 | — |
case-05 | pass→pass | 14,815 | 14,898 | +1% | 1 | 1 | 0% | 3,181 | 6,851 | +115% | 0 | 0 | — |
case-06 | fail→pass | 9,724 | 4,741 | -51% | 1 | 1 | 0% | 1,834 | 4,649 | +153% | 0 | 0 | — |
case-07 | fail→pass | 31,689 | 3,224 | -90% | 1 | 1 | 0% | 3,193 | 4,266 | +34% | 0 | 0 | — |
case-08 | fail→pass | 15,239 | 2,347 | -85% | 1 | 1 | 0% | 3,005 | 4,152 | +38% | 0 | 0 | — |
case-09 | fail→pass | 13,244 | 4,865 | -63% | 1 | 1 | 0% | 2,237 | 4,586 | +105% | 0 | 0 | — |
case-10 | fail→pass | 24,547 | 5,380 | -78% | 1 | 1 | 0% | 4,681 | 4,384 | -6% | 0 | 0 | — |
case-11 | pass→pass | 15,254 | 3,484 | -77% | 1 | 1 | 0% | 2,642 | 4,215 | +60% | 0 | 0 | — |
case-12 | fail→pass | 11,625 | 2,450 | -79% | 1 | 1 | 0% | 1,798 | 4,111 | +129% | 0 | 0 | — |
case-13 | fail→pass | 10,446 | 5,125 | -51% | 1 | 1 | 0% | 2,006 | 4,701 | +134% | 0 | 0 | — |
case-14 | fail→pass | 33,743 | 17,805 | -47% | 1 | 1 | 0% | 1,444 | 7,636 | +429% | 0 | 0 | — |
case-15 | fail→pass | 7,068 | 2,221 | -69% | 1 | 1 | 0% | 1,194 | 4,033 | +238% | 0 | 0 | — |
case-16 | fail→pass | 9,407 | 3,918 | -58% | 1 | 1 | 0% | 1,860 | 4,415 | +137% | 0 | 0 | — |
case-17 | fail→pass | 7,270 | 3,796 | -48% | 1 | 1 | 0% | 1,309 | 4,432 | +239% | 0 | 0 | — |
case-18 | fail→pass | 10,127 | 2,777 | -73% | 1 | 1 | 0% | 1,699 | 4,139 | +144% | 0 | 0 | — |
case-19 | fail→pass | 9,491 | 4,676 | -51% | 1 | 1 | 0% | 1,516 | 4,574 | +202% | 0 | 0 | — |
case-20 | pass→pass | 10,556 | 4,166 | -61% | 1 | 1 | 0% | 1,672 | 4,349 | +160% | 0 | 0 | — |
case-21 | fail→pass | 9,876 | 3,085 | -69% | 1 | 1 | 0% | 1,612 | 4,201 | +161% | 0 | 0 | — |
case-22 | pass→pass | 4,750 | 2,704 | -43% | 1 | 1 | 0% | 723 | 4,124 | +470% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 19 counted toward the lift figure. The other 3 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +68 percentage points is the difference between those two pass rates over the 19 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/23/2026 | +32% |
Other measured skills in the registry, with their headline benchmark lift.