@zetaloop/chappie 0.5.0 → 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.
@@ -0,0 +1,1042 @@
1
+ import { createRequire } from "node:module"; const require = createRequire(import.meta.url);
2
+ import {
3
+ IpcClient,
4
+ source,
5
+ sourceLabel
6
+ } from "./chunk-EIPDEA3I.js";
7
+ import {
8
+ Resources,
9
+ copyFiles,
10
+ getDirectory,
11
+ resourceSessionId,
12
+ transferFiles,
13
+ transferResult
14
+ } from "./chunk-GWYP7B66.js";
15
+ import {
16
+ __callDispose,
17
+ __using
18
+ } from "./chunk-UOFCCHBS.js";
19
+
20
+ // src/provider.ts
21
+ function createMessage(model, stopReason) {
22
+ return {
23
+ role: "assistant",
24
+ content: [],
25
+ api: model.api,
26
+ provider: model.provider,
27
+ model: model.id,
28
+ usage: {
29
+ input: 0,
30
+ output: 0,
31
+ cacheRead: 0,
32
+ cacheWrite: 0,
33
+ totalTokens: 0,
34
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
35
+ },
36
+ stopReason,
37
+ timestamp: Date.now()
38
+ };
39
+ }
40
+ var ProviderOutput = class {
41
+ stream;
42
+ message;
43
+ finished;
44
+ #resolveFinished;
45
+ #removeAbort;
46
+ #started = false;
47
+ #closed = false;
48
+ constructor(message, stream, signal) {
49
+ this.stream = stream;
50
+ this.message = message;
51
+ const completion = Promise.withResolvers();
52
+ this.finished = completion.promise;
53
+ this.#resolveFinished = completion.resolve;
54
+ if (signal) {
55
+ const abort = () => this.fail(new Error("Chappie provider request was cancelled"), true);
56
+ signal.addEventListener("abort", abort, { once: true });
57
+ this.#removeAbort = () => signal.removeEventListener("abort", abort);
58
+ if (signal.aborted) abort();
59
+ }
60
+ }
61
+ get closed() {
62
+ return this.#closed;
63
+ }
64
+ begin() {
65
+ if (this.#started || this.#closed) return;
66
+ this.#started = true;
67
+ this.stream.push({ type: "start", partial: this.message });
68
+ }
69
+ text(text) {
70
+ if (this.#closed)
71
+ throw new Error("Chappie provider response is already complete");
72
+ this.begin();
73
+ const contentIndex = this.message.content.length;
74
+ const block = { type: "text", text: "" };
75
+ this.message.content.push(block);
76
+ this.stream.push({
77
+ type: "text_start",
78
+ contentIndex,
79
+ partial: this.message
80
+ });
81
+ block.text = text;
82
+ this.stream.push({
83
+ type: "text_delta",
84
+ contentIndex,
85
+ delta: text,
86
+ partial: this.message
87
+ });
88
+ this.stream.push({
89
+ type: "text_end",
90
+ contentIndex,
91
+ content: text,
92
+ partial: this.message
93
+ });
94
+ }
95
+ toolCalls(calls) {
96
+ if (this.#closed)
97
+ throw new Error("Chappie provider response is already complete");
98
+ this.begin();
99
+ for (const call of calls) {
100
+ const contentIndex = this.message.content.length;
101
+ const block = { ...call, arguments: {} };
102
+ this.message.content.push(block);
103
+ this.stream.push({
104
+ type: "toolcall_start",
105
+ contentIndex,
106
+ partial: this.message
107
+ });
108
+ block.arguments = call.arguments;
109
+ this.stream.push({
110
+ type: "toolcall_delta",
111
+ contentIndex,
112
+ delta: JSON.stringify(call.arguments),
113
+ partial: this.message
114
+ });
115
+ this.stream.push({
116
+ type: "toolcall_end",
117
+ contentIndex,
118
+ toolCall: block,
119
+ partial: this.message
120
+ });
121
+ }
122
+ }
123
+ done(reason = "stop") {
124
+ if (this.#closed) return;
125
+ this.begin();
126
+ this.message.stopReason = reason;
127
+ this.stream.push({ type: "done", reason, message: this.message });
128
+ this.#finish();
129
+ }
130
+ fail(error, aborted = false) {
131
+ if (this.#closed) return;
132
+ this.begin();
133
+ this.message.stopReason = aborted ? "aborted" : "error";
134
+ this.message.errorMessage = error instanceof Error ? error.message : String(error);
135
+ this.stream.push({
136
+ type: "error",
137
+ reason: aborted ? "aborted" : "error",
138
+ error: this.message
139
+ });
140
+ this.#finish();
141
+ }
142
+ #finish() {
143
+ this.#closed = true;
144
+ this.#removeAbort?.();
145
+ this.stream.end();
146
+ this.#resolveFinished();
147
+ }
148
+ };
149
+
150
+ // src/session.ts
151
+ import { randomUUID } from "node:crypto";
152
+ import { addAbortListener } from "node:events";
153
+ import { hostname } from "node:os";
154
+ var Session = class {
155
+ #host;
156
+ #config;
157
+ #syncs = /* @__PURE__ */ new Map();
158
+ #stores = /* @__PURE__ */ new Map();
159
+ #ready = /* @__PURE__ */ new Set();
160
+ #generations = /* @__PURE__ */ new Map();
161
+ #queue = [];
162
+ #pendingInputs = /* @__PURE__ */ new Map();
163
+ #deliveries = /* @__PURE__ */ new Map();
164
+ #histories = /* @__PURE__ */ new Map();
165
+ #requests = /* @__PURE__ */ new Map();
166
+ #operations = /* @__PURE__ */ new Map();
167
+ #resources = new Resources();
168
+ #receivedDeliveries = [];
169
+ #connection;
170
+ #output;
171
+ #active;
172
+ #nextRequestId = 1;
173
+ #starting = false;
174
+ #sessionId;
175
+ #flushing = Promise.resolve();
176
+ #provider;
177
+ constructor(host, config = {}) {
178
+ this.#host = host;
179
+ this.#config = config;
180
+ }
181
+ get localTools() {
182
+ return Boolean(this.#config.localTools && !this.#host.active());
183
+ }
184
+ get active() {
185
+ return this.#host.active();
186
+ }
187
+ deliveries() {
188
+ return this.#receivedDeliveries.splice(0);
189
+ }
190
+ get id() {
191
+ return this.#host.describe().id;
192
+ }
193
+ async sessions(sessionId, signal) {
194
+ const result = await this.#request(
195
+ { type: "sessions", ...sessionId ? { sessionId } : {} },
196
+ signal
197
+ );
198
+ if (!("sessions" in result))
199
+ throw new Error("Broker returned no session list");
200
+ return { self: this.id, sessions: result.sessions };
201
+ }
202
+ async tools(sessionId, names, signal) {
203
+ const result = await this.#request({ type: "inspect", sessionId }, signal);
204
+ if (!("inspection" in result))
205
+ throw new Error("Session returned no tool catalog");
206
+ return {
207
+ ...result.inspection,
208
+ inputs: result.inputs,
209
+ ...result.globalAgents ? { globalAgents: result.globalAgents } : {},
210
+ tools: result.inspection.tools.filter(
211
+ (tool) => !names || names.includes(tool.name)
212
+ )
213
+ };
214
+ }
215
+ async resource(uri, signal) {
216
+ signal?.throwIfAborted();
217
+ const sessionId = resourceSessionId(uri);
218
+ if (sessionId === this.id) return this.#resources.read(sessionId, uri);
219
+ const result = await this.#request(
220
+ { type: "readResource", sessionId, uri },
221
+ signal
222
+ );
223
+ if (!("resource" in result))
224
+ throw new Error("Session returned no resource");
225
+ return result.resource;
226
+ }
227
+ async history(range, sessionId, signal) {
228
+ const self = this.#host.describe();
229
+ if (!sessionId || sessionId === self.id) {
230
+ const history = await this.#host.history(range);
231
+ this.#publish(this.#resources.rememberImages(self.id, history.content));
232
+ return history;
233
+ }
234
+ const result = await this.#request(
235
+ {
236
+ type: "history",
237
+ sessionId,
238
+ range,
239
+ ...source(self.id, randomUUID(), self.name ?? self.agent)
240
+ },
241
+ signal
242
+ );
243
+ if (!("history" in result)) throw new Error("Session returned no history");
244
+ return result.history;
245
+ }
246
+ async call(sessionId, calls, signal) {
247
+ const self = this.#host.describe();
248
+ const result = await this.#request(
249
+ {
250
+ type: "call",
251
+ sessionId,
252
+ calls: calls.map((call) => ({
253
+ ...call,
254
+ type: "toolCall",
255
+ id: `chappie-${randomUUID()}`
256
+ })),
257
+ ...source(self.id, randomUUID(), self.name ?? self.agent)
258
+ },
259
+ signal
260
+ );
261
+ if (!("toolResults" in result))
262
+ throw new Error("Session returned no tool results");
263
+ return { sessionId, ...result };
264
+ }
265
+ async chat(sessionId, text, replyTo, signal) {
266
+ const self = this.#host.describe();
267
+ const result = await this.#request(
268
+ {
269
+ type: "chat",
270
+ sessionId,
271
+ text,
272
+ ...replyTo ? { replyTo } : {},
273
+ ...source(self.id, randomUUID(), self.name ?? self.agent)
274
+ },
275
+ signal
276
+ );
277
+ if (!("message" in result) && !("cancelled" in result))
278
+ throw new Error("Session returned no assistant message");
279
+ return { sessionId, ...result };
280
+ }
281
+ async ready(signal) {
282
+ var _stack = [];
283
+ try {
284
+ signal.throwIfAborted();
285
+ if (this.#generations.size || this.#output && !this.#output.closed)
286
+ return;
287
+ const completion = Promise.withResolvers();
288
+ const _abort = __using(_stack, addAbortListener(
289
+ signal,
290
+ () => completion.reject(signal.reason)
291
+ ));
292
+ this.#ready.add(completion);
293
+ try {
294
+ this.#wake();
295
+ await completion.promise;
296
+ } finally {
297
+ this.#ready.delete(completion);
298
+ }
299
+ } catch (_) {
300
+ var _error = _, _hasError = true;
301
+ } finally {
302
+ __callDispose(_stack, _error, _hasError);
303
+ }
304
+ }
305
+ async settled(error) {
306
+ const starting = this.#starting;
307
+ this.#starting = false;
308
+ const reason = error instanceof Error ? error.message : error === void 0 ? "Session ended before the request completed" : String(error);
309
+ if (error !== void 0 || starting)
310
+ this.#rejectReady(error ?? new Error(reason));
311
+ const active = this.#active;
312
+ if (active && !active.completed) {
313
+ active.completed = true;
314
+ active.error = reason;
315
+ } else if (!active && starting) {
316
+ const request = this.#queue.shift();
317
+ if (request) await this.#sendError(request.id, reason);
318
+ }
319
+ this.#collectInputs();
320
+ this.historyChanged();
321
+ await this.#completeActive();
322
+ this.#dispatch();
323
+ }
324
+ #notify(message, type = "info", activity = {}) {
325
+ this.#host.notify?.(message, type, activity);
326
+ if (activity.event !== "history") this.historyChanged();
327
+ }
328
+ async start(output) {
329
+ const connection = this.#connection;
330
+ if (!this.#host.active() || !connection) {
331
+ throw new Error("Chappie is not active for this session");
332
+ }
333
+ if (this.#output && !this.#output.closed) {
334
+ throw new Error("Chappie already has an active provider request");
335
+ }
336
+ this.#starting = false;
337
+ this.#output = output;
338
+ for (const pending of this.#ready) pending.resolve();
339
+ try {
340
+ await connection.connect();
341
+ if (output.closed) return;
342
+ this.#collectInputs();
343
+ this.historyChanged();
344
+ await this.#completeActive();
345
+ await this.#sync();
346
+ if (output.closed) return;
347
+ output.begin();
348
+ this.#dispatch();
349
+ await output.finished;
350
+ } finally {
351
+ if (this.#output === output) this.#output = void 0;
352
+ void this.#sync().catch(() => {
353
+ });
354
+ }
355
+ }
356
+ async generate(output, request) {
357
+ const connection = this.#connection;
358
+ if (!this.#host.active() || !connection)
359
+ throw new Error("Chappie is not active for this session");
360
+ if (output.closed) return;
361
+ const id = randomUUID();
362
+ this.#starting = false;
363
+ this.#generations.set(id, { output, request });
364
+ for (const pending of this.#ready) pending.resolve();
365
+ try {
366
+ await connection.connect();
367
+ if (output.closed) return;
368
+ this.historyChanged();
369
+ await this.#completeActive();
370
+ output.begin();
371
+ await this.#sync();
372
+ for (const queued of this.#queue.splice(0))
373
+ await this.#cancelPending(queued);
374
+ await output.finished;
375
+ } finally {
376
+ this.#generations.delete(id);
377
+ void this.#sync().catch(() => {
378
+ });
379
+ this.#dispatch();
380
+ }
381
+ }
382
+ async transfer(args, signal, update) {
383
+ const context = {
384
+ ...this.#host.describe(),
385
+ resources: this.#resources,
386
+ ...this.#host.mutate ? { mutate: this.#host.mutate } : {}
387
+ };
388
+ if ([args.files, args.from, args.to].filter(Boolean).length > 1)
389
+ throw new Error("Supply one of files, from, or to");
390
+ if (args.from) {
391
+ const result2 = await this.#request(
392
+ {
393
+ type: "export",
394
+ sessionId: args.from.sessionId,
395
+ paths: args.from.paths
396
+ },
397
+ signal
398
+ );
399
+ if (!("transfer" in result2))
400
+ throw new Error("Session returned no resources");
401
+ const copying = signal ?? new AbortController().signal;
402
+ const files = await copyFiles(
403
+ args.paths,
404
+ result2.transfer.resources,
405
+ context.cwd,
406
+ args.overwrite === true,
407
+ (resource) => this.#readChunks(resource, copying),
408
+ copying,
409
+ context.mutate
410
+ );
411
+ return transferResult({
412
+ device: hostname(),
413
+ files,
414
+ resources: [],
415
+ from: {
416
+ sessionId: args.from.sessionId,
417
+ device: result2.transfer.device
418
+ }
419
+ });
420
+ }
421
+ if (!args.to) {
422
+ const result2 = await transferFiles(args, context, signal, update);
423
+ this.#publish(result2.details.resources);
424
+ return result2;
425
+ }
426
+ if (args.paths.length !== args.to.paths.length)
427
+ throw new Error("Source and destination counts must match");
428
+ const { sessions } = await this.sessions(args.to.sessionId, signal);
429
+ const destination = sessions[0];
430
+ if (!destination) throw new Error("Destination session is unavailable");
431
+ const exported = await transferFiles(
432
+ { paths: args.paths },
433
+ context,
434
+ signal
435
+ );
436
+ this.#publish(exported.details.resources);
437
+ update?.({
438
+ ...exported.details,
439
+ to: {
440
+ sessionId: args.to.sessionId,
441
+ device: destination.device
442
+ }
443
+ });
444
+ const result = await this.#request(
445
+ {
446
+ type: "copy",
447
+ sessionId: args.to.sessionId,
448
+ paths: args.to.paths,
449
+ resources: exported.details.resources,
450
+ ...args.overwrite !== void 0 ? { overwrite: args.overwrite } : {}
451
+ },
452
+ signal
453
+ );
454
+ if (!("transfer" in result))
455
+ throw new Error("Session returned no transfer result");
456
+ return transferResult({ ...result.transfer, device: hostname() });
457
+ }
458
+ close() {
459
+ const sessionId = this.#sessionId;
460
+ if (sessionId && this.#connection?.connected) {
461
+ void this.#connection.send({ type: "unregister", sessionId }).catch(() => {
462
+ });
463
+ }
464
+ this.#output?.fail(new Error("Chappie session ended"), true);
465
+ for (const { output } of this.#generations.values())
466
+ output.fail(new Error("Chappie session ended"), true);
467
+ this.#generations.clear();
468
+ this.#output = void 0;
469
+ this.#active = void 0;
470
+ this.#queue.length = 0;
471
+ this.#starting = false;
472
+ this.#connection?.close();
473
+ this.#connection = void 0;
474
+ this.resetInputs();
475
+ this.#cancelRequests(new Error("Chappie session ended"));
476
+ this.#rejectReady(new Error("Chappie session ended"));
477
+ this.#rejectSyncs(new Error("Chappie session ended"));
478
+ this.#rejectStores(new Error("Chappie session ended"));
479
+ for (const id of this.#histories.keys()) this.#finishHistory(id);
480
+ }
481
+ update() {
482
+ const active = this.#host.active();
483
+ const id = this.#host.describe().id;
484
+ const changed = this.#sessionId !== id || this.#provider !== active;
485
+ if (changed) {
486
+ if (this.#connection) this.close();
487
+ else this.resetInputs();
488
+ }
489
+ this.#provider = active;
490
+ if (!active && !this.#config.localTools) return changed;
491
+ if (!this.#connection) {
492
+ this.#connection = new IpcClient(getDirectory(), this.#config.connect, {
493
+ onOpen: async () => {
494
+ this.#publish(this.#resources.list());
495
+ await this.#sync();
496
+ await this.#flushDeliveries();
497
+ },
498
+ onMessage: (message) => this.#receive(message),
499
+ onClose: (error) => {
500
+ this.#cancelRequests(error);
501
+ this.#rejectReady(error);
502
+ for (const id2 of this.#histories.keys()) this.#finishHistory(id2);
503
+ this.#rejectSyncs(error);
504
+ this.#rejectStores(error);
505
+ if (this.#output && !this.#output.closed) this.#output.fail(error);
506
+ else this.#notify(error.message, "error");
507
+ for (const { output } of this.#generations.values())
508
+ output.fail(error);
509
+ this.#active = void 0;
510
+ this.#queue.length = 0;
511
+ this.#starting = false;
512
+ }
513
+ });
514
+ this.#connection.start();
515
+ } else {
516
+ void this.#sync().catch(() => {
517
+ });
518
+ }
519
+ return changed;
520
+ }
521
+ #description(sessionId = this.#sessionId) {
522
+ if (sessionId !== this.#sessionId)
523
+ throw new Error("The requested session is no longer active");
524
+ return {
525
+ ...this.#host.describe(),
526
+ status: this.#generations.size ? "generating" : this.#active ? "executing" : this.#output && !this.#output.closed ? "ready" : "idle"
527
+ };
528
+ }
529
+ async #sync() {
530
+ const connection = this.#connection;
531
+ if (!connection?.connected || !this.#host.active()) return;
532
+ const id = this.#nextRequestId++;
533
+ const completion = Promise.withResolvers();
534
+ this.#syncs.set(id, completion);
535
+ try {
536
+ await connection.send({ type: "sync", id, session: this.#description() });
537
+ await completion.promise;
538
+ } finally {
539
+ this.#syncs.delete(id);
540
+ }
541
+ }
542
+ async #receive(message) {
543
+ switch (message.type) {
544
+ case "response": {
545
+ const pending = this.#requests.get(message.id);
546
+ if (!pending) break;
547
+ if ("error" in message) pending.reject(new Error(message.error));
548
+ else {
549
+ const { type: _type, id: _id, deliveries, ...result } = message;
550
+ this.#receivedDeliveries.push(...deliveries ?? []);
551
+ pending.resolve(result);
552
+ }
553
+ break;
554
+ }
555
+ case "synced":
556
+ this.#syncs.get(message.id)?.resolve();
557
+ break;
558
+ case "stored":
559
+ this.#stores.get(message.id)?.resolve();
560
+ break;
561
+ case "notice":
562
+ if (message.sessionId === this.#sessionId) {
563
+ this.#notify(message.message, "info", message.activity);
564
+ }
565
+ break;
566
+ case "inspect":
567
+ await this.#reply(message.id, async (signal) => {
568
+ const { globalAgents, ...environment } = await this.#host.inspect(signal);
569
+ return {
570
+ type: "result",
571
+ id: message.id,
572
+ inspection: {
573
+ session: this.#description(message.sessionId),
574
+ ...environment
575
+ },
576
+ inputs: this.#inputs(),
577
+ ...globalAgents ? { globalAgents } : {}
578
+ };
579
+ });
580
+ break;
581
+ case "inputs":
582
+ await this.#reply(message.id, () => {
583
+ this.#description(message.sessionId);
584
+ return {
585
+ type: "result",
586
+ id: message.id,
587
+ inputs: this.#inputs()
588
+ };
589
+ });
590
+ break;
591
+ case "history":
592
+ await this.#readHistory(message);
593
+ break;
594
+ case "ackInputs":
595
+ if (message.sessionId === this.#sessionId) {
596
+ for (const id of message.ids) this.#pendingInputs.delete(id);
597
+ }
598
+ break;
599
+ case "readResource":
600
+ await this.#reply(message.id, async () => ({
601
+ type: "result",
602
+ id: message.id,
603
+ resource: await this.#resources.read(
604
+ message.sessionId,
605
+ message.uri,
606
+ message.offset
607
+ )
608
+ }));
609
+ break;
610
+ case "export":
611
+ await this.#reply(message.id, async (signal) => {
612
+ this.#description(message.sessionId);
613
+ const result = await this.transfer({ paths: message.paths }, signal);
614
+ return { type: "result", id: message.id, transfer: result.details };
615
+ });
616
+ break;
617
+ case "copy": {
618
+ await this.#reply(message.id, async (signal) => {
619
+ const context = this.#description(message.sessionId);
620
+ const files = await copyFiles(
621
+ message.paths,
622
+ message.resources,
623
+ context.cwd,
624
+ message.overwrite === true,
625
+ (resource) => this.#readChunks(resource, signal),
626
+ signal,
627
+ this.#host.mutate
628
+ );
629
+ return {
630
+ type: "result",
631
+ id: message.id,
632
+ transfer: {
633
+ device: hostname(),
634
+ files,
635
+ resources: [],
636
+ to: { sessionId: message.sessionId, device: hostname() }
637
+ }
638
+ };
639
+ });
640
+ break;
641
+ }
642
+ case "cancel": {
643
+ const operation = this.#operations.get(message.id);
644
+ if (operation) {
645
+ operation.abort(new Error(message.reason));
646
+ break;
647
+ }
648
+ if (this.#histories.has(message.id)) {
649
+ this.#finishHistory(message.id);
650
+ break;
651
+ }
652
+ const queued = this.#queue.findIndex(
653
+ (request2) => request2.id === message.id
654
+ );
655
+ const request = this.#queue[queued] ?? (this.#active?.request.id === message.id ? this.#active.request : void 0);
656
+ if (!request) break;
657
+ const name = request.type === "call" ? [...new Set(request.calls.map((call) => call.name))].join(", ") : "chat";
658
+ this.#notify(
659
+ `${name} cancelled for ${sourceLabel(request)}: ${message.reason}`,
660
+ "warning",
661
+ {
662
+ event: "cancelled",
663
+ ...source(request.clientId, request.requestId, request.label)
664
+ }
665
+ );
666
+ if (queued !== -1) {
667
+ this.#queue.splice(queued, 1);
668
+ break;
669
+ }
670
+ const active = this.#active;
671
+ if (!active) break;
672
+ active.cancelled = message.reason;
673
+ if (active.completed) await this.#completeActive();
674
+ else await this.#host.abort();
675
+ break;
676
+ }
677
+ case "chat":
678
+ case "call":
679
+ if (message.sessionId !== this.#sessionId) {
680
+ await this.#sendError(
681
+ message.id,
682
+ "The requested session is no longer active"
683
+ );
684
+ break;
685
+ }
686
+ if (message.type === "chat" && message.replyTo) {
687
+ const generation = this.#generations.get(message.replyTo);
688
+ if (!generation || generation.output.closed) {
689
+ await this.#sendError(message.id, "The model request has ended");
690
+ break;
691
+ }
692
+ const output = generation.output;
693
+ output.message.chappie = source(
694
+ message.clientId,
695
+ message.requestId,
696
+ message.label
697
+ );
698
+ output.text(message.text);
699
+ output.done();
700
+ this.#generations.delete(message.replyTo);
701
+ await this.#connection?.send({
702
+ type: "result",
703
+ id: message.id,
704
+ cwd: this.#host.describe().cwd,
705
+ message: output.message,
706
+ inputs: this.#inputs()
707
+ });
708
+ break;
709
+ }
710
+ if (this.#generations.size) {
711
+ await this.#cancelPending(message);
712
+ break;
713
+ }
714
+ this.#queue.push(message);
715
+ this.#dispatch();
716
+ break;
717
+ }
718
+ }
719
+ async #cancelPending(request) {
720
+ await this.#connection?.send({
721
+ type: "result",
722
+ id: request.id,
723
+ cwd: this.#host.describe().cwd,
724
+ cancelled: request.type === "call" ? "The requested tools were not executed. A model request is awaiting a reply." : "A model request is awaiting a reply. Set replyTo to its request ID.",
725
+ toolResults: [],
726
+ inputs: this.#inputs()
727
+ });
728
+ }
729
+ #publish(resources) {
730
+ if (!resources.length) return;
731
+ if (this.#connection?.connected) {
732
+ void this.#connection.send({ type: "resources", resources }).catch(() => {
733
+ });
734
+ }
735
+ }
736
+ async #request(request, signal) {
737
+ signal?.throwIfAborted();
738
+ const connection = this.#connection;
739
+ if (!connection?.connected) throw new Error("Chappie is not connected");
740
+ const id = this.#nextRequestId++;
741
+ const completion = Promise.withResolvers();
742
+ const onAbort = () => {
743
+ completion.reject(signal?.reason);
744
+ void connection.send({ type: "cancelRequest", id }).catch(() => {
745
+ });
746
+ };
747
+ this.#requests.set(id, completion);
748
+ signal?.addEventListener("abort", onAbort, { once: true });
749
+ void connection.send({ type: "request", id, clientId: this.id, request }).catch(completion.reject);
750
+ try {
751
+ return await completion.promise;
752
+ } finally {
753
+ this.#requests.delete(id);
754
+ signal?.removeEventListener("abort", onAbort);
755
+ }
756
+ }
757
+ async *#readChunks(resource, signal) {
758
+ for (let offset = 0; offset < resource.size; ) {
759
+ const result = await this.#request(
760
+ {
761
+ type: "readResource",
762
+ sessionId: resourceSessionId(resource.uri),
763
+ uri: resource.uri,
764
+ offset
765
+ },
766
+ signal
767
+ );
768
+ if (!("resource" in result))
769
+ throw new Error("Session returned no resource");
770
+ const data = Buffer.from(result.resource.blob, "base64");
771
+ if (data.length === 0)
772
+ throw new Error(
773
+ `Source ended before ${resource.size} bytes: ${resource.name}`
774
+ );
775
+ yield data;
776
+ offset += data.length;
777
+ }
778
+ }
779
+ #cancelRequests(error) {
780
+ for (const pending of this.#requests.values()) pending.reject(error);
781
+ this.#requests.clear();
782
+ for (const controller of this.#operations.values()) controller.abort(error);
783
+ this.#operations.clear();
784
+ }
785
+ async #readHistory(request, wait = request.range.wait) {
786
+ try {
787
+ const context = this.#host.describe();
788
+ if (context.id !== request.sessionId)
789
+ throw new Error("The requested session is no longer active");
790
+ const history = await this.history(request.range);
791
+ if (wait && !request.range.before && history.count === 0) {
792
+ if (!this.#histories.has(request.id)) {
793
+ this.#histories.set(request.id, {
794
+ request,
795
+ timeout: setTimeout(() => {
796
+ void this.#readHistory(request, false).catch(() => {
797
+ });
798
+ }, 3e4)
799
+ });
800
+ }
801
+ return;
802
+ }
803
+ this.#finishHistory(request.id);
804
+ if (!request.range.observer) {
805
+ this.#notify(
806
+ `${sourceLabel(request)} read history: ${history.count} entries`,
807
+ "info",
808
+ {
809
+ event: "history",
810
+ ...source(request.clientId, request.requestId, request.label)
811
+ }
812
+ );
813
+ }
814
+ await this.#connection?.send({
815
+ type: "result",
816
+ id: request.id,
817
+ cwd: context.cwd,
818
+ history
819
+ });
820
+ } catch (error) {
821
+ this.#finishHistory(request.id);
822
+ await this.#sendError(
823
+ request.id,
824
+ error instanceof Error ? error.message : String(error)
825
+ );
826
+ }
827
+ }
828
+ historyChanged() {
829
+ for (const { request } of this.#histories.values()) {
830
+ void this.#readHistory(request).catch(() => {
831
+ });
832
+ }
833
+ }
834
+ #finishHistory(id) {
835
+ const pending = this.#histories.get(id);
836
+ if (!pending) return;
837
+ clearTimeout(pending.timeout);
838
+ this.#histories.delete(id);
839
+ }
840
+ #dispatch() {
841
+ if (this.#active || this.#generations.size) return;
842
+ const output = this.#output;
843
+ if (!output || output.closed) {
844
+ this.#wake();
845
+ return;
846
+ }
847
+ const request = this.#queue.shift();
848
+ if (!request) return;
849
+ output.message.chappie = source(
850
+ request.clientId,
851
+ request.requestId,
852
+ request.label
853
+ );
854
+ this.#active = {
855
+ request,
856
+ session: this.#description(),
857
+ message: output.message,
858
+ completed: false,
859
+ cancelled: void 0,
860
+ toolResults: []
861
+ };
862
+ void this.#sync().catch(() => {
863
+ });
864
+ if (request.type === "chat") {
865
+ output.text(request.text);
866
+ output.done();
867
+ } else {
868
+ output.toolCalls(request.calls);
869
+ output.done("toolUse");
870
+ }
871
+ }
872
+ #wake() {
873
+ if (this.#starting || this.#generations.size || this.#output || this.#active || this.#queue.length === 0 && this.#ready.size === 0 || !this.#host.isIdle())
874
+ return;
875
+ this.#starting = true;
876
+ void Promise.resolve().then(() => this.#host.wake()).catch((error) => {
877
+ this.#starting = false;
878
+ this.#rejectReady(error);
879
+ const request = this.#queue.shift();
880
+ if (request)
881
+ return this.#sendError(
882
+ request.id,
883
+ error instanceof Error ? error.message : String(error)
884
+ );
885
+ });
886
+ }
887
+ complete(message, toolResults, error) {
888
+ this.historyChanged();
889
+ const active = this.#active;
890
+ const completed = message;
891
+ const origin = completed?.chappie;
892
+ const failure = error ?? completed?.errorMessage;
893
+ if (!active || failure === void 0 && message !== active.message && (!origin || origin.clientId !== active.request.clientId || origin.requestId !== active.request.requestId))
894
+ return;
895
+ const sessionId = active.session.id;
896
+ for (const result of toolResults)
897
+ this.#publish(this.#resources.rememberImages(sessionId, result.content));
898
+ if (failure !== void 0) active.error = failure;
899
+ active.toolResults = toolResults;
900
+ if (active.request.type === "call") {
901
+ const results = new Map(
902
+ toolResults.map((result) => [result.toolCallId, result])
903
+ );
904
+ active.toolResults = active.request.calls.flatMap(
905
+ (call) => results.get(call.id) ?? []
906
+ );
907
+ const missing = active.request.calls.flatMap(
908
+ (call, index) => results.has(call.id) ? [] : [`${index + 1} (${call.name})`]
909
+ );
910
+ if (missing.length)
911
+ active.error ??= `No results for calls ${missing.join(", ")}`;
912
+ }
913
+ active.completed = true;
914
+ }
915
+ async #completeActive() {
916
+ const active = this.#active;
917
+ if (!active?.completed) return;
918
+ this.#collectInputs();
919
+ const inputs = this.#inputs();
920
+ if (active.cancelled !== void 0) {
921
+ const delivery = {
922
+ id: randomUUID(),
923
+ ...source(
924
+ active.request.clientId,
925
+ active.request.requestId,
926
+ active.request.label
927
+ ),
928
+ sessionId: active.session.id,
929
+ cwd: active.session.cwd,
930
+ toolResults: active.toolResults,
931
+ error: active.cancelled
932
+ };
933
+ this.#deliveries.set(delivery.id, delivery);
934
+ void this.#flushDeliveries().catch(() => {
935
+ });
936
+ } else if (active.error !== void 0) {
937
+ await this.#sendError(active.request.id, active.error);
938
+ } else {
939
+ await this.#connection?.send({
940
+ type: "result",
941
+ id: active.request.id,
942
+ cwd: active.session.cwd,
943
+ message: active.message,
944
+ inputs,
945
+ ...active.request.type === "call" ? { toolResults: active.toolResults } : {}
946
+ });
947
+ }
948
+ this.#active = void 0;
949
+ void this.#sync().catch(() => {
950
+ });
951
+ }
952
+ resetInputs() {
953
+ this.#sessionId = this.#host.describe().id;
954
+ this.#pendingInputs.clear();
955
+ this.#host.resetInputs();
956
+ }
957
+ #collectInputs() {
958
+ if (!this.#host.active()) return;
959
+ for (const input of this.#host.inputs()) {
960
+ if ("message" in input && typeof input.message.content !== "string")
961
+ this.#publish(
962
+ this.#resources.rememberImages(
963
+ input.sessionId,
964
+ input.message.content
965
+ )
966
+ );
967
+ this.#pendingInputs.set(input.id, input);
968
+ }
969
+ }
970
+ #inputs() {
971
+ this.#collectInputs();
972
+ const generation = this.#generations.entries().next().value;
973
+ return [
974
+ ...generation ? [
975
+ {
976
+ id: generation[0],
977
+ sessionId: this.id,
978
+ request: generation[1].request
979
+ }
980
+ ] : [],
981
+ ...this.#pendingInputs.values()
982
+ ];
983
+ }
984
+ async #flushDeliveries() {
985
+ const flushed = this.#flushing.then(async () => {
986
+ const connection = this.#connection;
987
+ if (!connection?.connected) return;
988
+ for (const delivery of this.#deliveries.values()) {
989
+ const completion = Promise.withResolvers();
990
+ this.#stores.set(delivery.id, completion);
991
+ try {
992
+ await connection.send({ type: "delivery", delivery });
993
+ await completion.promise;
994
+ this.#deliveries.delete(delivery.id);
995
+ this.#notify(`Result saved for ${sourceLabel(delivery)}`, "info", {
996
+ event: "result_saved",
997
+ ...source(delivery.clientId, delivery.requestId, delivery.label)
998
+ });
999
+ } finally {
1000
+ this.#stores.delete(delivery.id);
1001
+ }
1002
+ }
1003
+ });
1004
+ this.#flushing = flushed.catch(() => {
1005
+ });
1006
+ return flushed;
1007
+ }
1008
+ async #reply(id, response) {
1009
+ const controller = new AbortController();
1010
+ this.#operations.set(id, controller);
1011
+ try {
1012
+ await this.#connection?.send(await response(controller.signal));
1013
+ } catch (error) {
1014
+ await this.#sendError(
1015
+ id,
1016
+ error instanceof Error ? error.message : String(error)
1017
+ );
1018
+ } finally {
1019
+ this.#operations.delete(id);
1020
+ }
1021
+ }
1022
+ #rejectReady(error) {
1023
+ for (const pending of this.#ready) pending.reject(error);
1024
+ }
1025
+ async #sendError(id, error) {
1026
+ await this.#connection?.send({ type: "result", id, error });
1027
+ }
1028
+ #rejectSyncs(error) {
1029
+ for (const sync of this.#syncs.values()) sync.reject(error);
1030
+ this.#syncs.clear();
1031
+ }
1032
+ #rejectStores(error) {
1033
+ for (const store of this.#stores.values()) store.reject(error);
1034
+ this.#stores.clear();
1035
+ }
1036
+ };
1037
+
1038
+ export {
1039
+ createMessage,
1040
+ ProviderOutput,
1041
+ Session
1042
+ };