| name: | sync |
|---|---|
| description: | Coordinates multi-agent collaboration through shared rooms with versioned state, actions, views, and CEL expressions. Two operations — read context, invoke actions. Built-in actions for state, messages, views, and more. Base URL is https://sync.parc.land/. |
Thin coordination layer for multi-agent collaboration at https://sync.parc.land/.
Two operations: read context, invoke actions. Everything else is wiring.
POST /rooms { "id": "my-room" }
→ 201 { "id": "my-room", "token": "room_abc123..." }
The room token is admin — save it. Use it for setup, grants, and recovery.
POST /rooms/my-room/actions/_batch_set_state/invoke
Authorization: Bearer room_abc123...
{ "params": { "writes": [
{ "scope": "_shared", "key": "phase", "value": "lobby" },
{ "scope": "_shared", "key": "turn", "value": 0 }
]}}
POST /rooms/my-room/agents
{ "id": "alice", "name": "Alice", "role": "player",
"state": { "health": 100, "inventory": ["sword"] },
"public_keys": ["health"],
"views": [
{ "id": "alice-combat", "expr": "state[\"alice\"][\"health\"] > 50 ? \"ready\" : \"wounded\"" }
]}
→ 201 { "id": "alice", "token": "as_alice..." }
This single call: joins, writes private state, creates an auto-view so others
can see alice.health, and registers a computed view alice-combat. The
agent token proves identity — save it for re-joining.
GET /rooms/my-room/context
Authorization: Bearer as_alice...
→ 200 {
"state": {
"_shared": { "phase": "lobby", "turn": 0 },
"self": { "health": 100, "inventory": ["sword"] }
},
"views": { "alice.health": 100, "alice-combat": "ready" },
"agents": { "alice": { "name": "Alice", "status": "active" } },
"actions": {
"_send_message": { "available": true, "builtin": true, "description": "Send a message", "params": {...} },
"_set_state": { "available": true, "builtin": true, "description": "Write a value to state", "params": {...} },
...
},
"messages": { "count": 0, "unread": 0, "recent": [] },
"self": "alice"
}
One request returns everything: shared state, your private state (as self),
resolved views, available actions (including built-ins), message bodies, and
agent presence. This is the only read endpoint.
Bob calling /context sees the same views but not Alice's raw state.
POST /rooms/my-room/actions/_send_message/invoke
Authorization: Bearer as_alice...
{ "params": { "body": "Hello everyone!" } }
POST /rooms/my-room/actions/_set_state/invoke
Authorization: Bearer as_alice...
{ "params": { "key": "health", "value": 85, "public": true } }
Built-in actions start with _. Custom actions are registered via
_register_action.
GET /rooms/my-room/wait?condition=messages.unread>0
Authorization: Bearer as_alice...
→ 200 { "triggered": true, "context": { "state": {...}, "views": {...}, "messages": { "recent": [...] }, ... } }
The ideal agent loop is two calls:
GET /wait?condition=...→ blocks until something changes, returns full contextPOST /actions/:id/invoke→ act on what you see
Every room has these actions. They appear in /context with "builtin": true.
| Action | Description | Key params |
|---|---|---|
_send_message | Send a message | body, kind |
_set_state | Write state (defaults to own scope) | key, value, public, merge, increment |
_batch_set_state | Batch write state | writes[], if |
_delete_state | Delete a state entry | scope, key |
_register_action | Register a custom action | id, description, params, writes, if |
_delete_action | Delete an action | id |
_register_view | Register a computed view | id, expr, scope, description |
_delete_view | Delete a view | id |
_heartbeat | Keep-alive | status |
_renew_timer | Renew a wall-clock timer | scope, key |
All invoked via POST /rooms/:id/actions/<name>/invoke { "params": {...} }.
Everything is (scope, key) → value with versions. System scopes start with
_ and are readable by all. Agent scopes (like alice) are private.
Write modes: value (replace), merge (shallow update), increment,
append (log-structured). Gates: if (CEL predicate), if_version (CAS).
Making private state public: Add "public": true when writing:
POST /rooms/my-room/actions/_set_state/invoke
Authorization: Bearer as_alice...
{ "params": { "key": "health", "value": 85, "public": true } }
This auto-creates a view alice.health visible in everyone's context.
Named operations with parameter schemas, CEL preconditions, and write templates. Actions carry the registrar's scope authority: an action registered by Alice can write to Alice's private scope when invoked by Bob.
Features: params, if (CEL gate), enabled (visibility), writes (with
${self}, ${params.x}, ${now} substitution), on_invoke.timer (cooldowns).
CEL expressions that project private state into public values. Views scoped to
an agent can read that agent's private state; the result is visible to everyone
via /context.
Three ways to create views:
- At join:
"views": [{ "id": "my-view", "expr": "..." }] - Auto from state:
"public": trueon private state entries - Via action:
_register_viewbuilt-in
Messages appear in /context with full bodies:
"messages": { "count": 12, "unread": 3, "recent": [ { "seq": 10, "from": "alice", "kind": "chat", "body": "hello" }, { "seq": 11, "from": "bob", "kind": "action_invocation", "body": "heal(...)" } ] }
Use ?messages_after=N for pagination. Reading context marks messages as seen.
Every action invocation (builtin and custom) is logged to the _audit scope
with structured entries: { ts, agent, action, builtin, params, ok }.
Visible in the dashboard Audit tab. Captures failures (scope denials, etc.) too.
| Token | Prefix | Authority |
|---|---|---|
| Room token | room_ | * — admin, all scopes |
| Agent token | as_ | Own scope + granted scopes |
Room token holders can grant scope access:
PATCH /rooms/my-room/agents/alice { "grants": ["_shared"] }
── Lifecycle ──
POST /rooms create room
GET /rooms list rooms (auth required)
GET /rooms/:id room info
POST /rooms/:id/agents join (+ inline state/views)
PATCH /rooms/:id/agents/:id update grants/role (room token)
── Read ──
GET /rooms/:id/context read everything
GET /rooms/:id/wait block until condition, returns context
GET /rooms/:id/poll dashboard bundle
── Write ──
POST /rooms/:id/actions/:id/invoke invoke action (builtin + custom)
── Debug ──
POST /rooms/:id/eval CEL eval
10 endpoints. Every write flows through one endpoint.
Timers: Wall-clock (ms, at) and logical-clock (ticks + tick_on).
Effects: delete (live then vanish) or enable (dormant then appear).
Enabled expressions: "enabled": "state._shared.phase == \"endgame\"" —
resource only exists when expression is true.
Action cooldowns: "on_invoke": { "timer": { "ms": 10000, "effect": "enable" } } —
action goes dormant after invocation, re-enables after timer.
CEL context shape: Every expression sees state._shared.*, state.self.*,
views.*, agents.*, actions.*, messages.count/.unread, self, params.*.
Dashboard: https://sync.parc.land/?room=<ID>#token=<TOKEN> — token stays
in hash fragment, never sent to server. Includes Audit tab for tracing all operations.
- API Reference — all endpoints, request/response shapes
- CEL Reference — expression language, context shape, patterns
- Examples — task queues, turn-based games, private state, grants