@thurstonsand/pi-agent-switchboard 0.0.0 → 0.2.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.
Files changed (2) hide show
  1. package/index.js +73 -16
  2. package/package.json +3 -2
package/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ // packages/pi/src/index.ts
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+
1
5
  // packages/shared/src/index.ts
2
6
  import { execFileSync } from "node:child_process";
3
7
  import { readFileSync } from "node:fs";
@@ -97,18 +101,49 @@ function branch(cwd) {
97
101
  }
98
102
 
99
103
  // packages/pi/src/index.ts
104
+ import { Type } from "typebox";
100
105
  var KEY = Symbol.for("agent-switchboard.recorder");
101
106
  var store = globalThis;
102
107
  function claim() {
103
108
  const env = process.env;
104
109
  if (env.SWB_MANAGED === "1" && env.SWB_DB && env.SWB_TMUX_SESSION) {
105
- store[KEY] = { launch: Object.freeze({ db: env.SWB_DB, tmuxSession: env.SWB_TMUX_SESSION }), attention: new Set };
110
+ store[KEY] = {
111
+ launch: Object.freeze({ db: env.SWB_DB, tmuxSession: env.SWB_TMUX_SESSION, bin: env.SWB_BIN ?? "" }),
112
+ attention: new Set,
113
+ ...env.SWB_BIN ? {} : { failure: "started by an swb older than this recorder; upgrade swb" }
114
+ };
106
115
  }
107
116
  delete env.SWB_MANAGED;
108
117
  delete env.SWB_DB;
109
118
  delete env.SWB_TMUX_SESSION;
119
+ delete env.SWB_BIN;
110
120
  return store[KEY];
111
121
  }
122
+ var run = promisify(execFile);
123
+ function host(bin) {
124
+ const swb = async (...args) => {
125
+ try {
126
+ return (await run(bin, args, { timeout: 15000 })).stdout;
127
+ } catch (error) {
128
+ const { stderr, message } = error;
129
+ throw new Error(stderr?.trim() || message);
130
+ }
131
+ };
132
+ return {
133
+ name: "swb",
134
+ async launch(input) {
135
+ await swb("launch", "--cwd", input.cwd, "--session-id", input.sessionId, "--model", input.model);
136
+ return { success: true };
137
+ },
138
+ async listSessions() {
139
+ const sessions = JSON.parse(await swb("ls", "--json"));
140
+ return sessions.filter((s) => s.open).map((s) => ({ sessionId: s.id, cwd: s.cwd, title: s.title ?? s.id }));
141
+ },
142
+ async wake(sessionId) {
143
+ await swb("wake", sessionId);
144
+ }
145
+ };
146
+ }
112
147
  function liveHost(row) {
113
148
  if (row && row.boot_id === bootId() && pidAlive(row.pid))
114
149
  return row.tmux_session;
@@ -126,6 +161,8 @@ function src_default(pi) {
126
161
  return;
127
162
  const proc = claimed;
128
163
  let ui;
164
+ if (proc.launch.bin)
165
+ pi.events.on("pi-sessions:hosts:v1", (request) => request.register(host(proc.launch.bin)));
129
166
  const showFailure = () => ui?.setStatus("swb", ui.theme.fg("error", `swb: not recording: ${proc.failure}`));
130
167
  const connect = () => {
131
168
  proc.db ??= new Db(proc.launch.db, { create: false });
@@ -191,9 +228,7 @@ function src_default(pi) {
191
228
  db.run("UPDATE sessions SET phase = 'idle', cwd = ?, project_root = ?, branch = ?, transcript = ? WHERE session_id = ?", ctx.cwd, projectRoot(ctx.cwd), branch(ctx.cwd), transcript, id);
192
229
  });
193
230
  });
194
- pi.on("message_start", (event, ctx) => {
195
- if (event.message.role !== "user")
196
- return;
231
+ pi.on("agent_start", (_event, ctx) => {
197
232
  writeFor(ctx, (db, id) => {
198
233
  const transcript = ctx.sessionManager.getSessionFile();
199
234
  if (!transcript)
@@ -201,12 +236,10 @@ function src_default(pi) {
201
236
  const now = Date.now();
202
237
  db.run(`INSERT INTO sessions (session_id, tool, cwd, project_root, transcript, title, branch, phase, created_at, last_prompt_at)
203
238
  VALUES (?, 'pi', ?, ?, ?, ?, ?, 'working', ?, ?)
204
- ON CONFLICT (session_id) DO UPDATE SET phase = 'working', last_prompt_at = excluded.last_prompt_at`, id, ctx.cwd, projectRoot(ctx.cwd), transcript, ctx.sessionManager.getSessionName() ?? null, branch(ctx.cwd), now, now);
239
+ ON CONFLICT (session_id) DO UPDATE SET phase = 'working',
240
+ last_prompt_at = CASE WHEN sessions.phase = 'idle' THEN excluded.last_prompt_at ELSE sessions.last_prompt_at END`, id, ctx.cwd, projectRoot(ctx.cwd), transcript, ctx.sessionManager.getSessionName() ?? null, branch(ctx.cwd), now, now);
205
241
  });
206
242
  });
207
- pi.on("agent_start", (_event, ctx) => {
208
- writeFor(ctx, (db, id) => db.run("UPDATE sessions SET phase = 'working' WHERE session_id = ?", id));
209
- });
210
243
  const attention = (open) => (data) => {
211
244
  const attentionId = data.attentionId;
212
245
  if (typeof attentionId !== "string" || !current)
@@ -230,6 +263,16 @@ function src_default(pi) {
230
263
  pi.on("session_info_changed", (event, ctx) => {
231
264
  writeFor(ctx, (db, id) => db.run("UPDATE sessions SET title = ? WHERE session_id = ?", event.name ?? null, id));
232
265
  });
266
+ const markArchived = (ctx) => {
267
+ let archived = false;
268
+ writeFor(ctx, (db, id) => {
269
+ if (!db.get("SELECT 1 FROM sessions WHERE session_id = ?", id))
270
+ return;
271
+ db.run("INSERT INTO marks (session_id, archived_at) VALUES (?, ?) ON CONFLICT (session_id) DO UPDATE SET archived_at = excluded.archived_at", id, Date.now());
272
+ archived = true;
273
+ });
274
+ return archived;
275
+ };
233
276
  pi.registerCommand("archive", {
234
277
  description: "Archive this session in swb and quit",
235
278
  handler: async (_args, ctx) => {
@@ -237,19 +280,33 @@ function src_default(pi) {
237
280
  ctx.ui.notify("swb: turn running: wait for it to complete", "error");
238
281
  return;
239
282
  }
240
- let archived = false;
241
- writeFor(ctx, (db, id) => {
242
- if (!db.get("SELECT 1 FROM sessions WHERE session_id = ?", id))
243
- return;
244
- db.run("INSERT INTO marks (session_id, archived_at) VALUES (?, ?) ON CONFLICT (session_id) DO UPDATE SET archived_at = excluded.archived_at", id, Date.now());
245
- archived = true;
246
- });
247
- if (archived)
283
+ if (markArchived(ctx))
248
284
  ctx.shutdown();
249
285
  else if (!proc.failure)
250
286
  ctx.ui.notify("swb: nothing to archive before the first prompt", "error");
251
287
  }
252
288
  });
289
+ pi.registerTool({
290
+ name: "swb_archive",
291
+ label: "Archive session",
292
+ description: "Archive this session in Agent Switchboard. It leaves the user's roster of open sessions, and pi quits as soon as this turn ends. The user can restore it with `swb unarchive <id>`.",
293
+ promptSnippet: "Archive this session when the user asks",
294
+ promptGuidelines: [
295
+ "Use swb_archive only when the user asks to archive this session; never archive on your own initiative.",
296
+ "Call swb_archive as the last action of the turn, then end with a brief final message: pi quits once the turn ends."
297
+ ],
298
+ parameters: Type.Object({}),
299
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
300
+ if (!markArchived(ctx)) {
301
+ throw new Error(proc.failure ? `swb: not recording: ${proc.failure}` : "swb: no session recorded yet");
302
+ }
303
+ ctx.shutdown();
304
+ return {
305
+ content: [{ type: "text", text: "Archived. pi quits when this turn ends." }],
306
+ details: undefined
307
+ };
308
+ }
309
+ });
253
310
  pi.on("session_shutdown", (event, ctx) => {
254
311
  if (event.reason !== "quit")
255
312
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thurstonsand/pi-agent-switchboard",
3
- "version": "0.0.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Records managed pi sessions for Agent Switchboard (swb)",
6
6
  "license": "MIT",
@@ -17,6 +17,7 @@
17
17
  ]
18
18
  },
19
19
  "peerDependencies": {
20
- "@earendil-works/pi-coding-agent": "*"
20
+ "@earendil-works/pi-coding-agent": "*",
21
+ "typebox": "*"
21
22
  }
22
23
  }