@contraagent/api 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -2
- package/dist/client.d.ts +54 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +68 -0
- package/dist/client.js.map +1 -0
- package/dist/derive.d.ts +81 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +89 -0
- package/dist/derive.js.map +1 -0
- package/dist/export.d.ts +36 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +130 -0
- package/dist/export.js.map +1 -0
- package/dist/feed.d.ts +27 -0
- package/dist/feed.d.ts.map +1 -0
- package/dist/feed.js +39 -0
- package/dist/feed.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/outcomes.d.ts +117 -0
- package/dist/outcomes.d.ts.map +1 -0
- package/dist/outcomes.js +201 -0
- package/dist/outcomes.js.map +1 -0
- package/dist/panels.d.ts +31 -0
- package/dist/panels.d.ts.map +1 -0
- package/dist/panels.js +12 -0
- package/dist/panels.js.map +1 -0
- package/dist/quota.d.ts +32 -0
- package/dist/quota.d.ts.map +1 -0
- package/dist/quota.js +46 -0
- package/dist/quota.js.map +1 -0
- package/dist/reasons.d.ts +25 -0
- package/dist/reasons.d.ts.map +1 -0
- package/dist/reasons.js +46 -0
- package/dist/reasons.js.map +1 -0
- package/dist/run-state.d.ts +62 -0
- package/dist/run-state.d.ts.map +1 -0
- package/dist/run-state.js +109 -0
- package/dist/run-state.js.map +1 -0
- package/dist/schema.d.ts +3718 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +3 -0
- package/dist/schema.js.map +1 -0
- package/dist/spend.d.ts +46 -0
- package/dist/spend.d.ts.map +1 -0
- package/dist/spend.js +68 -0
- package/dist/spend.js.map +1 -0
- package/dist/store.d.ts +121 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +286 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +112 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -4
- package/src/client.ts +119 -0
- package/src/derive.ts +155 -0
- package/src/export.ts +158 -0
- package/src/feed.ts +81 -0
- package/src/index.ts +12 -0
- package/src/outcomes.ts +267 -0
- package/src/panels.ts +51 -0
- package/src/quota.ts +78 -0
- package/src/reasons.ts +57 -0
- package/src/run-state.ts +166 -0
- package/src/schema.ts +3719 -0
- package/src/spend.ts +108 -0
- package/src/store.ts +338 -0
- package/src/types.ts +165 -0
package/src/panels.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Panel bodies: what a pipeline's own view returns (`monitor.Panel`). A typed body
|
|
2
|
+
// (tiles, a table, charts, blocks of them) is drawn with the monitor's elements;
|
|
3
|
+
// any other JSON object is shown as a JSON tree.
|
|
4
|
+
import type { components } from './schema.js';
|
|
5
|
+
import type { JsonObject, PanelInfo } from './types.js';
|
|
6
|
+
|
|
7
|
+
type S = components['schemas'];
|
|
8
|
+
|
|
9
|
+
export type TilesBody = S['TilesBody'];
|
|
10
|
+
export type PanelTile = S['PanelTile'];
|
|
11
|
+
export type TableBody = S['TableBody'];
|
|
12
|
+
export type TableColumn = S['TableColumn'];
|
|
13
|
+
export type ColumnsBody = S['ColumnsBody'];
|
|
14
|
+
export type LinesBody = S['LinesBody'];
|
|
15
|
+
export type HeatmapBody = S['HeatmapBody'];
|
|
16
|
+
export type HBarsBody = S['HBarsBody'];
|
|
17
|
+
export type HBar = S['HBar'];
|
|
18
|
+
export type StatusGridBody = S['StatusGridBody'];
|
|
19
|
+
export type GridCell = S['GridCell'];
|
|
20
|
+
export type NoteBody = S['NoteBody'];
|
|
21
|
+
export type BlocksBody = S['BlocksBody'];
|
|
22
|
+
export type ChartSeries = S['ChartSeries'];
|
|
23
|
+
/** How a number of a body is written: `pct` is a fraction (0.97 is 97%), `duration` seconds, `datetime` an ISO time. */
|
|
24
|
+
export type ValueFormat = NonNullable<TableColumn['format']>;
|
|
25
|
+
|
|
26
|
+
/** A body with a `type`: drawn with the monitor's own elements. */
|
|
27
|
+
export type TypedPanelBody =
|
|
28
|
+
| TilesBody
|
|
29
|
+
| TableBody
|
|
30
|
+
| ColumnsBody
|
|
31
|
+
| LinesBody
|
|
32
|
+
| HeatmapBody
|
|
33
|
+
| HBarsBody
|
|
34
|
+
| StatusGridBody
|
|
35
|
+
| NoteBody
|
|
36
|
+
| BlocksBody;
|
|
37
|
+
/** What a panel returns: a typed body, or any JSON object. */
|
|
38
|
+
export type PanelBody = TypedPanelBody | JsonObject;
|
|
39
|
+
|
|
40
|
+
/** The `type` of each typed body. */
|
|
41
|
+
export const PANEL_TYPES = ['tiles', 'table', 'columns', 'lines', 'heatmap', 'hbars', 'status_grid', 'note', 'blocks'] as const;
|
|
42
|
+
|
|
43
|
+
/** `body` as a typed body, or null when it is any other JSON object (a JSON tree). */
|
|
44
|
+
export function typedBody(body: unknown): TypedPanelBody | null {
|
|
45
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return null;
|
|
46
|
+
const type = (body as { type?: unknown }).type;
|
|
47
|
+
return typeof type === 'string' && (PANEL_TYPES as readonly string[]).includes(type) ? (body as TypedPanelBody) : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A panel computed per run: a tab of each run, sent on its live feed (a trends panel is not). */
|
|
51
|
+
export const isRunPanel = (p: Pick<PanelInfo, 'scope'>): boolean => (p.scope ?? 'run') === 'run';
|
package/src/quota.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// A subscription's utilization as the coding agent reported it. Claude Code
|
|
2
|
+
// sends a reading when a session starts or resumes, never when it ends; every
|
|
3
|
+
// reading is account-wide (parallel tasks and anything else on the account
|
|
4
|
+
// share it). So a try has readings at its session starts, and the closest
|
|
5
|
+
// reading after it ended is whatever session started next. Pure.
|
|
6
|
+
import type { JournalRow, QuotaReading } from './types.js';
|
|
7
|
+
|
|
8
|
+
/** A reading, and the row (try) it came with. */
|
|
9
|
+
export interface SourcedReading {
|
|
10
|
+
reading: QuotaReading;
|
|
11
|
+
key: string;
|
|
12
|
+
attempt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const at = (r: QuotaReading): number => Date.parse(r.observed_at ?? '');
|
|
16
|
+
|
|
17
|
+
const usable = (r: QuotaReading): boolean =>
|
|
18
|
+
!Number.isNaN(at(r)) && (r.five_hour_pct != null || r.seven_day_pct != null);
|
|
19
|
+
|
|
20
|
+
/** Every reading of the run, oldest first. */
|
|
21
|
+
export function runReadings(journal: readonly JournalRow[]): SourcedReading[] {
|
|
22
|
+
const out: SourcedReading[] = [];
|
|
23
|
+
for (const row of journal) {
|
|
24
|
+
for (const reading of row.quota ?? []) {
|
|
25
|
+
if (usable(reading)) out.push({ reading, key: row.key, attempt: row.attempt ?? 0 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out.sort((a, b) => at(a.reading) - at(b.reading));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One account's utilization over the run: the credential that reported it, and the account (`account`, a digest; '' when not known). */
|
|
32
|
+
export interface AccountUsage {
|
|
33
|
+
credential: string;
|
|
34
|
+
account?: string;
|
|
35
|
+
first: QuotaReading;
|
|
36
|
+
last: QuotaReading;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The account a reading is of: its credential and its account digest. `ambient` on two coding agents is two accounts. */
|
|
40
|
+
export const accountKey = (r: { credential?: string | null; account?: string | null }): string => `${r.credential ?? ''}\u0000${r.account ?? ''}`;
|
|
41
|
+
|
|
42
|
+
/** Per account, the run's first and last reading; the account with the highest weekly use first. */
|
|
43
|
+
export function runAccounts(journal: readonly JournalRow[]): AccountUsage[] {
|
|
44
|
+
const by = new Map<string, QuotaReading[]>();
|
|
45
|
+
for (const { reading } of runReadings(journal)) {
|
|
46
|
+
const k = accountKey(reading);
|
|
47
|
+
let list = by.get(k);
|
|
48
|
+
if (!list) by.set(k, (list = []));
|
|
49
|
+
list.push(reading);
|
|
50
|
+
}
|
|
51
|
+
const out: AccountUsage[] = [];
|
|
52
|
+
for (const list of by.values()) {
|
|
53
|
+
const first = list[0];
|
|
54
|
+
const last = list[list.length - 1];
|
|
55
|
+
if (first && last) out.push({ credential: last.credential ?? '', account: last.account ?? '', first, last });
|
|
56
|
+
}
|
|
57
|
+
return out.sort((a, b) => (b.last.seven_day_pct ?? 0) - (a.last.seven_day_pct ?? 0));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TryQuota {
|
|
61
|
+
/** The try's first reading: usage when its (first) session started. */
|
|
62
|
+
start: QuotaReading | null;
|
|
63
|
+
/** The first reading of the same account received after the try ended (`row.ts`), if any. */
|
|
64
|
+
next: QuotaReading | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One try's readings: at its start, and the next one observed after it ended. */
|
|
68
|
+
export function tryQuota(row: JournalRow, readings: readonly SourcedReading[]): TryQuota {
|
|
69
|
+
const own = (row.quota ?? []).filter(usable).sort((a, b) => at(a) - at(b));
|
|
70
|
+
const ended = Date.parse(row.ts);
|
|
71
|
+
const start = own[0] ?? null;
|
|
72
|
+
const account = start === null ? '' : accountKey(start);
|
|
73
|
+
const next =
|
|
74
|
+
start === null || Number.isNaN(ended)
|
|
75
|
+
? undefined
|
|
76
|
+
: readings.find((r) => at(r.reading) > ended && accountKey(r.reading) === account);
|
|
77
|
+
return { start, next: next?.reading ?? null };
|
|
78
|
+
}
|
package/src/reasons.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// What a journal row's `reason` says, read as data: the inputs a refusal names,
|
|
2
|
+
// and paths cut to the run directory. Pure; the words for them are the UI's.
|
|
3
|
+
|
|
4
|
+
/** One input a task was refused for, read from its `invalid input: ...` reason. */
|
|
5
|
+
export interface InputProblem {
|
|
6
|
+
/** The file, as the reason gives it (absolute). */
|
|
7
|
+
path: string;
|
|
8
|
+
/** The key of the task that writes it, when the reason names one. */
|
|
9
|
+
producer: string;
|
|
10
|
+
/**
|
|
11
|
+
* `missing`: the file is not there; `checks`: it is there and failed its checks;
|
|
12
|
+
* `failed`: it is there, written by a task that did not succeed (`producer`).
|
|
13
|
+
*/
|
|
14
|
+
problem: 'missing' | 'checks' | 'failed';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The inputs a refused task's reason names (`invalid input: input from <key> is
|
|
19
|
+
* missing: <path>`, `invalid input: input from <key> ended <stop>: <path>`,
|
|
20
|
+
* `invalid input: <path>: required input is missing`, `invalid input: <path>:
|
|
21
|
+
* <check error>`), in order; [] for any other reason.
|
|
22
|
+
*/
|
|
23
|
+
export function inputProblems(reason: string | null | undefined): InputProblem[] {
|
|
24
|
+
const m = /^invalid input:\s*(.*)$/s.exec(reason || '');
|
|
25
|
+
if (!m) return [];
|
|
26
|
+
const out: InputProblem[] = [];
|
|
27
|
+
for (const part of (m[1] ?? '').split('; ')) {
|
|
28
|
+
let x = /^input from (\S+) is missing: (\S+)/.exec(part);
|
|
29
|
+
if (x) {
|
|
30
|
+
out.push({ path: x[2] ?? '', producer: x[1] ?? '', problem: 'missing' });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
x = /^input from (\S+) ended \S+: (\S+)/.exec(part);
|
|
34
|
+
if (x) {
|
|
35
|
+
out.push({ path: x[2] ?? '', producer: x[1] ?? '', problem: 'failed' });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
x = /^(\S+?): required input is missing/.exec(part);
|
|
39
|
+
if (x) {
|
|
40
|
+
out.push({ path: x[1] ?? '', producer: '', problem: 'missing' });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
x = /^(\S+?): /.exec(part);
|
|
44
|
+
if (x && !out.some((o) => o.path === x?.[1])) out.push({ path: x[1] ?? '', producer: '', problem: 'checks' });
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A text from the run (a failure reason) with absolute paths inside the run
|
|
51
|
+
* directory cut to the part after `runs/<run>/`: `out/summarize/zeta.json`.
|
|
52
|
+
*/
|
|
53
|
+
export function runRelative(text: string | null | undefined, run: string): string {
|
|
54
|
+
if (!text || !run) return text ?? '';
|
|
55
|
+
const esc = run.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
56
|
+
return text.replace(new RegExp(`(?:/[^\\s:'"]*)?/${esc}/`, 'g'), '');
|
|
57
|
+
}
|
package/src/run-state.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// What the live feed of one run folds into, and the questions the views ask of it.
|
|
2
|
+
// Pure: no I/O, no clock unless passed in.
|
|
3
|
+
import { runActivity } from './derive.js';
|
|
4
|
+
import type {
|
|
5
|
+
CursorView,
|
|
6
|
+
JournalRow,
|
|
7
|
+
JsonObject,
|
|
8
|
+
Outputs,
|
|
9
|
+
Plan,
|
|
10
|
+
Procs,
|
|
11
|
+
RunFeedMessage,
|
|
12
|
+
RunSummary,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
/** The newest rows kept; older ones drop off the front. */
|
|
16
|
+
export const JOURNAL_CAP = 4000;
|
|
17
|
+
|
|
18
|
+
export interface RunState {
|
|
19
|
+
/** Finished attempts in arrival order, at most `JOURNAL_CAP`. */
|
|
20
|
+
journal: JournalRow[];
|
|
21
|
+
/** Null until the first `procs` message. */
|
|
22
|
+
procs: Procs | null;
|
|
23
|
+
plan: Plan;
|
|
24
|
+
outputs: Outputs;
|
|
25
|
+
/** Null for a run that keeps no cursor. */
|
|
26
|
+
cursor: CursorView | null;
|
|
27
|
+
panels: Record<string, JsonObject>;
|
|
28
|
+
/** The feed's replay is over (`ready`): a journal row after this is new. */
|
|
29
|
+
ready: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function emptyRunState(): RunState {
|
|
33
|
+
return {
|
|
34
|
+
journal: [],
|
|
35
|
+
procs: null,
|
|
36
|
+
plan: { nodes: [], groups: [], flow_calls: [] },
|
|
37
|
+
outputs: { contracts: [], tally: { valid: 0, invalid: 0, refused: 0, pending: 0, failed: 0, running: 0 } },
|
|
38
|
+
cursor: null,
|
|
39
|
+
panels: {},
|
|
40
|
+
ready: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A feed message, or the feed (re)connecting: the server then replays the journal from the start. */
|
|
45
|
+
export type RunAction = RunFeedMessage | { event: 'connected' };
|
|
46
|
+
|
|
47
|
+
/** Fold a batch of actions into the state. The journal is copied once per batch, not per row. */
|
|
48
|
+
export function reduceRun(state: RunState, actions: readonly RunAction[]): RunState {
|
|
49
|
+
let next = state;
|
|
50
|
+
let rows: JournalRow[] | null = null;
|
|
51
|
+
const flush = () => {
|
|
52
|
+
if (!rows) return;
|
|
53
|
+
const all = next.journal.concat(rows);
|
|
54
|
+
next = { ...next, journal: all.length > JOURNAL_CAP ? all.slice(all.length - JOURNAL_CAP) : all };
|
|
55
|
+
rows = null;
|
|
56
|
+
};
|
|
57
|
+
for (const a of actions) {
|
|
58
|
+
if (a.event === 'journal') {
|
|
59
|
+
(rows ??= []).push(a.data);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
flush();
|
|
63
|
+
switch (a.event) {
|
|
64
|
+
case 'connected':
|
|
65
|
+
next = { ...next, journal: [], ready: false };
|
|
66
|
+
break;
|
|
67
|
+
case 'ready':
|
|
68
|
+
next = { ...next, ready: true };
|
|
69
|
+
break;
|
|
70
|
+
case 'procs':
|
|
71
|
+
next = { ...next, procs: a.data };
|
|
72
|
+
break;
|
|
73
|
+
case 'plan':
|
|
74
|
+
next = { ...next, plan: a.data };
|
|
75
|
+
break;
|
|
76
|
+
case 'outputs':
|
|
77
|
+
next = { ...next, outputs: a.data };
|
|
78
|
+
break;
|
|
79
|
+
case 'cursor':
|
|
80
|
+
next = { ...next, cursor: a.data };
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
next = { ...next, panels: { ...next.panels, [a.event.slice('panel:'.length)]: a.data } };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
flush();
|
|
87
|
+
return next;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- selectors ----------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/** A finished attempt that neither completed, nor was partial (ok, its output a placeholder), nor was skipped. */
|
|
93
|
+
export const isBad = (row: { stop?: string | undefined }): boolean =>
|
|
94
|
+
!!row.stop && row.stop !== 'completed' && row.stop !== 'partial' && row.stop !== 'skipped';
|
|
95
|
+
|
|
96
|
+
/** A Python task (the journal, plan and contracts say who executed it). */
|
|
97
|
+
export const isPython = (x: { executor?: string | undefined }): boolean => x.executor === 'python';
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Which start of the run each of one task's journal rows (in journal order) belongs
|
|
101
|
+
* to, 0 for the first start that ran it: a resumed run numbers a task's attempts
|
|
102
|
+
* from 0 again, so a row whose attempt is not above the one before it begins the
|
|
103
|
+
* next start that ran the task.
|
|
104
|
+
*/
|
|
105
|
+
export function runStarts(rows: readonly { attempt?: number | null | undefined }[]): number[] {
|
|
106
|
+
let start = 0;
|
|
107
|
+
return rows.map((r, i) => {
|
|
108
|
+
const before = rows[i - 1];
|
|
109
|
+
if (before && (r.attempt ?? 0) <= (before.attempt ?? 0)) start++;
|
|
110
|
+
return start;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A stable identity for a journal row. */
|
|
115
|
+
export const rowId = (row: JournalRow): string => `${row.ts}|${row.key}`;
|
|
116
|
+
|
|
117
|
+
export const runRow = (runs: readonly RunSummary[], run: string): RunSummary | undefined =>
|
|
118
|
+
runs.find((r) => r.run === run);
|
|
119
|
+
|
|
120
|
+
/** The run's process is alive (by the feed, or by the run list): `runActivity` is `running`. */
|
|
121
|
+
export const isRunLive = (state: RunState, summary: RunSummary | undefined): boolean =>
|
|
122
|
+
runActivity(summary, !!state.procs?.alive) === 'running';
|
|
123
|
+
|
|
124
|
+
/** Something running right now: an agent launch or a Python task. */
|
|
125
|
+
export interface Activity {
|
|
126
|
+
/** `llm` for an agent launch, `py` for a Python task. */
|
|
127
|
+
cls: 'llm' | 'py';
|
|
128
|
+
/** Stable across updates: the task key, prefixed by kind of activity. */
|
|
129
|
+
id: string;
|
|
130
|
+
kind: string;
|
|
131
|
+
/** The unit; '' for a task of the whole run. */
|
|
132
|
+
unit: string;
|
|
133
|
+
detail: string;
|
|
134
|
+
/** Seconds it has run, as of the procs message. */
|
|
135
|
+
ageS: number;
|
|
136
|
+
/** The task key, for an agent launch (it has a transcript). */
|
|
137
|
+
key: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function liveActivities(procs: Procs | null): Activity[] {
|
|
141
|
+
if (!procs) return [];
|
|
142
|
+
return [
|
|
143
|
+
...procs.launches.map(
|
|
144
|
+
(c): Activity => ({
|
|
145
|
+
cls: 'llm',
|
|
146
|
+
id: `c:${c.key}`,
|
|
147
|
+
kind: c.kind || 'task',
|
|
148
|
+
unit: c.unit || '',
|
|
149
|
+
detail: [c.unit, c.model || c.role].filter(Boolean).join(' | '),
|
|
150
|
+
ageS: c.age_s,
|
|
151
|
+
key: c.key,
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
154
|
+
...procs.python_tasks.map(
|
|
155
|
+
(p): Activity => ({
|
|
156
|
+
cls: 'py',
|
|
157
|
+
id: `p:${p.key}`,
|
|
158
|
+
kind: p.kind || 'task',
|
|
159
|
+
unit: p.unit || '',
|
|
160
|
+
detail: p.unit || 'Python',
|
|
161
|
+
ageS: p.age_s,
|
|
162
|
+
key: '',
|
|
163
|
+
}),
|
|
164
|
+
),
|
|
165
|
+
];
|
|
166
|
+
}
|