@coltonkirsten/a12s 0.3.0 → 0.4.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.
@@ -0,0 +1,306 @@
1
+ // Runs in Node, Deno, Bun and browsers: no node: imports here (`npm run check:browser` enforces it).
2
+ import { A12sClient, A12sError, TurnStream, } from "./client.js";
3
+ const enc = encodeURIComponent;
4
+ const maxReplaceTries = 5;
5
+ /**
6
+ * A running Fleet. Its agent computers, Services and tokens are in {@link agentComputers}, {@link services} and
7
+ * {@link tokens}.
8
+ *
9
+ * ```ts
10
+ * const fleet = await Fleet.connect();
11
+ * const hello = await fleet.agentComputers.create("hello", { instructions: "Be helpful" });
12
+ * await hello.prompt("hi");
13
+ * await hello.remove();
14
+ * ```
15
+ */
16
+ export class Fleet {
17
+ /** The lower-level client the handles use, for API calls that have no method here. */
18
+ client;
19
+ agentComputers;
20
+ services;
21
+ tokens;
22
+ credentials;
23
+ /** No discovery: `url` and `token` as given, by default the page's own origin. */
24
+ constructor(opts = {}) {
25
+ this.client = new A12sClient(opts);
26
+ this.agentComputers = new AgentComputers(this.client);
27
+ this.services = new Services(this.client);
28
+ this.tokens = new Tokens(this.client);
29
+ this.credentials = new Credentials(this.client);
30
+ }
31
+ /**
32
+ * In a browser: the Fleet behind the page's own origin, which is `a12s gateway` serving the page (no token
33
+ * needed). In Node: found like {@link connect} finds it. Pass `url` and `token` to reach anything else.
34
+ */
35
+ static async connect(opts = {}) {
36
+ return new Fleet(opts);
37
+ }
38
+ get url() {
39
+ return this.client.url;
40
+ }
41
+ async get() {
42
+ const r = await this.client.request("GET", "/v1/fleet");
43
+ return { version: r.version, spec: r.fleet ?? emptySpec(), drift: r.drift ?? [] };
44
+ }
45
+ /**
46
+ * Reads the Fleet, lets `fn` change it (in place, or by returning a new spec) and writes it back only if nobody
47
+ * wrote in between; otherwise it reads again and retries, up to 5 times. `spec.defaults` is always present.
48
+ */
49
+ async replace(fn) {
50
+ for (let attempt = 1;; attempt++) {
51
+ const cur = await this.get();
52
+ cur.spec.defaults ??= {};
53
+ const next = (await fn(cur.spec)) ?? cur.spec;
54
+ try {
55
+ await this.client.request("PUT", "/v1/fleet", { fleet: next }, {
56
+ headers: { "if-match": `"${cur.version}"` },
57
+ });
58
+ }
59
+ catch (err) {
60
+ if (attempt < maxReplaceTries && err instanceof A12sError && err.code === "version_mismatch")
61
+ continue;
62
+ throw err;
63
+ }
64
+ return this.get();
65
+ }
66
+ }
67
+ /**
68
+ * Live events of the named agent computers, or (no argument) all of them, and the Fleet's own (`fleet.changed`,
69
+ * `agent_computer.phase`). Every stream of this Fleet shares one connection, which reconnects without losing
70
+ * events. `await stream.ready()` before sending work you want to see from the start; `close()` (or `break`) ends it.
71
+ * `turn` or `after` (one agent computer) replays from its journal instead.
72
+ */
73
+ events(agentComputers, opts = {}) {
74
+ if (opts.turn !== undefined || opts.after !== undefined)
75
+ return this.client.events(agentComputers, opts);
76
+ return this.client.sharedEvents(namesOf(agentComputers), opts);
77
+ }
78
+ /** Finished Turns of the named agent computers, or all of them, as they end. */
79
+ turns(agentComputers, opts = {}) {
80
+ return new TurnStream(this.client.sharedEvents(namesOf(agentComputers), opts));
81
+ }
82
+ /** Every agent computer and Service: phase, session, active Turn, inbox, power. */
83
+ status() {
84
+ return this.client.status();
85
+ }
86
+ }
87
+ function namesOf(names) {
88
+ return typeof names === "string" ? [names] : names;
89
+ }
90
+ function emptySpec() {
91
+ return { apiVersion: "a12s.ai/v1alpha1", kind: "Fleet", metadata: { name: "" }, agent_computers: {} };
92
+ }
93
+ function writeHeaders(opts) {
94
+ return opts.replace ? {} : { "if-none-match": "*" };
95
+ }
96
+ export class AgentComputers {
97
+ client;
98
+ constructor(client) {
99
+ this.client = client;
100
+ }
101
+ /** Adds an agent computer to the Fleet and starts it. Fails with code `exists` if the name is taken. */
102
+ async create(name, spec = {}) {
103
+ const { replace, ...body } = spec;
104
+ const r = await this.client.request("PUT", `/v1/agent-computers/${enc(name)}`, body, {
105
+ headers: writeHeaders({ replace }),
106
+ });
107
+ return r.agentComputer ? new AgentComputer(this.client, r.agentComputer) : this.get(name);
108
+ }
109
+ /** Throws {@link NotFound} if there is none. */
110
+ async get(name) {
111
+ return new AgentComputer(this.client, await this.client.request("GET", `/v1/agent-computers/${enc(name)}`));
112
+ }
113
+ async list() {
114
+ const r = await this.client.request("GET", "/v1/agent-computers");
115
+ return (r.agentComputers ?? []).map((d) => new AgentComputer(this.client, d));
116
+ }
117
+ }
118
+ /** A handle on one agent computer. `spec`, `effective` and `status` are as of the last create, get, reload or update. */
119
+ export class AgentComputer {
120
+ client;
121
+ name;
122
+ spec;
123
+ /** The spec with the Fleet's defaults merged in. */
124
+ effective;
125
+ status;
126
+ constructor(client, data) {
127
+ this.client = client;
128
+ this.name = data.name;
129
+ this.set(data);
130
+ }
131
+ set(d) {
132
+ this.spec = d.spec;
133
+ this.effective = d.effective;
134
+ this.status = d.status;
135
+ return this;
136
+ }
137
+ get path() {
138
+ return `/v1/agent-computers/${enc(this.name)}`;
139
+ }
140
+ async reload() {
141
+ return this.set(await this.client.request("GET", this.path));
142
+ }
143
+ /** Changes the spec: a key set to null goes back to the Fleet's default. A busy one picks it up when idle. */
144
+ async update(changes) {
145
+ const r = await this.client.request("PATCH", this.path, changes);
146
+ return r.agentComputer ? this.set(r.agentComputer) : this.reload();
147
+ }
148
+ /** Removes it from the Fleet and stops it; its drives are kept. */
149
+ async remove() {
150
+ await this.client.request("DELETE", this.path);
151
+ }
152
+ /** Sends a Message, waits for the Turn that delivers it and resolves when that Turn ends. */
153
+ prompt(text, opts = {}) {
154
+ return this.client.prompt(this.name, text, opts);
155
+ }
156
+ /** Puts a Message in the inbox and returns at once; it is delivered at the next idle moment. */
157
+ send(text, opts = {}) {
158
+ return this.client.send(this.name, { ...opts, body: text });
159
+ }
160
+ /** Its events, live, from the Fleet's shared connection; `turn` replays one Turn from its start instead. */
161
+ events(opts = {}) {
162
+ if (opts.turn !== undefined || opts.after !== undefined)
163
+ return this.client.events(this.name, opts);
164
+ return this.client.sharedEvents([this.name], opts);
165
+ }
166
+ turns(opts = {}) {
167
+ const { limit, ...rest } = opts;
168
+ if (limit !== undefined)
169
+ return this.pastTurns(limit);
170
+ return new TurnStream(this.client.sharedEvents([this.name], rest));
171
+ }
172
+ async pastTurns(limit) {
173
+ const r = await this.client.request("GET", `${this.path}/turns?limit=${limit}`);
174
+ return (r.turns ?? []).map((t) => ({
175
+ ...t,
176
+ computer: t.agentComputer,
177
+ tools: t.tools.map((tc) => ({ ...tc, isError: tc.isError ?? false })),
178
+ }));
179
+ }
180
+ /** Stops its processes and keeps its disks, session and inbox. */
181
+ sleep() {
182
+ return this.client.sleep(this.name);
183
+ }
184
+ wake() {
185
+ return this.client.wake(this.name);
186
+ }
187
+ /** Interrupts a Turn (default: the active one). */
188
+ stop(turn) {
189
+ return this.client.stop(this.name, turn);
190
+ }
191
+ inbox(opts = {}) {
192
+ return this.client.inbox(this.name, opts);
193
+ }
194
+ message(id) {
195
+ return this.client.message(this.name, id);
196
+ }
197
+ /** Withdraws a pending Message; a 409 A12sError names its state if it is too late. */
198
+ withdraw(id) {
199
+ return this.client.withdraw(this.name, id);
200
+ }
201
+ }
202
+ export class Services {
203
+ client;
204
+ constructor(client) {
205
+ this.client = client;
206
+ }
207
+ /** Adds a Service to the Fleet. Fails with code `exists` if the name is taken. */
208
+ async create(name, spec = {}) {
209
+ const { replace, ...body } = spec;
210
+ const r = await this.client.request("PUT", `/v1/services/${enc(name)}`, body, {
211
+ headers: writeHeaders({ replace }),
212
+ });
213
+ return r.service ? new Service(this.client, r.service) : this.get(name);
214
+ }
215
+ /** Throws {@link NotFound} if there is none. */
216
+ async get(name) {
217
+ return new Service(this.client, await this.client.service(name));
218
+ }
219
+ async list() {
220
+ return (await this.client.services()).map((s) => new Service(this.client, s));
221
+ }
222
+ }
223
+ /** A handle on one Service. `spec` and `status` are as of the last create, get, reload, update or power change. */
224
+ export class Service {
225
+ client;
226
+ name;
227
+ status;
228
+ constructor(client, status) {
229
+ this.client = client;
230
+ this.name = status.name;
231
+ this.status = status;
232
+ }
233
+ /** As written in the Fleet. */
234
+ get spec() {
235
+ return this.status.spec ?? {};
236
+ }
237
+ get path() {
238
+ return `/v1/services/${enc(this.name)}`;
239
+ }
240
+ async reload() {
241
+ this.status = await this.client.service(this.name);
242
+ return this;
243
+ }
244
+ /** Changes the spec as a JSON merge patch: a key set to null is removed. */
245
+ async update(changes) {
246
+ const r = await this.client.request("PATCH", this.path, changes);
247
+ if (!r.service)
248
+ return this.reload();
249
+ this.status = r.service;
250
+ return this;
251
+ }
252
+ /** Removes it from the Fleet and stops it; its state drive is kept. */
253
+ async remove() {
254
+ await this.client.request("DELETE", this.path);
255
+ }
256
+ sleep() {
257
+ return this.power("asleep");
258
+ }
259
+ wake() {
260
+ return this.power("awake");
261
+ }
262
+ async power(state) {
263
+ this.status = await this.client.request("POST", `${this.path}/power`, { state });
264
+ return this;
265
+ }
266
+ }
267
+ export class Tokens {
268
+ client;
269
+ constructor(client) {
270
+ this.client = client;
271
+ }
272
+ /**
273
+ * A token for another program; owner only. The secret is in the result once. With `prefix` it may do everything
274
+ * the owner can for names that start with it, and nothing outside; otherwise `actions` on `agentComputers`.
275
+ */
276
+ create(name, opts) {
277
+ if ("prefix" in opts) {
278
+ return this.client.createToken({ name, prefix: opts.prefix });
279
+ }
280
+ return this.client.createToken({ name, actions: opts.actions, computers: opts.agentComputers ?? ["*"] });
281
+ }
282
+ async list() {
283
+ return (await this.client.request("GET", "/v1/tokens")).tokens ?? [];
284
+ }
285
+ async remove(name) {
286
+ await this.client.request("DELETE", `/v1/tokens/${enc(name)}`);
287
+ }
288
+ }
289
+ /** Credentials by name: set once, then name them in a spec's `credentials`. Values are never read back. */
290
+ export class Credentials {
291
+ client;
292
+ constructor(client) {
293
+ this.client = client;
294
+ }
295
+ /** Stores the value on the Controller; agent computers that use it get the new value at once. */
296
+ set(name, value) {
297
+ return this.client.request("PUT", `/v1/credentials/${enc(name)}`, { value });
298
+ }
299
+ async list() {
300
+ return (await this.client.request("GET", "/v1/credentials")).credentials ?? [];
301
+ }
302
+ /** Fails with A12sError code `in_use` while an agent computer or Service uses it. */
303
+ async remove(name) {
304
+ await this.client.request("DELETE", `/v1/credentials/${enc(name)}`);
305
+ }
306
+ }
@@ -1,8 +1,11 @@
1
1
  import { A12sClient, type A12sClientOptions } from "./client.js";
2
2
  export * from "./client.js";
3
+ export * from "./fleet.js";
3
4
  export * from "./types.js";
4
5
  export type ConnectOptions = A12sClientOptions;
5
6
  /**
7
+ * @deprecated Use `Fleet.connect()`. Kept for one release.
8
+ *
6
9
  * In a browser: a client for the page's own origin, which is `a12s gateway` serving the page (no token needed).
7
10
  * Pass `url` and `token` to reach anything else.
8
11
  */
package/dist/src/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  // The default entry (browsers, Deno, workers): no node: imports. Node resolves "@coltonkirsten/a12s" to ./node.ts instead, through
2
- // the "node" export condition, so `import { connect } from "@coltonkirsten/a12s"` works everywhere.
2
+ // the "node" export condition, so `import { Fleet } from "@coltonkirsten/a12s"` works everywhere.
3
3
  import { A12sClient } from "./client.js";
4
4
  export * from "./client.js";
5
+ export * from "./fleet.js";
5
6
  export * from "./types.js";
6
7
  /**
8
+ * @deprecated Use `Fleet.connect()`. Kept for one release.
9
+ *
7
10
  * In a browser: a client for the page's own origin, which is `a12s gateway` serving the page (no token needed).
8
11
  * Pass `url` and `token` to reach anything else.
9
12
  */
@@ -1,6 +1,12 @@
1
1
  import { A12sClient, type A12sClientOptions } from "./client.js";
2
+ import { Fleet as PageFleet } from "./fleet.js";
2
3
  export * from "./client.js";
4
+ export * from "./fleet.js";
3
5
  export * from "./types.js";
6
+ export declare class Fleet extends PageFleet {
7
+ /** A running Fleet, as its owner, found like {@link connect} finds it. */
8
+ static connect(opts?: ConnectOptions): Promise<Fleet>;
9
+ }
4
10
  export interface ConnectOptions extends A12sClientOptions {
5
11
  /** The Fleet's name (`metadata.name`). Default: found as described on {@link connect}. */
6
12
  fleet?: string;
@@ -10,6 +16,8 @@ export interface ConnectOptions extends A12sClientOptions {
10
16
  cwd?: string;
11
17
  }
12
18
  /**
19
+ * @deprecated Use `Fleet.connect()`, which finds the Fleet the same way. Kept for one release.
20
+ *
13
21
  * A client for a running Fleet, as its owner. It uses the first of:
14
22
  * 1. `url` (and `token`), or `$A12S_URL` (and `$A12S_TOKEN`);
15
23
  * 2. the Fleet named by `fleet`;
package/dist/src/node.js CHANGED
@@ -3,9 +3,19 @@ import { readdir, readFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { A12sClient } from "./client.js";
6
+ import { Fleet as PageFleet } from "./fleet.js";
6
7
  export * from "./client.js";
8
+ export * from "./fleet.js";
7
9
  export * from "./types.js";
10
+ export class Fleet extends PageFleet {
11
+ /** A running Fleet, as its owner, found like {@link connect} finds it. */
12
+ static async connect(opts = {}) {
13
+ return new Fleet(await discover(opts));
14
+ }
15
+ }
8
16
  /**
17
+ * @deprecated Use `Fleet.connect()`, which finds the Fleet the same way. Kept for one release.
18
+ *
9
19
  * A client for a running Fleet, as its owner. It uses the first of:
10
20
  * 1. `url` (and `token`), or `$A12S_URL` (and `$A12S_TOKEN`);
11
21
  * 2. the Fleet named by `fleet`;
@@ -14,9 +24,12 @@ export * from "./types.js";
14
24
  * For 2–4 it reads `~/.a12s/<fleet>/controller.json`, which `a12s up` writes.
15
25
  */
16
26
  export async function connect(opts = {}) {
27
+ return new A12sClient(await discover(opts));
28
+ }
29
+ async function discover(opts) {
17
30
  const url = opts.url ?? process.env.A12S_URL;
18
31
  if (url)
19
- return new A12sClient({ url, token: opts.token ?? process.env.A12S_TOKEN, fetch: opts.fetch });
32
+ return { url, token: opts.token ?? process.env.A12S_TOKEN, fetch: opts.fetch };
20
33
  const home = join(opts.home ?? homedir(), ".a12s");
21
34
  const fleet = opts.fleet ?? (await fleetNameNearby(opts.cwd ?? process.cwd())) ?? (await onlyRunningFleet(home));
22
35
  const path = join(home, fleet, "controller.json");
@@ -27,7 +40,7 @@ export async function connect(opts = {}) {
27
40
  catch (err) {
28
41
  throw new Error(`Fleet ${fleet} isn't running (no ${path}): run a12s up in its directory first`, { cause: err });
29
42
  }
30
- return new A12sClient({ url: `http://127.0.0.1:${cf.port}`, token: cf.token, fetch: opts.fetch });
43
+ return { url: `http://127.0.0.1:${cf.port}`, token: cf.token, fetch: opts.fetch };
31
44
  }
32
45
  async function fleetNameNearby(cwd) {
33
46
  const file = process.env.A12S_FLEET ? resolve(cwd, process.env.A12S_FLEET) : await findUp(cwd, "fleet.yaml");
@@ -8,12 +8,108 @@ export interface Harness {
8
8
  version: string;
9
9
  model?: string;
10
10
  }
11
- /** A parsed fleet.yaml. a12s validates it; the client passes it through. */
12
- export type Fleet = Record<string, unknown> & {
13
- metadata: {
11
+ export interface AgentComputerSpec {
12
+ harness?: {
13
+ kind: "claude-code" | "codex" | (string & {});
14
+ version?: string;
15
+ model?: string;
16
+ };
17
+ image?: string;
18
+ instructions?: string;
19
+ /** File contents by path, written into the agent computer's home. */
20
+ files?: Record<string, string>;
21
+ resources?: {
22
+ cpu?: string | number;
23
+ memory?: string | number;
24
+ };
25
+ home?: {
26
+ size?: string | number;
27
+ preset?: string;
28
+ };
29
+ autonomy?: "full" | "restricted";
30
+ /** Credential names; their values come from `a12s up` or the credentials API. */
31
+ credentials?: string[];
32
+ drives?: Array<{
33
+ name: string;
34
+ at: string;
35
+ readOnly?: boolean;
36
+ }>;
37
+ egress?: {
38
+ allow?: string[];
39
+ };
40
+ /** `seccomp` is the profile JSON itself. */
41
+ security?: {
42
+ seccomp?: Record<string, unknown>;
43
+ };
44
+ inbox?: {
45
+ maxMessages?: number;
46
+ maxBytes?: string;
47
+ intro?: string;
48
+ };
49
+ power?: {
50
+ sleepAfterIdle?: string;
51
+ wakeOnMessage?: {
52
+ from: string[];
53
+ maxWakesPerHour?: number;
54
+ };
55
+ };
56
+ uses?: Array<string | {
14
57
  name: string;
58
+ mcp?: boolean;
59
+ }>;
60
+ grants?: {
61
+ send?: string[];
15
62
  };
63
+ }
64
+ /** A JSON merge patch of T: a key set to null is deleted. */
65
+ export type MergePatch<T> = {
66
+ [K in keyof T]?: T[K] | null;
16
67
  };
68
+ /** A Fleet as written: the defaults and each agent computer before the defaults are merged in. */
69
+ export interface FleetSpec {
70
+ apiVersion: string;
71
+ kind: string;
72
+ metadata: {
73
+ name: string;
74
+ };
75
+ egress?: {
76
+ enforcement?: string;
77
+ allowPrivate?: string[];
78
+ };
79
+ defaults?: AgentComputerSpec;
80
+ drives?: Record<string, {
81
+ size?: string | number;
82
+ access: string;
83
+ }>;
84
+ agent_computers: Record<string, AgentComputerSpec>;
85
+ services?: Record<string, Record<string, unknown>>;
86
+ }
87
+ export interface Change {
88
+ op: "create" | "update" | "remove" | (string & {});
89
+ kind: "agentComputer" | "service" | (string & {});
90
+ name: string;
91
+ /** "idle" for an update that waits for a busy agent computer's next idle boundary. */
92
+ when: "now" | "idle" | (string & {});
93
+ /** The top-level spec fields that differ, for an update. */
94
+ fields?: string[];
95
+ }
96
+ export interface FleetResponse {
97
+ version: number;
98
+ fleet: FleetSpec | null;
99
+ /** What changed through the API since the last whole-Fleet write: `a12s up` from the file undoes it. */
100
+ drift: Change[];
101
+ }
102
+ export interface AgentComputerData {
103
+ name: string;
104
+ /** As written. */
105
+ spec: AgentComputerSpec;
106
+ /** With the Fleet's defaults merged in. */
107
+ effective: AgentComputerSpec;
108
+ status: ComputerStatus;
109
+ }
110
+ export interface AgentComputersResponse {
111
+ agentComputers: AgentComputerData[];
112
+ }
17
113
  export interface Lock {
18
114
  a12s?: string;
19
115
  images: Record<string, string>;
@@ -24,16 +120,12 @@ export interface Lock {
24
120
  seccomp?: Record<string, string>;
25
121
  }
26
122
  export interface PutFleetRequest {
27
- fleet: Fleet;
123
+ fleet: FleetSpec;
28
124
  lock?: Lock;
29
125
  /** Credential values by name, base64-encoded (Go []byte). Kept in the Controller's memory only. */
30
126
  credentials?: Record<string, string>;
31
127
  asleep?: boolean;
32
128
  refresh?: string[];
33
- /** Home presets as base64 tar streams, keyed by their path in the Fleet. */
34
- presets?: Record<string, string>;
35
- /** security.seccomp profiles (JSON text), keyed by their path in the Fleet. */
36
- seccomp?: Record<string, string>;
37
129
  }
38
130
  export interface ServiceChanges {
39
131
  created: string[] | null;
@@ -41,6 +133,10 @@ export interface ServiceChanges {
41
133
  removed: string[] | null;
42
134
  }
43
135
  export interface PutFleetResponse {
136
+ /** The Fleet's version after the write; unchanged by a dry run or a write that changed nothing. */
137
+ version: number;
138
+ dryRun?: boolean;
139
+ changes: Change[];
44
140
  created: string[] | null;
45
141
  updated: string[] | null;
46
142
  staged: string[] | null;
@@ -49,6 +145,10 @@ export interface PutFleetResponse {
49
145
  errors?: Record<string, string>;
50
146
  warnings?: string[];
51
147
  services: ServiceChanges;
148
+ /** The written agent computer, after a PUT or PATCH of one. */
149
+ agentComputer?: AgentComputerData;
150
+ /** The written Service, after a PUT or PATCH of one. */
151
+ service?: ServiceStatus;
52
152
  }
53
153
  export interface LimitValue {
54
154
  value: number;
@@ -146,10 +246,14 @@ export interface ServiceStatus {
146
246
  stateDrive?: string;
147
247
  egressMode?: string;
148
248
  warnings?: string[];
249
+ /** The Service as written in the Fleet. */
250
+ spec?: Record<string, unknown>;
149
251
  }
150
252
  export interface StatusResponse {
151
253
  fleet: string;
152
254
  egressEnforcement: string;
255
+ agentComputers: ComputerStatus[];
256
+ /** @deprecated `agentComputers` under its old name. */
153
257
  computers: ComputerStatus[];
154
258
  orphanedDrives?: DriveRow[];
155
259
  credentialsLoaded: boolean;
@@ -213,10 +317,12 @@ export interface InboxListOptions {
213
317
  bodies?: boolean;
214
318
  after?: number;
215
319
  }
216
- /** The fields every journal event has. `seq` is the Computer's journal position and the SSE id. */
320
+ /** The fields every event has. `seq` is the agent computer's journal position (0 for the Controller's own events). */
217
321
  export interface EventEnvelope {
218
322
  v: number;
219
- /** The Computer that journaled it. */
323
+ /** The agent computer it is about; absent on `fleet.changed` and in journals older than the field. */
324
+ agentComputer?: string;
325
+ /** `agentComputer` under its old name, for one release. */
220
326
  computer: string;
221
327
  session: string;
222
328
  /** The Turn it belongs to; "" for events outside a Turn. */
@@ -264,8 +370,34 @@ export interface TurnFinishedData {
264
370
  status: "completed" | "failed" | "interrupted";
265
371
  /** The harness's own usage object (tokens, cost), or null. */
266
372
  usage: unknown;
373
+ /** The same usage, normalized across harnesses, when the harness reported it. */
374
+ tokens?: TokenUsage;
267
375
  error?: string;
268
376
  }
377
+ /** A Turn's token usage, the same for every harness. `input` excludes cached input. */
378
+ export interface TokenUsage {
379
+ input: number;
380
+ output: number;
381
+ cacheRead: number;
382
+ cacheWrite: number;
383
+ }
384
+ /** The egress proxy refused a connection. */
385
+ export interface EgressDeniedData {
386
+ host: string;
387
+ port: number;
388
+ reason?: string;
389
+ }
390
+ /** The Fleet's version moved; `names` are the agent computers and Services the write changed. */
391
+ export interface FleetChangedData {
392
+ version: number;
393
+ /** Who wrote it. */
394
+ by: string;
395
+ names: string[];
396
+ }
397
+ export interface PhaseData {
398
+ phase: Phase;
399
+ error?: Failure;
400
+ }
269
401
  export interface ToolStartedData {
270
402
  id: string;
271
403
  /** MCP tools are named `mcp__<server>__<tool>`, e.g. `mcp__a12s__send`. */
@@ -331,10 +463,15 @@ export type TurnRefusedEvent = Ev<"turn.refused", {
331
463
  }>;
332
464
  /** A Message that ended without delivery (cleared, withdrawn, expired, replaced), body included. */
333
465
  export type InboxDiscardedEvent = Ev<"inbox.discarded", Record<string, unknown>>;
466
+ export type EgressDeniedEvent = Ev<"egress.denied", EgressDeniedData>;
467
+ /** Made by the Controller, on the fleet-wide stream only. */
468
+ export type FleetChangedEvent = Ev<"fleet.changed", FleetChangedData>;
469
+ /** Made by the Controller, on the fleet-wide stream only. */
470
+ export type PhaseEvent = Ev<"agent_computer.phase", PhaseData>;
334
471
  /** Made by the SDK, never journaled: the stream could not resume from its cursor and events were lost. */
335
472
  export type GapEvent = Ev<"gap", GapData>;
336
- /** One event of a Computer's journal. Narrow on `type` to get typed `data`. */
337
- export type A12sEvent = TurnStartedEvent | TurnDispatchedEvent | TextDeltaEvent | ToolStartedEvent | ToolFinishedEvent | PermissionEvent | DiagnosticEvent | TurnFinishedEvent | SessionChangedEvent | ResumeFailedEvent | LifecycleEvent | HarnessRestartedEvent | ContinuationAbandonedEvent | TurnRefusedEvent | InboxDiscardedEvent | GapEvent;
473
+ /** One event of an agent computer's journal, or the Controller's own on the fleet-wide stream. Narrow on `type` to get typed `data`. */
474
+ export type A12sEvent = TurnStartedEvent | TurnDispatchedEvent | TextDeltaEvent | ToolStartedEvent | ToolFinishedEvent | PermissionEvent | DiagnosticEvent | TurnFinishedEvent | SessionChangedEvent | ResumeFailedEvent | LifecycleEvent | HarnessRestartedEvent | ContinuationAbandonedEvent | TurnRefusedEvent | InboxDiscardedEvent | EgressDeniedEvent | FleetChangedEvent | PhaseEvent | GapEvent;
338
475
  export type EventType = A12sEvent["type"];
339
476
  /** One tool call in a Turn. A `send` to another Computer is `mcp__a12s__send` with `input.body`. */
340
477
  export interface ToolCall {
@@ -346,6 +483,8 @@ export interface ToolCall {
346
483
  }
347
484
  /** A finished Turn: what `turns()` yields and `prompt()` returns. */
348
485
  export interface Turn {
486
+ agentComputer: string;
487
+ /** `agentComputer` under its old name, for one release. */
349
488
  computer: string;
350
489
  /** The Turn id. */
351
490
  id: string;
@@ -359,24 +498,47 @@ export interface Turn {
359
498
  /** The tools it called, in order, with their input and output: what it `send`s to other Computers is here. */
360
499
  tools: ToolCall[];
361
500
  usage: unknown;
501
+ tokens?: TokenUsage;
362
502
  startedAt?: string;
363
503
  finishedAt: string;
364
504
  }
505
+ /** Past Turns, newest first, as GET /v1/agent-computers/{name}/turns sends them. */
506
+ export interface TurnsResponse {
507
+ turns: Array<Omit<Turn, "computer" | "tools"> & {
508
+ tools: Array<Omit<ToolCall, "isError"> & {
509
+ isError?: boolean;
510
+ }>;
511
+ }>;
512
+ }
365
513
  export interface ApiErrorBody {
366
514
  code: string;
367
515
  message: string;
368
516
  fix?: string;
369
517
  details?: Record<string, string>;
370
518
  }
519
+ export type TokenAction = "send" | "inbox" | "status" | "events" | "power" | "stop";
520
+ /** Either `prefix`, or `actions` on `computers` (agent computer names, or "*"). */
371
521
  export interface TokenRequest {
372
522
  name: string;
373
- actions: Array<"send" | "inbox" | "status" | "events" | "power" | "stop">;
374
- computers: string[];
523
+ actions?: TokenAction[];
524
+ computers?: string[];
525
+ /** Every power over agent computers, Services and credentials whose names start with it; none outside. */
526
+ prefix?: string;
527
+ }
528
+ /** A credential's name, whether it has a value, and who uses it. */
529
+ export interface CredentialInfo {
530
+ name: string;
531
+ set: boolean;
532
+ usedBy?: string[];
533
+ }
534
+ export interface CredentialsResponse {
535
+ credentials: CredentialInfo[];
375
536
  }
376
537
  export interface Token {
377
538
  name: string;
378
539
  actions: string[];
379
540
  computers: string[];
541
+ prefix?: string;
380
542
  createdAt: string;
381
543
  secret?: string;
382
544
  }