pi-session-mail 0.2.2 → 0.4.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 +61 -1
- package/README.md +44 -27
- package/package.json +3 -1
- package/src/index.ts +150 -53
- package/src/mail-status.ts +68 -0
- package/src/picker.ts +285 -0
- package/src/store.ts +150 -38
- package/src/tools.ts +92 -19
package/src/tools.ts
CHANGED
|
@@ -24,8 +24,6 @@ import { type Envelope, send } from "./store.ts";
|
|
|
24
24
|
|
|
25
25
|
/** What the tools need from the extension around them. */
|
|
26
26
|
export type ToolHooks = {
|
|
27
|
-
/** Called after a tool wrote mail, so the footer can count it. */
|
|
28
|
-
sent: () => void;
|
|
29
27
|
/** The current turn's hop count, which mail sent now carries. */
|
|
30
28
|
hops: () => number;
|
|
31
29
|
/** Report a broken `session-mail.json`; the extension shows one warning per session. */
|
|
@@ -118,11 +116,64 @@ function checkHops(hops: number, hooks: ToolHooks): void {
|
|
|
118
116
|
);
|
|
119
117
|
}
|
|
120
118
|
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
/**
|
|
120
|
+
* The one result every tool returns. `structuredContent` is what codemode scripts
|
|
121
|
+
* receive; it must match the tool's `outputSchema`, which Pi does not check.
|
|
122
|
+
*/
|
|
123
|
+
function toolResult<D, S>(text: string, details: D, structuredContent: S) {
|
|
124
|
+
return { content: [{ type: "text" as const, text }], details, structuredContent };
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/** Hints for a tool that adds mail and never deletes or overwrites any, on this machine only. */
|
|
128
|
+
const SENDS_MAIL = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
129
|
+
|
|
130
|
+
const SessionList = Type.Object({
|
|
131
|
+
sessions: Type.Array(
|
|
132
|
+
Type.Object({
|
|
133
|
+
id: Type.String({ description: "The session's full id; pass it as `to`" }),
|
|
134
|
+
name: Type.Optional(Type.String({ description: "The session name; absent when none is set" })),
|
|
135
|
+
cwd: Type.String(),
|
|
136
|
+
state: Type.Union([Type.Literal("idle"), Type.Literal("busy")]),
|
|
137
|
+
waitingOn: Type.Optional(
|
|
138
|
+
Type.String({ description: "The full id of the session this one's ask waits on; absent when none" }),
|
|
139
|
+
),
|
|
140
|
+
self: Type.Boolean({ description: "True for the calling session" }),
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const SendResult = Type.Object({
|
|
146
|
+
id: Type.String({ description: "The message's id" }),
|
|
147
|
+
to: Type.String({ description: "The recipient's full id" }),
|
|
148
|
+
running: Type.Boolean({ description: "False when the message waits in a closed session's inbox" }),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const asked = {
|
|
152
|
+
id: Type.String({ description: "The ask's id" }),
|
|
153
|
+
to: Type.String({ description: "The asked session's full id" }),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** One variant per {@link AskOutcome}, told apart by `outcome`. */
|
|
157
|
+
const AskResult = Type.Union([
|
|
158
|
+
Type.Object({
|
|
159
|
+
...asked,
|
|
160
|
+
outcome: Type.Literal("answered"),
|
|
161
|
+
status: Type.String({
|
|
162
|
+
description: '"done" for an answer; any other status means the run ended otherwise, and body is not an answer',
|
|
163
|
+
}),
|
|
164
|
+
body: Type.String(),
|
|
165
|
+
}),
|
|
166
|
+
Type.Object({ ...asked, outcome: Type.Literal("timed-out") }),
|
|
167
|
+
Type.Object({ ...asked, outcome: Type.Literal("stopped-running") }),
|
|
168
|
+
Type.Object({ ...asked, outcome: Type.Literal("stopped-by-user") }),
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
const ReplyResult = Type.Object({
|
|
172
|
+
id: Type.String({ description: "The reply's id" }),
|
|
173
|
+
ask: Type.String({ description: "The id of the ask it answers" }),
|
|
174
|
+
to: Type.String({ description: "The asker's full id" }),
|
|
175
|
+
});
|
|
176
|
+
|
|
126
177
|
/** One running session as the list shows it. */
|
|
127
178
|
function describe(record: RunningRecord, byAddress: ReadonlyMap<string, RunningRecord>, me: string): string {
|
|
128
179
|
const waiting = record.waitingOn === "" ? "" : `, waiting on ${waitLabel(record.waitingOn, byAddress)}`;
|
|
@@ -170,16 +221,31 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
170
221
|
description:
|
|
171
222
|
"List the Pi sessions running on this machine, in every agent root: each one's name (or short id when it has none), full id, working directory, idle or busy state, and whom it is waiting on. The calling session is marked. A session is addressed by its name, its full id, or an id prefix of at least 8 characters.",
|
|
172
223
|
parameters: Type.Object({}),
|
|
224
|
+
outputSchema: SessionList,
|
|
225
|
+
// Listing deletes the records of sessions whose process is gone, which is housekeeping, not a change.
|
|
226
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
173
227
|
async execute(_toolCallId: string, _params: unknown, _signal: unknown, _onUpdate: unknown, ctx: ExtensionContext) {
|
|
174
228
|
const me = ctx.sessionManager.getSessionId();
|
|
175
229
|
const running = listRunning();
|
|
176
|
-
if (running.length === 0)
|
|
230
|
+
if (running.length === 0)
|
|
231
|
+
return toolResult("No Pi sessions are running with a mailbox.", { sessions: [] }, { sessions: [] });
|
|
177
232
|
const byAddress = new Map(running.map((record) => [record.address, record]));
|
|
178
233
|
const lines = running.map((record) => describe(record, byAddress, me));
|
|
179
234
|
const count = `${running.length} running session${running.length === 1 ? "" : "s"}:`;
|
|
180
|
-
return toolResult(
|
|
181
|
-
|
|
182
|
-
|
|
235
|
+
return toolResult(
|
|
236
|
+
[count, ...lines].join("\n"),
|
|
237
|
+
{ sessions: running.map((record) => ({ ...record, self: record.address === me })) },
|
|
238
|
+
{
|
|
239
|
+
sessions: running.map(({ address, name, cwd, state, waitingOn }) => ({
|
|
240
|
+
id: address,
|
|
241
|
+
...(name === undefined ? {} : { name }),
|
|
242
|
+
cwd,
|
|
243
|
+
state,
|
|
244
|
+
...(waitingOn === "" ? {} : { waitingOn }),
|
|
245
|
+
self: address === me,
|
|
246
|
+
})),
|
|
247
|
+
},
|
|
248
|
+
);
|
|
183
249
|
},
|
|
184
250
|
});
|
|
185
251
|
|
|
@@ -196,6 +262,8 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
196
262
|
description: "The message text. The recipient sees it labelled as coming from this session.",
|
|
197
263
|
}),
|
|
198
264
|
}),
|
|
265
|
+
outputSchema: SendResult,
|
|
266
|
+
annotations: SENDS_MAIL,
|
|
199
267
|
async execute(
|
|
200
268
|
_toolCallId: string,
|
|
201
269
|
params: { to: string; message: string },
|
|
@@ -210,13 +278,13 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
210
278
|
const hops = hooks.hops();
|
|
211
279
|
checkHops(hops, hooks);
|
|
212
280
|
const envelope = send(me, to, body, { kind: "message", hops });
|
|
213
|
-
hooks.sent();
|
|
214
281
|
const target = listRunning().find((record) => record.address === to);
|
|
215
282
|
const text =
|
|
216
283
|
target === undefined
|
|
217
284
|
? `Message ${envelope.id} left for ${to}, which is not running: it waits in that session's inbox and is delivered when the session starts or resumes.`
|
|
218
285
|
: `Sent message ${envelope.id} to ${label(target)} (${to}). It expects no answer; any answer arrives as a message.`;
|
|
219
|
-
|
|
286
|
+
const result = { id: envelope.id, to, running: target !== undefined };
|
|
287
|
+
return toolResult(text, result, result);
|
|
220
288
|
},
|
|
221
289
|
});
|
|
222
290
|
|
|
@@ -234,6 +302,8 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
234
302
|
"The question. The recipient sees it labelled as coming from this session, with this session waiting.",
|
|
235
303
|
}),
|
|
236
304
|
}),
|
|
305
|
+
outputSchema: AskResult,
|
|
306
|
+
annotations: SENDS_MAIL,
|
|
237
307
|
async execute(
|
|
238
308
|
_toolCallId: string,
|
|
239
309
|
params: { to: string; message: string },
|
|
@@ -262,7 +332,6 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
262
332
|
const hops = hooks.hops();
|
|
263
333
|
checkHops(hops, hooks);
|
|
264
334
|
const ask = send(me, to, body, { kind: "ask", hops });
|
|
265
|
-
hooks.sent();
|
|
266
335
|
const outcome = await new Promise<AskOutcome>((resolve) => {
|
|
267
336
|
const finish = (result: AskOutcome) => {
|
|
268
337
|
if (waiting?.id !== ask.id) return;
|
|
@@ -283,7 +352,8 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
283
352
|
if (signal?.aborted) onAbort();
|
|
284
353
|
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
285
354
|
});
|
|
286
|
-
|
|
355
|
+
const result = { id: ask.id, to, ...outcome };
|
|
356
|
+
return toolResult(askText(ask, target, outcome), result, result);
|
|
287
357
|
},
|
|
288
358
|
});
|
|
289
359
|
|
|
@@ -296,6 +366,9 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
296
366
|
ask: Type.String({ description: "The ask id, as given in the ask's label." }),
|
|
297
367
|
message: Type.String({ description: "The answer." }),
|
|
298
368
|
}),
|
|
369
|
+
outputSchema: ReplyResult,
|
|
370
|
+
// A repeated reply to the same ask is refused and sends nothing.
|
|
371
|
+
annotations: { ...SENDS_MAIL, idempotentHint: true },
|
|
299
372
|
async execute(
|
|
300
373
|
_toolCallId: string,
|
|
301
374
|
params: { ask: string; message: string },
|
|
@@ -307,14 +380,14 @@ export function registerTools(pi: ExtensionAPI, hooks: ToolHooks): Tools {
|
|
|
307
380
|
const body = typeof params.message === "string" ? params.message.trim() : "";
|
|
308
381
|
if (body === "") throw new Error("the answer is empty; nothing was sent");
|
|
309
382
|
const answer = reply(id, body);
|
|
310
|
-
hooks.sent();
|
|
311
383
|
const sender = listRunning().find((record) => record.address === answer.to);
|
|
312
384
|
const who = sender === undefined ? answer.to : `${label(sender)} (${answer.to})`;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
ask
|
|
316
|
-
|
|
317
|
-
|
|
385
|
+
const result = { id: answer.id, ask: id, to: answer.to };
|
|
386
|
+
return toolResult(
|
|
387
|
+
`Answered ask ${id} from ${who}; this run's last message will not be sent for it.`,
|
|
388
|
+
result,
|
|
389
|
+
result,
|
|
390
|
+
);
|
|
318
391
|
},
|
|
319
392
|
});
|
|
320
393
|
|