@contraagent/api
A typed client for the contraagent monitor’s HTTP API: types generated from the
monitor’s OpenAPI document, a REST client, the two live feeds, and a store that
folds a run’s feed into one typed state. No UI: @contraagent/ui draws it.
The types are generated from the monitor’s pydantic models via OpenAPI, so a field the server removes fails your type check, not your page.
A ::code line quotes a file of the package; the documentation site shows the quoted lines in place. The same declarations, with their comments, are in dist/**/*.d.ts.
Getting it
Section titled “Getting it”Install the package from the npm registry:
npm install @contraagent/apiA checkout of the repository packs its own version (Use it from a
checkout). Every monitor also serves the client, the feeds and the store as
exports of its /app.js (@contraagent/ui’s documentation).
Use it from a checkout
Section titled “Use it from a checkout”In the repository’s web/ (Node 22, pnpm through corepack enable):
pnpm install && pnpm tarballspnpm tarballs builds the packages and writes one .tgz file for each to
web/tarballs/. Then, in your project, with npm or pnpm:
npm install <checkout>/web/tarballs/contraagent-api-*.tgzpnpm add <checkout>/web/tarballs/contraagent-api-*.tgzEvery response and feed message has a type, with readable aliases:
JournalRow, RunSummary, Plan, Outputs, TaskDetail, StreamPage,
TranscriptEvent, and so on. Fields the server may leave out are optional.
Closed vocabularies are unions: Stop (how an attempt ended, JournalRow['stop']),
TaskState, ContractStatus.
The live feeds are discriminated unions on the SSE event name, so a switch on
msg.event narrows msg.data:
export function reduceRun(state: RunState, actions: readonly RunAction[]): RunState { let next = state; let rows: JournalRow[] | null = null; const flush = () => { if (!rows) return; const all = next.journal.concat(rows); next = { ...next, journal: all.length > JOURNAL_CAP ? all.slice(all.length - JOURNAL_CAP) : all }; rows = null; }; for (const a of actions) { if (a.event === 'journal') { (rows ??= []).push(a.data); continue; } flush(); switch (a.event) { case 'connected': next = { ...next, journal: [], ready: false }; break; case 'ready': next = { ...next, ready: true }; break; case 'procs': next = { ...next, procs: a.data }; break; case 'plan': next = { ...next, plan: a.data }; break; case 'outputs': next = { ...next, outputs: a.data }; break; case 'cursor': next = { ...next, cursor: a.data }; break; default: next = { ...next, panels: { ...next.panels, [a.event.slice('panel:'.length)]: a.data } }; } } flush(); return next;}The client
Section titled “The client”createMonitorClient({ baseUrl }) has one typed method per JSON endpoint
(src/client.ts); each throws ApiError when it gets no JSON answer: its
failure is network, status or not-json (such as an HTML page from a
server without the API), with the url, status and media type.
The live feeds
Section titled “The live feeds”openRunFeed(run, panels, handlers): the run, live. On every (re)connect the server replays the whole journal and then sendsready; every message afterreadyis new.openStreamFeed(run, key, handlers): one transcript, tailed: the tail page first, then only what is new.
Each returns a function that closes it.
An export
Section titled “An export”An export (contraagent monitor export) is the monitor’s views of some runs,
written as files and read with no server. createExportClient(url) is a client of
its data/ directory: the same methods as the server’s client, each read from its
file. openExportFeed(client) is the run feed for it: it plays a run’s files once
and ends with ready. Give both to MonitorStore (client and openRunFeed) to
read an export as a server is read.
The store
Section titled “The store”MonitorStore keeps the run list fresh, opens the selected run’s feed, folds
its messages into one RunState, and tells subscribers. It knows nothing of
any UI framework: a Lit controller, a React hook or plain code subscribe alike.
Its snapshot’s error is the ApiError of the last request for the title or
the run list that got no answer, and
runsLoaded says the run list has been answered.
The reducer behind it, reduceRun(state, actions), is pure: use it with your
own state management if you prefer. @contraagent/ui’s documentation shows
the store feeding its elements.
Tasks and their outcomes
Section titled “Tasks and their outcomes”A task is one task key. deriveRun(journal, plan) gives what every view counts
and lists about the run’s tasks, computed once per state (the same journal and
plan objects give the same result):
outcomesandrows: every task of the run, the plan joined with the journal, with its outcome (done,recovered,partial,failed,not_started,running,waiting,skipped;partial: ok, its output a placeholder, neither done nor failed);counts: tasks by outcome,doneTotal(done and recovered: “N of M tasks done”) andfinished(done, recovered, partial and skipped);problems: failed tasks with the tasks they stopped, tasks not started for a reason of their own, and recovered tasks;kindsandtries.
latestTask(rows) is the task a page opens by default. A refusal before launch
(invalid_input) is not a try: a task refused and then done is done, not
recovered.