@vicvic-im/agent-bot 0.1.2 → 0.1.3

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/dist/server.cjs CHANGED
@@ -249,6 +249,9 @@ var acp = __toESM(require("@agentclientprotocol/sdk"), 1);
249
249
  // src/agents/types.ts
250
250
  var EMPTY_USAGE = { used: 0, size: 0, cost: null };
251
251
 
252
+ // src/version.ts
253
+ var VBOT_VERSION = "0.1.3";
254
+
252
255
  // src/agents/acp.ts
253
256
  var AgentAbortError = class extends Error {
254
257
  constructor() {
@@ -265,6 +268,14 @@ var AgentStartError = class extends Error {
265
268
  stderr;
266
269
  };
267
270
  var STDERR_LIMIT = 4e3;
271
+ function flattenSelectOptions(options) {
272
+ return options.flatMap(
273
+ (entry) => "group" in entry ? entry.options.map((option) => ({ value: option.value, label: option.name })) : [{ value: entry.value, label: entry.name }]
274
+ );
275
+ }
276
+ function isModelOption(option) {
277
+ return option.type === "select" && (option.category === "model" || option.id === "model");
278
+ }
268
279
  var AcpAgent = class {
269
280
  constructor(name, config) {
270
281
  this.name = name;
@@ -294,6 +305,14 @@ var AcpAgent = class {
294
305
  sessionId() {
295
306
  return this.session;
296
307
  }
308
+ currentModel() {
309
+ const option = this.configOptions.find(isModelOption);
310
+ if (!option || option.type !== "select") return null;
311
+ const value = String(option.currentValue);
312
+ const options = flattenSelectOptions(option.options);
313
+ const label = options.find((entry) => entry.value === value)?.label ?? value;
314
+ return { value, label, options };
315
+ }
297
316
  resetSession() {
298
317
  this.session = null;
299
318
  }
@@ -361,7 +380,7 @@ var AcpAgent = class {
361
380
  const init = await this.ctx.request(acp.methods.agent.initialize, {
362
381
  protocolVersion: 1,
363
382
  clientCapabilities: {},
364
- clientInfo: { name: "vbot", version: "0.1.0" }
383
+ clientInfo: { name: "vbot", version: VBOT_VERSION }
365
384
  });
366
385
  this.initCaps = init.agentCapabilities;
367
386
  } catch (error) {
@@ -480,15 +499,14 @@ var AcpAgent = class {
480
499
  this.pendingModel = model;
481
500
  return true;
482
501
  }
483
- const option = this.configOptions.find(
484
- (entry) => entry.type === "select" && (entry.id === "model" || entry.category === "model")
485
- );
502
+ const option = this.configOptions.find(isModelOption);
486
503
  if (!option) return false;
487
- await this.ctx.request(acp.methods.agent.session.setConfigOption, {
504
+ const response = await this.ctx.request(acp.methods.agent.session.setConfigOption, {
488
505
  sessionId: this.session,
489
506
  configId: option.id,
490
507
  value: model
491
508
  });
509
+ if (response?.configOptions?.length) this.configOptions = response.configOptions;
492
510
  this.pendingModel = null;
493
511
  return true;
494
512
  }
@@ -624,21 +642,29 @@ var BotQueue = class {
624
642
  }
625
643
  };
626
644
 
627
- // src/version.ts
628
- var VBOT_VERSION = "0.1.2";
629
-
630
645
  // src/core/dispatcher.ts
631
- var HELP_TEXT = `Available commands:
632
- /new \u2014 Start a new session
633
- /stop \u2014 Abort the current request
634
- /status \u2014 Show agent, session and queue status
635
- /agent [name] \u2014 Show or switch agent backend
636
- /model [name] \u2014 Show or switch model
637
- /cost \u2014 Show context usage and cost
638
- /version \u2014 Show vbot version
639
- /help \u2014 Show this help
640
-
641
- Other /commands are sent to the agent as prompts.`;
646
+ var import_node_os2 = require("os");
647
+ var HELP_ROWS = [
648
+ ["/new", "Start a new session"],
649
+ ["/stop", "Abort the current request"],
650
+ ["/status", "Show agent, session and queue status"],
651
+ ["/agent [name]", "Show or switch agent backend"],
652
+ ["/model [name]", "Show or switch model"],
653
+ ["/cost", "Show context usage and cost"],
654
+ ["/version", "Show vbot version"],
655
+ ["/help", "Show this help"]
656
+ ];
657
+ var HELP_FOOTER = "Other /commands are sent to the agent as prompts.";
658
+ var NEW_BUSY_TEXT = "Cannot reset the session while a request is running. Use /stop first.";
659
+ var MODEL_BUSY_TEXT = "Cannot switch model while a request is running. Use /stop first.";
660
+ var HELP_TEXT = [
661
+ table(
662
+ ["Command", "Description"],
663
+ HELP_ROWS.map(([command, description]) => [command, description])
664
+ ),
665
+ "",
666
+ HELP_FOOTER
667
+ ].join("\n");
642
668
  function createDispatcher(runtime) {
643
669
  return (text, reply) => {
644
670
  const send = (message) => {
@@ -652,11 +678,57 @@ function createDispatcher(runtime) {
652
678
  });
653
679
  };
654
680
  }
681
+ function escapeCell(value) {
682
+ return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
683
+ }
684
+ function table(headers, rows) {
685
+ const head = `| ${headers.map(escapeCell).join(" | ")} |`;
686
+ const separator = `| ${headers.map(() => "---").join(" | ")} |`;
687
+ const body = rows.map((row) => `| ${row.map(escapeCell).join(" | ")} |`);
688
+ return [head, separator, ...body].join("\n");
689
+ }
690
+ function formatNumber(value) {
691
+ if (!Number.isFinite(value)) return String(value);
692
+ const negative = value < 0;
693
+ const digits = Math.abs(Math.trunc(value)).toString();
694
+ let out = "";
695
+ for (let i = 0; i < digits.length; i += 1) {
696
+ if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
697
+ out += digits[i];
698
+ }
699
+ return negative ? "-" + out : out;
700
+ }
701
+ function percentOf(used, size) {
702
+ if (size <= 0) return null;
703
+ return `${(used / size * 100).toFixed(1)}%`;
704
+ }
705
+ function usageText(runtime) {
706
+ const { used, size } = runtime.state().usage;
707
+ if (size <= 0) return "not reported";
708
+ const base = `${formatNumber(used)} / ${formatNumber(size)}`;
709
+ const percent = percentOf(used, size);
710
+ return percent ? `${base} (${percent})` : base;
711
+ }
712
+ function compactPath(path) {
713
+ const home = (0, import_node_os2.homedir)();
714
+ if (!home) return path;
715
+ if (path === home) return "~";
716
+ const prefix = home + "/";
717
+ if (path.startsWith(prefix)) return "~" + path.slice(home.length);
718
+ return path;
719
+ }
720
+ function modelText(runtime) {
721
+ const current = runtime.agent().currentModel();
722
+ if (current) return current.label;
723
+ const configured = runtime.config().agent.model;
724
+ if (configured) return `${configured} (configured, not applied yet)`;
725
+ return "unknown (session not started)";
726
+ }
655
727
  function handleSlashCommand(runtime, text, send) {
656
728
  const [rawCommand, ...args] = text.split(/\s+/);
657
729
  switch (rawCommand.toLowerCase()) {
658
730
  case "/new":
659
- handleNew(runtime, send);
731
+ void handleNew(runtime, send);
660
732
  return null;
661
733
  case "/stop":
662
734
  handleStop(runtime, send);
@@ -668,7 +740,7 @@ function handleSlashCommand(runtime, text, send) {
668
740
  handleAgent(runtime, args, send);
669
741
  return null;
670
742
  case "/model":
671
- handleModel(runtime, args, send);
743
+ void handleModel(runtime, args, send);
672
744
  return null;
673
745
  case "/cost":
674
746
  send(costText(runtime));
@@ -683,12 +755,18 @@ function handleSlashCommand(runtime, text, send) {
683
755
  return text.slice(1);
684
756
  }
685
757
  }
686
- function handleNew(runtime, send) {
687
- runtime.agent().resetSession();
688
- const state = runtime.state();
689
- state.acpSessionId = null;
690
- state.usage = { used: 0, size: 0, cost: null };
691
- runtime.saveState();
758
+ async function handleNew(runtime, send) {
759
+ if (runtime.queue().status().busy) {
760
+ send(NEW_BUSY_TEXT);
761
+ return;
762
+ }
763
+ await runtime.serialize(async () => {
764
+ runtime.agent().resetSession();
765
+ const state = runtime.state();
766
+ state.acpSessionId = null;
767
+ state.usage = { used: 0, size: 0, cost: null };
768
+ runtime.saveState();
769
+ });
692
770
  send("Session reset. Starting a new conversation.");
693
771
  }
694
772
  function handleStop(runtime, send) {
@@ -704,12 +782,18 @@ function handleStop(runtime, send) {
704
782
  }
705
783
  function handleAgent(runtime, args, send) {
706
784
  const name = args.join(" ").trim();
785
+ const current = runtime.config().agent.name;
707
786
  if (!name) {
708
- const current = runtime.config().agent.name;
709
- const list = AGENT_NAMES.map((entry) => `${entry === current ? "*" : " "} ${entry}`).join("\n");
710
- send(`Current agent: ${current}
711
- Available agents:
712
- ${list}`);
787
+ send(
788
+ table(
789
+ ["Agent", "Backend", ""],
790
+ AGENT_NAMES.map((entry) => [
791
+ entry,
792
+ AGENTS[entry].label,
793
+ entry === current ? "\u2190 current" : ""
794
+ ])
795
+ )
796
+ );
713
797
  return;
714
798
  }
715
799
  if (!isAgentName(name)) {
@@ -725,56 +809,95 @@ ${list}`);
725
809
  (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
726
810
  );
727
811
  }
728
- function handleModel(runtime, args, send) {
812
+ async function handleModel(runtime, args, send) {
729
813
  const model = args.join(" ").trim();
730
814
  if (!model) {
731
- send(`Current model: ${runtime.config().agent.model ?? "default"}`);
815
+ send(modelReport(runtime));
816
+ return;
817
+ }
818
+ if (runtime.queue().status().busy) {
819
+ send(MODEL_BUSY_TEXT);
732
820
  return;
733
821
  }
734
822
  const config = runtime.config();
735
823
  const hadSession = runtime.agent().sessionId() !== null;
736
- config.agent.model = model;
737
- runtime.saveConfig();
738
- runtime.agent().setModel(model).then((supported) => {
739
- if (!supported) {
740
- send(
741
- `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
742
- );
743
- return;
744
- }
824
+ const supported = await runtime.serialize(async () => {
825
+ const accepted = await runtime.agent().setModel(model);
826
+ config.agent.model = model;
827
+ runtime.saveConfig();
828
+ return accepted;
829
+ });
830
+ const applied = runtime.agent().currentModel()?.label ?? model;
831
+ if (!supported) {
745
832
  send(
746
- hadSession ? `Model switched to: ${model}` : `Model set to: ${model}. It will apply when the session starts.`
833
+ `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
747
834
  );
748
- }).catch(
749
- (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
835
+ return;
836
+ }
837
+ send(
838
+ hadSession ? `Model switched to: ${applied}` : `Model set to: ${applied}. It will apply when the session starts.`
750
839
  );
751
840
  }
841
+ function modelReport(runtime) {
842
+ const current = runtime.agent().currentModel();
843
+ if (!current) {
844
+ const configured = runtime.config().agent.model;
845
+ const hint = configured ? `Configured: ${configured}
846
+
847
+ The agent has not reported a model yet; it becomes visible after the first message.` : "This agent does not expose a runtime model option, or the session has not started.";
848
+ return `Current: unknown
849
+
850
+ ${hint}`;
851
+ }
852
+ const lines = [`Current: ${current.label}`];
853
+ if (current.options.length > 0) {
854
+ lines.push(
855
+ "",
856
+ table(
857
+ ["Model", ""],
858
+ current.options.map((option) => [
859
+ option.label,
860
+ option.value === current.value ? "\u2190 current" : ""
861
+ ])
862
+ )
863
+ );
864
+ }
865
+ lines.push("", "Switch: /model <name>");
866
+ return lines.join("\n");
867
+ }
752
868
  function statusText(runtime) {
753
869
  const config = runtime.config();
754
870
  const agent = runtime.agent();
755
871
  const state = runtime.state();
756
872
  const queue = runtime.queue().status();
757
- const lines = [
758
- `Agent: ${config.agent.name} (${AGENTS[config.agent.name].label})`,
759
- `Session: ${agent.sessionId() ?? "none"}`,
760
- `Work dir: ${config.agent.cwd}`,
761
- `Model: ${config.agent.model ?? "default"}`,
762
- `Status: ${queue.busy ? "busy" : "idle"}`,
763
- `Queue: ${queue.queued}`,
764
- `Usage: ${state.usage.used}/${state.usage.size} tokens`,
765
- `Capabilities: ${capabilitiesText(agent.capabilities())}`
873
+ const { used, size } = state.usage;
874
+ const summary = [
875
+ `**${config.agent.name}**`,
876
+ queue.busy ? "busy" : "idle",
877
+ size > 0 ? `${formatNumber(used)} / ${formatNumber(size)}` : null
878
+ ].filter(Boolean).join(" \xB7 ");
879
+ const rows = [
880
+ ["Agent", `${config.agent.name} (${AGENTS[config.agent.name].label})`],
881
+ ["Session", agent.sessionId() ?? "none"],
882
+ ["Work dir", compactPath(config.agent.cwd)],
883
+ ["Model", modelText(runtime)],
884
+ ["Status", queue.busy ? "busy" : "idle"],
885
+ ["Queue", String(queue.queued)],
886
+ ["Usage", usageText(runtime)],
887
+ ["Capabilities", capabilitiesText(agent.capabilities())]
766
888
  ];
767
- return lines.join("\n");
889
+ return [summary, "", table(["Field", "Value"], rows)].join("\n");
768
890
  }
769
891
  function costText(runtime) {
770
892
  const usage = runtime.state().usage;
771
- const context = `Context: ${usage.used}/${usage.size} tokens`;
772
- if (!usage.cost) {
773
- return `${context}
774
- Cost: this agent does not report cost.`;
775
- }
776
- return `${context}
777
- Cost: ${usage.cost.amount} ${usage.cost.currency}`;
893
+ const cost = usage.cost ? `${usage.cost.amount} ${usage.cost.currency}` : "not reported by this agent";
894
+ return table(
895
+ ["Field", "Value"],
896
+ [
897
+ ["Context", usageText(runtime)],
898
+ ["Cost", cost]
899
+ ]
900
+ );
778
901
  }
779
902
  function capabilitiesText(caps) {
780
903
  const enabled = [
@@ -829,10 +952,10 @@ var import_node_path3 = require("path");
829
952
  // src/core/state.ts
830
953
  var import_node_crypto = require("crypto");
831
954
  var import_node_fs2 = require("fs");
832
- var import_node_os2 = require("os");
955
+ var import_node_os3 = require("os");
833
956
  var import_node_path2 = require("path");
834
957
  function vbotHome() {
835
- return process.env.VBOT_HOME?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".vbot");
958
+ return process.env.VBOT_HOME?.trim() || (0, import_node_path2.join)((0, import_node_os3.homedir)(), ".vbot");
836
959
  }
837
960
  function stateDir() {
838
961
  return (0, import_node_path2.join)(vbotHome(), "state");
@@ -1161,13 +1284,17 @@ async function startBot(configPath) {
1161
1284
  saveConfig: () => saveConfig(config, configPath)
1162
1285
  };
1163
1286
  holder.queue = new BotQueue((item, signal) => gate(() => processPrompt(ctx, item, signal)));
1164
- const runtime = createRuntime(ctx, (name) => {
1165
- if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1166
- switching = true;
1167
- return gate(() => switchAgent(ctx, name)).finally(() => {
1168
- switching = false;
1169
- });
1170
- });
1287
+ const runtime = createRuntime(
1288
+ ctx,
1289
+ (task) => gate(task),
1290
+ (name) => {
1291
+ if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1292
+ switching = true;
1293
+ return gate(() => switchAgent(ctx, name)).finally(() => {
1294
+ switching = false;
1295
+ });
1296
+ }
1297
+ );
1171
1298
  const dispatch = createDispatcher(runtime);
1172
1299
  let notifySignal = () => void 0;
1173
1300
  const wire = new VicvicWire({
@@ -1228,7 +1355,7 @@ async function runBot(configPath) {
1228
1355
  const bot = await startBot(configPath);
1229
1356
  await bot.finished;
1230
1357
  }
1231
- function createRuntime(ctx, switchAgent2) {
1358
+ function createRuntime(ctx, gate, switchAgent2) {
1232
1359
  return {
1233
1360
  config: () => ctx.config,
1234
1361
  agent: () => ctx.getAgent(),
@@ -1236,6 +1363,7 @@ function createRuntime(ctx, switchAgent2) {
1236
1363
  state: () => ctx.state,
1237
1364
  saveState: () => ctx.saveState(),
1238
1365
  saveConfig: () => ctx.saveConfig(),
1366
+ serialize: (task) => gate(task),
1239
1367
  switchAgent: switchAgent2
1240
1368
  };
1241
1369
  }