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/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 and waits, one at a time, for the answer,
9
- // which comes back as its result rather than as a message.
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
- /** How a waiting ask ended. */
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 and wait for its answer, which comes back as this tool's result. The answer is the other session's final message when its run settles, or an earlier reply it sends. The wait ends after 10 minutes, when the other session stops running, or when the user stops it; a later answer then arrives as a message. Only one ask waits at a time, and a session that is waiting on this one cannot be asked. `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.",
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
- hooks.sent();
339
- const outcome = await new Promise<AskOutcome>((resolve) => {
340
- const finish = (result: AskOutcome) => {
341
- if (waiting?.id !== ask.id) return;
342
- waiting = undefined;
343
- clearTimeout(timer);
344
- clearInterval(check);
345
- signal?.removeEventListener("abort", onAbort);
346
- hooks.waitingOn("");
347
- resolve(result);
348
- };
349
- const onAbort = () => finish({ outcome: "stopped-by-user" });
350
- const timer = setTimeout(() => finish({ outcome: "timed-out" }), ASK_TIMEOUT_MS);
351
- const check = setInterval(() => {
352
- if (!listRunning().some((r) => r.address === to)) finish({ outcome: "stopped-running" });
353
- }, TARGET_CHECK_MS);
354
- waiting = { id: ask.id, to, finish };
355
- hooks.waitingOn(to);
356
- if (signal?.aborted) onAbort();
357
- else signal?.addEventListener("abort", onAbort, { once: true });
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 };