@superatomai/sdk-node 0.0.69-dsp → 0.0.70-dsp

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.mjs CHANGED
@@ -282,7 +282,13 @@ var UserPromptRequestPayloadSchema = z3.object({
282
282
  threadId: z3.string(),
283
283
  uiBlockId: z3.string()
284
284
  }).optional(),
285
- responseMode: z3.enum(["component", "text"]).optional()
285
+ responseMode: z3.enum(["component", "text"]).optional(),
286
+ /**
287
+ * Where the question was asked from — e.g. the dashboard being viewed. See
288
+ * `PromptContext`. Accepted as anything so a client sending an unexpected shape
289
+ * is never rejected; the handler keeps only the fields it recognises.
290
+ */
291
+ context: z3.unknown().optional()
286
292
  });
287
293
  var MessageAuthContextSchema = z3.object({
288
294
  userId: z3.string(),
@@ -989,9 +995,25 @@ var SchemaResponseMessageSchema = z3.object({
989
995
 
990
996
  // src/utils/logger.ts
991
997
  import fs from "fs";
998
+ import path from "path";
992
999
  var PREFIX = "[SuperatomSDK]";
993
- var LOG_FILE_PATH = "superatom-sdk.log";
994
- var LOGSTREAM = fs.createWriteStream(LOG_FILE_PATH, { flags: "a" });
1000
+ var LEGACY_LOG_FILE_PATH = "superatom-sdk.log";
1001
+ var LOG_DIR = "logs";
1002
+ function slugifyLabel(label) {
1003
+ return label.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 60);
1004
+ }
1005
+ function buildLogFilePath(label) {
1006
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1007
+ const slug2 = label ? slugifyLabel(label) : "";
1008
+ const filename = slug2 ? `sdk-${stamp}-${slug2}.log` : `sdk-${stamp}.log`;
1009
+ return path.join(LOG_DIR, filename);
1010
+ }
1011
+ try {
1012
+ fs.mkdirSync(LOG_DIR, { recursive: true });
1013
+ } catch {
1014
+ }
1015
+ var currentLogFilePath = LEGACY_LOG_FILE_PATH;
1016
+ var LOGSTREAM = fs.createWriteStream(LEGACY_LOG_FILE_PATH, { flags: "a" });
995
1017
  var LOG_LEVEL_PRIORITY = {
996
1018
  errors: 0,
997
1019
  warnings: 1,
@@ -1076,17 +1098,31 @@ var Logger = class {
1076
1098
  }
1077
1099
  }
1078
1100
  /**
1079
- * Write to log file
1101
+ * Write to log file — each line timestamped so a request that made many
1102
+ * calls (LLM turns, tool dispatches, direct-tool runs) can be read back in
1103
+ * order, matching "know what happened when" rather than just "what
1104
+ * happened".
1080
1105
  */
1081
1106
  file(...args) {
1082
- LOGSTREAM.write(args.join(" ") + "\n");
1107
+ LOGSTREAM.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${args.join(" ")}
1108
+ `);
1109
+ }
1110
+ /** The log file the current request is writing to — print/return this so it's easy to find afterward. */
1111
+ getCurrentLogFilePath() {
1112
+ return currentLogFilePath;
1083
1113
  }
1084
1114
  /**
1085
- * Clear the log file (call at start of new user request)
1115
+ * Start a fresh, uniquely-named log file for a new top-level request
1116
+ * (chat turn, dashComp request, reportComp request, ...) instead of
1117
+ * truncating one shared file — pass a short label (a prompt snippet, a
1118
+ * request type) so the resulting filename is identifiable at a glance.
1119
+ * Falls back to the untimestamped legacy path when `label` is omitted, for
1120
+ * any caller not yet updated to pass one.
1086
1121
  */
1087
- clearFile() {
1122
+ clearFile(label) {
1088
1123
  LOGSTREAM.end();
1089
- LOGSTREAM = fs.createWriteStream(LOG_FILE_PATH, { flags: "w" });
1124
+ currentLogFilePath = label ? buildLogFilePath(label) : LEGACY_LOG_FILE_PATH;
1125
+ LOGSTREAM = fs.createWriteStream(currentLogFilePath, { flags: "w" });
1090
1126
  LOGSTREAM.write(`
1091
1127
  ${"=".repeat(80)}
1092
1128
  `);
@@ -1095,6 +1131,7 @@ ${"=".repeat(80)}
1095
1131
  LOGSTREAM.write(`${"=".repeat(80)}
1096
1132
 
1097
1133
  `);
1134
+ this.info(`Logging this request to ${currentLogFilePath}`);
1098
1135
  }
1099
1136
  /**
1100
1137
  * Log LLM method prompts with clear labeling
@@ -1102,7 +1139,7 @@ ${"=".repeat(80)}
1102
1139
  logLLMPrompt(methodName, promptType, content) {
1103
1140
  const header = `
1104
1141
  ${"#".repeat(80)}
1105
- [LLM METHOD: ${methodName}] - ${promptType.toUpperCase()} PROMPT
1142
+ [${(/* @__PURE__ */ new Date()).toISOString()}] [LLM METHOD: ${methodName}] - ${promptType.toUpperCase()} PROMPT
1106
1143
  ${"#".repeat(80)}
1107
1144
  `;
1108
1145
  LOGSTREAM.write(header);
@@ -1827,6 +1864,7 @@ var QueryCache = class {
1827
1864
  this.maxCacheSize = 5e3;
1828
1865
  // Max data cache entries
1829
1866
  this.cleanupInterval = null;
1867
+ this.resultCacheEnabled = true;
1830
1868
  // Encryption for queryId tokens
1831
1869
  this.algorithm = "aes-256-gcm";
1832
1870
  const keySource = process.env.QUERY_TOKEN_SECRET || "superatom-query-cache-default-key-v1";
@@ -1850,6 +1888,15 @@ var QueryCache = class {
1850
1888
  getTTL() {
1851
1889
  return this.ttlMs / 60 / 1e3;
1852
1890
  }
1891
+ /**
1892
+ * Stop keeping result data in memory, for a host that caches results itself.
1893
+ * `set` stores nothing and `get` finds nothing; queryId tokens are unaffected.
1894
+ */
1895
+ disableResultCache() {
1896
+ this.resultCacheEnabled = false;
1897
+ this.cache.clear();
1898
+ logger.info("[QueryCache] Result cache disabled \u2014 the host caches query results");
1899
+ }
1853
1900
  /**
1854
1901
  * Which access bucket this call reads and writes.
1855
1902
  *
@@ -1882,6 +1929,7 @@ var QueryCache = class {
1882
1929
  * a genuinely new key past the size limit.
1883
1930
  */
1884
1931
  set(query, data) {
1932
+ if (!this.resultCacheEnabled) return;
1885
1933
  const key = this.scopedKey(query);
1886
1934
  if (key === null) return;
1887
1935
  this.cache.delete(key);
@@ -1902,6 +1950,7 @@ var QueryCache = class {
1902
1950
  * iteration order — turning FIFO eviction into true LRU.
1903
1951
  */
1904
1952
  get(query) {
1953
+ if (!this.resultCacheEnabled) return null;
1905
1954
  const key = this.scopedKey(query);
1906
1955
  if (key === null) return null;
1907
1956
  const entry = this.cache.get(key);
@@ -2680,7 +2729,7 @@ function sendDataResponse(id, collection, op, data, meta, sendMessage) {
2680
2729
 
2681
2730
  // src/bundle.ts
2682
2731
  import * as fs2 from "fs";
2683
- import * as path from "path";
2732
+ import * as path2 from "path";
2684
2733
  function getBundleDir(configDir) {
2685
2734
  const bundleDir = configDir || process.env.SA_BUNDLE_DIR;
2686
2735
  if (!bundleDir) {
@@ -2713,7 +2762,7 @@ function getJS(bundleDir) {
2713
2762
  `Could not find index-*.js file in ${bundleDir}. Expected a file matching pattern: index-*.js`
2714
2763
  );
2715
2764
  }
2716
- const filePath = path.join(bundleDir, indexFile);
2765
+ const filePath = path2.join(bundleDir, indexFile);
2717
2766
  logger.info(`Loading bundle from ${filePath}`);
2718
2767
  try {
2719
2768
  return fs2.readFileSync(filePath, "utf8");
@@ -3152,12 +3201,12 @@ function sendDataResponse3(id, res, sendMessage, clientId) {
3152
3201
 
3153
3202
  // src/utils/user-prompt-error-logger.ts
3154
3203
  import fs3 from "fs";
3155
- import path2 from "path";
3204
+ import path3 from "path";
3156
3205
  var UserPromptErrorLogger = class {
3157
3206
  constructor() {
3158
3207
  this.logStream = null;
3159
3208
  this.hasErrors = false;
3160
- this.logPath = process.env.USER_PROMPT_ERROR_LOG_PATH || path2.join(process.cwd(), "user-prompt-req-errors");
3209
+ this.logPath = process.env.USER_PROMPT_ERROR_LOG_PATH || path3.join(process.cwd(), "user-prompt-req-errors");
3161
3210
  this.enabled = process.env.USER_PROMPT_ERROR_LOGGING !== "false";
3162
3211
  }
3163
3212
  /**
@@ -3170,7 +3219,7 @@ var UserPromptErrorLogger = class {
3170
3219
  this.logStream.end();
3171
3220
  this.logStream = null;
3172
3221
  }
3173
- const dir = path2.dirname(this.logPath);
3222
+ const dir = path3.dirname(this.logPath);
3174
3223
  if (dir !== "." && !fs3.existsSync(dir)) {
3175
3224
  fs3.mkdirSync(dir, { recursive: true });
3176
3225
  }
@@ -3650,7 +3699,8 @@ var findExactMatch = async ({
3650
3699
  userPrompt,
3651
3700
  collections,
3652
3701
  userId,
3653
- maxAgeHours
3702
+ maxAgeHours,
3703
+ statuses
3654
3704
  }) => {
3655
3705
  try {
3656
3706
  if (!collections || !collections["conversation-history"] || !collections["conversation-history"]["exactMatch"]) {
@@ -3660,7 +3710,8 @@ var findExactMatch = async ({
3660
3710
  const result = await collections["conversation-history"]["exactMatch"]({
3661
3711
  userPrompt,
3662
3712
  userId,
3663
- maxAgeHours
3713
+ maxAgeHours,
3714
+ statuses
3664
3715
  });
3665
3716
  if (!result || !result.uiBlock) {
3666
3717
  logger.info("[ConversationSearch] No exact match found");
@@ -3823,7 +3874,7 @@ var TaggedStreamBuffer = class extends StreamBuffer {
3823
3874
  }
3824
3875
  };
3825
3876
  function streamDelay(ms = STREAM_DELAY_MS) {
3826
- return new Promise((resolve3) => setTimeout(resolve3, ms));
3877
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
3827
3878
  }
3828
3879
  async function withProgressHeartbeat(operation, progressMessage, streamBuffer, intervalMs = PROGRESS_HEARTBEAT_INTERVAL_MS) {
3829
3880
  if (!streamBuffer.hasCallback()) {
@@ -4016,7 +4067,7 @@ import { jsonrepair } from "jsonrepair";
4016
4067
 
4017
4068
  // src/utils/llm-usage-logger.ts
4018
4069
  import fs4 from "fs";
4019
- import path3 from "path";
4070
+ import path4 from "path";
4020
4071
  var PRICING = {
4021
4072
  // Anthropic (December 2025)
4022
4073
  "claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
@@ -4080,7 +4131,7 @@ var LLMUsageLogger = class {
4080
4131
  totalCostUSD: 0,
4081
4132
  totalDurationMs: 0
4082
4133
  };
4083
- this.logPath = process.env.LLM_USAGE_LOG_PATH || path3.join(process.cwd(), "llm-usage-logs.log");
4134
+ this.logPath = process.env.LLM_USAGE_LOG_PATH || path4.join(process.cwd(), "llm-usage-logs.log");
4084
4135
  this.enabled = process.env.LLM_USAGE_LOGGING !== "false";
4085
4136
  if (this.enabled) {
4086
4137
  this.initLogStream();
@@ -4088,7 +4139,7 @@ var LLMUsageLogger = class {
4088
4139
  }
4089
4140
  initLogStream() {
4090
4141
  try {
4091
- const dir = path3.dirname(this.logPath);
4142
+ const dir = path4.dirname(this.logPath);
4092
4143
  if (!fs4.existsSync(dir)) {
4093
4144
  fs4.mkdirSync(dir, { recursive: true });
4094
4145
  }
@@ -4754,7 +4805,7 @@ var LLM = class {
4754
4805
  iterations++;
4755
4806
  const iterationStartTime = Date.now();
4756
4807
  const requestId = llmUsageLogger.generateRequestId();
4757
- const toolChoice = iterations === 1 && options.firstIterationToolChoice ? options.firstIterationToolChoice : void 0;
4808
+ const toolChoice = iterations === 1 && options.firstIterationToolChoice ? options.firstIterationToolChoice : iterations === 2 && options.secondIterationToolChoice ? options.secondIterationToolChoice : void 0;
4758
4809
  let stream;
4759
4810
  try {
4760
4811
  stream = await client.messages.create({
@@ -5712,7 +5763,7 @@ var LLM = class {
5712
5763
  const iterStart = Date.now();
5713
5764
  const requestId = llmUsageLogger.generateRequestId();
5714
5765
  let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0;
5715
- const toolChoice = iterations === 1 && options.firstIterationToolChoice ? this._openaiToolChoice(options.firstIterationToolChoice) : void 0;
5766
+ const toolChoice = iterations === 1 && options.firstIterationToolChoice ? this._openaiToolChoice(options.firstIterationToolChoice) : iterations === 2 && options.secondIterationToolChoice ? this._openaiToolChoice(options.secondIterationToolChoice) : void 0;
5716
5767
  const createParams = {
5717
5768
  model: modelName,
5718
5769
  messages: conversationMessages,
@@ -5903,7 +5954,7 @@ var LLM = class {
5903
5954
 
5904
5955
  // src/userResponse/prompt-loader.ts
5905
5956
  import fs5 from "fs";
5906
- import path4 from "path";
5957
+ import path5 from "path";
5907
5958
 
5908
5959
  // src/userResponse/prompts.ts
5909
5960
  var PROMPTS = {
@@ -9317,7 +9368,7 @@ var PromptLoader = class {
9317
9368
  this.databaseRulesCache = /* @__PURE__ */ new Map();
9318
9369
  this.isInitialized = false;
9319
9370
  this.databaseType = "postgresql";
9320
- this.promptsDir = config2?.promptsDir || path4.join(process.cwd(), ".prompts");
9371
+ this.promptsDir = config2?.promptsDir || path5.join(process.cwd(), ".prompts");
9321
9372
  }
9322
9373
  /**
9323
9374
  * Load a prompt template from file system OR fallback to hardcoded prompts
@@ -9326,8 +9377,8 @@ var PromptLoader = class {
9326
9377
  */
9327
9378
  loadPromptTemplate(promptName) {
9328
9379
  try {
9329
- const systemPath = path4.join(this.promptsDir, promptName, "system.md");
9330
- const userPath = path4.join(this.promptsDir, promptName, "user.md");
9380
+ const systemPath = path5.join(this.promptsDir, promptName, "system.md");
9381
+ const userPath = path5.join(this.promptsDir, promptName, "user.md");
9331
9382
  if (fs5.existsSync(systemPath) && fs5.existsSync(userPath)) {
9332
9383
  const system = fs5.readFileSync(systemPath, "utf-8");
9333
9384
  const user = fs5.readFileSync(userPath, "utf-8");
@@ -9477,7 +9528,7 @@ var PromptLoader = class {
9477
9528
  if (this.databaseRulesCache.has(this.databaseType)) {
9478
9529
  return this.databaseRulesCache.get(this.databaseType);
9479
9530
  }
9480
- const rulesPath = path4.join(this.promptsDir, "database-rules", `${this.databaseType}.md`);
9531
+ const rulesPath = path5.join(this.promptsDir, "database-rules", `${this.databaseType}.md`);
9481
9532
  try {
9482
9533
  if (fs5.existsSync(rulesPath)) {
9483
9534
  const rules2 = fs5.readFileSync(rulesPath, "utf-8");
@@ -9503,6 +9554,8 @@ var PromptLoader = class {
9503
9554
  "postgres": "postgresql",
9504
9555
  "postgresql": "postgresql",
9505
9556
  "mssql": "mssql",
9557
+ "fabric": "mssql",
9558
+ // Microsoft Fabric SQL endpoints speak T-SQL
9506
9559
  "mysql": "postgresql",
9507
9560
  // MySQL uses similar rules to PostgreSQL
9508
9561
  "excel": "duckdb",
@@ -9520,7 +9573,7 @@ var PromptLoader = class {
9520
9573
  if (this.databaseRulesCache.has(dbType)) {
9521
9574
  return this.databaseRulesCache.get(dbType);
9522
9575
  }
9523
- const rulesPath = path4.join(this.promptsDir, "database-rules", `${dbType}.md`);
9576
+ const rulesPath = path5.join(this.promptsDir, "database-rules", `${dbType}.md`);
9524
9577
  try {
9525
9578
  if (fs5.existsSync(rulesPath)) {
9526
9579
  const rules2 = fs5.readFileSync(rulesPath, "utf-8");
@@ -9550,7 +9603,7 @@ var PromptLoader = class {
9550
9603
  const cacheKey = "__script_body_contract__";
9551
9604
  let raw = this.databaseRulesCache.get(cacheKey);
9552
9605
  if (raw === void 0) {
9553
- const contractPath = path4.join(this.promptsDir, "script-rules", "body-contract.md");
9606
+ const contractPath = path5.join(this.promptsDir, "script-rules", "body-contract.md");
9554
9607
  try {
9555
9608
  if (fs5.existsSync(contractPath)) {
9556
9609
  raw = fs5.readFileSync(contractPath, "utf-8");
@@ -9582,7 +9635,7 @@ var PromptLoader = class {
9582
9635
  return DATABASE_RULES[dbType] ?? DATABASE_RULES["postgresql"];
9583
9636
  }
9584
9637
  };
9585
- var defaultPromptsPath = process.env.PROMPTS_DIR || path4.join(process.cwd(), ".prompts");
9638
+ var defaultPromptsPath = process.env.PROMPTS_DIR || path5.join(process.cwd(), ".prompts");
9586
9639
  var promptLoader = new PromptLoader({
9587
9640
  promptsDir: defaultPromptsPath
9588
9641
  });
@@ -10011,7 +10064,7 @@ function getDatabaseType() {
10011
10064
  function getDbTypeFromToolId(toolId) {
10012
10065
  if (!toolId) return getDatabaseType();
10013
10066
  const prefix = toolId.split("-")[0]?.toLowerCase();
10014
- if (prefix.startsWith("mssql")) return "mssql";
10067
+ if (prefix.startsWith("mssql") || prefix === "fabric") return "mssql";
10015
10068
  if (prefix.startsWith("postgres")) return "postgresql";
10016
10069
  if (prefix.startsWith("mysql")) return "mysql";
10017
10070
  if (prefix.startsWith("snowflake")) return "snowflake";
@@ -10332,6 +10385,7 @@ var SourceAgent = class {
10332
10385
  if (toolName.endsWith("_search_schema") && schemaSearchFn) {
10333
10386
  const keywords = toolInput.keywords || [];
10334
10387
  logger.info(`[SourceAgent:${this.tool.name}] Schema search: ${keywords.join(", ")}`);
10388
+ logger.file(`[SourceAgent:${this.tool.name}] SCHEMA SEARCH:`, keywords.join(", "));
10335
10389
  if (this.streamBuffer.hasCallback()) {
10336
10390
  this.streamBuffer.write(`\u{1F50D} **Looking up tables:** ${keywords.join(", ")}
10337
10391
 
@@ -10344,6 +10398,7 @@ var SourceAgent = class {
10344
10398
  } catch (traceErr) {
10345
10399
  logger.warn(`[SourceAgent:${this.tool.name}] Trace capture failed for search_schema lookup (non-blocking): ${traceErr}`);
10346
10400
  }
10401
+ logger.file(`[SourceAgent:${this.tool.name}] SCHEMA SEARCH RESULT:`, result);
10347
10402
  return `Schema search results:
10348
10403
 
10349
10404
  ${result}`;
@@ -10387,6 +10442,7 @@ ${result}`;
10387
10442
  cappedInput.query = ensureQueryLimit(cappedInput.query, this.config.maxRowsFetched, this.config.maxRowsFetched, _st);
10388
10443
  }
10389
10444
  queryExecuted = cappedInput.sql || cappedInput.query || JSON.stringify(cappedInput);
10445
+ logger.file(`[SourceAgent:${this.tool.name}] QUERY (attempt ${this.attempts}):`, queryExecuted);
10390
10446
  if (this.streamBuffer.hasCallback() && queryExecuted) {
10391
10447
  const queryDisplay = cappedInput.sql || cappedInput.query;
10392
10448
  if (queryDisplay) {
@@ -10415,6 +10471,7 @@ ${queryDisplay}
10415
10471
  if (result && result.error) {
10416
10472
  const errorMsg = typeof result.error === "string" ? result.error : JSON.stringify(result.error);
10417
10473
  logger.warn(`[SourceAgent:${this.tool.name}] Tool returned error (attempt ${this.attempts}/${this.config.maxRetries}): ${errorMsg}`);
10474
+ logger.file(`[SourceAgent:${this.tool.name}] QUERY ERROR (attempt ${this.attempts}):`, errorMsg);
10418
10475
  if (this.streamBuffer.hasCallback()) {
10419
10476
  this.streamBuffer.write(`\u274C **Query failed:** ${errorMsg}
10420
10477
 
@@ -10428,6 +10485,7 @@ Analyze the error and try again with a corrected query.`;
10428
10485
  totalRowsMatched = result.metadata?.totalCount || result.count || resultData.length;
10429
10486
  querySucceeded = true;
10430
10487
  successfulQueries++;
10488
+ logger.file(`[SourceAgent:${this.tool.name}] QUERY RESULT (attempt ${this.attempts}):`, `${resultData.length} rows (${totalRowsMatched} total)`, "metadata:", JSON.stringify(result.metadata ?? {}));
10431
10489
  if (this.streamBuffer.hasCallback()) {
10432
10490
  const totalInfo = totalRowsMatched > resultData.length ? ` of ${totalRowsMatched} total` : "";
10433
10491
  this.streamBuffer.write(`\u2705 **${resultData.length} rows${totalInfo} from ${this.tool.name}**
@@ -10835,7 +10893,7 @@ Even if a table appears in the detailed schema above, search_schema returns samp
10835
10893
  const match = this.tool.id.match(/^(\w+)-[a-f0-9]+_/);
10836
10894
  if (!match) return "unknown";
10837
10895
  const raw = match[1].toLowerCase();
10838
- if (raw.startsWith("mssql")) return "mssql";
10896
+ if (raw.startsWith("mssql") || raw === "fabric") return "mssql";
10839
10897
  if (raw.startsWith("postgres")) return "postgres";
10840
10898
  if (raw.startsWith("mysql")) return "mysql";
10841
10899
  if (raw.startsWith("snowflake")) return "snowflake";
@@ -10861,7 +10919,7 @@ Even if a table appears in the detailed schema above, search_schema returns samp
10861
10919
 
10862
10920
  // src/userResponse/scripts/script-runner.ts
10863
10921
  import { spawn } from "child_process";
10864
- import * as path5 from "path";
10922
+ import * as path6 from "path";
10865
10923
  import * as os from "os";
10866
10924
 
10867
10925
  // src/userResponse/scripts/script-ipc.ts
@@ -11004,7 +11062,7 @@ var Semaphore = class {
11004
11062
  `Script execution at capacity (${this.max} running, ${this.waiters.length} queued). Try again shortly.`
11005
11063
  );
11006
11064
  }
11007
- return new Promise((resolve3) => this.waiters.push(resolve3));
11065
+ return new Promise((resolve4) => this.waiters.push(resolve4));
11008
11066
  }
11009
11067
  release() {
11010
11068
  const next = this.waiters.shift();
@@ -11022,8 +11080,8 @@ function resolveTsxBinary() {
11022
11080
  if (tsxBinaryPath) return tsxBinaryPath;
11023
11081
  try {
11024
11082
  const tsxPkg = __require.resolve("tsx/package.json");
11025
- const tsxDir = path5.dirname(tsxPkg);
11026
- tsxBinaryPath = path5.join(tsxDir, "dist", "cli.mjs");
11083
+ const tsxDir = path6.dirname(tsxPkg);
11084
+ tsxBinaryPath = path6.join(tsxDir, "dist", "cli.mjs");
11027
11085
  return tsxBinaryPath;
11028
11086
  } catch (err) {
11029
11087
  throw new Error(
@@ -11032,7 +11090,7 @@ function resolveTsxBinary() {
11032
11090
  }
11033
11091
  }
11034
11092
  function resolveBootstrapPath() {
11035
- return path5.resolve(__dirname, "userResponse/scripts/script-bootstrap.js");
11093
+ return path6.resolve(__dirname, "userResponse/scripts/script-bootstrap.js");
11036
11094
  }
11037
11095
  async function runScript(recipe, scriptPath, params, options) {
11038
11096
  const startedAt = Date.now();
@@ -11134,7 +11192,7 @@ FIX: pass every required parameter to execute_script, or give it a default in wr
11134
11192
  }
11135
11193
  }
11136
11194
  function executeInSubprocess(scriptPath, params, paramSchema, externalTools, streamBuffer, timeoutMs, signal, mode, analysisData, analysisDatasets, entities, appliedPolicies) {
11137
- return new Promise((resolve3) => {
11195
+ return new Promise((resolve4) => {
11138
11196
  const tsxBin = resolveTsxBinary();
11139
11197
  const bootstrap = resolveBootstrapPath();
11140
11198
  const contextToken = mintContextToken();
@@ -11190,7 +11248,7 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
11190
11248
  clearTimeout(timer);
11191
11249
  if (signal && onAbort) signal.removeEventListener("abort", onAbort);
11192
11250
  cleanup();
11193
- resolve3(r);
11251
+ resolve4(r);
11194
11252
  };
11195
11253
  const timer = setTimeout(() => {
11196
11254
  finish({
@@ -11540,6 +11598,7 @@ function codeOnly(source) {
11540
11598
  var BARE_CONST_RE = /\bconst\s+(\w+)\s*=\s*(\d{2,}(?:\.\d+)?)\s*[;,)]/g;
11541
11599
  var FMT_CALL_RE = /\bfmt\.\w+\(([^)]*)\)/g;
11542
11600
  var DIRECT_BARE_ARG_RE = /(?<![\w.\[])(\d{2,}(?:\.\d+)?)(?![\w.\]])/;
11601
+ var OBJECT_PROP_RE = /(?<![\w.])([A-Za-z_]\w*)\s*:\s*(\d{2,}(?:\.\d+)?)\s*[,}]/g;
11543
11602
  function interpolationContents(source) {
11544
11603
  const out = [];
11545
11604
  for (const tpl of source.matchAll(/`(?:\\.|[^`\\])*`/g)) {
@@ -11571,6 +11630,24 @@ function findHardcodedAggregates(source) {
11571
11630
  reason: `a bare number ("${m[1]}") is passed directly into a fmt.*() call \u2014 it will report this SAME figure on every future replay no matter how much data actually exists then. Compute it from the data instead (e.g. \`data.length\`, a value read from \`datasets\`, or a real SQL aggregate).`
11572
11631
  });
11573
11632
  }
11633
+ const propCandidates = [...code.matchAll(OBJECT_PROP_RE)].map((m) => ({ name: m[1], text: m[0].trim().replace(/[,}]$/, "") }));
11634
+ for (const c of propCandidates) {
11635
+ const accessPattern = `\\??\\.${c.name}\\b`;
11636
+ const accessRe = new RegExp(accessPattern);
11637
+ let flows = fmtArgLists.some((args) => accessRe.test(args));
11638
+ if (!flows) {
11639
+ const hop = code.match(new RegExp(`\\bconst\\s+(\\w+)\\s*=\\s*\\w+${accessPattern}`));
11640
+ if (hop) {
11641
+ const varRe = new RegExp(`\\b${hop[1]}\\b`);
11642
+ flows = fmtArgLists.some((args) => varRe.test(args));
11643
+ }
11644
+ }
11645
+ if (!flows) continue;
11646
+ violations.push({
11647
+ text: c.text,
11648
+ reason: `"${c.name}" is an object-literal property bound directly to a bare number, later read back via .${c.name} and passed to a fmt.*() call \u2014 it will report this SAME figure on every future replay no matter how much data actually exists then. Compute it from the data (e.g. \`data.length\`, a real SQL aggregate, or a value derived at read time), never type it in as a property value.`
11649
+ });
11650
+ }
11574
11651
  return violations;
11575
11652
  }
11576
11653
  function literalChunks(source) {
@@ -11688,7 +11765,7 @@ function resolveScriptRecipeStore(collections) {
11688
11765
 
11689
11766
  // src/userResponse/scripts/script-store.ts
11690
11767
  import * as fs6 from "fs";
11691
- import * as path6 from "path";
11768
+ import * as path7 from "path";
11692
11769
  import { createHash, randomBytes as randomBytes2 } from "crypto";
11693
11770
  var DEFAULT_STORE_DIR = "scripts-store";
11694
11771
  function stripAnalysisExport(body) {
@@ -11782,7 +11859,7 @@ export { getData };
11782
11859
  var ScriptStore = class {
11783
11860
  constructor(opts) {
11784
11861
  this.store = opts?.store ?? resolveScriptRecipeStore(opts?.collections) ?? null;
11785
- this.storeDir = opts?.baseDir || path6.join(process.cwd(), DEFAULT_STORE_DIR);
11862
+ this.storeDir = opts?.baseDir || path7.join(process.cwd(), DEFAULT_STORE_DIR);
11786
11863
  this.projectId = opts?.projectId;
11787
11864
  if (!this.store) {
11788
11865
  logger.warn("[ScriptStore] No script-recipes metadata store injected \u2014 script reuse disabled this run.");
@@ -11852,7 +11929,7 @@ var ScriptStore = class {
11852
11929
  if (version === row.version) {
11853
11930
  return this.rowToRecipe(row, this.readBody(row.fileBase));
11854
11931
  }
11855
- const archivePath = path6.join(this.archiveDir, `${row.fileBase}.v${version}.ts`);
11932
+ const archivePath = path7.join(this.archiveDir, `${row.fileBase}.v${version}.ts`);
11856
11933
  if (!fs6.existsSync(archivePath)) {
11857
11934
  logger.warn(`[ScriptStore] getVersion(${id}, v${version}) \u2014 archived body not found, recipe is now at v${row.version}`);
11858
11935
  return null;
@@ -11876,10 +11953,10 @@ var ScriptStore = class {
11876
11953
  }
11877
11954
  if (recipe.scriptBody) {
11878
11955
  const normalized = normalizeScriptBody(recipe.scriptBody);
11879
- const hash = this.hash(normalized);
11880
- if (hash !== recipe.bodyHash) {
11956
+ const hash2 = this.hash(normalized);
11957
+ if (hash2 !== recipe.bodyHash) {
11881
11958
  this.writeBody(recipe.fileBase, normalized);
11882
- recipe.bodyHash = hash;
11959
+ recipe.bodyHash = hash2;
11883
11960
  }
11884
11961
  }
11885
11962
  recipe.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -12167,7 +12244,7 @@ ${carried.trim()}
12167
12244
  /** Absolute path to the .ts body for a recipe (used by the runner/MainAgent). */
12168
12245
  getScriptPath(recipe) {
12169
12246
  const base = recipe.fileBase || this.toSlug(recipe.name);
12170
- return path6.join(this.storeDir, `${base}.ts`);
12247
+ return path7.join(this.storeDir, `${base}.ts`);
12171
12248
  }
12172
12249
  // ============================================
12173
12250
  // Internals
@@ -12353,7 +12430,7 @@ ${analysisBody.trim()}
12353
12430
  return true;
12354
12431
  }
12355
12432
  bodyPath(fileBase) {
12356
- return path6.join(this.storeDir, `${fileBase}.ts`);
12433
+ return path7.join(this.storeDir, `${fileBase}.ts`);
12357
12434
  }
12358
12435
  readBody(fileBase) {
12359
12436
  try {
@@ -12365,7 +12442,7 @@ ${analysisBody.trim()}
12365
12442
  }
12366
12443
  /** Directory holding superseded bodies. Dot-prefixed so IDEs/`ls` hide it. */
12367
12444
  get archiveDir() {
12368
- return path6.join(this.storeDir, ".versions");
12445
+ return path7.join(this.storeDir, ".versions");
12369
12446
  }
12370
12447
  /**
12371
12448
  * Archive a superseded body as `.versions/<fileBase>.v<n>.ts`.
@@ -12375,7 +12452,7 @@ ${analysisBody.trim()}
12375
12452
  */
12376
12453
  writeArchive(fileBase, version, body) {
12377
12454
  if (!fs6.existsSync(this.archiveDir)) fs6.mkdirSync(this.archiveDir, { recursive: true });
12378
- const finalPath = path6.join(this.archiveDir, `${fileBase}.v${version}.ts`);
12455
+ const finalPath = path7.join(this.archiveDir, `${fileBase}.v${version}.ts`);
12379
12456
  const tmpPath = `${finalPath}.tmp-${randomBytes2(4).toString("hex")}`;
12380
12457
  fs6.writeFileSync(tmpPath, body);
12381
12458
  fs6.renameSync(tmpPath, finalPath);
@@ -12393,7 +12470,7 @@ ${analysisBody.trim()}
12393
12470
  for (const f of fs6.readdirSync(this.archiveDir)) {
12394
12471
  if (!f.startsWith(prefix) || !f.endsWith(".ts")) continue;
12395
12472
  const suffix = f.slice(oldBase.length);
12396
- fs6.renameSync(path6.join(this.archiveDir, f), path6.join(this.archiveDir, `${newBase}${suffix}`));
12473
+ fs6.renameSync(path7.join(this.archiveDir, f), path7.join(this.archiveDir, `${newBase}${suffix}`));
12397
12474
  }
12398
12475
  } catch (err) {
12399
12476
  logger.warn(`[ScriptStore] failed to move archives ${oldBase} \u2192 ${newBase}: ${err}`);
@@ -12419,7 +12496,7 @@ ${analysisBody.trim()}
12419
12496
  const prefix = `${fileBase}.v`;
12420
12497
  for (const f of fs6.readdirSync(this.archiveDir)) {
12421
12498
  if (f.startsWith(prefix) && f.endsWith(".ts")) {
12422
- fs6.unlinkSync(path6.join(this.archiveDir, f));
12499
+ fs6.unlinkSync(path7.join(this.archiveDir, f));
12423
12500
  }
12424
12501
  }
12425
12502
  } catch (err) {
@@ -12704,7 +12781,7 @@ var WRITE_SCRIPT_TOOL_DEF = {
12704
12781
  };
12705
12782
  var RESOLVE_ENTITIES_TOOL_DEF = {
12706
12783
  name: "resolve_entities",
12707
- description: 'ALWAYS call this FIRST, before any source query, on every question. It resolves BOTH halves of what the question refers to: the things the user NAMED become exact database ids, and the things the question asks you to COMPUTE come back as the project\'s own formulas. Pass `concepts` for anything the question asks you to compute, total, forecast, rank or classify \u2014 named from the CONCEPT CATALOG in your system prompt, and including concepts not named literally in the question ("how much did we bill" is a revenue question). A definition returned here OVERRIDES the knowledge base, the schema, and your own reasoning: use its body as written, and treat every filter it states as required. Pass `mentions` for any proper noun or identifier the user referred to \u2014 customer/company/project names, people, places, invoice or document numbers. Pass the user\'s wording VERBATIM; do not clean it up, expand abbreviations, or correct a spelling. Misspellings and partial names are matched for you. Set a mention\'s `entityType` ONLY when the question itself says what the value is ("project 314023", "customer Acme Ltd", "invoice CHL/895/26-27"). DO NOT GUESS IT. A wrong guess is worse than none: the lookup is confined to that type, and if anything there matches even weakly you silently get the wrong record instead of the right one from another type. Leaving it off searches every type and returns the best candidate from each, labelled \u2014 so you can choose from what actually exists rather than from a guess. NEVER write a WHERE clause on a name you have not resolved here, and NEVER invent an id. Reading the result: use the top pick when it is an exact or high-scoring match. When it is flagged `ambiguous`, OR when the top score is low, READ `alternatives` before choosing \u2014 each is labelled with its `entityType`, and the right record is often there under a type you would not have guessed. `entityMap` comes back automatically for every type your mentions resolved to: it gives the id column to filter on and the table/column the name lives in. Filter on the id, never on the name. A mention in `recognized` is NOT indexed but IS locatable \u2014 values like invoice or GCN numbers change daily, so only where they live is registered. If it carries `searchSql`, RUN THAT QUERY AS GIVEN; otherwise use its `field` with the ready-made `match` predicate. Do not rewrite either comparison as `=`, because the stored data is irregular. A mention in `unresolved` is not in the index: say so for that item and still answer the rest, rather than dropping it or inventing an id. Call it again later in the turn if query results reveal another name you need an id for.',
12784
+ description: 'ALWAYS call this FIRST, before any source query, on every question. It resolves ONLY the things the user NAMED into exact database ids \u2014 proper nouns, identifiers, searchable values. It has no notion of formulas, computation, totals, or business logic \u2014 for anything the question asks you to compute, forecast, rank, or classify, use the separate concept-resolution tool instead, if one is available; never pass that kind of thing here. Pass `mentions` for any proper noun or identifier the user referred to \u2014 customer/company/project names, people, places, invoice or document numbers. Pass the user\'s wording VERBATIM; do not clean it up, expand abbreviations, or correct a spelling. Misspellings and partial names are matched for you. Set a mention\'s `entityType` ONLY when the question itself says what the value is ("project 314023", "customer Acme Ltd", "invoice CHL/895/26-27"). DO NOT GUESS IT. A wrong guess is worse than none: the lookup is confined to that type, and if anything there matches even weakly you silently get the wrong record instead of the right one from another type. Leaving it off searches every type and returns the best candidate from each, labelled \u2014 so you can choose from what actually exists rather than from a guess. NEVER write a WHERE clause on a name you have not resolved here, and NEVER invent an id. Reading the result: use the top pick when it is an exact or high-scoring match. When it is flagged `ambiguous`, OR when the top score is low, READ `alternatives` before choosing \u2014 each is labelled with its `entityType`, and the right record is often there under a type you would not have guessed. `entityMap` comes back automatically for every type your mentions resolved to: it gives the id column to filter on and the table/column the name lives in. Filter on the id, never on the name. A mention in `recognized` is NOT indexed but IS locatable \u2014 values like invoice or GCN numbers change daily, so only where they live is registered. If it carries `searchSql`, RUN THAT QUERY AS GIVEN; otherwise use its `field` with the ready-made `match` predicate. Do not rewrite either comparison as `=`, because the stored data is irregular. A mention in `unresolved` is not in the index: say so for that item and still answer the rest, rather than dropping it or inventing an id. Call it again later in the turn if query results reveal another name you need an id for.',
12708
12785
  input_schema: {
12709
12786
  type: "object",
12710
12787
  properties: {
@@ -12722,30 +12799,10 @@ var RESOLVE_ENTITIES_TOOL_DEF = {
12722
12799
  },
12723
12800
  required: ["text"]
12724
12801
  }
12725
- },
12726
- concepts: {
12727
- type: "array",
12728
- items: { type: "string" },
12729
- description: "Names from the CONCEPT CATALOG for anything the question asks you to compute, total, forecast, rank or classify \u2014 including when the concept is not named literally. Empty only when the question asks for no computation at all. Prerequisites come back automatically."
12730
12802
  }
12731
12803
  }
12732
12804
  }
12733
12805
  };
12734
- var RESOLVE_CONCEPTS_TOOL_DEF = {
12735
- name: "resolve_concepts",
12736
- description: 'Fetch the project\'s OWN definition of a business concept \u2014 the formula, filters and joins that define it \u2014 BEFORE computing that concept yourself. Call this whenever the question asks you to compute, calculate, total, forecast, or classify something, even when the concept is not named literally in the question. Pass names from the CONCEPT CATALOG in your system prompt; ask for the SPECIFIC name that fits, since a vague single word matches whichever entry is closest by string. Anything a concept depends on is returned automatically alongside it \u2014 you do not need to work out or request its prerequisites. A clear misspelling is corrected for you and reported in `corrected`. But when a name is too vague or too close to several catalog entries ("project", when the catalog holds project name / project budget / project type), NOTHING is resolved: you get `didYouMean` candidates instead. Those are candidates, NOT an answer \u2014 choose the one the question means and call again with that exact name, never assume the first. When a concept comes back, USE ITS BODY AS WRITTEN: it overrides whatever the schema, column names or your own reasoning suggest, and every filter it states is REQUIRED, not optional. Honour its `returns` contract too \u2014 if it declares a unit or grain (e.g. "per-currency, unconverted"), do not collapse across that unit without saying so. A name that comes back in `unmatched` has no definition here: derive it yourself and say plainly in your answer that you did.',
12737
- input_schema: {
12738
- type: "object",
12739
- properties: {
12740
- names: {
12741
- type: "array",
12742
- items: { type: "string" },
12743
- description: "Concept names from the CONCEPT CATALOG. Ask only for what this question needs \u2014 dependencies come back on their own."
12744
- }
12745
- },
12746
- required: ["names"]
12747
- }
12748
- };
12749
12806
  var EXECUTE_SCRIPT_TOOL_DEF = {
12750
12807
  name: "execute_script",
12751
12808
  description: "Run the current draft script in a subprocess and return { ok, sampleRows, executedQueries, error?, errorPhase? }. Call this after write_script. If ok is false, fix the draft with a fresh write_script (max 3 attempts per turn). If ok is true AND the sample rows answer the user question, proceed to your final analysis.",
@@ -12801,9 +12858,11 @@ var MainAgent = class {
12801
12858
  */
12802
12859
  this.resolvedEntities = [];
12803
12860
  /**
12804
- * Concept bodies resolved this turn (via resolve_entities' embedded concept
12805
- * lookup or a standalone resolve_concepts call) — retained the same way as
12806
- * resolvedEntities above, so they can be attached to the SourceAgent dispatch.
12861
+ * Concept bodies resolved this turn (currently: invariants only, via
12862
+ * loadConceptInvariants — concept resolution is no longer part of
12863
+ * resolve_entities or any other MainAgent-owned tool) — retained the same
12864
+ * way as resolvedEntities above, so they can be attached to the SourceAgent
12865
+ * dispatch.
12807
12866
  *
12808
12867
  * Without this, a concept resolved HERE (MainAgent's own tool loop) never
12809
12868
  * reaches SourceAgent at all: SourceAgent only gets the natural-language
@@ -12879,7 +12938,6 @@ var MainAgent = class {
12879
12938
  const entitySearchActive = this.entitySearchEnabled && entityCatalog.count > 0;
12880
12939
  if (entityCatalog.count) logger.info(`[MainAgent] Entity catalog: ${entityCatalog.count} type(s) \u2014 resolve_entities pinned to iteration 1`);
12881
12940
  const conceptCatalog = this.conceptsEnabled ? await this.loadConceptCatalog() : { text: "", count: 0 };
12882
- const conceptsActive = this.conceptsEnabled && conceptCatalog.count > 0;
12883
12941
  const conceptInvariants = this.conceptsEnabled ? await this.loadConceptInvariants() : { text: "", count: 0 };
12884
12942
  if (conceptCatalog.count) {
12885
12943
  logger.info(
@@ -12920,7 +12978,6 @@ var MainAgent = class {
12920
12978
  ...directToolDefs,
12921
12979
  ...workflowToolDefs,
12922
12980
  ...entitySearchActive ? [RESOLVE_ENTITIES_TOOL_DEF] : [],
12923
- ...conceptsActive ? [RESOLVE_CONCEPTS_TOOL_DEF] : [],
12924
12981
  ...this.scriptingEnabled ? [WRITE_SCRIPT_TOOL_DEF, EXECUTE_SCRIPT_TOOL_DEF, WRITE_ANALYSIS_TOOL_DEF] : [],
12925
12982
  // Only offered when there is a catalog to choose from; without one the
12926
12983
  // flow falls back to generateScriptComponents exactly as before.
@@ -12936,7 +12993,7 @@ var MainAgent = class {
12936
12993
  let sourceCallCounter = 0;
12937
12994
  const toolIdCallCounts = {};
12938
12995
  let selectedWorkflow;
12939
- const toolHandler = async (toolName, toolInput) => {
12996
+ const toolHandlerInner = async (toolName, toolInput) => {
12940
12997
  const workflow = this.workflows.find((w) => w.id === toolName);
12941
12998
  if (workflow) {
12942
12999
  return this.handleWorkflow(workflow, toolInput, (w) => {
@@ -12944,10 +13001,7 @@ var MainAgent = class {
12944
13001
  });
12945
13002
  }
12946
13003
  if (toolName === "resolve_entities") {
12947
- return this.handleResolveEntities(toolInput, userPrompt);
12948
- }
12949
- if (toolName === "resolve_concepts") {
12950
- return this.handleResolveConcepts(toolInput);
13004
+ return this.handleResolveEntities(toolInput);
12951
13005
  }
12952
13006
  if (toolName === "write_components") {
12953
13007
  if (!this.scriptingEnabled) return "Component authoring is not enabled for this request.";
@@ -12975,7 +13029,8 @@ var MainAgent = class {
12975
13029
  }
12976
13030
  const sourceInput = {
12977
13031
  intent: toolInput.intent || toolInput.query || JSON.stringify(toolInput),
12978
- aggregation: toolInput.aggregation || "raw"
13032
+ aggregation: toolInput.aggregation || "raw",
13033
+ concepts: Array.isArray(toolInput.concepts) ? toolInput.concepts.filter((c) => typeof c === "string") : void 0
12979
13034
  };
12980
13035
  logger.info(`[MainAgent] Dispatching SourceAgent for "${externalTool.name}" | intent: "${sourceInput.intent}"`);
12981
13036
  const preResolved = await this.preResolveSchema(externalTool, sourceInput.intent);
@@ -13004,10 +13059,20 @@ var MainAgent = class {
13004
13059
  if (entityBlock) {
13005
13060
  logger.info(`[MainAgent] Attached ${this.resolvedEntities.length} resolved entity id(s) to the dispatch: ${this.resolvedEntities.map((e) => `${e.entityType}=${e.instanceId}`).join(", ")}`);
13006
13061
  }
13007
- const conceptBlock = this.buildResolvedConceptsBlock();
13062
+ const conceptBlock = this.buildResolvedConceptsBlock(sourceInput.concepts);
13063
+ const wantedConcepts = new Set(sourceInput.concepts ?? []);
13064
+ const attachedConcepts = this.resolvedConcepts.filter((c) => c.alwaysInclude || wantedConcepts.has(c.name));
13008
13065
  if (conceptBlock) {
13009
- logger.info(`[MainAgent] Attached ${this.resolvedConcepts.length} resolved concept(s) to the dispatch: ${this.resolvedConcepts.map((c) => c.name).join(", ")}`);
13010
- }
13066
+ logger.info(`[MainAgent] Attached ${attachedConcepts.length} resolved concept(s) to the dispatch: ${attachedConcepts.map((c) => c.name).join(", ")}`);
13067
+ }
13068
+ logger.file(
13069
+ `[DISPATCH] ${externalTool.name} | sourceInput.concepts requested:`,
13070
+ JSON.stringify(sourceInput.concepts ?? []),
13071
+ "\n entityBlock:",
13072
+ entityBlock || "(none)",
13073
+ "\n conceptBlock:",
13074
+ conceptBlock || "(none)"
13075
+ );
13011
13076
  const result = await sourceAgent.execute({
13012
13077
  ...sourceInput,
13013
13078
  intent: sourceInput.intent + entityBlock + conceptBlock
@@ -13036,7 +13101,7 @@ var MainAgent = class {
13036
13101
  const entry = sourceAgent.getTraceEntry();
13037
13102
  if (entry) {
13038
13103
  entry.result = formatted;
13039
- if (this.resolvedConcepts.length) entry.resolvedConcepts = [...this.resolvedConcepts];
13104
+ if (attachedConcepts.length) entry.resolvedConcepts = [...attachedConcepts];
13040
13105
  entry.aggregation = sourceInput.aggregation;
13041
13106
  }
13042
13107
  } catch (traceErr) {
@@ -13044,8 +13109,21 @@ var MainAgent = class {
13044
13109
  }
13045
13110
  return formatted;
13046
13111
  };
13112
+ const toolHandler = async (toolName, toolInput) => {
13113
+ logger.file(`[TOOL CALL] ${toolName} | input:`, JSON.stringify(toolInput));
13114
+ try {
13115
+ const result = await toolHandlerInner(toolName, toolInput);
13116
+ logger.file(`[TOOL RESULT] ${toolName} | result:`, typeof result === "string" ? result.slice(0, 4e3) : JSON.stringify(result).slice(0, 4e3));
13117
+ return result;
13118
+ } catch (err) {
13119
+ logger.file(`[TOOL ERROR] ${toolName} | error:`, err instanceof Error ? err.message : String(err));
13120
+ throw err;
13121
+ }
13122
+ };
13047
13123
  let text = "";
13048
13124
  const firstIterationToolChoice = entitySearchActive ? { type: "tool", name: "resolve_entities" } : (this.forkMode || this.editMode) && this.scriptingEnabled ? { type: "any" } : void 0;
13125
+ const conceptToolRegistered = this.externalTools.some((t) => t.id === "concept");
13126
+ const secondIterationToolChoice = conceptToolRegistered ? { type: "tool", name: "concept" } : void 0;
13049
13127
  try {
13050
13128
  text = await LLM.streamWithTools(
13051
13129
  {
@@ -13064,7 +13142,8 @@ var MainAgent = class {
13064
13142
  apiKey: apiKey || this.config.apiKey,
13065
13143
  partial: streamCallback,
13066
13144
  signal,
13067
- ...firstIterationToolChoice ? { firstIterationToolChoice } : {}
13145
+ ...firstIterationToolChoice ? { firstIterationToolChoice } : {},
13146
+ ...secondIterationToolChoice ? { secondIterationToolChoice } : {}
13068
13147
  },
13069
13148
  this.config.maxIterations
13070
13149
  );
@@ -13235,24 +13314,6 @@ ${catalog}`,
13235
13314
  const hardcodedComponentText = findHardcodedComponentText(body);
13236
13315
  const hardcodedEntityNames = findHardcodedEntityNames(body, this.resolvedEntities.map((e) => e.displayName));
13237
13316
  const componentViolations = [...hardcodedAggregates, ...hardcodedComponentText, ...hardcodedEntityNames];
13238
- if (componentViolations.length > 0) {
13239
- const named = componentViolations.map((v) => `"${v.text}"`).join(", ");
13240
- const repeatedTexts = componentViolations.map((v) => v.text).filter((t) => this.lastComponentsViolationTexts.includes(t));
13241
- this.lastComponentsViolationTexts = componentViolations.map((v) => v.text);
13242
- logger.warn(`[MainAgent] write_components rejected \u2014 ${componentViolations.length} hardcoded value(s): ${named}`);
13243
- const lines2 = componentViolations.map((v) => ` \u2022 "${v.text}" \u2014 ${v.reason}`).join("\n");
13244
- let message = `write_components rejected: getComponents hardcodes ${componentViolations.length} value(s) that belong to this run's data, not fixed text:
13245
- ${lines2}
13246
-
13247
- FIX: derive every such value from \`data\`/\`datasets\` at render time via \`fmt\` (e.g. \`Total: \${fmt.money(datasets.summary[0].total)}\`), never a number typed directly into the string.`;
13248
- if (repeatedTexts.length > 0) {
13249
- const repeatedList = repeatedTexts.map((t) => `"${t}"`).join(", ");
13250
- message += `
13251
-
13252
- STILL UNCHANGED FROM THE LAST ATTEMPT: ${repeatedList}. Restating the same reason won't help a second time \u2014 this is almost certainly a parameter or an already-resolved value: if it's an id, use \`params.<name>\` directly; if it's a name or a period, read it from \`data\`/\`datasets\`/\`fmt.range\`. Replace the literal itself, not the wording around it.`;
13253
- }
13254
- return message;
13255
- }
13256
13317
  const recipeId = this.scriptState.recipeId;
13257
13318
  const draft = recipeId ? await this.scriptStore.get(recipeId) : null;
13258
13319
  if (!recipeId || !draft) return "Draft was lost from the store. Call write_script again.";
@@ -13713,12 +13774,19 @@ The verified script is intact. You can still call write_analysis and write_compo
13713
13774
  RESOLVED ENTITIES \u2014 these were looked up already. Filter on the ids; do NOT search for them by name. Where a name must appear in output or a comparison, use the "name in DB" spelling EXACTLY as given. Where a READY-MADE SCOPE is given, paste it into the query as-is instead of writing your own join/city-match/lookup for the same thing:
13714
13775
  ${lines.join("\n")}`;
13715
13776
  }
13716
- /** Dedupe-and-retain helper shared by resolveConceptsForTurn and handleResolveConcepts. */
13717
- retainResolvedConcepts(resolved) {
13777
+ /**
13778
+ * Dedupe-and-retain helper shared by loadConceptInvariants and the generic
13779
+ * direct-tool `resolvedConcepts` opt-in hook. `alwaysInclude` marks entries
13780
+ * that buildResolvedConceptsBlock must attach to EVERY dispatch regardless
13781
+ * of that dispatch's `concepts` param — true only for invariants, which are
13782
+ * meant to apply everywhere by definition. Concepts retained without it
13783
+ * (e.g. via the `concept` tool's `get`) are scoped per-dispatch instead.
13784
+ */
13785
+ retainResolvedConcepts(resolved, opts) {
13718
13786
  for (const c of resolved) {
13719
13787
  if (!c?.name || !c?.body) continue;
13720
13788
  if (this.resolvedConcepts.some((existing) => existing.name === c.name)) continue;
13721
- this.resolvedConcepts.push({ name: String(c.name), kind: String(c.kind ?? ""), body: String(c.body) });
13789
+ this.resolvedConcepts.push({ name: String(c.name), kind: String(c.kind ?? ""), body: String(c.body), alwaysInclude: !!opts?.alwaysInclude });
13722
13790
  }
13723
13791
  }
13724
13792
  /**
@@ -13726,10 +13794,29 @@ ${lines.join("\n")}`;
13726
13794
  * entity ids — appended to the dispatch intent so SourceAgent's own,
13727
13795
  * independent LLM call receives the resolved definitions verbatim rather
13728
13796
  * than only MainAgent's paraphrased intent text.
13797
+ *
13798
+ * Unlike buildResolvedEntitiesBlock, non-invariant entries here are SCOPED
13799
+ * to `names` (this dispatch's SourceToolInput.concepts) rather than
13800
+ * dumping the whole-turn `this.resolvedConcepts` accumulator into every
13801
+ * dispatch. A concept body is phrased as an authoritative override
13802
+ * ("overrides the schema... your own reasoning") — fetching several via
13803
+ * `get` to compare/discard, or a turn needing different concepts for
13804
+ * different sub-questions, must not leak an irrelevant concept's filters
13805
+ * into an unrelated dispatch. An entry not named in `names` and not
13806
+ * `alwaysInclude` is left out — a concept the model forgot to name is a
13807
+ * visible, fixable miss; one that leaks in uninvited is not.
13808
+ *
13809
+ * `alwaysInclude` entries (invariants — see loadConceptInvariants) bypass
13810
+ * `names` entirely and attach to every dispatch regardless, same as before
13811
+ * this method took a `names` param — they are meant to apply everywhere,
13812
+ * not just when the model happens to ask for them.
13729
13813
  */
13730
- buildResolvedConceptsBlock() {
13814
+ buildResolvedConceptsBlock(names) {
13731
13815
  if (!this.resolvedConcepts.length) return "";
13732
- const lines = this.resolvedConcepts.map((c) => `- ${c.name}${c.kind ? ` (${c.kind})` : ""}: ${c.body}`);
13816
+ const wanted = new Set(names ?? []);
13817
+ const scoped = this.resolvedConcepts.filter((c) => c.alwaysInclude || wanted.has(c.name));
13818
+ if (!scoped.length) return "";
13819
+ const lines = scoped.map((c) => `- ${c.name}${c.kind ? ` (${c.kind})` : ""}: ${c.body}`);
13733
13820
  return `
13734
13821
 
13735
13822
  RESOLVED CONCEPTS \u2014 authoritative definitions for this turn. Use each body EXACTLY as written; it overrides the schema, the knowledge base, and your own reasoning:
@@ -13744,11 +13831,21 @@ ${lines.join("\n")}`;
13744
13831
  return !!this.config.collections?.["concepts"]?.["resolve"];
13745
13832
  }
13746
13833
  /**
13747
- * Concept names for the system prompt. Names ONLY — the body comes back from
13834
+ * The concept catalog for the system prompt: one line per concept — name,
13835
+ * aliases, and what it is for. No bodies — the body comes back from
13748
13836
  * resolve_concepts once the LLM has named one, exactly as with the entity
13749
13837
  * catalog. Putting bodies here would rebuild the always-on context problem
13750
13838
  * concepts exist to solve.
13751
13839
  *
13840
+ * The description is what makes the list usable. It used to be names only,
13841
+ * which asked the model to judge from a name alone whether `ref-data null
13842
+ * sentinel` bears on the question in front of it — and a wrong judgement is
13843
+ * silent, since it simply does not fetch. A line of prose per concept costs
13844
+ * a few tokens and is read rather than matched.
13845
+ *
13846
+ * Aliases stay, because they are how a user is likely to phrase it, and
13847
+ * showing them is what stops the LLM inventing a near-miss name.
13848
+ *
13752
13849
  * Not cached, for the same reason loadEntityCatalog isn't: unchanged rows
13753
13850
  * render byte-identical text so the prompt-cache prefix still hits, and a
13754
13851
  * process cache would only buy a window where a new concept is invisible.
@@ -13761,7 +13858,8 @@ ${lines.join("\n")}`;
13761
13858
  const rows = res?.data || [];
13762
13859
  const text = rows.map((r) => {
13763
13860
  const aka = r.aliases?.length ? ` (also: ${r.aliases.join(", ")})` : "";
13764
- return `- ${r.name}${aka}`;
13861
+ const what = r.description?.trim() ? ` \u2014 ${r.description.trim()}` : "";
13862
+ return `- ${r.name}${aka}${what}`;
13765
13863
  }).join("\n");
13766
13864
  return { text, count: rows.length };
13767
13865
  } catch (err) {
@@ -13779,10 +13877,14 @@ ${lines.join("\n")}`;
13779
13877
  * Bodies ARE included here — unlike the catalog — because there is nothing to
13780
13878
  * request: being unrequestable is what makes them invariants.
13781
13879
  *
13782
- * Also retained into resolvedConcepts (same as an explicit resolve_concepts
13783
- * call) so these bodies reach SourceAgent's dispatch too — an invariant is
13784
- * meant to apply everywhere, not just wherever the model happens to ask for
13785
- * something whose `requires` chain reaches it.
13880
+ * Also retained into resolvedConcepts so these bodies reach SourceAgent's
13881
+ * dispatch too — an invariant is meant to apply everywhere, not just
13882
+ * wherever the model happens to ask for something whose `requires` chain
13883
+ * reaches it. This is now the ONLY way a concept body reaches
13884
+ * resolvedConcepts/SourceAgent's dispatch automatically — a non-invariant
13885
+ * concept fetched through a project's own concept-lookup tool (if any) is
13886
+ * NOT retained here, and will not reach a later SourceAgent dispatch unless
13887
+ * the model itself restates it in the intent it writes.
13786
13888
  */
13787
13889
  async loadConceptInvariants() {
13788
13890
  const fn = this.config.collections?.["concepts"]?.["invariants"];
@@ -13790,7 +13892,7 @@ ${lines.join("\n")}`;
13790
13892
  try {
13791
13893
  const res = await fn({});
13792
13894
  const rows = res?.data || [];
13793
- this.retainResolvedConcepts(rows);
13895
+ this.retainResolvedConcepts(rows, { alwaysInclude: true });
13794
13896
  const text = rows.map((r) => `### ${r.name}
13795
13897
  ${r.body}`).join("\n\n");
13796
13898
  return { text, count: rows.length };
@@ -13799,161 +13901,11 @@ ${r.body}`).join("\n\n");
13799
13901
  return { text: "", count: 0 };
13800
13902
  }
13801
13903
  }
13802
- /**
13803
- * Resolve the concepts for a turn, with a SERVER-SIDE FLOOR.
13804
- *
13805
- * Pinning a tool guarantees it is called; it does not guarantee the model
13806
- * fills in the arguments. Observed: a question containing the exact words
13807
- * "revenue forecast" produced a call with no concepts at all, and the agent
13808
- * then used an incomplete copy of the formula from the knowledge base — the
13809
- * resulting query filtered a customer id against a project column and
13810
- * returned zero rows on every attempt.
13811
- *
13812
- * So when the model names nothing, scan the question itself for catalog names
13813
- * and aliases on word boundaries. Precise (a literal name is evidence, not a
13814
- * guess) and asymmetric in the safe direction: an unneeded definition costs a
13815
- * few hundred ignorable tokens, a missing one costs a wrong answer.
13816
- */
13817
- /**
13818
- * Returns the tool-facing fields to spread into the resolve_entities JSON
13819
- * response, PLUS a `_trace` field with everything the trace needs that the
13820
- * model does not (asked/viaFloor/unmatched). Callers must pull `_trace` out
13821
- * before spreading the rest into what the model sees — see handleResolveEntities.
13822
- */
13823
- async resolveConceptsForTurn(asked, userPrompt) {
13824
- const resolveFn = this.config.collections?.["concepts"]?.["resolve"];
13825
- if (!resolveFn) return {};
13826
- let names = [...asked];
13827
- let viaFloor = [];
13828
- if (!names.length && userPrompt) {
13829
- try {
13830
- const catFn = this.config.collections?.["concepts"]?.["catalog"];
13831
- const cat = catFn ? await catFn({}) : null;
13832
- const haystack = ` ${userPrompt.toLowerCase().replace(/\s+/g, " ")} `;
13833
- for (const row of cat?.data || []) {
13834
- const labels = [row.name, ...row.aliases || []].filter(Boolean).sort((a, b) => b.length - a.length);
13835
- for (const label of labels) {
13836
- const l = String(label).toLowerCase().trim();
13837
- if (l.length < 4) continue;
13838
- if (haystack.includes(` ${l} `) || haystack.includes(` ${l},`) || haystack.includes(` ${l}?`) || haystack.includes(` ${l}.`)) {
13839
- names.push(row.name);
13840
- viaFloor.push(row.name);
13841
- break;
13842
- }
13843
- }
13844
- }
13845
- } catch (err) {
13846
- logger.warn(`[MainAgent] concept floor scan failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
13847
- }
13848
- }
13849
- if (!names.length) return {};
13850
- try {
13851
- const res = await resolveFn({ names });
13852
- const resolved = res?.resolved || [];
13853
- const impliedKeys = new Set((res?.implied || []).map((s) => s.toLowerCase()));
13854
- this.retainResolvedConcepts(resolved);
13855
- logger.info(
13856
- `[MainAgent] concepts (in resolve_entities): asked ${asked.length}` + (viaFloor.length ? `, ${viaFloor.length} found in the question text (${viaFloor.join(", ")})` : "") + ` \u2192 ${resolved.length} returned` + (res?.implied?.length ? `, ${res.implied.length} via requires` : "") + (res?.corrected?.length ? `, corrected: ${res.corrected.map((c) => `${c.asked}\u2192${c.matched}`).join(", ")}` : "") + (res?.suggestions?.length ? `, ambiguous: ${res.suggestions.map((s) => s.asked).join(", ")}` : "")
13857
- );
13858
- const _trace = {
13859
- asked,
13860
- viaFloor,
13861
- returned: resolved.map((c) => ({
13862
- name: c.name,
13863
- kind: c.kind,
13864
- definition: c.body,
13865
- ...c.returns && Object.keys(c.returns).length ? { returns: c.returns } : {},
13866
- ...impliedKeys.has(String(c.name).toLowerCase()) ? { viaRequires: true } : {}
13867
- })),
13868
- implied: res?.implied || [],
13869
- unmatched: res?.unmatched || [],
13870
- corrected: res?.corrected || [],
13871
- suggestions: res?.suggestions || []
13872
- };
13873
- if (!resolved.length) return { _trace };
13874
- return {
13875
- concepts: resolved.map((c) => ({
13876
- name: c.name,
13877
- kind: c.kind,
13878
- definition: c.body,
13879
- ...c.returns && Object.keys(c.returns).length ? { returns: c.returns } : {},
13880
- ...impliedKeys.has(String(c.name).toLowerCase()) ? { viaRequires: true } : {}
13881
- })),
13882
- ...res?.corrected?.length ? { conceptsCorrected: res.corrected } : {},
13883
- ...res?.suggestions?.length ? { conceptsDidYouMean: res.suggestions } : {},
13884
- conceptNote: "These definitions are AUTHORITATIVE. Use each body as written \u2014 it overrides the knowledge base, the column names in the schema, and your own reasoning, and every filter it states is required. Entries marked viaRequires are prerequisites returned automatically; they apply just as much as the ones asked for." + (viaFloor.length ? ` Included because the question mentions them: ${viaFloor.join(", ")}.` : ""),
13885
- _trace
13886
- };
13887
- } catch (err) {
13888
- logger.warn(`[MainAgent] concept resolution inside resolve_entities failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
13889
- return { _trace: { asked, viaFloor, error: err instanceof Error ? err.message : String(err) } };
13890
- }
13891
- }
13892
13904
  /**
13893
13905
  * Never throws: a concept-store outage must degrade to "no definition found"
13894
13906
  * and leave the agent deriving the formula as it does today, not take the
13895
13907
  * whole answer down.
13896
13908
  */
13897
- async handleResolveConcepts(toolInput) {
13898
- const fn = this.config.collections?.["concepts"]?.["resolve"];
13899
- const names = Array.isArray(toolInput?.names) ? toolInput.names.filter(Boolean) : [];
13900
- if (!fn) {
13901
- return JSON.stringify({ concepts: [], unmatched: names, note: "Concept definitions are not available; derive from the schema and say so." });
13902
- }
13903
- if (!names.length) {
13904
- return JSON.stringify({ concepts: [], unmatched: [], note: "No concept names given \u2014 continue." });
13905
- }
13906
- try {
13907
- const res = await fn({ names });
13908
- const resolved = res?.resolved || [];
13909
- const impliedKeys = new Set((res?.implied || []).map((s) => s.toLowerCase()));
13910
- this.retainResolvedConcepts(resolved);
13911
- logger.info(
13912
- `[MainAgent] resolve_concepts: asked ${names.length} \u2192 ${resolved.length} returned` + (res?.implied?.length ? `, ${res.implied.length} pulled in as dependencies` : "") + (res?.corrected?.length ? `, corrected: ${res.corrected.map((c) => `${c.asked}\u2192${c.matched}`).join(", ")}` : "") + (res?.suggestions?.length ? `, ambiguous: ${res.suggestions.map((s) => s.asked).join(", ")}` : "") + (res?.unmatched?.length ? `, unmatched: ${res.unmatched.join(", ")}` : "")
13913
- );
13914
- const suggestions = res?.suggestions || [];
13915
- const corrected = res?.corrected || [];
13916
- const notes = [];
13917
- if (resolved.length) notes.push("Use these definitions VERBATIM \u2014 every filter stated is required. Entries marked viaRequires are prerequisites returned automatically; they apply just as much as the ones you asked for.");
13918
- if (corrected.length) notes.push(`Read as a corrected spelling: ${corrected.map((c) => `"${c.asked}" \u2192 "${c.matched}"`).join(", ")}. If that is not what you meant, ask again with the exact catalog name.`);
13919
- if (suggestions.length) notes.push(`No confident match for ${suggestions.map((s) => `"${s.asked}"`).join(", ")} \u2014 these are CANDIDATES, not answers. Pick the one the question actually means and call resolve_concepts again with that exact name; do NOT assume the first.`);
13920
- if (!resolved.length && !suggestions.length) notes.push("No definition on file \u2014 derive it yourself and say so in your answer.");
13921
- const seenByLLM = {
13922
- concepts: resolved.map((c) => ({
13923
- name: c.name,
13924
- kind: c.kind,
13925
- definition: c.body,
13926
- ...c.returns && Object.keys(c.returns).length ? { returns: c.returns } : {},
13927
- ...impliedKeys.has(String(c.name).toLowerCase()) ? { viaRequires: true } : {}
13928
- })),
13929
- ...corrected.length ? { corrected } : {},
13930
- ...suggestions.length ? { didYouMean: suggestions } : {},
13931
- unmatched: res?.unmatched || [],
13932
- note: notes.join(" ")
13933
- };
13934
- try {
13935
- if (this.config.trace) {
13936
- this.config.trace.concepts.calls.push({
13937
- via: "resolve_concepts",
13938
- asked: names,
13939
- implied: res?.implied || [],
13940
- returned: seenByLLM.concepts,
13941
- ...corrected.length ? { corrected } : {},
13942
- ...suggestions.length ? { suggestions } : {},
13943
- unmatched: res?.unmatched || [],
13944
- note: notes.join(" ")
13945
- });
13946
- }
13947
- } catch (traceErr) {
13948
- logger.warn(`[MainAgent] Trace capture failed for resolve_concepts (non-blocking): ${traceErr}`);
13949
- }
13950
- return JSON.stringify(seenByLLM);
13951
- } catch (err) {
13952
- const msg = err instanceof Error ? err.message : String(err);
13953
- logger.warn(`[MainAgent] resolve_concepts failed (non-blocking): ${msg}`);
13954
- return JSON.stringify({ concepts: [], unmatched: names, note: "Concept lookup failed; derive from the schema and say so." });
13955
- }
13956
- }
13957
13909
  /**
13958
13910
  * Compact catalog (type + gloss) for the system prompt. This is what lets the
13959
13911
  * LLM NAME a concept instead of guessing at our vocabulary — it can infer
@@ -13984,20 +13936,18 @@ ${r.body}`).join("\n\n");
13984
13936
  * Never throws: a resolver outage must degrade to "nothing resolved" and
13985
13937
  * leave the agent exactly as it was, not take the whole answer down.
13986
13938
  */
13987
- async handleResolveEntities(toolInput, userPrompt) {
13939
+ async handleResolveEntities(toolInput) {
13988
13940
  const fn = this.config.collections?.["entity-search"]?.["resolveEntities"];
13989
13941
  const mentions = Array.isArray(toolInput?.mentions) ? toolInput.mentions.filter((m) => m?.text) : [];
13990
13942
  const entityTypes = Array.isArray(toolInput?.entityTypes) ? toolInput.entityTypes.filter(Boolean) : [];
13991
- const askedConcepts = Array.isArray(toolInput?.concepts) ? toolInput.concepts.filter(Boolean) : [];
13992
- const { _trace: conceptTrace, ...conceptBlock } = await this.resolveConceptsForTurn(askedConcepts, userPrompt);
13993
- if (conceptTrace) {
13994
- try {
13995
- if (this.config.trace) {
13996
- this.config.trace.concepts.calls.push({ via: "resolve_entities", ...conceptTrace });
13997
- }
13998
- } catch (traceErr) {
13999
- logger.warn(`[MainAgent] Trace capture failed for concepts-in-resolve_entities (non-blocking): ${traceErr}`);
14000
- }
13943
+ if (this.streamBuffer.hasCallback()) {
13944
+ const named = mentions.map((m) => `"${m.text}"`).join(", ");
13945
+ this.streamBuffer.write(`
13946
+
13947
+ \u{1F50D} **Resolving entities${named ? `: ${named}` : ""}...**
13948
+
13949
+ `);
13950
+ await streamDelay();
14001
13951
  }
14002
13952
  const traceEarlyReturn = (note) => {
14003
13953
  try {
@@ -14010,23 +13960,48 @@ ${r.body}`).join("\n\n");
14010
13960
  };
14011
13961
  if (!fn) {
14012
13962
  traceEarlyReturn("Entity search unavailable");
14013
- return JSON.stringify({ resolved: [], unresolved: [], entityMap: {}, ...conceptBlock, note: "Entity search is not available; proceed using the schema." });
13963
+ if (this.streamBuffer.hasCallback()) {
13964
+ this.streamBuffer.write(`\u26A0\uFE0F **Entity search unavailable** \u2014 proceeding on schema alone.
13965
+
13966
+ `);
13967
+ }
13968
+ return JSON.stringify({ resolved: [], unresolved: [], entityMap: {}, note: "Entity search is not available; proceed using the schema." });
14014
13969
  }
14015
13970
  if (!mentions.length && !entityTypes.length) {
14016
13971
  traceEarlyReturn("Nothing named in the question to resolve");
13972
+ if (this.streamBuffer.hasCallback()) {
13973
+ this.streamBuffer.write(`\u2705 **Entity search:** nothing to resolve for this question.
13974
+
13975
+ `);
13976
+ }
14017
13977
  return JSON.stringify({
14018
13978
  resolved: [],
14019
13979
  unresolved: [],
14020
13980
  entityMap: {},
14021
- ...conceptBlock,
14022
- note: conceptBlock.concepts?.length ? "The question named nothing to look up, but the definitions below apply \u2014 use them as written." : "Nothing to resolve for this question \u2014 continue."
13981
+ note: "Nothing to resolve for this question \u2014 continue."
14023
13982
  });
14024
13983
  }
14025
13984
  try {
14026
13985
  const res = await fn({ mentions, entityTypes });
14027
13986
  logger.info(
14028
- `[MainAgent] resolve_entities: ${res?.resolved?.length ?? 0} resolved, ${res?.unresolved?.length ?? 0} unresolved, ${Object.keys(res?.entityMap || {}).length} concept(s)`
13987
+ `[MainAgent] resolve_entities: ${res?.resolved?.length ?? 0} resolved, ${res?.unresolved?.length ?? 0} unresolved, ${Object.keys(res?.entityMap || {}).length} type(s)`
14029
13988
  );
13989
+ if (this.streamBuffer.hasCallback()) {
13990
+ const resolvedCount = res?.resolved?.length ?? 0;
13991
+ const unresolvedCount = res?.unresolved?.length ?? 0;
13992
+ const names = (Array.isArray(res?.resolved) ? res.resolved : []).map((r) => {
13993
+ const label = r?.displayName || r?.mention;
13994
+ if (!label) return null;
13995
+ return r?.entityType ? `${label} (${r.entityType})` : label;
13996
+ }).filter(Boolean).join(", ");
13997
+ const unresolvedNames = (Array.isArray(res?.unresolved) ? res.unresolved : []).map((u) => typeof u === "string" ? u : u?.text).filter(Boolean).join(", ");
13998
+ let line = `\u2705 **Entity search:** ${resolvedCount} resolved`;
13999
+ if (names) line += ` (${names})`;
14000
+ if (unresolvedCount) line += `, ${unresolvedCount} unresolved${unresolvedNames ? ` (${unresolvedNames})` : ""}`;
14001
+ this.streamBuffer.write(`${line}
14002
+
14003
+ `);
14004
+ }
14030
14005
  const TIE_EPSILON = 0.05;
14031
14006
  const key = (e) => `${e.entityType}::${e.instanceId}`;
14032
14007
  const add = (mention, c) => {
@@ -14062,7 +14037,7 @@ ${r.body}`).join("\n\n");
14062
14037
  attrs: r.attrs && typeof r.attrs === "object" ? r.attrs : void 0
14063
14038
  });
14064
14039
  }
14065
- const seenByLLM = { ...res, ...conceptBlock };
14040
+ const seenByLLM = { ...res };
14066
14041
  try {
14067
14042
  if (this.config.trace) {
14068
14043
  this.config.trace.entityResolution.calls.push({ mentions, entityTypes, ...seenByLLM });
@@ -14074,6 +14049,13 @@ ${r.body}`).join("\n\n");
14074
14049
  } catch (err) {
14075
14050
  const msg = err instanceof Error ? err.message : String(err);
14076
14051
  logger.warn(`[MainAgent] resolve_entities failed (non-blocking): ${msg}`);
14052
+ if (this.streamBuffer.hasCallback()) {
14053
+ this.streamBuffer.write(`
14054
+
14055
+ \u274C **Entity search failed:** ${msg}
14056
+
14057
+ `);
14058
+ }
14077
14059
  try {
14078
14060
  if (this.config.trace) {
14079
14061
  this.config.trace.entityResolution.calls.push({
@@ -14092,8 +14074,7 @@ ${r.body}`).join("\n\n");
14092
14074
  resolved: [],
14093
14075
  entityMap: {},
14094
14076
  error: msg,
14095
- unresolved: mentions.map((m) => m.text),
14096
- ...conceptBlock
14077
+ unresolved: mentions.map((m) => m.text)
14097
14078
  });
14098
14079
  }
14099
14080
  }
@@ -14186,7 +14167,7 @@ ${lines.join("\n")}`;
14186
14167
  if (this.streamBuffer.hasCallback()) {
14187
14168
  this.streamBuffer.write(`
14188
14169
 
14189
- \u{1F527} **Running ${tool.name}...**
14170
+ \u{1F527} **Running ${describeDirectToolCall(tool, toolInput)}...**
14190
14171
 
14191
14172
  `);
14192
14173
  await streamDelay();
@@ -14197,19 +14178,24 @@ ${lines.join("\n")}`;
14197
14178
  if (result && result.error) {
14198
14179
  const errorMsg = typeof result.error === "string" ? result.error : JSON.stringify(result.error);
14199
14180
  logger.warn(`[MainAgent] Direct tool "${tool.name}" returned error: ${errorMsg}`);
14181
+ this.config.trace?.directTools.calls.push({ toolId: tool.id, input: toolInput, error: errorMsg, durationMs: executionTimeMs });
14200
14182
  return `\u274C Tool "${tool.name}" error: ${errorMsg}`;
14201
14183
  }
14184
+ if (Array.isArray(result?.resolvedConcepts)) {
14185
+ this.retainResolvedConcepts(result.resolvedConcepts, { alwaysInclude: true });
14186
+ }
14202
14187
  const resultData = result.data || result;
14203
14188
  const rowCount = Array.isArray(resultData) ? resultData.length : 1;
14204
14189
  if (this.streamBuffer.hasCallback()) {
14205
- this.streamBuffer.write(`\u2705 **${tool.name}** completed (${rowCount} results, ${executionTimeMs}ms)
14190
+ this.streamBuffer.write(`\u2705 **${describeDirectToolCall(tool, toolInput)}** completed (${rowCount} results, ${executionTimeMs}ms)
14206
14191
 
14207
14192
  `);
14208
14193
  }
14194
+ const isConceptTool = tool.id === "concept";
14209
14195
  const formattedResult = formatToolResultForLLM(result, {
14210
14196
  toolName: tool.name,
14211
- maxRows: 5,
14212
- maxCharsPerField: 200
14197
+ maxRows: isConceptTool ? 25 : 5,
14198
+ maxCharsPerField: isConceptTool ? 4e3 : 200
14213
14199
  });
14214
14200
  const sampleData = Array.isArray(resultData) ? resultData.slice(0, TOOL_TRACKING_SAMPLE_ROWS) : Array.isArray(formattedResult.data) ? formattedResult.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS) : [];
14215
14201
  executedTools.push({
@@ -14225,6 +14211,7 @@ ${lines.join("\n")}`;
14225
14211
  outputSchema: tool.outputSchema
14226
14212
  });
14227
14213
  const formatted = typeof formattedResult === "string" ? formattedResult : JSON.stringify(formattedResult);
14214
+ this.config.trace?.directTools.calls.push({ toolId: tool.id, input: toolInput, output: result, durationMs: executionTimeMs });
14228
14215
  return `\u2705 Tool "${tool.name}" executed successfully. ${rowCount} results returned.
14229
14216
 
14230
14217
  ${formatted}`;
@@ -14235,10 +14222,11 @@ ${formatted}`;
14235
14222
  if (this.streamBuffer.hasCallback()) {
14236
14223
  this.streamBuffer.write(`
14237
14224
 
14238
- \u274C **${tool.name} failed:** ${errorMsg}
14225
+ \u274C **${describeDirectToolCall(tool, toolInput)} failed:** ${errorMsg}
14239
14226
 
14240
14227
  `);
14241
14228
  }
14229
+ this.config.trace?.directTools.calls.push({ toolId: tool.id, input: toolInput, error: errorMsg, durationMs: executionTimeMs });
14242
14230
  return `\u274C Tool "${tool.name}" failed: ${errorMsg}`;
14243
14231
  }
14244
14232
  }
@@ -14391,6 +14379,11 @@ ${propLines}` : ""
14391
14379
  type: "string",
14392
14380
  enum: ["raw", "pre-aggregate", "summary"],
14393
14381
  description: 'How to return data. "pre-aggregate": use GROUP BY/SUM/COUNT for totals. "summary": high-level metrics. "raw": individual records.'
14382
+ },
14383
+ concepts: {
14384
+ type: "array",
14385
+ items: { type: "string" },
14386
+ description: `Exact concept name(s), already fetched this turn via the concept tool's "get" method, that THIS query needs. Only names given here get attached as authoritative definitions to this dispatch \u2014 omit a concept you looked at but are not using for this specific query. Leave empty/omit entirely when this query needs none.`
14394
14387
  }
14395
14388
  },
14396
14389
  required: ["intent"]
@@ -14614,6 +14607,22 @@ ${scopeWarning} The script re-runs them, so you can include more than one \`ctx.
14614
14607
  return buildSourceSummaries(this.externalTools);
14615
14608
  }
14616
14609
  };
14610
+ function describeDirectToolCall(tool, toolInput) {
14611
+ const method = toolInput?.method;
14612
+ if (typeof method !== "string") return tool.name;
14613
+ switch (method) {
14614
+ case "find":
14615
+ return typeof toolInput.query === "string" ? `${tool.name} (find: "${toolInput.query}")` : `${tool.name} (find)`;
14616
+ case "get": {
14617
+ const names = Array.isArray(toolInput.names) ? toolInput.names.join(", ") : void 0;
14618
+ return names ? `${tool.name} (get: ${names})` : `${tool.name} (get)`;
14619
+ }
14620
+ case "run":
14621
+ return typeof toolInput.name === "string" ? `${tool.name} (run: ${toolInput.name})` : `${tool.name} (run)`;
14622
+ default:
14623
+ return `${tool.name} (${method})`;
14624
+ }
14625
+ }
14617
14626
  function extractTablesFromSQL(sqls) {
14618
14627
  const tables = /* @__PURE__ */ new Set();
14619
14628
  const pattern = /(?:FROM|JOIN)\s+"?([a-zA-Z_][\w.]*)"?/gi;
@@ -14637,6 +14646,7 @@ function createRunTrace() {
14637
14646
  scriptMatcher: null,
14638
14647
  entityResolution: { calls: [] },
14639
14648
  concepts: { calls: [] },
14649
+ directTools: { calls: [] },
14640
14650
  componentGen: []
14641
14651
  };
14642
14652
  }
@@ -15931,12 +15941,12 @@ function answerCatalogForBinding(components) {
15931
15941
  import dotenv from "dotenv";
15932
15942
 
15933
15943
  // src/userResponse/schema.ts
15934
- import path7 from "path";
15944
+ import path8 from "path";
15935
15945
  import fs7 from "fs";
15936
15946
  var Schema = class {
15937
15947
  constructor(schemaFilePath) {
15938
15948
  this.cachedSchema = null;
15939
- this.schemaFilePath = schemaFilePath || path7.join(process.cwd(), "../analysis/data/schema.json");
15949
+ this.schemaFilePath = schemaFilePath || path8.join(process.cwd(), "../analysis/data/schema.json");
15940
15950
  }
15941
15951
  /**
15942
15952
  * Gets the database schema from the schema file
@@ -15944,7 +15954,7 @@ var Schema = class {
15944
15954
  */
15945
15955
  getDatabaseSchema() {
15946
15956
  try {
15947
- const dir = path7.dirname(this.schemaFilePath);
15957
+ const dir = path8.dirname(this.schemaFilePath);
15948
15958
  if (!fs7.existsSync(dir)) {
15949
15959
  logger.info(`Creating directory structure: ${dir}`);
15950
15960
  fs7.mkdirSync(dir, { recursive: true });
@@ -17548,7 +17558,7 @@ ${executedToolsText}`);
17548
17558
  */
17549
17559
  async handleUserRequest(userPrompt, components, apiKey, conversationHistory, responseMode = "text", streamCallback, collections, externalTools, userId) {
17550
17560
  const startTime = Date.now();
17551
- logger.clearFile();
17561
+ logger.clearFile(`legacy-${userPrompt}`);
17552
17562
  logger.logLLMPrompt("handleUserRequest", "user", `User Prompt: ${userPrompt}`);
17553
17563
  try {
17554
17564
  const conversationMatch = await conversation_search_default.searchConversationsWithReranking({
@@ -18081,13 +18091,28 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
18081
18091
  let runTrace;
18082
18092
  const answerComps = answerComponents(components);
18083
18093
  const bindingComps = projectForDatasetBinding(answerComps);
18084
- logger.clearFile();
18094
+ logger.clearFile(`chat-${prompt}`);
18085
18095
  logger.logLLMPrompt("agentUserResponse", "user", `User Prompt: ${prompt}`);
18086
18096
  logger.info(`[AgentFlow] Starting | provider: ${provider} | prompt: "${prompt.substring(0, 50)}..."`);
18087
18097
  try {
18088
18098
  const cached = await replayExactMatch({ prompt, collections, userId, streamCallback, method: `${provider}-agent-exact-match`, startTime });
18089
18099
  if (cached?.response) return cached.response;
18090
- const directReplay = cached?.directReplay;
18100
+ let directReplay = cached?.directReplay;
18101
+ if (!cached) {
18102
+ const supersededMatch = await conversation_search_default.findExactMatch({
18103
+ userPrompt: prompt,
18104
+ collections,
18105
+ userId,
18106
+ statuses: ["superseded"]
18107
+ });
18108
+ const boundBinding = supersededMatch?.uiBlock?.scriptBinding;
18109
+ if (boundBinding?.recipeId) {
18110
+ directReplay = { recipeId: boundBinding.recipeId, params: boundBinding.params || {} };
18111
+ logger.info(
18112
+ `[AgentFlow] Exact match found a SUPERSEDED row (recipe edited since) \u2014 will re-run current recipe ${directReplay.recipeId} directly (skip matcher LLM)`
18113
+ );
18114
+ }
18115
+ }
18091
18116
  logger.info(`[AgentFlow] No exact match \u2014 proceeding with agent`);
18092
18117
  runTrace = createRunTrace();
18093
18118
  const apiKey = (() => {
@@ -18172,7 +18197,7 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
18172
18197
  const boundRecipe = await scriptStore.get(directReplay.recipeId);
18173
18198
  if (boundRecipe) {
18174
18199
  scriptMatch = { tier: "high", recipe: boundRecipe, extractedParams: directReplay.params };
18175
- logger.info(`[AgentFlow] TTL-expired exact match \u2014 replaying bound recipe "${boundRecipe.name}" directly (skipped ScriptMatcher LLM)`);
18200
+ logger.info(`[AgentFlow] Direct replay \u2014 running current recipe "${boundRecipe.name}" directly (skipped ScriptMatcher LLM)`);
18176
18201
  } else {
18177
18202
  logger.info(`[AgentFlow] Bound recipe ${directReplay.recipeId} no longer in store \u2014 falling back to ScriptMatcher LLM`);
18178
18203
  }
@@ -18524,6 +18549,19 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${scriptMatch
18524
18549
  }
18525
18550
  };
18526
18551
  }
18552
+ if (isDisplayEdit) {
18553
+ try {
18554
+ const invalidate = collections?.["conversation-history"]?.["invalidateByScriptRecipe"];
18555
+ if (invalidate) {
18556
+ const n = await invalidate({ recipeId: scriptMatch.recipe.id, userId });
18557
+ logger.info(`[AgentFlow] Superseded ${n?.count ?? n ?? 0} cached answer(s) bound to display-edited recipe ${scriptMatch.recipe.id}`);
18558
+ } else {
18559
+ logger.warn(`[AgentFlow] conversation-history.invalidateByScriptRecipe not wired \u2014 cached answers for recipe ${scriptMatch.recipe.id} will replay the PRE-EDIT component set`);
18560
+ }
18561
+ } catch (invErr) {
18562
+ logger.warn(`[AgentFlow] Conversation invalidation failed (non-blocking): ${invErr}`);
18563
+ }
18564
+ }
18527
18565
  const elapsedTime2 = Date.now() - startTime;
18528
18566
  logger.info(`[AgentFlow] Script flow complete | ${scriptResult.executedQueries.length} queries | ${matchedComponents2.length} components | ${elapsedTime2}ms`);
18529
18567
  return {
@@ -18679,6 +18717,67 @@ BEGIN NOW. Your VERY FIRST action MUST be a \`write_script\` tool call containin
18679
18717
  const textResponse = rawText.replace(/_?_?SB_END_?_?/g, "").replace(/__SB_\w+_(?:START|MSG)_?_?/g, "").replace(/__QUERY_TIMER_START_[^_]*__/g, "").replace(/__QUERY_TIMER_DONE_[\d.]+__/g, "").replace(/__TEXT_COMPLETE__COMPONENT_GENERATION_START__/g, "").replace(/\[COMPLEXITY:\s*(?:simple|medium|complex)\]/gi, "");
18680
18718
  streamBuffer.flush();
18681
18719
  if (signal?.aborted) throw new RequestAbortedError();
18720
+ if (agentResponse.savedScript && editContext) {
18721
+ try {
18722
+ const authored = agentResponse.savedScript;
18723
+ if (runTrace) {
18724
+ try {
18725
+ runTrace.script = {
18726
+ reused: false,
18727
+ recipeId: authored.recipeId,
18728
+ body: authored.scriptBody,
18729
+ reasoning: editContext.instruction ?? null,
18730
+ executionResult: authored.executionResult ?? null,
18731
+ failedAttempts: agentResponse.failedScriptAttempts
18732
+ };
18733
+ } catch (traceErr) {
18734
+ logger.warn(`[AgentFlow] Trace capture failed for edited script (non-blocking): ${traceErr}`);
18735
+ }
18736
+ }
18737
+ const summaryLine = (agentResponse.text || "").split("\n").map((l) => l.trim()).find((l) => l.startsWith("\u270F\uFE0F"));
18738
+ const changeSummary = (summaryLine || editContext.instruction).replace(/^✏️\s*/, "").replace(/^\**\s*Updated the script:\s*/i, "").replace(/\*\*/g, "").replace(/\s+/g, " ").trim().slice(0, 300);
18739
+ const committed = await scriptStore.commitEdit(
18740
+ editContext.recipeId,
18741
+ {
18742
+ recipeId: authored.recipeId,
18743
+ name: authored.name,
18744
+ intentDescription: authored.intentDescription,
18745
+ tags: authored.tags,
18746
+ parameters: authored.parameters,
18747
+ scriptBody: authored.scriptBody,
18748
+ sourceIds: authored.sourceIds,
18749
+ tables: authored.tables
18750
+ },
18751
+ { instruction: editContext.instruction, changeSummary, editedBy: userId }
18752
+ );
18753
+ if (committed) {
18754
+ agentResponse.savedScript.recipeId = committed.id;
18755
+ scriptBinding = {
18756
+ recipeId: committed.id,
18757
+ version: committed.version,
18758
+ params: {},
18759
+ name: committed.name,
18760
+ columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
18761
+ };
18762
+ try {
18763
+ const invalidate = collections?.["conversation-history"]?.["invalidateByScriptRecipe"];
18764
+ if (invalidate) {
18765
+ const n = await invalidate({ recipeId: committed.id, userId });
18766
+ logger.info(`[AgentFlow] Superseded ${n?.count ?? n ?? 0} cached answer(s) bound to edited recipe ${committed.id}`);
18767
+ } else {
18768
+ logger.warn(`[AgentFlow] conversation-history.invalidateByScriptRecipe not wired \u2014 cached answers for recipe ${committed.id} will replay the PRE-EDIT result`);
18769
+ }
18770
+ } catch (invErr) {
18771
+ logger.warn(`[AgentFlow] Conversation invalidation failed (non-blocking): ${invErr}`);
18772
+ }
18773
+ } else {
18774
+ logger.warn(`[AgentFlow] commitEdit failed for "${editedRecipeName}" \u2014 original recipe left unchanged`);
18775
+ }
18776
+ } catch (err) {
18777
+ const msg = err instanceof Error ? err.message : String(err);
18778
+ logger.warn(`[AgentFlow] Failed to commit script edit (non-blocking): ${msg}`);
18779
+ }
18780
+ }
18682
18781
  if (agentResponse.executedTools.length === 0 && agentResponse.savedScript?.executedQueries && agentResponse.savedScript.executedQueries.length > 0) {
18683
18782
  for (const q of agentResponse.savedScript.executedQueries) {
18684
18783
  agentResponse.executedTools.push({
@@ -18952,66 +19051,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${carriedRequ
18952
19051
  }
18953
19052
  };
18954
19053
  }
18955
- if (agentResponse.savedScript && editContext) {
18956
- try {
18957
- const authored = agentResponse.savedScript;
18958
- if (runTrace) {
18959
- try {
18960
- runTrace.script = {
18961
- reused: false,
18962
- recipeId: authored.recipeId,
18963
- body: authored.scriptBody,
18964
- reasoning: editContext.instruction ?? null,
18965
- executionResult: authored.executionResult ?? null,
18966
- failedAttempts: agentResponse.failedScriptAttempts
18967
- };
18968
- } catch (traceErr) {
18969
- logger.warn(`[AgentFlow] Trace capture failed for edited script (non-blocking): ${traceErr}`);
18970
- }
18971
- }
18972
- const summaryLine = (agentResponse.text || "").split("\n").map((l) => l.trim()).find((l) => l.startsWith("\u270F\uFE0F"));
18973
- const changeSummary = (summaryLine || editContext.instruction).replace(/^✏️\s*/, "").replace(/^\**\s*Updated the script:\s*/i, "").replace(/\*\*/g, "").replace(/\s+/g, " ").trim().slice(0, 300);
18974
- const committed = await scriptStore.commitEdit(
18975
- editContext.recipeId,
18976
- {
18977
- recipeId: authored.recipeId,
18978
- name: authored.name,
18979
- intentDescription: authored.intentDescription,
18980
- tags: authored.tags,
18981
- parameters: authored.parameters,
18982
- scriptBody: authored.scriptBody,
18983
- sourceIds: authored.sourceIds,
18984
- tables: authored.tables
18985
- },
18986
- { instruction: editContext.instruction, changeSummary, editedBy: userId }
18987
- );
18988
- if (committed) {
18989
- scriptBinding = {
18990
- recipeId: committed.id,
18991
- version: committed.version,
18992
- params: {},
18993
- name: committed.name,
18994
- columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
18995
- };
18996
- try {
18997
- const invalidate = collections?.["conversation-history"]?.["invalidateByScriptRecipe"];
18998
- if (invalidate) {
18999
- const n = await invalidate({ recipeId: committed.id, userId });
19000
- logger.info(`[AgentFlow] Superseded ${n?.count ?? n ?? 0} cached answer(s) bound to edited recipe ${committed.id}`);
19001
- } else {
19002
- logger.warn(`[AgentFlow] conversation-history.invalidateByScriptRecipe not wired \u2014 cached answers for recipe ${committed.id} will replay the PRE-EDIT result`);
19003
- }
19004
- } catch (invErr) {
19005
- logger.warn(`[AgentFlow] Conversation invalidation failed (non-blocking): ${invErr}`);
19006
- }
19007
- } else {
19008
- logger.warn(`[AgentFlow] commitEdit failed for "${editedRecipeName}" \u2014 original recipe left unchanged`);
19009
- }
19010
- } catch (err) {
19011
- const msg = err instanceof Error ? err.message : String(err);
19012
- logger.warn(`[AgentFlow] Failed to commit script edit (non-blocking): ${msg}`);
19013
- }
19014
- } else if (agentResponse.savedScript) {
19054
+ if (agentResponse.savedScript && !editContext) {
19015
19055
  try {
19016
19056
  const authored = agentResponse.savedScript;
19017
19057
  if (runTrace) {
@@ -19240,7 +19280,8 @@ function transformUIBlockForDB(uiblock, userPrompt, uiBlockId) {
19240
19280
  };
19241
19281
  }
19242
19282
  function hasTraceContent(trace) {
19243
- return trace.kbNodes.global.length > 0 || trace.kbNodes.query.length > 0 || trace.schemaNodes.routing.length > 0 || trace.schemaNodes.preResolve.length > 0 || !!trace.schemaNodes.mainAgentCatalog || trace.schemaNodes.sourceAgentSchemas.length > 0 || trace.script != null || trace.scriptMatcher != null || !!trace.entityResolution.catalogText || trace.entityResolution.calls.length > 0 || !!trace.conversationHistory || !!trace.userAccessContext || !!trace.userKbContext || !!trace.concepts?.catalogText || !!trace.concepts?.invariantsText || (trace.concepts?.calls?.length ?? 0) > 0 || (trace.componentGen?.length ?? 0) > 0;
19283
+ if (!trace || typeof trace !== "object") return false;
19284
+ return (trace.kbNodes?.global?.length ?? 0) > 0 || (trace.kbNodes?.query?.length ?? 0) > 0 || (trace.schemaNodes?.routing?.length ?? 0) > 0 || (trace.schemaNodes?.preResolve?.length ?? 0) > 0 || !!trace.schemaNodes?.mainAgentCatalog || (trace.schemaNodes?.sourceAgentSchemas?.length ?? 0) > 0 || trace.script != null || trace.scriptMatcher != null || !!trace.entityResolution?.catalogText || (trace.entityResolution?.calls?.length ?? 0) > 0 || !!trace.conversationHistory || !!trace.userAccessContext || !!trace.userKbContext || !!trace.concepts?.catalogText || !!trace.concepts?.invariantsText || (trace.concepts?.calls?.length ?? 0) > 0 || (trace.directTools?.calls?.length ?? 0) > 0 || (trace.componentGen?.length ?? 0) > 0;
19244
19285
  }
19245
19286
  async function saveConversation(params) {
19246
19287
  const { userId, userPrompt, uiblock, uiBlockId, threadId, collections, status = "success", error: turnError, trace } = params;
@@ -19386,6 +19427,18 @@ var CONTEXT_CONFIG = {
19386
19427
  function stripStreamMarkers(text) {
19387
19428
  return text.replace(/__TEXT_COMPLETE__[\s\S]*/g, "").replace(/__ANSWER_COMPONENT_START__[\s\S]*?__ANSWER_COMPONENT_END__/g, "").replace(/__COMPONENT_GENERATION_START__/g, "").trim();
19388
19429
  }
19430
+ function promptContextFrom(raw) {
19431
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
19432
+ const source = raw;
19433
+ const context = {};
19434
+ if (typeof source.dashboardId === "string" && source.dashboardId.trim()) {
19435
+ context.dashboardId = source.dashboardId.trim();
19436
+ }
19437
+ if (source.filters && typeof source.filters === "object" && !Array.isArray(source.filters)) {
19438
+ context.filters = source.filters;
19439
+ }
19440
+ return Object.keys(context).length ? context : void 0;
19441
+ }
19389
19442
  var inFlightAborts = /* @__PURE__ */ new Map();
19390
19443
  var recentlyAborted = /* @__PURE__ */ new Map();
19391
19444
  var ABORT_TOMBSTONE_TTL_MS = 3e4;
@@ -19449,6 +19502,10 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19449
19502
  const userConfig = payload.config;
19450
19503
  const SA_RUNTIME = payload.SA_RUNTIME;
19451
19504
  const wsId = userPromptRequest.from.id || "unknown";
19505
+ const reported = (response) => ({
19506
+ model: response?.data?.model,
19507
+ usage: response?.data?.usage
19508
+ });
19452
19509
  const promptContext = `User Prompt: ${prompt?.substring(0, 50)}${(prompt?.length || 0) > 50 ? "..." : ""}`;
19453
19510
  llmUsageLogger.resetLogFile(promptContext);
19454
19511
  userPromptErrorLogger.resetLogFile(promptContext);
@@ -19572,7 +19629,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19572
19629
  conversationHistory,
19573
19630
  onStream: streamCallback,
19574
19631
  onNarration: narrationCallback,
19575
- signal: abortController.signal
19632
+ signal: abortController.signal,
19633
+ context: promptContextFrom(payload.context)
19576
19634
  }) : await get_agent_user_response(
19577
19635
  prompt,
19578
19636
  components,
@@ -19629,7 +19687,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19629
19687
  }
19630
19688
  if (userId) {
19631
19689
  const latencyMs = Date.now() - requestStartTime;
19632
- const model = mainAgentModel || "unknown";
19690
+ const turn = reported(userResponse);
19691
+ const model = turn.model || mainAgentModel || "unknown";
19633
19692
  const event = analyticsClient.buildEvent({
19634
19693
  type: "chat_agent",
19635
19694
  userId,
@@ -19642,7 +19701,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19642
19701
  model,
19643
19702
  errorMessage: "aborted",
19644
19703
  conversationId: savedConversationId2,
19645
- response: savedResponse2
19704
+ response: savedResponse2,
19705
+ usage: turn.usage
19646
19706
  });
19647
19707
  analyticsClient.track(event).catch(() => {
19648
19708
  });
@@ -19696,7 +19756,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19696
19756
  }
19697
19757
  if (userId) {
19698
19758
  const latencyMs = Date.now() - requestStartTime;
19699
- const model = mainAgentModel || "unknown";
19759
+ const turn = reported(userResponse);
19760
+ const model = turn.model || mainAgentModel || "unknown";
19700
19761
  const event = analyticsClient.buildEvent({
19701
19762
  type: "chat_agent",
19702
19763
  userId,
@@ -19709,7 +19770,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19709
19770
  model,
19710
19771
  errorMessage,
19711
19772
  conversationId: savedConversationId2,
19712
- response: savedResponse2
19773
+ response: savedResponse2,
19774
+ usage: turn.usage
19713
19775
  });
19714
19776
  analyticsClient.track(event).catch(() => {
19715
19777
  });
@@ -19793,7 +19855,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19793
19855
  llmUsageLogger.logSessionSummary(prompt?.substring(0, 50));
19794
19856
  if (userId) {
19795
19857
  const latencyMs = Date.now() - requestStartTime;
19796
- const model = mainAgentModel || "unknown";
19858
+ const turn = reported(userResponse);
19859
+ const model = turn.model || mainAgentModel || "unknown";
19797
19860
  const componentConfig = userResponse.data?.component?.props?.config;
19798
19861
  const queryMap = componentConfig?._queryMap || {};
19799
19862
  const sqlStatements = Object.values(queryMap).filter((v) => typeof v === "string");
@@ -19825,7 +19888,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19825
19888
  sourcesUsed: sourcesUsed.length > 0 ? sourcesUsed : void 0,
19826
19889
  sqlGenerated: sqlStatements.length > 0 ? sqlStatements.join(";\n") : void 0,
19827
19890
  conversationId: savedConversationId,
19828
- response: savedResponse
19891
+ response: savedResponse,
19892
+ usage: turn.usage
19829
19893
  });
19830
19894
  analyticsClient.track(event).catch(() => {
19831
19895
  });
@@ -19848,7 +19912,7 @@ async function handleUserPromptRequest(data, components, sendMessage, anthropicA
19848
19912
  id: `comp_${Math.random().toString(36).substring(2, 8)}`
19849
19913
  }));
19850
19914
  }
19851
- const { trace: _trace, ...dataWithoutTrace } = response.data ?? {};
19915
+ const { trace: _trace, usage: _usage, model: _model, ...dataWithoutTrace } = response.data ?? {};
19852
19916
  sendDataResponse4(
19853
19917
  response.id || data.id,
19854
19918
  {
@@ -23938,7 +24002,7 @@ var processDashCompRequest = async (data, components, _sendMessage, anthropicApi
23938
24002
  const conversationHistory = dashboardId ? dashboardConversationHistory.getHistory(dashboardId, userId) : void 0;
23939
24003
  const promptContext = `DASH_COMP[${reqType}]: ${prompt?.substring(0, 50)}${(prompt?.length || 0) > 50 ? "..." : ""}`;
23940
24004
  llmUsageLogger.resetLogFile(promptContext);
23941
- logger.clearFile();
24005
+ logger.clearFile(promptContext);
23942
24006
  if (!prompt) {
23943
24007
  errors.push("Prompt is required");
23944
24008
  }
@@ -24469,6 +24533,9 @@ ${marker}
24469
24533
  }
24470
24534
 
24471
24535
  // src/codingAgent/drivers/pi.ts
24536
+ import * as crypto3 from "crypto";
24537
+ import * as fs8 from "fs";
24538
+ import * as path9 from "path";
24472
24539
  var dynamicImport2 = new Function("specifier", "return import(specifier)");
24473
24540
  var piModulePromise = null;
24474
24541
  function loadPi() {
@@ -24480,46 +24547,111 @@ function loadPi() {
24480
24547
  }
24481
24548
  return piModulePromise;
24482
24549
  }
24483
- var authAndRegistry = null;
24484
- function getAuthAndRegistry() {
24485
- if (!authAndRegistry) {
24486
- authAndRegistry = loadPi().then(({ AuthStorage, ModelRegistry }) => {
24487
- const authStorage = AuthStorage.create();
24488
- return { authStorage, modelRegistry: ModelRegistry.create(authStorage) };
24550
+ var runtimePromise = null;
24551
+ function modelRuntime(pi) {
24552
+ if (!runtimePromise) {
24553
+ const agentDir = pi.getAgentDir();
24554
+ runtimePromise = pi.ModelRuntime.create({
24555
+ authPath: `${agentDir}/auth.json`,
24556
+ modelsStorePath: `${agentDir}/models-store.json`,
24557
+ allowModelNetwork: true
24558
+ }).catch((err) => {
24559
+ runtimePromise = null;
24560
+ throw err;
24489
24561
  });
24490
24562
  }
24491
- return authAndRegistry;
24563
+ return runtimePromise;
24492
24564
  }
24493
- var DEFAULT_PROVIDER = "openai-codex";
24494
- var DEFAULT_MODEL_ID = process.env.PI_AGENT_MODEL || "gpt-5.5";
24565
+ var DEFAULT_PROVIDER = process.env.PI_PROVIDER || "openai-codex";
24566
+ var DEFAULT_MODEL_ID = process.env.PI_MODEL || process.env.PI_AGENT_MODEL || "gpt-5.6-luna";
24495
24567
  var KNOWN_UNUSABLE_MODEL_IDS = /* @__PURE__ */ new Set(["gpt-5.3-codex-spark"]);
24496
- async function resolveModel(modelRegistry, spec) {
24497
- const [provider, modelId] = spec?.includes("/") ? [spec.slice(0, spec.indexOf("/")), spec.slice(spec.indexOf("/") + 1)] : [spec || DEFAULT_PROVIDER, void 0];
24498
- const available = modelRegistry.getAvailable().filter((m) => m.provider === provider);
24499
- const wanted = modelId ?? (spec ? void 0 : DEFAULT_MODEL_ID);
24500
- const exact = wanted && (modelRegistry.find(provider, wanted) ?? available.find((m) => m.id === wanted));
24501
- if (exact && modelRegistry.hasConfiguredAuth(exact)) return exact;
24502
- const usable = available.filter((m) => !KNOWN_UNUSABLE_MODEL_IDS.has(m.id));
24503
- if (usable.length) return usable[0];
24504
- if (available.length) return available[0];
24505
- throw new Error(
24506
- `No model available from "${provider}". Authorise it in a terminal on this machine: run \`pi\`, then \`/login\`.`
24507
- );
24568
+ async function resolveModel(runtime, spec) {
24569
+ const slash = spec?.indexOf("/") ?? -1;
24570
+ const provider = slash > 0 ? spec.slice(0, slash) : DEFAULT_PROVIDER;
24571
+ const wanted = slash > 0 ? spec.slice(slash + 1) : spec || DEFAULT_MODEL_ID;
24572
+ const available = await runtime.getAvailable(provider);
24573
+ const exact = available.find((m) => m.id === wanted);
24574
+ if (exact) return exact;
24575
+ const fallback = available.find((m) => !KNOWN_UNUSABLE_MODEL_IDS.has(m.id)) ?? available[0];
24576
+ if (fallback) {
24577
+ console.warn(`[pi] model ${provider}/${wanted} unavailable \u2014 using ${fallback.provider}/${fallback.id}`);
24578
+ return fallback;
24579
+ }
24580
+ throw new Error(`No model available from "${provider}". Authorise it in a terminal on this machine: run \`pi\`, then \`/login\`.`);
24581
+ }
24582
+ var SHELL_ENV_ALLOWLIST = [
24583
+ "PATH",
24584
+ "Path",
24585
+ "HOME",
24586
+ "USER",
24587
+ "USERNAME",
24588
+ "LOGNAME",
24589
+ "SHELL",
24590
+ "LANG",
24591
+ "LC_ALL",
24592
+ "TERM",
24593
+ "TMPDIR",
24594
+ "TEMP",
24595
+ "TMP",
24596
+ "NODE_EXTRA_CA_CERTS",
24597
+ // Windows: without these a shell cannot start programs.
24598
+ "SystemRoot",
24599
+ "SYSTEMROOT",
24600
+ "windir",
24601
+ "ComSpec",
24602
+ "COMSPEC",
24603
+ "PATHEXT",
24604
+ "USERPROFILE",
24605
+ "APPDATA",
24606
+ "LOCALAPPDATA",
24607
+ "HOMEDRIVE",
24608
+ "HOMEPATH",
24609
+ "ProgramFiles",
24610
+ "ProgramData",
24611
+ "MSYSTEM"
24612
+ ];
24613
+ function shellEnv(run, inherit) {
24614
+ if (inherit) return { ...process.env, ...run };
24615
+ const env = {};
24616
+ for (const key of SHELL_ENV_ALLOWLIST) {
24617
+ if (process.env[key] !== void 0) env[key] = process.env[key];
24618
+ }
24619
+ return { ...env, ...run };
24620
+ }
24621
+ function bashTool(pi, cwd, env) {
24622
+ const local = pi.createLocalBashOperations();
24623
+ const definition = pi.createBashToolDefinition(cwd, {
24624
+ operations: {
24625
+ exec: (command, dir, options) => local.exec(command, dir, { ...options, timeout: void 0, env: env() })
24626
+ }
24627
+ });
24628
+ const parameters = definition.parameters;
24629
+ return {
24630
+ ...definition,
24631
+ description: definition.description.replace(/\s*Optionally provide a timeout in seconds\./, ""),
24632
+ parameters: { ...parameters, properties: { command: parameters.properties.command }, required: ["command"] }
24633
+ };
24508
24634
  }
24509
24635
  function emptyUsage2() {
24510
24636
  return { inputTokens: 0, outputTokens: 0, reasoningTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, toolCalls: 0 };
24511
24637
  }
24512
24638
  var BUFFER_CAP2 = 64e3;
24513
24639
  var EVENT_CAP2 = 600;
24514
- function normEvent2(ev) {
24515
- switch (ev.type) {
24516
- case "tool_execution_start":
24517
- return { kind: "command", id: ev.toolCallId, command: ev.toolName ? `${ev.toolName} ${jsonPreview(ev.args)}` : void 0, status: "in_progress" };
24518
- case "tool_execution_end":
24519
- return { kind: "command", id: ev.toolCallId, status: ev.isError ? "failed" : "completed", done: true, output: textOf(ev.result) };
24520
- default:
24521
- return null;
24640
+ function textOf(content) {
24641
+ const blocks = content?.content ?? content;
24642
+ if (!Array.isArray(blocks)) return typeof content === "string" ? content : "";
24643
+ return blocks.filter((b) => b?.type === "text").map((b) => String(b.text ?? "")).join("").trim();
24644
+ }
24645
+ function assistantParts(message) {
24646
+ const record = message;
24647
+ if (record?.role !== "assistant" || !Array.isArray(record.content)) return { text: "", thinking: "" };
24648
+ let text = "";
24649
+ let thinking = "";
24650
+ for (const block of record.content) {
24651
+ if (block?.type === "text") text += String(block.text ?? "");
24652
+ else if (block?.type === "thinking" && !block.redacted) thinking += String(block.thinking ?? "");
24522
24653
  }
24654
+ return { text, thinking };
24523
24655
  }
24524
24656
  function jsonPreview(v) {
24525
24657
  if (v == null) return "";
@@ -24529,10 +24661,10 @@ function jsonPreview(v) {
24529
24661
  return "";
24530
24662
  }
24531
24663
  }
24532
- function textOf(content) {
24533
- const blocks = content?.content ?? content;
24534
- if (!Array.isArray(blocks)) return typeof content === "string" ? content : "";
24535
- return blocks.filter((b) => b?.type === "text").map((b) => String(b.text ?? "")).join("").trim();
24664
+ function toolEvent(name, id, args) {
24665
+ if (name === "bash") return { kind: "command", id, command: String(args?.command ?? "") };
24666
+ if (name === "write" || name === "edit") return { kind: "file", id, text: String(args?.path ?? args?.file_path ?? "") };
24667
+ return { kind: "tool", id, tool: { name: name ?? "tool", args } };
24536
24668
  }
24537
24669
  var TRANSCRIPT_CAP = 400;
24538
24670
  var sessionListeners = /* @__PURE__ */ new Set();
@@ -24550,9 +24682,6 @@ function publish(sessionKey, live, entry) {
24550
24682
  for (const listener of sessionListeners) listener(sessionKey, entry);
24551
24683
  }
24552
24684
  var liveSessions = /* @__PURE__ */ new Map();
24553
- function newSessionId() {
24554
- return `pi_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
24555
- }
24556
24685
  var IDLE_TIMEOUT_MS = Number(process.env.PI_AGENT_IDLE_MS ?? 30 * 60 * 1e3);
24557
24686
  var sweeper = null;
24558
24687
  function startSweeper() {
@@ -24571,29 +24700,76 @@ function startSweeper() {
24571
24700
  }, 6e4);
24572
24701
  sweeper.unref?.();
24573
24702
  }
24703
+ function readInstructions(opts) {
24704
+ if (!opts.instructionsFile) return void 0;
24705
+ const file = path9.resolve(opts.cwd, opts.instructionsFile);
24706
+ try {
24707
+ return { file, content: fs8.readFileSync(file, "utf-8") };
24708
+ } catch {
24709
+ return void 0;
24710
+ }
24711
+ }
24712
+ var hash = (s) => crypto3.createHash("sha256").update(s).digest("hex");
24574
24713
  function createPiSession(opts) {
24575
24714
  let running = false;
24576
- let sessionKey = opts.resumeId && liveSessions.has(opts.resumeId) ? opts.resumeId : void 0;
24577
- async function ensureSession() {
24578
- const existing = sessionKey ? liveSessions.get(sessionKey) : void 0;
24579
- if (existing) return existing;
24580
- const { createAgentSession, SessionManager } = await loadPi();
24581
- const { authStorage, modelRegistry } = await getAuthAndRegistry();
24582
- const model = await resolveModel(modelRegistry, opts.model);
24583
- const { session } = await createAgentSession({
24715
+ let sessionKey = opts.resumeId;
24716
+ async function open(resumeFile) {
24717
+ const pi = await loadPi();
24718
+ const runtime = await modelRuntime(pi);
24719
+ const model = await resolveModel(runtime, opts.model);
24720
+ const agentDir = pi.getAgentDir();
24721
+ const instructions = readInstructions(opts);
24722
+ const resourceLoader = new pi.DefaultResourceLoader({
24723
+ cwd: opts.cwd,
24724
+ agentDir,
24725
+ // The workspace's own instructions, and only them — not whatever
24726
+ // AGENTS.md files the machine's user keeps in ~/.pi or a parent folder.
24727
+ agentsFilesOverride: (base) => instructions ? { agentsFiles: [{ path: instructions.file, content: instructions.content }] } : base
24728
+ });
24729
+ await resourceLoader.reload();
24730
+ const sessionManager = resumeFile && fs8.existsSync(resumeFile) ? pi.SessionManager.open(resumeFile) : pi.SessionManager.create(opts.cwd);
24731
+ const live = {
24732
+ running: false,
24733
+ transcript: [],
24734
+ lastActive: Date.now(),
24735
+ eventLog: [],
24736
+ buf: "",
24737
+ env: opts.env,
24738
+ inheritEnv: opts.inheritEnv,
24739
+ instructions: hash(instructions?.content ?? "")
24740
+ };
24741
+ const { session } = await pi.createAgentSession({
24584
24742
  cwd: opts.cwd,
24585
- sessionManager: SessionManager.inMemory(),
24586
- authStorage,
24587
- modelRegistry,
24743
+ agentDir,
24744
+ modelRuntime: runtime,
24588
24745
  model,
24589
- tools: opts.tools ?? ["bash"]
24746
+ ...opts.tools ? { tools: opts.tools } : {},
24747
+ customTools: [bashTool(pi, opts.cwd, () => shellEnv(live.env, live.inheritEnv))],
24748
+ resourceLoader,
24749
+ sessionManager
24590
24750
  });
24591
- sessionKey = newSessionId();
24592
- const live = { session, running: false, transcript: [], lastActive: Date.now(), eventLog: [], buf: "" };
24593
- liveSessions.set(sessionKey, live);
24751
+ live.session = session;
24752
+ const file = sessionManager.getSessionFile();
24753
+ if (!file) throw new Error("pi did not give this session a file, so it could not be resumed later.");
24754
+ sessionKey = file;
24755
+ liveSessions.set(file, live);
24594
24756
  startSweeper();
24595
24757
  return live;
24596
24758
  }
24759
+ async function ensureSession() {
24760
+ const existing = sessionKey ? liveSessions.get(sessionKey) : void 0;
24761
+ if (existing) {
24762
+ if (existing.instructions !== hash(readInstructions(opts)?.content ?? "") && !existing.running) {
24763
+ existing.session.dispose();
24764
+ liveSessions.delete(sessionKey);
24765
+ return open(sessionKey);
24766
+ }
24767
+ existing.env = opts.env;
24768
+ existing.inheritEnv = opts.inheritEnv;
24769
+ return existing;
24770
+ }
24771
+ return open(sessionKey);
24772
+ }
24597
24773
  async function runTurn(prompt, handlers, signal) {
24598
24774
  const startedAt = Date.now();
24599
24775
  const usage = emptyUsage2();
@@ -24625,29 +24801,70 @@ function createPiSession(opts) {
24625
24801
  live.buf = (live.buf + chunk).slice(-BUFFER_CAP2);
24626
24802
  handlers?.onOutput?.(chunk);
24627
24803
  };
24804
+ const send = (event) => {
24805
+ live.eventLog.push(event);
24806
+ if (live.eventLog.length > EVENT_CAP2) live.eventLog.shift();
24807
+ handlers?.onEvent?.(event);
24808
+ publish(key, live, { at: Date.now(), kind: "activity", event });
24809
+ };
24810
+ let streamed = 0;
24811
+ let messageSeq = 0;
24812
+ const started = /* @__PURE__ */ new Map();
24628
24813
  const unsubscribe = live.session.subscribe((ev) => {
24629
- if (ev.type === "message_update" && ev.assistantMessageEvent?.type === "text_delta") {
24630
- const delta = ev.assistantMessageEvent.delta;
24631
- if (delta) {
24632
- emit(delta);
24633
- handlers?.onNarration?.(delta);
24814
+ switch (ev.type) {
24815
+ case "turn_start":
24816
+ send({ kind: "turn" });
24817
+ return;
24818
+ case "message_start":
24819
+ streamed = 0;
24820
+ return;
24821
+ case "message_update": {
24822
+ const { text } = assistantParts(ev.message);
24823
+ if (text.length > streamed) {
24824
+ const delta = text.slice(streamed);
24825
+ streamed = text.length;
24826
+ emit(delta);
24827
+ handlers?.onNarration?.(delta);
24828
+ }
24829
+ return;
24634
24830
  }
24635
- return;
24636
- }
24637
- if (ev.type === "tool_execution_start" || ev.type === "tool_execution_end") {
24638
- if (ev.type === "tool_execution_end") usage.toolCalls++;
24639
- const event = normEvent2(ev);
24640
- if (!event) return;
24641
- live.eventLog.push(event);
24642
- if (live.eventLog.length > EVENT_CAP2) live.eventLog.shift();
24643
- handlers?.onEvent?.(event);
24644
- publish(key, live, { at: Date.now(), kind: "activity", event });
24645
- if (ev.type === "tool_execution_start") emit(`
24831
+ case "message_end": {
24832
+ const { text, thinking } = assistantParts(ev.message);
24833
+ const id = `msg_${++messageSeq}`;
24834
+ if (thinking.trim()) send({ kind: "reasoning", id: `${id}_r`, text: thinking.trim(), status: "completed", done: true });
24835
+ if (text.trim()) send({ kind: "message", id, text: text.trim(), status: "completed", done: true });
24836
+ streamed = 0;
24837
+ return;
24838
+ }
24839
+ case "tool_execution_start": {
24840
+ const event = { ...toolEvent(ev.toolName, ev.toolCallId, ev.args), status: "in_progress" };
24841
+ if (ev.toolCallId) started.set(ev.toolCallId, event);
24842
+ send(event);
24843
+ emit(`
24646
24844
  \u2192 ${ev.toolName ?? "tool"} ${jsonPreview(ev.args)}
24647
24845
  `);
24648
- return;
24846
+ return;
24847
+ }
24848
+ case "tool_execution_end": {
24849
+ usage.toolCalls++;
24850
+ const start = ev.toolCallId && started.get(ev.toolCallId) || toolEvent(ev.toolName, ev.toolCallId, void 0);
24851
+ send({ ...start, status: ev.isError ? "failed" : "completed", done: true, output: textOf(ev.result) });
24852
+ return;
24853
+ }
24854
+ default:
24855
+ return;
24649
24856
  }
24650
24857
  });
24858
+ let early = false;
24859
+ const poll = handlers?.doneWhen ? setInterval(async () => {
24860
+ try {
24861
+ if (!early && await handlers.doneWhen()) {
24862
+ early = true;
24863
+ void live.session.abort();
24864
+ }
24865
+ } catch {
24866
+ }
24867
+ }, 1e3) : null;
24651
24868
  try {
24652
24869
  if (signal?.aborted) {
24653
24870
  status = "aborted";
@@ -24655,16 +24872,20 @@ function createPiSession(opts) {
24655
24872
  }
24656
24873
  await live.session.prompt(prompt);
24657
24874
  const err = live.session.agent.state.errorMessage;
24658
- if (err) {
24659
- status = signal?.aborted ? "aborted" : "error";
24875
+ if (early) status = "early";
24876
+ else if (signal?.aborted) status = "aborted";
24877
+ else if (err) {
24878
+ status = "error";
24660
24879
  error = err;
24661
- } else if (signal?.aborted) {
24662
- status = "aborted";
24663
24880
  }
24664
24881
  } catch (err) {
24665
- status = signal?.aborted ? "aborted" : "error";
24666
- error = err?.message ?? String(err);
24882
+ if (early) status = "early";
24883
+ else {
24884
+ status = signal?.aborted ? "aborted" : "error";
24885
+ error = err?.message ?? String(err);
24886
+ }
24667
24887
  } finally {
24888
+ if (poll) clearInterval(poll);
24668
24889
  unsubscribe();
24669
24890
  signal?.removeEventListener("abort", onAbort);
24670
24891
  live.running = false;
@@ -24705,8 +24926,8 @@ function createPiSession(opts) {
24705
24926
  }
24706
24927
  sessionKey = void 0;
24707
24928
  },
24708
- // Local-only, same as the codex driver: the live session stays in the
24709
- // module map so a later run with this sessionId can still resume it.
24929
+ // Local-only, same as the codex driver: the live session stays in the map
24930
+ // so a later run with this sessionId resumes it.
24710
24931
  dispose: () => {
24711
24932
  }
24712
24933
  };
@@ -24725,11 +24946,11 @@ function createCodingAgentSession(harness, opts) {
24725
24946
  }
24726
24947
 
24727
24948
  // src/codingAgent/registry.ts
24728
- import * as path8 from "path";
24949
+ import * as path10 from "path";
24729
24950
  var agents = /* @__PURE__ */ new Map();
24730
24951
  var config = {
24731
- promptRoot: path8.join(process.cwd(), ".prompts", "coding-agents"),
24732
- runtimeDir: path8.join(process.cwd(), ".coding-agent-runtime"),
24952
+ promptRoot: path10.join(process.cwd(), ".prompts", "coding-agents"),
24953
+ runtimeDir: path10.join(process.cwd(), ".coding-agent-runtime"),
24733
24954
  harness: "codex",
24734
24955
  keepRuns: 20,
24735
24956
  keepThreads: 200
@@ -24768,13 +24989,13 @@ function getCodingAgentsConfig() {
24768
24989
  return config;
24769
24990
  }
24770
24991
  function resolvePromptDir(def) {
24771
- return def.promptDir || path8.join(config.promptRoot, def.id);
24992
+ return def.promptDir || path10.join(config.promptRoot, def.id);
24772
24993
  }
24773
24994
 
24774
24995
  // src/codingAgent/runner.ts
24775
- import * as fs8 from "fs";
24996
+ import * as fs9 from "fs";
24776
24997
  import * as fsp from "fs/promises";
24777
- import * as path9 from "path";
24998
+ import * as path11 from "path";
24778
24999
  var DEFAULT_TIMEOUT_MS = 15 * 60 * 1e3;
24779
25000
  var inFlight = /* @__PURE__ */ new Set();
24780
25001
  function slug(v) {
@@ -24784,11 +25005,11 @@ function resolveScope(def, opts) {
24784
25005
  const scope = def.scope ?? (def.mode === "chat" ? "thread" : "project");
24785
25006
  if (scope === "thread") {
24786
25007
  const id = slug(opts.threadId || "default");
24787
- return { key: `thread:${id}`, dirName: path9.join("threads", id) };
25008
+ return { key: `thread:${id}`, dirName: path11.join("threads", id) };
24788
25009
  }
24789
25010
  if (scope === "user") {
24790
25011
  const id = slug(opts.userId || "anonymous");
24791
- return { key: `user:${id}`, dirName: path9.join("users", id) };
25012
+ return { key: `user:${id}`, dirName: path11.join("users", id) };
24792
25013
  }
24793
25014
  return { key: "project", dirName: "_project" };
24794
25015
  }
@@ -24797,30 +25018,30 @@ function makeRunId() {
24797
25018
  return `${t}-${Math.random().toString(36).slice(2, 8)}`;
24798
25019
  }
24799
25020
  function statePath(scopeDir) {
24800
- return path9.join(scopeDir, "_state.json");
25021
+ return path11.join(scopeDir, "_state.json");
24801
25022
  }
24802
25023
  function loadState(scopeDir) {
24803
25024
  try {
24804
- return JSON.parse(fs8.readFileSync(statePath(scopeDir), "utf-8"));
25025
+ return JSON.parse(fs9.readFileSync(statePath(scopeDir), "utf-8"));
24805
25026
  } catch {
24806
25027
  return { turns: 0, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
24807
25028
  }
24808
25029
  }
24809
25030
  function saveState(scopeDir, state) {
24810
25031
  try {
24811
- fs8.mkdirSync(scopeDir, { recursive: true });
24812
- fs8.writeFileSync(statePath(scopeDir), JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
25032
+ fs9.mkdirSync(scopeDir, { recursive: true });
25033
+ fs9.writeFileSync(statePath(scopeDir), JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
24813
25034
  } catch (err) {
24814
25035
  logger.warn(`[CodingAgent] failed to persist session state: ${err?.message ?? err}`);
24815
25036
  }
24816
25037
  }
24817
25038
  async function clearTransient(dir) {
24818
- await fsp.rm(path9.join(dir, "input"), { recursive: true, force: true });
25039
+ await fsp.rm(path11.join(dir, "input"), { recursive: true, force: true });
24819
25040
  }
24820
25041
  function createWorkspace(dir, runId) {
24821
- const abs = (rel) => path9.join(dir, rel);
25042
+ const abs = (rel) => path11.join(dir, rel);
24822
25043
  const ensureDir = async (rel) => {
24823
- await fsp.mkdir(path9.dirname(abs(rel)), { recursive: true });
25044
+ await fsp.mkdir(path11.dirname(abs(rel)), { recursive: true });
24824
25045
  };
24825
25046
  return {
24826
25047
  dir,
@@ -24849,7 +25070,7 @@ function createWorkspace(dir, runId) {
24849
25070
  }
24850
25071
  var INSTRUCTIONS_FILE = "system.md";
24851
25072
  async function installInstructions(def, ws) {
24852
- const src = path9.join(resolvePromptDir(def), "system.md");
25073
+ const src = path11.join(resolvePromptDir(def), "system.md");
24853
25074
  try {
24854
25075
  await ws.write(INSTRUCTIONS_FILE, await fsp.readFile(src, "utf-8"));
24855
25076
  } catch {
@@ -24863,7 +25084,7 @@ async function pruneThreads(agentId, threadsDir, keep) {
24863
25084
  const dirs = (await fsp.readdir(threadsDir, { withFileTypes: true })).filter((e) => e.isDirectory());
24864
25085
  if (dirs.length <= keep) return;
24865
25086
  const aged = await Promise.all(dirs.map(async (e) => {
24866
- const dir = path9.join(threadsDir, e.name);
25087
+ const dir = path11.join(threadsDir, e.name);
24867
25088
  const stat2 = await fsp.stat(statePath(dir)).catch(() => fsp.stat(dir));
24868
25089
  return { name: e.name, dir, used: stat2.mtimeMs };
24869
25090
  }));
@@ -24883,7 +25104,7 @@ async function pruneRuns(agentDir, keep) {
24883
25104
  try {
24884
25105
  const entries = (await fsp.readdir(agentDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
24885
25106
  for (const name of entries.slice(0, Math.max(0, entries.length - keep))) {
24886
- await fsp.rm(path9.join(agentDir, name), { recursive: true, force: true });
25107
+ await fsp.rm(path11.join(agentDir, name), { recursive: true, force: true });
24887
25108
  }
24888
25109
  } catch {
24889
25110
  }
@@ -24898,17 +25119,17 @@ async function runCodingAgent(agentId, options = {}) {
24898
25119
  }
24899
25120
  const cfg = getCodingAgentsConfig();
24900
25121
  const runId = makeRunId();
24901
- const agentDir = path9.join(cfg.runtimeDir, def.id);
25122
+ const agentDir = path11.join(cfg.runtimeDir, def.id);
24902
25123
  const policy = def.session ?? {};
24903
25124
  const persistentWorkspace = policy.workspace === "persistent";
24904
25125
  const persistentSession = policy.scope === "persistent";
24905
25126
  const runAsJob = !(options.chat === true && def.mode === "task");
24906
25127
  const { key: scopeKey, dirName } = resolveScope(def, { userId: options.userId, threadId: options.threadId });
24907
- const scopeDir = path9.join(agentDir, dirName);
24908
- const dir = options.workspaceDir || (persistentWorkspace ? scopeDir : path9.join(scopeDir, "runs", runId));
24909
- fs8.mkdirSync(dir, { recursive: true });
25128
+ const scopeDir = path11.join(agentDir, dirName);
25129
+ const dir = options.workspaceDir || (persistentWorkspace ? scopeDir : path11.join(scopeDir, "runs", runId));
25130
+ fs9.mkdirSync(dir, { recursive: true });
24910
25131
  if (persistentWorkspace && !options.workspaceDir) {
24911
- fs8.mkdirSync(path9.join(dir, "memory"), { recursive: true });
25132
+ fs9.mkdirSync(path11.join(dir, "memory"), { recursive: true });
24912
25133
  if (runAsJob) await clearTransient(dir);
24913
25134
  }
24914
25135
  const lockKey = persistentWorkspace || persistentSession ? `${def.id}:${scopeKey}` : `dir:${dir}`;
@@ -25009,7 +25230,7 @@ async function runCodingAgent(agentId, options = {}) {
25009
25230
  onUsage: ctx.onUsage,
25010
25231
  // Opt-in only: see stopOnDeliverable. Letting the turn end naturally
25011
25232
  // is what makes usage reporting work at all.
25012
- ...outputPath && def.stopOnDeliverable ? { doneWhen: () => fs8.existsSync(outputPath) } : {}
25233
+ ...outputPath && def.stopOnDeliverable ? { doneWhen: () => fs9.existsSync(outputPath) } : {}
25013
25234
  },
25014
25235
  timeout.signal
25015
25236
  );
@@ -25113,9 +25334,9 @@ Fix ONLY that, and rewrite the same file at the same path. Do not re-run any que
25113
25334
  logger.info(
25114
25335
  `[CodingAgent:${def.id}] ${runAsJob ? "run" : "chat"}=${runId} status=${record.status} ms=${record.ms} tokens=${record.usage.inputTokens}/${record.usage.outputTokens} tools=${record.usage.toolCalls}` + (record.applied ? ` applied="${record.applied.summary}"` : "") + (record.error ? ` error="${record.error}"` : "")
25115
25336
  );
25116
- if (!options.workspaceDir && !persistentWorkspace) await pruneRuns(path9.join(scopeDir, "runs"), cfg.keepRuns);
25117
- if (!options.workspaceDir && dirName.startsWith(`threads${path9.sep}`)) {
25118
- await pruneThreads(def.id, path9.join(agentDir, "threads"), cfg.keepThreads);
25337
+ if (!options.workspaceDir && !persistentWorkspace) await pruneRuns(path11.join(scopeDir, "runs"), cfg.keepRuns);
25338
+ if (!options.workspaceDir && dirName.startsWith(`threads${path11.sep}`)) {
25339
+ await pruneThreads(def.id, path11.join(agentDir, "threads"), cfg.keepThreads);
25119
25340
  }
25120
25341
  }
25121
25342
  }
@@ -25175,14 +25396,14 @@ ${assertReadOnlySql.toString().replace("export function", "function")}
25175
25396
  `;
25176
25397
 
25177
25398
  // src/codingAgent/tools/shims.ts
25178
- import * as fs9 from "fs";
25179
- import * as path10 from "path";
25180
- var resolve2 = (target, name) => typeof target === "string" ? path10.join(target, name) : target.pathTo(name);
25399
+ import * as fs10 from "fs";
25400
+ import * as path12 from "path";
25401
+ var resolve3 = (target, name) => typeof target === "string" ? path12.join(target, name) : target.pathTo(name);
25181
25402
  function writeShims(defs, target) {
25182
25403
  for (const def of defs) {
25183
- const file = resolve2(target, def.name);
25184
- fs9.writeFileSync(file, def.body, "utf-8");
25185
- fs9.chmodSync(file, 493);
25404
+ const file = resolve3(target, def.name);
25405
+ fs10.writeFileSync(file, def.body, "utf-8");
25406
+ fs10.chmodSync(file, 493);
25186
25407
  }
25187
25408
  return defs.map((def) => ({ name: `./${def.name}`, summary: def.summary }));
25188
25409
  }
@@ -25441,17 +25662,17 @@ console.log(${JSON.stringify(
25441
25662
  }
25442
25663
 
25443
25664
  // src/codingAgent/triggers.ts
25444
- import * as fs10 from "fs";
25445
- import * as path11 from "path";
25665
+ import * as fs11 from "fs";
25666
+ import * as path13 from "path";
25446
25667
  var EVENT_BURST_CAP = 200;
25447
25668
  var capEvents = (events) => events.length > EVENT_BURST_CAP ? events.slice(-EVENT_BURST_CAP) : events;
25448
25669
  function statePath2(agentId) {
25449
- return path11.join(getCodingAgentsConfig().runtimeDir, agentId, "_triggers.json");
25670
+ return path13.join(getCodingAgentsConfig().runtimeDir, agentId, "_triggers.json");
25450
25671
  }
25451
25672
  function loadTriggerState(agentId) {
25452
25673
  let raw;
25453
25674
  try {
25454
- raw = JSON.parse(fs10.readFileSync(statePath2(agentId), "utf-8"));
25675
+ raw = JSON.parse(fs11.readFileSync(statePath2(agentId), "utf-8"));
25455
25676
  } catch {
25456
25677
  return { counts: {}, pending: {} };
25457
25678
  }
@@ -25471,8 +25692,8 @@ function loadTriggerState(agentId) {
25471
25692
  function saveTriggerState(agentId, state) {
25472
25693
  try {
25473
25694
  const p = statePath2(agentId);
25474
- fs10.mkdirSync(path11.dirname(p), { recursive: true });
25475
- fs10.writeFileSync(p, JSON.stringify(state, null, 2));
25695
+ fs11.mkdirSync(path13.dirname(p), { recursive: true });
25696
+ fs11.writeFileSync(p, JSON.stringify(state, null, 2));
25476
25697
  } catch (err) {
25477
25698
  logger.warn(`[CodingAgent:${agentId}] failed to persist trigger state: ${err?.message ?? err}`);
25478
25699
  }
@@ -25608,13 +25829,13 @@ function describeTriggers() {
25608
25829
  }
25609
25830
 
25610
25831
  // src/codingAgent/collection.ts
25611
- import * as fs11 from "fs";
25612
- import * as path12 from "path";
25832
+ import * as fs12 from "fs";
25833
+ import * as path14 from "path";
25613
25834
  function isChattable(agent) {
25614
25835
  return agent.mode !== "task";
25615
25836
  }
25616
25837
  function registerCodingAgentCollection(sdk, config2 = {}) {
25617
- const runtimeDir = config2.runtimeDir || process.env.CODING_AGENT_RUNTIME_DIR || path12.join(process.cwd(), ".coding-agent-runtime");
25838
+ const runtimeDir = config2.runtimeDir || process.env.CODING_AGENT_RUNTIME_DIR || path14.join(process.cwd(), ".coding-agent-runtime");
25618
25839
  const defaultHarness = config2.harness || process.env.CODING_AGENT_HARNESS || "codex";
25619
25840
  const sessions = /* @__PURE__ */ new Map();
25620
25841
  const running = /* @__PURE__ */ new Set();
@@ -25674,8 +25895,8 @@ function registerCodingAgentCollection(sdk, config2 = {}) {
25674
25895
  ...record.status === "skipped" ? { error: `${def.title} is in the middle of a run \u2014 try again in a moment.` } : record.error ? { error: record.error } : {}
25675
25896
  };
25676
25897
  }
25677
- const cwd = path12.join(runtimeDir, "threads", threadId);
25678
- fs11.mkdirSync(cwd, { recursive: true });
25898
+ const cwd = path14.join(runtimeDir, "threads", threadId);
25899
+ fs12.mkdirSync(cwd, { recursive: true });
25679
25900
  let session = sessions.get(threadId);
25680
25901
  if (!session) {
25681
25902
  session = createCodingAgentSession(harness || defaultHarness, {
@@ -26954,8 +27175,8 @@ function getLLMProviders() {
26954
27175
  }
26955
27176
 
26956
27177
  // src/auth/user-manager.ts
26957
- import fs12 from "fs";
26958
- import path13 from "path";
27178
+ import fs13 from "fs";
27179
+ import path15 from "path";
26959
27180
  import os2 from "os";
26960
27181
  var UserManager = class {
26961
27182
  /**
@@ -26968,7 +27189,7 @@ var UserManager = class {
26968
27189
  this.hasChanged = false;
26969
27190
  this.syncInterval = null;
26970
27191
  this.isInitialized = false;
26971
- this.filePath = path13.join(os2.homedir(), ".superatom", "projects", projectId, "users.json");
27192
+ this.filePath = path15.join(os2.homedir(), ".superatom", "projects", projectId, "users.json");
26972
27193
  this.syncIntervalMs = syncIntervalMs;
26973
27194
  }
26974
27195
  /**
@@ -26993,20 +27214,20 @@ var UserManager = class {
26993
27214
  */
26994
27215
  async loadUsersFromFile() {
26995
27216
  try {
26996
- const dir = path13.dirname(this.filePath);
26997
- if (!fs12.existsSync(dir)) {
27217
+ const dir = path15.dirname(this.filePath);
27218
+ if (!fs13.existsSync(dir)) {
26998
27219
  logger.info(`Creating directory structure: ${dir}`);
26999
- fs12.mkdirSync(dir, { recursive: true });
27220
+ fs13.mkdirSync(dir, { recursive: true });
27000
27221
  }
27001
- if (!fs12.existsSync(this.filePath)) {
27222
+ if (!fs13.existsSync(this.filePath)) {
27002
27223
  logger.info(`Users file does not exist at ${this.filePath}, creating with empty users`);
27003
27224
  const initialData = { users: [] };
27004
- fs12.writeFileSync(this.filePath, JSON.stringify(initialData, null, 4));
27225
+ fs13.writeFileSync(this.filePath, JSON.stringify(initialData, null, 4));
27005
27226
  this.users = [];
27006
27227
  this.hasChanged = false;
27007
27228
  return;
27008
27229
  }
27009
- const fileContent = fs12.readFileSync(this.filePath, "utf-8");
27230
+ const fileContent = fs13.readFileSync(this.filePath, "utf-8");
27010
27231
  const rawData = JSON.parse(fileContent);
27011
27232
  const validatedData = UsersDataSchema.parse(rawData);
27012
27233
  this.users = validatedData.users;
@@ -27025,16 +27246,16 @@ var UserManager = class {
27025
27246
  return;
27026
27247
  }
27027
27248
  try {
27028
- const dir = path13.dirname(this.filePath);
27029
- if (!fs12.existsSync(dir)) {
27030
- fs12.mkdirSync(dir, { recursive: true });
27249
+ const dir = path15.dirname(this.filePath);
27250
+ if (!fs13.existsSync(dir)) {
27251
+ fs13.mkdirSync(dir, { recursive: true });
27031
27252
  }
27032
27253
  const usersToSave = this.users.map((user) => {
27033
27254
  const { wsIds, ...userWithoutWsIds } = user;
27034
27255
  return userWithoutWsIds;
27035
27256
  });
27036
27257
  const data = { users: usersToSave };
27037
- fs12.writeFileSync(this.filePath, JSON.stringify(data, null, 4));
27258
+ fs13.writeFileSync(this.filePath, JSON.stringify(data, null, 4));
27038
27259
  this.hasChanged = false;
27039
27260
  logger.debug(`Synced ${this.users.length} users to file (wsIds excluded)`);
27040
27261
  } catch (error) {
@@ -27252,8 +27473,8 @@ var UserManager = class {
27252
27473
  };
27253
27474
 
27254
27475
  // src/dashboards/dashboard-manager.ts
27255
- import fs13 from "fs";
27256
- import path14 from "path";
27476
+ import fs14 from "fs";
27477
+ import path16 from "path";
27257
27478
  import os3 from "os";
27258
27479
  var DashboardManager = class {
27259
27480
  /**
@@ -27262,7 +27483,7 @@ var DashboardManager = class {
27262
27483
  */
27263
27484
  constructor(projectId = "snowflake-dataset") {
27264
27485
  this.projectId = projectId;
27265
- this.dashboardsBasePath = path14.join(
27486
+ this.dashboardsBasePath = path16.join(
27266
27487
  os3.homedir(),
27267
27488
  ".superatom",
27268
27489
  "projects",
@@ -27276,7 +27497,7 @@ var DashboardManager = class {
27276
27497
  * @returns Full path to dashboard data.json file
27277
27498
  */
27278
27499
  getDashboardPath(dashboardId) {
27279
- return path14.join(this.dashboardsBasePath, dashboardId, "data.json");
27500
+ return path16.join(this.dashboardsBasePath, dashboardId, "data.json");
27280
27501
  }
27281
27502
  /**
27282
27503
  * Create a new dashboard
@@ -27286,13 +27507,13 @@ var DashboardManager = class {
27286
27507
  */
27287
27508
  createDashboard(dashboardId, dashboard) {
27288
27509
  const dashboardPath = this.getDashboardPath(dashboardId);
27289
- const dashboardDir = path14.dirname(dashboardPath);
27290
- if (fs13.existsSync(dashboardPath)) {
27510
+ const dashboardDir = path16.dirname(dashboardPath);
27511
+ if (fs14.existsSync(dashboardPath)) {
27291
27512
  throw new Error(`Dashboard '${dashboardId}' already exists`);
27292
27513
  }
27293
27514
  const validated = DSLRendererPropsSchema.parse(dashboard);
27294
- fs13.mkdirSync(dashboardDir, { recursive: true });
27295
- fs13.writeFileSync(dashboardPath, JSON.stringify(validated, null, 4));
27515
+ fs14.mkdirSync(dashboardDir, { recursive: true });
27516
+ fs14.writeFileSync(dashboardPath, JSON.stringify(validated, null, 4));
27296
27517
  logger.info(`Dashboard created: ${dashboardId}`);
27297
27518
  return validated;
27298
27519
  }
@@ -27303,12 +27524,12 @@ var DashboardManager = class {
27303
27524
  */
27304
27525
  getDashboard(dashboardId) {
27305
27526
  const dashboardPath = this.getDashboardPath(dashboardId);
27306
- if (!fs13.existsSync(dashboardPath)) {
27527
+ if (!fs14.existsSync(dashboardPath)) {
27307
27528
  logger.warn(`Dashboard not found: ${dashboardId}`);
27308
27529
  return null;
27309
27530
  }
27310
27531
  try {
27311
- const fileContent = fs13.readFileSync(dashboardPath, "utf-8");
27532
+ const fileContent = fs14.readFileSync(dashboardPath, "utf-8");
27312
27533
  const dashboard = JSON.parse(fileContent);
27313
27534
  const validated = DSLRendererPropsSchema.parse(dashboard);
27314
27535
  return validated;
@@ -27322,16 +27543,16 @@ var DashboardManager = class {
27322
27543
  * @returns Array of dashboard objects with their IDs
27323
27544
  */
27324
27545
  getAllDashboards() {
27325
- if (!fs13.existsSync(this.dashboardsBasePath)) {
27326
- fs13.mkdirSync(this.dashboardsBasePath, { recursive: true });
27546
+ if (!fs14.existsSync(this.dashboardsBasePath)) {
27547
+ fs14.mkdirSync(this.dashboardsBasePath, { recursive: true });
27327
27548
  return [];
27328
27549
  }
27329
27550
  const dashboards = [];
27330
27551
  try {
27331
- const dashboardDirs = fs13.readdirSync(this.dashboardsBasePath);
27552
+ const dashboardDirs = fs14.readdirSync(this.dashboardsBasePath);
27332
27553
  for (const dashboardId of dashboardDirs) {
27333
27554
  const dashboardPath = this.getDashboardPath(dashboardId);
27334
- if (fs13.existsSync(dashboardPath)) {
27555
+ if (fs14.existsSync(dashboardPath)) {
27335
27556
  const dashboard = this.getDashboard(dashboardId);
27336
27557
  if (dashboard) {
27337
27558
  dashboards.push({ dashboardId, dashboard });
@@ -27353,13 +27574,13 @@ var DashboardManager = class {
27353
27574
  */
27354
27575
  updateDashboard(dashboardId, dashboard) {
27355
27576
  const dashboardPath = this.getDashboardPath(dashboardId);
27356
- if (!fs13.existsSync(dashboardPath)) {
27577
+ if (!fs14.existsSync(dashboardPath)) {
27357
27578
  logger.warn(`Dashboard not found for update: ${dashboardId}`);
27358
27579
  return null;
27359
27580
  }
27360
27581
  try {
27361
27582
  const validated = DSLRendererPropsSchema.parse(dashboard);
27362
- fs13.writeFileSync(dashboardPath, JSON.stringify(validated, null, 4));
27583
+ fs14.writeFileSync(dashboardPath, JSON.stringify(validated, null, 4));
27363
27584
  logger.info(`Dashboard updated: ${dashboardId}`);
27364
27585
  return validated;
27365
27586
  } catch (error) {
@@ -27374,13 +27595,13 @@ var DashboardManager = class {
27374
27595
  */
27375
27596
  deleteDashboard(dashboardId) {
27376
27597
  const dashboardPath = this.getDashboardPath(dashboardId);
27377
- const dashboardDir = path14.dirname(dashboardPath);
27378
- if (!fs13.existsSync(dashboardPath)) {
27598
+ const dashboardDir = path16.dirname(dashboardPath);
27599
+ if (!fs14.existsSync(dashboardPath)) {
27379
27600
  logger.warn(`Dashboard not found for deletion: ${dashboardId}`);
27380
27601
  return false;
27381
27602
  }
27382
27603
  try {
27383
- fs13.rmSync(dashboardDir, { recursive: true, force: true });
27604
+ fs14.rmSync(dashboardDir, { recursive: true, force: true });
27384
27605
  logger.info(`Dashboard deleted: ${dashboardId}`);
27385
27606
  return true;
27386
27607
  } catch (error) {
@@ -27395,21 +27616,21 @@ var DashboardManager = class {
27395
27616
  */
27396
27617
  dashboardExists(dashboardId) {
27397
27618
  const dashboardPath = this.getDashboardPath(dashboardId);
27398
- return fs13.existsSync(dashboardPath);
27619
+ return fs14.existsSync(dashboardPath);
27399
27620
  }
27400
27621
  /**
27401
27622
  * Get dashboard count
27402
27623
  * @returns Number of dashboards
27403
27624
  */
27404
27625
  getDashboardCount() {
27405
- if (!fs13.existsSync(this.dashboardsBasePath)) {
27626
+ if (!fs14.existsSync(this.dashboardsBasePath)) {
27406
27627
  return 0;
27407
27628
  }
27408
27629
  try {
27409
- const dashboardDirs = fs13.readdirSync(this.dashboardsBasePath);
27630
+ const dashboardDirs = fs14.readdirSync(this.dashboardsBasePath);
27410
27631
  return dashboardDirs.filter((dir) => {
27411
27632
  const dashboardPath = this.getDashboardPath(dir);
27412
- return fs13.existsSync(dashboardPath);
27633
+ return fs14.existsSync(dashboardPath);
27413
27634
  }).length;
27414
27635
  } catch (error) {
27415
27636
  logger.error("Failed to get dashboard count:", error);
@@ -27419,8 +27640,8 @@ var DashboardManager = class {
27419
27640
  };
27420
27641
 
27421
27642
  // src/reports/report-manager.ts
27422
- import fs14 from "fs";
27423
- import path15 from "path";
27643
+ import fs15 from "fs";
27644
+ import path17 from "path";
27424
27645
  import os4 from "os";
27425
27646
  var ReportManager = class {
27426
27647
  /**
@@ -27429,7 +27650,7 @@ var ReportManager = class {
27429
27650
  */
27430
27651
  constructor(projectId = "snowflake-dataset") {
27431
27652
  this.projectId = projectId;
27432
- this.reportsBasePath = path15.join(
27653
+ this.reportsBasePath = path17.join(
27433
27654
  os4.homedir(),
27434
27655
  ".superatom",
27435
27656
  "projects",
@@ -27443,7 +27664,7 @@ var ReportManager = class {
27443
27664
  * @returns Full path to report data.json file
27444
27665
  */
27445
27666
  getReportPath(reportId) {
27446
- return path15.join(this.reportsBasePath, reportId, "data.json");
27667
+ return path17.join(this.reportsBasePath, reportId, "data.json");
27447
27668
  }
27448
27669
  /**
27449
27670
  * Create a new report
@@ -27453,13 +27674,13 @@ var ReportManager = class {
27453
27674
  */
27454
27675
  createReport(reportId, report) {
27455
27676
  const reportPath = this.getReportPath(reportId);
27456
- const reportDir = path15.dirname(reportPath);
27457
- if (fs14.existsSync(reportPath)) {
27677
+ const reportDir = path17.dirname(reportPath);
27678
+ if (fs15.existsSync(reportPath)) {
27458
27679
  throw new Error(`Report '${reportId}' already exists`);
27459
27680
  }
27460
27681
  const validated = DSLRendererPropsSchema2.parse(report);
27461
- fs14.mkdirSync(reportDir, { recursive: true });
27462
- fs14.writeFileSync(reportPath, JSON.stringify(validated, null, 4));
27682
+ fs15.mkdirSync(reportDir, { recursive: true });
27683
+ fs15.writeFileSync(reportPath, JSON.stringify(validated, null, 4));
27463
27684
  logger.info(`Report created: ${reportId}`);
27464
27685
  return validated;
27465
27686
  }
@@ -27470,12 +27691,12 @@ var ReportManager = class {
27470
27691
  */
27471
27692
  getReport(reportId) {
27472
27693
  const reportPath = this.getReportPath(reportId);
27473
- if (!fs14.existsSync(reportPath)) {
27694
+ if (!fs15.existsSync(reportPath)) {
27474
27695
  logger.warn(`Report not found: ${reportId}`);
27475
27696
  return null;
27476
27697
  }
27477
27698
  try {
27478
- const fileContent = fs14.readFileSync(reportPath, "utf-8");
27699
+ const fileContent = fs15.readFileSync(reportPath, "utf-8");
27479
27700
  const report = JSON.parse(fileContent);
27480
27701
  const validated = DSLRendererPropsSchema2.parse(report);
27481
27702
  return validated;
@@ -27489,16 +27710,16 @@ var ReportManager = class {
27489
27710
  * @returns Array of report objects with their IDs
27490
27711
  */
27491
27712
  getAllReports() {
27492
- if (!fs14.existsSync(this.reportsBasePath)) {
27493
- fs14.mkdirSync(this.reportsBasePath, { recursive: true });
27713
+ if (!fs15.existsSync(this.reportsBasePath)) {
27714
+ fs15.mkdirSync(this.reportsBasePath, { recursive: true });
27494
27715
  return [];
27495
27716
  }
27496
27717
  const reports = [];
27497
27718
  try {
27498
- const reportDirs = fs14.readdirSync(this.reportsBasePath);
27719
+ const reportDirs = fs15.readdirSync(this.reportsBasePath);
27499
27720
  for (const reportId of reportDirs) {
27500
27721
  const reportPath = this.getReportPath(reportId);
27501
- if (fs14.existsSync(reportPath)) {
27722
+ if (fs15.existsSync(reportPath)) {
27502
27723
  const report = this.getReport(reportId);
27503
27724
  if (report) {
27504
27725
  reports.push({ reportId, report });
@@ -27520,13 +27741,13 @@ var ReportManager = class {
27520
27741
  */
27521
27742
  updateReport(reportId, report) {
27522
27743
  const reportPath = this.getReportPath(reportId);
27523
- if (!fs14.existsSync(reportPath)) {
27744
+ if (!fs15.existsSync(reportPath)) {
27524
27745
  logger.warn(`Report not found for update: ${reportId}`);
27525
27746
  return null;
27526
27747
  }
27527
27748
  try {
27528
27749
  const validated = DSLRendererPropsSchema2.parse(report);
27529
- fs14.writeFileSync(reportPath, JSON.stringify(validated, null, 4));
27750
+ fs15.writeFileSync(reportPath, JSON.stringify(validated, null, 4));
27530
27751
  logger.info(`Report updated: ${reportId}`);
27531
27752
  return validated;
27532
27753
  } catch (error) {
@@ -27541,13 +27762,13 @@ var ReportManager = class {
27541
27762
  */
27542
27763
  deleteReport(reportId) {
27543
27764
  const reportPath = this.getReportPath(reportId);
27544
- const reportDir = path15.dirname(reportPath);
27545
- if (!fs14.existsSync(reportPath)) {
27765
+ const reportDir = path17.dirname(reportPath);
27766
+ if (!fs15.existsSync(reportPath)) {
27546
27767
  logger.warn(`Report not found for deletion: ${reportId}`);
27547
27768
  return false;
27548
27769
  }
27549
27770
  try {
27550
- fs14.rmSync(reportDir, { recursive: true, force: true });
27771
+ fs15.rmSync(reportDir, { recursive: true, force: true });
27551
27772
  logger.info(`Report deleted: ${reportId}`);
27552
27773
  return true;
27553
27774
  } catch (error) {
@@ -27562,21 +27783,21 @@ var ReportManager = class {
27562
27783
  */
27563
27784
  reportExists(reportId) {
27564
27785
  const reportPath = this.getReportPath(reportId);
27565
- return fs14.existsSync(reportPath);
27786
+ return fs15.existsSync(reportPath);
27566
27787
  }
27567
27788
  /**
27568
27789
  * Get report count
27569
27790
  * @returns Number of reports
27570
27791
  */
27571
27792
  getReportCount() {
27572
- if (!fs14.existsSync(this.reportsBasePath)) {
27793
+ if (!fs15.existsSync(this.reportsBasePath)) {
27573
27794
  return 0;
27574
27795
  }
27575
27796
  try {
27576
- const reportDirs = fs14.readdirSync(this.reportsBasePath);
27797
+ const reportDirs = fs15.readdirSync(this.reportsBasePath);
27577
27798
  return reportDirs.filter((dir) => {
27578
27799
  const reportPath = this.getReportPath(dir);
27579
- return fs14.existsSync(reportPath);
27800
+ return fs15.existsSync(reportPath);
27580
27801
  }).length;
27581
27802
  } catch (error) {
27582
27803
  logger.error("Failed to get report count:", error);
@@ -28070,8 +28291,8 @@ var CleanupService = class _CleanupService {
28070
28291
  };
28071
28292
 
28072
28293
  // src/dashboardAgent/collection.ts
28073
- import path16 from "path";
28074
- import fs15 from "fs";
28294
+ import path18 from "path";
28295
+ import fs16 from "fs";
28075
28296
 
28076
28297
  // src/dashboardAgent/tools.ts
28077
28298
  import { Type } from "typebox";
@@ -28283,12 +28504,16 @@ function loadPi2() {
28283
28504
  if (!piModulePromise2) piModulePromise2 = dynamicImport3("@earendil-works/pi-coding-agent");
28284
28505
  return piModulePromise2;
28285
28506
  }
28286
- var authAndRegistry2 = null;
28287
- function getAuthAndRegistry2() {
28288
- if (!authAndRegistry2) {
28289
- authAndRegistry2 = loadPi2().then(({ AuthStorage, ModelRegistry }) => {
28290
- const authStorage = AuthStorage.create();
28291
- const modelRegistry = ModelRegistry.create(authStorage);
28507
+ var authAndRegistry = null;
28508
+ function getAuthAndRegistry() {
28509
+ if (!authAndRegistry) {
28510
+ authAndRegistry = loadPi2().then(async ({ ModelRuntime, getAgentDir }) => {
28511
+ const agentDir = getAgentDir();
28512
+ const modelRegistry = await ModelRuntime.create({
28513
+ authPath: `${agentDir}/auth.json`,
28514
+ modelsStorePath: `${agentDir}/models-store.json`,
28515
+ allowModelNetwork: true
28516
+ });
28292
28517
  const proxyRoot = process.env.SUPERATOM_LLM_PROXY_URL;
28293
28518
  if (proxyRoot) {
28294
28519
  modelRegistry.registerProvider("openrouter", {
@@ -28300,10 +28525,10 @@ function getAuthAndRegistry2() {
28300
28525
  '[DashboardAgent] OPENROUTER_API_KEY is a proxy key (sk-prox-\u2026) but SUPERATOM_LLM_PROXY_URL is not set \u2014 requests would be sent directly to openrouter.ai, which will 401 ("Missing Authentication header"). Set SUPERATOM_LLM_PROXY_URL (e.g. https://ai.superatom.ai) in backend/.env.'
28301
28526
  );
28302
28527
  }
28303
- return { authStorage, modelRegistry };
28528
+ return { modelRuntime: modelRegistry };
28304
28529
  });
28305
28530
  }
28306
- return authAndRegistry2;
28531
+ return authAndRegistry;
28307
28532
  }
28308
28533
  async function preResolveSchema(sdk, tool, intent, projectId) {
28309
28534
  const tier = tool.schemaTier;
@@ -28399,16 +28624,16 @@ function textDelta(event) {
28399
28624
  }
28400
28625
  var runningDashboards = /* @__PURE__ */ new Set();
28401
28626
  function registerDashboardAgentCollection(sdk, config2 = {}) {
28402
- const piCwd = config2.cwd || process.env.PI_AGENT_DIR || path16.join(process.cwd(), ".prompts", "dashboard-agent");
28403
- const runtimeDir = config2.runtimeDir || process.env.PI_AGENT_RUNTIME_DIR || path16.join(process.cwd(), ".pi-dashboard-agent-runtime");
28627
+ const piCwd = config2.cwd || process.env.PI_AGENT_DIR || path18.join(process.cwd(), ".prompts", "dashboard-agent");
28628
+ const runtimeDir = config2.runtimeDir || process.env.PI_AGENT_RUNTIME_DIR || path18.join(process.cwd(), ".pi-dashboard-agent-runtime");
28404
28629
  const freshSession = config2.freshSession ?? process.env.PI_AGENT_FRESH_SESSION !== "false";
28405
- const SESSIONS_FILE = path16.join(runtimeDir, ".pi-dashboard-sessions.json");
28406
- const DRAFTS_DIR = path16.join(runtimeDir, "drafts");
28407
- const DASHBOARD_LOG_FILE = path16.join(runtimeDir, "dashboard.log");
28630
+ const SESSIONS_FILE = path18.join(runtimeDir, ".pi-dashboard-sessions.json");
28631
+ const DRAFTS_DIR = path18.join(runtimeDir, "drafts");
28632
+ const DASHBOARD_LOG_FILE = path18.join(runtimeDir, "dashboard.log");
28408
28633
  function appendLog(text) {
28409
28634
  try {
28410
- fs15.mkdirSync(runtimeDir, { recursive: true });
28411
- fs15.appendFileSync(DASHBOARD_LOG_FILE, text);
28635
+ fs16.mkdirSync(runtimeDir, { recursive: true });
28636
+ fs16.appendFileSync(DASHBOARD_LOG_FILE, text);
28412
28637
  } catch (err) {
28413
28638
  console.warn(`[DashboardAgent] Failed to write dashboard.log: ${err?.message ?? err}`);
28414
28639
  }
@@ -28420,15 +28645,15 @@ ${fullPrompt}
28420
28645
  --- Response ---
28421
28646
  `;
28422
28647
  try {
28423
- fs15.mkdirSync(runtimeDir, { recursive: true });
28424
- fs15.writeFileSync(DASHBOARD_LOG_FILE, header);
28648
+ fs16.mkdirSync(runtimeDir, { recursive: true });
28649
+ fs16.writeFileSync(DASHBOARD_LOG_FILE, header);
28425
28650
  } catch (err) {
28426
28651
  console.warn(`[DashboardAgent] Failed to write dashboard.log: ${err?.message ?? err}`);
28427
28652
  }
28428
28653
  }
28429
28654
  function readDraft(dashboardId) {
28430
28655
  try {
28431
- const raw = fs15.readFileSync(path16.join(DRAFTS_DIR, `${dashboardId}.json`), "utf-8");
28656
+ const raw = fs16.readFileSync(path18.join(DRAFTS_DIR, `${dashboardId}.json`), "utf-8");
28432
28657
  return JSON.parse(raw);
28433
28658
  } catch (err) {
28434
28659
  console.warn(`[DashboardAgent] Failed to read draft for ${dashboardId}: ${err?.message ?? err}`);
@@ -28437,7 +28662,7 @@ ${fullPrompt}
28437
28662
  }
28438
28663
  function loadSessionMap() {
28439
28664
  try {
28440
- return JSON.parse(fs15.readFileSync(SESSIONS_FILE, "utf-8"));
28665
+ return JSON.parse(fs16.readFileSync(SESSIONS_FILE, "utf-8"));
28441
28666
  } catch {
28442
28667
  return {};
28443
28668
  }
@@ -28446,8 +28671,8 @@ ${fullPrompt}
28446
28671
  const map = loadSessionMap();
28447
28672
  map[dashboardId] = sessionFile;
28448
28673
  try {
28449
- fs15.mkdirSync(piCwd, { recursive: true });
28450
- fs15.writeFileSync(SESSIONS_FILE, JSON.stringify(map, null, 2));
28674
+ fs16.mkdirSync(piCwd, { recursive: true });
28675
+ fs16.writeFileSync(SESSIONS_FILE, JSON.stringify(map, null, 2));
28451
28676
  } catch (err) {
28452
28677
  console.warn(`[DashboardAgent] Failed to persist session file for ${dashboardId}: ${err?.message ?? err}`);
28453
28678
  }
@@ -28517,14 +28742,14 @@ ${fullPrompt}
28517
28742
  }
28518
28743
  try {
28519
28744
  const { createAgentSession, SessionManager } = await loadPi2();
28520
- const { authStorage, modelRegistry } = await getAuthAndRegistry2();
28745
+ const { modelRuntime: modelRuntime2 } = await getAuthAndRegistry();
28521
28746
  let model;
28522
28747
  const explicitProvider = config2.provider || process.env.PI_AGENT_PROVIDER;
28523
28748
  const explicitModel = config2.model || process.env.PI_AGENT_MODEL;
28524
28749
  if (explicitProvider || explicitModel) {
28525
28750
  const providerName = explicitProvider || "openrouter";
28526
28751
  const modelId = explicitModel || "anthropic/claude-sonnet-4.5";
28527
- model = modelRegistry.find(providerName, modelId);
28752
+ model = modelRuntime2.getModel(providerName, modelId);
28528
28753
  if (!model) {
28529
28754
  return { success: false, error: `Model not found: ${providerName}/${modelId}. Check the configured provider/model and that its API key is configured.` };
28530
28755
  }
@@ -28537,8 +28762,8 @@ ${fullPrompt}
28537
28762
  { provider: "groq", modelId: "llama-3.3-70b-versatile" }
28538
28763
  ];
28539
28764
  for (const candidate of FALLBACK_MODELS) {
28540
- const found = modelRegistry.find(candidate.provider, candidate.modelId);
28541
- if (found && modelRegistry.hasConfiguredAuth(found)) {
28765
+ const found = modelRuntime2.getModel(candidate.provider, candidate.modelId);
28766
+ if (found && modelRuntime2.hasConfiguredAuth(found.provider)) {
28542
28767
  model = found;
28543
28768
  break;
28544
28769
  }
@@ -28623,7 +28848,7 @@ ${turn.responseText ?? ""}`).join("\n\n");
28623
28848
  console.warn(`[DashboardAgent] Failed to fetch previous responses for ${dashboardId}: ${err?.message ?? err}`);
28624
28849
  }
28625
28850
  }
28626
- const draftPath = path16.join(DRAFTS_DIR, `${dashboardId}.json`);
28851
+ const draftPath = path18.join(DRAFTS_DIR, `${dashboardId}.json`);
28627
28852
  const fullPrompt = [
28628
28853
  `Dashboard ID: ${dashboardId}.`,
28629
28854
  projectId ? `Project ID: ${projectId}.` : null,
@@ -28655,7 +28880,7 @@ ${previousResponseText}` : null,
28655
28880
  let sessionManager;
28656
28881
  if (!freshSession) {
28657
28882
  const existingSessionFile = loadSessionMap()[dashboardId];
28658
- if (existingSessionFile && fs15.existsSync(existingSessionFile)) {
28883
+ if (existingSessionFile && fs16.existsSync(existingSessionFile)) {
28659
28884
  sessionManager = SessionManager.open(existingSessionFile, runtimeDir);
28660
28885
  console.log(`[DashboardAgent] Resuming session file ${existingSessionFile} for dashboard ${dashboardId}`);
28661
28886
  } else {
@@ -28672,8 +28897,7 @@ ${previousResponseText}` : null,
28672
28897
  const created = await createAgentSession({
28673
28898
  cwd: piCwd,
28674
28899
  sessionManager,
28675
- authStorage,
28676
- modelRegistry,
28900
+ modelRuntime: modelRuntime2,
28677
28901
  model,
28678
28902
  customTools
28679
28903
  });
@@ -28683,8 +28907,7 @@ ${previousResponseText}` : null,
28683
28907
  const created = await createAgentSession({
28684
28908
  cwd: piCwd,
28685
28909
  sessionManager: SessionManager.create(piCwd, runtimeDir),
28686
- authStorage,
28687
- modelRegistry,
28910
+ modelRuntime: modelRuntime2,
28688
28911
  model,
28689
28912
  customTools
28690
28913
  });
@@ -28736,7 +28959,7 @@ ${description}
28736
28959
  };
28737
28960
  signal?.addEventListener("abort", onAbort, { once: true });
28738
28961
  try {
28739
- fs15.unlinkSync(draftPath);
28962
+ fs16.unlinkSync(draftPath);
28740
28963
  } catch {
28741
28964
  }
28742
28965
  let result;
@@ -28756,7 +28979,7 @@ ${description}
28756
28979
  signal?.removeEventListener("abort", onAbort);
28757
28980
  if (freshSession && session.sessionFile) {
28758
28981
  try {
28759
- fs15.unlinkSync(session.sessionFile);
28982
+ fs16.unlinkSync(session.sessionFile);
28760
28983
  } catch (err) {
28761
28984
  console.warn(`[DashboardAgent] Failed to delete fresh-session file ${session.sessionFile}: ${err?.message ?? err}`);
28762
28985
  }
@@ -28956,6 +29179,9 @@ var SuperatomSDK = class {
28956
29179
  if (config2.queryCacheTTL !== void 0) {
28957
29180
  queryCache.setTTL(config2.queryCacheTTL);
28958
29181
  }
29182
+ if (config2.queryResultCache === false) {
29183
+ queryCache.disableResultCache();
29184
+ }
28959
29185
  if (config2.dashboardHistoryTTL !== void 0) {
28960
29186
  dashboardConversationHistory.setTTL(config2.dashboardHistoryTTL);
28961
29187
  }
@@ -29040,7 +29266,7 @@ var SuperatomSDK = class {
29040
29266
  logger.info("Already connected to WebSocket");
29041
29267
  return Promise.resolve();
29042
29268
  }
29043
- return new Promise((resolve3, reject) => {
29269
+ return new Promise((resolve4, reject) => {
29044
29270
  try {
29045
29271
  const url = new URL(this.url);
29046
29272
  if (this.apiKey) {
@@ -29056,7 +29282,7 @@ var SuperatomSDK = class {
29056
29282
  logger.info("WebSocket connected successfully");
29057
29283
  this.startHeartbeat();
29058
29284
  this.flushPendingOutbox();
29059
- resolve3();
29285
+ resolve4();
29060
29286
  });
29061
29287
  this.ws.addEventListener("message", (event) => {
29062
29288
  this.handleMessage(event.data);