@zetaloop/chappie 0.2.0 → 0.3.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/src/ipc.ts CHANGED
@@ -20,13 +20,15 @@ import type {
20
20
  import type { DeliveryRecord } from "./delivery.ts";
21
21
  import type { ResourceData } from "./resources.ts";
22
22
 
23
+ const defaultPort = 24274;
24
+
23
25
  export type SessionStatus = "idle" | "ready" | "executing";
24
26
 
25
27
  export interface SessionDescription {
26
28
  id: string;
27
29
  cwd: string;
30
+ device: string;
28
31
  name?: string;
29
- sessionFile?: string;
30
32
  status: SessionStatus;
31
33
  }
32
34
 
@@ -43,10 +45,15 @@ export interface SessionInput {
43
45
  }
44
46
 
45
47
  export type SessionResult =
46
- | { inspection: SessionInspection; inputs: SessionInput[] }
47
- | { message: AssistantMessage; inputs: SessionInput[] }
48
+ | {
49
+ inspection: SessionInspection;
50
+ inputs: SessionInput[];
51
+ globalAgents?: string;
52
+ }
53
+ | { message: AssistantMessage; cwd: string; inputs: SessionInput[] }
48
54
  | {
49
55
  message: AssistantMessage;
56
+ cwd: string;
50
57
  toolResults: ToolResultMessage[];
51
58
  inputs: SessionInput[];
52
59
  }
@@ -62,6 +69,7 @@ export type SessionMessage =
62
69
  export type BrokerMessage =
63
70
  | { type: "synced"; id: number; sessionId: string }
64
71
  | { type: "stored"; id: string }
72
+ | { type: "notice"; sessionId: string; message: string }
65
73
  | { type: "inspect"; id: number; sessionId: string }
66
74
  | {
67
75
  type: "chat";
@@ -77,7 +85,7 @@ export type BrokerMessage =
77
85
  sessionId: string;
78
86
  calls: ToolCall[];
79
87
  }
80
- | { type: "cancel"; id: number; sessionId: string }
88
+ | { type: "cancel"; id: number; sessionId: string; reason: string }
81
89
  | { type: "readResource"; id: number; sessionId: string; uri: string }
82
90
  | { type: "ackInputs"; sessionId: string; ids: string[] };
83
91
 
@@ -171,7 +179,7 @@ export class IpcServer {
171
179
  peer: JsonLinePeer<SessionMessage, BrokerMessage>,
172
180
  ) => void;
173
181
  readonly #peers = new Set<JsonLinePeer<SessionMessage, BrokerMessage>>();
174
- #server: Server | undefined;
182
+ readonly #servers = new Set<Server>();
175
183
 
176
184
  constructor(
177
185
  agentDir: string,
@@ -186,10 +194,34 @@ export class IpcServer {
186
194
  this.#onClose = onClose;
187
195
  }
188
196
 
189
- async start(): Promise<void> {
190
- if (this.#server) return;
197
+ async start(network: boolean | number = false): Promise<void> {
198
+ if (this.#servers.size > 0) return;
191
199
  if (process.platform !== "win32") await prepareUnixSocket(this.#endpoint);
192
- const server = createServer((socket) => {
200
+ try {
201
+ const local = this.#createServer();
202
+ await listenServer(local, this.#endpoint);
203
+ this.#servers.add(local);
204
+ if (!network) return;
205
+
206
+ const remote = this.#createServer();
207
+ await listenServer(remote, network === true ? defaultPort : network);
208
+ this.#servers.add(remote);
209
+ } catch (error) {
210
+ await this.close();
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ async close(): Promise<void> {
216
+ for (const peer of this.#peers) peer.close();
217
+ this.#peers.clear();
218
+ const servers = [...this.#servers];
219
+ this.#servers.clear();
220
+ await Promise.all(servers.map(closeServer));
221
+ }
222
+
223
+ #createServer(): Server {
224
+ return createServer((socket) => {
193
225
  let peer: JsonLinePeer<SessionMessage, BrokerMessage>;
194
226
  peer = new JsonLinePeer(
195
227
  socket,
@@ -201,25 +233,6 @@ export class IpcServer {
201
233
  );
202
234
  this.#peers.add(peer);
203
235
  });
204
- await new Promise<void>((resolveListen, rejectListen) => {
205
- server.once("error", rejectListen);
206
- server.listen(this.#endpoint, () => {
207
- server.off("error", rejectListen);
208
- resolveListen();
209
- });
210
- });
211
- this.#server = server;
212
- }
213
-
214
- async close(): Promise<void> {
215
- for (const peer of this.#peers) peer.close();
216
- this.#peers.clear();
217
- const server = this.#server;
218
- this.#server = undefined;
219
- if (!server) return;
220
- await new Promise<void>((resolveClose, rejectClose) =>
221
- server.close((error) => (error ? rejectClose(error) : resolveClose())),
222
- );
223
236
  }
224
237
  }
225
238
 
@@ -231,14 +244,21 @@ interface ConnectionCallbacks {
231
244
 
232
245
  export class IpcClient {
233
246
  readonly #endpoint: string;
247
+ readonly #remote: string | undefined;
234
248
  readonly #callbacks: ConnectionCallbacks;
235
249
  #peer: JsonLinePeer<BrokerMessage, SessionMessage> | undefined;
236
250
  #opening: Promise<void> | undefined;
251
+ #controller: AbortController | undefined;
237
252
  #retry: NodeJS.Timeout | undefined;
238
253
  #closed = false;
239
254
 
240
- constructor(agentDir: string, callbacks: ConnectionCallbacks) {
255
+ constructor(
256
+ agentDir: string,
257
+ remote: string | undefined,
258
+ callbacks: ConnectionCallbacks,
259
+ ) {
241
260
  this.#endpoint = ipcEndpoint(agentDir);
261
+ this.#remote = remote;
242
262
  this.#callbacks = callbacks;
243
263
  }
244
264
 
@@ -257,8 +277,11 @@ export class IpcClient {
257
277
  if (this.#opening) return this.#opening;
258
278
  clearTimeout(this.#retry);
259
279
  this.#retry = undefined;
260
- this.#opening = this.#open().finally(() => {
280
+ const controller = new AbortController();
281
+ this.#controller = controller;
282
+ this.#opening = this.#open(controller.signal).finally(() => {
261
283
  this.#opening = undefined;
284
+ if (this.#controller === controller) this.#controller = undefined;
262
285
  });
263
286
  return this.#opening;
264
287
  }
@@ -274,24 +297,25 @@ export class IpcClient {
274
297
  this.#closed = true;
275
298
  clearTimeout(this.#retry);
276
299
  this.#retry = undefined;
300
+ this.#controller?.abort(new Error("Chappie IPC client is closed"));
301
+ this.#controller = undefined;
277
302
  this.#peer?.close();
278
303
  this.#peer = undefined;
279
304
  }
280
305
 
281
- async #open(): Promise<void> {
282
- const socket = createConnection(this.#endpoint);
283
- let failure = new Error("Chappie broker connection ended");
284
- await new Promise<void>((resolveOpen, rejectOpen) => {
285
- socket.once("connect", resolveOpen);
286
- socket.once("error", (error) => {
287
- failure = error;
288
- rejectOpen(error);
289
- });
290
- }).catch((error: unknown) => {
291
- socket.destroy();
306
+ async #open(signal: AbortSignal): Promise<void> {
307
+ let socket: Socket | undefined;
308
+ let failure = new Error("Chappie disconnected");
309
+ try {
310
+ socket = this.#remote
311
+ ? createConnection(networkEndpoint(this.#remote))
312
+ : createConnection(this.#endpoint);
313
+ await connectSocket(socket, signal);
314
+ } catch (error) {
315
+ socket?.destroy();
292
316
  this.#scheduleReconnect();
293
317
  throw error;
294
- });
318
+ }
295
319
 
296
320
  let peer: JsonLinePeer<BrokerMessage, SessionMessage>;
297
321
  peer = new JsonLinePeer(
@@ -326,6 +350,85 @@ export class IpcClient {
326
350
  }
327
351
  }
328
352
 
353
+ function listenServer(
354
+ server: Server,
355
+ endpoint: string | number,
356
+ ): Promise<void> {
357
+ return new Promise<void>((resolveListen, rejectListen) => {
358
+ const onError = (error: Error): void => {
359
+ server.off("listening", onListening);
360
+ rejectListen(error);
361
+ };
362
+ const onListening = (): void => {
363
+ server.off("error", onError);
364
+ resolveListen();
365
+ };
366
+ server.once("error", onError);
367
+ server.once("listening", onListening);
368
+ server.listen(endpoint);
369
+ });
370
+ }
371
+
372
+ function closeServer(server: Server): Promise<void> {
373
+ return new Promise<void>((resolveClose, rejectClose) =>
374
+ server.close((error) => (error ? rejectClose(error) : resolveClose())),
375
+ );
376
+ }
377
+
378
+ interface NetworkEndpoint {
379
+ host: string;
380
+ port: number;
381
+ }
382
+
383
+ function networkEndpoint(value: string): NetworkEndpoint {
384
+ const url = new URL(`tcp://${value}`);
385
+ if (url.username || url.password || url.pathname || url.search || url.hash)
386
+ throw new Error(`Invalid Chappie broker address: ${value}`);
387
+ const host = url.hostname.startsWith("[")
388
+ ? url.hostname.slice(1, -1)
389
+ : url.hostname;
390
+ if (!host) throw new Error(`Invalid Chappie broker address: ${value}`);
391
+ return { host, port: url.port ? Number(url.port) : defaultPort };
392
+ }
393
+
394
+ function connectSocket(socket: Socket, signal: AbortSignal): Promise<void> {
395
+ return new Promise<void>((resolveConnect, rejectConnect) => {
396
+ const cleanup = (): void => {
397
+ signal.removeEventListener("abort", onAbort);
398
+ socket.off("connect", onConnect);
399
+ socket.off("error", onError);
400
+ };
401
+ const onConnect = (): void => {
402
+ cleanup();
403
+ resolveConnect();
404
+ };
405
+ const onError = (error: Error): void => {
406
+ cleanup();
407
+ rejectConnect(error);
408
+ };
409
+ const onAbort = (): void => {
410
+ cleanup();
411
+ socket.destroy();
412
+ rejectConnect(abortError(signal));
413
+ };
414
+ if (signal.aborted) {
415
+ onAbort();
416
+ return;
417
+ }
418
+ signal.addEventListener("abort", onAbort, { once: true });
419
+ socket.once("connect", onConnect);
420
+ socket.once("error", onError);
421
+ });
422
+ }
423
+
424
+ function abortError(signal: AbortSignal): Error {
425
+ return signal.reason instanceof Error
426
+ ? signal.reason
427
+ : new Error(
428
+ typeof signal.reason === "string" ? signal.reason : "Request cancelled",
429
+ );
430
+ }
431
+
329
432
  async function prepareUnixSocket(endpoint: string): Promise<void> {
330
433
  try {
331
434
  await lstat(endpoint);
package/src/provider.ts CHANGED
@@ -147,19 +147,17 @@ export class ProviderOutput {
147
147
  }
148
148
 
149
149
  export function createChappieProvider(
150
- start: (output: ProviderOutput, context: Context) => Promise<void>,
150
+ start: (output: ProviderOutput) => Promise<void>,
151
151
  ) {
152
152
  const stream = (
153
153
  model: Model<Api>,
154
- context: Context,
154
+ _context: Context,
155
155
  options?: StreamOptions,
156
156
  ) => {
157
157
  const output = new ProviderOutput(model, options?.signal);
158
158
  if (!output.closed) {
159
159
  queueMicrotask(() => {
160
- void start(output, context).catch((error: unknown) =>
161
- output.fail(error),
162
- );
160
+ void start(output).catch((error: unknown) => output.fail(error));
163
161
  });
164
162
  }
165
163
  return output.stream;