@thurstonsand/pi-agent-switchboard 0.6.3 → 0.7.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/index.js +320 -84
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// packages/lines/pi/src/index.ts
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { mkdirSync as
|
|
4
|
-
import { dirname as
|
|
3
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4
|
+
import { dirname as dirname4, join as join3 } from "node:path";
|
|
5
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
|
|
7
7
|
// packages/shared/src/index.ts
|
|
@@ -73,8 +73,13 @@ class Db {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// packages/shared/src/index.ts
|
|
76
|
-
var SCHEMA_VERSION =
|
|
76
|
+
var SCHEMA_VERSION = 6;
|
|
77
|
+
var VIEWS = ["agent", "editor", "split"];
|
|
77
78
|
var HARNESSES = ["pi", "claude", "codex"];
|
|
79
|
+
var SNOOZE_PRESETS = ["one_hour", "one_day", "monday", "next_message"];
|
|
80
|
+
function stateDir() {
|
|
81
|
+
return join(process.env.XDG_STATE_HOME || join(homedir(), ".local/state"), "agent-switchboard");
|
|
82
|
+
}
|
|
78
83
|
function configDir() {
|
|
79
84
|
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "agent-switchboard");
|
|
80
85
|
}
|
|
@@ -88,6 +93,23 @@ function operatorDir() {
|
|
|
88
93
|
return physicalOperatorDir;
|
|
89
94
|
}
|
|
90
95
|
var physicalOperatorDir;
|
|
96
|
+
function shortlistPath() {
|
|
97
|
+
return join(stateDir(), "shortlist.txt");
|
|
98
|
+
}
|
|
99
|
+
function launchGuide() {
|
|
100
|
+
try {
|
|
101
|
+
return readFileSync(shortlistPath(), "utf8");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error.code === "ENOENT")
|
|
104
|
+
return "";
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function describeTool(name, description, guide) {
|
|
109
|
+
return name === "session_launch" && guide !== "" ? `${description}
|
|
110
|
+
|
|
111
|
+
${guide}` : description;
|
|
112
|
+
}
|
|
91
113
|
function bootId() {
|
|
92
114
|
if (process.platform === "darwin")
|
|
93
115
|
return execFileSync("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], { encoding: "utf8" }).trim();
|
|
@@ -118,39 +140,8 @@ function branch(cwd) {
|
|
|
118
140
|
}
|
|
119
141
|
|
|
120
142
|
// packages/shared/src/mailbox.ts
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return HARNESS_NAMES[harness];
|
|
124
|
-
}
|
|
125
|
-
function pendingFor(db, targetId) {
|
|
126
|
-
return db.all(`SELECT id, kind, source_id, target_id, body, request_response, created_at FROM messages
|
|
127
|
-
WHERE target_id = ? AND delivered_at IS NULL AND cancelled_at IS NULL ORDER BY created_at, rowid`, targetId).map((row) => ({
|
|
128
|
-
id: row.id,
|
|
129
|
-
kind: row.kind,
|
|
130
|
-
sourceId: row.source_id,
|
|
131
|
-
targetId: row.target_id,
|
|
132
|
-
body: row.body,
|
|
133
|
-
requestResponse: row.request_response === 1,
|
|
134
|
-
createdAt: row.created_at
|
|
135
|
-
}));
|
|
136
|
-
}
|
|
137
|
-
function markDelivered(db, id) {
|
|
138
|
-
db.run("UPDATE messages SET delivered_at = coalesce(delivered_at, ?) WHERE id = ?", Date.now(), id);
|
|
139
|
-
}
|
|
140
|
-
function sender(db, sourceId) {
|
|
141
|
-
const row = db.get("SELECT title, harness FROM sessions WHERE session_id = ?", sourceId);
|
|
142
|
-
return { title: row?.title ?? sourceId, harness: row?.harness ?? null };
|
|
143
|
-
}
|
|
144
|
-
function frame(db, m) {
|
|
145
|
-
const from = sender(db, m.sourceId);
|
|
146
|
-
const harness = from.harness ? `, ${harnessName(from.harness)}` : "";
|
|
147
|
-
const reply = m.requestResponse ? `
|
|
148
|
-
|
|
149
|
-
Response requested: reply with session_send_message to session ${m.sourceId}.` : "";
|
|
150
|
-
return `Message from session "${from.title}" (session: ${m.sourceId}${harness}):
|
|
151
|
-
|
|
152
|
-
${m.body}${reply}`;
|
|
153
|
-
}
|
|
143
|
+
import { mkdirSync as mkdirSync2, rmSync, watch, writeFileSync } from "node:fs";
|
|
144
|
+
import { dirname as dirname3, join as join2 } from "node:path";
|
|
154
145
|
|
|
155
146
|
// packages/shared/src/record.ts
|
|
156
147
|
function recordTx(db, body) {
|
|
@@ -187,10 +178,15 @@ function startHosting(db, h) {
|
|
|
187
178
|
function startTurn(db, t) {
|
|
188
179
|
recordTx(db, () => {
|
|
189
180
|
const now = Date.now();
|
|
181
|
+
const first = !db.get("SELECT 1 FROM sessions WHERE session_id = ?", t.sessionId);
|
|
190
182
|
db.run(`INSERT INTO sessions (session_id, harness, parent_id, cwd, project_root, transcript, title, branch, phase, created_at, last_prompt_at)
|
|
191
183
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'working', ?, ?)
|
|
192
184
|
ON CONFLICT (session_id) DO UPDATE SET phase = 'working',
|
|
193
185
|
last_prompt_at = CASE WHEN sessions.phase = 'idle' THEN excluded.last_prompt_at ELSE sessions.last_prompt_at END`, t.sessionId, t.harness, t.parentId, t.cwd, projectRoot(t.cwd), t.transcript, t.title, branch(t.cwd), now, now);
|
|
186
|
+
if (first && t.parentId)
|
|
187
|
+
db.run(`INSERT INTO marks (session_id, group_name)
|
|
188
|
+
SELECT ?, m.group_name FROM sessions p JOIN marks m USING (session_id)
|
|
189
|
+
WHERE p.session_id = ? AND p.project_root = ? AND m.group_name IS NOT NULL`, t.sessionId, t.parentId, projectRoot(t.cwd));
|
|
194
190
|
});
|
|
195
191
|
}
|
|
196
192
|
function setPhase(db, sessionId, phase) {
|
|
@@ -198,13 +194,33 @@ function setPhase(db, sessionId, phase) {
|
|
|
198
194
|
}
|
|
199
195
|
function settle(db, s) {
|
|
200
196
|
return recordTx(db, () => {
|
|
197
|
+
harnessTitled(db, s.sessionId, s.title);
|
|
201
198
|
db.run("UPDATE sessions SET phase = 'idle', last_settled_at = ?, cwd = ?, project_root = ?, branch = ?, title = coalesce(?, title) WHERE session_id = ?", Date.now(), s.cwd, projectRoot(s.cwd), branch(s.cwd), s.title, s.sessionId);
|
|
202
199
|
db.run("UPDATE runtimes SET cwd = ?, project_root = ? WHERE tmux_session = ?", s.cwd, projectRoot(s.cwd), s.host);
|
|
203
200
|
return { archived: isArchived(db, s.sessionId) };
|
|
204
201
|
});
|
|
205
202
|
}
|
|
203
|
+
function turnErrored(db, sessionId) {
|
|
204
|
+
recordTx(db, () => db.run("UPDATE sessions SET errored_at = ? WHERE session_id = ?", Date.now(), sessionId));
|
|
205
|
+
}
|
|
206
206
|
function setTitle(db, sessionId, title) {
|
|
207
|
-
recordTx(db, () =>
|
|
207
|
+
recordTx(db, () => {
|
|
208
|
+
harnessTitled(db, sessionId, title);
|
|
209
|
+
db.run("UPDATE sessions SET title = ? WHERE session_id = ?", title, sessionId);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function harnessTitled(db, sessionId, title) {
|
|
213
|
+
if (title === null)
|
|
214
|
+
return;
|
|
215
|
+
db.run("UPDATE marks SET title = NULL WHERE session_id = ? AND ? IS NOT (SELECT title FROM sessions WHERE session_id = ?)", sessionId, title, sessionId);
|
|
216
|
+
}
|
|
217
|
+
function pendingTitle(db, sessionId) {
|
|
218
|
+
const row = db.get("SELECT m.title FROM marks m JOIN sessions s USING (session_id) WHERE m.session_id = ? AND m.title IS NOT NULL AND m.title IS NOT s.title", sessionId);
|
|
219
|
+
return row?.title ?? null;
|
|
220
|
+
}
|
|
221
|
+
function titleOf(db, sessionId) {
|
|
222
|
+
const row = db.get("SELECT coalesce(m.title, s.title) AS title FROM sessions s LEFT JOIN marks m USING (session_id) WHERE s.session_id = ?", sessionId);
|
|
223
|
+
return row?.title ?? null;
|
|
208
224
|
}
|
|
209
225
|
function endHosting(db, host) {
|
|
210
226
|
recordTx(db, () => db.run("DELETE FROM runtimes WHERE tmux_session = ?", host));
|
|
@@ -216,18 +232,93 @@ function markArchived(db, sessionId) {
|
|
|
216
232
|
if (!db.get("SELECT 1 FROM sessions WHERE session_id = ?", sessionId))
|
|
217
233
|
return false;
|
|
218
234
|
const now = Date.now();
|
|
219
|
-
db.run(
|
|
235
|
+
db.run(`INSERT INTO marks (session_id, archived_at) VALUES (?, ?)
|
|
236
|
+
ON CONFLICT (session_id) DO UPDATE SET archived_at = excluded.archived_at, snoozed_at = NULL, snoozed_until = NULL`, sessionId, now);
|
|
220
237
|
db.run("UPDATE messages SET cancelled_at = ? WHERE target_id = ? AND delivered_at IS NULL AND cancelled_at IS NULL", now, sessionId);
|
|
238
|
+
db.run("UPDATE schedules SET cancelled_at = ? WHERE target_id = ? AND next_at IS NOT NULL AND cancelled_at IS NULL", now, sessionId);
|
|
221
239
|
return true;
|
|
222
240
|
}
|
|
223
241
|
|
|
242
|
+
// packages/shared/src/mailbox.ts
|
|
243
|
+
function doorbells(db) {
|
|
244
|
+
const dir = join2(dirname3(db.path), "doorbells");
|
|
245
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
246
|
+
return dir;
|
|
247
|
+
}
|
|
248
|
+
function listen(db, heard) {
|
|
249
|
+
const watcher = watch(doorbells(db), (_event, name) => {
|
|
250
|
+
if (name)
|
|
251
|
+
heard(name.toString());
|
|
252
|
+
});
|
|
253
|
+
watcher.unref();
|
|
254
|
+
return watcher;
|
|
255
|
+
}
|
|
256
|
+
var HARNESS_NAMES = { pi: "pi", claude: "Claude Code", codex: "Codex" };
|
|
257
|
+
function harnessName(harness) {
|
|
258
|
+
return HARNESS_NAMES[harness];
|
|
259
|
+
}
|
|
260
|
+
function pendingFor(db, targetId) {
|
|
261
|
+
return db.all(`SELECT id, kind, source_id, target_id, body, request_response, created_at, schedule_id, scheduled_for FROM messages
|
|
262
|
+
WHERE target_id = ? AND delivered_at IS NULL AND cancelled_at IS NULL ORDER BY created_at, rowid`, targetId).map((row) => ({
|
|
263
|
+
id: row.id,
|
|
264
|
+
kind: row.kind,
|
|
265
|
+
sourceId: row.source_id,
|
|
266
|
+
targetId: row.target_id,
|
|
267
|
+
body: row.body,
|
|
268
|
+
requestResponse: row.request_response === 1,
|
|
269
|
+
createdAt: row.created_at,
|
|
270
|
+
scheduleId: row.schedule_id,
|
|
271
|
+
scheduledFor: row.scheduled_for
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
function markDelivered(db, id) {
|
|
275
|
+
db.run("UPDATE messages SET delivered_at = coalesce(delivered_at, ?) WHERE id = ?", Date.now(), id);
|
|
276
|
+
}
|
|
277
|
+
function sender(db, sourceId) {
|
|
278
|
+
const row = db.get("SELECT harness FROM sessions WHERE session_id = ?", sourceId);
|
|
279
|
+
return { title: titleOf(db, sourceId) ?? sourceId, harness: row?.harness ?? null };
|
|
280
|
+
}
|
|
281
|
+
function clockTime(ms) {
|
|
282
|
+
const d = new Date(ms);
|
|
283
|
+
const day = d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" }).replace(",", "");
|
|
284
|
+
return `${day}, ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
285
|
+
}
|
|
286
|
+
var LATE_MS = 60000;
|
|
287
|
+
function frame(db, m, now = Date.now()) {
|
|
288
|
+
const from = sender(db, m.sourceId);
|
|
289
|
+
const who = m.sourceId === m.targetId ? ", this session" : from.harness ? `, ${harnessName(from.harness)}` : "";
|
|
290
|
+
let when = "";
|
|
291
|
+
if (m.scheduledFor !== null) {
|
|
292
|
+
when = `, scheduled for ${clockTime(m.scheduledFor)}`;
|
|
293
|
+
if (now - m.scheduledFor > LATE_MS)
|
|
294
|
+
when += `, delivered ${clockTime(now)}`;
|
|
295
|
+
if (m.scheduleId)
|
|
296
|
+
when += ` (schedule: ${m.scheduleId})`;
|
|
297
|
+
}
|
|
298
|
+
const reply = m.requestResponse ? `
|
|
299
|
+
|
|
300
|
+
Response requested: reply with session_send_message to session ${m.sourceId}.` : "";
|
|
301
|
+
return `Message from session "${from.title}" (session: ${m.sourceId}${who})${when}:
|
|
302
|
+
|
|
303
|
+
${m.body}${reply}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
224
306
|
// packages/shared/src/tools.ts
|
|
225
307
|
import { Type } from "typebox";
|
|
226
308
|
import { Value } from "typebox/value";
|
|
227
309
|
var session = Type.String({ description: "The target session's id, from session_reachable." });
|
|
310
|
+
var requestResponse = Type.Boolean({ description: "Whether the recipient should reply to this session." });
|
|
311
|
+
var cwd = Type.Optional(Type.String({ description: "Its working directory; defaults to this session's." }));
|
|
312
|
+
var title = Type.String({ description: "A short title for the roster." });
|
|
313
|
+
var preset = {
|
|
314
|
+
harness: Type.Optional(Type.Union(HARNESSES.map((name) => Type.Literal(name)), { description: "pi, claude (Claude Code), or codex." })),
|
|
315
|
+
model: Type.Optional(Type.String({ description: "As listed." })),
|
|
316
|
+
effort: Type.Optional(Type.String({ description: "As listed; needed only between presets of one model." })),
|
|
317
|
+
agent: Type.Optional(Type.String({ description: "A Claude Code agent, as listed, in place of model and effort." }))
|
|
318
|
+
};
|
|
228
319
|
var TOOLS = {
|
|
229
320
|
session_reachable: {
|
|
230
|
-
description: "List every open session on the switchboard
|
|
321
|
+
description: "List every open session on the switchboard except this one.",
|
|
231
322
|
guidelines: [
|
|
232
323
|
"Use session_reachable to find another session before messaging, cancelling, or archiving it.",
|
|
233
324
|
"Sessions in other projects and other harnesses are all reachable; a dormant one wakes when messaged."
|
|
@@ -244,46 +335,140 @@ var TOOLS = {
|
|
|
244
335
|
parameters: Type.Object({
|
|
245
336
|
session,
|
|
246
337
|
message: Type.String({ description: "The message, self-contained: the recipient doesn't see this conversation." }),
|
|
247
|
-
requestResponse
|
|
338
|
+
requestResponse
|
|
248
339
|
}),
|
|
249
340
|
readOnly: false
|
|
250
341
|
},
|
|
251
342
|
session_cancel: {
|
|
252
|
-
description: "
|
|
343
|
+
description: "Stop another session's running turn. The session stays open.",
|
|
253
344
|
guidelines: ["Only cancel another session's turn when the user asks for it."],
|
|
254
345
|
parameters: Type.Object({ session }),
|
|
255
346
|
readOnly: false
|
|
256
347
|
},
|
|
257
348
|
session_launch: {
|
|
258
|
-
description: "Start a new session on the switchboard,
|
|
349
|
+
description: "Start a new session on the switchboard, on one of the presets below.",
|
|
259
350
|
guidelines: [
|
|
260
|
-
"Write the goal as a complete briefing: objective, relevant context, constraints, and what done means.
|
|
261
|
-
"Launch a new session when the work is independent of this one, belongs to another directory, or the user asks for one."
|
|
351
|
+
"Write the goal as a complete briefing: objective, relevant context, constraints, and what done means.",
|
|
352
|
+
"Launch a new session when the work is independent of this one, belongs to another directory, or the user asks for one.",
|
|
353
|
+
"Omit the preset fields to carry forward what this session runs now. To switch, pass the fields of the preset whose note fits the work; anything not listed is refused."
|
|
262
354
|
],
|
|
263
355
|
parameters: Type.Object({
|
|
264
|
-
|
|
356
|
+
...preset,
|
|
265
357
|
goal: Type.String({ description: "The self-contained briefing, delivered as its first message." }),
|
|
266
|
-
cwd
|
|
267
|
-
title
|
|
358
|
+
cwd,
|
|
359
|
+
title,
|
|
268
360
|
requestResponse: Type.Boolean({ description: "Whether it should report back to this session when done." })
|
|
269
361
|
}),
|
|
270
362
|
readOnly: false
|
|
271
363
|
},
|
|
364
|
+
session_schedule: {
|
|
365
|
+
description: "Schedule a message for later, once or on a recurring cron schedule: to this session, another session, or a new session launched when it fires.",
|
|
366
|
+
guidelines: [
|
|
367
|
+
"Schedule only when the user asks to remind, follow up, check back, or repeat something. To wait on another session, set requestResponse on your message instead of scheduling checks.",
|
|
368
|
+
"For something expected within a few minutes, keep working in this turn instead.",
|
|
369
|
+
"Write the message as instructions for when it fires: what to check, and what to do with each outcome. A recurring message should say when to unschedule itself.",
|
|
370
|
+
"To message this session, omit both session and launch.",
|
|
371
|
+
"Set snooze only when the user asks for it."
|
|
372
|
+
],
|
|
373
|
+
parameters: Type.Object({
|
|
374
|
+
message: Type.String({ description: "What to deliver, self-contained when it goes to another session." }),
|
|
375
|
+
at: Type.Optional(Type.String({
|
|
376
|
+
description: 'When, once: an ISO 8601 time, local unless it has an offset ("2026-10-23T15:00"), or a duration from now ("PT2H", "P1D").'
|
|
377
|
+
})),
|
|
378
|
+
cron: Type.Optional(Type.String({
|
|
379
|
+
description: 'Recurring instead: standard 5-field cron in local time, "M H DoM Mon DoW" (e.g. "57 8 * * 1-5" = weekdays at 8:57am).'
|
|
380
|
+
})),
|
|
381
|
+
label: Type.String({ description: 'When it runs, in plain words, such as "Weekdays, 8:57 AM".' }),
|
|
382
|
+
session: Type.Optional(session),
|
|
383
|
+
launch: Type.Optional(Type.Object({
|
|
384
|
+
...preset,
|
|
385
|
+
cwd,
|
|
386
|
+
title
|
|
387
|
+
}, {
|
|
388
|
+
description: "Instead of an existing session, launch a new one each time it fires, on a preset as session_launch takes it, with the message as its goal."
|
|
389
|
+
})),
|
|
390
|
+
requestResponse: Type.Optional(requestResponse),
|
|
391
|
+
snooze: Type.Optional(Type.Boolean({ description: "Snooze the recipient until this fires." }))
|
|
392
|
+
}),
|
|
393
|
+
readOnly: false
|
|
394
|
+
},
|
|
395
|
+
session_schedules: {
|
|
396
|
+
description: "List this session's schedules, the ones it set and the ones that deliver to it: pending ones with their next run, finished ones with their last.",
|
|
397
|
+
guidelines: [],
|
|
398
|
+
parameters: Type.Object({}),
|
|
399
|
+
readOnly: true
|
|
400
|
+
},
|
|
401
|
+
session_unschedule: {
|
|
402
|
+
description: "Cancel a pending schedule.",
|
|
403
|
+
guidelines: ["Unschedule only when the user asks, or when the schedule's stated completion condition is met."],
|
|
404
|
+
parameters: Type.Object({ id: Type.String({ description: "The schedule's id, from session_schedules." }) }),
|
|
405
|
+
readOnly: false
|
|
406
|
+
},
|
|
272
407
|
swb_update_session: {
|
|
273
|
-
description: "
|
|
408
|
+
description: "Update one session on the switchboard: this one, unless you name another.",
|
|
274
409
|
guidelines: [
|
|
275
410
|
"Archive only when the user asks, or when you started that session and have driven it to completion.",
|
|
276
|
-
"Archiving
|
|
277
|
-
"Archiving
|
|
411
|
+
"Archiving this session quits it when the turn ends: call it last, then end with a brief final message.",
|
|
412
|
+
"Archiving refuses any session other than this one that is mid-turn, blocked, cut off, or errored, and names them. If they should go anyway, call again with force, which cancels their turns.",
|
|
413
|
+
"Unarchiving doesn't restart a session; session_send_message wakes it.",
|
|
414
|
+
"Snooze or unsnooze only when the user asks.",
|
|
415
|
+
"Group sessions when the user asks, or to keep sessions you started for one piece of work together. Reuse a Group session_reachable lists in that Project rather than inventing a near-duplicate.",
|
|
416
|
+
"Retitle a session when the user asks, or when its title no longer says what it is working on."
|
|
278
417
|
],
|
|
279
418
|
parameters: Type.Object({
|
|
280
|
-
session: Type.Optional(
|
|
281
|
-
archived: Type.Boolean()
|
|
419
|
+
session: Type.Optional(session),
|
|
420
|
+
archived: Type.Optional(Type.Boolean({ description: "True to archive, false to unarchive." })),
|
|
421
|
+
family: Type.Optional(Type.Boolean({ description: "With archived true: archive every open session it started too, and the ones they started." })),
|
|
422
|
+
force: Type.Optional(Type.Boolean({
|
|
423
|
+
description: "With archived true: archive sessions mid-turn, blocked, cut off, or errored anyway, cancelling their turns."
|
|
424
|
+
})),
|
|
425
|
+
group: Type.Optional(Type.Union([Type.String(), Type.Null()], {
|
|
426
|
+
description: "The Group to move it into, within its Project; null takes it out of its Group."
|
|
427
|
+
})),
|
|
428
|
+
title: Type.Optional(Type.String({ description: "A new, short title." })),
|
|
429
|
+
snooze: Type.Optional(Type.Union([...[...SNOOZE_PRESETS, "off"].map((preset) => Type.Literal(preset)), Type.Object({ until: Type.String() })], {
|
|
430
|
+
description: 'How long to snooze it, off to unsnooze, or {"until":"2026-11-05T09:00:00-08:00"} for a set time.'
|
|
431
|
+
}))
|
|
432
|
+
}),
|
|
433
|
+
readOnly: false
|
|
434
|
+
},
|
|
435
|
+
swb_show_session: {
|
|
436
|
+
description: "Show the user a specific session by switching focus to it.",
|
|
437
|
+
guidelines: ["Call it last in your turn, and end with one short line; the user won't be watching this session afterward."],
|
|
438
|
+
parameters: Type.Object({
|
|
439
|
+
session,
|
|
440
|
+
view: Type.Optional(Type.Union(VIEWS.map((view) => Type.Literal(view)), { description: "agent, its directory's editor, or a split of both; omit unless the user asks for one." }))
|
|
282
441
|
}),
|
|
283
442
|
readOnly: false
|
|
284
443
|
}
|
|
285
444
|
};
|
|
286
445
|
|
|
446
|
+
// packages/shared/src/trace.ts
|
|
447
|
+
import { appendFileSync } from "node:fs";
|
|
448
|
+
var file = process.env.SWB_TRACE;
|
|
449
|
+
function now() {
|
|
450
|
+
return performance.timeOrigin + performance.now();
|
|
451
|
+
}
|
|
452
|
+
function write(event) {
|
|
453
|
+
if (file)
|
|
454
|
+
appendFileSync(file, `${JSON.stringify({ ...event, pid: process.pid })}
|
|
455
|
+
`);
|
|
456
|
+
}
|
|
457
|
+
function mark(name) {
|
|
458
|
+
if (file)
|
|
459
|
+
write({ name, at: now() });
|
|
460
|
+
}
|
|
461
|
+
function span(name, run) {
|
|
462
|
+
if (!file)
|
|
463
|
+
return run();
|
|
464
|
+
const at = now();
|
|
465
|
+
const result = run();
|
|
466
|
+
if (result instanceof Promise)
|
|
467
|
+
return result.finally(() => write({ name, at, ms: now() - at }));
|
|
468
|
+
write({ name, at, ms: now() - at });
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
|
|
287
472
|
// packages/lines/pi/src/index.ts
|
|
288
473
|
var KEY = Symbol.for("agent-switchboard.recorder");
|
|
289
474
|
var store = globalThis;
|
|
@@ -328,6 +513,7 @@ function swb(bin, args, stdin = "") {
|
|
|
328
513
|
});
|
|
329
514
|
}
|
|
330
515
|
function src_default(pi) {
|
|
516
|
+
mark("pi-line.load");
|
|
331
517
|
pi.on("project_trust", (event) => ({ trusted: event.cwd === operatorDir() ? "yes" : "undecided" }));
|
|
332
518
|
const claimed = claim();
|
|
333
519
|
if (!claimed)
|
|
@@ -379,7 +565,7 @@ function src_default(pi) {
|
|
|
379
565
|
ctx.ui.notify(`swb: that session is already open in another pi (${host})`, "error");
|
|
380
566
|
return { cancel: true };
|
|
381
567
|
});
|
|
382
|
-
const draftPath = (id) =>
|
|
568
|
+
const draftPath = (id) => join3(dirname4(proc.launch.db), "drafts", id);
|
|
383
569
|
const restoreDraft = (ctx, id) => {
|
|
384
570
|
let draft;
|
|
385
571
|
try {
|
|
@@ -391,7 +577,7 @@ function src_default(pi) {
|
|
|
391
577
|
}
|
|
392
578
|
if (!ctx.ui.getEditorText())
|
|
393
579
|
ctx.ui.setEditorText(draft);
|
|
394
|
-
|
|
580
|
+
rmSync2(draftPath(id));
|
|
395
581
|
};
|
|
396
582
|
let seenVersion = -1;
|
|
397
583
|
const deliver = () => {
|
|
@@ -414,6 +600,9 @@ function src_default(pi) {
|
|
|
414
600
|
}
|
|
415
601
|
markDelivered(db, m.id);
|
|
416
602
|
}
|
|
603
|
+
const title = pendingTitle(db, current);
|
|
604
|
+
if (title !== null && title !== ctx.sessionManager.getSessionName())
|
|
605
|
+
pi.setSessionName(title);
|
|
417
606
|
} catch (error) {
|
|
418
607
|
fail(error);
|
|
419
608
|
}
|
|
@@ -436,7 +625,7 @@ ${details.body}`, 0, 0);
|
|
|
436
625
|
pi.appendEntry(LINEAGE, { parentSessionId: proc.parent });
|
|
437
626
|
delete proc.parent;
|
|
438
627
|
}
|
|
439
|
-
writeFor(ctx, (db, id) => {
|
|
628
|
+
span("pi-line.start-hosting", () => writeFor(ctx, (db, id) => {
|
|
440
629
|
const transcript = ctx.sessionManager.getSessionFile();
|
|
441
630
|
if (!transcript)
|
|
442
631
|
throw new Error("pi is running without a session file");
|
|
@@ -449,11 +638,18 @@ ${details.body}`, 0, 0);
|
|
|
449
638
|
pid: process.pid,
|
|
450
639
|
idle: ctx.isIdle()
|
|
451
640
|
});
|
|
452
|
-
});
|
|
641
|
+
}));
|
|
453
642
|
if (proc.inbox)
|
|
454
643
|
clearInterval(proc.inbox);
|
|
455
644
|
proc.inbox = setInterval(deliver, 250);
|
|
456
645
|
proc.inbox.unref();
|
|
646
|
+
proc.doorbell?.close();
|
|
647
|
+
write(current, (db) => {
|
|
648
|
+
proc.doorbell = listen(db, (id) => {
|
|
649
|
+
if (id === current)
|
|
650
|
+
deliver();
|
|
651
|
+
});
|
|
652
|
+
});
|
|
457
653
|
});
|
|
458
654
|
pi.on("agent_start", (_event, ctx) => {
|
|
459
655
|
writeFor(ctx, (db, id) => {
|
|
@@ -465,22 +661,35 @@ ${details.body}`, 0, 0);
|
|
|
465
661
|
startTurn(db, { harness: "pi", sessionId: id, cwd: ctx.cwd, transcript, title, parentId });
|
|
466
662
|
});
|
|
467
663
|
});
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
if (typeof attentionId !== "string" || !current)
|
|
664
|
+
const waiting = (key, open) => {
|
|
665
|
+
if (!current)
|
|
471
666
|
return;
|
|
472
667
|
if (open)
|
|
473
|
-
proc.attention.add(
|
|
668
|
+
proc.attention.add(key);
|
|
474
669
|
else
|
|
475
|
-
proc.attention.delete(
|
|
670
|
+
proc.attention.delete(key);
|
|
476
671
|
const phase = proc.attention.size > 0 ? "blocked" : "working";
|
|
477
672
|
write(current, (db, id) => setPhase(db, id, phase));
|
|
478
673
|
};
|
|
674
|
+
const attention = (open) => (data) => {
|
|
675
|
+
const attentionId = data.attentionId;
|
|
676
|
+
if (typeof attentionId === "string")
|
|
677
|
+
waiting(`glimpse:${attentionId}`, open);
|
|
678
|
+
};
|
|
479
679
|
pi.events.on("glimpseui:attention:request", attention(true));
|
|
480
680
|
pi.events.on("glimpseui:attention:resolve", attention(false));
|
|
481
|
-
pi.on("
|
|
681
|
+
pi.on("ui_prompt_start", () => waiting("ui-prompt", true));
|
|
682
|
+
pi.on("ui_prompt_end", () => waiting("ui-prompt", false));
|
|
683
|
+
let stopReason;
|
|
684
|
+
pi.on("message_end", (event) => {
|
|
685
|
+
if (event.message.role === "assistant")
|
|
686
|
+
stopReason = event.message.stopReason;
|
|
687
|
+
});
|
|
688
|
+
pi.on("agent_settled", (event, ctx) => {
|
|
482
689
|
proc.attention.clear();
|
|
483
690
|
writeFor(ctx, (db, id) => {
|
|
691
|
+
if (stopReason === "error" && !event.aborted)
|
|
692
|
+
turnErrored(db, id);
|
|
484
693
|
const { archived } = settle(db, {
|
|
485
694
|
host: proc.launch.tmuxSession,
|
|
486
695
|
sessionId: id,
|
|
@@ -514,11 +723,12 @@ ${details.body}`, 0, 0);
|
|
|
514
723
|
ctx.ui.notify("swb: nothing to archive before the first prompt", "error");
|
|
515
724
|
}
|
|
516
725
|
});
|
|
726
|
+
const guide = launchGuide();
|
|
517
727
|
const tool = (name, label, target, run) => pi.registerTool({
|
|
518
728
|
name,
|
|
519
729
|
label,
|
|
520
|
-
description: TOOLS[name].description,
|
|
521
|
-
promptSnippet: TOOLS[name].description,
|
|
730
|
+
description: describeTool(name, TOOLS[name].description, guide),
|
|
731
|
+
promptSnippet: describeTool(name, TOOLS[name].description, guide),
|
|
522
732
|
promptGuidelines: TOOLS[name].guidelines,
|
|
523
733
|
parameters: TOOLS[name].parameters,
|
|
524
734
|
renderCall(args, theme) {
|
|
@@ -537,9 +747,9 @@ ${details.body}`, 0, 0);
|
|
|
537
747
|
}
|
|
538
748
|
});
|
|
539
749
|
const self = (ctx) => ctx.sessionManager.getSessionId();
|
|
540
|
-
const
|
|
750
|
+
const titleOf2 = (id) => typeof id === "string" ? titleOf(connect(), id) ?? id : "";
|
|
541
751
|
tool("session_reachable", "Reachable sessions", () => "", (_args, ctx) => swb(proc.launch.bin, ["reachable", "--from", self(ctx)]));
|
|
542
|
-
tool("session_send_message", "Message", (args) =>
|
|
752
|
+
tool("session_send_message", "Message", (args) => titleOf2(args.session), (args, ctx, toolCallId) => swb(proc.launch.bin, [
|
|
543
753
|
"send",
|
|
544
754
|
"--from",
|
|
545
755
|
self(ctx),
|
|
@@ -549,11 +759,10 @@ ${details.body}`, 0, 0);
|
|
|
549
759
|
toolCallId,
|
|
550
760
|
...args.requestResponse ? ["--request-response"] : []
|
|
551
761
|
], args.message));
|
|
552
|
-
tool("session_cancel", "Cancel", (args) =>
|
|
553
|
-
tool("session_launch", "Launch", (args) => `${args.harness} · ${args.title}`, (args, ctx) => swb(proc.launch.bin, [
|
|
762
|
+
tool("session_cancel", "Cancel", (args) => titleOf2(args.session), (args, ctx) => swb(proc.launch.bin, ["send", "--from", self(ctx), "--to", args.session, "--cancel"]));
|
|
763
|
+
tool("session_launch", "Launch", (args) => `${[args.harness, args.agent, args.model].filter(Boolean).join(" ") || "default"} · ${args.title}`, (args, ctx) => swb(proc.launch.bin, [
|
|
554
764
|
"launch",
|
|
555
|
-
"
|
|
556
|
-
args.harness,
|
|
765
|
+
...["harness", "model", "effort", "agent"].flatMap((k) => args[k] ? [`--${k}`, args[k]] : []),
|
|
557
766
|
"--from",
|
|
558
767
|
self(ctx),
|
|
559
768
|
"--cwd",
|
|
@@ -562,30 +771,57 @@ ${details.body}`, 0, 0);
|
|
|
562
771
|
args.title,
|
|
563
772
|
...args.requestResponse ? ["--request-response"] : []
|
|
564
773
|
], args.goal));
|
|
565
|
-
tool("
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
774
|
+
tool("session_schedule", "Schedule", (args) => `${args.launch ? `new ${args.launch.title}` : args.session ? titleOf2(args.session) : "this session"} · ${args.label}${args.snooze ? " · snooze" : ""}`, (args, ctx, toolCallId) => swb(proc.launch.bin, [
|
|
775
|
+
"schedule",
|
|
776
|
+
"--from",
|
|
777
|
+
self(ctx),
|
|
778
|
+
"--label",
|
|
779
|
+
args.label,
|
|
780
|
+
"--request-id",
|
|
781
|
+
toolCallId,
|
|
782
|
+
...args.at !== undefined ? ["--at", args.at] : [],
|
|
783
|
+
...args.cron !== undefined ? ["--cron", args.cron] : [],
|
|
784
|
+
...args.session !== undefined ? ["--to", args.session] : [],
|
|
785
|
+
...args.launch ? [
|
|
786
|
+
...["harness", "model", "effort", "agent"].flatMap((k) => args.launch?.[k] ? [`--launch-${k}`, args.launch[k]] : []),
|
|
787
|
+
"--launch-title",
|
|
788
|
+
args.launch.title,
|
|
789
|
+
"--launch-cwd",
|
|
790
|
+
args.launch.cwd ?? ctx.cwd
|
|
791
|
+
] : [],
|
|
792
|
+
...args.requestResponse ? ["--request-response"] : [],
|
|
793
|
+
...args.snooze ? ["--snooze"] : []
|
|
794
|
+
], args.message));
|
|
795
|
+
tool("session_schedules", "Schedules", () => "", (_args, ctx) => swb(proc.launch.bin, ["schedules", "--from", self(ctx)]));
|
|
796
|
+
tool("session_unschedule", "Unschedule", (args) => String(args.id), (args, ctx) => swb(proc.launch.bin, ["unschedule", args.id, "--from", self(ctx)]));
|
|
797
|
+
tool("swb_show_session", "Show", (args) => `${titleOf2(args.session)}${args.view ? ` · ${args.view}` : ""}`, (args, ctx) => swb(proc.launch.bin, ["show", args.session, "--from", self(ctx), ...args.view ? ["--view", args.view] : []]));
|
|
798
|
+
const snoozeText = (snooze) => typeof snooze === "string" ? snooze : snooze.until;
|
|
799
|
+
tool("swb_update_session", "Update session", (args) => {
|
|
800
|
+
const parts = [
|
|
801
|
+
args.archived === true ? `archive${args.family ? " family" : ""}${args.force ? " · force" : ""}` : "",
|
|
802
|
+
args.archived === false ? "unarchive" : "",
|
|
803
|
+
args.snooze !== undefined ? `snooze ${snoozeText(args.snooze)}` : "",
|
|
804
|
+
args.group === null ? "ungroup" : args.group !== undefined ? `group ${args.group}` : "",
|
|
805
|
+
args.title !== undefined ? `retitle as "${args.title}"` : ""
|
|
806
|
+
].filter(Boolean);
|
|
807
|
+
if (args.title !== undefined)
|
|
808
|
+
return parts.join(" · ");
|
|
809
|
+
return `${parts.join(" · ")} ${args.session ? titleOf2(args.session) : "this session"}`;
|
|
810
|
+
}, (args, ctx) => swb(proc.launch.bin, ["update", "--from", self(ctx)], JSON.stringify(args)));
|
|
577
811
|
pi.on("session_shutdown", (event, ctx) => {
|
|
578
812
|
if (proc.inbox)
|
|
579
813
|
clearInterval(proc.inbox);
|
|
580
814
|
proc.inbox = undefined;
|
|
815
|
+
proc.doorbell?.close();
|
|
816
|
+
proc.doorbell = undefined;
|
|
581
817
|
ctxNow = undefined;
|
|
582
818
|
if (event.reason !== "quit")
|
|
583
819
|
return;
|
|
584
820
|
const draft = ctx.ui.getEditorText();
|
|
585
821
|
if (draft.trim()) {
|
|
586
822
|
const path = draftPath(ctx.sessionManager.getSessionId());
|
|
587
|
-
|
|
588
|
-
|
|
823
|
+
mkdirSync3(dirname4(path), { recursive: true, mode: 448 });
|
|
824
|
+
writeFileSync2(path, draft, { mode: 384 });
|
|
589
825
|
}
|
|
590
826
|
writeFor(ctx, (db) => endHosting(db, proc.launch.tmuxSession));
|
|
591
827
|
});
|