@coltonkirsten/a12s 0.4.1 → 0.4.3

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/dist/a12s.js CHANGED
@@ -109,7 +109,7 @@ var A12sClient = class {
109
109
  }
110
110
  async computer(name) {
111
111
  const c = (await this.status()).agentComputers.find((x) => x.name === name);
112
- if (!c) throw new NotFound(404, { code: "not_found", message: `no Computer ${name}` });
112
+ if (!c) throw new NotFound(404, { code: "not_found", message: `no agent computer ${name}` });
113
113
  return c;
114
114
  }
115
115
  /**
@@ -142,11 +142,12 @@ var A12sClient = class {
142
142
  events(computers, opts = {}) {
143
143
  const names = typeof computers === "string" ? [computers] : computers;
144
144
  if ((opts.turn !== void 0 || opts.after !== void 0) && names?.length !== 1) {
145
- throw new TypeError("events: turn and after need exactly one Computer");
145
+ throw new TypeError("events: turn and after need exactly one agent computer");
146
146
  }
147
147
  return new EventStream((name, live, signal) => {
148
148
  const q = opts.turn ? `?turn=${enc(opts.turn)}` : "";
149
- return this.stream(name, `/v1/agent-computers/${enc(name)}/events${q}`, opts.after, { ...opts, signal, live });
149
+ const events = this.stream(name, `/v1/agent-computers/${enc(name)}/events${q}`, opts.after, { ...opts, signal, live });
150
+ return opts.turn ? untilTurnFinished(events) : events;
150
151
  }, names ?? (async () => (await this.status()).agentComputers.map((c) => c.name)), opts.signal);
151
152
  }
152
153
  /**
@@ -195,13 +196,30 @@ var A12sClient = class {
195
196
  const id = turn ?? (await this.computer(computer)).activeTurn;
196
197
  if (id) await this.request("POST", `/v1/agent-computers/${enc(computer)}/turns/${enc(id)}/stop`, {});
197
198
  }
198
- /** Stops the Computer's processes and keeps its disks, session and inbox. */
199
- async sleep(computer) {
200
- await this.request("POST", `/v1/agent-computers/${enc(computer)}/power`, { state: "asleep" });
199
+ /**
200
+ * Stops the agent computer's processes and keeps its disks, session and inbox. It stays asleep until wake(); with
201
+ * `allowWake` its power policy (wakeOnMessage) may wake it instead.
202
+ */
203
+ async sleep(computer, opts = {}) {
204
+ await this.request("POST", `/v1/agent-computers/${enc(computer)}/power`, {
205
+ state: "asleep",
206
+ ...opts.allowWake ? { allowWake: true } : {}
207
+ });
208
+ }
209
+ /** Starts a sleeping agent computer; its session resumes on the next Turn. `clearInbox` discards pending Messages. */
210
+ async wake(computer, opts = {}) {
211
+ await this.request("POST", `/v1/agent-computers/${enc(computer)}/power`, {
212
+ state: "awake",
213
+ ...opts.clearInbox ? { clearInbox: true, requestId: crypto.randomUUID() } : {}
214
+ });
201
215
  }
202
- /** Starts a sleeping Computer; its session resumes on the next Turn. */
203
- async wake(computer) {
204
- await this.request("POST", `/v1/agent-computers/${enc(computer)}/power`, { state: "awake" });
216
+ /** Holds the inbox: Messages still queue, none is delivered until resume(). */
217
+ hold(computer, opts = {}) {
218
+ return this.request("POST", `/v1/agent-computers/${enc(computer)}/inbox/hold`, opts.reason ? { reason: opts.reason } : {});
219
+ }
220
+ /** Releases a hold; pending Messages are delivered at the next idle moment. */
221
+ resume(computer) {
222
+ return this.request("POST", `/v1/agent-computers/${enc(computer)}/inbox/resume`, {});
205
223
  }
206
224
  async services() {
207
225
  return (await this.request("GET", "/v1/services")).services ?? [];
@@ -472,6 +490,16 @@ function wanted(names, ev) {
472
490
  if (ev.type === "service.phase") return names.has(ev.data?.service);
473
491
  return names.has(agentComputerOf(ev));
474
492
  }
493
+ async function* untilTurnFinished(events) {
494
+ try {
495
+ for await (const ev of events) {
496
+ yield ev;
497
+ if (ev.type === "turn.finished") return;
498
+ }
499
+ } finally {
500
+ await events.return(void 0);
501
+ }
502
+ }
475
503
  var EventStream = class {
476
504
  constructor(source, names, signal) {
477
505
  this.source = source;
@@ -729,9 +757,11 @@ var Fleet = class _Fleet {
729
757
  cur.spec.defaults ??= {};
730
758
  const next = await fn(cur.spec) ?? cur.spec;
731
759
  try {
732
- await this.client.request("PUT", "/v1/fleet", { fleet: next }, {
733
- headers: { "if-match": `"${cur.version}"` }
734
- });
760
+ warnWrites(
761
+ await this.client.request("PUT", "/v1/fleet", { fleet: next }, {
762
+ headers: { "if-match": `"${cur.version}"` }
763
+ })
764
+ );
735
765
  } catch (err) {
736
766
  if (attempt < maxReplaceTries && err instanceof A12sError && err.code === "version_mismatch") continue;
737
767
  throw err;
@@ -777,6 +807,7 @@ var AgentComputers = class {
777
807
  const r = await this.client.request("PUT", `/v1/agent-computers/${enc2(name)}`, body, {
778
808
  headers: writeHeaders({ replace })
779
809
  });
810
+ warnWrites(r);
780
811
  return r.agentComputer ? new AgentComputer(this.client, r.agentComputer) : this.get(name);
781
812
  }
782
813
  /** Throws {@link NotFound} if there is none. */
@@ -814,6 +845,7 @@ var AgentComputer = class {
814
845
  /** Changes the spec: a key set to null goes back to the Fleet's default. A busy one picks it up when idle. */
815
846
  async update(changes) {
816
847
  const r = await this.client.request("PATCH", this.path, changes);
848
+ warnWrites(r);
817
849
  return r.agentComputer ? this.set(r.agentComputer) : this.reload();
818
850
  }
819
851
  /** Removes it from the Fleet and stops it; its drives are kept. */
@@ -846,12 +878,21 @@ var AgentComputer = class {
846
878
  tools: t.tools.map((tc) => ({ ...tc, isError: tc.isError ?? false }))
847
879
  }));
848
880
  }
849
- /** Stops its processes and keeps its disks, session and inbox. */
850
- sleep() {
851
- return this.client.sleep(this.name);
881
+ /** Stops its processes and keeps its disks, session and inbox until wake(); `allowWake` lets wakeOnMessage wake it. */
882
+ sleep(opts = {}) {
883
+ return this.client.sleep(this.name, opts);
852
884
  }
853
- wake() {
854
- return this.client.wake(this.name);
885
+ /** Wakes it; `clearInbox` discards the pending Messages first. */
886
+ wake(opts = {}) {
887
+ return this.client.wake(this.name, opts);
888
+ }
889
+ /** Holds its inbox: Messages still queue, none is delivered until resume(). */
890
+ hold(opts = {}) {
891
+ return this.client.hold(this.name, opts);
892
+ }
893
+ /** Releases a hold on its inbox. */
894
+ resume() {
895
+ return this.client.resume(this.name);
855
896
  }
856
897
  /** Interrupts a Turn (default: the active one). */
857
898
  stop(turn) {
@@ -878,6 +919,7 @@ var Services = class {
878
919
  const r = await this.client.request("PUT", `/v1/services/${enc2(name)}`, body, {
879
920
  headers: writeHeaders({ replace })
880
921
  });
922
+ warnWrites(r);
881
923
  return r.service ? new Service(this.client, r.service) : this.get(name);
882
924
  }
883
925
  /** Throws {@link NotFound} if there is none. */
@@ -910,6 +952,7 @@ var Service = class {
910
952
  /** Changes the spec as a JSON merge patch: a key set to null is removed. */
911
953
  async update(changes) {
912
954
  const r = await this.client.request("PATCH", this.path, changes);
955
+ warnWrites(r);
913
956
  if (!r.service) return this.reload();
914
957
  this.status = r.service;
915
958
  return this;
@@ -966,6 +1009,9 @@ var Credentials = class {
966
1009
  await this.client.request("DELETE", `/v1/credentials/${enc2(name)}`);
967
1010
  }
968
1011
  };
1012
+ function warnWrites(r) {
1013
+ for (const w of r?.warnings ?? []) console.warn(`a12s: ${w}`);
1014
+ }
969
1015
 
970
1016
  // src/index.ts
971
1017
  async function connect(opts = {}) {
@@ -106,10 +106,23 @@ export declare class A12sClient {
106
106
  withdraw(computer: string, id: string): Promise<Message>;
107
107
  /** Interrupts a Turn (default: the active one). The Turn ends `interrupted`; nothing is replayed. */
108
108
  stop(computer: string, turn?: string): Promise<void>;
109
- /** Stops the Computer's processes and keeps its disks, session and inbox. */
110
- sleep(computer: string): Promise<void>;
111
- /** Starts a sleeping Computer; its session resumes on the next Turn. */
112
- wake(computer: string): Promise<void>;
109
+ /**
110
+ * Stops the agent computer's processes and keeps its disks, session and inbox. It stays asleep until wake(); with
111
+ * `allowWake` its power policy (wakeOnMessage) may wake it instead.
112
+ */
113
+ sleep(computer: string, opts?: {
114
+ allowWake?: boolean;
115
+ }): Promise<void>;
116
+ /** Starts a sleeping agent computer; its session resumes on the next Turn. `clearInbox` discards pending Messages. */
117
+ wake(computer: string, opts?: {
118
+ clearInbox?: boolean;
119
+ }): Promise<void>;
120
+ /** Holds the inbox: Messages still queue, none is delivered until resume(). */
121
+ hold(computer: string, opts?: {
122
+ reason?: string;
123
+ }): Promise<InboxListResponse>;
124
+ /** Releases a hold; pending Messages are delivered at the next idle moment. */
125
+ resume(computer: string): Promise<InboxListResponse>;
113
126
  services(): Promise<ServiceStatus[]>;
114
127
  service(name: string): Promise<ServiceStatus>;
115
128
  /** A scoped token for another program; owner only. The secret is in the response once. */
@@ -82,7 +82,7 @@ export class A12sClient {
82
82
  async computer(name) {
83
83
  const c = (await this.status()).agentComputers.find((x) => x.name === name);
84
84
  if (!c)
85
- throw new NotFound(404, { code: "not_found", message: `no Computer ${name}` });
85
+ throw new NotFound(404, { code: "not_found", message: `no agent computer ${name}` });
86
86
  return c;
87
87
  }
88
88
  /**
@@ -117,11 +117,12 @@ export class A12sClient {
117
117
  events(computers, opts = {}) {
118
118
  const names = typeof computers === "string" ? [computers] : computers;
119
119
  if ((opts.turn !== undefined || opts.after !== undefined) && names?.length !== 1) {
120
- throw new TypeError("events: turn and after need exactly one Computer");
120
+ throw new TypeError("events: turn and after need exactly one agent computer");
121
121
  }
122
122
  return new EventStream((name, live, signal) => {
123
123
  const q = opts.turn ? `?turn=${enc(opts.turn)}` : "";
124
- return this.stream(name, `/v1/agent-computers/${enc(name)}/events${q}`, opts.after, { ...opts, signal, live });
124
+ const events = this.stream(name, `/v1/agent-computers/${enc(name)}/events${q}`, opts.after, { ...opts, signal, live });
125
+ return opts.turn ? untilTurnFinished(events) : events;
125
126
  }, names ?? (async () => (await this.status()).agentComputers.map((c) => c.name)), opts.signal);
126
127
  }
127
128
  /**
@@ -168,13 +169,30 @@ export class A12sClient {
168
169
  if (id)
169
170
  await this.request("POST", `/v1/agent-computers/${enc(computer)}/turns/${enc(id)}/stop`, {});
170
171
  }
171
- /** Stops the Computer's processes and keeps its disks, session and inbox. */
172
- async sleep(computer) {
173
- await this.request("POST", `/v1/agent-computers/${enc(computer)}/power`, { state: "asleep" });
172
+ /**
173
+ * Stops the agent computer's processes and keeps its disks, session and inbox. It stays asleep until wake(); with
174
+ * `allowWake` its power policy (wakeOnMessage) may wake it instead.
175
+ */
176
+ async sleep(computer, opts = {}) {
177
+ await this.request("POST", `/v1/agent-computers/${enc(computer)}/power`, {
178
+ state: "asleep",
179
+ ...(opts.allowWake ? { allowWake: true } : {}),
180
+ });
181
+ }
182
+ /** Starts a sleeping agent computer; its session resumes on the next Turn. `clearInbox` discards pending Messages. */
183
+ async wake(computer, opts = {}) {
184
+ await this.request("POST", `/v1/agent-computers/${enc(computer)}/power`, {
185
+ state: "awake",
186
+ ...(opts.clearInbox ? { clearInbox: true, requestId: crypto.randomUUID() } : {}),
187
+ });
188
+ }
189
+ /** Holds the inbox: Messages still queue, none is delivered until resume(). */
190
+ hold(computer, opts = {}) {
191
+ return this.request("POST", `/v1/agent-computers/${enc(computer)}/inbox/hold`, opts.reason ? { reason: opts.reason } : {});
174
192
  }
175
- /** Starts a sleeping Computer; its session resumes on the next Turn. */
176
- async wake(computer) {
177
- await this.request("POST", `/v1/agent-computers/${enc(computer)}/power`, { state: "awake" });
193
+ /** Releases a hold; pending Messages are delivered at the next idle moment. */
194
+ resume(computer) {
195
+ return this.request("POST", `/v1/agent-computers/${enc(computer)}/inbox/resume`, {});
178
196
  }
179
197
  async services() {
180
198
  return (await this.request("GET", "/v1/services")).services ?? [];
@@ -496,6 +514,19 @@ function wanted(names, ev) {
496
514
  return names.has(ev.data?.service);
497
515
  return names.has(agentComputerOf(ev));
498
516
  }
517
+ /** A Turn replay: its events up to and including its turn.finished. The server keeps the stream open after. */
518
+ async function* untilTurnFinished(events) {
519
+ try {
520
+ for await (const ev of events) {
521
+ yield ev;
522
+ if (ev.type === "turn.finished")
523
+ return;
524
+ }
525
+ }
526
+ finally {
527
+ await events.return(undefined);
528
+ }
529
+ }
499
530
  /** Events of one or more Computers, merged in arrival order. Iterate it with `for await`. */
500
531
  export class EventStream {
501
532
  source;
@@ -95,9 +95,20 @@ export declare class AgentComputer {
95
95
  limit?: undefined;
96
96
  }): TurnStream;
97
97
  private pastTurns;
98
- /** Stops its processes and keeps its disks, session and inbox. */
99
- sleep(): Promise<void>;
100
- wake(): Promise<void>;
98
+ /** Stops its processes and keeps its disks, session and inbox until wake(); `allowWake` lets wakeOnMessage wake it. */
99
+ sleep(opts?: {
100
+ allowWake?: boolean;
101
+ }): Promise<void>;
102
+ /** Wakes it; `clearInbox` discards the pending Messages first. */
103
+ wake(opts?: {
104
+ clearInbox?: boolean;
105
+ }): Promise<void>;
106
+ /** Holds its inbox: Messages still queue, none is delivered until resume(). */
107
+ hold(opts?: {
108
+ reason?: string;
109
+ }): Promise<InboxListResponse>;
110
+ /** Releases a hold on its inbox. */
111
+ resume(): Promise<InboxListResponse>;
101
112
  /** Interrupts a Turn (default: the active one). */
102
113
  stop(turn?: string): Promise<void>;
103
114
  inbox(opts?: InboxListOptions): Promise<InboxListResponse>;
package/dist/src/fleet.js CHANGED
@@ -52,9 +52,9 @@ export class Fleet {
52
52
  cur.spec.defaults ??= {};
53
53
  const next = (await fn(cur.spec)) ?? cur.spec;
54
54
  try {
55
- await this.client.request("PUT", "/v1/fleet", { fleet: next }, {
55
+ warnWrites(await this.client.request("PUT", "/v1/fleet", { fleet: next }, {
56
56
  headers: { "if-match": `"${cur.version}"` },
57
- });
57
+ }));
58
58
  }
59
59
  catch (err) {
60
60
  if (attempt < maxReplaceTries && err instanceof A12sError && err.code === "version_mismatch")
@@ -104,6 +104,7 @@ export class AgentComputers {
104
104
  const r = await this.client.request("PUT", `/v1/agent-computers/${enc(name)}`, body, {
105
105
  headers: writeHeaders({ replace }),
106
106
  });
107
+ warnWrites(r);
107
108
  return r.agentComputer ? new AgentComputer(this.client, r.agentComputer) : this.get(name);
108
109
  }
109
110
  /** Throws {@link NotFound} if there is none. */
@@ -143,6 +144,7 @@ export class AgentComputer {
143
144
  /** Changes the spec: a key set to null goes back to the Fleet's default. A busy one picks it up when idle. */
144
145
  async update(changes) {
145
146
  const r = await this.client.request("PATCH", this.path, changes);
147
+ warnWrites(r);
146
148
  return r.agentComputer ? this.set(r.agentComputer) : this.reload();
147
149
  }
148
150
  /** Removes it from the Fleet and stops it; its drives are kept. */
@@ -177,12 +179,21 @@ export class AgentComputer {
177
179
  tools: t.tools.map((tc) => ({ ...tc, isError: tc.isError ?? false })),
178
180
  }));
179
181
  }
180
- /** Stops its processes and keeps its disks, session and inbox. */
181
- sleep() {
182
- return this.client.sleep(this.name);
182
+ /** Stops its processes and keeps its disks, session and inbox until wake(); `allowWake` lets wakeOnMessage wake it. */
183
+ sleep(opts = {}) {
184
+ return this.client.sleep(this.name, opts);
183
185
  }
184
- wake() {
185
- return this.client.wake(this.name);
186
+ /** Wakes it; `clearInbox` discards the pending Messages first. */
187
+ wake(opts = {}) {
188
+ return this.client.wake(this.name, opts);
189
+ }
190
+ /** Holds its inbox: Messages still queue, none is delivered until resume(). */
191
+ hold(opts = {}) {
192
+ return this.client.hold(this.name, opts);
193
+ }
194
+ /** Releases a hold on its inbox. */
195
+ resume() {
196
+ return this.client.resume(this.name);
186
197
  }
187
198
  /** Interrupts a Turn (default: the active one). */
188
199
  stop(turn) {
@@ -210,6 +221,7 @@ export class Services {
210
221
  const r = await this.client.request("PUT", `/v1/services/${enc(name)}`, body, {
211
222
  headers: writeHeaders({ replace }),
212
223
  });
224
+ warnWrites(r);
213
225
  return r.service ? new Service(this.client, r.service) : this.get(name);
214
226
  }
215
227
  /** Throws {@link NotFound} if there is none. */
@@ -244,6 +256,7 @@ export class Service {
244
256
  /** Changes the spec as a JSON merge patch: a key set to null is removed. */
245
257
  async update(changes) {
246
258
  const r = await this.client.request("PATCH", this.path, changes);
259
+ warnWrites(r);
247
260
  if (!r.service)
248
261
  return this.reload();
249
262
  this.status = r.service;
@@ -304,3 +317,9 @@ export class Credentials {
304
317
  await this.client.request("DELETE", `/v1/credentials/${enc(name)}`);
305
318
  }
306
319
  }
320
+ /** A write's warnings (a grant to an agent computer that doesn't exist yet, a missing harness credential) go to the
321
+ * console instead of being dropped. */
322
+ function warnWrites(r) {
323
+ for (const w of r?.warnings ?? [])
324
+ console.warn(`a12s: ${w}`);
325
+ }
@@ -23,7 +23,8 @@ export interface ConnectOptions extends A12sClientOptions {
23
23
  * 2. the Fleet named by `fleet`;
24
24
  * 3. the Fleet file in `$A12S_FLEET`, else the nearest `fleet.yaml` in the current directory or a parent;
25
25
  * 4. the only Fleet with a running Controller on this machine.
26
- * For 2–4 it reads `~/.a12s/<fleet>/controller.json`, which `a12s up` writes.
26
+ * For 2–4 it reads `~/.a12s/<fleet>/controller.json`, which `a12s up` writes: its URL always, and its owner token
27
+ * unless `token` is given (a prefix or narrow token on this machine).
27
28
  */
28
29
  export declare function connect(opts?: ConnectOptions): Promise<A12sClient>;
29
30
  /** metadata.name from a Fleet file, in either `metadata: { name: x }` or block form. */
package/dist/src/node.js CHANGED
@@ -21,7 +21,8 @@ export class Fleet extends PageFleet {
21
21
  * 2. the Fleet named by `fleet`;
22
22
  * 3. the Fleet file in `$A12S_FLEET`, else the nearest `fleet.yaml` in the current directory or a parent;
23
23
  * 4. the only Fleet with a running Controller on this machine.
24
- * For 2–4 it reads `~/.a12s/<fleet>/controller.json`, which `a12s up` writes.
24
+ * For 2–4 it reads `~/.a12s/<fleet>/controller.json`, which `a12s up` writes: its URL always, and its owner token
25
+ * unless `token` is given (a prefix or narrow token on this machine).
25
26
  */
26
27
  export async function connect(opts = {}) {
27
28
  return new A12sClient(await discover(opts));
@@ -40,7 +41,7 @@ async function discover(opts) {
40
41
  catch (err) {
41
42
  throw new Error(`Fleet ${fleet} isn't running (no ${path}): run a12s up in its directory first`, { cause: err });
42
43
  }
43
- return { url: `http://127.0.0.1:${cf.port}`, token: cf.token, fetch: opts.fetch };
44
+ return { url: `http://127.0.0.1:${cf.port}`, token: opts.token ?? cf.token, fetch: opts.fetch };
44
45
  }
45
46
  async function fleetNameNearby(cwd) {
46
47
  const file = process.env.A12S_FLEET ? resolve(cwd, process.env.A12S_FLEET) : await findUp(cwd, "fleet.yaml");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coltonkirsten/a12s",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "TypeScript client for a12s: send work to agent Computers, stream their events and Turns, in Node or the browser",
5
5
  "license": "MIT",
6
6
  "type": "module",