@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/README.md +27 -7
- package/docs/tools.md +55 -68
- package/package.json +3 -1
- package/src/broker.ts +240 -50
- package/src/config.ts +21 -0
- package/src/delivery.ts +27 -56
- package/src/index.ts +15 -10
- package/src/instructions.md +8 -4
- package/src/ipc.ts +144 -41
- package/src/provider.ts +3 -5
- package/src/question.html +384 -0
- package/src/questions.ts +95 -0
- package/src/server.ts +250 -69
- package/src/session.ts +131 -67
- package/src/state.ts +104 -9
- package/src/stdio.ts +11 -2
- package/src/tools.ts +6 -23
- package/src/transfer.ts +3 -3
package/src/session.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import type {
|
|
3
6
|
AssistantMessage,
|
|
4
|
-
Context,
|
|
5
7
|
ToolResultMessage,
|
|
6
8
|
UserMessage,
|
|
7
9
|
} from "@earendil-works/pi-ai";
|
|
8
10
|
import type {
|
|
9
11
|
ExtensionAPI,
|
|
10
12
|
ExtensionContext,
|
|
13
|
+
SessionEntry,
|
|
11
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
12
16
|
import type { DeliveryRecord } from "./delivery.ts";
|
|
13
17
|
import {
|
|
14
18
|
type BrokerMessage,
|
|
@@ -23,6 +27,11 @@ import { readSessionResource, rememberImages } from "./resources.ts";
|
|
|
23
27
|
|
|
24
28
|
type RemoteRequest = Extract<BrokerMessage, { type: "chat" | "call" }>;
|
|
25
29
|
|
|
30
|
+
interface Notice {
|
|
31
|
+
message: string;
|
|
32
|
+
type: "info" | "warning" | "error";
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
interface SyncRequest {
|
|
27
36
|
resolve(): void;
|
|
28
37
|
reject(error: Error): void;
|
|
@@ -35,15 +44,17 @@ interface StoreRequest {
|
|
|
35
44
|
|
|
36
45
|
interface ActiveRequest {
|
|
37
46
|
request: RemoteRequest;
|
|
47
|
+
session: SessionDescription;
|
|
38
48
|
message: AssistantMessage;
|
|
39
49
|
completed: boolean;
|
|
40
|
-
cancelled:
|
|
50
|
+
cancelled: string | undefined;
|
|
41
51
|
toolResults: ToolResultMessage[];
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
export class LocalSession {
|
|
45
55
|
readonly #pi: ExtensionAPI;
|
|
46
56
|
readonly #agentDir: string;
|
|
57
|
+
readonly #connect: string | undefined;
|
|
47
58
|
readonly #syncs = new Map<number, SyncRequest>();
|
|
48
59
|
readonly #stores = new Map<string, StoreRequest>();
|
|
49
60
|
readonly #queue: RemoteRequest[] = [];
|
|
@@ -58,14 +69,27 @@ export class LocalSession {
|
|
|
58
69
|
#nextSyncId = 1;
|
|
59
70
|
#starting = false;
|
|
60
71
|
#sessionId: string | undefined;
|
|
72
|
+
#inputCursor: string | null = null;
|
|
61
73
|
#flushing = Promise.resolve();
|
|
62
74
|
|
|
63
|
-
constructor(pi: ExtensionAPI, agentDir: string) {
|
|
75
|
+
constructor(pi: ExtensionAPI, agentDir: string, connect?: string) {
|
|
64
76
|
this.#pi = pi;
|
|
65
77
|
this.#agentDir = agentDir;
|
|
78
|
+
this.#connect = connect;
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
install(): void {
|
|
82
|
+
this.#pi.registerEntryRenderer<Notice>(
|
|
83
|
+
"chappie.notice",
|
|
84
|
+
({ data }, _options, theme) => {
|
|
85
|
+
if (!data) return;
|
|
86
|
+
return new Text(
|
|
87
|
+
theme.fg(data.type === "info" ? "dim" : data.type, data.message),
|
|
88
|
+
1,
|
|
89
|
+
0,
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
69
93
|
this.#pi.on("session_start", (_event, context) => this.#update(context));
|
|
70
94
|
this.#pi.on("model_select", (event, context) =>
|
|
71
95
|
this.#update(context, event.model.provider === "chappie"),
|
|
@@ -74,11 +98,15 @@ export class LocalSession {
|
|
|
74
98
|
this.#context = context;
|
|
75
99
|
void this.#sync().catch(() => {});
|
|
76
100
|
});
|
|
77
|
-
this.#pi.on("context", (event) => ({
|
|
78
|
-
messages:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
101
|
+
this.#pi.on("context", (event, context) => ({
|
|
102
|
+
messages:
|
|
103
|
+
context.model?.provider === "chappie"
|
|
104
|
+
? []
|
|
105
|
+
: event.messages.filter(
|
|
106
|
+
(message) =>
|
|
107
|
+
message.role !== "custom" ||
|
|
108
|
+
message.customType !== "chappie.request",
|
|
109
|
+
),
|
|
82
110
|
}));
|
|
83
111
|
this.#pi.on("turn_end", (event, context) =>
|
|
84
112
|
this.#turnEnd(event.message, event.toolResults, context),
|
|
@@ -93,10 +121,12 @@ export class LocalSession {
|
|
|
93
121
|
this.#pi.on("session_shutdown", () => this.close());
|
|
94
122
|
}
|
|
95
123
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
124
|
+
#notify(message: string, type: Notice["type"] = "info"): void {
|
|
125
|
+
if (this.#context)
|
|
126
|
+
this.#pi.appendEntry<Notice>("chappie.notice", { message, type });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async start(output: ProviderOutput): Promise<void> {
|
|
100
130
|
const context = this.#context;
|
|
101
131
|
const connection = this.#connection;
|
|
102
132
|
if (context?.model?.provider !== "chappie" || !connection) {
|
|
@@ -155,7 +185,8 @@ export class LocalSession {
|
|
|
155
185
|
return;
|
|
156
186
|
}
|
|
157
187
|
if (!this.#connection) {
|
|
158
|
-
this.#
|
|
188
|
+
this.#notify("Connecting to Chappie…");
|
|
189
|
+
this.#connection = new IpcClient(this.#agentDir, this.#connect, {
|
|
159
190
|
onOpen: async () => {
|
|
160
191
|
await this.#sync();
|
|
161
192
|
await this.#flushDeliveries();
|
|
@@ -164,7 +195,8 @@ export class LocalSession {
|
|
|
164
195
|
onClose: (error) => {
|
|
165
196
|
this.#rejectSyncs(error);
|
|
166
197
|
this.#rejectStores(error);
|
|
167
|
-
this.#output
|
|
198
|
+
if (this.#output && !this.#output.closed) this.#output.fail(error);
|
|
199
|
+
else this.#notify(error.message, "error");
|
|
168
200
|
this.#active = undefined;
|
|
169
201
|
this.#queue.length = 0;
|
|
170
202
|
this.#starting = false;
|
|
@@ -181,13 +213,12 @@ export class LocalSession {
|
|
|
181
213
|
const context = this.#context;
|
|
182
214
|
if (!context) throw new Error("Chappie session is not available");
|
|
183
215
|
const name = this.#pi.getSessionName();
|
|
184
|
-
const sessionFile = context.sessionManager.getSessionFile();
|
|
185
216
|
return {
|
|
186
217
|
id: context.sessionManager.getSessionId(),
|
|
187
218
|
cwd: context.cwd,
|
|
219
|
+
device: hostname(),
|
|
188
220
|
status: this.#status,
|
|
189
221
|
...(name ? { name } : {}),
|
|
190
|
-
...(sessionFile ? { sessionFile } : {}),
|
|
191
222
|
};
|
|
192
223
|
}
|
|
193
224
|
|
|
@@ -218,13 +249,24 @@ export class LocalSession {
|
|
|
218
249
|
case "stored":
|
|
219
250
|
this.#stores.get(message.id)?.resolve();
|
|
220
251
|
break;
|
|
252
|
+
case "notice":
|
|
253
|
+
if (
|
|
254
|
+
message.sessionId === this.#context?.sessionManager.getSessionId()
|
|
255
|
+
) {
|
|
256
|
+
this.#notify(message.message);
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
221
259
|
case "inspect":
|
|
222
|
-
await this.#reply(message.id, message.sessionId, () =>
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
260
|
+
await this.#reply(message.id, message.sessionId, async () => {
|
|
261
|
+
const globalAgents = await this.#readGlobalAgents();
|
|
262
|
+
return {
|
|
263
|
+
type: "result",
|
|
264
|
+
id: message.id,
|
|
265
|
+
inspection: this.#inspection(),
|
|
266
|
+
inputs: this.#inputs(),
|
|
267
|
+
...(globalAgents ? { globalAgents } : {}),
|
|
268
|
+
};
|
|
269
|
+
});
|
|
228
270
|
break;
|
|
229
271
|
case "ackInputs":
|
|
230
272
|
if (
|
|
@@ -244,13 +286,28 @@ export class LocalSession {
|
|
|
244
286
|
const queued = this.#queue.findIndex(
|
|
245
287
|
(request) => request.id === message.id,
|
|
246
288
|
);
|
|
289
|
+
const request =
|
|
290
|
+
this.#queue[queued] ??
|
|
291
|
+
(this.#active?.request.id === message.id
|
|
292
|
+
? this.#active.request
|
|
293
|
+
: undefined);
|
|
294
|
+
if (!request) break;
|
|
295
|
+
const name =
|
|
296
|
+
request.type === "call"
|
|
297
|
+
? [...new Set(request.calls.map((call) => call.name))].join(", ")
|
|
298
|
+
: "chat";
|
|
299
|
+
this.#notify(
|
|
300
|
+
`${name} cancelled for ChatGPT ${request.chatId.slice(-4)}: ${message.reason}`,
|
|
301
|
+
"warning",
|
|
302
|
+
);
|
|
247
303
|
if (queued !== -1) {
|
|
248
304
|
this.#queue.splice(queued, 1);
|
|
249
305
|
break;
|
|
250
306
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
307
|
+
const active = this.#active;
|
|
308
|
+
if (!active) break;
|
|
309
|
+
active.cancelled = message.reason;
|
|
310
|
+
if (active.completed) await this.#completeActive();
|
|
254
311
|
else this.#context?.abort();
|
|
255
312
|
break;
|
|
256
313
|
}
|
|
@@ -285,6 +342,15 @@ export class LocalSession {
|
|
|
285
342
|
};
|
|
286
343
|
}
|
|
287
344
|
|
|
345
|
+
async #readGlobalAgents(): Promise<string | undefined> {
|
|
346
|
+
try {
|
|
347
|
+
return await readFile(join(this.#agentDir, "AGENTS.md"), "utf8");
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
288
354
|
#dispatch(): void {
|
|
289
355
|
if (this.#active) return;
|
|
290
356
|
const request = this.#queue[0];
|
|
@@ -298,9 +364,10 @@ export class LocalSession {
|
|
|
298
364
|
this.#queue.shift();
|
|
299
365
|
this.#active = {
|
|
300
366
|
request,
|
|
367
|
+
session: this.#description(),
|
|
301
368
|
message: output.message,
|
|
302
369
|
completed: false,
|
|
303
|
-
cancelled:
|
|
370
|
+
cancelled: undefined,
|
|
304
371
|
toolResults: [],
|
|
305
372
|
};
|
|
306
373
|
this.#status = "executing";
|
|
@@ -342,7 +409,7 @@ export class LocalSession {
|
|
|
342
409
|
this.#context = context;
|
|
343
410
|
const active = this.#active;
|
|
344
411
|
if (!active || message !== active.message) return;
|
|
345
|
-
const sessionId =
|
|
412
|
+
const sessionId = active.session.id;
|
|
346
413
|
for (const result of toolResults) rememberImages(sessionId, result.content);
|
|
347
414
|
active.completed = true;
|
|
348
415
|
active.toolResults = toolResults;
|
|
@@ -353,45 +420,28 @@ export class LocalSession {
|
|
|
353
420
|
if (!active?.completed) return;
|
|
354
421
|
this.#collectInputs();
|
|
355
422
|
const inputs = this.#inputs();
|
|
356
|
-
if (active.cancelled) {
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
: [],
|
|
368
|
-
...(sessionFile
|
|
369
|
-
? { sessionFile }
|
|
370
|
-
: { inlineResults: active.toolResults }),
|
|
371
|
-
...(active.message.errorMessage
|
|
372
|
-
? { error: active.message.errorMessage }
|
|
373
|
-
: { error: "Request cancelled" }),
|
|
374
|
-
};
|
|
375
|
-
this.#deliveries.set(delivery.id, delivery);
|
|
376
|
-
await this.#flushDeliveries().catch(() => {});
|
|
377
|
-
}
|
|
423
|
+
if (active.cancelled !== undefined) {
|
|
424
|
+
const delivery: DeliveryRecord = {
|
|
425
|
+
id: randomUUID(),
|
|
426
|
+
chatId: active.request.chatId,
|
|
427
|
+
sessionId: active.session.id,
|
|
428
|
+
cwd: active.session.cwd,
|
|
429
|
+
toolResults: active.toolResults,
|
|
430
|
+
error: active.cancelled,
|
|
431
|
+
};
|
|
432
|
+
this.#deliveries.set(delivery.id, delivery);
|
|
433
|
+
void this.#flushDeliveries().catch(() => {});
|
|
378
434
|
} else {
|
|
379
|
-
await this.#connection?.send(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
type: "result",
|
|
390
|
-
id: active.request.id,
|
|
391
|
-
message: active.message,
|
|
392
|
-
inputs,
|
|
393
|
-
},
|
|
394
|
-
);
|
|
435
|
+
await this.#connection?.send({
|
|
436
|
+
type: "result",
|
|
437
|
+
id: active.request.id,
|
|
438
|
+
cwd: active.session.cwd,
|
|
439
|
+
message: active.message,
|
|
440
|
+
inputs,
|
|
441
|
+
...(active.request.type === "call"
|
|
442
|
+
? { toolResults: active.toolResults }
|
|
443
|
+
: {}),
|
|
444
|
+
});
|
|
395
445
|
}
|
|
396
446
|
this.#active = undefined;
|
|
397
447
|
this.#status = "idle";
|
|
@@ -401,13 +451,25 @@ export class LocalSession {
|
|
|
401
451
|
#collectInputs(): void {
|
|
402
452
|
const context = this.#context;
|
|
403
453
|
if (!context) return;
|
|
404
|
-
const
|
|
454
|
+
const sessionManager = context.sessionManager;
|
|
455
|
+
const sessionId = sessionManager.getSessionId();
|
|
405
456
|
if (this.#sessionId !== sessionId) {
|
|
406
457
|
this.#sessionId = sessionId;
|
|
458
|
+
this.#inputCursor = null;
|
|
407
459
|
this.#seenInputs.clear();
|
|
408
460
|
this.#pendingInputs.clear();
|
|
409
461
|
}
|
|
410
|
-
|
|
462
|
+
const leafId = sessionManager.getLeafId();
|
|
463
|
+
if (leafId === this.#inputCursor) return;
|
|
464
|
+
const entries: SessionEntry[] = [];
|
|
465
|
+
let current = sessionManager.getLeafEntry();
|
|
466
|
+
while (current && current.id !== this.#inputCursor) {
|
|
467
|
+
entries.push(current);
|
|
468
|
+
current = current.parentId
|
|
469
|
+
? sessionManager.getEntry(current.parentId)
|
|
470
|
+
: undefined;
|
|
471
|
+
}
|
|
472
|
+
for (const entry of entries.reverse()) {
|
|
411
473
|
if (
|
|
412
474
|
entry.type !== "message" ||
|
|
413
475
|
entry.message.role !== "user" ||
|
|
@@ -421,6 +483,7 @@ export class LocalSession {
|
|
|
421
483
|
this.#seenInputs.add(entry.id);
|
|
422
484
|
this.#pendingInputs.set(entry.id, { id: entry.id, sessionId, message });
|
|
423
485
|
}
|
|
486
|
+
this.#inputCursor = leafId;
|
|
424
487
|
}
|
|
425
488
|
|
|
426
489
|
#inputs(): SessionInput[] {
|
|
@@ -439,6 +502,7 @@ export class LocalSession {
|
|
|
439
502
|
await connection.send({ type: "delivery", delivery });
|
|
440
503
|
await completion.promise;
|
|
441
504
|
this.#deliveries.delete(delivery.id);
|
|
505
|
+
this.#notify(`Result saved for ChatGPT ${delivery.chatId.slice(-4)}`);
|
|
442
506
|
} finally {
|
|
443
507
|
this.#stores.delete(delivery.id);
|
|
444
508
|
}
|
package/src/state.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { DeliveryRecord } from "./delivery.ts";
|
|
4
|
+
import type { QuestionAnswer, QuestionRecord } from "./questions.ts";
|
|
4
5
|
|
|
5
6
|
interface StateFile {
|
|
6
7
|
bindings?: Record<string, string>;
|
|
8
|
+
workflows?: Record<string, string>;
|
|
7
9
|
deliveries?: DeliveryRecord[];
|
|
10
|
+
questions?: QuestionRecord[];
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
export class State {
|
|
11
14
|
readonly #path: string;
|
|
12
15
|
readonly #temporaryPath: string;
|
|
13
16
|
readonly #bindings = new Map<string, string>();
|
|
17
|
+
readonly #workflows = new Map<string, string>();
|
|
14
18
|
readonly #deliveries = new Map<string, DeliveryRecord>();
|
|
19
|
+
readonly #questions = new Map<string, QuestionRecord>();
|
|
15
20
|
#writes = Promise.resolve();
|
|
16
21
|
|
|
17
22
|
constructor(agentDir: string) {
|
|
@@ -31,17 +36,41 @@ export class State {
|
|
|
31
36
|
for (const [chatId, sessionId] of Object.entries(state.bindings ?? {})) {
|
|
32
37
|
if (typeof sessionId === "string") this.#bindings.set(chatId, sessionId);
|
|
33
38
|
}
|
|
39
|
+
for (const [chatId, id] of Object.entries(state.workflows ?? {})) {
|
|
40
|
+
this.#workflows.set(chatId, id);
|
|
41
|
+
}
|
|
34
42
|
for (const delivery of state.deliveries ?? []) {
|
|
35
43
|
if (delivery?.id) this.#deliveries.set(delivery.id, delivery);
|
|
36
44
|
}
|
|
45
|
+
for (const question of state.questions ?? [])
|
|
46
|
+
this.#questions.set(question.id, question);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
workflow(chatId: string): string | undefined {
|
|
50
|
+
return this.#workflows.get(chatId);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setWorkflow(chatId: string, id: string): Promise<void> {
|
|
54
|
+
this.#workflows.set(chatId, id);
|
|
55
|
+
return this.#save();
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
binding(chatId: string): string | undefined {
|
|
40
59
|
return this.#bindings.get(chatId);
|
|
41
60
|
}
|
|
42
61
|
|
|
43
|
-
|
|
44
|
-
return
|
|
62
|
+
chats(sessionId: string): string[] {
|
|
63
|
+
return [...this.#bindings]
|
|
64
|
+
.filter(([, target]) => target === sessionId)
|
|
65
|
+
.map(([chatId]) => chatId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
bindingCounts(): Map<string, number> {
|
|
69
|
+
const counts = new Map<string, number>();
|
|
70
|
+
for (const sessionId of this.#bindings.values()) {
|
|
71
|
+
counts.set(sessionId, (counts.get(sessionId) ?? 0) + 1);
|
|
72
|
+
}
|
|
73
|
+
return counts;
|
|
45
74
|
}
|
|
46
75
|
|
|
47
76
|
setBinding(chatId: string, sessionId: string): Promise<void> {
|
|
@@ -60,23 +89,89 @@ export class State {
|
|
|
60
89
|
return this.#save();
|
|
61
90
|
}
|
|
62
91
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
question(chatId: string, id: string): QuestionRecord {
|
|
93
|
+
const question = this.#questions.get(id);
|
|
94
|
+
if (!question || question.chatId !== chatId)
|
|
95
|
+
throw new Error("Question not found in this ChatGPT conversation");
|
|
96
|
+
return question;
|
|
66
97
|
}
|
|
67
98
|
|
|
68
|
-
async
|
|
69
|
-
|
|
70
|
-
this.#deliveries.set(delivery.id, delivery);
|
|
99
|
+
async addQuestion(question: QuestionRecord): Promise<void> {
|
|
100
|
+
this.#questions.set(question.id, question);
|
|
71
101
|
await this.#save();
|
|
72
102
|
}
|
|
73
103
|
|
|
104
|
+
async answer(
|
|
105
|
+
chatId: string,
|
|
106
|
+
id: string,
|
|
107
|
+
answer: QuestionAnswer,
|
|
108
|
+
): Promise<QuestionRecord> {
|
|
109
|
+
const question = this.question(chatId, id);
|
|
110
|
+
const selections = [...new Set(answer.selections)].sort((a, b) => a - b);
|
|
111
|
+
if (selections.some((index) => !question.options[index]))
|
|
112
|
+
throw new Error("Unknown question option");
|
|
113
|
+
if (!question.allowMultiple && selections.length > 1)
|
|
114
|
+
throw new Error("Select one option");
|
|
115
|
+
if (answer.skipped && (selections.length > 0 || answer.text))
|
|
116
|
+
throw new Error("A skipped question cannot include an answer");
|
|
117
|
+
if (!answer.skipped && selections.length === 0 && !answer.text)
|
|
118
|
+
throw new Error("Select an option or enter an answer");
|
|
119
|
+
const value: QuestionAnswer = {
|
|
120
|
+
selections,
|
|
121
|
+
text: answer.text,
|
|
122
|
+
...(answer.skipped ? { skipped: true } : {}),
|
|
123
|
+
};
|
|
124
|
+
if (JSON.stringify(question.answer) === JSON.stringify(value))
|
|
125
|
+
return question;
|
|
126
|
+
const updated = { ...question, answer: value, delivered: false };
|
|
127
|
+
await this.addQuestion(updated);
|
|
128
|
+
return updated;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
answers(chatId: string): QuestionRecord[] {
|
|
132
|
+
return [...this.#questions.values()].filter(
|
|
133
|
+
(question) =>
|
|
134
|
+
question.chatId === chatId && question.answer && !question.delivered,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async acknowledge(
|
|
139
|
+
deliveries: DeliveryRecord[],
|
|
140
|
+
answers: QuestionRecord[],
|
|
141
|
+
signal: AbortSignal,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
if (deliveries.length === 0 && answers.length === 0) return;
|
|
144
|
+
signal.throwIfAborted();
|
|
145
|
+
const delivered = new Map(
|
|
146
|
+
answers.map((question) => [question, { ...question, delivered: true }]),
|
|
147
|
+
);
|
|
148
|
+
for (const delivery of deliveries) this.#deliveries.delete(delivery.id);
|
|
149
|
+
for (const [question, updated] of delivered) {
|
|
150
|
+
if (this.#questions.get(question.id) === question)
|
|
151
|
+
this.#questions.set(question.id, updated);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
await this.#save();
|
|
155
|
+
signal.throwIfAborted();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
for (const delivery of deliveries)
|
|
158
|
+
this.#deliveries.set(delivery.id, delivery);
|
|
159
|
+
for (const [question, updated] of delivered) {
|
|
160
|
+
if (this.#questions.get(question.id) === updated)
|
|
161
|
+
this.#questions.set(question.id, question);
|
|
162
|
+
}
|
|
163
|
+
await this.#save();
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
74
168
|
#save(): Promise<void> {
|
|
75
169
|
const saved = this.#writes.then(async () => {
|
|
76
170
|
const bindings = Object.fromEntries(this.#bindings);
|
|
171
|
+
const workflows = Object.fromEntries(this.#workflows);
|
|
77
172
|
await writeFile(
|
|
78
173
|
this.#temporaryPath,
|
|
79
|
-
`${JSON.stringify({ bindings, deliveries: [...this.#deliveries.values()] }, null, 2)}\n`,
|
|
174
|
+
`${JSON.stringify({ bindings, workflows, deliveries: [...this.#deliveries.values()], questions: [...this.#questions.values()] }, null, 2)}\n`,
|
|
80
175
|
{
|
|
81
176
|
mode: 0o600,
|
|
82
177
|
},
|
package/src/stdio.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Writable } from "node:stream";
|
|
2
2
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import {
|
|
4
4
|
StdioServerTransport,
|
|
@@ -15,7 +15,15 @@ const terminationSignals =
|
|
|
15
15
|
export async function serveChappie(): Promise<never> {
|
|
16
16
|
const broker = new Broker(getAgentDir());
|
|
17
17
|
await broker.start();
|
|
18
|
-
const output =
|
|
18
|
+
const output = new Writable({
|
|
19
|
+
write(chunk, encoding, callback) {
|
|
20
|
+
Reflect.apply(Writable.prototype.write, process.stdout, [
|
|
21
|
+
chunk,
|
|
22
|
+
encoding,
|
|
23
|
+
callback,
|
|
24
|
+
]);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
19
27
|
const transport = new StdioServerTransport(process.stdin, output);
|
|
20
28
|
let stop: (() => void) | undefined;
|
|
21
29
|
const stopped = new Promise<void>((resolve) => {
|
|
@@ -23,6 +31,7 @@ export async function serveChappie(): Promise<never> {
|
|
|
23
31
|
});
|
|
24
32
|
const requestStop = (): void => stop?.();
|
|
25
33
|
|
|
34
|
+
output.once("error", requestStop);
|
|
26
35
|
process.stdin.once("end", requestStop);
|
|
27
36
|
process.stdin.once("close", requestStop);
|
|
28
37
|
for (const signal of terminationSignals) {
|
package/src/tools.ts
CHANGED
|
@@ -6,11 +6,9 @@ import {
|
|
|
6
6
|
createWriteToolDefinition,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { fromJsonSchema } from "@modelcontextprotocol/server";
|
|
9
|
+
import { toolResultsContent } from "./delivery.ts";
|
|
9
10
|
import type { SessionInput } from "./ipc.ts";
|
|
10
|
-
import {
|
|
11
|
-
contentWithImageReferences,
|
|
12
|
-
resourceDescriptors,
|
|
13
|
-
} from "./resources.ts";
|
|
11
|
+
import { contentWithImageReferences } from "./resources.ts";
|
|
14
12
|
import { transfer } from "./transfer.ts";
|
|
15
13
|
|
|
16
14
|
export interface ToolInput {
|
|
@@ -38,28 +36,13 @@ export const directTools = definitions.map((definition) => ({
|
|
|
38
36
|
export function toolResult(
|
|
39
37
|
toolResults: ToolResultMessage[],
|
|
40
38
|
sessionId: string,
|
|
39
|
+
cwd: string,
|
|
41
40
|
inputs: SessionInput[] = [],
|
|
42
41
|
) {
|
|
43
42
|
return {
|
|
44
43
|
content: [
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
type: "text" as const,
|
|
48
|
-
text: JSON.stringify({
|
|
49
|
-
toolCallId: result.toolCallId,
|
|
50
|
-
toolName: result.toolName,
|
|
51
|
-
isError: result.isError,
|
|
52
|
-
}),
|
|
53
|
-
},
|
|
54
|
-
...contentWithImageReferences(sessionId, result.content),
|
|
55
|
-
...resourceDescriptors(result.details).map((resource) => ({
|
|
56
|
-
type: "resource_link" as const,
|
|
57
|
-
uri: resource.uri,
|
|
58
|
-
name: resource.name,
|
|
59
|
-
mimeType: resource.mimeType,
|
|
60
|
-
size: resource.size,
|
|
61
|
-
})),
|
|
62
|
-
]),
|
|
44
|
+
{ type: "text" as const, text: JSON.stringify({ sessionId, cwd }) },
|
|
45
|
+
...toolResultsContent(toolResults, sessionId),
|
|
63
46
|
...inputContent(inputs),
|
|
64
47
|
],
|
|
65
48
|
isError: toolResults.some((result) => result.isError),
|
|
@@ -70,7 +53,7 @@ export function inputContent(inputs: SessionInput[]) {
|
|
|
70
53
|
return inputs.flatMap(({ id, sessionId, message }) => [
|
|
71
54
|
{
|
|
72
55
|
type: "text" as const,
|
|
73
|
-
text: JSON.stringify({ piInput: id }),
|
|
56
|
+
text: JSON.stringify({ piInput: id, sessionId }),
|
|
74
57
|
},
|
|
75
58
|
...(typeof message.content === "string"
|
|
76
59
|
? [{ type: "text" as const, text: message.content }]
|
package/src/transfer.ts
CHANGED
|
@@ -28,18 +28,18 @@ export const transfer = {
|
|
|
28
28
|
name: "transfer",
|
|
29
29
|
label: "transfer",
|
|
30
30
|
description:
|
|
31
|
-
"Copy ChatGPT files into Pi paths
|
|
31
|
+
"Copy ChatGPT files into Pi paths when files is supplied. Otherwise, return resource links for Pi paths or Chappie image references.",
|
|
32
32
|
parameters: Type.Object({
|
|
33
33
|
paths: Type.Array(Type.String(), {
|
|
34
34
|
minItems: 1,
|
|
35
35
|
description:
|
|
36
|
-
"Pi
|
|
36
|
+
"Pi destinations for import; Pi paths or chappie:// image references for export",
|
|
37
37
|
}),
|
|
38
38
|
files: Type.Optional(
|
|
39
39
|
Type.Array(transferFile, {
|
|
40
40
|
minItems: 1,
|
|
41
41
|
description:
|
|
42
|
-
"ChatGPT files
|
|
42
|
+
"ChatGPT files paired with paths in order; omit for export",
|
|
43
43
|
}),
|
|
44
44
|
),
|
|
45
45
|
overwrite: Type.Optional(
|