UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

116 lines (88 loc) 5.62 kB
# The agent JSONL protocol — argument & response shapes `kestrel agent` is the machine face: it reads one JSON **request envelope** per line on stdin (`{ "op": <name>, ...args }`) and writes one versioned **response object** per line on stdout (`{ "v": "kestrel.agent/v1", "kind": <op>, "value": <payload> }`). Diagnostics — including typed refusals — go to **stderr**; the process exits nonzero on the first refusal. You do not have to reverse-engineer the argument shapes. The protocol is **self-describing**: pipe a single `describe` request in and it emits every op's argument and response shape on the wire. ```sh echo '{"op":"describe"}' | kestrel agent ``` `describe` needs no session. Its `value` is `{ "protocol": "kestrel.agent/v1", "ops": AgentOpSchema[] }`, one entry per op — the exact same catalogue rendered below and by `kestrel agent --help`. The wire copy is generated from the handler's own schema, so it can never drift from what the CLI actually accepts. ## The `{ kind: }` argument objects These are the object shapes an integrator most often has to get right. A string where an object is expected — or the wrong field name — is refused with code `bad-arg`, and the refusal **names** the expected shape. ### `openSession.subject` — a `SessionRef` ```jsonc { "kind": "catalog", "id": "<catalog entry id>" } // a pinned catalog subject { "kind": "dataset", "source": "<plan text>", "dataset": "<dataset ref>" } // a raw dataset (managed backend) ``` A bare string (`"subject": "choppy-1101-strict"`) is **rejected** — `subject` must be an object with a string `kind`. Discover the available catalog ids with the `catalog` op. `openSession` also accepts an optional `document` (a string): the customer strategy (Kestrel plan text) the managed author-no-strategy fence requires for a catalog subject. The LOCAL transport already carries the pinned recipe's strategy and ignores it. ### `advance.response` / `revise.response` — an `AuthoredResponse` ```jsonc { "kind": "authored", "document": "<Kestrel plan text>" } // author at the pending slot { "kind": "pass" } // stand pat — standing Plans keep managing { "kind": "stand-down", "reason": "<why>" } // an explicit, gradable stand-down ``` `document` is the **only** accepted authored-body field name — `text`, `bytes`, `source`, and `content` are all rejected. ### `submit.response` — a `BoundResponse` ```jsonc { "sessionId": "<id>", "ordinal": <n>, "parentHash": "<hash>", "frameRoot": "<hash>", "body": <TurnBody> } ``` The explicit-binding turn: the full pentad header plus a `TurnBody`. Field-level reconciliation against the recorded slot is the Session's job. ### `resume.after` / `resumeOperation.after` — an optional `EventCursor` ```jsonc { "sequence": <n>, "token": "<opaque>" } ``` Omit it to resume from the start. A present-but-malformed cursor is a `bad-arg` refusal (never silently ignored). ## The op set | op | session? | args | emits (`value`) | | --- | --- | --- | --- | | `describe` | no || `{ protocol, ops: AgentOpSchema[] }` | | `catalog` | no || `CatalogEntry[]` | | `validate` | no | `document` | `ValidateOutcome { ok, diagnostics }` | | `openSession` | no | `subject`, `document?` | `{ gated:false, sessionId } \| { gated:true, payment }` | | `start` | yes || `Delivery` | | `advance` | yes | `response` (`AuthoredResponse`) | `SessionResponse` | | `revise` | yes | `response` (`AuthoredResponse`) | `SessionResponse` | | `submit` | yes | `response` (`BoundResponse`) | `SessionResponse` | | `resume` | yes | `after?` (`EventCursor`) | `SessionTranscript` | | `finalize` | yes || `{ blotter, sessionId, tipHash, conformanceRoot, artifacts }` | | `grade` | no | `blotters` (`string[]`) | `Gated<GradeOutcome>` | | `artifact` | no | `ref` | `ArtifactResult { ref, kind, value }` | | `resumeOperation` | no | `operationId`, `after?` | `OperationResumption` | Session verbs (`start` `finalize`) require a prior `openSession` in the same stream; issued first they refuse with code `no-session`. An unknown op refuses with code `unknown-op` and lists the valid ops. ## View addressing — deliberately not on this wire (today) No op or argument shape carries a rendered-View selection or a seat: `openSession` takes a subject and an optional document, not a harness config, and `session/frame` deliveries are the **typed** frame struct — rendering it (and therefore choosing its View) is the client's concern, off the wire. The one View-shaped thing that does travel here is *authored text*: a Wake's `DELIVER <view>` clause inside an `advance`/`revise` document. The role-keyed axis (`AgentConfig.seatViews`, the founder-View opt-in) has no protocol carrier; it is reachable only by programs that construct an `AgentConfig` directly (see the [API reference](./api-reference.md)). This is deliberate fail-closed scope, not an omission — a remote View projection would be a new contract route, and none exists; a client asking for one is refused, not silently given default panes as if it had been honored. ## A minimal LOCAL session ```sh kestrel agent <<'EOF' {"op":"describe"} {"op":"catalog"} {"op":"openSession","subject":{"kind":"catalog","id":"<an id from catalog>"}} {"op":"start"} {"op":"advance","response":{"kind":"pass"}} {"op":"finalize"} EOF ``` No `--api` runs everything in-process (no network); `--api default` (or `KESTREL_API`) routes the same request stream to the managed remote, byte-identical protocol objects across transports.