@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/index.cjs CHANGED
@@ -360,6 +360,9 @@ var acp = __toESM(require("@agentclientprotocol/sdk"), 1);
360
360
  // src/agents/types.ts
361
361
  var EMPTY_USAGE = { used: 0, size: 0, cost: null };
362
362
 
363
+ // src/version.ts
364
+ var VBOT_VERSION = "0.1.3";
365
+
363
366
  // src/agents/acp.ts
364
367
  var AgentAbortError = class extends Error {
365
368
  constructor() {
@@ -376,6 +379,14 @@ var AgentStartError = class extends Error {
376
379
  stderr;
377
380
  };
378
381
  var STDERR_LIMIT = 4e3;
382
+ function flattenSelectOptions(options) {
383
+ return options.flatMap(
384
+ (entry) => "group" in entry ? entry.options.map((option) => ({ value: option.value, label: option.name })) : [{ value: entry.value, label: entry.name }]
385
+ );
386
+ }
387
+ function isModelOption(option) {
388
+ return option.type === "select" && (option.category === "model" || option.id === "model");
389
+ }
379
390
  var AcpAgent = class {
380
391
  constructor(name, config) {
381
392
  this.name = name;
@@ -405,6 +416,14 @@ var AcpAgent = class {
405
416
  sessionId() {
406
417
  return this.session;
407
418
  }
419
+ currentModel() {
420
+ const option = this.configOptions.find(isModelOption);
421
+ if (!option || option.type !== "select") return null;
422
+ const value = String(option.currentValue);
423
+ const options = flattenSelectOptions(option.options);
424
+ const label = options.find((entry) => entry.value === value)?.label ?? value;
425
+ return { value, label, options };
426
+ }
408
427
  resetSession() {
409
428
  this.session = null;
410
429
  }
@@ -472,7 +491,7 @@ var AcpAgent = class {
472
491
  const init = await this.ctx.request(acp.methods.agent.initialize, {
473
492
  protocolVersion: 1,
474
493
  clientCapabilities: {},
475
- clientInfo: { name: "vbot", version: "0.1.0" }
494
+ clientInfo: { name: "vbot", version: VBOT_VERSION }
476
495
  });
477
496
  this.initCaps = init.agentCapabilities;
478
497
  } catch (error) {
@@ -591,15 +610,14 @@ var AcpAgent = class {
591
610
  this.pendingModel = model;
592
611
  return true;
593
612
  }
594
- const option = this.configOptions.find(
595
- (entry) => entry.type === "select" && (entry.id === "model" || entry.category === "model")
596
- );
613
+ const option = this.configOptions.find(isModelOption);
597
614
  if (!option) return false;
598
- await this.ctx.request(acp.methods.agent.session.setConfigOption, {
615
+ const response = await this.ctx.request(acp.methods.agent.session.setConfigOption, {
599
616
  sessionId: this.session,
600
617
  configId: option.id,
601
618
  value: model
602
619
  });
620
+ if (response?.configOptions?.length) this.configOptions = response.configOptions;
603
621
  this.pendingModel = null;
604
622
  return true;
605
623
  }
@@ -735,21 +753,29 @@ var BotQueue = class {
735
753
  }
736
754
  };
737
755
 
738
- // src/version.ts
739
- var VBOT_VERSION = "0.1.2";
740
-
741
756
  // src/core/dispatcher.ts
742
- var HELP_TEXT = `Available commands:
743
- /new \u2014 Start a new session
744
- /stop \u2014 Abort the current request
745
- /status \u2014 Show agent, session and queue status
746
- /agent [name] \u2014 Show or switch agent backend
747
- /model [name] \u2014 Show or switch model
748
- /cost \u2014 Show context usage and cost
749
- /version \u2014 Show vbot version
750
- /help \u2014 Show this help
751
-
752
- Other /commands are sent to the agent as prompts.`;
757
+ var import_node_os2 = require("os");
758
+ var HELP_ROWS = [
759
+ ["/new", "Start a new session"],
760
+ ["/stop", "Abort the current request"],
761
+ ["/status", "Show agent, session and queue status"],
762
+ ["/agent [name]", "Show or switch agent backend"],
763
+ ["/model [name]", "Show or switch model"],
764
+ ["/cost", "Show context usage and cost"],
765
+ ["/version", "Show vbot version"],
766
+ ["/help", "Show this help"]
767
+ ];
768
+ var HELP_FOOTER = "Other /commands are sent to the agent as prompts.";
769
+ var NEW_BUSY_TEXT = "Cannot reset the session while a request is running. Use /stop first.";
770
+ var MODEL_BUSY_TEXT = "Cannot switch model while a request is running. Use /stop first.";
771
+ var HELP_TEXT = [
772
+ table(
773
+ ["Command", "Description"],
774
+ HELP_ROWS.map(([command, description]) => [command, description])
775
+ ),
776
+ "",
777
+ HELP_FOOTER
778
+ ].join("\n");
753
779
  function createDispatcher(runtime) {
754
780
  return (text, reply) => {
755
781
  const send = (message) => {
@@ -763,11 +789,57 @@ function createDispatcher(runtime) {
763
789
  });
764
790
  };
765
791
  }
792
+ function escapeCell(value) {
793
+ return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
794
+ }
795
+ function table(headers, rows) {
796
+ const head = `| ${headers.map(escapeCell).join(" | ")} |`;
797
+ const separator = `| ${headers.map(() => "---").join(" | ")} |`;
798
+ const body = rows.map((row) => `| ${row.map(escapeCell).join(" | ")} |`);
799
+ return [head, separator, ...body].join("\n");
800
+ }
801
+ function formatNumber(value) {
802
+ if (!Number.isFinite(value)) return String(value);
803
+ const negative = value < 0;
804
+ const digits = Math.abs(Math.trunc(value)).toString();
805
+ let out = "";
806
+ for (let i = 0; i < digits.length; i += 1) {
807
+ if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
808
+ out += digits[i];
809
+ }
810
+ return negative ? "-" + out : out;
811
+ }
812
+ function percentOf(used, size) {
813
+ if (size <= 0) return null;
814
+ return `${(used / size * 100).toFixed(1)}%`;
815
+ }
816
+ function usageText(runtime) {
817
+ const { used, size } = runtime.state().usage;
818
+ if (size <= 0) return "not reported";
819
+ const base = `${formatNumber(used)} / ${formatNumber(size)}`;
820
+ const percent = percentOf(used, size);
821
+ return percent ? `${base} (${percent})` : base;
822
+ }
823
+ function compactPath(path) {
824
+ const home = (0, import_node_os2.homedir)();
825
+ if (!home) return path;
826
+ if (path === home) return "~";
827
+ const prefix = home + "/";
828
+ if (path.startsWith(prefix)) return "~" + path.slice(home.length);
829
+ return path;
830
+ }
831
+ function modelText(runtime) {
832
+ const current = runtime.agent().currentModel();
833
+ if (current) return current.label;
834
+ const configured = runtime.config().agent.model;
835
+ if (configured) return `${configured} (configured, not applied yet)`;
836
+ return "unknown (session not started)";
837
+ }
766
838
  function handleSlashCommand(runtime, text, send) {
767
839
  const [rawCommand, ...args] = text.split(/\s+/);
768
840
  switch (rawCommand.toLowerCase()) {
769
841
  case "/new":
770
- handleNew(runtime, send);
842
+ void handleNew(runtime, send);
771
843
  return null;
772
844
  case "/stop":
773
845
  handleStop(runtime, send);
@@ -779,7 +851,7 @@ function handleSlashCommand(runtime, text, send) {
779
851
  handleAgent(runtime, args, send);
780
852
  return null;
781
853
  case "/model":
782
- handleModel(runtime, args, send);
854
+ void handleModel(runtime, args, send);
783
855
  return null;
784
856
  case "/cost":
785
857
  send(costText(runtime));
@@ -794,12 +866,18 @@ function handleSlashCommand(runtime, text, send) {
794
866
  return text.slice(1);
795
867
  }
796
868
  }
797
- function handleNew(runtime, send) {
798
- runtime.agent().resetSession();
799
- const state = runtime.state();
800
- state.acpSessionId = null;
801
- state.usage = { used: 0, size: 0, cost: null };
802
- runtime.saveState();
869
+ async function handleNew(runtime, send) {
870
+ if (runtime.queue().status().busy) {
871
+ send(NEW_BUSY_TEXT);
872
+ return;
873
+ }
874
+ await runtime.serialize(async () => {
875
+ runtime.agent().resetSession();
876
+ const state = runtime.state();
877
+ state.acpSessionId = null;
878
+ state.usage = { used: 0, size: 0, cost: null };
879
+ runtime.saveState();
880
+ });
803
881
  send("Session reset. Starting a new conversation.");
804
882
  }
805
883
  function handleStop(runtime, send) {
@@ -815,12 +893,18 @@ function handleStop(runtime, send) {
815
893
  }
816
894
  function handleAgent(runtime, args, send) {
817
895
  const name = args.join(" ").trim();
896
+ const current = runtime.config().agent.name;
818
897
  if (!name) {
819
- const current = runtime.config().agent.name;
820
- const list = AGENT_NAMES.map((entry) => `${entry === current ? "*" : " "} ${entry}`).join("\n");
821
- send(`Current agent: ${current}
822
- Available agents:
823
- ${list}`);
898
+ send(
899
+ table(
900
+ ["Agent", "Backend", ""],
901
+ AGENT_NAMES.map((entry) => [
902
+ entry,
903
+ AGENTS[entry].label,
904
+ entry === current ? "\u2190 current" : ""
905
+ ])
906
+ )
907
+ );
824
908
  return;
825
909
  }
826
910
  if (!isAgentName(name)) {
@@ -836,56 +920,95 @@ ${list}`);
836
920
  (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
837
921
  );
838
922
  }
839
- function handleModel(runtime, args, send) {
923
+ async function handleModel(runtime, args, send) {
840
924
  const model = args.join(" ").trim();
841
925
  if (!model) {
842
- send(`Current model: ${runtime.config().agent.model ?? "default"}`);
926
+ send(modelReport(runtime));
927
+ return;
928
+ }
929
+ if (runtime.queue().status().busy) {
930
+ send(MODEL_BUSY_TEXT);
843
931
  return;
844
932
  }
845
933
  const config = runtime.config();
846
934
  const hadSession = runtime.agent().sessionId() !== null;
847
- config.agent.model = model;
848
- runtime.saveConfig();
849
- runtime.agent().setModel(model).then((supported) => {
850
- if (!supported) {
851
- send(
852
- `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
853
- );
854
- return;
855
- }
935
+ const supported = await runtime.serialize(async () => {
936
+ const accepted = await runtime.agent().setModel(model);
937
+ config.agent.model = model;
938
+ runtime.saveConfig();
939
+ return accepted;
940
+ });
941
+ const applied = runtime.agent().currentModel()?.label ?? model;
942
+ if (!supported) {
856
943
  send(
857
- hadSession ? `Model switched to: ${model}` : `Model set to: ${model}. It will apply when the session starts.`
944
+ `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
858
945
  );
859
- }).catch(
860
- (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
946
+ return;
947
+ }
948
+ send(
949
+ hadSession ? `Model switched to: ${applied}` : `Model set to: ${applied}. It will apply when the session starts.`
861
950
  );
862
951
  }
952
+ function modelReport(runtime) {
953
+ const current = runtime.agent().currentModel();
954
+ if (!current) {
955
+ const configured = runtime.config().agent.model;
956
+ const hint = configured ? `Configured: ${configured}
957
+
958
+ 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.";
959
+ return `Current: unknown
960
+
961
+ ${hint}`;
962
+ }
963
+ const lines = [`Current: ${current.label}`];
964
+ if (current.options.length > 0) {
965
+ lines.push(
966
+ "",
967
+ table(
968
+ ["Model", ""],
969
+ current.options.map((option) => [
970
+ option.label,
971
+ option.value === current.value ? "\u2190 current" : ""
972
+ ])
973
+ )
974
+ );
975
+ }
976
+ lines.push("", "Switch: /model <name>");
977
+ return lines.join("\n");
978
+ }
863
979
  function statusText(runtime) {
864
980
  const config = runtime.config();
865
981
  const agent = runtime.agent();
866
982
  const state = runtime.state();
867
983
  const queue = runtime.queue().status();
868
- const lines = [
869
- `Agent: ${config.agent.name} (${AGENTS[config.agent.name].label})`,
870
- `Session: ${agent.sessionId() ?? "none"}`,
871
- `Work dir: ${config.agent.cwd}`,
872
- `Model: ${config.agent.model ?? "default"}`,
873
- `Status: ${queue.busy ? "busy" : "idle"}`,
874
- `Queue: ${queue.queued}`,
875
- `Usage: ${state.usage.used}/${state.usage.size} tokens`,
876
- `Capabilities: ${capabilitiesText(agent.capabilities())}`
984
+ const { used, size } = state.usage;
985
+ const summary = [
986
+ `**${config.agent.name}**`,
987
+ queue.busy ? "busy" : "idle",
988
+ size > 0 ? `${formatNumber(used)} / ${formatNumber(size)}` : null
989
+ ].filter(Boolean).join(" \xB7 ");
990
+ const rows = [
991
+ ["Agent", `${config.agent.name} (${AGENTS[config.agent.name].label})`],
992
+ ["Session", agent.sessionId() ?? "none"],
993
+ ["Work dir", compactPath(config.agent.cwd)],
994
+ ["Model", modelText(runtime)],
995
+ ["Status", queue.busy ? "busy" : "idle"],
996
+ ["Queue", String(queue.queued)],
997
+ ["Usage", usageText(runtime)],
998
+ ["Capabilities", capabilitiesText(agent.capabilities())]
877
999
  ];
878
- return lines.join("\n");
1000
+ return [summary, "", table(["Field", "Value"], rows)].join("\n");
879
1001
  }
880
1002
  function costText(runtime) {
881
1003
  const usage = runtime.state().usage;
882
- const context = `Context: ${usage.used}/${usage.size} tokens`;
883
- if (!usage.cost) {
884
- return `${context}
885
- Cost: this agent does not report cost.`;
886
- }
887
- return `${context}
888
- Cost: ${usage.cost.amount} ${usage.cost.currency}`;
1004
+ const cost = usage.cost ? `${usage.cost.amount} ${usage.cost.currency}` : "not reported by this agent";
1005
+ return table(
1006
+ ["Field", "Value"],
1007
+ [
1008
+ ["Context", usageText(runtime)],
1009
+ ["Cost", cost]
1010
+ ]
1011
+ );
889
1012
  }
890
1013
  function capabilitiesText(caps) {
891
1014
  const enabled = [
@@ -904,10 +1027,10 @@ var import_node_path4 = require("path");
904
1027
  // src/core/state.ts
905
1028
  var import_node_crypto = require("crypto");
906
1029
  var import_node_fs3 = require("fs");
907
- var import_node_os2 = require("os");
1030
+ var import_node_os3 = require("os");
908
1031
  var import_node_path3 = require("path");
909
1032
  function vbotHome() {
910
- return process.env.VBOT_HOME?.trim() || (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".vbot");
1033
+ return process.env.VBOT_HOME?.trim() || (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".vbot");
911
1034
  }
912
1035
  function stateDir() {
913
1036
  return (0, import_node_path3.join)(vbotHome(), "state");
@@ -1281,13 +1404,17 @@ async function startBot(configPath) {
1281
1404
  saveConfig: () => saveConfig(config, configPath)
1282
1405
  };
1283
1406
  holder.queue = new BotQueue((item, signal) => gate(() => processPrompt(ctx, item, signal)));
1284
- const runtime = createRuntime(ctx, (name) => {
1285
- if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1286
- switching = true;
1287
- return gate(() => switchAgent(ctx, name)).finally(() => {
1288
- switching = false;
1289
- });
1290
- });
1407
+ const runtime = createRuntime(
1408
+ ctx,
1409
+ (task) => gate(task),
1410
+ (name) => {
1411
+ if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1412
+ switching = true;
1413
+ return gate(() => switchAgent(ctx, name)).finally(() => {
1414
+ switching = false;
1415
+ });
1416
+ }
1417
+ );
1291
1418
  const dispatch = createDispatcher(runtime);
1292
1419
  let notifySignal = () => void 0;
1293
1420
  const wire = new VicvicWire({
@@ -1348,7 +1475,7 @@ async function runBot(configPath) {
1348
1475
  const bot = await startBot(configPath);
1349
1476
  await bot.finished;
1350
1477
  }
1351
- function createRuntime(ctx, switchAgent2) {
1478
+ function createRuntime(ctx, gate, switchAgent2) {
1352
1479
  return {
1353
1480
  config: () => ctx.config,
1354
1481
  agent: () => ctx.getAgent(),
@@ -1356,6 +1483,7 @@ function createRuntime(ctx, switchAgent2) {
1356
1483
  state: () => ctx.state,
1357
1484
  saveState: () => ctx.saveState(),
1358
1485
  saveConfig: () => ctx.saveConfig(),
1486
+ serialize: (task) => gate(task),
1359
1487
  switchAgent: switchAgent2
1360
1488
  };
1361
1489
  }