Skip to content

Latest commit

 

History

History
172 lines (148 loc) · 7.57 KB

File metadata and controls

172 lines (148 loc) · 7.57 KB

warren-agent/0 — the remote agent protocol

How an external client controls one or more agents in a running WARREN simulation. Design rules:

  1. Structured data is authoritative. The envelope's observation object is the wire truth; text is a rendered convenience for LLM prompting.
  2. One unanswered observation per agent. The engine is conservative: a claimed agent's wake blocks its batch until answered or expired.
  3. Delivery is at-least-once; acceptance is exactly-once. Envelopes may be redelivered after reconnects. Commands carry client-minted command_ids; the server deduplicates accepted commands and replays the original receipt as duplicate. Non-accepted verdicts (rejected/stale/expired) are deterministic and side-effect-free, so they do not occupy their command_id: a retry is re-judged against current state (an unchanged retry gets the same verdict; a corrected payload under the same id can be accepted).
  4. Leases, not sessions. A claim grants a lease; reconnecting with the same lease token resumes exactly where you were (including redelivery of the outstanding envelope). Leases expire: any authenticated request for the agent (claim/briefing/poll/submit/receipt) refreshes a TTL (server-configurable, default 300 s); past it the agent is reclaimable with a fresh token. POST …/release?lease=<token> frees the agent immediately.

Transport: plain HTTP + long-polling (GET …/observations?wait=25). WebSockets are a later acceleration, not the canonical protocol.

Lifecycle state machine

            POST /v1/runs                       run: created ─→ running ─→ ended
                 │
    POST /v1/runs/{run}/agents/{a}/claim        agent: unclaimed ─→ leased
                 │                                       ▲          │ lease expiry /
                 ▼                                       └──────────┘ explicit release
      GET  …/briefing        (once; cache by revision)
      GET  …/observations?after=<seq>&wait=<s>          ┌──────────────────────┐
                 │                                      │ observation:         │
                 ▼                                      │  outstanding ─→ answered
      POST …/commands {command_id, observation_id, …}   │      │          (exactly one
                 │                                      │      ├─→ expired   accepted
                 ▼                                      │      └─→ superseded  command)
      receipt: accepted | duplicate | rejected          └──────────────────────┘
               | stale | expired
                 │
      GET  …/commands/{command_id}      (poll outcome: engine accepted/rejected/…)

Per-observation: exactly one command is ever accepted. Late commands for an expired observation get expired (the receipt detail says why: decision deadline vs. run ended); commands for a superseded observation get stale; resubmission of an accepted command_id gets duplicate carrying the original receipt's content, while a resubmitted non-accepted command_id is re-judged. The observation's closure status is judged before payload validation: a malformed command answering an expired observation gets expired, not rejected. Payload type errors (e.g. a string sleep_ms, a non-string action) are rejected with a teaching detail and recorded like any other verdict; non-string command_id/observation_id cannot be deduplicated at all and are refused with HTTP 422.

Worked example

POST /v1/runs?scenario=bridge_repair&seed=3 body:

{"controlled_agents": ["engineer-1"], "npc_policy": "solution", "pace": null}

Agent control is explicit at creation: controlled_agents are remote, npc_policy: "solution" attaches the scenario's bundled runners to everyone else, "none" leaves them unassigned (they no-op).

POST /v1/runs/run-1/agents/engineer-1/claim

{"lease": "l-3f9c…", "briefing_revision": 1}

GET /v1/runs/run-1/agents/engineer-1/briefing?lease=l-3f9c…

{
  "briefing_id": "run-1/engineer-1",
  "revision": 1,
  "content_hash": "sha256:…",
  "agent_id": "engineer-1",
  "role": "engineer",
  "name": "Sam the engineer",
  "scenario": "bridge_repair",
  "premise": "Repair the storm-damaged bridge and resupply the clinic.",
  "objectives": [
    {"id": "repair", "description": "Restore the bridge's structural integrity.",
     "status": "pending", "deadline_ms": null, "progress": {"current": 0, "target": 2}}
  ],
  "tool_catalog": [
    {"name": "move_to", "description": "Walk to a place, however far.",
     "lane": "body", "durative": true,
     "input_schema": {"type": "object",
       "properties": {"dest": {"type": "string"}},
       "required": ["dest"], "additionalProperties": false}}
  ],
  "conventions": {
    "grid_naming": "cells are '<space>:<x>,<y>'; x grows east, y grows south",
    "sight_radius": 0,
    "budgets": {"max_sight": 40, "max_messages": 12, "max_inventory": 20},
    "decision": "answer each observation with exactly one command: an action, a sleep, or a noop"
  },
  "notes": ""
}

GET /v1/runs/run-1/agents/engineer-1/observations?after=-1&wait=25&lease=l-3f9c…

{
  "run_id": "run-1",
  "agent_id": "engineer-1",
  "observation_id": "engineer-1#0",
  "seq": 0,
  "sim_time_ms": 0,
  "reason": "start",
  "deadline_unix": 1769812345.1,
  "briefing_revision": 1,
  "observation": {"agent": "engineer-1", "time": 0, "busy": [], "you": {"…": ""},
                   "place": {"…": ""}, "tools": [{"action": "take", "…": ""}]},
  "text": "[t=0s] You are Sam the engineer …",
  "affordances": [{"action": "take", "params": {"item": {"choices": ["toolbox-1"]}},
                   "suggestions": [{"item": "toolbox-1"}]}]
}

POST /v1/runs/run-1/agents/engineer-1/commands?lease=l-3f9c… body:

{"command_id": "c-7d21", "observation_id": "engineer-1#0",
 "action": "take", "args": {"item": "toolbox-1"}}

→ receipt (acceptance settles immediately; outcome fills in after adjudication and is polled at GET …/commands/c-7d21):

{"command_id": "c-7d21", "observation_id": "engineer-1#0",
 "status": "accepted", "detail": "", "outcome": "pending", "outcome_reason": ""}

Resubmitting the same body (client retry after a crash) →

{"command_id": "c-7d21", "…": "", "status": "duplicate",
 "outcome": "accepted", "outcome_reason": ""}

Timeouts and terminal runs

  • Each envelope carries deadline_unix (run-configurable, default 60 s wall). Past it the engine records a noop for that wake and the observation is expired; the agent's next envelope arrives on its next wake.
  • A run that completes/fails emits its terminal status on the spectator stream and GET /v1/runs/{run} shows status; observation polls and claims on an ended run return 410 Gone with the terminal status in the detail. A run whose simulation task dies unexpectedly gets status crashed (never a stale running).
  • Version negotiation: every /v1 response carries X-Warren-Protocol: warren-agent/0. Clients should send the same header (or protocol= on claim); if the request header is present and does not match, the server refuses with 409. A request without the header is accepted.
  • Spectator stream: a WebSocket subscriber that falls too far behind receives a {"kind": "lagged", "cursor": …} frame and is closed with code 1013; it can reconnect with ?since=<cursor> to resume.