nextclaw-core 0.2.8 → 0.4.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 (3) hide show
  1. package/dist/index.d.ts +603 -109
  2. package/dist/index.js +1394 -125
  3. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -93,9 +93,11 @@ var MemoryStore = class {
93
93
  this.workspace = workspace;
94
94
  this.memoryDir = ensureDir(join(workspace, "memory"));
95
95
  this.memoryFile = join(this.memoryDir, "MEMORY.md");
96
+ this.workspaceMemoryFile = join(workspace, "MEMORY.md");
96
97
  }
97
98
  memoryDir;
98
99
  memoryFile;
100
+ workspaceMemoryFile;
99
101
  getTodayFile() {
100
102
  return join(this.memoryDir, `${todayDate()}.md`);
101
103
  }
@@ -127,6 +129,12 @@ ${content}`;
127
129
  }
128
130
  return "";
129
131
  }
132
+ readWorkspaceMemory() {
133
+ if (existsSync2(this.workspaceMemoryFile)) {
134
+ return readFileSync(this.workspaceMemoryFile, "utf-8");
135
+ }
136
+ return "";
137
+ }
130
138
  writeLongTerm(content) {
131
139
  writeFileSync(this.memoryFile, content, "utf-8");
132
140
  }
@@ -152,6 +160,11 @@ ${content}`;
152
160
  }
153
161
  getMemoryContext() {
154
162
  const parts = [];
163
+ const workspaceMemory = this.readWorkspaceMemory();
164
+ if (workspaceMemory) {
165
+ parts.push(`## Workspace Memory
166
+ ${workspaceMemory}`);
167
+ }
155
168
  const longTerm = this.readLongTerm();
156
169
  if (longTerm) {
157
170
  parts.push(`## Long-term Memory
@@ -372,27 +385,50 @@ ${this.stripFrontmatter(content)}`);
372
385
  };
373
386
 
374
387
  // src/agent/context.ts
375
- var BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md", "IDENTITY.md"];
388
+ var DEFAULT_CONTEXT_CONFIG = {
389
+ bootstrap: {
390
+ files: ["AGENTS.md", "SOUL.md", "USER.md", "IDENTITY.md", "TOOLS.md", "BOOT.md", "BOOTSTRAP.md", "HEARTBEAT.md"],
391
+ minimalFiles: ["AGENTS.md", "SOUL.md", "TOOLS.md", "IDENTITY.md"],
392
+ heartbeatFiles: ["HEARTBEAT.md"],
393
+ perFileChars: 4e3,
394
+ totalChars: 12e3
395
+ },
396
+ memory: {
397
+ enabled: true,
398
+ maxChars: 8e3
399
+ }
400
+ };
376
401
  var ContextBuilder = class {
377
- constructor(workspace) {
402
+ constructor(workspace, contextConfig) {
378
403
  this.workspace = workspace;
379
404
  this.memory = new MemoryStore(workspace);
380
405
  this.skills = new SkillsLoader(workspace, join3(fileURLToPath2(new URL("..", import.meta.url)), "skills"));
406
+ this.contextConfig = {
407
+ bootstrap: {
408
+ ...DEFAULT_CONTEXT_CONFIG.bootstrap,
409
+ ...contextConfig?.bootstrap ?? {}
410
+ },
411
+ memory: {
412
+ ...DEFAULT_CONTEXT_CONFIG.memory,
413
+ ...contextConfig?.memory ?? {}
414
+ }
415
+ };
381
416
  }
382
417
  memory;
383
418
  skills;
384
- buildSystemPrompt(skillNames) {
419
+ contextConfig;
420
+ buildSystemPrompt(skillNames, sessionKey) {
385
421
  const parts = [];
386
422
  parts.push(this.getIdentity());
387
- const bootstrap = this.loadBootstrapFiles();
423
+ const bootstrap = this.loadBootstrapFiles(sessionKey);
388
424
  if (bootstrap) {
389
- parts.push(bootstrap);
425
+ parts.push(`# Workspace Context
426
+
427
+ ${bootstrap}`);
390
428
  }
391
- const memory = this.memory.getMemoryContext();
429
+ const memory = this.buildMemorySection();
392
430
  if (memory) {
393
- parts.push(`# Memory
394
-
395
- ${memory}`);
431
+ parts.push(memory);
396
432
  }
397
433
  const alwaysSkills = this.skills.getAlwaysSkills();
398
434
  if (alwaysSkills.length) {
@@ -424,13 +460,17 @@ ${skillsSummary}`);
424
460
  }
425
461
  buildMessages(params) {
426
462
  const messages = [];
427
- let systemPrompt = this.buildSystemPrompt(params.skillNames);
463
+ let systemPrompt = this.buildSystemPrompt(params.skillNames, params.sessionKey);
428
464
  if (params.channel && params.chatId) {
429
465
  systemPrompt += `
430
466
 
431
467
  ## Current Session
432
468
  Channel: ${params.channel}
433
469
  Chat ID: ${params.chatId}`;
470
+ }
471
+ if (params.sessionKey) {
472
+ systemPrompt += `
473
+ Session: ${params.sessionKey}`;
434
474
  }
435
475
  messages.push({ role: "system", content: systemPrompt });
436
476
  messages.push(...params.history);
@@ -462,12 +502,78 @@ Chat ID: ${params.chatId}`;
462
502
  const now = (/* @__PURE__ */ new Date()).toLocaleString();
463
503
  return `# ${APP_NAME} \u{1F916}
464
504
 
465
- You are ${APP_NAME}, a helpful AI assistant. You have access to tools that allow you to:
466
- - Read, write, and edit files
467
- - Execute shell commands
468
- - Search the web and fetch web pages
469
- - Send messages to users on chat channels
470
- - Spawn subagents for complex background tasks
505
+ You are a personal assistant running inside ${APP_NAME}.
506
+
507
+ ## Instruction Priority
508
+ 1) System message (this prompt)
509
+ 2) AGENTS.md \u2014 operational rules
510
+ 3) SOUL.md \u2014 personality and tone
511
+ 4) IDENTITY.md \u2014 product identity
512
+ 5) USER.md \u2014 user preferences and context
513
+ 6) TOOLS.md \u2014 tool usage guidance
514
+ 7) BOOT.md / BOOTSTRAP.md \u2014 project context
515
+ 8) HEARTBEAT.md \u2014 recurring tasks
516
+
517
+ If instructions conflict, follow the highest priority source.
518
+
519
+ ## Tooling
520
+ Tool names are case-sensitive. Use only the tools listed here:
521
+ - read_file, write_file, edit_file, list_dir
522
+ - exec (shell commands)
523
+ - web_search, web_fetch
524
+ - message (action=send)
525
+ - sessions_list, sessions_history, sessions_send
526
+ - spawn (create subagent), subagents (list/steer/kill)
527
+ - memory_search, memory_get
528
+ - cron
529
+ - gateway
530
+
531
+ TOOLS.md does not change tool availability; it is guidance only.
532
+ Do not use exec/curl for provider messaging; use message/sessions_send instead.
533
+
534
+ ## Tool Call Style
535
+ - Default: do not narrate routine, low-risk tool calls.
536
+ - Narrate only when it helps (multi-step work, complex problems, sensitive actions, or if the user asks).
537
+ - Keep narration brief and value-dense.
538
+
539
+ ## Messaging
540
+ - Normal replies go to the current session automatically.
541
+ - Cross-session messaging: use sessions_send(sessionKey, message).
542
+ - Proactive channel send: use message with channel/chatId.
543
+ - Sub-agent orchestration: use subagents(action=list|steer|kill) and spawn.
544
+ - Do not poll subagents list / sessions_list in a loop; only check on-demand.
545
+ - If you use message (action=send) to deliver your user-visible reply, respond with ONLY: NO_REPLY (avoid duplicate replies).
546
+ - If a [System Message] reports completed cron/subagent work and asks for a user update, rewrite it in your normal assistant voice and send that update (do not forward raw system text or default to NO_REPLY).
547
+
548
+ ## Reply Tags
549
+ - [[reply_to_current]] replies to the triggering message.
550
+ - [[reply_to:<id>]] replies to a specific message id.
551
+ - Whitespace inside the tag is allowed (e.g. [[ reply_to_current ]] / [[ reply_to: 123 ]]).
552
+ - Tags are stripped before sending.
553
+
554
+ ## Memory Recall
555
+ Before answering anything about prior work, decisions, dates, people, preferences, or todos:
556
+ 1) Run memory_search on MEMORY.md + memory/*.md.
557
+ 2) Use memory_get to pull only the needed lines.
558
+ If low confidence after search, say you checked.
559
+
560
+ ## Silent Replies
561
+ When you have nothing to say, respond with ONLY: NO_REPLY
562
+ - Never append it to a real response.
563
+ - Do not wrap it in quotes or markdown.
564
+ - Correct: NO_REPLY
565
+ - Wrong: "NO_REPLY" or "Here you go... NO_REPLY"
566
+
567
+ ## ${APP_NAME} CLI Quick Reference
568
+ ${APP_NAME} is controlled via subcommands. Do not invent commands.
569
+ - ${APP_NAME.toLowerCase()} start | stop | status
570
+ - ${APP_NAME.toLowerCase()} gateway status | start | stop | restart
571
+ If unsure, ask the user to run \`${APP_NAME.toLowerCase()} help\` and paste the output.
572
+
573
+ ## ${APP_NAME} Self-Update
574
+ - Only run update.run when the user explicitly asks for an update.
575
+ - Actions: config.get, config.schema, config.apply (validate + write full config, then restart), config.patch (merge + restart), update.run (update deps or git, then restart).
576
+ - After restart, the last active session will be pinged automatically.
471
577
 
472
578
  ## Current Time
473
579
  ${now}
@@ -481,26 +587,83 @@ Your workspace is at: ${this.workspace}
481
587
  - Daily notes: ${this.workspace}/memory/YYYY-MM-DD.md
482
588
  - Custom skills: ${this.workspace}/skills/{skill-name}/SKILL.md
483
589
 
484
- IMPORTANT: When responding to direct questions or conversations, reply directly with your text response.
485
- Only use the 'message' tool when you need to send a message to a specific chat channel (like WhatsApp).
486
- For normal conversation, just respond with text - do not call the message tool.
487
-
488
- Always be helpful, accurate, and concise. When using tools, explain what you're doing.
489
- When remembering something, write to ${this.workspace}/memory/MEMORY.md`;
590
+ ## Behavior
591
+ - For normal conversation, respond with plain text; do not call the message tool.
592
+ - Use the message tool only when you need to send a reply to a specific chat channel.
593
+ - When using tools, briefly explain what you're doing.
594
+ - When remembering something, write to ${this.workspace}/memory/MEMORY.md`;
490
595
  }
491
- loadBootstrapFiles() {
596
+ loadBootstrapFiles(sessionKey) {
492
597
  const parts = [];
493
- for (const filename of BOOTSTRAP_FILES) {
598
+ const { perFileChars, totalChars } = this.contextConfig.bootstrap;
599
+ const fileList = this.selectBootstrapFiles(sessionKey);
600
+ const totalLimit = totalChars > 0 ? totalChars : Number.POSITIVE_INFINITY;
601
+ let remaining = totalLimit;
602
+ for (const filename of fileList) {
494
603
  const filePath = join3(this.workspace, filename);
495
604
  if (existsSync4(filePath)) {
496
- const content = readFileSync3(filePath, "utf-8");
605
+ const raw = readFileSync3(filePath, "utf-8").trim();
606
+ if (!raw) {
607
+ continue;
608
+ }
609
+ const perFileLimit = perFileChars > 0 ? perFileChars : raw.length;
610
+ const allowed = Math.min(perFileLimit, remaining);
611
+ if (allowed <= 0) {
612
+ break;
613
+ }
614
+ const content = this.truncateText(raw, allowed);
497
615
  parts.push(`## ${filename}
498
616
 
499
617
  ${content}`);
618
+ remaining -= content.length;
619
+ if (remaining <= 0) {
620
+ break;
621
+ }
500
622
  }
501
623
  }
502
624
  return parts.join("\n\n");
503
625
  }
626
+ selectBootstrapFiles(sessionKey) {
627
+ const { files, minimalFiles, heartbeatFiles } = this.contextConfig.bootstrap;
628
+ if (!sessionKey) {
629
+ return files;
630
+ }
631
+ if (sessionKey === "heartbeat") {
632
+ return dedupeStrings([...minimalFiles, ...heartbeatFiles]);
633
+ }
634
+ if (sessionKey.startsWith("cron:") || sessionKey.startsWith("subagent:")) {
635
+ return minimalFiles;
636
+ }
637
+ return files;
638
+ }
639
+ buildMemorySection() {
640
+ const memoryConfig = this.contextConfig.memory;
641
+ if (!memoryConfig.enabled) {
642
+ return "";
643
+ }
644
+ const memory = this.memory.getMemoryContext();
645
+ if (!memory) {
646
+ return "";
647
+ }
648
+ const truncated = this.truncateText(memory, memoryConfig.maxChars);
649
+ return `# Memory
650
+
651
+ ${truncated}`;
652
+ }
653
+ truncateText(text, limit) {
654
+ if (limit <= 0 || text.length <= limit) {
655
+ return text;
656
+ }
657
+ const omitted = text.length - limit;
658
+ const suffix = `
659
+
660
+ ...[truncated ${omitted} chars]`;
661
+ if (suffix.length >= limit) {
662
+ return text.slice(0, limit).trimEnd();
663
+ }
664
+ const head = text.slice(0, limit - suffix.length).trimEnd();
665
+ return `${head}${suffix}`;
666
+ }
504
667
  buildUserContent(text, media) {
505
668
  if (!media.length) {
506
669
  return text;
@@ -532,6 +695,18 @@ function guessImageMime(path) {
532
695
  if (ext === ".webp") return "image/webp";
533
696
  return null;
534
697
  }
698
+ function dedupeStrings(values) {
699
+ const seen = /* @__PURE__ */ new Set();
700
+ const result = [];
701
+ for (const value of values) {
702
+ if (seen.has(value)) {
703
+ continue;
704
+ }
705
+ seen.add(value);
706
+ result.push(value);
707
+ }
708
+ return result;
709
+ }
535
710
 
536
711
  // src/agent/tools/registry.ts
537
712
  var ToolRegistry = class {
@@ -898,7 +1073,7 @@ function truncateOutput(result, maxLen = 1e4) {
898
1073
  }
899
1074
 
900
1075
  // src/agent/tools/web.ts
901
- import { fetch } from "undici";
1076
+ import { fetch as fetch2 } from "undici";
902
1077
  var WebSearchTool = class extends Tool {
903
1078
  constructor(apiKey, maxResults = 5) {
904
1079
  super();
@@ -930,7 +1105,7 @@ var WebSearchTool = class extends Tool {
930
1105
  const url = new URL("https://api.search.brave.com/res/v1/web/search");
931
1106
  url.searchParams.set("q", query);
932
1107
  url.searchParams.set("count", String(maxResults));
933
- const response = await fetch(url.toString(), {
1108
+ const response = await fetch2(url.toString(), {
934
1109
  headers: {
935
1110
  "Accept": "application/json",
936
1111
  "X-Subscription-Token": this.apiKey
@@ -973,7 +1148,7 @@ var WebFetchTool = class extends Tool {
973
1148
  }
974
1149
  async execute(params) {
975
1150
  const url = String(params.url ?? "");
976
- const response = await fetch(url, { headers: { "User-Agent": APP_USER_AGENT } });
1151
+ const response = await fetch2(url, { headers: { "User-Agent": APP_USER_AGENT } });
977
1152
  if (!response.ok) {
978
1153
  return `Error: Fetch failed (${response.status})`;
979
1154
  }
@@ -1000,11 +1175,16 @@ var MessageTool = class extends Tool {
1000
1175
  return {
1001
1176
  type: "object",
1002
1177
  properties: {
1178
+ action: { type: "string", enum: ["send"], description: "Action to perform" },
1003
1179
  content: { type: "string", description: "Message to send" },
1180
+ message: { type: "string", description: "Alias for content" },
1004
1181
  channel: { type: "string", description: "Channel name" },
1005
- chatId: { type: "string", description: "Chat ID" }
1182
+ chatId: { type: "string", description: "Chat ID" },
1183
+ to: { type: "string", description: "Alias for chatId" },
1184
+ replyTo: { type: "string", description: "Message ID to reply to" },
1185
+ silent: { type: "boolean", description: "Send without notification where supported" }
1006
1186
  },
1007
- required: ["content"]
1187
+ required: []
1008
1188
  };
1009
1189
  }
1010
1190
  setContext(channel, chatId) {
@@ -1012,15 +1192,25 @@ var MessageTool = class extends Tool {
1012
1192
  this.chatId = chatId;
1013
1193
  }
1014
1194
  async execute(params) {
1015
- const content = String(params.content ?? "");
1195
+ const action = params.action ? String(params.action) : "send";
1196
+ if (action !== "send") {
1197
+ return `Error: Unsupported action '${action}'`;
1198
+ }
1199
+ const content = String(params.content ?? params.message ?? "");
1200
+ if (!content) {
1201
+ return "Error: content/message is required";
1202
+ }
1016
1203
  const channel = String(params.channel ?? this.channel);
1017
- const chatId = String(params.chatId ?? this.chatId);
1204
+ const chatId = String(params.chatId ?? params.to ?? this.chatId);
1205
+ const replyTo = params.replyTo ? String(params.replyTo) : void 0;
1206
+ const silent = typeof params.silent === "boolean" ? params.silent : void 0;
1018
1207
  await this.sendCallback({
1019
1208
  channel,
1020
1209
  chatId,
1021
1210
  content,
1211
+ replyTo,
1022
1212
  media: [],
1023
- metadata: {}
1213
+ metadata: silent !== void 0 ? { silent } : {}
1024
1214
  });
1025
1215
  return `Message sent to ${channel}:${chatId}`;
1026
1216
  }
@@ -1129,6 +1319,707 @@ var CronTool = class extends Tool {
1129
1319
  }
1130
1320
  };
1131
1321
 
1322
+ // src/agent/tools/sessions.ts
1323
+ import crypto from "crypto";
1324
+ var DEFAULT_LIMIT = 20;
1325
+ var DEFAULT_MESSAGE_LIMIT = 0;
1326
+ var MAX_MESSAGE_LIMIT = 20;
1327
+ var HISTORY_MAX_BYTES = 80 * 1024;
1328
+ var HISTORY_TEXT_MAX_CHARS = 4e3;
1329
+ var toInt = (value, fallback) => {
1330
+ const parsed = Number(value);
1331
+ return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : fallback;
1332
+ };
1333
+ var parseSessionKey2 = (key) => {
1334
+ const trimmed = key.trim();
1335
+ if (!trimmed.includes(":")) {
1336
+ return null;
1337
+ }
1338
+ const [channel, chatId] = trimmed.split(":", 2);
1339
+ if (!channel || !chatId) {
1340
+ return null;
1341
+ }
1342
+ return { channel, chatId };
1343
+ };
1344
+ var classifySessionKind = (key) => {
1345
+ if (key.startsWith("cron:") || key === "heartbeat") {
1346
+ return "cron";
1347
+ }
1348
+ if (key.startsWith("hook:")) {
1349
+ return "hook";
1350
+ }
1351
+ if (key.startsWith("subagent:") || key.startsWith("node:")) {
1352
+ return "node";
1353
+ }
1354
+ if (key.startsWith("system:")) {
1355
+ return "other";
1356
+ }
1357
+ return "main";
1358
+ };
1359
+ var truncateHistoryText = (text) => {
1360
+ if (text.length <= HISTORY_TEXT_MAX_CHARS) {
1361
+ return { text, truncated: false };
1362
+ }
1363
+ return { text: `${text.slice(0, HISTORY_TEXT_MAX_CHARS)}
1364
+ \u2026(truncated)\u2026`, truncated: true };
1365
+ };
1366
+ var sanitizeHistoryMessage = (msg) => {
1367
+ const entry = { ...msg };
1368
+ const res = truncateHistoryText(entry.content ?? "");
1369
+ return { message: { ...entry, content: res.text }, truncated: res.truncated };
1370
+ };
1371
+ var jsonBytes = (value) => {
1372
+ try {
1373
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
1374
+ } catch {
1375
+ return Buffer.byteLength(String(value), "utf8");
1376
+ }
1377
+ };
1378
+ var enforceHistoryHardCap = (items) => {
1379
+ const bytes = jsonBytes(items);
1380
+ if (bytes <= HISTORY_MAX_BYTES) {
1381
+ return items;
1382
+ }
1383
+ const last = items.at(-1);
1384
+ if (last && jsonBytes([last]) <= HISTORY_MAX_BYTES) {
1385
+ return [last];
1386
+ }
1387
+ return [{ role: "assistant", content: "[sessions_history omitted: message too large]" }];
1388
+ };
1389
+ var toTimestamp = (value) => {
1390
+ if (typeof value === "number" && Number.isFinite(value)) {
1391
+ return value;
1392
+ }
1393
+ if (typeof value === "string") {
1394
+ const parsed = Date.parse(value);
1395
+ if (Number.isFinite(parsed)) {
1396
+ return parsed;
1397
+ }
1398
+ }
1399
+ return void 0;
1400
+ };
1401
+ var SessionsListTool = class extends Tool {
1402
+ constructor(sessions) {
1403
+ super();
1404
+ this.sessions = sessions;
1405
+ }
1406
+ get name() {
1407
+ return "sessions_list";
1408
+ }
1409
+ get description() {
1410
+ return "List available sessions with timestamps";
1411
+ }
1412
+ get parameters() {
1413
+ return {
1414
+ type: "object",
1415
+ properties: {
1416
+ kinds: {
1417
+ type: "array",
1418
+ items: { type: "string" },
1419
+ description: "Filter by session kinds (main/group/cron/hook/other)"
1420
+ },
1421
+ limit: { type: "integer", minimum: 1, description: "Maximum number of sessions to return" },
1422
+ activeMinutes: { type: "integer", minimum: 1, description: "Only include active sessions" },
1423
+ messageLimit: { type: "integer", minimum: 0, description: "Include last N messages (max 20)" }
1424
+ }
1425
+ };
1426
+ }
1427
+ async execute(params) {
1428
+ const limit = toInt(params.limit, DEFAULT_LIMIT);
1429
+ const rawKinds = Array.isArray(params.kinds) ? params.kinds.map((k) => String(k).toLowerCase()) : [];
1430
+ const kinds = rawKinds.length ? new Set(rawKinds) : null;
1431
+ const activeMinutes = toInt(params.activeMinutes, 0);
1432
+ const messageLimit = Math.min(toInt(params.messageLimit, DEFAULT_MESSAGE_LIMIT), MAX_MESSAGE_LIMIT);
1433
+ const now = Date.now();
1434
+ const sessions = this.sessions.listSessions().sort((a, b) => (toTimestamp(b.updated_at) ?? 0) - (toTimestamp(a.updated_at) ?? 0)).filter((entry) => {
1435
+ if (activeMinutes > 0 && entry.updated_at) {
1436
+ const updated = Date.parse(String(entry.updated_at));
1437
+ if (Number.isFinite(updated) && now - updated > activeMinutes * 60 * 1e3) {
1438
+ return false;
1439
+ }
1440
+ }
1441
+ if (kinds) {
1442
+ const kind = classifySessionKind(String(entry.key ?? ""));
1443
+ if (!kinds.has(kind)) {
1444
+ return false;
1445
+ }
1446
+ }
1447
+ return true;
1448
+ }).slice(0, limit).map((entry) => {
1449
+ const key = String(entry.key ?? "");
1450
+ const kind = classifySessionKind(key);
1451
+ const parsed = parseSessionKey2(key);
1452
+ const metadata = entry.metadata ?? {};
1453
+ const label = typeof metadata.label === "string" ? metadata.label : typeof metadata.session_label === "string" ? metadata.session_label : void 0;
1454
+ const displayName = typeof metadata.displayName === "string" ? metadata.displayName : typeof metadata.display_name === "string" ? metadata.display_name : void 0;
1455
+ const deliveryContext = metadata.deliveryContext && typeof metadata.deliveryContext === "object" ? metadata.deliveryContext : void 0;
1456
+ const updatedAt = toTimestamp(entry.updated_at);
1457
+ const createdAt = toTimestamp(entry.created_at);
1458
+ const base = {
1459
+ key,
1460
+ kind,
1461
+ channel: parsed?.channel,
1462
+ label,
1463
+ displayName,
1464
+ deliveryContext,
1465
+ updatedAt,
1466
+ createdAt,
1467
+ sessionId: key,
1468
+ lastChannel: typeof metadata.last_channel === "string" ? metadata.last_channel : parsed?.channel ?? void 0,
1469
+ lastTo: typeof metadata.last_to === "string" ? metadata.last_to : parsed?.chatId ?? void 0,
1470
+ lastAccountId: typeof metadata.last_account_id === "string" ? metadata.last_account_id : void 0,
1471
+ transcriptPath: entry.path
1472
+ };
1473
+ if (messageLimit > 0) {
1474
+ const session = this.sessions.getIfExists(key);
1475
+ if (session) {
1476
+ const filtered = session.messages.filter((msg) => msg.role !== "tool");
1477
+ const recent = filtered.slice(-messageLimit).map((msg) => ({
1478
+ role: msg.role,
1479
+ content: msg.content,
1480
+ timestamp: msg.timestamp
1481
+ }));
1482
+ const sanitized = recent.map((msg) => sanitizeHistoryMessage(msg).message);
1483
+ base.messages = sanitized;
1484
+ }
1485
+ }
1486
+ return base;
1487
+ });
1488
+ return JSON.stringify({ sessions }, null, 2);
1489
+ }
1490
+ };
1491
+ var SessionsHistoryTool = class extends Tool {
1492
+ constructor(sessions) {
1493
+ super();
1494
+ this.sessions = sessions;
1495
+ }
1496
+ get name() {
1497
+ return "sessions_history";
1498
+ }
1499
+ get description() {
1500
+ return "Fetch recent messages from a session";
1501
+ }
1502
+ get parameters() {
1503
+ return {
1504
+ type: "object",
1505
+ properties: {
1506
+ sessionKey: { type: "string", description: "Session key in the format channel:chatId" },
1507
+ limit: { type: "integer", minimum: 1, description: "Maximum number of messages to return" },
1508
+ includeTools: { type: "boolean", description: "Include tool messages" }
1509
+ },
1510
+ required: ["sessionKey"]
1511
+ };
1512
+ }
1513
+ async execute(params) {
1514
+ const sessionKey = String(params.sessionKey ?? "").trim();
1515
+ if (!sessionKey) {
1516
+ return "Error: sessionKey is required";
1517
+ }
1518
+ let session = this.sessions.getIfExists(sessionKey);
1519
+ if (!session) {
1520
+ const candidates = this.sessions.listSessions();
1521
+ const match = candidates.find((entry) => {
1522
+ const key = typeof entry.key === "string" ? entry.key : "";
1523
+ if (key === sessionKey || key.endsWith(`:${sessionKey}`)) {
1524
+ return true;
1525
+ }
1526
+ const meta = entry.metadata;
1527
+ const metaLabel = meta?.label ?? meta?.session_label;
1528
+ return typeof metaLabel === "string" && metaLabel.trim() === sessionKey;
1529
+ });
1530
+ const resolvedKey = match && typeof match.key === "string" ? match.key : "";
1531
+ if (resolvedKey) {
1532
+ session = this.sessions.getIfExists(resolvedKey);
1533
+ }
1534
+ }
1535
+ if (!session) {
1536
+ return `Error: session '${sessionKey}' not found`;
1537
+ }
1538
+ const limit = toInt(params.limit, DEFAULT_LIMIT);
1539
+ const includeTools = typeof params.includeTools === "boolean" ? params.includeTools : false;
1540
+ const filtered = includeTools ? session.messages : session.messages.filter((msg) => msg.role !== "tool");
1541
+ const recent = filtered.slice(-limit).map((msg) => ({
1542
+ role: msg.role,
1543
+ content: msg.content,
1544
+ timestamp: msg.timestamp
1545
+ }));
1546
+ const sanitized = recent.map((msg) => sanitizeHistoryMessage(msg).message);
1547
+ const capped = enforceHistoryHardCap(sanitized);
1548
+ return JSON.stringify({ sessionKey, messages: capped }, null, 2);
1549
+ }
1550
+ };
1551
+ var SessionsSendTool = class extends Tool {
1552
+ constructor(sessions, bus) {
1553
+ super();
1554
+ this.sessions = sessions;
1555
+ this.bus = bus;
1556
+ }
1557
+ get name() {
1558
+ return "sessions_send";
1559
+ }
1560
+ get description() {
1561
+ return "Send a message to another session (cross-channel delivery)";
1562
+ }
1563
+ get parameters() {
1564
+ return {
1565
+ type: "object",
1566
+ properties: {
1567
+ sessionKey: { type: "string", description: "Target session key in the format channel:chatId" },
1568
+ label: { type: "string", description: "Session label (if sessionKey not provided)" },
1569
+ agentId: { type: "string", description: "Optional agent id (unused in local runtime)" },
1570
+ message: { type: "string", description: "Message content to send" },
1571
+ timeoutSeconds: { type: "number", description: "Optional timeout in seconds" },
1572
+ content: { type: "string", description: "Alias for message" },
1573
+ replyTo: { type: "string", description: "Message ID to reply to" },
1574
+ silent: { type: "boolean", description: "Send without notification where supported" }
1575
+ },
1576
+ required: []
1577
+ };
1578
+ }
1579
+ async execute(params) {
1580
+ const runId = crypto.randomUUID();
1581
+ const sessionKeyParam = String(params.sessionKey ?? "").trim();
1582
+ const labelParam = String(params.label ?? "").trim();
1583
+ if (sessionKeyParam && labelParam) {
1584
+ return JSON.stringify(
1585
+ { runId, status: "error", error: "Provide either sessionKey or label (not both)" },
1586
+ null,
1587
+ 2
1588
+ );
1589
+ }
1590
+ let sessionKey = sessionKeyParam;
1591
+ const message = String(params.message ?? params.content ?? "");
1592
+ if (!message) {
1593
+ return JSON.stringify({ runId, status: "error", error: "message is required" }, null, 2);
1594
+ }
1595
+ if (!sessionKey) {
1596
+ const label = labelParam;
1597
+ if (!label) {
1598
+ return JSON.stringify({ runId, status: "error", error: "sessionKey or label is required" }, null, 2);
1599
+ }
1600
+ const candidates = this.sessions.listSessions();
1601
+ const match = candidates.find((entry) => {
1602
+ const key = typeof entry.key === "string" ? entry.key : "";
1603
+ if (key === label || key.endsWith(`:${label}`)) {
1604
+ return true;
1605
+ }
1606
+ const meta = entry.metadata;
1607
+ const metaLabel = meta?.label ?? meta?.session_label;
1608
+ return typeof metaLabel === "string" && metaLabel.trim() === label;
1609
+ });
1610
+ sessionKey = match && typeof match.key === "string" ? match.key : "";
1611
+ if (!sessionKey) {
1612
+ return JSON.stringify(
1613
+ { runId, status: "error", error: `no session found for label '${label}'` },
1614
+ null,
1615
+ 2
1616
+ );
1617
+ }
1618
+ }
1619
+ const parsed = parseSessionKey2(sessionKey);
1620
+ if (!parsed) {
1621
+ return JSON.stringify(
1622
+ { runId, status: "error", error: "sessionKey must be in the format channel:chatId" },
1623
+ null,
1624
+ 2
1625
+ );
1626
+ }
1627
+ const replyTo = params.replyTo ? String(params.replyTo) : void 0;
1628
+ const silent = typeof params.silent === "boolean" ? params.silent : void 0;
1629
+ const outbound = {
1630
+ channel: parsed.channel,
1631
+ chatId: parsed.chatId,
1632
+ content: message,
1633
+ replyTo,
1634
+ media: [],
1635
+ metadata: silent !== void 0 ? { silent } : {}
1636
+ };
1637
+ await this.bus.publishOutbound(outbound);
1638
+ const session = this.sessions.getOrCreate(sessionKey);
1639
+ this.sessions.addMessage(session, "assistant", message, { via: "sessions_send" });
1640
+ this.sessions.save(session);
1641
+ return JSON.stringify(
1642
+ { runId, status: "ok", sessionKey: `${parsed.channel}:${parsed.chatId}` },
1643
+ null,
1644
+ 2
1645
+ );
1646
+ }
1647
+ };
1648
+
1649
+ // src/agent/tools/memory.ts
1650
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
1651
+ import { join as join4, resolve as resolve4 } from "path";
1652
+ var DEFAULT_LIMIT2 = 20;
1653
+ var DEFAULT_CONTEXT_LINES = 0;
1654
+ var toInt2 = (value, fallback) => {
1655
+ const parsed = Number(value);
1656
+ return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : fallback;
1657
+ };
1658
+ var normalizeQuery = (value) => String(value ?? "").trim();
1659
+ var isWithin = (child, parent) => {
1660
+ const resolvedChild = resolve4(child);
1661
+ const resolvedParent = resolve4(parent);
1662
+ return resolvedChild === resolvedParent || resolvedChild.startsWith(`${resolvedParent}/`);
1663
+ };
1664
+ var getMemoryFiles = (workspace) => {
1665
+ const files = [];
1666
+ const workspaceMemory = join4(workspace, "MEMORY.md");
1667
+ if (existsSync6(workspaceMemory)) {
1668
+ files.push(workspaceMemory);
1669
+ }
1670
+ const memoryDir = join4(workspace, "memory");
1671
+ if (existsSync6(memoryDir)) {
1672
+ const entries = readdirSync4(memoryDir);
1673
+ for (const entry of entries) {
1674
+ if (!entry.endsWith(".md")) {
1675
+ continue;
1676
+ }
1677
+ files.push(join4(memoryDir, entry));
1678
+ }
1679
+ }
1680
+ return files;
1681
+ };
1682
+ var MemorySearchTool = class extends Tool {
1683
+ constructor(workspace) {
1684
+ super();
1685
+ this.workspace = workspace;
1686
+ }
1687
+ get name() {
1688
+ return "memory_search";
1689
+ }
1690
+ get description() {
1691
+ return "Mandatory recall step: search MEMORY.md + memory/*.md; returns snippets with path + lines.";
1692
+ }
1693
+ get parameters() {
1694
+ return {
1695
+ type: "object",
1696
+ properties: {
1697
+ query: { type: "string", description: "Search query" },
1698
+ maxResults: { type: "integer", minimum: 1, description: "Maximum number of matches to return" },
1699
+ minScore: { type: "number", description: "Minimum score (ignored for local search)" }
1700
+ },
1701
+ required: ["query"]
1702
+ };
1703
+ }
1704
+ async execute(params) {
1705
+ const query = normalizeQuery(params.query);
1706
+ if (!query) {
1707
+ return "Error: query is required";
1708
+ }
1709
+ const limit = toInt2(params.maxResults ?? params.limit, DEFAULT_LIMIT2);
1710
+ const contextLines = toInt2(params.contextLines, DEFAULT_CONTEXT_LINES);
1711
+ const lowerQuery = query.toLowerCase();
1712
+ const results = [];
1713
+ const files = getMemoryFiles(this.workspace);
1714
+ for (const filePath of files) {
1715
+ const content = readFileSync5(filePath, "utf-8");
1716
+ const lines = content.split("\n");
1717
+ for (let i = 0; i < lines.length; i += 1) {
1718
+ if (lines[i].toLowerCase().includes(lowerQuery)) {
1719
+ const start = Math.max(0, i - contextLines);
1720
+ const end = Math.min(lines.length, i + contextLines + 1);
1721
+ const snippet = lines.slice(start, end).join("\n");
1722
+ results.push({ path: filePath, line: i + 1, text: snippet, score: 1 });
1723
+ if (results.length >= limit) {
1724
+ return JSON.stringify(
1725
+ { results, provider: "local", model: "regex", fallback: false, citations: "off" },
1726
+ null,
1727
+ 2
1728
+ );
1729
+ }
1730
+ }
1731
+ }
1732
+ }
1733
+ return JSON.stringify(
1734
+ { results, provider: "local", model: "regex", fallback: false, citations: "off" },
1735
+ null,
1736
+ 2
1737
+ );
1738
+ }
1739
+ };
1740
+ var MemoryGetTool = class extends Tool {
1741
+ constructor(workspace) {
1742
+ super();
1743
+ this.workspace = workspace;
1744
+ }
1745
+ get name() {
1746
+ return "memory_get";
1747
+ }
1748
+ get description() {
1749
+ return "Safe snippet read from MEMORY.md or memory/*.md; use after memory_search.";
1750
+ }
1751
+ get parameters() {
1752
+ return {
1753
+ type: "object",
1754
+ properties: {
1755
+ path: { type: "string", description: "Path to memory file (relative or absolute)" },
1756
+ from: { type: "integer", minimum: 1, description: "Start line (1-based)" },
1757
+ lines: { type: "integer", minimum: 1, description: "Number of lines to read" }
1758
+ },
1759
+ required: ["path"]
1760
+ };
1761
+ }
1762
+ async execute(params) {
1763
+ const pathParam = String(params.path ?? "").trim();
1764
+ if (!pathParam) {
1765
+ return "Error: path is required";
1766
+ }
1767
+ const resolvedPath = resolve4(this.workspace, pathParam);
1768
+ const memoryDir = join4(this.workspace, "memory");
1769
+ const workspaceMemory = join4(this.workspace, "MEMORY.md");
1770
+ if (!isWithin(resolvedPath, this.workspace)) {
1771
+ return "Error: path must be within workspace";
1772
+ }
1773
+ if (!(resolvedPath === resolve4(workspaceMemory) || isWithin(resolvedPath, memoryDir))) {
1774
+ return "Error: path must be MEMORY.md or memory/*.md within workspace";
1775
+ }
1776
+ if (!existsSync6(resolvedPath)) {
1777
+ return `Error: file not found: ${resolvedPath}`;
1778
+ }
1779
+ const content = readFileSync5(resolvedPath, "utf-8");
1780
+ const lines = content.split("\n");
1781
+ const startLine = toInt2(params.from ?? params.startLine, 1);
1782
+ const requestedLines = toInt2(params.lines ?? params.endLine, Math.max(lines.length - startLine + 1, 1));
1783
+ const endLine = Math.min(lines.length, startLine + requestedLines - 1);
1784
+ const startIdx = Math.max(0, startLine - 1);
1785
+ const endIdx = Math.min(lines.length, endLine);
1786
+ const selected = lines.slice(startIdx, endIdx);
1787
+ const numbered = selected.map((line, idx) => `${startIdx + idx + 1}: ${line}`);
1788
+ return JSON.stringify(
1789
+ { path: pathParam, from: startLine, lines: endIdx - startIdx, text: numbered.join("\n") },
1790
+ null,
1791
+ 2
1792
+ );
1793
+ }
1794
+ };
1795
+
1796
+ // src/agent/tools/gateway.ts
1797
+ var GatewayTool = class extends Tool {
1798
+ constructor(controller) {
1799
+ super();
1800
+ this.controller = controller;
1801
+ }
1802
+ get name() {
1803
+ return "gateway";
1804
+ }
1805
+ get description() {
1806
+ return "Restart or update gateway config (config.get/schema/apply/patch) and trigger restart.";
1807
+ }
1808
+ get parameters() {
1809
+ return {
1810
+ type: "object",
1811
+ properties: {
1812
+ action: {
1813
+ type: "string",
1814
+ enum: [
1815
+ "restart",
1816
+ "config.get",
1817
+ "config.schema",
1818
+ "config.apply",
1819
+ "config.patch",
1820
+ "update.run"
1821
+ ],
1822
+ description: "Action to perform"
1823
+ },
1824
+ delayMs: { type: "number", description: "Restart delay (ms)" },
1825
+ reason: { type: "string", description: "Optional reason for the action" },
1826
+ gatewayUrl: { type: "string", description: "Optional gateway url (unused in local runtime)" },
1827
+ gatewayToken: { type: "string", description: "Optional gateway token (unused in local runtime)" },
1828
+ timeoutMs: { type: "number", description: "Optional timeout (ms)" },
1829
+ raw: { type: "string", description: "Raw config JSON string for apply/patch" },
1830
+ baseHash: { type: "string", description: "Config base hash (from config.get)" },
1831
+ sessionKey: { type: "string", description: "Session key for restart notification" },
1832
+ note: { type: "string", description: "Completion note for config apply/patch" },
1833
+ restartDelayMs: { type: "number", description: "Restart delay after apply/patch (ms)" }
1834
+ },
1835
+ required: ["action"]
1836
+ };
1837
+ }
1838
+ async execute(params) {
1839
+ const action = String(params.action ?? "");
1840
+ if (!this.controller) {
1841
+ return JSON.stringify({ ok: false, error: "gateway controller not available in this runtime" }, null, 2);
1842
+ }
1843
+ if (action === "config.get") {
1844
+ if (!this.controller.getConfig) {
1845
+ return JSON.stringify({ ok: false, error: "config.get not supported" }, null, 2);
1846
+ }
1847
+ const result = await this.controller.getConfig();
1848
+ return JSON.stringify({ ok: true, result }, null, 2);
1849
+ }
1850
+ if (action === "config.schema") {
1851
+ if (!this.controller.getConfigSchema) {
1852
+ return JSON.stringify({ ok: false, error: "config.schema not supported" }, null, 2);
1853
+ }
1854
+ const result = await this.controller.getConfigSchema();
1855
+ return JSON.stringify({ ok: true, result }, null, 2);
1856
+ }
1857
+ if (action === "config.apply" || action === "config.patch") {
1858
+ const raw = params.raw;
1859
+ if (typeof raw !== "string" || !raw.trim()) {
1860
+ return JSON.stringify({ ok: false, error: "raw config string is required" }, null, 2);
1861
+ }
1862
+ const note = typeof params.note === "string" ? params.note.trim() || void 0 : void 0;
1863
+ const restartDelayMs = typeof params.restartDelayMs === "number" && Number.isFinite(params.restartDelayMs) ? Math.floor(params.restartDelayMs) : void 0;
1864
+ let baseHash = typeof params.baseHash === "string" && params.baseHash.trim() ? params.baseHash.trim() : void 0;
1865
+ if (!baseHash && this.controller.getConfig) {
1866
+ const snapshot = await this.controller.getConfig();
1867
+ if (snapshot && typeof snapshot === "object") {
1868
+ const hashValue = snapshot.hash;
1869
+ if (typeof hashValue === "string" && hashValue.trim()) {
1870
+ baseHash = hashValue.trim();
1871
+ }
1872
+ }
1873
+ }
1874
+ const sessionKey = typeof params.sessionKey === "string" && params.sessionKey.trim() ? params.sessionKey.trim() : void 0;
1875
+ if (action === "config.apply") {
1876
+ if (!this.controller.applyConfig) {
1877
+ return JSON.stringify({ ok: false, error: "config.apply not supported" }, null, 2);
1878
+ }
1879
+ const result2 = await this.controller.applyConfig({
1880
+ raw,
1881
+ baseHash,
1882
+ note,
1883
+ restartDelayMs,
1884
+ sessionKey
1885
+ });
1886
+ return JSON.stringify({ ok: true, result: result2 }, null, 2);
1887
+ }
1888
+ if (!this.controller.patchConfig) {
1889
+ return JSON.stringify({ ok: false, error: "config.patch not supported" }, null, 2);
1890
+ }
1891
+ const result = await this.controller.patchConfig({
1892
+ raw,
1893
+ baseHash,
1894
+ note,
1895
+ restartDelayMs,
1896
+ sessionKey
1897
+ });
1898
+ return JSON.stringify({ ok: true, result }, null, 2);
1899
+ }
1900
+ if (action === "restart") {
1901
+ if (!this.controller.restart) {
1902
+ return JSON.stringify({ ok: false, error: "restart not supported" }, null, 2);
1903
+ }
1904
+ const delayMs = typeof params.delayMs === "number" && Number.isFinite(params.delayMs) ? Math.floor(params.delayMs) : void 0;
1905
+ const reason = typeof params.reason === "string" ? params.reason.trim() || void 0 : void 0;
1906
+ const result = await this.controller.restart({ delayMs, reason });
1907
+ return JSON.stringify({ ok: true, result: result ?? "Restart scheduled" }, null, 2);
1908
+ }
1909
+ if (action === "update.run") {
1910
+ if (!this.controller.updateRun) {
1911
+ return JSON.stringify({ ok: false, error: "update.run not supported in this runtime" }, null, 2);
1912
+ }
1913
+ const restartDelayMs = typeof params.restartDelayMs === "number" && Number.isFinite(params.restartDelayMs) ? Math.floor(params.restartDelayMs) : void 0;
1914
+ const timeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? Math.max(1, Math.floor(params.timeoutMs)) : void 0;
1915
+ const sessionKey = typeof params.sessionKey === "string" && params.sessionKey.trim() ? params.sessionKey.trim() : void 0;
1916
+ const note = typeof params.note === "string" ? params.note.trim() || void 0 : void 0;
1917
+ const result = await this.controller.updateRun({ note, restartDelayMs, timeoutMs, sessionKey });
1918
+ return JSON.stringify({ ok: true, result }, null, 2);
1919
+ }
1920
+ return JSON.stringify({ ok: false, error: `Unknown action: ${action}` }, null, 2);
1921
+ }
1922
+ };
1923
+
1924
+ // src/agent/tools/subagents.ts
1925
+ var SubagentsTool = class extends Tool {
1926
+ constructor(manager) {
1927
+ super();
1928
+ this.manager = manager;
1929
+ }
1930
+ get name() {
1931
+ return "subagents";
1932
+ }
1933
+ get description() {
1934
+ return "Manage running subagents (list/steer/kill)";
1935
+ }
1936
+ get parameters() {
1937
+ return {
1938
+ type: "object",
1939
+ properties: {
1940
+ action: {
1941
+ type: "string",
1942
+ enum: ["list", "kill", "steer"],
1943
+ description: "Action to perform"
1944
+ },
1945
+ target: { type: "string", description: "Subagent target (id/label/last/index)" },
1946
+ message: { type: "string", description: "Steer instruction for a running subagent" },
1947
+ recentMinutes: { type: "number", description: "Only list recent runs" },
1948
+ id: { type: "string", description: "Alias for target" },
1949
+ note: { type: "string", description: "Alias for message" }
1950
+ },
1951
+ required: ["action"]
1952
+ };
1953
+ }
1954
+ async execute(params) {
1955
+ const action = String(params.action ?? "");
1956
+ if (action === "list") {
1957
+ const recentMinutes = typeof params.recentMinutes === "number" && Number.isFinite(params.recentMinutes) ? Math.max(1, Math.floor(params.recentMinutes)) : null;
1958
+ const runs = this.manager.listRuns();
1959
+ const now = Date.now();
1960
+ const filtered = recentMinutes ? runs.filter((run) => {
1961
+ const startedAt = Date.parse(run.startedAt);
1962
+ const doneAt = run.doneAt ? Date.parse(run.doneAt) : NaN;
1963
+ const ts = Number.isFinite(doneAt) ? doneAt : startedAt;
1964
+ return Number.isFinite(ts) && now - ts <= recentMinutes * 60 * 1e3;
1965
+ }) : runs;
1966
+ return JSON.stringify({ runs: filtered }, null, 2);
1967
+ }
1968
+ if (action === "kill") {
1969
+ const target = String(params.target ?? params.id ?? "").trim();
1970
+ if (!target) {
1971
+ return "Error: target is required for kill";
1972
+ }
1973
+ const resolved = resolveTarget(target, this.manager.listRuns());
1974
+ if (!resolved) {
1975
+ return `Error: subagent not found (${target})`;
1976
+ }
1977
+ const ok = this.manager.cancelRun(resolved.id);
1978
+ return ok ? `Subagent ${resolved.id} killed` : `Subagent ${resolved.id} not found`;
1979
+ }
1980
+ if (action === "steer") {
1981
+ const target = String(params.target ?? params.id ?? "").trim();
1982
+ const note = String(params.message ?? params.note ?? "").trim();
1983
+ if (!target || !note) {
1984
+ return "Error: target and message are required for steer";
1985
+ }
1986
+ const resolved = resolveTarget(target, this.manager.listRuns());
1987
+ if (!resolved) {
1988
+ return `Error: subagent not found (${target})`;
1989
+ }
1990
+ const ok = this.manager.steerRun(resolved.id, note);
1991
+ return ok ? `Subagent ${resolved.id} steer applied` : `Subagent ${resolved.id} not running`;
1992
+ }
1993
+ return "Error: invalid action";
1994
+ }
1995
+ };
1996
+ function resolveTarget(token, runs) {
1997
+ const trimmed = token.trim();
1998
+ if (!trimmed) {
1999
+ return null;
2000
+ }
2001
+ const sorted = [...runs].sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt));
2002
+ if (trimmed === "last") {
2003
+ return sorted[0] ?? null;
2004
+ }
2005
+ if (/^\d+$/.test(trimmed)) {
2006
+ const idx = Number.parseInt(trimmed, 10);
2007
+ if (Number.isFinite(idx) && idx > 0 && idx <= sorted.length) {
2008
+ return sorted[idx - 1];
2009
+ }
2010
+ }
2011
+ const byId = runs.find((run) => run.id === trimmed);
2012
+ if (byId) {
2013
+ return byId;
2014
+ }
2015
+ const lower = trimmed.toLowerCase();
2016
+ const byLabel = runs.filter((run) => run.label.toLowerCase() === lower);
2017
+ if (byLabel.length === 1) {
2018
+ return byLabel[0];
2019
+ }
2020
+ return null;
2021
+ }
2022
+
1132
2023
  // src/agent/subagent.ts
1133
2024
  import { randomUUID } from "crypto";
1134
2025
  var SubagentManager = class {
@@ -1136,6 +2027,8 @@ var SubagentManager = class {
1136
2027
  this.options = options;
1137
2028
  }
1138
2029
  runningTasks = /* @__PURE__ */ new Map();
2030
+ runs = /* @__PURE__ */ new Map();
2031
+ steerQueue = /* @__PURE__ */ new Map();
1139
2032
  async spawn(params) {
1140
2033
  const taskId = randomUUID().slice(0, 8);
1141
2034
  const displayLabel = params.label ?? `${params.task.slice(0, 30)}${params.task.length > 30 ? "..." : ""}`;
@@ -1143,6 +2036,16 @@ var SubagentManager = class {
1143
2036
  channel: params.originChannel ?? "cli",
1144
2037
  chatId: params.originChatId ?? "direct"
1145
2038
  };
2039
+ this.runs.set(taskId, {
2040
+ id: taskId,
2041
+ label: displayLabel,
2042
+ task: params.task,
2043
+ origin,
2044
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2045
+ status: "running",
2046
+ cancelled: false
2047
+ });
2048
+ this.steerQueue.set(taskId, []);
1146
2049
  const background = this.runSubagent({
1147
2050
  taskId,
1148
2051
  task: params.task,
@@ -1150,11 +2053,23 @@ var SubagentManager = class {
1150
2053
  origin
1151
2054
  });
1152
2055
  this.runningTasks.set(taskId, background);
1153
- background.finally(() => this.runningTasks.delete(taskId));
2056
+ background.finally(() => {
2057
+ this.runningTasks.delete(taskId);
2058
+ const run = this.runs.get(taskId);
2059
+ if (run && run.status === "running") {
2060
+ run.status = run.cancelled ? "cancelled" : "done";
2061
+ run.doneAt = (/* @__PURE__ */ new Date()).toISOString();
2062
+ }
2063
+ this.steerQueue.delete(taskId);
2064
+ });
1154
2065
  return `Subagent [${displayLabel}] started (id: ${taskId}). I'll notify you when it completes.`;
1155
2066
  }
1156
2067
  async runSubagent(params) {
1157
2068
  try {
2069
+ const run = this.runs.get(params.taskId);
2070
+ if (run?.cancelled) {
2071
+ return;
2072
+ }
1158
2073
  const tools = new ToolRegistry();
1159
2074
  const allowedDir = this.options.restrictToWorkspace ? this.options.workspace : void 0;
1160
2075
  tools.register(new ReadFileTool(allowedDir));
@@ -1178,7 +2093,13 @@ var SubagentManager = class {
1178
2093
  let finalResult = null;
1179
2094
  while (iteration < 15) {
1180
2095
  iteration += 1;
1181
- const response = await this.options.provider.chat({
2096
+ const queued = this.steerQueue.get(params.taskId);
2097
+ if (queued && queued.length) {
2098
+ for (const note of queued.splice(0, queued.length)) {
2099
+ messages.push({ role: "user", content: `Steer: ${note}` });
2100
+ }
2101
+ }
2102
+ const response = await this.options.providerManager.get().chat({
1182
2103
  messages,
1183
2104
  tools: tools.getDefinitions(),
1184
2105
  model: this.options.model
@@ -1201,25 +2122,38 @@ var SubagentManager = class {
1201
2122
  finalResult = response.content ?? "";
1202
2123
  break;
1203
2124
  }
2125
+ if (this.runs.get(params.taskId)?.cancelled) {
2126
+ return;
2127
+ }
1204
2128
  }
1205
2129
  if (!finalResult) {
1206
2130
  finalResult = "Task completed but no final response was generated.";
1207
2131
  }
1208
- await this.announceResult({
1209
- label: params.label,
1210
- task: params.task,
1211
- result: finalResult,
1212
- origin: params.origin,
1213
- status: "ok"
1214
- });
2132
+ const runAfter = this.runs.get(params.taskId);
2133
+ if (runAfter && !runAfter.cancelled) {
2134
+ runAfter.status = "done";
2135
+ runAfter.doneAt = (/* @__PURE__ */ new Date()).toISOString();
2136
+ await this.announceResult({
2137
+ label: params.label,
2138
+ task: params.task,
2139
+ result: finalResult,
2140
+ origin: params.origin,
2141
+ status: "ok"
2142
+ });
2143
+ }
1215
2144
  } catch (err) {
1216
- await this.announceResult({
1217
- label: params.label,
1218
- task: params.task,
1219
- result: `Error: ${String(err)}`,
1220
- origin: params.origin,
1221
- status: "error"
1222
- });
2145
+ const runAfter = this.runs.get(params.taskId);
2146
+ if (runAfter && !runAfter.cancelled) {
2147
+ runAfter.status = "error";
2148
+ runAfter.doneAt = (/* @__PURE__ */ new Date()).toISOString();
2149
+ await this.announceResult({
2150
+ label: params.label,
2151
+ task: params.task,
2152
+ result: `Error: ${String(err)}`,
2153
+ origin: params.origin,
2154
+ status: "error"
2155
+ });
2156
+ }
1223
2157
  }
1224
2158
  }
1225
2159
  async announceResult(params) {
@@ -1276,11 +2210,43 @@ When you have completed the task, provide a clear summary of your findings or ac
1276
2210
  getRunningCount() {
1277
2211
  return this.runningTasks.size;
1278
2212
  }
2213
+ listRuns() {
2214
+ return Array.from(this.runs.values()).map((run) => ({
2215
+ id: run.id,
2216
+ label: run.label,
2217
+ status: run.status,
2218
+ startedAt: run.startedAt,
2219
+ doneAt: run.doneAt
2220
+ }));
2221
+ }
2222
+ steerRun(id, note) {
2223
+ const run = this.runs.get(id);
2224
+ if (!run || run.cancelled || run.status !== "running") {
2225
+ return false;
2226
+ }
2227
+ const queue = this.steerQueue.get(id);
2228
+ if (!queue) {
2229
+ return false;
2230
+ }
2231
+ queue.push(note);
2232
+ return true;
2233
+ }
2234
+ cancelRun(id) {
2235
+ const run = this.runs.get(id);
2236
+ if (!run) {
2237
+ return false;
2238
+ }
2239
+ run.cancelled = true;
2240
+ run.status = "cancelled";
2241
+ run.doneAt = (/* @__PURE__ */ new Date()).toISOString();
2242
+ this.steerQueue.delete(id);
2243
+ return true;
2244
+ }
1279
2245
  };
1280
2246
 
1281
2247
  // src/session/manager.ts
1282
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync6, readdirSync as readdirSync4, unlinkSync } from "fs";
1283
- import { join as join4 } from "path";
2248
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync7, readdirSync as readdirSync5, unlinkSync } from "fs";
2249
+ import { join as join5 } from "path";
1284
2250
  var SessionManager = class {
1285
2251
  constructor(workspace) {
1286
2252
  this.workspace = workspace;
@@ -1290,7 +2256,7 @@ var SessionManager = class {
1290
2256
  cache = /* @__PURE__ */ new Map();
1291
2257
  getSessionPath(key) {
1292
2258
  const safeKey = safeFilename(key.replace(/:/g, "_"));
1293
- return join4(this.sessionsDir, `${safeKey}.jsonl`);
2259
+ return join5(this.sessionsDir, `${safeKey}.jsonl`);
1294
2260
  }
1295
2261
  getOrCreate(key) {
1296
2262
  const cached = this.cache.get(key);
@@ -1308,6 +2274,17 @@ var SessionManager = class {
1308
2274
  this.cache.set(key, session);
1309
2275
  return session;
1310
2276
  }
2277
+ getIfExists(key) {
2278
+ const cached = this.cache.get(key);
2279
+ if (cached) {
2280
+ return cached;
2281
+ }
2282
+ const loaded = this.load(key);
2283
+ if (loaded) {
2284
+ this.cache.set(key, loaded);
2285
+ }
2286
+ return loaded;
2287
+ }
1311
2288
  addMessage(session, role, content, extra = {}) {
1312
2289
  const msg = {
1313
2290
  role,
@@ -1328,11 +2305,11 @@ var SessionManager = class {
1328
2305
  }
1329
2306
  load(key) {
1330
2307
  const path = this.getSessionPath(key);
1331
- if (!existsSync6(path)) {
2308
+ if (!existsSync7(path)) {
1332
2309
  return null;
1333
2310
  }
1334
2311
  try {
1335
- const lines = readFileSync5(path, "utf-8").split("\n").filter(Boolean);
2312
+ const lines = readFileSync6(path, "utf-8").split("\n").filter(Boolean);
1336
2313
  const messages = [];
1337
2314
  let metadata = {};
1338
2315
  let createdAt = /* @__PURE__ */ new Date();
@@ -1378,7 +2355,7 @@ var SessionManager = class {
1378
2355
  delete(key) {
1379
2356
  this.cache.delete(key);
1380
2357
  const path = this.getSessionPath(key);
1381
- if (existsSync6(path)) {
2358
+ if (existsSync7(path)) {
1382
2359
  unlinkSync(path);
1383
2360
  return true;
1384
2361
  }
@@ -1386,12 +2363,12 @@ var SessionManager = class {
1386
2363
  }
1387
2364
  listSessions() {
1388
2365
  const sessions = [];
1389
- for (const entry of readdirSync4(this.sessionsDir, { withFileTypes: true })) {
2366
+ for (const entry of readdirSync5(this.sessionsDir, { withFileTypes: true })) {
1390
2367
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
1391
2368
  continue;
1392
2369
  }
1393
- const path = join4(this.sessionsDir, entry.name);
1394
- const firstLine = readFileSync5(path, "utf-8").split("\n")[0];
2370
+ const path = join5(this.sessionsDir, entry.name);
2371
+ const firstLine = readFileSync6(path, "utf-8").split("\n")[0];
1395
2372
  if (!firstLine) {
1396
2373
  continue;
1397
2374
  }
@@ -1402,7 +2379,8 @@ var SessionManager = class {
1402
2379
  key: entry.name.replace(/\.jsonl$/, "").replace(/_/g, ":"),
1403
2380
  created_at: data.created_at,
1404
2381
  updated_at: data.updated_at,
1405
- path
2382
+ path,
2383
+ metadata: data.metadata ?? {}
1406
2384
  });
1407
2385
  }
1408
2386
  } catch {
@@ -1413,18 +2391,34 @@ var SessionManager = class {
1413
2391
  }
1414
2392
  };
1415
2393
 
2394
+ // src/agent/tokens.ts
2395
+ var SILENT_REPLY_TOKEN = "NO_REPLY";
2396
+ var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2397
+ function isSilentReplyText(text, token = SILENT_REPLY_TOKEN) {
2398
+ if (!text) {
2399
+ return false;
2400
+ }
2401
+ const escaped = escapeRegExp(token);
2402
+ const prefix = new RegExp(`^\\s*${escaped}(?=$|\\W)`);
2403
+ if (prefix.test(text)) {
2404
+ return true;
2405
+ }
2406
+ const suffix = new RegExp(`\\b${escaped}\\b\\W*$`);
2407
+ return suffix.test(text);
2408
+ }
2409
+
1416
2410
  // src/agent/loop.ts
1417
2411
  var AgentLoop = class {
1418
2412
  constructor(options) {
1419
2413
  this.options = options;
1420
- this.context = new ContextBuilder(options.workspace);
2414
+ this.context = new ContextBuilder(options.workspace, options.contextConfig);
1421
2415
  this.sessions = options.sessionManager ?? new SessionManager(options.workspace);
1422
2416
  this.tools = new ToolRegistry();
1423
2417
  this.subagents = new SubagentManager({
1424
- provider: options.provider,
2418
+ providerManager: options.providerManager,
1425
2419
  workspace: options.workspace,
1426
2420
  bus: options.bus,
1427
- model: options.model ?? options.provider.getDefaultModel(),
2421
+ model: options.model ?? options.providerManager.get().getDefaultModel(),
1428
2422
  braveApiKey: options.braveApiKey ?? void 0,
1429
2423
  execConfig: options.execConfig ?? { timeout: 60 },
1430
2424
  restrictToWorkspace: options.restrictToWorkspace ?? false
@@ -1455,6 +2449,13 @@ var AgentLoop = class {
1455
2449
  this.tools.register(messageTool);
1456
2450
  const spawnTool = new SpawnTool(this.subagents);
1457
2451
  this.tools.register(spawnTool);
2452
+ this.tools.register(new SessionsListTool(this.sessions));
2453
+ this.tools.register(new SessionsHistoryTool(this.sessions));
2454
+ this.tools.register(new SessionsSendTool(this.sessions, this.options.bus));
2455
+ this.tools.register(new MemorySearchTool(this.options.workspace));
2456
+ this.tools.register(new MemoryGetTool(this.options.workspace));
2457
+ this.tools.register(new SubagentsTool(this.subagents));
2458
+ this.tools.register(new GatewayTool(this.options.gatewayController));
1458
2459
  if (this.options.cronService) {
1459
2460
  const cronTool = new CronTool(this.options.cronService);
1460
2461
  this.tools.register(cronTool);
@@ -1502,6 +2503,20 @@ var AgentLoop = class {
1502
2503
  }
1503
2504
  const sessionKey = sessionKeyOverride ?? `${msg.channel}:${msg.chatId}`;
1504
2505
  const session = this.sessions.getOrCreate(sessionKey);
2506
+ const messageId = msg.metadata?.message_id;
2507
+ if (messageId) {
2508
+ session.metadata.last_message_id = messageId;
2509
+ }
2510
+ const sessionLabel = msg.metadata?.session_label;
2511
+ if (sessionLabel) {
2512
+ session.metadata.label = sessionLabel;
2513
+ }
2514
+ session.metadata.last_channel = msg.channel;
2515
+ session.metadata.last_to = msg.chatId;
2516
+ const accountId = msg.metadata?.account_id ?? msg.metadata?.accountId;
2517
+ if (accountId) {
2518
+ session.metadata.last_account_id = accountId;
2519
+ }
1505
2520
  const messageTool = this.tools.get("message");
1506
2521
  if (messageTool instanceof MessageTool) {
1507
2522
  messageTool.setContext(msg.channel, msg.chatId);
@@ -1519,14 +2534,15 @@ var AgentLoop = class {
1519
2534
  currentMessage: msg.content,
1520
2535
  media: msg.media,
1521
2536
  channel: msg.channel,
1522
- chatId: msg.chatId
2537
+ chatId: msg.chatId,
2538
+ sessionKey
1523
2539
  });
1524
2540
  let iteration = 0;
1525
2541
  let finalContent = null;
1526
2542
  const maxIterations = this.options.maxIterations ?? 20;
1527
2543
  while (iteration < maxIterations) {
1528
2544
  iteration += 1;
1529
- const response = await this.options.provider.chat({
2545
+ const response = await this.options.providerManager.get().chat({
1530
2546
  messages,
1531
2547
  tools: this.tools.getDefinitions(),
1532
2548
  model: this.options.model ?? void 0
@@ -1553,6 +2569,13 @@ var AgentLoop = class {
1553
2569
  if (!finalContent) {
1554
2570
  finalContent = "I've completed processing but have no response to give.";
1555
2571
  }
2572
+ const { content: cleanedContent, replyTo } = parseReplyTags(finalContent, messageId);
2573
+ finalContent = cleanedContent;
2574
+ if (isSilentReplyText(finalContent, SILENT_REPLY_TOKEN)) {
2575
+ this.sessions.addMessage(session, "user", msg.content);
2576
+ this.sessions.save(session);
2577
+ return null;
2578
+ }
1556
2579
  this.sessions.addMessage(session, "user", msg.content);
1557
2580
  this.sessions.addMessage(session, "assistant", finalContent);
1558
2581
  this.sessions.save(session);
@@ -1560,6 +2583,7 @@ var AgentLoop = class {
1560
2583
  channel: msg.channel,
1561
2584
  chatId: msg.chatId,
1562
2585
  content: finalContent,
2586
+ replyTo,
1563
2587
  media: [],
1564
2588
  metadata: msg.metadata ?? {}
1565
2589
  };
@@ -1584,14 +2608,15 @@ var AgentLoop = class {
1584
2608
  history: this.sessions.getHistory(session),
1585
2609
  currentMessage: msg.content,
1586
2610
  channel: originChannel,
1587
- chatId: originChatId
2611
+ chatId: originChatId,
2612
+ sessionKey
1588
2613
  });
1589
2614
  let iteration = 0;
1590
2615
  let finalContent = null;
1591
2616
  const maxIterations = this.options.maxIterations ?? 20;
1592
2617
  while (iteration < maxIterations) {
1593
2618
  iteration += 1;
1594
- const response = await this.options.provider.chat({
2619
+ const response = await this.options.providerManager.get().chat({
1595
2620
  messages,
1596
2621
  tools: this.tools.getDefinitions(),
1597
2622
  model: this.options.model ?? void 0
@@ -1618,6 +2643,11 @@ var AgentLoop = class {
1618
2643
  if (!finalContent) {
1619
2644
  finalContent = "Background task completed.";
1620
2645
  }
2646
+ const { content: cleanedContent, replyTo } = parseReplyTags(finalContent, void 0);
2647
+ finalContent = cleanedContent;
2648
+ if (isSilentReplyText(finalContent, SILENT_REPLY_TOKEN)) {
2649
+ return null;
2650
+ }
1621
2651
  this.sessions.addMessage(session, "user", `[System: ${msg.senderId}] ${msg.content}`);
1622
2652
  this.sessions.addMessage(session, "assistant", finalContent);
1623
2653
  this.sessions.save(session);
@@ -1625,11 +2655,27 @@ var AgentLoop = class {
1625
2655
  channel: originChannel,
1626
2656
  chatId: originChatId,
1627
2657
  content: finalContent,
2658
+ replyTo,
1628
2659
  media: [],
1629
2660
  metadata: {}
1630
2661
  };
1631
2662
  }
1632
2663
  };
2664
+ function parseReplyTags(content, currentMessageId) {
2665
+ let replyTo;
2666
+ const replyCurrent = /\[\[\s*reply_to_current\s*\]\]/gi;
2667
+ if (replyCurrent.test(content)) {
2668
+ replyTo = currentMessageId;
2669
+ content = content.replace(replyCurrent, "").trim();
2670
+ }
2671
+ const replyId = /\[\[\s*reply_to\s*:\s*([^\]]+?)\s*\]\]/i;
2672
+ const match = content.match(replyId);
2673
+ if (match && match[1]) {
2674
+ replyTo = match[1].trim();
2675
+ content = content.replace(replyId, "").trim();
2676
+ }
2677
+ return { content, replyTo };
2678
+ }
1633
2679
 
1634
2680
  // src/bus/queue.ts
1635
2681
  var AsyncQueue = class {
@@ -1647,8 +2693,8 @@ var AsyncQueue = class {
1647
2693
  if (this.items.length > 0) {
1648
2694
  return this.items.shift();
1649
2695
  }
1650
- return new Promise((resolve5) => {
1651
- this.waiters.push(resolve5);
2696
+ return new Promise((resolve6) => {
2697
+ this.waiters.push(resolve6);
1652
2698
  });
1653
2699
  }
1654
2700
  size() {
@@ -1798,9 +2844,9 @@ var BaseChannel = class {
1798
2844
  };
1799
2845
 
1800
2846
  // src/providers/transcription.ts
1801
- import { createReadStream, existsSync as existsSync7 } from "fs";
2847
+ import { createReadStream, existsSync as existsSync8 } from "fs";
1802
2848
  import { basename } from "path";
1803
- import { FormData, fetch as fetch2 } from "undici";
2849
+ import { FormData, fetch as fetch3 } from "undici";
1804
2850
  var GroqTranscriptionProvider = class {
1805
2851
  apiKey;
1806
2852
  apiUrl = "https://api.groq.com/openai/v1/audio/transcriptions";
@@ -1811,13 +2857,13 @@ var GroqTranscriptionProvider = class {
1811
2857
  if (!this.apiKey) {
1812
2858
  return "";
1813
2859
  }
1814
- if (!existsSync7(filePath)) {
2860
+ if (!existsSync8(filePath)) {
1815
2861
  return "";
1816
2862
  }
1817
2863
  const form = new FormData();
1818
2864
  form.append("file", createReadStream(filePath), basename(filePath));
1819
2865
  form.append("model", "whisper-large-v3");
1820
- const response = await fetch2(this.apiUrl, {
2866
+ const response = await fetch3(this.apiUrl, {
1821
2867
  method: "POST",
1822
2868
  headers: {
1823
2869
  Authorization: `Bearer ${this.apiKey}`
@@ -1833,7 +2879,7 @@ var GroqTranscriptionProvider = class {
1833
2879
  };
1834
2880
 
1835
2881
  // src/channels/telegram.ts
1836
- import { join as join5 } from "path";
2882
+ import { join as join6 } from "path";
1837
2883
  import { mkdirSync as mkdirSync2 } from "fs";
1838
2884
  var BOT_COMMANDS = [
1839
2885
  { command: "start", description: "Start the bot" },
@@ -1920,10 +2966,20 @@ Just send me a text message to chat!`;
1920
2966
  }
1921
2967
  this.stopTyping(msg.chatId);
1922
2968
  const htmlContent = markdownToTelegramHtml(msg.content ?? "");
2969
+ const silent = msg.metadata?.silent === true;
2970
+ const replyTo = msg.replyTo ? Number(msg.replyTo) : void 0;
2971
+ const options = {
2972
+ parse_mode: "HTML",
2973
+ ...replyTo ? { reply_to_message_id: replyTo } : {},
2974
+ ...silent ? { disable_notification: true } : {}
2975
+ };
1923
2976
  try {
1924
- await this.bot.sendMessage(Number(msg.chatId), htmlContent, { parse_mode: "HTML" });
2977
+ await this.bot.sendMessage(Number(msg.chatId), htmlContent, options);
1925
2978
  } catch {
1926
- await this.bot.sendMessage(Number(msg.chatId), msg.content ?? "");
2979
+ await this.bot.sendMessage(Number(msg.chatId), msg.content ?? "", {
2980
+ ...replyTo ? { reply_to_message_id: replyTo } : {},
2981
+ ...silent ? { disable_notification: true } : {}
2982
+ });
1927
2983
  }
1928
2984
  }
1929
2985
  async handleIncoming(message) {
@@ -1945,7 +3001,7 @@ Just send me a text message to chat!`;
1945
3001
  }
1946
3002
  const { fileId, mediaType, mimeType } = resolveMedia(message);
1947
3003
  if (fileId && mediaType) {
1948
- const mediaDir = join5(getDataPath(), "media");
3004
+ const mediaDir = join6(getDataPath(), "media");
1949
3005
  mkdirSync2(mediaDir, { recursive: true });
1950
3006
  const extension = getExtension(mediaType, mimeType);
1951
3007
  const downloaded = await this.bot.downloadFile(fileId, mediaDir);
@@ -2077,7 +3133,7 @@ var WhatsAppChannel = class extends BaseChannel {
2077
3133
  const bridgeUrl = this.config.bridgeUrl;
2078
3134
  while (this.running) {
2079
3135
  try {
2080
- await new Promise((resolve5, reject) => {
3136
+ await new Promise((resolve6, reject) => {
2081
3137
  const ws = new WebSocket(bridgeUrl);
2082
3138
  this.ws = ws;
2083
3139
  ws.on("open", () => {
@@ -2090,7 +3146,7 @@ var WhatsAppChannel = class extends BaseChannel {
2090
3146
  ws.on("close", () => {
2091
3147
  this.connected = false;
2092
3148
  this.ws = null;
2093
- resolve5();
3149
+ resolve6();
2094
3150
  });
2095
3151
  ws.on("error", (_err) => {
2096
3152
  this.connected = false;
@@ -2173,17 +3229,18 @@ var WhatsAppChannel = class extends BaseChannel {
2173
3229
  }
2174
3230
  };
2175
3231
  function sleep(ms) {
2176
- return new Promise((resolve5) => setTimeout(resolve5, ms));
3232
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
2177
3233
  }
2178
3234
 
2179
3235
  // src/channels/discord.ts
2180
3236
  import {
2181
3237
  Client as Client2,
2182
3238
  GatewayIntentBits,
2183
- Partials
3239
+ Partials,
3240
+ MessageFlags
2184
3241
  } from "discord.js";
2185
- import { fetch as fetch3 } from "undici";
2186
- import { join as join6 } from "path";
3242
+ import { fetch as fetch4 } from "undici";
3243
+ import { join as join7 } from "path";
2187
3244
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
2188
3245
  var MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
2189
3246
  var DiscordChannel = class extends BaseChannel {
@@ -2237,6 +3294,9 @@ var DiscordChannel = class extends BaseChannel {
2237
3294
  if (msg.replyTo) {
2238
3295
  payload.reply = { messageReference: msg.replyTo };
2239
3296
  }
3297
+ if (msg.metadata?.silent === true) {
3298
+ payload.flags = MessageFlags.SuppressNotifications;
3299
+ }
2240
3300
  await textChannel.send(payload);
2241
3301
  }
2242
3302
  async handleIncoming(message) {
@@ -2254,7 +3314,7 @@ var DiscordChannel = class extends BaseChannel {
2254
3314
  contentParts.push(message.content);
2255
3315
  }
2256
3316
  if (message.attachments.size) {
2257
- const mediaDir = join6(getDataPath(), "media");
3317
+ const mediaDir = join7(getDataPath(), "media");
2258
3318
  mkdirSync3(mediaDir, { recursive: true });
2259
3319
  for (const attachment of message.attachments.values()) {
2260
3320
  if (attachment.size && attachment.size > MAX_ATTACHMENT_BYTES) {
@@ -2262,14 +3322,14 @@ var DiscordChannel = class extends BaseChannel {
2262
3322
  continue;
2263
3323
  }
2264
3324
  try {
2265
- const res = await fetch3(attachment.url);
3325
+ const res = await fetch4(attachment.url);
2266
3326
  if (!res.ok) {
2267
3327
  contentParts.push(`[attachment: ${attachment.name ?? "file"} - download failed]`);
2268
3328
  continue;
2269
3329
  }
2270
3330
  const buffer = Buffer.from(await res.arrayBuffer());
2271
3331
  const filename = `${attachment.id}_${(attachment.name ?? "file").replace(/\//g, "_")}`;
2272
- const filePath = join6(mediaDir, filename);
3332
+ const filePath = join7(mediaDir, filename);
2273
3333
  writeFileSync4(filePath, buffer);
2274
3334
  mediaPaths.push(filePath);
2275
3335
  contentParts.push(`[attachment: ${filePath}]`);
@@ -2515,9 +3575,9 @@ function parseMdTable(tableText) {
2515
3575
 
2516
3576
  // src/channels/mochat.ts
2517
3577
  import { io } from "socket.io-client";
2518
- import { fetch as fetch4 } from "undici";
2519
- import { join as join7 } from "path";
2520
- import { mkdirSync as mkdirSync4, existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
3578
+ import { fetch as fetch5 } from "undici";
3579
+ import { join as join8 } from "path";
3580
+ import { mkdirSync as mkdirSync4, existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2521
3581
  var MAX_SEEN_MESSAGE_IDS = 2e3;
2522
3582
  var CURSOR_SAVE_DEBOUNCE_MS = 500;
2523
3583
  var AsyncLock = class {
@@ -2536,8 +3596,8 @@ var MochatChannel = class extends BaseChannel {
2536
3596
  socket = null;
2537
3597
  wsConnected = false;
2538
3598
  wsReady = false;
2539
- stateDir = join7(getDataPath(), "mochat");
2540
- cursorPath = join7(this.stateDir, "session_cursors.json");
3599
+ stateDir = join8(getDataPath(), "mochat");
3600
+ cursorPath = join8(this.stateDir, "session_cursors.json");
2541
3601
  sessionCursor = {};
2542
3602
  cursorSaveTimer = null;
2543
3603
  sessionSet = /* @__PURE__ */ new Set();
@@ -2723,15 +3783,15 @@ var MochatChannel = class extends BaseChannel {
2723
3783
  socket.on(eventName, notifyHandler(eventName));
2724
3784
  });
2725
3785
  this.socket = socket;
2726
- return new Promise((resolve5) => {
2727
- const timer = setTimeout(() => resolve5(false), timeout);
3786
+ return new Promise((resolve6) => {
3787
+ const timer = setTimeout(() => resolve6(false), timeout);
2728
3788
  socket.once("connect", () => {
2729
3789
  clearTimeout(timer);
2730
- resolve5(true);
3790
+ resolve6(true);
2731
3791
  });
2732
3792
  socket.once("connect_error", () => {
2733
3793
  clearTimeout(timer);
2734
- resolve5(false);
3794
+ resolve6(false);
2735
3795
  });
2736
3796
  });
2737
3797
  }
@@ -2793,17 +3853,17 @@ var MochatChannel = class extends BaseChannel {
2793
3853
  if (!this.socket) {
2794
3854
  return { result: false, message: "socket not connected" };
2795
3855
  }
2796
- return new Promise((resolve5) => {
3856
+ return new Promise((resolve6) => {
2797
3857
  this.socket?.timeout(1e4).emit(eventName, payload, (err, response) => {
2798
3858
  if (err) {
2799
- resolve5({ result: false, message: String(err) });
3859
+ resolve6({ result: false, message: String(err) });
2800
3860
  return;
2801
3861
  }
2802
3862
  if (response && typeof response === "object") {
2803
- resolve5(response);
3863
+ resolve6(response);
2804
3864
  return;
2805
3865
  }
2806
- resolve5({ result: true, data: response });
3866
+ resolve6({ result: true, data: response });
2807
3867
  });
2808
3868
  });
2809
3869
  }
@@ -3226,11 +4286,11 @@ var MochatChannel = class extends BaseChannel {
3226
4286
  }
3227
4287
  }
3228
4288
  async loadSessionCursors() {
3229
- if (!existsSync8(this.cursorPath)) {
4289
+ if (!existsSync9(this.cursorPath)) {
3230
4290
  return;
3231
4291
  }
3232
4292
  try {
3233
- const raw = readFileSync6(this.cursorPath, "utf-8");
4293
+ const raw = readFileSync7(this.cursorPath, "utf-8");
3234
4294
  const data = JSON.parse(raw);
3235
4295
  const cursors = data.cursors;
3236
4296
  if (cursors && typeof cursors === "object") {
@@ -3259,7 +4319,7 @@ var MochatChannel = class extends BaseChannel {
3259
4319
  }
3260
4320
  async postJson(path, payload) {
3261
4321
  const url = `${this.config.baseUrl.trim().replace(/\/$/, "")}${path}`;
3262
- const response = await fetch4(url, {
4322
+ const response = await fetch5(url, {
3263
4323
  method: "POST",
3264
4324
  headers: {
3265
4325
  "content-type": "application/json",
@@ -3459,12 +4519,12 @@ function readGroupId(metadata) {
3459
4519
  return null;
3460
4520
  }
3461
4521
  function sleep2(ms) {
3462
- return new Promise((resolve5) => setTimeout(resolve5, ms));
4522
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
3463
4523
  }
3464
4524
 
3465
4525
  // src/channels/dingtalk.ts
3466
4526
  import { DWClient, EventAck, TOPIC_ROBOT } from "dingtalk-stream";
3467
- import { fetch as fetch5 } from "undici";
4527
+ import { fetch as fetch6 } from "undici";
3468
4528
  var DingTalkChannel = class extends BaseChannel {
3469
4529
  name = "dingtalk";
3470
4530
  client = null;
@@ -3511,7 +4571,7 @@ var DingTalkChannel = class extends BaseChannel {
3511
4571
  title: `${APP_TITLE} Reply`
3512
4572
  })
3513
4573
  };
3514
- const response = await fetch5(url, {
4574
+ const response = await fetch6(url, {
3515
4575
  method: "POST",
3516
4576
  headers: {
3517
4577
  "content-type": "application/json",
@@ -3565,7 +4625,7 @@ var DingTalkChannel = class extends BaseChannel {
3565
4625
  appKey: this.config.clientId,
3566
4626
  appSecret: this.config.clientSecret
3567
4627
  };
3568
- const response = await fetch5(url, {
4628
+ const response = await fetch6(url, {
3569
4629
  method: "POST",
3570
4630
  headers: { "content-type": "application/json" },
3571
4631
  body: JSON.stringify(payload)
@@ -3589,7 +4649,7 @@ var DingTalkChannel = class extends BaseChannel {
3589
4649
  import { ImapFlow } from "imapflow";
3590
4650
  import { simpleParser } from "mailparser";
3591
4651
  import nodemailer from "nodemailer";
3592
- var sleep3 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
4652
+ var sleep3 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
3593
4653
  var EmailChannel = class extends BaseChannel {
3594
4654
  name = "email";
3595
4655
  lastSubjectByChat = /* @__PURE__ */ new Map();
@@ -4186,8 +5246,8 @@ var ChannelManager = class {
4186
5246
  };
4187
5247
 
4188
5248
  // src/config/loader.ts
4189
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
4190
- import { resolve as resolve4 } from "path";
5249
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync10, mkdirSync as mkdirSync5 } from "fs";
5250
+ import { resolve as resolve5 } from "path";
4191
5251
  import { z as z2 } from "zod";
4192
5252
 
4193
5253
  // src/config/schema.ts
@@ -4257,7 +5317,10 @@ var PROVIDERS = [
4257
5317
  detectByBaseKeyword: "",
4258
5318
  defaultApiBase: "",
4259
5319
  stripModelPrefix: false,
4260
- modelOverrides: []
5320
+ modelOverrides: [],
5321
+ supportsWireApi: true,
5322
+ wireApiOptions: ["auto", "chat", "responses"],
5323
+ defaultWireApi: "auto"
4261
5324
  },
4262
5325
  {
4263
5326
  name: "deepseek",
@@ -4549,13 +5612,39 @@ var AgentDefaultsSchema = z.object({
4549
5612
  temperature: z.number().default(0.7),
4550
5613
  maxToolIterations: z.number().int().default(20)
4551
5614
  });
5615
+ var ContextBootstrapSchema = z.object({
5616
+ files: z.array(z.string()).default([
5617
+ "AGENTS.md",
5618
+ "SOUL.md",
5619
+ "USER.md",
5620
+ "IDENTITY.md",
5621
+ "TOOLS.md",
5622
+ "BOOT.md",
5623
+ "BOOTSTRAP.md",
5624
+ "HEARTBEAT.md"
5625
+ ]),
5626
+ minimalFiles: z.array(z.string()).default(["AGENTS.md", "SOUL.md", "TOOLS.md", "IDENTITY.md"]),
5627
+ heartbeatFiles: z.array(z.string()).default(["HEARTBEAT.md"]),
5628
+ perFileChars: z.number().int().default(4e3),
5629
+ totalChars: z.number().int().default(12e3)
5630
+ });
5631
+ var ContextMemorySchema = z.object({
5632
+ enabled: z.boolean().default(true),
5633
+ maxChars: z.number().int().default(8e3)
5634
+ });
5635
+ var ContextConfigSchema = z.object({
5636
+ bootstrap: ContextBootstrapSchema.default({}),
5637
+ memory: ContextMemorySchema.default({})
5638
+ });
4552
5639
  var AgentsConfigSchema = z.object({
4553
- defaults: AgentDefaultsSchema.default({})
5640
+ defaults: AgentDefaultsSchema.default({}),
5641
+ context: ContextConfigSchema.default({})
4554
5642
  });
4555
5643
  var ProviderConfigSchema = z.object({
4556
5644
  apiKey: z.string().default(""),
4557
5645
  apiBase: z.string().nullable().default(null),
4558
- extraHeaders: z.record(z.string()).nullable().default(null)
5646
+ extraHeaders: z.record(z.string()).nullable().default(null),
5647
+ wireApi: z.enum(["auto", "chat", "responses"]).default("auto")
4559
5648
  });
4560
5649
  var ProvidersConfigSchema = z.object({
4561
5650
  anthropic: ProviderConfigSchema.default({}),
@@ -4649,16 +5738,16 @@ function getApiBase(config, model) {
4649
5738
 
4650
5739
  // src/config/loader.ts
4651
5740
  function getConfigPath() {
4652
- return resolve4(getDataPath(), "config.json");
5741
+ return resolve5(getDataPath(), "config.json");
4653
5742
  }
4654
5743
  function getDataDir() {
4655
5744
  return getDataPath();
4656
5745
  }
4657
5746
  function loadConfig(configPath) {
4658
5747
  const path = configPath ?? getConfigPath();
4659
- if (existsSync9(path)) {
5748
+ if (existsSync10(path)) {
4660
5749
  try {
4661
- const raw = readFileSync7(path, "utf-8");
5750
+ const raw = readFileSync8(path, "utf-8");
4662
5751
  const data = JSON.parse(raw);
4663
5752
  const migrated = migrateConfig(data);
4664
5753
  return ConfigSchema.parse(migrated);
@@ -4671,7 +5760,7 @@ function loadConfig(configPath) {
4671
5760
  }
4672
5761
  function saveConfig(config, configPath) {
4673
5762
  const path = configPath ?? getConfigPath();
4674
- mkdirSync5(resolve4(path, ".."), { recursive: true });
5763
+ mkdirSync5(resolve5(path, ".."), { recursive: true });
4675
5764
  writeFileSync6(path, JSON.stringify(config, null, 2));
4676
5765
  }
4677
5766
  function migrateConfig(data) {
@@ -4696,7 +5785,7 @@ var isPlainObject = (value) => {
4696
5785
  };
4697
5786
  var RELOAD_RULES = [
4698
5787
  { prefix: "channels", kind: "restart-channels" },
4699
- { prefix: "providers", kind: "restart-required" },
5788
+ { prefix: "providers", kind: "reload-providers" },
4700
5789
  { prefix: "agents.defaults.model", kind: "restart-required" },
4701
5790
  { prefix: "agents.defaults.maxTokens", kind: "restart-required" },
4702
5791
  { prefix: "agents.defaults.temperature", kind: "restart-required" },
@@ -4745,6 +5834,7 @@ function buildReloadPlan(changedPaths) {
4745
5834
  const plan = {
4746
5835
  changedPaths,
4747
5836
  restartChannels: false,
5837
+ reloadProviders: false,
4748
5838
  restartRequired: [],
4749
5839
  noopPaths: []
4750
5840
  };
@@ -4758,6 +5848,10 @@ function buildReloadPlan(changedPaths) {
4758
5848
  plan.restartChannels = true;
4759
5849
  continue;
4760
5850
  }
5851
+ if (rule.kind === "reload-providers") {
5852
+ plan.reloadProviders = true;
5853
+ continue;
5854
+ }
4761
5855
  if (rule.kind === "restart-required") {
4762
5856
  plan.restartRequired.push(path);
4763
5857
  continue;
@@ -4768,7 +5862,7 @@ function buildReloadPlan(changedPaths) {
4768
5862
  }
4769
5863
 
4770
5864
  // src/cron/service.ts
4771
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
5865
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
4772
5866
  import { dirname as dirname2 } from "path";
4773
5867
  import { randomUUID as randomUUID2 } from "crypto";
4774
5868
  import cronParser from "cron-parser";
@@ -4806,9 +5900,9 @@ var CronService = class {
4806
5900
  if (this.store) {
4807
5901
  return this.store;
4808
5902
  }
4809
- if (existsSync10(this.storePath)) {
5903
+ if (existsSync11(this.storePath)) {
4810
5904
  try {
4811
- const data = JSON.parse(readFileSync8(this.storePath, "utf-8"));
5905
+ const data = JSON.parse(readFileSync9(this.storePath, "utf-8"));
4812
5906
  const jobs = (data.jobs ?? []).map((job) => ({
4813
5907
  id: String(job.id),
4814
5908
  name: String(job.name),
@@ -5011,8 +6105,8 @@ var CronService = class {
5011
6105
  };
5012
6106
 
5013
6107
  // src/heartbeat/service.ts
5014
- import { readFileSync as readFileSync9, existsSync as existsSync11 } from "fs";
5015
- import { join as join8 } from "path";
6108
+ import { readFileSync as readFileSync10, existsSync as existsSync12 } from "fs";
6109
+ import { join as join9 } from "path";
5016
6110
  var DEFAULT_HEARTBEAT_INTERVAL_S = 30 * 60;
5017
6111
  var HEARTBEAT_PROMPT = "Read HEARTBEAT.md in your workspace (if it exists).\nFollow any instructions or tasks listed there.\nIf nothing needs attention, reply with just: HEARTBEAT_OK";
5018
6112
  var HEARTBEAT_OK_TOKEN = "HEARTBEAT_OK";
@@ -5040,12 +6134,12 @@ var HeartbeatService = class {
5040
6134
  running = false;
5041
6135
  timer = null;
5042
6136
  get heartbeatFile() {
5043
- return join8(this.workspace, "HEARTBEAT.md");
6137
+ return join9(this.workspace, "HEARTBEAT.md");
5044
6138
  }
5045
6139
  readHeartbeatFile() {
5046
- if (existsSync11(this.heartbeatFile)) {
6140
+ if (existsSync12(this.heartbeatFile)) {
5047
6141
  try {
5048
- return readFileSync9(this.heartbeatFile, "utf-8");
6142
+ return readFileSync10(this.heartbeatFile, "utf-8");
5049
6143
  } catch {
5050
6144
  return null;
5051
6145
  }
@@ -5107,10 +6201,12 @@ var OpenAICompatibleProvider = class extends LLMProvider {
5107
6201
  client;
5108
6202
  defaultModel;
5109
6203
  extraHeaders;
6204
+ wireApi;
5110
6205
  constructor(options) {
5111
6206
  super(options.apiKey, options.apiBase);
5112
6207
  this.defaultModel = options.defaultModel;
5113
6208
  this.extraHeaders = options.extraHeaders ?? null;
6209
+ this.wireApi = options.wireApi ?? "auto";
5114
6210
  this.client = new OpenAI({
5115
6211
  apiKey: options.apiKey ?? void 0,
5116
6212
  baseURL: options.apiBase ?? void 0,
@@ -5121,6 +6217,22 @@ var OpenAICompatibleProvider = class extends LLMProvider {
5121
6217
  return this.defaultModel;
5122
6218
  }
5123
6219
  async chat(params) {
6220
+ if (this.wireApi === "chat") {
6221
+ return this.chatCompletions(params);
6222
+ }
6223
+ if (this.wireApi === "responses") {
6224
+ return this.chatResponses(params);
6225
+ }
6226
+ try {
6227
+ return await this.chatCompletions(params);
6228
+ } catch (error) {
6229
+ if (this.shouldFallbackToResponses(error)) {
6230
+ return await this.chatResponses(params);
6231
+ }
6232
+ throw error;
6233
+ }
6234
+ }
6235
+ async chatCompletions(params) {
5124
6236
  const model = params.model ?? this.defaultModel;
5125
6237
  const temperature = params.temperature ?? 0.7;
5126
6238
  const maxTokens = params.maxTokens ?? 4096;
@@ -5166,6 +6278,141 @@ var OpenAICompatibleProvider = class extends LLMProvider {
5166
6278
  reasoningContent
5167
6279
  };
5168
6280
  }
6281
+ async chatResponses(params) {
6282
+ const model = params.model ?? this.defaultModel;
6283
+ const input = this.toResponsesInput(params.messages);
6284
+ const body = { model, input };
6285
+ if (params.tools && params.tools.length) {
6286
+ body.tools = params.tools;
6287
+ }
6288
+ const base = this.apiBase ?? "https://api.openai.com/v1";
6289
+ const responseUrl = new URL("responses", base.endsWith("/") ? base : `${base}/`);
6290
+ const response = await fetch(responseUrl.toString(), {
6291
+ method: "POST",
6292
+ headers: {
6293
+ "Authorization": this.apiKey ? `Bearer ${this.apiKey}` : "",
6294
+ "Content-Type": "application/json",
6295
+ ...this.extraHeaders ?? {}
6296
+ },
6297
+ body: JSON.stringify(body)
6298
+ });
6299
+ if (!response.ok) {
6300
+ const text = await response.text();
6301
+ throw new Error(`Responses API failed (${response.status}): ${text.slice(0, 200)}`);
6302
+ }
6303
+ const responseAny = await response.json();
6304
+ const outputItems = responseAny.output ?? [];
6305
+ const toolCalls = [];
6306
+ const contentParts = [];
6307
+ let reasoningContent = null;
6308
+ for (const item of outputItems) {
6309
+ const itemAny = item;
6310
+ if (itemAny.type === "reasoning" && Array.isArray(itemAny.summary)) {
6311
+ const summaryText = itemAny.summary.map((entry) => typeof entry === "string" ? entry : String(entry.text ?? "")).filter(Boolean).join("\n");
6312
+ reasoningContent = summaryText || reasoningContent;
6313
+ }
6314
+ if (itemAny.type === "message" && Array.isArray(itemAny.content)) {
6315
+ for (const part of itemAny.content) {
6316
+ const partAny = part;
6317
+ if (partAny?.type === "output_text" || partAny?.type === "text") {
6318
+ const text = String(partAny?.text ?? "");
6319
+ if (text) {
6320
+ contentParts.push(text);
6321
+ }
6322
+ }
6323
+ }
6324
+ }
6325
+ if (itemAny.type === "tool_call" || itemAny.type === "function_call") {
6326
+ const itemFunction = itemAny.function;
6327
+ const name = String(itemAny.name ?? itemFunction?.name ?? "");
6328
+ const rawArgs = itemAny.arguments ?? itemFunction?.arguments ?? itemAny.input ?? itemFunction?.input ?? "{}";
6329
+ let args = {};
6330
+ try {
6331
+ args = typeof rawArgs === "string" ? JSON.parse(rawArgs) : rawArgs;
6332
+ } catch {
6333
+ args = {};
6334
+ }
6335
+ toolCalls.push({
6336
+ id: String(itemAny.id ?? itemAny.call_id ?? `${name}-${toolCalls.length}`),
6337
+ name,
6338
+ arguments: args
6339
+ });
6340
+ }
6341
+ }
6342
+ const usage = responseAny.usage ?? {};
6343
+ return {
6344
+ content: contentParts.join("") || null,
6345
+ toolCalls,
6346
+ finishReason: responseAny.status ?? "stop",
6347
+ usage: {
6348
+ prompt_tokens: usage.input_tokens ?? usage.prompt_tokens ?? 0,
6349
+ completion_tokens: usage.output_tokens ?? usage.completion_tokens ?? 0,
6350
+ total_tokens: usage.total_tokens ?? 0
6351
+ },
6352
+ reasoningContent
6353
+ };
6354
+ }
6355
+ shouldFallbackToResponses(error) {
6356
+ const err = error;
6357
+ const status = err?.status;
6358
+ const message = err?.message ?? "";
6359
+ if (status === 404) {
6360
+ return true;
6361
+ }
6362
+ if (message.includes("Cannot POST") && message.includes("chat/completions")) {
6363
+ return true;
6364
+ }
6365
+ if (message.includes("chat/completions") && message.includes("404")) {
6366
+ return true;
6367
+ }
6368
+ return false;
6369
+ }
6370
+ toResponsesInput(messages) {
6371
+ const input = [];
6372
+ for (const msg of messages) {
6373
+ const role = String(msg.role ?? "user");
6374
+ const content = msg.content;
6375
+ if (role === "tool") {
6376
+ const callId = typeof msg.tool_call_id === "string" ? msg.tool_call_id : "";
6377
+ const outputText = typeof content === "string" ? content : Array.isArray(content) ? JSON.stringify(content) : String(content ?? "");
6378
+ input.push({
6379
+ type: "function_call_output",
6380
+ call_id: callId,
6381
+ output: outputText
6382
+ });
6383
+ continue;
6384
+ }
6385
+ const output = { role };
6386
+ if (Array.isArray(content) || typeof content === "string") {
6387
+ output.content = content;
6388
+ } else {
6389
+ output.content = String(content ?? "");
6390
+ }
6391
+ if (typeof msg.reasoning_content === "string" && msg.reasoning_content) {
6392
+ output.reasoning = msg.reasoning_content;
6393
+ }
6394
+ input.push(output);
6395
+ if (Array.isArray(msg.tool_calls)) {
6396
+ for (const call of msg.tool_calls) {
6397
+ const callAny = call;
6398
+ const functionAny = callAny.function ?? {};
6399
+ const callId = String(callAny.id ?? callAny.call_id ?? "");
6400
+ const name = String(functionAny.name ?? callAny.name ?? "");
6401
+ const args = String(functionAny.arguments ?? callAny.arguments ?? "{}");
6402
+ if (!callId || !name) {
6403
+ continue;
6404
+ }
6405
+ input.push({
6406
+ type: "function_call",
6407
+ name,
6408
+ arguments: args,
6409
+ call_id: callId
6410
+ });
6411
+ }
6412
+ }
6413
+ }
6414
+ return input;
6415
+ }
5169
6416
  };
5170
6417
 
5171
6418
  // src/providers/litellm_provider.ts
@@ -5181,11 +6428,14 @@ var LiteLLMProvider = class extends LLMProvider {
5181
6428
  this.extraHeaders = options.extraHeaders ?? null;
5182
6429
  this.providerName = options.providerName ?? null;
5183
6430
  this.gatewaySpec = findGateway(this.providerName, options.apiKey ?? null, options.apiBase ?? null) ?? void 0;
6431
+ const providerSpec = this.providerName ? findProviderByName(this.providerName) : void 0;
6432
+ const wireApi = providerSpec?.supportsWireApi ? options.wireApi ?? providerSpec.defaultWireApi ?? "auto" : void 0;
5184
6433
  this.client = new OpenAICompatibleProvider({
5185
6434
  apiKey: options.apiKey ?? null,
5186
6435
  apiBase: options.apiBase ?? null,
5187
6436
  defaultModel: options.defaultModel,
5188
- extraHeaders: options.extraHeaders ?? null
6437
+ extraHeaders: options.extraHeaders ?? null,
6438
+ wireApi
5189
6439
  });
5190
6440
  }
5191
6441
  getDefaultModel() {
@@ -5263,6 +6513,20 @@ var LiteLLMProvider = class extends LLMProvider {
5263
6513
  return findProviderByModel(model) ?? (this.providerName ? findProviderByName(this.providerName) : void 0);
5264
6514
  }
5265
6515
  };
6516
+
6517
+ // src/providers/provider_manager.ts
6518
+ var ProviderManager = class {
6519
+ provider;
6520
+ constructor(provider) {
6521
+ this.provider = provider;
6522
+ }
6523
+ get() {
6524
+ return this.provider;
6525
+ }
6526
+ set(next) {
6527
+ this.provider = next;
6528
+ }
6529
+ };
5266
6530
  export {
5267
6531
  APP_NAME,
5268
6532
  APP_REPLY_SUBJECT,
@@ -5275,6 +6539,9 @@ export {
5275
6539
  ChannelManager,
5276
6540
  ChannelsConfigSchema,
5277
6541
  ConfigSchema,
6542
+ ContextBootstrapSchema,
6543
+ ContextConfigSchema,
6544
+ ContextMemorySchema,
5278
6545
  CronService,
5279
6546
  DEFAULT_CONFIG_FILE,
5280
6547
  DEFAULT_CONFIG_PATH,
@@ -5290,6 +6557,7 @@ export {
5290
6557
  ExecToolConfigSchema,
5291
6558
  FeishuConfigSchema,
5292
6559
  GatewayConfigSchema,
6560
+ GatewayTool,
5293
6561
  HEARTBEAT_OK_TOKEN,
5294
6562
  HEARTBEAT_PROMPT,
5295
6563
  HeartbeatService,
@@ -5301,6 +6569,7 @@ export {
5301
6569
  MochatMentionSchema,
5302
6570
  PROVIDERS,
5303
6571
  ProviderConfigSchema,
6572
+ ProviderManager,
5304
6573
  ProvidersConfigSchema,
5305
6574
  QQConfigSchema,
5306
6575
  SKILL_METADATA_KEY,