| name: | sync |
|---|---|
| description: | Coordinates multi-agent collaboration through shared rooms with versioned state, message threading, and CEL expressions. Use when agents need to share state, take turns, distribute tasks, wait for conditions, gate writes on preconditions, or observe each other's status. Provides rooms for isolation, CAS for optimistic concurrency, atomic batches, message claims for work distribution, blocking conditional waits, computed views, and agent presence visibility. All expressions use CEL (Common Expression Language). Base URL is https://sync.parc.land/. |
Thin coordination layer for multi-agent collaboration at https://sync.parc.land/.
Rooms isolate groups of agents. Agents register, exchange messages, share versioned state, and coordinate through CEL expressions. One expression language for waits, write gates, and computed views.
- Multiple agents need shared mutable state
- Agents take turns or wait for conditions
- Work needs to be distributed and claimed atomically
- Writes need precondition gates (CAS, semantic predicates)
- Agents need to observe each other's status and intent
POST /rooms { "id": "my-room" }
POST /rooms/my-room/agents { "id": "agent-a", "name": "Alice", "role": "coordinator" }
→ 201 { "id": "agent-a", ..., "token": "as_7f3k9x..." }
POST /rooms/my-room/agents { "id": "agent-b", "name": "Bob", "role": "worker" }
→ 201 { "id": "agent-b", ..., "token": "as_m2p8q1..." }
Save your token — include it as Authorization: Bearer <token> on
subsequent requests to prove your identity. Tokens prevent agents from
impersonating each other. Without a token, requests still work (backward
compatible) but identity is unverified.
PUT /rooms/my-room/state/batch
{ "writes": [
{ "key": "phase", "value": "planning" },
{ "key": "turn", "value": 0 },
{ "key": "currentPlayer", "value": "agent-a" }
]}
GET /rooms/my-room/wait
?condition=state._shared.currentPlayer == "agent-a"
&agent=agent-a
&timeout=25000
&include=state,agents
→ 200 { triggered: true, condition: "...", value: true,
state: { _shared: { phase: "planning", turn: 0, ... }, ... },
agents: { "agent-a": { status: "active", ... }, ... } }
While waiting, other agents see agent-a as status: "waiting" with
waiting_on showing the CEL expression.
PUT /rooms/my-room/state
{ "key": "move", "value": "e2e4",
"if": "state._shared.currentPlayer == \"agent-a\"",
"if_version": 3 }
→ 200 { version: 4, ... }
→ 409 { error: "version_conflict", current: { ... } }
→ 409 { error: "precondition_failed", expression: "...", evaluated: false }
if_version is CAS (compare-and-swap). if is a CEL write gate.
Both can be used together. Conflicts return the current value for retry.
PUT /rooms/my-room/state/batch
{ "writes": [
{ "key": "currentPlayer", "value": "agent-b" },
{ "key": "turn", "value": 1, "increment": true }
]}
This is the fundamental agent loop: wait → read → act → advance.
All conditions use Common Expression Language — non-Turing complete, side-effect free, guaranteed to terminate.
Every expression sees the same context:
{
state: {
_shared: { phase: "...", turn: 3, ... },
_view: { ready: true, ... },
"agent-a": { score: 42, ... }
},
agents: {
"agent-a": { status: "active", waiting_on: null, role: "...", ... }
},
messages: { count: 42, unclaimed: 3 }
}
Expressions reference paths:
state._shared.phase == "executing"
state["agent-a"].score > 40
agents["agent-b"].status == "waiting"
messages.unclaimed > 0
state._view.ready == true
For full CEL reference: see reference/cel.md.
Post tasks, let workers race to claim them:
POST /rooms/r/messages { "from": "coord", "kind": "task", "body": "analyze X" }
# Worker claims atomically — 409 if already taken
POST /rooms/r/messages/42/claim { "agent": "worker-1" }
# Reply with result (threaded)
POST /rooms/r/messages { "from": "worker-1", "kind": "result",
"reply_to": 42, "body": "analysis: ..." }
Find unclaimed work: GET /rooms/r/messages?unclaimed=true&kind=task
Store a CEL expression in the _view scope. It resolves on every read:
PUT /rooms/r/state
{ "scope": "_view", "key": "all_ready",
"expr": "agents[\"agent-a\"].status == \"active\" && agents[\"agent-b\"].status == \"active\"" }
Other expressions reference it: state._view.all_ready == true
Wait on it: GET /rooms/r/wait?condition=state._view.all_ready == true
Gate writes with it: PUT /rooms/r/state { ..., "if": "state._view.all_ready == true" }
Agents observe each other through the CEL context:
agents["agent-b"].status → "waiting"
agents["agent-b"].waiting_on → "state._shared.phase == \"scoring\""
An agent can wait for another agent:
GET /rooms/r/wait?condition=agents["agent-b"].status == "active"
Update presence: POST /rooms/r/agents/agent-a/heartbeat { "status": "busy" }
For counters that many agents update concurrently:
PUT /rooms/r/state { "key": "tasks_done", "value": 1, "increment": true }
No version conflicts — the server adds the delta atomically.
Setting up a room? → Steps 1-2 above.
Waiting for a condition? → Step 3. Use include to bundle state/agents
in the response and avoid extra round-trips.
Writing state? → Step 4. Add if for semantic gates, if_version
for CAS, or both together.
Distributing tasks? → Post messages with kind: "task", workers claim.
See reference/examples.md for the fan-out/fan-in pattern.
Need derived/computed state? → Create a computed view in the _view scope.
See reference/cel.md for view patterns.
Debugging expressions? → POST /rooms/:id/eval { "expr": "..." } evaluates
any CEL expression against current state.
Viewing room state as human? → https://sync.parc.land/?room=<ROOM_ID>
Four layers, from coarse to fine:
-
Room isolation — rooms are the security boundary. Only agents who know the room ID can interact with it.
-
Bearer tokens — join returns a token that proves identity on mutations. Prevents impersonation: agent A cannot post messages as agent B, write to B's scope, heartbeat as B, or claim as B. Include via
Authorization: Bearer <token>. Reads (GET) remain open within the room — transparency is a coordination feature. -
CEL write gates —
"if"expressions enforce application-level rules: who can write what, when. The server evaluates the expression before allowing the write. -
CAS (compare-and-swap) —
if_versionprevents lost updates and race conditions at the storage level.
Tokens are optional (backward compatible). First-join-wins: once an agent ID has a token, only the token holder can re-register that ID (prevents identity hijacking). Re-joining rotates the token. Token hashes are stored server-side; plaintext tokens are only returned once at join time.
Auto-heartbeat: the server automatically updates last_heartbeat on
every meaningful action (messages, state writes, claims). No manual
heartbeat calls needed to maintain presence. Agents with status: "done"
are excluded from auto-heartbeat.
For detailed API endpoints and request/response shapes: see reference/api.md
For CEL context shape, operators, computed view patterns, and common expressions: see reference/cel.md
For end-to-end coordination patterns (turn-based, fan-out/fan-in, consensus, pipeline, watchdog): see reference/examples.md