@chipschat/harness 0.0.0-stage → 1.0.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/LICENSE +201 -0
- package/README.md +66 -2
- package/dist/asks.d.ts +75 -0
- package/dist/asks.d.ts.map +1 -0
- package/dist/asks.js +103 -0
- package/dist/asks.js.map +1 -0
- package/dist/connect.d.ts +406 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +883 -0
- package/dist/connect.js.map +1 -0
- package/dist/events.d.ts +89 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +36 -0
- package/dist/events.js.map +1 -0
- package/dist/files.d.ts +34 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +78 -0
- package/dist/files.js.map +1 -0
- package/dist/heard.d.ts +22 -0
- package/dist/heard.d.ts.map +1 -0
- package/dist/heard.js +32 -0
- package/dist/heard.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/join.d.ts +101 -0
- package/dist/join.d.ts.map +1 -0
- package/dist/join.js +289 -0
- package/dist/join.js.map +1 -0
- package/dist/keep.d.ts +33 -0
- package/dist/keep.d.ts.map +1 -0
- package/dist/keep.js +160 -0
- package/dist/keep.js.map +1 -0
- package/dist/lane.d.ts +73 -0
- package/dist/lane.d.ts.map +1 -0
- package/dist/lane.js +105 -0
- package/dist/lane.js.map +1 -0
- package/dist/outbox.d.ts +50 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +52 -0
- package/dist/outbox.js.map +1 -0
- package/dist/project.d.ts +224 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +619 -0
- package/dist/project.js.map +1 -0
- package/dist/prompt.d.ts +61 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +87 -0
- package/dist/prompt.js.map +1 -0
- package/dist/rules.d.ts +18 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +25 -0
- package/dist/rules.js.map +1 -0
- package/dist/run.d.ts +246 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +295 -0
- package/dist/run.js.map +1 -0
- package/dist/sending.d.ts +78 -0
- package/dist/sending.d.ts.map +1 -0
- package/dist/sending.js +149 -0
- package/dist/sending.js.map +1 -0
- package/dist/service.d.ts +124 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +349 -0
- package/dist/service.js.map +1 -0
- package/dist/settings.d.ts +137 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +228 -0
- package/dist/settings.js.map +1 -0
- package/dist/steps.d.ts +27 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/steps.js +26 -0
- package/dist/steps.js.map +1 -0
- package/dist/store.d.ts +35 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +52 -0
- package/dist/store.js.map +1 -0
- package/package.json +37 -3
package/dist/connect.js
ADDED
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `connect`: one socket to `chipsd`, the handshake, a subscription that resumes from where it
|
|
3
|
+
* got to, and the outbox that makes every send safe to repeat. Node 22's own `WebSocket` is
|
|
4
|
+
* enough; there is nothing to install beyond `@chipschat/protocol`. Two calls go over HTTP, with
|
|
5
|
+
* Node 22's own `fetch` and the same token: the rooms a person asked this harness for, read at
|
|
6
|
+
* connect, and the claim that makes one of them the harness's own.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { basename } from "node:path";
|
|
10
|
+
import { createIdGenerator, PROTOCOL_VERSION, } from "@chipschat/protocol";
|
|
11
|
+
import { Asks, firstWords } from "./asks.js";
|
|
12
|
+
import { Emitter } from "./events.js";
|
|
13
|
+
import { download, upload } from "./files.js";
|
|
14
|
+
import { createOutbox, RefusedError } from "./outbox.js";
|
|
15
|
+
import { Project } from "./project.js";
|
|
16
|
+
import { Run } from "./run.js";
|
|
17
|
+
import { acceptsFor, Settings } from "./settings.js";
|
|
18
|
+
function agentOf(body) {
|
|
19
|
+
return {
|
|
20
|
+
agentId: String(body["agent_id"]),
|
|
21
|
+
key: String(body["key"]),
|
|
22
|
+
name: String(body["name"]),
|
|
23
|
+
lead: body["lead"] === true,
|
|
24
|
+
state: body["state"] === "archived" ? "archived" : "active",
|
|
25
|
+
spaces: Array.isArray(body["spaces"]) ? body["spaces"] : [],
|
|
26
|
+
room: typeof body["room"] === "string" ? body["room"] : null,
|
|
27
|
+
releasedFrom: typeof body["released_from"] === "string" ? body["released_from"] : null,
|
|
28
|
+
held: typeof body["held"] === "number" ? body["held"] : 0,
|
|
29
|
+
forkedFrom: typeof body["forked_from"] === "string" ? body["forked_from"] : null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export class ConnectError extends Error {
|
|
33
|
+
name = "ConnectError";
|
|
34
|
+
}
|
|
35
|
+
export class Chips {
|
|
36
|
+
options;
|
|
37
|
+
emitter = new Emitter();
|
|
38
|
+
outbox = createOutbox();
|
|
39
|
+
ids = createIdGenerator();
|
|
40
|
+
waiters = new Set();
|
|
41
|
+
activeRuns = new Map();
|
|
42
|
+
/** Pings in flight, by their `ts`, for `subscribed`. */
|
|
43
|
+
pongs = new Map();
|
|
44
|
+
/** Makes each ping's `ts` its own, so two in one millisecond are still told apart. */
|
|
45
|
+
pings = 0;
|
|
46
|
+
/**
|
|
47
|
+
* Events held back, and why: one hold until the harness could attach a handler after connect,
|
|
48
|
+
* and one for each catch-up on asked rooms still reading, so that a room's first words heard
|
|
49
|
+
* live and read from its history are handed over once. Delivered in order when none is left.
|
|
50
|
+
*/
|
|
51
|
+
held = [];
|
|
52
|
+
holds = 1;
|
|
53
|
+
askHandlers = new Emitter();
|
|
54
|
+
asks = new Asks(() => this.me()?.harness_id, {
|
|
55
|
+
listening: () => this.askHandlers.count("asked") > 0,
|
|
56
|
+
hand: (asked) => this.askHandlers.emit("asked", asked),
|
|
57
|
+
});
|
|
58
|
+
socket;
|
|
59
|
+
ready = false;
|
|
60
|
+
closed = false;
|
|
61
|
+
retry = 0;
|
|
62
|
+
timer;
|
|
63
|
+
since;
|
|
64
|
+
connectionState = "connecting";
|
|
65
|
+
helloEvent;
|
|
66
|
+
/** What this harness last said is wrong on its side, said again whenever it reconnects. */
|
|
67
|
+
trouble;
|
|
68
|
+
/** The runs this process started or took up and has not ended, for the store. */
|
|
69
|
+
openRuns = new Map();
|
|
70
|
+
/** Runs a process before this one left open, ended once this one has said hello. */
|
|
71
|
+
orphans = [];
|
|
72
|
+
saving;
|
|
73
|
+
constructor(options) {
|
|
74
|
+
this.options = options;
|
|
75
|
+
const kept = options.store?.load();
|
|
76
|
+
this.since = options.since ?? kept?.since;
|
|
77
|
+
for (const event of kept?.outbox ?? []) {
|
|
78
|
+
this.outbox.add(String(event["idempotency_key"]), event).catch(() => undefined);
|
|
79
|
+
}
|
|
80
|
+
this.orphans = [...(kept?.runs ?? [])];
|
|
81
|
+
this.settings = new Settings((path, body) => this.request("PUT", path, body), () => this.harnessId, options.settings, (path, body) => this.request("POST", path, body));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The harness's copies of what a person changes from an agent's page (ADRs 0134, 0140, 0143,
|
|
85
|
+
* 0146): send each when connected and whenever it changes on the harness's side. Handlers for
|
|
86
|
+
* a person's changes are `connect({ settings })`'s.
|
|
87
|
+
*/
|
|
88
|
+
settings;
|
|
89
|
+
/** Open the socket, say hello, subscribe. Resolves once the server has said who you are. */
|
|
90
|
+
static async connect(options) {
|
|
91
|
+
const chips = new Chips(options);
|
|
92
|
+
await chips.open(true);
|
|
93
|
+
chips.endOrphans();
|
|
94
|
+
// What `since` replayed during the handshake is delivered on the next tick, so handlers
|
|
95
|
+
// attached right after `await connect(...)` hear it rather than missing it.
|
|
96
|
+
setTimeout(() => chips.release(), 0);
|
|
97
|
+
return chips;
|
|
98
|
+
}
|
|
99
|
+
get state() {
|
|
100
|
+
return this.connectionState;
|
|
101
|
+
}
|
|
102
|
+
/** The server's half of the handshake: who this connection is, and what it may do. */
|
|
103
|
+
get hello() {
|
|
104
|
+
if (this.helloEvent === undefined)
|
|
105
|
+
throw new ConnectError("not connected yet");
|
|
106
|
+
return this.helloEvent;
|
|
107
|
+
}
|
|
108
|
+
get harnessId() {
|
|
109
|
+
const me = this.me();
|
|
110
|
+
if (me === undefined)
|
|
111
|
+
throw new ConnectError("the server presented no harness");
|
|
112
|
+
return me.harness_id;
|
|
113
|
+
}
|
|
114
|
+
/** Who this connection is: `hello.harness`, or the list's first from a server before it. */
|
|
115
|
+
me() {
|
|
116
|
+
return this.helloEvent?.harness ?? this.helloEvent?.harnesses[0];
|
|
117
|
+
}
|
|
118
|
+
get workspaceId() {
|
|
119
|
+
return this.hello.workspace_id;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The harness's agents (ADR 0087): `register` makes one, or finds it by your key for it,
|
|
123
|
+
* renames it when the name has changed, and archives it or brings it back as `state` says;
|
|
124
|
+
* `list` reads them with their rooms; `attach` puts one in a room in the place of another of
|
|
125
|
+
* yours there, or of the harness itself. One connection speaks for all of them:
|
|
126
|
+
* `chips.as(agentId)`. A harness that declares `agents` is told them at `hello` too
|
|
127
|
+
* (`chips.hello.agents`).
|
|
128
|
+
*/
|
|
129
|
+
agents = {
|
|
130
|
+
register: async (input) => {
|
|
131
|
+
const answer = await this.request("POST", `/v1/harnesses/${this.harnessId}/agents`, {
|
|
132
|
+
key: input.key,
|
|
133
|
+
name: input.name,
|
|
134
|
+
...(input.lead === undefined ? {} : { lead: input.lead }),
|
|
135
|
+
...(input.state === undefined ? {} : { state: input.state }),
|
|
136
|
+
...(input.forkedFrom === undefined ? {} : { forked_from: input.forkedFrom }),
|
|
137
|
+
...(input.madeFor === undefined ? {} : { made_for: input.madeFor }),
|
|
138
|
+
...(input.spaceId === undefined ? {} : { space_id: input.spaceId }),
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
...agentOf(answer),
|
|
142
|
+
created: answer["created"] === true,
|
|
143
|
+
renamed: answer["renamed"] === true,
|
|
144
|
+
...(typeof answer["attached"] === "boolean" ? { attached: answer["attached"] } : {}),
|
|
145
|
+
...(typeof answer["why"] === "string" ? { why: answer["why"] } : {}),
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
list: async () => {
|
|
149
|
+
const answer = await this.request("GET", `/v1/harnesses/${this.harnessId}/agents`);
|
|
150
|
+
return (Array.isArray(answer["agents"]) ? answer["agents"] : []).map((each) => agentOf(each));
|
|
151
|
+
},
|
|
152
|
+
/**
|
|
153
|
+
* An agent of yours in a room, in the place of `replacing`: the harness's own id, or another
|
|
154
|
+
* of its agents there. What that one said in the room reads as the agent's from then on.
|
|
155
|
+
* `attached` is false when it was there already.
|
|
156
|
+
*/
|
|
157
|
+
attach: async (agentId, where) => {
|
|
158
|
+
const answer = await this.request("POST", `/v1/agents/${agentId}/attach`, {
|
|
159
|
+
space_id: where.spaceId,
|
|
160
|
+
replacing: where.replacing,
|
|
161
|
+
});
|
|
162
|
+
return { attached: answer["attached"] === true };
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Speak as one of this harness's agents (ADR 0087): a post, and a run with everything sent on
|
|
167
|
+
* it, carry the agent as their actor. A harness with one agent in a room need not say which:
|
|
168
|
+
* what it sends as itself is read as that agent there. Where it has several, it must.
|
|
169
|
+
*/
|
|
170
|
+
as(agentId) {
|
|
171
|
+
const actor = { kind: "agent", id: agentId };
|
|
172
|
+
return {
|
|
173
|
+
post: (input) => this.postAs(input, actor),
|
|
174
|
+
runs: { start: (input) => this.startAs(input, actor) },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* A room's project for one of this harness's agents (ADR 0221): its copy in the agent's folder,
|
|
179
|
+
* brought up to date before each turn (`pull`) and sent after it (`push`).
|
|
180
|
+
*/
|
|
181
|
+
project(input) {
|
|
182
|
+
return new Project({ ...input, url: this.options.url, token: this.options.token });
|
|
183
|
+
}
|
|
184
|
+
get autonomy() {
|
|
185
|
+
return this.hello.autonomy;
|
|
186
|
+
}
|
|
187
|
+
get policy() {
|
|
188
|
+
return this.hello.policy;
|
|
189
|
+
}
|
|
190
|
+
/** The highest `seq` heard, to persist for `since` on the next start. */
|
|
191
|
+
get lastSeq() {
|
|
192
|
+
return this.since;
|
|
193
|
+
}
|
|
194
|
+
on(name, handler) {
|
|
195
|
+
return this.emitter.on(name, handler);
|
|
196
|
+
}
|
|
197
|
+
/** Say something in a space. */
|
|
198
|
+
post(input) {
|
|
199
|
+
return this.postAs(input, undefined);
|
|
200
|
+
}
|
|
201
|
+
postAs(input, actor) {
|
|
202
|
+
return this.send("message.post", {
|
|
203
|
+
space_id: input.spaceId,
|
|
204
|
+
...(input.runId === undefined ? {} : { run_id: input.runId }),
|
|
205
|
+
...(actor === undefined ? {} : { actor }),
|
|
206
|
+
}, {
|
|
207
|
+
text: input.text,
|
|
208
|
+
...(input.uploadIds === undefined ? {} : { upload_ids: input.uploadIds }),
|
|
209
|
+
...(input.replaces === undefined ? {} : { replaces: input.replaces }),
|
|
210
|
+
...(input.change === undefined ? {} : { change: input.change }),
|
|
211
|
+
...(input.replyTo === undefined ? {} : { reply_to: input.replyTo }),
|
|
212
|
+
...(input.to === undefined ? {} : { to: input.to }),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* A room a person asked this harness for: their first words in it, once per room. Registering
|
|
217
|
+
* reads the rooms asked while the harness was away (`GET /v1/harnesses/:id/spaces`) and their
|
|
218
|
+
* history, and every reconnect reads them again; a room asked while connected is heard live.
|
|
219
|
+
* The first words are the ask's, and are not heard again as `message.new`; what is said in the
|
|
220
|
+
* room after them is. Only a harness that declares `new_spaces` is ever asked. Make what the
|
|
221
|
+
* room is for, then `claimSpace` it under a key of your own and answer in it.
|
|
222
|
+
*/
|
|
223
|
+
onSpaceAsked(handler) {
|
|
224
|
+
const off = this.askHandlers.on("asked", handler);
|
|
225
|
+
if (this.ready)
|
|
226
|
+
void this.catchUp();
|
|
227
|
+
return off;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Make a room a person asked for this harness's own, under `key`, the harness's name for what
|
|
231
|
+
* it made of it: an agent, a session, a Space. The title names the room unless a person named
|
|
232
|
+
* it first. Asking for the key afterwards finds the room, as for a room the harness opened.
|
|
233
|
+
* A retry with the same key finds the room claimed already; a refusal throws `RefusedError`.
|
|
234
|
+
*/
|
|
235
|
+
async claimSpace(spaceId, key, title) {
|
|
236
|
+
const answer = await this.request("POST", `/v1/harnesses/${this.harnessId}/spaces`, {
|
|
237
|
+
key,
|
|
238
|
+
space_id: spaceId,
|
|
239
|
+
...(title === undefined ? {} : { title }),
|
|
240
|
+
});
|
|
241
|
+
return {
|
|
242
|
+
spaceId: String(answer["space_id"]),
|
|
243
|
+
claimed: answer["claimed"] === true,
|
|
244
|
+
retitled: answer["retitled"] === true,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* A room of this harness's own, under `key`: opened the first time, found again after. Chips puts
|
|
249
|
+
* the agent registered under the same key in it, or the harness's default agent (ADR 0087's
|
|
250
|
+
* addendum of 30 September, point 5). The title names a new room; untitled, it is called what
|
|
251
|
+
* its agent is.
|
|
252
|
+
*/
|
|
253
|
+
async openSpace(key, title) {
|
|
254
|
+
const answer = await this.request("POST", `/v1/harnesses/${this.harnessId}/spaces`, {
|
|
255
|
+
key,
|
|
256
|
+
...(title === undefined ? {} : { title }),
|
|
257
|
+
});
|
|
258
|
+
return String(answer["space_id"]);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* A room's lines, older than `before` (a line's `seq`, or a context's `before`), up to `limit`
|
|
262
|
+
* of them: 20 unless said otherwise, 200 at most (ADR 0088). Only a room this harness is in.
|
|
263
|
+
* What a harness reads to catch up on a room it was just added to, or to look further back
|
|
264
|
+
* than a message's context went.
|
|
265
|
+
*/
|
|
266
|
+
async roomContext(spaceId, page = {}) {
|
|
267
|
+
const query = new URLSearchParams();
|
|
268
|
+
if (page.before !== undefined)
|
|
269
|
+
query.set("before", String(page.before));
|
|
270
|
+
if (page.limit !== undefined)
|
|
271
|
+
query.set("limit", String(page.limit));
|
|
272
|
+
const search = query.toString();
|
|
273
|
+
const path = `/v1/spaces/${spaceId}/context${search === "" ? "" : `?${search}`}`;
|
|
274
|
+
const answer = await this.request("GET", path);
|
|
275
|
+
return answer;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Send a file, for a post's `uploadIds`: in pieces when it is large, picking up where it got to
|
|
279
|
+
* after a piece is lost (ADR 0052). Resolves with what the server stored.
|
|
280
|
+
*/
|
|
281
|
+
upload(input) {
|
|
282
|
+
return upload((method, path, body) => this.request(method, path, body), input);
|
|
283
|
+
}
|
|
284
|
+
/** Send a file from disk, under its own name unless given another. */
|
|
285
|
+
async uploadFile(path, filename) {
|
|
286
|
+
const bytes = new Uint8Array(await readFile(path));
|
|
287
|
+
return await this.upload({ bytes, filename: filename ?? basename(path) });
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* An attachment's bytes: a message's, or a room file's. Its URL is bound to this harness's
|
|
291
|
+
* token, so it is fetched with it.
|
|
292
|
+
*/
|
|
293
|
+
download(attachment) {
|
|
294
|
+
return download(this.options.fetch ?? fetch, this.options.token, attachment);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Say what is wrong on this harness's side: the part Chips cannot see, such as a back end it
|
|
298
|
+
* cannot reach (ADR 0083). People see the agent marked, and these words under a message it
|
|
299
|
+
* hasn't picked up. One line, up to 200 characters. It holds until `reportOk`, or until the
|
|
300
|
+
* connection closes; the SDK says it again whenever it reconnects, so say it once.
|
|
301
|
+
*/
|
|
302
|
+
async reportTrouble(reason) {
|
|
303
|
+
this.trouble = reason;
|
|
304
|
+
await this.status({ state: "trouble", reason });
|
|
305
|
+
}
|
|
306
|
+
/** Say that nothing is wrong on this harness's side any more (ADR 0083). */
|
|
307
|
+
async reportOk() {
|
|
308
|
+
this.trouble = undefined;
|
|
309
|
+
await this.status({ state: "ok" });
|
|
310
|
+
}
|
|
311
|
+
async status(body) {
|
|
312
|
+
await this.request("PUT", `/v1/harnesses/${this.harnessId}/status`, body);
|
|
313
|
+
}
|
|
314
|
+
/** The lane side of the connection, for the `Lane` a `run.task()` hands back. */
|
|
315
|
+
lanes = {
|
|
316
|
+
hear: (laneId, handler) => this.emitter.on("lane.updated", (event) => {
|
|
317
|
+
if (event.lane_id === laneId)
|
|
318
|
+
handler(event);
|
|
319
|
+
}),
|
|
320
|
+
accept: (runId, spaceId, laneId) => this.runFor(runId, spaceId, laneId),
|
|
321
|
+
};
|
|
322
|
+
/** A `Run` over the wire for this run id, registered so what people do to it reaches it. */
|
|
323
|
+
runFor(runId, spaceId, laneId, actor) {
|
|
324
|
+
const known = this.activeRuns.get(runId);
|
|
325
|
+
if (known !== undefined)
|
|
326
|
+
return known;
|
|
327
|
+
// Everything sent on a run an agent started is that agent's (ADR 0087).
|
|
328
|
+
const envelope = {
|
|
329
|
+
space_id: spaceId,
|
|
330
|
+
run_id: runId,
|
|
331
|
+
...(actor === undefined ? {} : { actor }),
|
|
332
|
+
};
|
|
333
|
+
const run = new Run(runId, spaceId, {
|
|
334
|
+
send: (type, payload, key) => this.send(type, envelope, payload, key),
|
|
335
|
+
newId: (kind) => this.ids.next(kind),
|
|
336
|
+
waitFor: (match) => this.waitFor(match),
|
|
337
|
+
lanes: this.lanes,
|
|
338
|
+
}, laneId);
|
|
339
|
+
this.activeRuns.set(runId, run);
|
|
340
|
+
return run;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Runs: `chips.runs.start({...})` opens one and resolves once the server has accepted it;
|
|
344
|
+
* `chips.runs.accept(requested)` takes up the run a lead routed here as a lane (M3.5) — the
|
|
345
|
+
* run is already seeded, so nothing is sent — and everything sent on it is the lane's.
|
|
346
|
+
*/
|
|
347
|
+
runs = {
|
|
348
|
+
start: (input) => this.startAs(input, undefined),
|
|
349
|
+
accept: (requested) => {
|
|
350
|
+
if (requested.run_id === undefined || requested.space_id === undefined) {
|
|
351
|
+
throw new ConnectError("a run.requested names the lane's run and its home");
|
|
352
|
+
}
|
|
353
|
+
this.opened({ runId: requested.run_id, spaceId: requested.space_id });
|
|
354
|
+
return this.runFor(requested.run_id, requested.space_id, requested.lane_id);
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
async startAs(input, actor) {
|
|
358
|
+
{
|
|
359
|
+
const runId = input.runId ?? this.ids.next("run");
|
|
360
|
+
const envelope = {
|
|
361
|
+
space_id: input.spaceId,
|
|
362
|
+
run_id: runId,
|
|
363
|
+
...(actor === undefined ? {} : { actor }),
|
|
364
|
+
};
|
|
365
|
+
const run = this.runFor(runId, input.spaceId, undefined, actor);
|
|
366
|
+
this.opened({ runId, spaceId: input.spaceId, agentId: actor?.id });
|
|
367
|
+
try {
|
|
368
|
+
await this.send("run.started", envelope, {
|
|
369
|
+
goal: input.goal,
|
|
370
|
+
...(input.plan === undefined
|
|
371
|
+
? {}
|
|
372
|
+
: { plan: input.plan.map((step) => ({ step_id: step.stepId, title: step.title })) }),
|
|
373
|
+
...(input.sessionRef === undefined ? {} : { session_ref: input.sessionRef }),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
this.activeRuns.delete(runId);
|
|
378
|
+
this.ended(runId);
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
return run;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** Send any event. The envelope's actor, ts and workspace are the server's to fill in. */
|
|
385
|
+
send(type, envelope, payload, key = `${this.options.client?.name ?? "harness"}-${this.ids.next("event").slice(4)}`) {
|
|
386
|
+
if (this.closed)
|
|
387
|
+
return Promise.reject(new RefusedError("closed", "the connection is closed", key));
|
|
388
|
+
const event = { type, ...envelope, idempotency_key: key, ...payload };
|
|
389
|
+
const answer = this.outbox.add(key, event);
|
|
390
|
+
if (type === "run.completed" || type === "run.failed")
|
|
391
|
+
this.ended(String(envelope["run_id"]));
|
|
392
|
+
this.save();
|
|
393
|
+
if (this.ready && this.socket !== undefined) {
|
|
394
|
+
this.socket.send(JSON.stringify({ frame: "event", event }));
|
|
395
|
+
}
|
|
396
|
+
return answer;
|
|
397
|
+
}
|
|
398
|
+
// ------------------------------------------------------------------ what survives a restart
|
|
399
|
+
opened(run) {
|
|
400
|
+
this.openRuns.set(run.runId, run);
|
|
401
|
+
this.save();
|
|
402
|
+
}
|
|
403
|
+
ended(runId) {
|
|
404
|
+
if (this.openRuns.delete(runId))
|
|
405
|
+
this.save();
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* End the runs a process before this one left open: its work died with it, and a run nobody
|
|
409
|
+
* ends keeps its thinking bubble until the harness's next one. Refused for one Chips had ended
|
|
410
|
+
* already, which is just as well.
|
|
411
|
+
*/
|
|
412
|
+
endOrphans() {
|
|
413
|
+
const orphans = this.orphans.filter((run) => !this.openRuns.has(run.runId));
|
|
414
|
+
this.orphans = [];
|
|
415
|
+
for (const run of orphans) {
|
|
416
|
+
this.send("run.failed", {
|
|
417
|
+
space_id: run.spaceId,
|
|
418
|
+
run_id: run.runId,
|
|
419
|
+
...(run.agentId === undefined ? {} : { actor: { kind: "agent", id: run.agentId } }),
|
|
420
|
+
}, { error: "The harness restarted before this run ended.", retryable: true }, `restarted-${run.runId}`).catch(() => undefined);
|
|
421
|
+
}
|
|
422
|
+
this.save();
|
|
423
|
+
}
|
|
424
|
+
/** Keep the state soon, once for however many changes come together. */
|
|
425
|
+
save() {
|
|
426
|
+
if (this.options.store === undefined || this.saving !== undefined)
|
|
427
|
+
return;
|
|
428
|
+
this.saving = setTimeout(() => {
|
|
429
|
+
this.saving = undefined;
|
|
430
|
+
this.saveNow();
|
|
431
|
+
}, 50);
|
|
432
|
+
this.saving.unref?.();
|
|
433
|
+
}
|
|
434
|
+
saveNow() {
|
|
435
|
+
this.options.store?.save({
|
|
436
|
+
...(this.since === undefined ? {} : { since: this.since }),
|
|
437
|
+
outbox: this.outbox.unanswered(),
|
|
438
|
+
runs: [...this.openRuns.values(), ...this.orphans],
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/** Resolve with the first event that matches, now or later. */
|
|
442
|
+
waitFor(match) {
|
|
443
|
+
return new Promise((resolve) => {
|
|
444
|
+
this.waiters.add({ match, resolve: resolve });
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
close() {
|
|
448
|
+
this.closed = true;
|
|
449
|
+
clearTimeout(this.timer);
|
|
450
|
+
// What was unanswered is kept for the next start before the outbox lets it go.
|
|
451
|
+
clearTimeout(this.saving);
|
|
452
|
+
this.saving = undefined;
|
|
453
|
+
this.saveNow();
|
|
454
|
+
this.socket?.close();
|
|
455
|
+
this.outbox.abandon("the connection was closed");
|
|
456
|
+
this.setState("closed");
|
|
457
|
+
}
|
|
458
|
+
// ------------------------------------------------------------------ the socket
|
|
459
|
+
/**
|
|
460
|
+
* The socket's URL, with a single-use ticket in it (SEC-22). The token goes to
|
|
461
|
+
* `POST /v1/socket-tickets` in a header, as every other call sends it; what comes back is
|
|
462
|
+
* spent on the upgrade and worth nothing afterwards. Node's `WebSocket` sets no headers on
|
|
463
|
+
* the upgrade, and a token in the URL would be a durable credential in every proxy log the
|
|
464
|
+
* URL crosses.
|
|
465
|
+
*/
|
|
466
|
+
async socketUrl() {
|
|
467
|
+
const base = this.options.url.replace(/\/$/, "").replace(/^http/, "ws");
|
|
468
|
+
const { ticket } = await this.request("POST", "/v1/socket-tickets");
|
|
469
|
+
return `${base}/v1/ws?ticket=${encodeURIComponent(String(ticket))}`;
|
|
470
|
+
}
|
|
471
|
+
open(first) {
|
|
472
|
+
return new Promise((resolve, reject) => {
|
|
473
|
+
this.setState("connecting");
|
|
474
|
+
// Before a socket exists, a failure tells the first caller. A reconnect has nobody to
|
|
475
|
+
// tell, so it tries again: a server still down on this attempt may be up by the next.
|
|
476
|
+
const failed = (what, error) => {
|
|
477
|
+
if (first)
|
|
478
|
+
reject(new ConnectError(`${what}: ${describe(error)}`));
|
|
479
|
+
else
|
|
480
|
+
this.scheduleReconnect();
|
|
481
|
+
};
|
|
482
|
+
void this.socketUrl().then((url) => {
|
|
483
|
+
let socket;
|
|
484
|
+
try {
|
|
485
|
+
socket = (this.options.createSocket ?? openSocket)(url);
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
failed("could not open a socket", error);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
this.socket = socket;
|
|
492
|
+
this.listen(socket, {
|
|
493
|
+
settled: (hello) => this.settled(socket, hello, first, resolve, reject),
|
|
494
|
+
});
|
|
495
|
+
}, (error) => failed("could not get a socket ticket", error));
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/** The handshake's outcome: the hello, a refusal, or the socket gone before either. */
|
|
499
|
+
settled(socket, hello, first, resolve, reject) {
|
|
500
|
+
if (hello === undefined) {
|
|
501
|
+
if (first)
|
|
502
|
+
reject(new ConnectError("the server closed the socket before saying hello"));
|
|
503
|
+
else
|
|
504
|
+
this.scheduleReconnect();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (hello instanceof ConnectError) {
|
|
508
|
+
if (first)
|
|
509
|
+
reject(hello);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
this.onHello(socket, hello);
|
|
513
|
+
void this.subscribed(socket).then(resolve);
|
|
514
|
+
}
|
|
515
|
+
/** The socket's four events. `settled` fires once: with the hello, a refusal, or nothing. */
|
|
516
|
+
listen(socket, on) {
|
|
517
|
+
let settled = false;
|
|
518
|
+
const settle = (outcome) => {
|
|
519
|
+
if (settled)
|
|
520
|
+
return;
|
|
521
|
+
settled = true;
|
|
522
|
+
on.settled(outcome);
|
|
523
|
+
};
|
|
524
|
+
socket.addEventListener("open", () => {
|
|
525
|
+
socket.send(JSON.stringify({
|
|
526
|
+
frame: "hello",
|
|
527
|
+
protocol: { min: PROTOCOL_VERSION, max: PROTOCOL_VERSION },
|
|
528
|
+
client: this.options.client ?? { name: "chips-harness", version: "0.0.0" },
|
|
529
|
+
...(this.options.context === undefined ? {} : { context: this.options.context }),
|
|
530
|
+
...this.accepts(),
|
|
531
|
+
}));
|
|
532
|
+
});
|
|
533
|
+
socket.addEventListener("message", (message) => {
|
|
534
|
+
const frame = JSON.parse(String(message.data));
|
|
535
|
+
if (settled) {
|
|
536
|
+
this.handleFrame(frame);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const outcome = this.handshakeOutcome(frame);
|
|
540
|
+
if (outcome !== undefined)
|
|
541
|
+
settle(outcome);
|
|
542
|
+
else
|
|
543
|
+
this.handleFrame(frame);
|
|
544
|
+
});
|
|
545
|
+
let gone = false;
|
|
546
|
+
const onGone = () => {
|
|
547
|
+
if (gone)
|
|
548
|
+
return;
|
|
549
|
+
gone = true;
|
|
550
|
+
this.ready = false;
|
|
551
|
+
const wasSettled = settled;
|
|
552
|
+
settle(undefined);
|
|
553
|
+
if (!wasSettled)
|
|
554
|
+
return;
|
|
555
|
+
this.setState("closed");
|
|
556
|
+
this.scheduleReconnect();
|
|
557
|
+
};
|
|
558
|
+
socket.addEventListener("close", onGone);
|
|
559
|
+
// An error is the socket gone, not a prelude to `close`: Node 22's WebSocket (undici 6) fires
|
|
560
|
+
// only `error` when the server refuses the handshake, so waiting for `close` hangs there. Where
|
|
561
|
+
// `close` does follow, it finds the work done.
|
|
562
|
+
socket.addEventListener("error", onGone);
|
|
563
|
+
}
|
|
564
|
+
/** The frames beyond the base set this connection takes: a restart, and the settings it handles. */
|
|
565
|
+
accepts() {
|
|
566
|
+
const accepts = [
|
|
567
|
+
...(this.options.onRestart === undefined ? [] : ["restart"]),
|
|
568
|
+
...acceptsFor(this.options.settings),
|
|
569
|
+
];
|
|
570
|
+
return accepts.length === 0 ? {} : { accepts };
|
|
571
|
+
}
|
|
572
|
+
/** During the handshake: the server's hello, or its refusal. Anything else is a frame. */
|
|
573
|
+
handshakeOutcome(frame) {
|
|
574
|
+
if (frame["frame"] === "event") {
|
|
575
|
+
const event = frame["event"];
|
|
576
|
+
return event["type"] === "hello" ? event : undefined;
|
|
577
|
+
}
|
|
578
|
+
if (frame["frame"] === "error") {
|
|
579
|
+
return new ConnectError(`${String(frame["code"])}: ${String(frame["message"])}`);
|
|
580
|
+
}
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
onHello(socket, hello) {
|
|
584
|
+
this.helloEvent = hello;
|
|
585
|
+
this.emitter.emit("hello", hello);
|
|
586
|
+
socket.send(JSON.stringify({
|
|
587
|
+
frame: "subscribe",
|
|
588
|
+
workspace_id: hello.workspace_id,
|
|
589
|
+
...(this.since === undefined ? {} : { since: this.since }),
|
|
590
|
+
}));
|
|
591
|
+
// Whatever the last socket did not get an answer for goes first: the server acks a resend
|
|
592
|
+
// it already applied as a duplicate, so sending twice is safe by design.
|
|
593
|
+
for (const event of this.outbox.unanswered()) {
|
|
594
|
+
socket.send(JSON.stringify({ frame: "event", event }));
|
|
595
|
+
}
|
|
596
|
+
this.ready = true;
|
|
597
|
+
this.retry = 0;
|
|
598
|
+
this.setState("open");
|
|
599
|
+
// A reconnect reads again what was asked while the socket was down.
|
|
600
|
+
if (this.askHandlers.count("asked") > 0)
|
|
601
|
+
void this.catchUp();
|
|
602
|
+
// And says again what is wrong, which ended when the last socket closed (ADR 0083).
|
|
603
|
+
const trouble = this.trouble;
|
|
604
|
+
if (trouble !== undefined) {
|
|
605
|
+
void this.status({ state: "trouble", reason: trouble }).catch(() => undefined);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* The server answers frames in order, so a pong to a ping sent after `subscribe` means the
|
|
610
|
+
* subscription is live: an event appended from here on is delivered, not missed as history.
|
|
611
|
+
*/
|
|
612
|
+
subscribed(socket) {
|
|
613
|
+
// An integer: the frame's schema says so, and a `ts` with a fraction is refused rather than
|
|
614
|
+
// answered — which cost every connect the whole two-second wait until M3.5 found it.
|
|
615
|
+
this.pings += 1;
|
|
616
|
+
const ts = Date.now() + this.pings;
|
|
617
|
+
return new Promise((resolve) => {
|
|
618
|
+
const timer = setTimeout(() => {
|
|
619
|
+
this.pongs.delete(ts);
|
|
620
|
+
resolve();
|
|
621
|
+
}, 2000);
|
|
622
|
+
this.pongs.set(ts, () => {
|
|
623
|
+
clearTimeout(timer);
|
|
624
|
+
this.pongs.delete(ts);
|
|
625
|
+
resolve();
|
|
626
|
+
});
|
|
627
|
+
socket.send(JSON.stringify({ frame: "ping", ts }));
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
/** Hand a person's ask to the harness's own restart; what it does after is its business. */
|
|
631
|
+
restart(frame) {
|
|
632
|
+
const handler = this.options.onRestart;
|
|
633
|
+
if (handler === undefined)
|
|
634
|
+
return;
|
|
635
|
+
const ask = { askedBy: String(frame["asked_by"]), askedAt: String(frame["asked_at"]) };
|
|
636
|
+
void Promise.resolve()
|
|
637
|
+
.then(() => handler(ask))
|
|
638
|
+
.catch(() => undefined);
|
|
639
|
+
}
|
|
640
|
+
handleFrame(frame) {
|
|
641
|
+
switch (frame["frame"]) {
|
|
642
|
+
case "ping":
|
|
643
|
+
this.socket?.send(JSON.stringify({ frame: "pong", ts: frame["ts"] }));
|
|
644
|
+
return;
|
|
645
|
+
case "pong":
|
|
646
|
+
this.pongs.get(Number(frame["ts"]))?.();
|
|
647
|
+
return;
|
|
648
|
+
case "event":
|
|
649
|
+
this.onEvent(frame["event"]);
|
|
650
|
+
return;
|
|
651
|
+
case "ack":
|
|
652
|
+
this.outbox.acked({
|
|
653
|
+
idempotencyKey: String(frame["idempotency_key"]),
|
|
654
|
+
seq: Number(frame["seq"]),
|
|
655
|
+
duplicate: frame["duplicate"] === true,
|
|
656
|
+
...laneOf(frame["lane"]),
|
|
657
|
+
...heldOf(frame),
|
|
658
|
+
});
|
|
659
|
+
this.save();
|
|
660
|
+
return;
|
|
661
|
+
case "error": {
|
|
662
|
+
const key = frame["idempotency_key"];
|
|
663
|
+
this.outbox.refused(new RefusedError(String(frame["code"]), String(frame["message"]), typeof key === "string" ? key : undefined));
|
|
664
|
+
this.save();
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
case "restart":
|
|
668
|
+
this.restart(frame);
|
|
669
|
+
return;
|
|
670
|
+
case "receipt":
|
|
671
|
+
this.emitter.emit("receipt", {
|
|
672
|
+
idempotencyKey: String(frame["idempotency_key"]),
|
|
673
|
+
disposition: frame["disposition"] === "steered" ? "steered" : "queued",
|
|
674
|
+
});
|
|
675
|
+
return;
|
|
676
|
+
default:
|
|
677
|
+
this.settings.hear(frame);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/** Let one hold go; with none left, deliver what was held, in order. */
|
|
682
|
+
release() {
|
|
683
|
+
this.holds -= 1;
|
|
684
|
+
if (this.holds > 0)
|
|
685
|
+
return;
|
|
686
|
+
const held = this.held;
|
|
687
|
+
this.held = [];
|
|
688
|
+
for (const event of held)
|
|
689
|
+
this.onEvent(event);
|
|
690
|
+
}
|
|
691
|
+
onEvent(event) {
|
|
692
|
+
if (this.holds > 0) {
|
|
693
|
+
this.held.push(event);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const seq = event["seq"];
|
|
697
|
+
if (typeof seq === "number") {
|
|
698
|
+
this.since = Math.max(this.since ?? 0, seq);
|
|
699
|
+
this.save();
|
|
700
|
+
}
|
|
701
|
+
if (event["type"] === "run.cancel" && typeof event["run_id"] === "string") {
|
|
702
|
+
this.ended(event["run_id"]);
|
|
703
|
+
}
|
|
704
|
+
this.settleWaiters(event);
|
|
705
|
+
this.emitter.emit("event", event);
|
|
706
|
+
this.routeToRuns(event);
|
|
707
|
+
const asked = this.asks.heard(event);
|
|
708
|
+
const type = event["type"];
|
|
709
|
+
if (typeof type === "string" && type in this.named && !asked) {
|
|
710
|
+
this.emitter.emit(type, event);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// ------------------------------------------------------------------ over HTTP
|
|
714
|
+
/**
|
|
715
|
+
* The rooms asked of this harness and not yet claimed, each with its history, handed over
|
|
716
|
+
* when their first words are there. Live events wait meanwhile. A failure is left for the next
|
|
717
|
+
* connect: a room stays asked until it is claimed.
|
|
718
|
+
*/
|
|
719
|
+
async catchUp() {
|
|
720
|
+
this.holds += 1;
|
|
721
|
+
try {
|
|
722
|
+
const listing = await this.request("GET", `/v1/harnesses/${this.harnessId}/spaces`);
|
|
723
|
+
const asked = Array.isArray(listing["asked"]) ? listing["asked"] : [];
|
|
724
|
+
for (const room of asked) {
|
|
725
|
+
if (this.asks.handed(room.space_id))
|
|
726
|
+
continue;
|
|
727
|
+
this.asks.caughtUp(room, await this.firstWordsIn(room.space_id));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
// Nothing to hand over this time; the room is listed again at the next connect.
|
|
732
|
+
}
|
|
733
|
+
finally {
|
|
734
|
+
this.release();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/** What a person first said in a room, read from its beginning a page at a time. */
|
|
738
|
+
async firstWordsIn(spaceId) {
|
|
739
|
+
let since = 0;
|
|
740
|
+
for (;;) {
|
|
741
|
+
const page = await this.request("GET", `/v1/events?space_id=${encodeURIComponent(spaceId)}&since=${since}&limit=100`);
|
|
742
|
+
const first = firstWords(page["events"] ?? []);
|
|
743
|
+
const next = page["next_cursor"];
|
|
744
|
+
if (first !== undefined || typeof next !== "number")
|
|
745
|
+
return first;
|
|
746
|
+
since = next;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* One call to chipsd with the harness's token: JSON, or a file's bytes. A refusal throws with
|
|
751
|
+
* the server's words.
|
|
752
|
+
*/
|
|
753
|
+
/**
|
|
754
|
+
* Any route, with this harness's token: for one the SDK has no call for yet. The answer's JSON,
|
|
755
|
+
* or `RefusedError` with the server's code and words.
|
|
756
|
+
*/
|
|
757
|
+
async request(method, path, body) {
|
|
758
|
+
const base = this.options.url.replace(/\/$/, "").replace(/^ws/, "http");
|
|
759
|
+
const bytes = body instanceof Uint8Array;
|
|
760
|
+
const response = await (this.options.fetch ?? fetch)(`${base}${path}`, {
|
|
761
|
+
method,
|
|
762
|
+
headers: {
|
|
763
|
+
authorization: `Bearer ${this.options.token}`,
|
|
764
|
+
...(body === undefined
|
|
765
|
+
? {}
|
|
766
|
+
: { "content-type": bytes ? "application/octet-stream" : "application/json" }),
|
|
767
|
+
},
|
|
768
|
+
...(body === undefined
|
|
769
|
+
? {}
|
|
770
|
+
: { body: bytes ? body : JSON.stringify(body) }),
|
|
771
|
+
});
|
|
772
|
+
const answer = (await response.json().catch(() => ({})));
|
|
773
|
+
if (response.ok)
|
|
774
|
+
return answer;
|
|
775
|
+
const error = (answer["error"] ?? {});
|
|
776
|
+
throw new RefusedError(typeof error.code === "string" ? error.code : String(response.status), typeof error.message === "string" ? error.message : `${method} ${path}: ${response.status}`);
|
|
777
|
+
}
|
|
778
|
+
settleWaiters(event) {
|
|
779
|
+
for (const waiter of [...this.waiters]) {
|
|
780
|
+
const value = waiter.match(event);
|
|
781
|
+
if (value !== undefined) {
|
|
782
|
+
this.waiters.delete(waiter);
|
|
783
|
+
waiter.resolve(value);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* An event into a run reaches that run; a message for this harness in a run's room reaches the
|
|
789
|
+
* run too. One it overheard does not: it was for somebody else (ADR 0081).
|
|
790
|
+
*/
|
|
791
|
+
routeToRuns(event) {
|
|
792
|
+
if (event["type"] === "message.overheard")
|
|
793
|
+
return;
|
|
794
|
+
const runId = event["run_id"];
|
|
795
|
+
if (typeof runId === "string") {
|
|
796
|
+
this.activeRuns.get(runId)?.hear(event);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (event["type"] !== "message.new")
|
|
800
|
+
return;
|
|
801
|
+
for (const run of this.activeRuns.values()) {
|
|
802
|
+
if (run.spaceId === event["space_id"] && !run.over)
|
|
803
|
+
run.hear(event);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
named = {
|
|
807
|
+
"message.new": true,
|
|
808
|
+
"message.overheard": true,
|
|
809
|
+
"approval.resolved": true,
|
|
810
|
+
"question.answered": true,
|
|
811
|
+
"run.steer": true,
|
|
812
|
+
"run.pause": true,
|
|
813
|
+
"run.resume": true,
|
|
814
|
+
"run.cancel": true,
|
|
815
|
+
"run.requested": true,
|
|
816
|
+
"lane.updated": true,
|
|
817
|
+
"policy.updated": true,
|
|
818
|
+
"budget.warning": true,
|
|
819
|
+
"agent.attached": true,
|
|
820
|
+
"agent.released": true,
|
|
821
|
+
"agent.fork": true,
|
|
822
|
+
"agent.asked": true,
|
|
823
|
+
feedback: true,
|
|
824
|
+
};
|
|
825
|
+
setState(state) {
|
|
826
|
+
if (this.connectionState === state)
|
|
827
|
+
return;
|
|
828
|
+
this.connectionState = state;
|
|
829
|
+
this.emitter.emit("state", state);
|
|
830
|
+
}
|
|
831
|
+
scheduleReconnect() {
|
|
832
|
+
if (this.closed || this.options.reconnect === false) {
|
|
833
|
+
if (!this.closed)
|
|
834
|
+
this.outbox.abandon("the connection dropped and reconnect is off");
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
this.retry += 1;
|
|
838
|
+
clearTimeout(this.timer);
|
|
839
|
+
this.timer = setTimeout(() => {
|
|
840
|
+
this.timer = undefined;
|
|
841
|
+
void this.open(false).catch(() => undefined);
|
|
842
|
+
}, Math.min(500 * this.retry, 5000));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
/** The lane an ack names, in the SDK's words, or nothing when the frame names none. */
|
|
846
|
+
/** Node's own `WebSocket`, as a `SocketLike`. */
|
|
847
|
+
function openSocket(url) {
|
|
848
|
+
return new WebSocket(url);
|
|
849
|
+
}
|
|
850
|
+
/** A refusal by its code and words, anything else as it says itself. */
|
|
851
|
+
function describe(error) {
|
|
852
|
+
return error instanceof RefusedError ? `${error.code}: ${error.message}` : String(error);
|
|
853
|
+
}
|
|
854
|
+
function heldOf(frame) {
|
|
855
|
+
if (frame["held"] !== true)
|
|
856
|
+
return {};
|
|
857
|
+
const exchange = frame["exchange"];
|
|
858
|
+
if (typeof exchange?.count !== "number")
|
|
859
|
+
return { held: true };
|
|
860
|
+
return {
|
|
861
|
+
held: true,
|
|
862
|
+
exchange: {
|
|
863
|
+
count: exchange.count,
|
|
864
|
+
...(typeof exchange.allowance === "number" ? { allowance: exchange.allowance } : {}),
|
|
865
|
+
},
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function laneOf(raw) {
|
|
869
|
+
if (typeof raw !== "object" || raw === null)
|
|
870
|
+
return {};
|
|
871
|
+
const lane = raw;
|
|
872
|
+
if (typeof lane["lane_id"] !== "string" ||
|
|
873
|
+
typeof lane["run_id"] !== "string" ||
|
|
874
|
+
typeof lane["space_id"] !== "string") {
|
|
875
|
+
return {};
|
|
876
|
+
}
|
|
877
|
+
return { lane: { laneId: lane["lane_id"], runId: lane["run_id"], spaceId: lane["space_id"] } };
|
|
878
|
+
}
|
|
879
|
+
/** Connect to chipsd. `const chips = await connect({ url, token })`. */
|
|
880
|
+
export function connect(options) {
|
|
881
|
+
return Chips.connect(options);
|
|
882
|
+
}
|
|
883
|
+
//# sourceMappingURL=connect.js.map
|