@spencerboucher/switchyard 0.5.0 → 0.6.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 (48) hide show
  1. package/README.md +4 -2
  2. package/dist/bridge/pi-model.js +44 -0
  3. package/dist/bridge/switchyard-bridge.js +24 -2
  4. package/dist/bridge/switchyard-terminal.js +84 -3
  5. package/dist/switchyard.mjs +404 -29
  6. package/package.json +1 -1
  7. package/web-dist/assets/{AutomationView-CFohp0SQ.js → AutomationView-DgZn3ltn.js} +2 -2
  8. package/web-dist/assets/{AutomationView-CFohp0SQ.js.map → AutomationView-DgZn3ltn.js.map} +1 -1
  9. package/web-dist/assets/{InteractionCard-B-xmpTNG.js → InteractionCard-cvPWKSu1.js} +2 -2
  10. package/web-dist/assets/{InteractionCard-B-xmpTNG.js.map → InteractionCard-cvPWKSu1.js.map} +1 -1
  11. package/web-dist/assets/{Menu-C4GUY_ym.js → Menu-GymfuHGP.js} +2 -2
  12. package/web-dist/assets/Menu-GymfuHGP.js.map +1 -0
  13. package/web-dist/assets/{Overlay--v0ur2lj.js → Overlay-oYnnwvFM.js} +2 -2
  14. package/web-dist/assets/{Overlay--v0ur2lj.js.map → Overlay-oYnnwvFM.js.map} +1 -1
  15. package/web-dist/assets/{attention-q6_xlnT4.js → attention-Bxg9W15n.js} +2 -2
  16. package/web-dist/assets/{attention-q6_xlnT4.js.map → attention-Bxg9W15n.js.map} +1 -1
  17. package/web-dist/assets/{automations-CyA83OAD.js → automations-DdB97dnL.js} +2 -2
  18. package/web-dist/assets/{automations-CyA83OAD.js.map → automations-DdB97dnL.js.map} +1 -1
  19. package/web-dist/assets/{board-BFvNcFhk.js → board-s4LuDPcg.js} +2 -2
  20. package/web-dist/assets/{board-BFvNcFhk.js.map → board-s4LuDPcg.js.map} +1 -1
  21. package/web-dist/assets/code-B0KXn7lD.js +8 -0
  22. package/web-dist/assets/code-B0KXn7lD.js.map +1 -0
  23. package/web-dist/assets/{describe-BirsjlUl.js → describe-C_3FJuSs.js} +2 -2
  24. package/web-dist/assets/{describe-BirsjlUl.js.map → describe-C_3FJuSs.js.map} +1 -1
  25. package/web-dist/assets/dist-Bgkh2hc5.js +56 -0
  26. package/web-dist/assets/dist-Bgkh2hc5.js.map +1 -0
  27. package/web-dist/assets/{enroll-C7UBC20s.js → enroll-DijmoolY.js} +2 -2
  28. package/web-dist/assets/{enroll-C7UBC20s.js.map → enroll-DijmoolY.js.map} +1 -1
  29. package/web-dist/assets/index-C5n2QB-Y.css +2 -0
  30. package/web-dist/assets/{index-IdzOP9GE.js → index-Cf6ZUCfK.js} +3 -3
  31. package/web-dist/assets/{index-IdzOP9GE.js.map → index-Cf6ZUCfK.js.map} +1 -1
  32. package/web-dist/assets/{more-syTnSpW3.js → more-CRGJsz6E.js} +2 -2
  33. package/web-dist/assets/{more-syTnSpW3.js.map → more-CRGJsz6E.js.map} +1 -1
  34. package/web-dist/assets/{now-BqsLS-tK.js → now-D6lDofYy.js} +2 -2
  35. package/web-dist/assets/{now-BqsLS-tK.js.map → now-D6lDofYy.js.map} +1 -1
  36. package/web-dist/assets/{settings-DRj7UPtP.js → settings-Csal1F-W.js} +6 -6
  37. package/web-dist/assets/settings-Csal1F-W.js.map +1 -0
  38. package/web-dist/assets/{useNow-BQpDYijR.js → useNow-Bc_NlDY3.js} +2 -2
  39. package/web-dist/assets/{useNow-BQpDYijR.js.map → useNow-Bc_NlDY3.js.map} +1 -1
  40. package/web-dist/index.html +2 -2
  41. package/web-dist/sw.js +1 -1
  42. package/web-dist/assets/Menu-C4GUY_ym.js.map +0 -1
  43. package/web-dist/assets/code-DavnmlQN.js +0 -8
  44. package/web-dist/assets/code-DavnmlQN.js.map +0 -1
  45. package/web-dist/assets/dist-C-h8FC-n.js +0 -56
  46. package/web-dist/assets/dist-C-h8FC-n.js.map +0 -1
  47. package/web-dist/assets/index-ByiuDoG0.css +0 -2
  48. package/web-dist/assets/settings-DRj7UPtP.js.map +0 -1
@@ -283,6 +283,8 @@ var RuntimeCapabilities = z3.object({
283
283
  costAccounting: z3.boolean(),
284
284
  /** The host runs sessions as interactive Pi in a terminal that clients can attach to. */
285
285
  terminal: z3.boolean().optional(),
286
+ /** The host answers session.models, session.commands, session.setModel, session.setThinking and session.compact. */
287
+ sessionControls: z3.boolean().optional(),
286
288
  unsupported: z3.array(z3.object({ capability: z3.string(), reason: z3.string() }))
287
289
  });
288
290
  var ProjectScan = z3.object({
@@ -364,6 +366,28 @@ var Workspace = z3.object({
364
366
  updatedAt: Iso
365
367
  });
366
368
  var ThinkingLevel = z3.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
369
+ var SessionModel = z3.object({
370
+ provider: z3.string(),
371
+ id: z3.string(),
372
+ name: z3.string().nullable(),
373
+ contextWindow: z3.number().nullable(),
374
+ reasoning: z3.boolean(),
375
+ /** In Pi's scoped (favorite) model list; set only when Pi has one. */
376
+ scoped: z3.boolean().optional(),
377
+ /** Effort levels this model accepts, in Pi's order; ["off"] without reasoning. */
378
+ thinkingLevels: z3.array(ThinkingLevel)
379
+ });
380
+ var SlashCommand = z3.object({
381
+ name: z3.string().min(1),
382
+ description: z3.string().nullable(),
383
+ source: z3.enum(["extension", "prompt", "skill"])
384
+ });
385
+ var ContextUsage = z3.object({
386
+ /** Estimated tokens in context; null right after compaction until the next reply. */
387
+ tokens: z3.number().nullable(),
388
+ window: z3.number(),
389
+ percent: z3.number().nullable()
390
+ });
367
391
  var FORBIDDEN_EXTRA_ARGS = ["--mode", "--fork", "--no-session", "-e", "--extension"];
368
392
  var ExtraArg = z3.string().refine((a) => a.startsWith("--") || !a.startsWith("-"), "extra args must be long flags").refine((a) => !FORBIDDEN_EXTRA_ARGS.includes(a.split("=")[0]) && !a.startsWith("--session"), "this flag is managed by Switchyard");
369
393
  var Profile = z3.object({
@@ -373,7 +397,9 @@ var Profile = z3.object({
373
397
  model: z3.string().nullable(),
374
398
  thinking: ThinkingLevel.nullable(),
375
399
  projectTrust: z3.enum(["ask", "approve", "ignore"]),
376
- extraArgs: z3.array(ExtraArg)
400
+ extraArgs: z3.array(ExtraArg),
401
+ /** "vanilla": --no-extensions --no-skills --no-prompt-templates. Undefined means "all". */
402
+ resources: z3.enum(["all", "vanilla"]).optional()
377
403
  });
378
404
  var RuntimeState = z3.enum([
379
405
  "starting",
@@ -387,7 +413,8 @@ var RuntimeState = z3.enum([
387
413
  var NativeHandle = z3.object({ sessionId: z3.string(), sessionFile: z3.string() });
388
414
  var SessionStats = z3.object({
389
415
  tokens: z3.number().nullable(),
390
- costUsd: z3.number().nullable()
416
+ costUsd: z3.number().nullable(),
417
+ context: ContextUsage.nullable().optional()
391
418
  });
392
419
  var Session = z3.object({
393
420
  id: Id,
@@ -410,6 +437,12 @@ var Session = z3.object({
410
437
  archivedAt: Iso.nullable(),
411
438
  queue: z3.object({ steering: z3.array(z3.string()), followUp: z3.array(z3.string()) }),
412
439
  stats: SessionStats.nullable(),
440
+ /** The live model (host-owned; last known when stopped). */
441
+ model: SessionModel.nullable().optional(),
442
+ /** The live effort (Pi's thinking level). */
443
+ thinking: ThinkingLevel.nullable().optional(),
444
+ /** Pi is compacting the context. */
445
+ compacting: z3.boolean().optional(),
413
446
  lastAssistantText: z3.string().nullable(),
414
447
  createdAt: Iso,
415
448
  /** A live terminal session: `switchyard tui` can attach to its Pi terminal. */
@@ -848,7 +881,8 @@ var ProfileInput = z4.object({
848
881
  model: z4.string().nullable().default(null),
849
882
  thinking: ThinkingLevel.nullable().default(null),
850
883
  projectTrust: z4.enum(["ask", "approve", "ignore"]).default("ask"),
851
- extraArgs: z4.array(ExtraArg).default([])
884
+ extraArgs: z4.array(ExtraArg).default([]),
885
+ resources: z4.enum(["all", "vanilla"]).default("all")
852
886
  });
853
887
  var CreateWorkspaceRequest = z4.object({
854
888
  checkoutId: z4.string(),
@@ -879,6 +913,18 @@ var PromptRequest = z4.object({
879
913
  clientMessageId: z4.string().optional()
880
914
  });
881
915
  var RenameRequest = z4.object({ title: z4.string().min(1).max(200) });
916
+ var SetModelRequest = z4.object({
917
+ provider: z4.string().min(1),
918
+ modelId: z4.string().min(1)
919
+ });
920
+ var SetThinkingRequest = z4.object({ level: ThinkingLevel });
921
+ var CompactRequest = z4.object({ instructions: z4.string().max(4e3).optional() });
922
+ var SessionModelsResult = z4.object({ models: z4.array(SessionModel) });
923
+ var SessionCommandsResult = z4.object({ commands: z4.array(SlashCommand) });
924
+ var ModelStateResult = z4.object({
925
+ model: SessionModel.nullable(),
926
+ thinking: ThinkingLevel.nullable()
927
+ });
882
928
  var ImportSessionRequest = z4.object({
883
929
  hostId: z4.string(),
884
930
  sessionFile: z4.string(),
@@ -1253,6 +1299,20 @@ var HostCommand = z8.discriminatedUnion("type", [
1253
1299
  z8.object({ type: z8.literal("session.importable") }),
1254
1300
  z8.object({ type: z8.literal("session.readNative"), sessionFile: z8.string() }),
1255
1301
  z8.object({ type: z8.literal("session.snapshot"), sessionId: z8.string() }),
1302
+ z8.object({ type: z8.literal("session.models"), sessionId: z8.string() }),
1303
+ z8.object({ type: z8.literal("session.commands"), sessionId: z8.string() }),
1304
+ z8.object({
1305
+ type: z8.literal("session.setModel"),
1306
+ sessionId: z8.string(),
1307
+ provider: z8.string().min(1),
1308
+ modelId: z8.string().min(1)
1309
+ }),
1310
+ z8.object({ type: z8.literal("session.setThinking"), sessionId: z8.string(), level: ThinkingLevel }),
1311
+ z8.object({
1312
+ type: z8.literal("session.compact"),
1313
+ sessionId: z8.string(),
1314
+ instructions: z8.string().nullable()
1315
+ }),
1256
1316
  z8.object({ type: z8.literal("checkout.inspect"), path: z8.string() }),
1257
1317
  z8.object({ type: z8.literal("projects.scan"), folder: z8.string().min(1) }),
1258
1318
  z8.object({
@@ -4278,7 +4338,7 @@ function resolveProfile(ctx, profileId, project) {
4278
4338
  const id = profileId ?? project?.defaultProfileId ?? null;
4279
4339
  if (id)
4280
4340
  return ctx.store.profiles.require(id);
4281
- const first = ctx.store.profiles.list(void 0, [], "created_at")[0];
4341
+ const first = ctx.store.profiles.list(void 0, [], "created_at, rowid")[0];
4282
4342
  if (!first)
4283
4343
  throw new SwitchyardError("invalid_request", "No Pi profile configured");
4284
4344
  return first;
@@ -7437,6 +7497,9 @@ var HOST_SESSION_FIELDS = [
7437
7497
  "runtimeDetail",
7438
7498
  "queue",
7439
7499
  "stats",
7500
+ "model",
7501
+ "thinking",
7502
+ "compacting",
7440
7503
  "lastAssistantText",
7441
7504
  "lastActivityAt",
7442
7505
  "terminal"
@@ -7955,7 +8018,7 @@ var HostHub = class {
7955
8018
  projectId: ws?.projectId ?? null,
7956
8019
  cwd: "",
7957
8020
  nativeHandle: null,
7958
- profileSnapshot: this.store.profiles.list(void 0, [], "created_at")[0] ?? FALLBACK_PROFILE,
8021
+ profileSnapshot: this.store.profiles.list(void 0, [], "created_at, rowid")[0] ?? FALLBACK_PROFILE,
7959
8022
  runtime: "idle",
7960
8023
  runtimeDetail: null,
7961
8024
  runId: null,
@@ -9133,23 +9196,39 @@ function registerOpsRoutes(app, ctx) {
9133
9196
 
9134
9197
  // packages/coordinator/dist/routes/profiles.js
9135
9198
  var ProfilePatch = Profile.omit({ id: true }).partial();
9136
- function seedDefaultProfile(ctx) {
9199
+ function seedDefaultProfiles(ctx) {
9137
9200
  ctx.store.tx(() => {
9138
- if (ctx.store.profiles.count() > 0)
9201
+ if (ctx.store.profiles.count() === 0) {
9202
+ ctx.store.putProfile({
9203
+ id: newId("prf"),
9204
+ name: "Default",
9205
+ provider: null,
9206
+ model: null,
9207
+ thinking: null,
9208
+ projectTrust: "ask",
9209
+ extraArgs: []
9210
+ });
9211
+ }
9212
+ if (ctx.store.getSetting("profiles.vanillaSeeded", false))
9139
9213
  return;
9140
- ctx.store.putProfile({
9141
- id: newId("prf"),
9142
- name: "Default",
9143
- provider: null,
9144
- model: null,
9145
- thinking: null,
9146
- projectTrust: "ask",
9147
- extraArgs: []
9148
- });
9214
+ const has = ctx.store.profiles.list().some((p) => p.resources === "vanilla");
9215
+ if (!has) {
9216
+ ctx.store.putProfile({
9217
+ id: newId("prf"),
9218
+ name: "Vanilla",
9219
+ provider: null,
9220
+ model: null,
9221
+ thinking: null,
9222
+ projectTrust: "ask",
9223
+ extraArgs: [],
9224
+ resources: "vanilla"
9225
+ });
9226
+ }
9227
+ ctx.store.setSetting("profiles.vanillaSeeded", true);
9149
9228
  });
9150
9229
  }
9151
9230
  function registerProfileRoutes(app, ctx) {
9152
- app.get("/api/profiles", async () => ctx.store.profiles.list(void 0, [], "created_at"));
9231
+ app.get("/api/profiles", async () => ctx.store.profiles.list(void 0, [], "created_at, rowid"));
9153
9232
  app.get("/api/profiles/:id", async (req) => ctx.store.profiles.require(req.params.id));
9154
9233
  app.post("/api/profiles", async (req, reply) => {
9155
9234
  const body = parse(ProfileInput, req.body);
@@ -9316,6 +9395,20 @@ function registerProjectRoutes(app, ctx) {
9316
9395
 
9317
9396
  // packages/coordinator/dist/routes/sessions.js
9318
9397
  import { z as z16 } from "zod";
9398
+ function requireLiveSession(ctx, s) {
9399
+ if (s.readOnly)
9400
+ throw new SwitchyardError("forbidden", SUBAGENT_READ_ONLY);
9401
+ if (!LIVE_RUNTIMES2.includes(s.runtime) || s.runtime === "starting") {
9402
+ throw new SwitchyardError("conflict", s.runtime === "starting" ? "Session is still starting" : `Session ${s.runtime}: Resume to send`);
9403
+ }
9404
+ requireConnected(ctx, s.hostId);
9405
+ }
9406
+ function requireSessionControls(ctx, s) {
9407
+ requireLiveSession(ctx, s);
9408
+ if (ctx.store.hosts.get(s.hostId)?.capabilities?.sessionControls !== true) {
9409
+ throw new SwitchyardError("invalid_request", "Update Switchyard on this host to change the model, effort or commands from here");
9410
+ }
9411
+ }
9319
9412
  var ListQuery5 = z16.object({
9320
9413
  projectId: z16.string().optional(),
9321
9414
  hostId: z16.string().optional(),
@@ -9449,12 +9542,7 @@ function registerSessionRoutes(app, ctx) {
9449
9542
  app.post("/api/sessions/:id/prompt", async (req) => {
9450
9543
  const s = get(idOf(req));
9451
9544
  const body = parse(PromptRequest, req.body);
9452
- if (s.readOnly)
9453
- throw new SwitchyardError("forbidden", SUBAGENT_READ_ONLY);
9454
- if (!LIVE_RUNTIMES2.includes(s.runtime) || s.runtime === "starting") {
9455
- throw new SwitchyardError("conflict", s.runtime === "starting" ? "Session is still starting" : `Session ${s.runtime}: Resume to send`);
9456
- }
9457
- requireConnected(ctx, s.hostId);
9545
+ requireLiveSession(ctx, s);
9458
9546
  const run = ctx.store.runForSession(s);
9459
9547
  const result = await ctx.hostHub.request(s.hostId, {
9460
9548
  type: "session.prompt",
@@ -9472,6 +9560,53 @@ function registerSessionRoutes(app, ctx) {
9472
9560
  automationActive: run?.controlMode === "automation"
9473
9561
  };
9474
9562
  });
9563
+ app.get("/api/sessions/:id/models", async (req) => {
9564
+ const s = get(idOf(req));
9565
+ requireSessionControls(ctx, s);
9566
+ return SessionModelsResult.parse(await ctx.hostHub.request(s.hostId, { type: "session.models", sessionId: s.id }));
9567
+ });
9568
+ app.get("/api/sessions/:id/commands", async (req) => {
9569
+ const s = get(idOf(req));
9570
+ requireSessionControls(ctx, s);
9571
+ return SessionCommandsResult.parse(await ctx.hostHub.request(s.hostId, { type: "session.commands", sessionId: s.id }));
9572
+ });
9573
+ app.post("/api/sessions/:id/model", async (req) => {
9574
+ const s = get(idOf(req));
9575
+ const body = parse(SetModelRequest, req.body);
9576
+ requireSessionControls(ctx, s);
9577
+ return ModelStateResult.parse(await ctx.hostHub.request(s.hostId, {
9578
+ type: "session.setModel",
9579
+ sessionId: s.id,
9580
+ provider: body.provider,
9581
+ modelId: body.modelId
9582
+ }));
9583
+ });
9584
+ app.post("/api/sessions/:id/thinking", async (req) => {
9585
+ const s = get(idOf(req));
9586
+ const body = parse(SetThinkingRequest, req.body);
9587
+ requireSessionControls(ctx, s);
9588
+ return ModelStateResult.parse(await ctx.hostHub.request(s.hostId, {
9589
+ type: "session.setThinking",
9590
+ sessionId: s.id,
9591
+ level: body.level
9592
+ }));
9593
+ });
9594
+ app.post("/api/sessions/:id/compact", async (req) => {
9595
+ const s = get(idOf(req));
9596
+ const body = parse(CompactRequest, req.body ?? {});
9597
+ requireSessionControls(ctx, s);
9598
+ if (s.runtime !== "idle") {
9599
+ throw new SwitchyardError("conflict", "Wait for the run to finish before compacting");
9600
+ }
9601
+ if (s.compacting)
9602
+ throw new SwitchyardError("conflict", "Compaction is already running");
9603
+ await ctx.hostHub.request(s.hostId, {
9604
+ type: "session.compact",
9605
+ sessionId: s.id,
9606
+ instructions: body.instructions?.trim() || null
9607
+ });
9608
+ return { ok: true };
9609
+ });
9475
9610
  app.post("/api/sessions/:id/abort", async (req) => {
9476
9611
  const s = get(idOf(req));
9477
9612
  const body = parse(AbortRequest, req.body);
@@ -10356,7 +10491,7 @@ var Store2 = class {
10356
10491
  projects: this.projects.list(void 0, [], "created_at"),
10357
10492
  checkouts: this.checkouts.list(),
10358
10493
  workspaces: this.workspaces.list("archived_at IS NULL"),
10359
- profiles: this.profiles.list(void 0, [], "created_at"),
10494
+ profiles: this.profiles.list(void 0, [], "created_at, rowid"),
10360
10495
  sessions,
10361
10496
  tasks,
10362
10497
  runs,
@@ -10546,7 +10681,7 @@ function createCoordinator(opts) {
10546
10681
  auth: new RateLimiter(limits ? limits.auth ?? AUTH_RATE_LIMIT : Number.POSITIVE_INFINITY, 6e4, storeClock),
10547
10682
  hosts: new RateLimiter(limits ? limits.hosts ?? HOSTS_RATE_LIMIT : Number.POSITIVE_INFINITY, 6e4, storeClock)
10548
10683
  };
10549
- seedDefaultProfile({ store });
10684
+ seedDefaultProfiles({ store });
10550
10685
  const attention = new AttentionService(store, hub);
10551
10686
  const hostHub = new HostHub(store, hub, attention, log.child({ mod: "host-hub" }), {
10552
10687
  unreachableAfterMs: config.unreachableAfterMs,
@@ -12776,7 +12911,7 @@ function toErrorBody(err) {
12776
12911
  function queueKey(command) {
12777
12912
  if (command.type === "checks.run")
12778
12913
  return `checks:${command.sessionId}`;
12779
- if (command.type === "session.answer" || command.type === "session.abort" || command.type === "session.setControl" || command.type === "session.adapterAction" || command.type === "session.snapshot") {
12914
+ if (command.type === "session.answer" || command.type === "session.abort" || command.type === "session.setControl" || command.type === "session.adapterAction" || command.type === "session.snapshot" || command.type === "session.models" || command.type === "session.commands") {
12780
12915
  return `control:${command.sessionId}`;
12781
12916
  }
12782
12917
  if ("sessionId" in command && typeof command.sessionId === "string") {
@@ -12931,6 +13066,16 @@ var CommandRouter = class {
12931
13066
  return sup.readNative(c.sessionFile);
12932
13067
  case "session.snapshot":
12933
13068
  return sup.snapshot(c.sessionId);
13069
+ case "session.models":
13070
+ return sup.models(c.sessionId);
13071
+ case "session.commands":
13072
+ return sup.commands(c.sessionId);
13073
+ case "session.setModel":
13074
+ return sup.setModel(c.sessionId, c.provider, c.modelId);
13075
+ case "session.setThinking":
13076
+ return sup.setThinking(c.sessionId, c.level);
13077
+ case "session.compact":
13078
+ return sup.compact(c.sessionId, c.instructions);
12934
13079
  case "checkout.inspect":
12935
13080
  return inspectCheckout2(c.path);
12936
13081
  case "projects.scan":
@@ -13871,6 +14016,45 @@ function createLogger2(opts = {}) {
13871
14016
  // packages/host-agent/dist/main.js
13872
14017
  import { readFileSync as readFileSync8 } from "node:fs";
13873
14018
 
14019
+ // packages/pi-runtime/dist/bridge/pi-model.js
14020
+ var PI_THINKING_LEVELS = [
14021
+ "off",
14022
+ "minimal",
14023
+ "low",
14024
+ "medium",
14025
+ "high",
14026
+ "xhigh",
14027
+ "max"
14028
+ ];
14029
+ function supportedThinkingLevels(model) {
14030
+ const known = PI_THINKING_LEVELS;
14031
+ if (Array.isArray(model?.thinkingLevels)) {
14032
+ return model.thinkingLevels.filter((l) => typeof l === "string" && known.includes(l));
14033
+ }
14034
+ if (!model?.reasoning)
14035
+ return ["off"];
14036
+ return PI_THINKING_LEVELS.filter((level) => {
14037
+ const mapped = model.thinkingLevelMap?.[level];
14038
+ if (mapped === null)
14039
+ return false;
14040
+ if (level === "xhigh" || level === "max")
14041
+ return mapped !== void 0;
14042
+ return true;
14043
+ });
14044
+ }
14045
+ function modelInfo(model) {
14046
+ if (!model || typeof model.provider !== "string" || typeof model.id !== "string")
14047
+ return null;
14048
+ return {
14049
+ provider: model.provider,
14050
+ id: model.id,
14051
+ name: typeof model.name === "string" && model.name ? model.name : null,
14052
+ contextWindow: typeof model.contextWindow === "number" ? model.contextWindow : null,
14053
+ reasoning: model.reasoning === true,
14054
+ thinkingLevels: supportedThinkingLevels(model)
14055
+ };
14056
+ }
14057
+
13874
14058
  // packages/pi-runtime/dist/capabilities.js
13875
14059
  import { execFile as execFile2 } from "node:child_process";
13876
14060
  import semver from "semver";
@@ -13889,6 +14073,7 @@ var PI_RPC_CAPABILITIES = {
13889
14073
  widgetsComponent: false,
13890
14074
  tokenAccounting: true,
13891
14075
  costAccounting: true,
14076
+ sessionControls: true,
13892
14077
  unsupported: [
13893
14078
  { capability: "customUi", reason: "ctx.ui.custom() returns undefined in RPC mode" },
13894
14079
  {
@@ -13912,6 +14097,7 @@ function noCapabilities(reason) {
13912
14097
  widgetsComponent: false,
13913
14098
  tokenAccounting: false,
13914
14099
  costAccounting: false,
14100
+ sessionControls: false,
13915
14101
  unsupported: [{ capability: "all", reason }]
13916
14102
  };
13917
14103
  }
@@ -15099,6 +15285,11 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15099
15285
  items = /* @__PURE__ */ new Map();
15100
15286
  partialToolOutput = /* @__PURE__ */ new Map();
15101
15287
  runAssistantText = null;
15288
+ _model = null;
15289
+ scoped = null;
15290
+ _thinking = null;
15291
+ _compacting = false;
15292
+ lastModelJson = "";
15102
15293
  constructor(opts) {
15103
15294
  super();
15104
15295
  this.opts = opts;
@@ -15149,6 +15340,12 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15149
15340
  get remoteDialogs() {
15150
15341
  return this.proc instanceof PiTerminalProcess ? this.proc.remoteDialogs : true;
15151
15342
  }
15343
+ get modelState() {
15344
+ return { model: this._model ? { ...this._model } : null, thinking: this._thinking };
15345
+ }
15346
+ get compacting() {
15347
+ return this._compacting;
15348
+ }
15152
15349
  get isAlive() {
15153
15350
  return this.proc !== null && this.proc.exited === null;
15154
15351
  }
@@ -15175,6 +15372,9 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15175
15372
  args.push("--approve");
15176
15373
  else if (profile.projectTrust === "ignore")
15177
15374
  args.push("--no-approve");
15375
+ if (profile.resources === "vanilla") {
15376
+ args.push("--no-extensions", "--no-skills", "--no-prompt-templates");
15377
+ }
15178
15378
  args.push(...profile.extraArgs);
15179
15379
  args.push(...this.opts.extraArgs ?? []);
15180
15380
  for (const ext of this.opts.extensions ?? [])
@@ -15245,6 +15445,9 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15245
15445
  this._native = { sessionId: state.sessionId, sessionFile: state.sessionFile };
15246
15446
  this.started = true;
15247
15447
  this.busy = state.isStreaming;
15448
+ this.applyModel(state.model);
15449
+ this.applyThinking(state.thinkingLevel);
15450
+ this.emitModelIfChanged();
15248
15451
  this.emit("native", { ...this._native });
15249
15452
  this.setRuntime();
15250
15453
  }
@@ -15272,6 +15475,9 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15272
15475
  }
15273
15476
  this.started = true;
15274
15477
  this.busy = state.isStreaming;
15478
+ this.applyModel(state.model);
15479
+ this.applyThinking(state.thinkingLevel);
15480
+ this.emitModelIfChanged();
15275
15481
  this.setRuntime();
15276
15482
  }
15277
15483
  /** Terminal transport: let go of the holder; Pi keeps running and no exit is reported. */
@@ -15341,10 +15547,115 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15341
15547
  async rename(title) {
15342
15548
  await this.call(this.requireAlive(), { type: "set_session_name", name: title });
15343
15549
  }
15550
+ /** Models Pi can use right now. */
15551
+ async models() {
15552
+ const data = await this.call(this.requireAlive(), {
15553
+ type: "get_available_models"
15554
+ });
15555
+ const out = [];
15556
+ for (const raw of data?.models ?? []) {
15557
+ const m = this.toSessionModel(raw);
15558
+ if (!m)
15559
+ continue;
15560
+ if (this.scoped && this.scoped.size > 0)
15561
+ m.scoped = this.scoped.has(`${m.provider}/${m.id}`);
15562
+ out.push(m);
15563
+ }
15564
+ return out.sort((a, b) => a.provider.localeCompare(b.provider) || (a.name ?? a.id).localeCompare(b.name ?? b.id));
15565
+ }
15566
+ /** Slash commands Pi knows (extension commands, prompt templates, skills). */
15567
+ async commands() {
15568
+ const data = await this.call(this.requireAlive(), {
15569
+ type: "get_commands"
15570
+ });
15571
+ const out = [];
15572
+ for (const c of data?.commands ?? []) {
15573
+ if (typeof c?.name !== "string" || !c.name)
15574
+ continue;
15575
+ if (c.source !== "extension" && c.source !== "prompt" && c.source !== "skill")
15576
+ continue;
15577
+ out.push({
15578
+ name: c.name,
15579
+ description: typeof c.description === "string" ? c.description : null,
15580
+ source: c.source
15581
+ });
15582
+ }
15583
+ return out.sort((a, b) => a.name.localeCompare(b.name));
15584
+ }
15585
+ async setModel(provider, modelId) {
15586
+ await this.call(this.requireAlive(), { type: "set_model", provider, modelId });
15587
+ await this.refreshModelState();
15588
+ return this.modelState;
15589
+ }
15590
+ async setThinking(level) {
15591
+ await this.call(this.requireAlive(), { type: "set_thinking_level", level });
15592
+ await this.refreshModelState();
15593
+ return this.modelState;
15594
+ }
15595
+ /** Start a manual compaction. Terminal sessions answer at once; Pi's RPC answers when done. */
15596
+ async compact(instructions) {
15597
+ const proc = this.requireAlive();
15598
+ const cmd = { type: "compact", ...instructions ? { customInstructions: instructions } : {} };
15599
+ if (this.transport === "terminal") {
15600
+ await this.call(proc, cmd, 1e4);
15601
+ } else {
15602
+ this.call(proc, cmd, 30 * 6e4).catch(() => {
15603
+ });
15604
+ }
15605
+ }
15606
+ async refreshModelState() {
15607
+ const s = await this.state();
15608
+ this.applyModel(s.model);
15609
+ this.applyThinking(s.thinkingLevel);
15610
+ this.emitModelIfChanged();
15611
+ }
15612
+ toSessionModel(raw) {
15613
+ const info = modelInfo(raw);
15614
+ if (!info)
15615
+ return null;
15616
+ return {
15617
+ ...info,
15618
+ thinkingLevels: info.thinkingLevels.flatMap((l) => {
15619
+ const r = ThinkingLevel.safeParse(l);
15620
+ return r.success ? [r.data] : [];
15621
+ })
15622
+ };
15623
+ }
15624
+ applyScoped(raw) {
15625
+ if (Array.isArray(raw))
15626
+ this.scoped = new Set(raw.filter((x) => typeof x === "string"));
15627
+ }
15628
+ applyModel(raw) {
15629
+ this._model = this.toSessionModel(raw);
15630
+ }
15631
+ applyThinking(raw) {
15632
+ const r = ThinkingLevel.safeParse(raw);
15633
+ this._thinking = r.success ? r.data : null;
15634
+ }
15635
+ emitModelIfChanged() {
15636
+ const json = JSON.stringify(this.modelState);
15637
+ if (json === this.lastModelJson)
15638
+ return;
15639
+ this.lastModelJson = json;
15640
+ this.emit("model", this.modelState);
15641
+ }
15642
+ setCompacting(v) {
15643
+ if (this._compacting === v)
15644
+ return;
15645
+ this._compacting = v;
15646
+ this.emit("compacting", v);
15647
+ }
15344
15648
  /** Token and cost totals (`get_session_stats`). */
15345
15649
  async stats() {
15346
15650
  const raw = await this.call(this.requireAlive(), { type: "get_session_stats" });
15651
+ const cu = raw?.contextUsage;
15652
+ const context = cu && typeof cu.contextWindow === "number" ? {
15653
+ tokens: typeof cu.tokens === "number" ? cu.tokens : null,
15654
+ window: cu.contextWindow,
15655
+ percent: typeof cu.percent === "number" ? cu.percent : null
15656
+ } : null;
15347
15657
  return {
15658
+ context,
15348
15659
  tokens: typeof raw?.tokens?.total === "number" ? raw.tokens.total : null,
15349
15660
  costUsd: typeof raw?.cost === "number" ? raw.cost : null,
15350
15661
  raw
@@ -15478,6 +15789,12 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15478
15789
  this.emit("bridge", msg);
15479
15790
  if (msg.type === "ui_prompt_end")
15480
15791
  this.onPromptEnd(msg);
15792
+ if (msg.type === "model_state") {
15793
+ this.applyScoped(msg.scoped);
15794
+ this.applyModel(msg.model);
15795
+ this.applyThinking(msg.thinkingLevel);
15796
+ this.emitModelIfChanged();
15797
+ }
15481
15798
  });
15482
15799
  proc.on("stderr", (text) => this.emit("stderr", text));
15483
15800
  proc.on("protocol_error", (err, line) => {
@@ -15520,6 +15837,7 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15520
15837
  this.emit("ui_resolved", { id, by: "exit", request: req });
15521
15838
  this.pending.clear();
15522
15839
  this.current = null;
15840
+ this.setCompacting(false);
15523
15841
  const wasBusy = this.busy;
15524
15842
  this.busy = false;
15525
15843
  if (this.terminal === "failed") {
@@ -15686,15 +16004,29 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15686
16004
  this.notice("error", `Retry failed: ${ev.finalError ?? "unknown error"}`);
15687
16005
  break;
15688
16006
  case "compaction_start":
16007
+ this.setCompacting(true);
15689
16008
  this.notice("info", `Compacting context (${ev.reason ?? "manual"})`);
15690
16009
  break;
15691
16010
  case "compaction_end":
16011
+ this.setCompacting(false);
15692
16012
  if (ev.result)
15693
16013
  this.notice("info", "Context compacted");
15694
16014
  else if (ev.aborted)
15695
16015
  this.notice("warning", "Compaction aborted");
15696
- else
15697
- this.notice("error", `Compaction failed: ${ev.errorMessage ?? "unknown error"}`);
16016
+ else {
16017
+ const msg = String(ev.errorMessage ?? "unknown error").replace(/^Compaction failed:\s*/, "");
16018
+ this.notice("error", `Compaction failed: ${msg}`);
16019
+ }
16020
+ break;
16021
+ case "model_state":
16022
+ this.applyScoped(ev.scoped);
16023
+ this.applyModel(ev.model);
16024
+ this.applyThinking(ev.thinkingLevel);
16025
+ this.emitModelIfChanged();
16026
+ break;
16027
+ case "thinking_level_changed":
16028
+ this.applyThinking(ev.level);
16029
+ this.emitModelIfChanged();
15698
16030
  break;
15699
16031
  case "extension_error":
15700
16032
  this.emit("diagnostic", {
@@ -15987,6 +16319,7 @@ var SessionSupervisor = class {
15987
16319
  markLost(rec, expireSummary) {
15988
16320
  rec.runtime = "interrupted";
15989
16321
  rec.runtimeDetail = "Host agent restarted";
16322
+ rec.compacting = false;
15990
16323
  rec.queue = { steering: [], followUp: [] };
15991
16324
  rec.terminal = null;
15992
16325
  this.expirePending(rec.id, expireSummary);
@@ -16116,6 +16449,9 @@ var SessionSupervisor = class {
16116
16449
  archivedAt: null,
16117
16450
  queue: rec.queue,
16118
16451
  stats: rec.stats,
16452
+ model: rec.model ?? null,
16453
+ thinking: rec.thinking ?? null,
16454
+ compacting: rec.compacting ?? false,
16119
16455
  lastAssistantText: rec.lastAssistantText,
16120
16456
  createdAt: rec.createdAt,
16121
16457
  terminal: rec.terminal != null
@@ -16241,6 +16577,9 @@ var SessionSupervisor = class {
16241
16577
  nextOrder,
16242
16578
  queue: { steering: [], followUp: [] },
16243
16579
  stats: prev?.stats ?? null,
16580
+ model: prev?.model ?? null,
16581
+ thinking: prev?.thinking ?? null,
16582
+ compacting: false,
16244
16583
  lastAssistantText: prev?.lastAssistantText ?? null,
16245
16584
  lastActivityAt: now,
16246
16585
  createdAt: prev?.createdAt ?? now
@@ -16331,6 +16670,28 @@ var SessionSupervisor = class {
16331
16670
  this.db.tx(() => this.saveAndEmit(rec));
16332
16671
  return { title };
16333
16672
  }
16673
+ async models(sessionId) {
16674
+ return { models: await this.requireLive(sessionId).models() };
16675
+ }
16676
+ async commands(sessionId) {
16677
+ return { commands: await this.requireLive(sessionId).commands() };
16678
+ }
16679
+ async setModel(sessionId, provider, modelId) {
16680
+ return this.requireLive(sessionId).setModel(provider, modelId);
16681
+ }
16682
+ async setThinking(sessionId, level) {
16683
+ return this.requireLive(sessionId).setThinking(level);
16684
+ }
16685
+ async compact(sessionId, instructions) {
16686
+ const ms = this.requireLive(sessionId);
16687
+ if (ms.compacting)
16688
+ throw new SwitchyardError("conflict", "Compaction is already running");
16689
+ if (ms.runtime !== "idle") {
16690
+ throw new SwitchyardError("conflict", "Wait for the run to finish before compacting");
16691
+ }
16692
+ await ms.compact(instructions);
16693
+ return { ok: true };
16694
+ }
16334
16695
  async adapterAction(sessionId, adapterId, actionId, payload) {
16335
16696
  const l = this.live.get(sessionId);
16336
16697
  if (!l?.adapters) {
@@ -16585,6 +16946,18 @@ var SessionSupervisor = class {
16585
16946
  this.db.tx(() => this.saveAndEmit(rec));
16586
16947
  }
16587
16948
  });
16949
+ ms.on("model", (m) => {
16950
+ rec.model = m.model;
16951
+ rec.thinking = m.thinking;
16952
+ this.db.tx(() => this.saveAndEmit(rec));
16953
+ void this.refreshStats(l);
16954
+ });
16955
+ ms.on("compacting", (c) => {
16956
+ rec.compacting = c;
16957
+ this.db.tx(() => this.saveAndEmit(rec));
16958
+ if (!c)
16959
+ void this.refreshStats(l);
16960
+ });
16588
16961
  ms.on("diagnostic", (d) => this.diagnostic(rec.id, null, d.level, d.message));
16589
16962
  ms.on("stderr", (text) => this.opts.log.debug({ sessionId: rec.id, stderr: text.slice(0, 2e3) }, "pi stderr"));
16590
16963
  ms.on("ui_request", (req) => this.enqueue(l, () => this.onUiRequest(l, req)));
@@ -16619,7 +16992,7 @@ var SessionSupervisor = class {
16619
16992
  return;
16620
16993
  try {
16621
16994
  const s = await ms.stats();
16622
- const stats = { tokens: s.tokens, costUsd: s.costUsd };
16995
+ const stats = { tokens: s.tokens, costUsd: s.costUsd, context: s.context };
16623
16996
  if (JSON.stringify(stats) === JSON.stringify(l.rec.stats))
16624
16997
  return;
16625
16998
  l.rec.stats = stats;
@@ -16664,6 +17037,8 @@ var SessionSupervisor = class {
16664
17037
  return;
16665
17038
  case "message_end": {
16666
17039
  const m = ev.message;
17040
+ if (m?.role === "assistant")
17041
+ void this.refreshStats(l);
16667
17042
  if (adapters && m?.role === "custom" && typeof m.customType === "string") {
16668
17043
  await adapters.onCustomMessage({
16669
17044
  customType: m.customType,