pi-session-mail 0.3.0 → 0.5.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/CHANGELOG.md +57 -1
- package/README.md +51 -31
- package/package.json +3 -1
- package/src/index.ts +152 -55
- package/src/mail-status.ts +68 -0
- package/src/picker.ts +285 -0
- package/src/store.ts +150 -38
- package/src/tools.ts +39 -31
package/src/tools.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
// `session_mail_list` reports every running Pi session on this machine, in
|
|
6
6
|
// every agent directory, and marks the calling one. `session_mail_send` leaves a plain
|
|
7
7
|
// message, which wakes or steers its recipient and expects no answer.
|
|
8
|
-
// `session_mail_ask` leaves an ask
|
|
9
|
-
//
|
|
8
|
+
// `session_mail_ask` leaves an ask. In a session a person is watching it
|
|
9
|
+
// returns at once and the answer arrives as a message; elsewhere it waits, one
|
|
10
|
+
// at a time, for the answer, which comes back as its result. An unattended
|
|
11
|
+
// session must wait because ending its turn ends its work: a delegate's settle
|
|
12
|
+
// is its result, and print mode exits.
|
|
10
13
|
// `session_mail_reply` answers an ask this session received, mid-run; the asks
|
|
11
14
|
// this session owes, and the kind of every envelope it has received, are kept
|
|
12
15
|
// here for it. Mail a
|
|
@@ -24,8 +27,6 @@ import { type Envelope, send } from "./store.ts";
|
|
|
24
27
|
|
|
25
28
|
/** What the tools need from the extension around them. */
|
|
26
29
|
export type ToolHooks = {
|
|
27
|
-
/** Called after a tool wrote mail, so the footer can count it. */
|
|
28
|
-
sent: () => void;
|
|
29
30
|
/** The current turn's hop count, which mail sent now carries. */
|
|
30
31
|
hops: () => number;
|
|
31
32
|
/** Report a broken `session-mail.json`; the extension shows one warning per session. */
|
|
@@ -55,8 +56,14 @@ const ASK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
55
56
|
/** How often a waiting ask checks that its target still runs. */
|
|
56
57
|
const TARGET_CHECK_MS = 1000;
|
|
57
58
|
|
|
58
|
-
/**
|
|
59
|
+
/** Whether a person is watching this session, so an ask need not hold its turn; `PI_UNATTENDED=1` marks a TUI nobody watches. */
|
|
60
|
+
function attended(ctx: ExtensionContext): boolean {
|
|
61
|
+
return ctx.mode === "tui" && process.env.PI_UNATTENDED !== "1";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** How an ask's tool call ended: at once in the background, or how its wait ended. */
|
|
59
65
|
type AskOutcome =
|
|
66
|
+
| { outcome: "background" }
|
|
60
67
|
| { outcome: "answered"; status: string; body: string }
|
|
61
68
|
| { outcome: "timed-out" }
|
|
62
69
|
| { outcome: "stopped-running" }
|
|
@@ -68,6 +75,8 @@ type WaitingAsk = { id: string; to: string; finish: (outcome: AskOutcome) => voi
|
|
|
68
75
|
function askText(ask: Envelope, target: string, outcome: AskOutcome): string {
|
|
69
76
|
const later = "A later answer arrives as a message.";
|
|
70
77
|
switch (outcome.outcome) {
|
|
78
|
+
case "background":
|
|
79
|
+
return `Sent ask ${ask.id} to ${target}. This session is not waiting: the answer arrives as a message and starts a turn on its own, so go on with other work or end your turn.`;
|
|
71
80
|
case "answered":
|
|
72
81
|
return outcome.status === "done"
|
|
73
82
|
? `${target} answered your ask ${ask.id}:\n\n${outcome.body}`
|
|
@@ -168,6 +177,7 @@ const AskResult = Type.Union([
|
|
|
168
177
|
Type.Object({ ...asked, outcome: Type.Literal("timed-out") }),
|
|
169
178
|
Type.Object({ ...asked, outcome: Type.Literal("stopped-running") }),
|
|
170
179
|
Type.Object({ ...asked, outcome: Type.Literal("stopped-by-user") }),
|
|
180
|
+
Type.Object({ ...asked, outcome: Type.Literal("background") }),
|
|
171
181
|
]);
|
|
172
182
|
|
|
173
183
|
const ReplyResult = Type.Object({
|
|
@@ -280,7 +290,6 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
280
290
|
const hops = hooks.hops();
|
|
281
291
|
checkHops(hops, hooks);
|
|
282
292
|
const envelope = send(me, to, body, { kind: "message", hops });
|
|
283
|
-
hooks.sent();
|
|
284
293
|
const target = listRunning().find((record) => record.address === to);
|
|
285
294
|
const text =
|
|
286
295
|
target === undefined
|
|
@@ -295,14 +304,13 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
295
304
|
name: "session_mail_ask",
|
|
296
305
|
label: "Session mail: ask",
|
|
297
306
|
description:
|
|
298
|
-
"Ask another running Pi session on this machine a question
|
|
307
|
+
"Ask another running Pi session on this machine a question. The answer is the other session's final message when its run settles, or an earlier reply it sends. In a session a person is watching, the ask returns at once and the answer arrives later as a message that starts a turn. In an unattended session, such as a delegate, the ask waits up to 10 minutes and the answer comes back as this tool's result. `to` is a running session's name, an id prefix of at least 8 characters, or a full session id; session_mail_list shows who is running. For a note that needs no answer, use session_mail_send.",
|
|
299
308
|
parameters: Type.Object({
|
|
300
309
|
to: Type.String({
|
|
301
310
|
description: "The running session to ask: its name, an id prefix of 8+ characters, or its full session id.",
|
|
302
311
|
}),
|
|
303
312
|
message: Type.String({
|
|
304
|
-
description:
|
|
305
|
-
"The question. The recipient sees it labelled as coming from this session, with this session waiting.",
|
|
313
|
+
description: "The question. The recipient sees it labelled as coming from this session.",
|
|
306
314
|
}),
|
|
307
315
|
}),
|
|
308
316
|
outputSchema: AskResult,
|
|
@@ -335,27 +343,28 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
335
343
|
const hops = hooks.hops();
|
|
336
344
|
checkHops(hops, hooks);
|
|
337
345
|
const ask = send(me, to, body, { kind: "ask", hops });
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
346
|
+
const outcome: AskOutcome = attended(ctx)
|
|
347
|
+
? { outcome: "background" }
|
|
348
|
+
: await new Promise<AskOutcome>((resolve) => {
|
|
349
|
+
const finish = (result: AskOutcome) => {
|
|
350
|
+
if (waiting?.id !== ask.id) return;
|
|
351
|
+
waiting = undefined;
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
clearInterval(check);
|
|
354
|
+
signal?.removeEventListener("abort", onAbort);
|
|
355
|
+
hooks.waitingOn("");
|
|
356
|
+
resolve(result);
|
|
357
|
+
};
|
|
358
|
+
const onAbort = () => finish({ outcome: "stopped-by-user" });
|
|
359
|
+
const timer = setTimeout(() => finish({ outcome: "timed-out" }), ASK_TIMEOUT_MS);
|
|
360
|
+
const check = setInterval(() => {
|
|
361
|
+
if (!listRunning().some((r) => r.address === to)) finish({ outcome: "stopped-running" });
|
|
362
|
+
}, TARGET_CHECK_MS);
|
|
363
|
+
waiting = { id: ask.id, to, finish };
|
|
364
|
+
hooks.waitingOn(to);
|
|
365
|
+
if (signal?.aborted) onAbort();
|
|
366
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
367
|
+
});
|
|
359
368
|
const result = { id: ask.id, to, ...outcome };
|
|
360
369
|
return toolResult(askText(ask, target, outcome), result, result);
|
|
361
370
|
},
|
|
@@ -384,7 +393,6 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
384
393
|
const body = typeof params.message === "string" ? params.message.trim() : "";
|
|
385
394
|
if (body === "") throw new Error("the answer is empty; nothing was sent");
|
|
386
395
|
const answer = reply(id, body);
|
|
387
|
-
hooks.sent();
|
|
388
396
|
const sender = listRunning().find((record) => record.address === answer.to);
|
|
389
397
|
const who = sender === undefined ? answer.to : `${label(sender)} (${answer.to})`;
|
|
390
398
|
const result = { id: answer.id, ask: id, to: answer.to };
|