@zetaloop/chappie 0.2.1 → 0.3.1
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 -74
- package/package.json +3 -1
- package/src/broker.ts +193 -35
- package/src/config.ts +21 -0
- package/src/delivery.ts +25 -56
- package/src/index.ts +15 -10
- package/src/instructions.md +7 -5
- package/src/ipc.ts +142 -40
- package/src/provider.ts +3 -5
- package/src/question.html +384 -0
- package/src/questions.ts +95 -0
- package/src/server.ts +206 -62
- package/src/session.ts +136 -52
- package/src/state.ts +98 -7
- package/src/stdio.ts +11 -2
- package/src/tools.ts +3 -22
- 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;
|
|
@@ -38,17 +47,17 @@ interface ActiveRequest {
|
|
|
38
47
|
session: SessionDescription;
|
|
39
48
|
message: AssistantMessage;
|
|
40
49
|
completed: boolean;
|
|
41
|
-
cancelled:
|
|
50
|
+
cancelled: string | undefined;
|
|
42
51
|
toolResults: ToolResultMessage[];
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
export class LocalSession {
|
|
46
55
|
readonly #pi: ExtensionAPI;
|
|
47
56
|
readonly #agentDir: string;
|
|
57
|
+
readonly #connect: string | undefined;
|
|
48
58
|
readonly #syncs = new Map<number, SyncRequest>();
|
|
49
59
|
readonly #stores = new Map<string, StoreRequest>();
|
|
50
60
|
readonly #queue: RemoteRequest[] = [];
|
|
51
|
-
readonly #seenInputs = new Set<string>();
|
|
52
61
|
readonly #pendingInputs = new Map<string, SessionInput>();
|
|
53
62
|
readonly #deliveries = new Map<string, DeliveryRecord>();
|
|
54
63
|
#context: ExtensionContext | undefined;
|
|
@@ -59,14 +68,27 @@ export class LocalSession {
|
|
|
59
68
|
#nextSyncId = 1;
|
|
60
69
|
#starting = false;
|
|
61
70
|
#sessionId: string | undefined;
|
|
71
|
+
#inputCursor: string | null = null;
|
|
62
72
|
#flushing = Promise.resolve();
|
|
63
73
|
|
|
64
|
-
constructor(pi: ExtensionAPI, agentDir: string) {
|
|
74
|
+
constructor(pi: ExtensionAPI, agentDir: string, connect?: string) {
|
|
65
75
|
this.#pi = pi;
|
|
66
76
|
this.#agentDir = agentDir;
|
|
77
|
+
this.#connect = connect;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
install(): void {
|
|
81
|
+
this.#pi.registerEntryRenderer<Notice>(
|
|
82
|
+
"chappie.notice",
|
|
83
|
+
({ data }, _options, theme) => {
|
|
84
|
+
if (!data) return;
|
|
85
|
+
return new Text(
|
|
86
|
+
theme.fg(data.type === "info" ? "dim" : data.type, data.message),
|
|
87
|
+
1,
|
|
88
|
+
0,
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
);
|
|
70
92
|
this.#pi.on("session_start", (_event, context) => this.#update(context));
|
|
71
93
|
this.#pi.on("model_select", (event, context) =>
|
|
72
94
|
this.#update(context, event.model.provider === "chappie"),
|
|
@@ -75,11 +97,21 @@ export class LocalSession {
|
|
|
75
97
|
this.#context = context;
|
|
76
98
|
void this.#sync().catch(() => {});
|
|
77
99
|
});
|
|
78
|
-
this.#pi.on("
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
this.#pi.on("session_tree", (event, context) => {
|
|
101
|
+
this.#context = context;
|
|
102
|
+
if (context.model?.provider === "chappie") {
|
|
103
|
+
this.#resetInputs(context, event.newLeafId);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
this.#pi.on("context", (event, context) => ({
|
|
107
|
+
messages:
|
|
108
|
+
context.model?.provider === "chappie"
|
|
109
|
+
? []
|
|
110
|
+
: event.messages.filter(
|
|
111
|
+
(message) =>
|
|
112
|
+
message.role !== "custom" ||
|
|
113
|
+
message.customType !== "chappie.request",
|
|
114
|
+
),
|
|
83
115
|
}));
|
|
84
116
|
this.#pi.on("turn_end", (event, context) =>
|
|
85
117
|
this.#turnEnd(event.message, event.toolResults, context),
|
|
@@ -94,10 +126,12 @@ export class LocalSession {
|
|
|
94
126
|
this.#pi.on("session_shutdown", () => this.close());
|
|
95
127
|
}
|
|
96
128
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
129
|
+
#notify(message: string, type: Notice["type"] = "info"): void {
|
|
130
|
+
if (this.#context)
|
|
131
|
+
this.#pi.appendEntry<Notice>("chappie.notice", { message, type });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async start(output: ProviderOutput): Promise<void> {
|
|
101
135
|
const context = this.#context;
|
|
102
136
|
const connection = this.#connection;
|
|
103
137
|
if (context?.model?.provider !== "chappie" || !connection) {
|
|
@@ -142,6 +176,7 @@ export class LocalSession {
|
|
|
142
176
|
this.#connection?.close();
|
|
143
177
|
this.#connection = undefined;
|
|
144
178
|
this.#context = undefined;
|
|
179
|
+
this.#resetInputs();
|
|
145
180
|
this.#rejectSyncs(new Error("Chappie session ended"));
|
|
146
181
|
this.#rejectStores(new Error("Chappie session ended"));
|
|
147
182
|
}
|
|
@@ -155,8 +190,12 @@ export class LocalSession {
|
|
|
155
190
|
this.close();
|
|
156
191
|
return;
|
|
157
192
|
}
|
|
193
|
+
if (this.#sessionId !== context.sessionManager.getSessionId()) {
|
|
194
|
+
this.#resetInputs(context);
|
|
195
|
+
}
|
|
158
196
|
if (!this.#connection) {
|
|
159
|
-
this.#
|
|
197
|
+
this.#notify("Connecting to Chappie…");
|
|
198
|
+
this.#connection = new IpcClient(this.#agentDir, this.#connect, {
|
|
160
199
|
onOpen: async () => {
|
|
161
200
|
await this.#sync();
|
|
162
201
|
await this.#flushDeliveries();
|
|
@@ -165,7 +204,8 @@ export class LocalSession {
|
|
|
165
204
|
onClose: (error) => {
|
|
166
205
|
this.#rejectSyncs(error);
|
|
167
206
|
this.#rejectStores(error);
|
|
168
|
-
this.#output
|
|
207
|
+
if (this.#output && !this.#output.closed) this.#output.fail(error);
|
|
208
|
+
else this.#notify(error.message, "error");
|
|
169
209
|
this.#active = undefined;
|
|
170
210
|
this.#queue.length = 0;
|
|
171
211
|
this.#starting = false;
|
|
@@ -182,13 +222,12 @@ export class LocalSession {
|
|
|
182
222
|
const context = this.#context;
|
|
183
223
|
if (!context) throw new Error("Chappie session is not available");
|
|
184
224
|
const name = this.#pi.getSessionName();
|
|
185
|
-
const sessionFile = context.sessionManager.getSessionFile();
|
|
186
225
|
return {
|
|
187
226
|
id: context.sessionManager.getSessionId(),
|
|
188
227
|
cwd: context.cwd,
|
|
228
|
+
device: hostname(),
|
|
189
229
|
status: this.#status,
|
|
190
230
|
...(name ? { name } : {}),
|
|
191
|
-
...(sessionFile ? { sessionFile } : {}),
|
|
192
231
|
};
|
|
193
232
|
}
|
|
194
233
|
|
|
@@ -219,13 +258,24 @@ export class LocalSession {
|
|
|
219
258
|
case "stored":
|
|
220
259
|
this.#stores.get(message.id)?.resolve();
|
|
221
260
|
break;
|
|
261
|
+
case "notice":
|
|
262
|
+
if (
|
|
263
|
+
message.sessionId === this.#context?.sessionManager.getSessionId()
|
|
264
|
+
) {
|
|
265
|
+
this.#notify(message.message);
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
222
268
|
case "inspect":
|
|
223
|
-
await this.#reply(message.id, message.sessionId, () =>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
269
|
+
await this.#reply(message.id, message.sessionId, async () => {
|
|
270
|
+
const globalAgents = await this.#readGlobalAgents();
|
|
271
|
+
return {
|
|
272
|
+
type: "result",
|
|
273
|
+
id: message.id,
|
|
274
|
+
inspection: this.#inspection(),
|
|
275
|
+
inputs: this.#inputs(),
|
|
276
|
+
...(globalAgents ? { globalAgents } : {}),
|
|
277
|
+
};
|
|
278
|
+
});
|
|
229
279
|
break;
|
|
230
280
|
case "ackInputs":
|
|
231
281
|
if (
|
|
@@ -245,13 +295,28 @@ export class LocalSession {
|
|
|
245
295
|
const queued = this.#queue.findIndex(
|
|
246
296
|
(request) => request.id === message.id,
|
|
247
297
|
);
|
|
298
|
+
const request =
|
|
299
|
+
this.#queue[queued] ??
|
|
300
|
+
(this.#active?.request.id === message.id
|
|
301
|
+
? this.#active.request
|
|
302
|
+
: undefined);
|
|
303
|
+
if (!request) break;
|
|
304
|
+
const name =
|
|
305
|
+
request.type === "call"
|
|
306
|
+
? [...new Set(request.calls.map((call) => call.name))].join(", ")
|
|
307
|
+
: "chat";
|
|
308
|
+
this.#notify(
|
|
309
|
+
`${name} cancelled for ChatGPT ${request.chatId.slice(-4)}: ${message.reason}`,
|
|
310
|
+
"warning",
|
|
311
|
+
);
|
|
248
312
|
if (queued !== -1) {
|
|
249
313
|
this.#queue.splice(queued, 1);
|
|
250
314
|
break;
|
|
251
315
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
316
|
+
const active = this.#active;
|
|
317
|
+
if (!active) break;
|
|
318
|
+
active.cancelled = message.reason;
|
|
319
|
+
if (active.completed) await this.#completeActive();
|
|
255
320
|
else this.#context?.abort();
|
|
256
321
|
break;
|
|
257
322
|
}
|
|
@@ -286,6 +351,15 @@ export class LocalSession {
|
|
|
286
351
|
};
|
|
287
352
|
}
|
|
288
353
|
|
|
354
|
+
async #readGlobalAgents(): Promise<string | undefined> {
|
|
355
|
+
try {
|
|
356
|
+
return await readFile(join(this.#agentDir, "AGENTS.md"), "utf8");
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
289
363
|
#dispatch(): void {
|
|
290
364
|
if (this.#active) return;
|
|
291
365
|
const request = this.#queue[0];
|
|
@@ -302,7 +376,7 @@ export class LocalSession {
|
|
|
302
376
|
session: this.#description(),
|
|
303
377
|
message: output.message,
|
|
304
378
|
completed: false,
|
|
305
|
-
cancelled:
|
|
379
|
+
cancelled: undefined,
|
|
306
380
|
toolResults: [],
|
|
307
381
|
};
|
|
308
382
|
this.#status = "executing";
|
|
@@ -355,26 +429,17 @@ export class LocalSession {
|
|
|
355
429
|
if (!active?.completed) return;
|
|
356
430
|
this.#collectInputs();
|
|
357
431
|
const inputs = this.#inputs();
|
|
358
|
-
if (active.cancelled) {
|
|
359
|
-
const sessionFile = active.session.sessionFile;
|
|
432
|
+
if (active.cancelled !== undefined) {
|
|
360
433
|
const delivery: DeliveryRecord = {
|
|
361
434
|
id: randomUUID(),
|
|
362
435
|
chatId: active.request.chatId,
|
|
363
436
|
sessionId: active.session.id,
|
|
364
437
|
cwd: active.session.cwd,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
? active.request.calls.map(({ id }) => id)
|
|
368
|
-
: [],
|
|
369
|
-
...(sessionFile
|
|
370
|
-
? { sessionFile }
|
|
371
|
-
: { inlineResults: active.toolResults }),
|
|
372
|
-
...(active.message.errorMessage
|
|
373
|
-
? { error: active.message.errorMessage }
|
|
374
|
-
: { error: "Request cancelled" }),
|
|
438
|
+
toolResults: active.toolResults,
|
|
439
|
+
error: active.cancelled,
|
|
375
440
|
};
|
|
376
441
|
this.#deliveries.set(delivery.id, delivery);
|
|
377
|
-
|
|
442
|
+
void this.#flushDeliveries().catch(() => {});
|
|
378
443
|
} else {
|
|
379
444
|
await this.#connection?.send({
|
|
380
445
|
type: "result",
|
|
@@ -392,29 +457,47 @@ export class LocalSession {
|
|
|
392
457
|
void this.#sync().catch(() => {});
|
|
393
458
|
}
|
|
394
459
|
|
|
460
|
+
#resetInputs(
|
|
461
|
+
context?: ExtensionContext,
|
|
462
|
+
cursor = context?.sessionManager.getLeafId() ?? null,
|
|
463
|
+
): void {
|
|
464
|
+
this.#sessionId = context?.sessionManager.getSessionId();
|
|
465
|
+
this.#inputCursor = cursor;
|
|
466
|
+
this.#pendingInputs.clear();
|
|
467
|
+
}
|
|
468
|
+
|
|
395
469
|
#collectInputs(): void {
|
|
396
470
|
const context = this.#context;
|
|
397
471
|
if (!context) return;
|
|
398
|
-
const
|
|
472
|
+
const sessionManager = context.sessionManager;
|
|
473
|
+
const sessionId = sessionManager.getSessionId();
|
|
399
474
|
if (this.#sessionId !== sessionId) {
|
|
400
|
-
this.#
|
|
401
|
-
|
|
402
|
-
|
|
475
|
+
this.#resetInputs(context);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const leafId = sessionManager.getLeafId();
|
|
479
|
+
if (leafId === this.#inputCursor) return;
|
|
480
|
+
const entries: SessionEntry[] = [];
|
|
481
|
+
let current = sessionManager.getLeafEntry();
|
|
482
|
+
while (current && current.id !== this.#inputCursor) {
|
|
483
|
+
entries.push(current);
|
|
484
|
+
current = current.parentId
|
|
485
|
+
? sessionManager.getEntry(current.parentId)
|
|
486
|
+
: undefined;
|
|
487
|
+
}
|
|
488
|
+
if (this.#inputCursor !== null && !current) {
|
|
489
|
+
this.#resetInputs(context);
|
|
490
|
+
return;
|
|
403
491
|
}
|
|
404
|
-
for (const entry of
|
|
405
|
-
if (
|
|
406
|
-
entry.type !== "message" ||
|
|
407
|
-
entry.message.role !== "user" ||
|
|
408
|
-
this.#seenInputs.has(entry.id)
|
|
409
|
-
)
|
|
410
|
-
continue;
|
|
492
|
+
for (const entry of entries.reverse()) {
|
|
493
|
+
if (entry.type !== "message" || entry.message.role !== "user") continue;
|
|
411
494
|
const message = entry.message as UserMessage;
|
|
412
495
|
if (typeof message.content !== "string") {
|
|
413
496
|
rememberImages(sessionId, message.content);
|
|
414
497
|
}
|
|
415
|
-
this.#seenInputs.add(entry.id);
|
|
416
498
|
this.#pendingInputs.set(entry.id, { id: entry.id, sessionId, message });
|
|
417
499
|
}
|
|
500
|
+
this.#inputCursor = leafId;
|
|
418
501
|
}
|
|
419
502
|
|
|
420
503
|
#inputs(): SessionInput[] {
|
|
@@ -433,6 +516,7 @@ export class LocalSession {
|
|
|
433
516
|
await connection.send({ type: "delivery", delivery });
|
|
434
517
|
await completion.promise;
|
|
435
518
|
this.#deliveries.delete(delivery.id);
|
|
519
|
+
this.#notify(`Result saved for ChatGPT ${delivery.chatId.slice(-4)}`);
|
|
436
520
|
} finally {
|
|
437
521
|
this.#stores.delete(delivery.id);
|
|
438
522
|
}
|
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,15 +36,35 @@ 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
|
|
|
62
|
+
chats(sessionId: string): string[] {
|
|
63
|
+
return [...this.#bindings]
|
|
64
|
+
.filter(([, target]) => target === sessionId)
|
|
65
|
+
.map(([chatId]) => chatId);
|
|
66
|
+
}
|
|
67
|
+
|
|
43
68
|
bindingCounts(): Map<string, number> {
|
|
44
69
|
const counts = new Map<string, number>();
|
|
45
70
|
for (const sessionId of this.#bindings.values()) {
|
|
@@ -64,23 +89,89 @@ export class State {
|
|
|
64
89
|
return this.#save();
|
|
65
90
|
}
|
|
66
91
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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;
|
|
70
97
|
}
|
|
71
98
|
|
|
72
|
-
async
|
|
73
|
-
|
|
74
|
-
this.#deliveries.set(delivery.id, delivery);
|
|
99
|
+
async addQuestion(question: QuestionRecord): Promise<void> {
|
|
100
|
+
this.#questions.set(question.id, question);
|
|
75
101
|
await this.#save();
|
|
76
102
|
}
|
|
77
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
|
+
|
|
78
168
|
#save(): Promise<void> {
|
|
79
169
|
const saved = this.#writes.then(async () => {
|
|
80
170
|
const bindings = Object.fromEntries(this.#bindings);
|
|
171
|
+
const workflows = Object.fromEntries(this.#workflows);
|
|
81
172
|
await writeFile(
|
|
82
173
|
this.#temporaryPath,
|
|
83
|
-
`${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`,
|
|
84
175
|
{
|
|
85
176
|
mode: 0o600,
|
|
86
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 {
|
|
@@ -44,24 +42,7 @@ export function toolResult(
|
|
|
44
42
|
return {
|
|
45
43
|
content: [
|
|
46
44
|
{ type: "text" as const, text: JSON.stringify({ sessionId, cwd }) },
|
|
47
|
-
...toolResults
|
|
48
|
-
{
|
|
49
|
-
type: "text" as const,
|
|
50
|
-
text: JSON.stringify({
|
|
51
|
-
toolCallId: result.toolCallId,
|
|
52
|
-
toolName: result.toolName,
|
|
53
|
-
isError: result.isError,
|
|
54
|
-
}),
|
|
55
|
-
},
|
|
56
|
-
...contentWithImageReferences(sessionId, result.content),
|
|
57
|
-
...resourceDescriptors(result.details).map((resource) => ({
|
|
58
|
-
type: "resource_link" as const,
|
|
59
|
-
uri: resource.uri,
|
|
60
|
-
name: resource.name,
|
|
61
|
-
mimeType: resource.mimeType,
|
|
62
|
-
size: resource.size,
|
|
63
|
-
})),
|
|
64
|
-
]),
|
|
45
|
+
...toolResultsContent(toolResults, sessionId),
|
|
65
46
|
...inputContent(inputs),
|
|
66
47
|
],
|
|
67
48
|
isError: toolResults.some((result) => result.isError),
|
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(
|