@agentconnect.md/daemon 1.0.0-rc.15 → 1.0.0-rc.17

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.js CHANGED
@@ -23126,6 +23126,34 @@ function routeRules(msg, rules, threadOwner) {
23126
23126
  return null;
23127
23127
  }
23128
23128
  //#endregion
23129
+ //#region src/commands/commands.ts
23130
+ /** Accepted command prefixes (Slack uses `!`; `/` is for future platforms). */
23131
+ const COMMAND_PREFIXES = ["!", "/"];
23132
+ const STOP_WORDS = /* @__PURE__ */ new Set(["stop", "cancel"]);
23133
+ const QUEUE_WORDS = /* @__PURE__ */ new Set(["queue"]);
23134
+ /**
23135
+ * Parse a leading control command from a message's text. Returns `null` when the
23136
+ * text is not a recognized command (so it flows to the agent unchanged). The
23137
+ * prefix must be the first non-whitespace character and be followed immediately by
23138
+ * a known command word, so ordinary text like `hello!` or `! note` is never a
23139
+ * command.
23140
+ */
23141
+ function parseCommand(raw) {
23142
+ const text = raw.trimStart();
23143
+ const prefix = COMMAND_PREFIXES.find((p) => text.startsWith(p));
23144
+ if (!prefix) return null;
23145
+ const m = /^([a-zA-Z]+)([\s\S]*)$/.exec(text.slice(prefix.length));
23146
+ if (!m) return null;
23147
+ const word = m[1].toLowerCase();
23148
+ const arg = (m[2] ?? "").trim();
23149
+ if (STOP_WORDS.has(word)) return { kind: "stop" };
23150
+ if (QUEUE_WORDS.has(word)) return {
23151
+ kind: "queue",
23152
+ text: arg
23153
+ };
23154
+ return null;
23155
+ }
23156
+ //#endregion
23129
23157
  //#region src/router/routing-rule.ts
23130
23158
  /**
23131
23159
  * Resolve an agent to its first Slack integration's `{ integrationId, botUserId }`.
@@ -80536,6 +80564,7 @@ const LOADING_MSGS = [
80536
80564
  "Crunching through it…",
80537
80565
  "Hang tight…"
80538
80566
  ];
80567
+ const MAX_QUEUED_PER_SESSION = 10;
80539
80568
  var Daemon = class {
80540
80569
  opts;
80541
80570
  store;
@@ -80697,6 +80726,11 @@ var Daemon = class {
80697
80726
  }
80698
80727
  this.seenMsgIds.add(msg.msgId);
80699
80728
  if (this.seenMsgIds.size > 2e3) this.seenMsgIds.clear();
80729
+ const command = parseCommand(msg.text);
80730
+ if (command) {
80731
+ this.handleCommand(command, msg);
80732
+ return;
80733
+ }
80700
80734
  const result = routeRules(msg, this.mergedRules(), (c, t) => this.sessions.threadOwner(c, t));
80701
80735
  if (!result) {
80702
80736
  this.log.debug(`routing: dropped message in ch=${msg.channel} (no agent matched — not a mention of a known bot, not a subscribed 'all' channel, not a thread/DM hit)`);
@@ -80705,6 +80739,67 @@ var Daemon = class {
80705
80739
  this.log.info(`routing: ch=${msg.channel} → agent "${result.agentId}" (integration ${result.integrationId})`);
80706
80740
  this.dispatch(result.agentId, msg, result.integrationId).catch((err) => this.log.error(`dispatch failed for agent "${result.agentId}": ${err.stack ?? err}`));
80707
80741
  }
80742
+ queued = /* @__PURE__ */ new Map();
80743
+ /**
80744
+ * Handle an in-conversation control command. Resolves the target agent via the
80745
+ * same routing ladder as a normal message (so thread-affinity + per-integration
80746
+ * `allowedUserIds` authz apply), then acts on that agent's session in this
80747
+ * (channel, thread).
80748
+ */
80749
+ handleCommand(command, msg) {
80750
+ const target = routeRules(msg, this.mergedRules(), (c, t) => this.sessions.threadOwner(c, t));
80751
+ if (!target) {
80752
+ this.log.debug(`command: '${command.kind}' in ch=${msg.channel} — no agent resolved, ignoring`);
80753
+ return;
80754
+ }
80755
+ const conn = this.replyConnFor(target.agentId, target.integrationId);
80756
+ const thread = msg.thread ?? msg.msgId;
80757
+ const acpSessionId = this.store.getSession(sessionKey(msg.platform, msg.channel, thread, target.agentId))?.acpSessionId;
80758
+ const inflight = !!(acpSessionId && this.pending.has(acpSessionId));
80759
+ if (command.kind === "stop") {
80760
+ if (!inflight) {
80761
+ conn?.postMessage(msg.channel, "Nothing is running to stop.", thread);
80762
+ return;
80763
+ }
80764
+ this.queued.delete(acpSessionId);
80765
+ this.log.info(`command: stop → agent "${target.agentId}" session ${acpSessionId}`);
80766
+ this.hosts.get(target.agentId)?.cancel(acpSessionId).catch((err) => this.log.error(`command stop: cancel failed: ${err.message}`));
80767
+ conn?.postMessage(msg.channel, "🛑 Stopped.", thread);
80768
+ return;
80769
+ }
80770
+ if (!command.text) {
80771
+ conn?.postMessage(msg.channel, "Usage: `!queue <message>` — runs when the current turn finishes.", thread);
80772
+ return;
80773
+ }
80774
+ const payload = {
80775
+ ...msg,
80776
+ text: command.text
80777
+ };
80778
+ if (!inflight) {
80779
+ this.log.info(`command: queue → agent "${target.agentId}" idle, dispatching now`);
80780
+ this.dispatch(target.agentId, payload, target.integrationId).catch((err) => this.log.error(`queued dispatch failed for agent "${target.agentId}": ${err.stack ?? err}`));
80781
+ return;
80782
+ }
80783
+ const q = this.queued.get(acpSessionId) ?? [];
80784
+ if (q.length >= MAX_QUEUED_PER_SESSION) {
80785
+ this.log.warn(`command: queue → agent "${target.agentId}" session ${acpSessionId} full (${q.length}), rejected`);
80786
+ conn?.postMessage(msg.channel, `Queue is full (${MAX_QUEUED_PER_SESSION} pending) — wait for the current turn to finish.`, thread);
80787
+ return;
80788
+ }
80789
+ q.push(payload);
80790
+ this.queued.set(acpSessionId, q);
80791
+ this.log.info(`command: queue → agent "${target.agentId}" session ${acpSessionId} (depth ${q.length})`);
80792
+ conn?.postMessage(msg.channel, `📥 Queued (#${q.length}) — will run when the current turn finishes.`, thread);
80793
+ }
80794
+ /** Drain one buffered message for a session whose turn just finished (FIFO). */
80795
+ flushQueued(agentId, sessionId, integrationId) {
80796
+ const q = this.queued.get(sessionId);
80797
+ if (!q || q.length === 0) return;
80798
+ const next = q.shift();
80799
+ if (q.length === 0) this.queued.delete(sessionId);
80800
+ this.log.info(`queue: dispatching buffered message to agent "${agentId}" session ${sessionId} (${q.length} left)`);
80801
+ this.dispatch(agentId, next, integrationId).catch((err) => this.log.error(`queued dispatch failed for agent "${agentId}": ${err.stack ?? err}`));
80802
+ }
80708
80803
  /** Local layer (agent.json) ∪ resolved CP layer; unservable CP rules are dropped + warn-logged. */
80709
80804
  mergedRules() {
80710
80805
  const local = [...this.agents.values()].flatMap((a) => rulesFromAgent(a, this.botUserIds));
@@ -80747,6 +80842,7 @@ var Daemon = class {
80747
80842
  } finally {
80748
80843
  this.pending.delete(sessionId);
80749
80844
  }
80845
+ this.flushQueued(agentId, sessionId, integrationId);
80750
80846
  }
80751
80847
  /** Route a converger action: set-status → setStatus (loading_messages only when not clearing); else postMessage. */
80752
80848
  async applyAction(action, conn, channel, thread) {