@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.js CHANGED
@@ -395,7 +395,13 @@ var UserPromptRequestPayloadSchema = import_zod3.z.object({
395
395
  threadId: import_zod3.z.string(),
396
396
  uiBlockId: import_zod3.z.string()
397
397
  }).optional(),
398
- responseMode: import_zod3.z.enum(["component", "text"]).optional()
398
+ responseMode: import_zod3.z.enum(["component", "text"]).optional(),
399
+ /**
400
+ * Where the question was asked from — e.g. the dashboard being viewed. See
401
+ * `PromptContext`. Accepted as anything so a client sending an unexpected shape
402
+ * is never rejected; the handler keeps only the fields it recognises.
403
+ */
404
+ context: import_zod3.z.unknown().optional()
399
405
  });
400
406
  var MessageAuthContextSchema = import_zod3.z.object({
401
407
  userId: import_zod3.z.string(),
@@ -1102,9 +1108,25 @@ var SchemaResponseMessageSchema = import_zod3.z.object({
1102
1108
 
1103
1109
  // src/utils/logger.ts
1104
1110
  var import_fs = __toESM(require("fs"));
1111
+ var import_path = __toESM(require("path"));
1105
1112
  var PREFIX = "[SuperatomSDK]";
1106
- var LOG_FILE_PATH = "superatom-sdk.log";
1107
- var LOGSTREAM = import_fs.default.createWriteStream(LOG_FILE_PATH, { flags: "a" });
1113
+ var LEGACY_LOG_FILE_PATH = "superatom-sdk.log";
1114
+ var LOG_DIR = "logs";
1115
+ function slugifyLabel(label) {
1116
+ return label.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 60);
1117
+ }
1118
+ function buildLogFilePath(label) {
1119
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1120
+ const slug2 = label ? slugifyLabel(label) : "";
1121
+ const filename = slug2 ? `sdk-${stamp}-${slug2}.log` : `sdk-${stamp}.log`;
1122
+ return import_path.default.join(LOG_DIR, filename);
1123
+ }
1124
+ try {
1125
+ import_fs.default.mkdirSync(LOG_DIR, { recursive: true });
1126
+ } catch {
1127
+ }
1128
+ var currentLogFilePath = LEGACY_LOG_FILE_PATH;
1129
+ var LOGSTREAM = import_fs.default.createWriteStream(LEGACY_LOG_FILE_PATH, { flags: "a" });
1108
1130
  var LOG_LEVEL_PRIORITY = {
1109
1131
  errors: 0,
1110
1132
  warnings: 1,
@@ -1189,17 +1211,31 @@ var Logger = class {
1189
1211
  }
1190
1212
  }
1191
1213
  /**
1192
- * Write to log file
1214
+ * Write to log file — each line timestamped so a request that made many
1215
+ * calls (LLM turns, tool dispatches, direct-tool runs) can be read back in
1216
+ * order, matching "know what happened when" rather than just "what
1217
+ * happened".
1193
1218
  */
1194
1219
  file(...args) {
1195
- LOGSTREAM.write(args.join(" ") + "\n");
1220
+ LOGSTREAM.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${args.join(" ")}
1221
+ `);
1222
+ }
1223
+ /** The log file the current request is writing to — print/return this so it's easy to find afterward. */
1224
+ getCurrentLogFilePath() {
1225
+ return currentLogFilePath;
1196
1226
  }
1197
1227
  /**
1198
- * Clear the log file (call at start of new user request)
1228
+ * Start a fresh, uniquely-named log file for a new top-level request
1229
+ * (chat turn, dashComp request, reportComp request, ...) instead of
1230
+ * truncating one shared file — pass a short label (a prompt snippet, a
1231
+ * request type) so the resulting filename is identifiable at a glance.
1232
+ * Falls back to the untimestamped legacy path when `label` is omitted, for
1233
+ * any caller not yet updated to pass one.
1199
1234
  */
1200
- clearFile() {
1235
+ clearFile(label) {
1201
1236
  LOGSTREAM.end();
1202
- LOGSTREAM = import_fs.default.createWriteStream(LOG_FILE_PATH, { flags: "w" });
1237
+ currentLogFilePath = label ? buildLogFilePath(label) : LEGACY_LOG_FILE_PATH;
1238
+ LOGSTREAM = import_fs.default.createWriteStream(currentLogFilePath, { flags: "w" });
1203
1239
  LOGSTREAM.write(`
1204
1240
  ${"=".repeat(80)}
1205
1241
  `);
@@ -1208,6 +1244,7 @@ ${"=".repeat(80)}
1208
1244
  LOGSTREAM.write(`${"=".repeat(80)}
1209
1245
 
1210
1246
  `);
1247
+ this.info(`Logging this request to ${currentLogFilePath}`);
1211
1248
  }
1212
1249
  /**
1213
1250
  * Log LLM method prompts with clear labeling
@@ -1215,7 +1252,7 @@ ${"=".repeat(80)}
1215
1252
  logLLMPrompt(methodName, promptType, content) {
1216
1253
  const header = `
1217
1254
  ${"#".repeat(80)}
1218
- [LLM METHOD: ${methodName}] - ${promptType.toUpperCase()} PROMPT
1255
+ [${(/* @__PURE__ */ new Date()).toISOString()}] [LLM METHOD: ${methodName}] - ${promptType.toUpperCase()} PROMPT
1219
1256
  ${"#".repeat(80)}
1220
1257
  `;
1221
1258
  LOGSTREAM.write(header);
@@ -1940,6 +1977,7 @@ var QueryCache = class {
1940
1977
  this.maxCacheSize = 5e3;
1941
1978
  // Max data cache entries
1942
1979
  this.cleanupInterval = null;
1980
+ this.resultCacheEnabled = true;
1943
1981
  // Encryption for queryId tokens
1944
1982
  this.algorithm = "aes-256-gcm";
1945
1983
  const keySource = process.env.QUERY_TOKEN_SECRET || "superatom-query-cache-default-key-v1";
@@ -1963,6 +2001,15 @@ var QueryCache = class {
1963
2001
  getTTL() {
1964
2002
  return this.ttlMs / 60 / 1e3;
1965
2003
  }
2004
+ /**
2005
+ * Stop keeping result data in memory, for a host that caches results itself.
2006
+ * `set` stores nothing and `get` finds nothing; queryId tokens are unaffected.
2007
+ */
2008
+ disableResultCache() {
2009
+ this.resultCacheEnabled = false;
2010
+ this.cache.clear();
2011
+ logger.info("[QueryCache] Result cache disabled \u2014 the host caches query results");
2012
+ }
1966
2013
  /**
1967
2014
  * Which access bucket this call reads and writes.
1968
2015
  *
@@ -1995,6 +2042,7 @@ var QueryCache = class {
1995
2042
  * a genuinely new key past the size limit.
1996
2043
  */
1997
2044
  set(query, data) {
2045
+ if (!this.resultCacheEnabled) return;
1998
2046
  const key = this.scopedKey(query);
1999
2047
  if (key === null) return;
2000
2048
  this.cache.delete(key);
@@ -2015,6 +2063,7 @@ var QueryCache = class {
2015
2063
  * iteration order — turning FIFO eviction into true LRU.
2016
2064
  */
2017
2065
  get(query) {
2066
+ if (!this.resultCacheEnabled) return null;
2018
2067
  const key = this.scopedKey(query);
2019
2068
  if (key === null) return null;
2020
2069
  const entry = this.cache.get(key);
@@ -2793,7 +2842,7 @@ function sendDataResponse(id, collection, op, data, meta, sendMessage) {
2793
2842
 
2794
2843
  // src/bundle.ts
2795
2844
  var fs2 = __toESM(require("fs"));
2796
- var path = __toESM(require("path"));
2845
+ var path2 = __toESM(require("path"));
2797
2846
  function getBundleDir(configDir) {
2798
2847
  const bundleDir = configDir || process.env.SA_BUNDLE_DIR;
2799
2848
  if (!bundleDir) {
@@ -2826,7 +2875,7 @@ function getJS(bundleDir) {
2826
2875
  `Could not find index-*.js file in ${bundleDir}. Expected a file matching pattern: index-*.js`
2827
2876
  );
2828
2877
  }
2829
- const filePath = path.join(bundleDir, indexFile);
2878
+ const filePath = path2.join(bundleDir, indexFile);
2830
2879
  logger.info(`Loading bundle from ${filePath}`);
2831
2880
  try {
2832
2881
  return fs2.readFileSync(filePath, "utf8");
@@ -3265,12 +3314,12 @@ function sendDataResponse3(id, res, sendMessage, clientId) {
3265
3314
 
3266
3315
  // src/utils/user-prompt-error-logger.ts
3267
3316
  var import_fs2 = __toESM(require("fs"));
3268
- var import_path = __toESM(require("path"));
3317
+ var import_path2 = __toESM(require("path"));
3269
3318
  var UserPromptErrorLogger = class {
3270
3319
  constructor() {
3271
3320
  this.logStream = null;
3272
3321
  this.hasErrors = false;
3273
- this.logPath = process.env.USER_PROMPT_ERROR_LOG_PATH || import_path.default.join(process.cwd(), "user-prompt-req-errors");
3322
+ this.logPath = process.env.USER_PROMPT_ERROR_LOG_PATH || import_path2.default.join(process.cwd(), "user-prompt-req-errors");
3274
3323
  this.enabled = process.env.USER_PROMPT_ERROR_LOGGING !== "false";
3275
3324
  }
3276
3325
  /**
@@ -3283,7 +3332,7 @@ var UserPromptErrorLogger = class {
3283
3332
  this.logStream.end();
3284
3333
  this.logStream = null;
3285
3334
  }
3286
- const dir = import_path.default.dirname(this.logPath);
3335
+ const dir = import_path2.default.dirname(this.logPath);
3287
3336
  if (dir !== "." && !import_fs2.default.existsSync(dir)) {
3288
3337
  import_fs2.default.mkdirSync(dir, { recursive: true });
3289
3338
  }
@@ -3763,7 +3812,8 @@ var findExactMatch = async ({
3763
3812
  userPrompt,
3764
3813
  collections,
3765
3814
  userId,
3766
- maxAgeHours
3815
+ maxAgeHours,
3816
+ statuses
3767
3817
  }) => {
3768
3818
  try {
3769
3819
  if (!collections || !collections["conversation-history"] || !collections["conversation-history"]["exactMatch"]) {
@@ -3773,7 +3823,8 @@ var findExactMatch = async ({
3773
3823
  const result = await collections["conversation-history"]["exactMatch"]({
3774
3824
  userPrompt,
3775
3825
  userId,
3776
- maxAgeHours
3826
+ maxAgeHours,
3827
+ statuses
3777
3828
  });
3778
3829
  if (!result || !result.uiBlock) {
3779
3830
  logger.info("[ConversationSearch] No exact match found");
@@ -3936,7 +3987,7 @@ var TaggedStreamBuffer = class extends StreamBuffer {
3936
3987
  }
3937
3988
  };
3938
3989
  function streamDelay(ms = STREAM_DELAY_MS) {
3939
- return new Promise((resolve3) => setTimeout(resolve3, ms));
3990
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
3940
3991
  }
3941
3992
  async function withProgressHeartbeat(operation, progressMessage, streamBuffer, intervalMs = PROGRESS_HEARTBEAT_INTERVAL_MS) {
3942
3993
  if (!streamBuffer.hasCallback()) {
@@ -4129,7 +4180,7 @@ var import_jsonrepair = require("jsonrepair");
4129
4180
 
4130
4181
  // src/utils/llm-usage-logger.ts
4131
4182
  var import_fs3 = __toESM(require("fs"));
4132
- var import_path2 = __toESM(require("path"));
4183
+ var import_path3 = __toESM(require("path"));
4133
4184
  var PRICING = {
4134
4185
  // Anthropic (December 2025)
4135
4186
  "claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
@@ -4193,7 +4244,7 @@ var LLMUsageLogger = class {
4193
4244
  totalCostUSD: 0,
4194
4245
  totalDurationMs: 0
4195
4246
  };
4196
- this.logPath = process.env.LLM_USAGE_LOG_PATH || import_path2.default.join(process.cwd(), "llm-usage-logs.log");
4247
+ this.logPath = process.env.LLM_USAGE_LOG_PATH || import_path3.default.join(process.cwd(), "llm-usage-logs.log");
4197
4248
  this.enabled = process.env.LLM_USAGE_LOGGING !== "false";
4198
4249
  if (this.enabled) {
4199
4250
  this.initLogStream();
@@ -4201,7 +4252,7 @@ var LLMUsageLogger = class {
4201
4252
  }
4202
4253
  initLogStream() {
4203
4254
  try {
4204
- const dir = import_path2.default.dirname(this.logPath);
4255
+ const dir = import_path3.default.dirname(this.logPath);
4205
4256
  if (!import_fs3.default.existsSync(dir)) {
4206
4257
  import_fs3.default.mkdirSync(dir, { recursive: true });
4207
4258
  }
@@ -4867,7 +4918,7 @@ var LLM = class {
4867
4918
  iterations++;
4868
4919
  const iterationStartTime = Date.now();
4869
4920
  const requestId = llmUsageLogger.generateRequestId();
4870
- const toolChoice = iterations === 1 && options.firstIterationToolChoice ? options.firstIterationToolChoice : void 0;
4921
+ const toolChoice = iterations === 1 && options.firstIterationToolChoice ? options.firstIterationToolChoice : iterations === 2 && options.secondIterationToolChoice ? options.secondIterationToolChoice : void 0;
4871
4922
  let stream;
4872
4923
  try {
4873
4924
  stream = await client.messages.create({
@@ -5825,7 +5876,7 @@ var LLM = class {
5825
5876
  const iterStart = Date.now();
5826
5877
  const requestId = llmUsageLogger.generateRequestId();
5827
5878
  let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0;
5828
- const toolChoice = iterations === 1 && options.firstIterationToolChoice ? this._openaiToolChoice(options.firstIterationToolChoice) : void 0;
5879
+ const toolChoice = iterations === 1 && options.firstIterationToolChoice ? this._openaiToolChoice(options.firstIterationToolChoice) : iterations === 2 && options.secondIterationToolChoice ? this._openaiToolChoice(options.secondIterationToolChoice) : void 0;
5829
5880
  const createParams = {
5830
5881
  model: modelName,
5831
5882
  messages: conversationMessages,
@@ -6016,7 +6067,7 @@ var LLM = class {
6016
6067
 
6017
6068
  // src/userResponse/prompt-loader.ts
6018
6069
  var import_fs4 = __toESM(require("fs"));
6019
- var import_path3 = __toESM(require("path"));
6070
+ var import_path4 = __toESM(require("path"));
6020
6071
 
6021
6072
  // src/userResponse/prompts.ts
6022
6073
  var PROMPTS = {
@@ -9430,7 +9481,7 @@ var PromptLoader = class {
9430
9481
  this.databaseRulesCache = /* @__PURE__ */ new Map();
9431
9482
  this.isInitialized = false;
9432
9483
  this.databaseType = "postgresql";
9433
- this.promptsDir = config2?.promptsDir || import_path3.default.join(process.cwd(), ".prompts");
9484
+ this.promptsDir = config2?.promptsDir || import_path4.default.join(process.cwd(), ".prompts");
9434
9485
  }
9435
9486
  /**
9436
9487
  * Load a prompt template from file system OR fallback to hardcoded prompts
@@ -9439,8 +9490,8 @@ var PromptLoader = class {
9439
9490
  */
9440
9491
  loadPromptTemplate(promptName) {
9441
9492
  try {
9442
- const systemPath = import_path3.default.join(this.promptsDir, promptName, "system.md");
9443
- const userPath = import_path3.default.join(this.promptsDir, promptName, "user.md");
9493
+ const systemPath = import_path4.default.join(this.promptsDir, promptName, "system.md");
9494
+ const userPath = import_path4.default.join(this.promptsDir, promptName, "user.md");
9444
9495
  if (import_fs4.default.existsSync(systemPath) && import_fs4.default.existsSync(userPath)) {
9445
9496
  const system = import_fs4.default.readFileSync(systemPath, "utf-8");
9446
9497
  const user = import_fs4.default.readFileSync(userPath, "utf-8");
@@ -9590,7 +9641,7 @@ var PromptLoader = class {
9590
9641
  if (this.databaseRulesCache.has(this.databaseType)) {
9591
9642
  return this.databaseRulesCache.get(this.databaseType);
9592
9643
  }
9593
- const rulesPath = import_path3.default.join(this.promptsDir, "database-rules", `${this.databaseType}.md`);
9644
+ const rulesPath = import_path4.default.join(this.promptsDir, "database-rules", `${this.databaseType}.md`);
9594
9645
  try {
9595
9646
  if (import_fs4.default.existsSync(rulesPath)) {
9596
9647
  const rules2 = import_fs4.default.readFileSync(rulesPath, "utf-8");
@@ -9616,6 +9667,8 @@ var PromptLoader = class {
9616
9667
  "postgres": "postgresql",
9617
9668
  "postgresql": "postgresql",
9618
9669
  "mssql": "mssql",
9670
+ "fabric": "mssql",
9671
+ // Microsoft Fabric SQL endpoints speak T-SQL
9619
9672
  "mysql": "postgresql",
9620
9673
  // MySQL uses similar rules to PostgreSQL
9621
9674
  "excel": "duckdb",
@@ -9633,7 +9686,7 @@ var PromptLoader = class {
9633
9686
  if (this.databaseRulesCache.has(dbType)) {
9634
9687
  return this.databaseRulesCache.get(dbType);
9635
9688
  }
9636
- const rulesPath = import_path3.default.join(this.promptsDir, "database-rules", `${dbType}.md`);
9689
+ const rulesPath = import_path4.default.join(this.promptsDir, "database-rules", `${dbType}.md`);
9637
9690
  try {
9638
9691
  if (import_fs4.default.existsSync(rulesPath)) {
9639
9692
  const rules2 = import_fs4.default.readFileSync(rulesPath, "utf-8");
@@ -9663,7 +9716,7 @@ var PromptLoader = class {
9663
9716
  const cacheKey = "__script_body_contract__";
9664
9717
  let raw = this.databaseRulesCache.get(cacheKey);
9665
9718
  if (raw === void 0) {
9666
- const contractPath = import_path3.default.join(this.promptsDir, "script-rules", "body-contract.md");
9719
+ const contractPath = import_path4.default.join(this.promptsDir, "script-rules", "body-contract.md");
9667
9720
  try {
9668
9721
  if (import_fs4.default.existsSync(contractPath)) {
9669
9722
  raw = import_fs4.default.readFileSync(contractPath, "utf-8");
@@ -9695,7 +9748,7 @@ var PromptLoader = class {
9695
9748
  return DATABASE_RULES[dbType] ?? DATABASE_RULES["postgresql"];
9696
9749
  }
9697
9750
  };
9698
- var defaultPromptsPath = process.env.PROMPTS_DIR || import_path3.default.join(process.cwd(), ".prompts");
9751
+ var defaultPromptsPath = process.env.PROMPTS_DIR || import_path4.default.join(process.cwd(), ".prompts");
9699
9752
  var promptLoader = new PromptLoader({
9700
9753
  promptsDir: defaultPromptsPath
9701
9754
  });
@@ -10124,7 +10177,7 @@ function getDatabaseType() {
10124
10177
  function getDbTypeFromToolId(toolId) {
10125
10178
  if (!toolId) return getDatabaseType();
10126
10179
  const prefix = toolId.split("-")[0]?.toLowerCase();
10127
- if (prefix.startsWith("mssql")) return "mssql";
10180
+ if (prefix.startsWith("mssql") || prefix === "fabric") return "mssql";
10128
10181
  if (prefix.startsWith("postgres")) return "postgresql";
10129
10182
  if (prefix.startsWith("mysql")) return "mysql";
10130
10183
  if (prefix.startsWith("snowflake")) return "snowflake";
@@ -10445,6 +10498,7 @@ var SourceAgent = class {
10445
10498
  if (toolName.endsWith("_search_schema") && schemaSearchFn) {
10446
10499
  const keywords = toolInput.keywords || [];
10447
10500
  logger.info(`[SourceAgent:${this.tool.name}] Schema search: ${keywords.join(", ")}`);
10501
+ logger.file(`[SourceAgent:${this.tool.name}] SCHEMA SEARCH:`, keywords.join(", "));
10448
10502
  if (this.streamBuffer.hasCallback()) {
10449
10503
  this.streamBuffer.write(`\u{1F50D} **Looking up tables:** ${keywords.join(", ")}
10450
10504
 
@@ -10457,6 +10511,7 @@ var SourceAgent = class {
10457
10511
  } catch (traceErr) {
10458
10512
  logger.warn(`[SourceAgent:${this.tool.name}] Trace capture failed for search_schema lookup (non-blocking): ${traceErr}`);
10459
10513
  }
10514
+ logger.file(`[SourceAgent:${this.tool.name}] SCHEMA SEARCH RESULT:`, result);
10460
10515
  return `Schema search results:
10461
10516
 
10462
10517
  ${result}`;
@@ -10500,6 +10555,7 @@ ${result}`;
10500
10555
  cappedInput.query = ensureQueryLimit(cappedInput.query, this.config.maxRowsFetched, this.config.maxRowsFetched, _st);
10501
10556
  }
10502
10557
  queryExecuted = cappedInput.sql || cappedInput.query || JSON.stringify(cappedInput);
10558
+ logger.file(`[SourceAgent:${this.tool.name}] QUERY (attempt ${this.attempts}):`, queryExecuted);
10503
10559
  if (this.streamBuffer.hasCallback() && queryExecuted) {
10504
10560
  const queryDisplay = cappedInput.sql || cappedInput.query;
10505
10561
  if (queryDisplay) {
@@ -10528,6 +10584,7 @@ ${queryDisplay}
10528
10584
  if (result && result.error) {
10529
10585
  const errorMsg = typeof result.error === "string" ? result.error : JSON.stringify(result.error);
10530
10586
  logger.warn(`[SourceAgent:${this.tool.name}] Tool returned error (attempt ${this.attempts}/${this.config.maxRetries}): ${errorMsg}`);
10587
+ logger.file(`[SourceAgent:${this.tool.name}] QUERY ERROR (attempt ${this.attempts}):`, errorMsg);
10531
10588
  if (this.streamBuffer.hasCallback()) {
10532
10589
  this.streamBuffer.write(`\u274C **Query failed:** ${errorMsg}
10533
10590
 
@@ -10541,6 +10598,7 @@ Analyze the error and try again with a corrected query.`;
10541
10598
  totalRowsMatched = result.metadata?.totalCount || result.count || resultData.length;
10542
10599
  querySucceeded = true;
10543
10600
  successfulQueries++;
10601
+ logger.file(`[SourceAgent:${this.tool.name}] QUERY RESULT (attempt ${this.attempts}):`, `${resultData.length} rows (${totalRowsMatched} total)`, "metadata:", JSON.stringify(result.metadata ?? {}));
10544
10602
  if (this.streamBuffer.hasCallback()) {
10545
10603
  const totalInfo = totalRowsMatched > resultData.length ? ` of ${totalRowsMatched} total` : "";
10546
10604
  this.streamBuffer.write(`\u2705 **${resultData.length} rows${totalInfo} from ${this.tool.name}**
@@ -10948,7 +11006,7 @@ Even if a table appears in the detailed schema above, search_schema returns samp
10948
11006
  const match = this.tool.id.match(/^(\w+)-[a-f0-9]+_/);
10949
11007
  if (!match) return "unknown";
10950
11008
  const raw = match[1].toLowerCase();
10951
- if (raw.startsWith("mssql")) return "mssql";
11009
+ if (raw.startsWith("mssql") || raw === "fabric") return "mssql";
10952
11010
  if (raw.startsWith("postgres")) return "postgres";
10953
11011
  if (raw.startsWith("mysql")) return "mysql";
10954
11012
  if (raw.startsWith("snowflake")) return "snowflake";
@@ -10974,7 +11032,7 @@ Even if a table appears in the detailed schema above, search_schema returns samp
10974
11032
 
10975
11033
  // src/userResponse/scripts/script-runner.ts
10976
11034
  var import_child_process = require("child_process");
10977
- var path5 = __toESM(require("path"));
11035
+ var path6 = __toESM(require("path"));
10978
11036
  var os = __toESM(require("os"));
10979
11037
 
10980
11038
  // src/userResponse/scripts/script-ipc.ts
@@ -11117,7 +11175,7 @@ var Semaphore = class {
11117
11175
  `Script execution at capacity (${this.max} running, ${this.waiters.length} queued). Try again shortly.`
11118
11176
  );
11119
11177
  }
11120
- return new Promise((resolve3) => this.waiters.push(resolve3));
11178
+ return new Promise((resolve4) => this.waiters.push(resolve4));
11121
11179
  }
11122
11180
  release() {
11123
11181
  const next = this.waiters.shift();
@@ -11135,8 +11193,8 @@ function resolveTsxBinary() {
11135
11193
  if (tsxBinaryPath) return tsxBinaryPath;
11136
11194
  try {
11137
11195
  const tsxPkg = require.resolve("tsx/package.json");
11138
- const tsxDir = path5.dirname(tsxPkg);
11139
- tsxBinaryPath = path5.join(tsxDir, "dist", "cli.mjs");
11196
+ const tsxDir = path6.dirname(tsxPkg);
11197
+ tsxBinaryPath = path6.join(tsxDir, "dist", "cli.mjs");
11140
11198
  return tsxBinaryPath;
11141
11199
  } catch (err) {
11142
11200
  throw new Error(
@@ -11145,7 +11203,7 @@ function resolveTsxBinary() {
11145
11203
  }
11146
11204
  }
11147
11205
  function resolveBootstrapPath() {
11148
- return path5.resolve(__dirname, "userResponse/scripts/script-bootstrap.js");
11206
+ return path6.resolve(__dirname, "userResponse/scripts/script-bootstrap.js");
11149
11207
  }
11150
11208
  async function runScript(recipe, scriptPath, params, options) {
11151
11209
  const startedAt = Date.now();
@@ -11247,7 +11305,7 @@ FIX: pass every required parameter to execute_script, or give it a default in wr
11247
11305
  }
11248
11306
  }
11249
11307
  function executeInSubprocess(scriptPath, params, paramSchema, externalTools, streamBuffer, timeoutMs, signal, mode, analysisData, analysisDatasets, entities, appliedPolicies) {
11250
- return new Promise((resolve3) => {
11308
+ return new Promise((resolve4) => {
11251
11309
  const tsxBin = resolveTsxBinary();
11252
11310
  const bootstrap = resolveBootstrapPath();
11253
11311
  const contextToken = mintContextToken();
@@ -11303,7 +11361,7 @@ function executeInSubprocess(scriptPath, params, paramSchema, externalTools, str
11303
11361
  clearTimeout(timer);
11304
11362
  if (signal && onAbort) signal.removeEventListener("abort", onAbort);
11305
11363
  cleanup();
11306
- resolve3(r);
11364
+ resolve4(r);
11307
11365
  };
11308
11366
  const timer = setTimeout(() => {
11309
11367
  finish({
@@ -11653,6 +11711,7 @@ function codeOnly(source) {
11653
11711
  var BARE_CONST_RE = /\bconst\s+(\w+)\s*=\s*(\d{2,}(?:\.\d+)?)\s*[;,)]/g;
11654
11712
  var FMT_CALL_RE = /\bfmt\.\w+\(([^)]*)\)/g;
11655
11713
  var DIRECT_BARE_ARG_RE = /(?<![\w.\[])(\d{2,}(?:\.\d+)?)(?![\w.\]])/;
11714
+ var OBJECT_PROP_RE = /(?<![\w.])([A-Za-z_]\w*)\s*:\s*(\d{2,}(?:\.\d+)?)\s*[,}]/g;
11656
11715
  function interpolationContents(source) {
11657
11716
  const out = [];
11658
11717
  for (const tpl of source.matchAll(/`(?:\\.|[^`\\])*`/g)) {
@@ -11684,6 +11743,24 @@ function findHardcodedAggregates(source) {
11684
11743
  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).`
11685
11744
  });
11686
11745
  }
11746
+ const propCandidates = [...code.matchAll(OBJECT_PROP_RE)].map((m) => ({ name: m[1], text: m[0].trim().replace(/[,}]$/, "") }));
11747
+ for (const c of propCandidates) {
11748
+ const accessPattern = `\\??\\.${c.name}\\b`;
11749
+ const accessRe = new RegExp(accessPattern);
11750
+ let flows = fmtArgLists.some((args) => accessRe.test(args));
11751
+ if (!flows) {
11752
+ const hop = code.match(new RegExp(`\\bconst\\s+(\\w+)\\s*=\\s*\\w+${accessPattern}`));
11753
+ if (hop) {
11754
+ const varRe = new RegExp(`\\b${hop[1]}\\b`);
11755
+ flows = fmtArgLists.some((args) => varRe.test(args));
11756
+ }
11757
+ }
11758
+ if (!flows) continue;
11759
+ violations.push({
11760
+ text: c.text,
11761
+ 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.`
11762
+ });
11763
+ }
11687
11764
  return violations;
11688
11765
  }
11689
11766
  function literalChunks(source) {
@@ -11801,7 +11878,7 @@ function resolveScriptRecipeStore(collections) {
11801
11878
 
11802
11879
  // src/userResponse/scripts/script-store.ts
11803
11880
  var fs6 = __toESM(require("fs"));
11804
- var path6 = __toESM(require("path"));
11881
+ var path7 = __toESM(require("path"));
11805
11882
  var import_crypto6 = require("crypto");
11806
11883
  var DEFAULT_STORE_DIR = "scripts-store";
11807
11884
  function stripAnalysisExport(body) {
@@ -11895,7 +11972,7 @@ export { getData };
11895
11972
  var ScriptStore = class {
11896
11973
  constructor(opts) {
11897
11974
  this.store = opts?.store ?? resolveScriptRecipeStore(opts?.collections) ?? null;
11898
- this.storeDir = opts?.baseDir || path6.join(process.cwd(), DEFAULT_STORE_DIR);
11975
+ this.storeDir = opts?.baseDir || path7.join(process.cwd(), DEFAULT_STORE_DIR);
11899
11976
  this.projectId = opts?.projectId;
11900
11977
  if (!this.store) {
11901
11978
  logger.warn("[ScriptStore] No script-recipes metadata store injected \u2014 script reuse disabled this run.");
@@ -11965,7 +12042,7 @@ var ScriptStore = class {
11965
12042
  if (version === row.version) {
11966
12043
  return this.rowToRecipe(row, this.readBody(row.fileBase));
11967
12044
  }
11968
- const archivePath = path6.join(this.archiveDir, `${row.fileBase}.v${version}.ts`);
12045
+ const archivePath = path7.join(this.archiveDir, `${row.fileBase}.v${version}.ts`);
11969
12046
  if (!fs6.existsSync(archivePath)) {
11970
12047
  logger.warn(`[ScriptStore] getVersion(${id}, v${version}) \u2014 archived body not found, recipe is now at v${row.version}`);
11971
12048
  return null;
@@ -11989,10 +12066,10 @@ var ScriptStore = class {
11989
12066
  }
11990
12067
  if (recipe.scriptBody) {
11991
12068
  const normalized = normalizeScriptBody(recipe.scriptBody);
11992
- const hash = this.hash(normalized);
11993
- if (hash !== recipe.bodyHash) {
12069
+ const hash2 = this.hash(normalized);
12070
+ if (hash2 !== recipe.bodyHash) {
11994
12071
  this.writeBody(recipe.fileBase, normalized);
11995
- recipe.bodyHash = hash;
12072
+ recipe.bodyHash = hash2;
11996
12073
  }
11997
12074
  }
11998
12075
  recipe.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -12280,7 +12357,7 @@ ${carried.trim()}
12280
12357
  /** Absolute path to the .ts body for a recipe (used by the runner/MainAgent). */
12281
12358
  getScriptPath(recipe) {
12282
12359
  const base = recipe.fileBase || this.toSlug(recipe.name);
12283
- return path6.join(this.storeDir, `${base}.ts`);
12360
+ return path7.join(this.storeDir, `${base}.ts`);
12284
12361
  }
12285
12362
  // ============================================
12286
12363
  // Internals
@@ -12466,7 +12543,7 @@ ${analysisBody.trim()}
12466
12543
  return true;
12467
12544
  }
12468
12545
  bodyPath(fileBase) {
12469
- return path6.join(this.storeDir, `${fileBase}.ts`);
12546
+ return path7.join(this.storeDir, `${fileBase}.ts`);
12470
12547
  }
12471
12548
  readBody(fileBase) {
12472
12549
  try {
@@ -12478,7 +12555,7 @@ ${analysisBody.trim()}
12478
12555
  }
12479
12556
  /** Directory holding superseded bodies. Dot-prefixed so IDEs/`ls` hide it. */
12480
12557
  get archiveDir() {
12481
- return path6.join(this.storeDir, ".versions");
12558
+ return path7.join(this.storeDir, ".versions");
12482
12559
  }
12483
12560
  /**
12484
12561
  * Archive a superseded body as `.versions/<fileBase>.v<n>.ts`.
@@ -12488,7 +12565,7 @@ ${analysisBody.trim()}
12488
12565
  */
12489
12566
  writeArchive(fileBase, version, body) {
12490
12567
  if (!fs6.existsSync(this.archiveDir)) fs6.mkdirSync(this.archiveDir, { recursive: true });
12491
- const finalPath = path6.join(this.archiveDir, `${fileBase}.v${version}.ts`);
12568
+ const finalPath = path7.join(this.archiveDir, `${fileBase}.v${version}.ts`);
12492
12569
  const tmpPath = `${finalPath}.tmp-${(0, import_crypto6.randomBytes)(4).toString("hex")}`;
12493
12570
  fs6.writeFileSync(tmpPath, body);
12494
12571
  fs6.renameSync(tmpPath, finalPath);
@@ -12506,7 +12583,7 @@ ${analysisBody.trim()}
12506
12583
  for (const f of fs6.readdirSync(this.archiveDir)) {
12507
12584
  if (!f.startsWith(prefix) || !f.endsWith(".ts")) continue;
12508
12585
  const suffix = f.slice(oldBase.length);
12509
- fs6.renameSync(path6.join(this.archiveDir, f), path6.join(this.archiveDir, `${newBase}${suffix}`));
12586
+ fs6.renameSync(path7.join(this.archiveDir, f), path7.join(this.archiveDir, `${newBase}${suffix}`));
12510
12587
  }
12511
12588
  } catch (err) {
12512
12589
  logger.warn(`[ScriptStore] failed to move archives ${oldBase} \u2192 ${newBase}: ${err}`);
@@ -12532,7 +12609,7 @@ ${analysisBody.trim()}
12532
12609
  const prefix = `${fileBase}.v`;
12533
12610
  for (const f of fs6.readdirSync(this.archiveDir)) {
12534
12611
  if (f.startsWith(prefix) && f.endsWith(".ts")) {
12535
- fs6.unlinkSync(path6.join(this.archiveDir, f));
12612
+ fs6.unlinkSync(path7.join(this.archiveDir, f));
12536
12613
  }
12537
12614
  }
12538
12615
  } catch (err) {
@@ -12817,7 +12894,7 @@ var WRITE_SCRIPT_TOOL_DEF = {
12817
12894
  };
12818
12895
  var RESOLVE_ENTITIES_TOOL_DEF = {
12819
12896
  name: "resolve_entities",
12820
- 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.',
12897
+ 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.',
12821
12898
  input_schema: {
12822
12899
  type: "object",
12823
12900
  properties: {
@@ -12835,30 +12912,10 @@ var RESOLVE_ENTITIES_TOOL_DEF = {
12835
12912
  },
12836
12913
  required: ["text"]
12837
12914
  }
12838
- },
12839
- concepts: {
12840
- type: "array",
12841
- items: { type: "string" },
12842
- 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."
12843
12915
  }
12844
12916
  }
12845
12917
  }
12846
12918
  };
12847
- var RESOLVE_CONCEPTS_TOOL_DEF = {
12848
- name: "resolve_concepts",
12849
- 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.',
12850
- input_schema: {
12851
- type: "object",
12852
- properties: {
12853
- names: {
12854
- type: "array",
12855
- items: { type: "string" },
12856
- description: "Concept names from the CONCEPT CATALOG. Ask only for what this question needs \u2014 dependencies come back on their own."
12857
- }
12858
- },
12859
- required: ["names"]
12860
- }
12861
- };
12862
12919
  var EXECUTE_SCRIPT_TOOL_DEF = {
12863
12920
  name: "execute_script",
12864
12921
  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.",
@@ -12914,9 +12971,11 @@ var MainAgent = class {
12914
12971
  */
12915
12972
  this.resolvedEntities = [];
12916
12973
  /**
12917
- * Concept bodies resolved this turn (via resolve_entities' embedded concept
12918
- * lookup or a standalone resolve_concepts call) — retained the same way as
12919
- * resolvedEntities above, so they can be attached to the SourceAgent dispatch.
12974
+ * Concept bodies resolved this turn (currently: invariants only, via
12975
+ * loadConceptInvariants — concept resolution is no longer part of
12976
+ * resolve_entities or any other MainAgent-owned tool) — retained the same
12977
+ * way as resolvedEntities above, so they can be attached to the SourceAgent
12978
+ * dispatch.
12920
12979
  *
12921
12980
  * Without this, a concept resolved HERE (MainAgent's own tool loop) never
12922
12981
  * reaches SourceAgent at all: SourceAgent only gets the natural-language
@@ -12992,7 +13051,6 @@ var MainAgent = class {
12992
13051
  const entitySearchActive = this.entitySearchEnabled && entityCatalog.count > 0;
12993
13052
  if (entityCatalog.count) logger.info(`[MainAgent] Entity catalog: ${entityCatalog.count} type(s) \u2014 resolve_entities pinned to iteration 1`);
12994
13053
  const conceptCatalog = this.conceptsEnabled ? await this.loadConceptCatalog() : { text: "", count: 0 };
12995
- const conceptsActive = this.conceptsEnabled && conceptCatalog.count > 0;
12996
13054
  const conceptInvariants = this.conceptsEnabled ? await this.loadConceptInvariants() : { text: "", count: 0 };
12997
13055
  if (conceptCatalog.count) {
12998
13056
  logger.info(
@@ -13033,7 +13091,6 @@ var MainAgent = class {
13033
13091
  ...directToolDefs,
13034
13092
  ...workflowToolDefs,
13035
13093
  ...entitySearchActive ? [RESOLVE_ENTITIES_TOOL_DEF] : [],
13036
- ...conceptsActive ? [RESOLVE_CONCEPTS_TOOL_DEF] : [],
13037
13094
  ...this.scriptingEnabled ? [WRITE_SCRIPT_TOOL_DEF, EXECUTE_SCRIPT_TOOL_DEF, WRITE_ANALYSIS_TOOL_DEF] : [],
13038
13095
  // Only offered when there is a catalog to choose from; without one the
13039
13096
  // flow falls back to generateScriptComponents exactly as before.
@@ -13049,7 +13106,7 @@ var MainAgent = class {
13049
13106
  let sourceCallCounter = 0;
13050
13107
  const toolIdCallCounts = {};
13051
13108
  let selectedWorkflow;
13052
- const toolHandler = async (toolName, toolInput) => {
13109
+ const toolHandlerInner = async (toolName, toolInput) => {
13053
13110
  const workflow = this.workflows.find((w) => w.id === toolName);
13054
13111
  if (workflow) {
13055
13112
  return this.handleWorkflow(workflow, toolInput, (w) => {
@@ -13057,10 +13114,7 @@ var MainAgent = class {
13057
13114
  });
13058
13115
  }
13059
13116
  if (toolName === "resolve_entities") {
13060
- return this.handleResolveEntities(toolInput, userPrompt);
13061
- }
13062
- if (toolName === "resolve_concepts") {
13063
- return this.handleResolveConcepts(toolInput);
13117
+ return this.handleResolveEntities(toolInput);
13064
13118
  }
13065
13119
  if (toolName === "write_components") {
13066
13120
  if (!this.scriptingEnabled) return "Component authoring is not enabled for this request.";
@@ -13088,7 +13142,8 @@ var MainAgent = class {
13088
13142
  }
13089
13143
  const sourceInput = {
13090
13144
  intent: toolInput.intent || toolInput.query || JSON.stringify(toolInput),
13091
- aggregation: toolInput.aggregation || "raw"
13145
+ aggregation: toolInput.aggregation || "raw",
13146
+ concepts: Array.isArray(toolInput.concepts) ? toolInput.concepts.filter((c) => typeof c === "string") : void 0
13092
13147
  };
13093
13148
  logger.info(`[MainAgent] Dispatching SourceAgent for "${externalTool.name}" | intent: "${sourceInput.intent}"`);
13094
13149
  const preResolved = await this.preResolveSchema(externalTool, sourceInput.intent);
@@ -13117,10 +13172,20 @@ var MainAgent = class {
13117
13172
  if (entityBlock) {
13118
13173
  logger.info(`[MainAgent] Attached ${this.resolvedEntities.length} resolved entity id(s) to the dispatch: ${this.resolvedEntities.map((e) => `${e.entityType}=${e.instanceId}`).join(", ")}`);
13119
13174
  }
13120
- const conceptBlock = this.buildResolvedConceptsBlock();
13175
+ const conceptBlock = this.buildResolvedConceptsBlock(sourceInput.concepts);
13176
+ const wantedConcepts = new Set(sourceInput.concepts ?? []);
13177
+ const attachedConcepts = this.resolvedConcepts.filter((c) => c.alwaysInclude || wantedConcepts.has(c.name));
13121
13178
  if (conceptBlock) {
13122
- logger.info(`[MainAgent] Attached ${this.resolvedConcepts.length} resolved concept(s) to the dispatch: ${this.resolvedConcepts.map((c) => c.name).join(", ")}`);
13123
- }
13179
+ logger.info(`[MainAgent] Attached ${attachedConcepts.length} resolved concept(s) to the dispatch: ${attachedConcepts.map((c) => c.name).join(", ")}`);
13180
+ }
13181
+ logger.file(
13182
+ `[DISPATCH] ${externalTool.name} | sourceInput.concepts requested:`,
13183
+ JSON.stringify(sourceInput.concepts ?? []),
13184
+ "\n entityBlock:",
13185
+ entityBlock || "(none)",
13186
+ "\n conceptBlock:",
13187
+ conceptBlock || "(none)"
13188
+ );
13124
13189
  const result = await sourceAgent.execute({
13125
13190
  ...sourceInput,
13126
13191
  intent: sourceInput.intent + entityBlock + conceptBlock
@@ -13149,7 +13214,7 @@ var MainAgent = class {
13149
13214
  const entry = sourceAgent.getTraceEntry();
13150
13215
  if (entry) {
13151
13216
  entry.result = formatted;
13152
- if (this.resolvedConcepts.length) entry.resolvedConcepts = [...this.resolvedConcepts];
13217
+ if (attachedConcepts.length) entry.resolvedConcepts = [...attachedConcepts];
13153
13218
  entry.aggregation = sourceInput.aggregation;
13154
13219
  }
13155
13220
  } catch (traceErr) {
@@ -13157,8 +13222,21 @@ var MainAgent = class {
13157
13222
  }
13158
13223
  return formatted;
13159
13224
  };
13225
+ const toolHandler = async (toolName, toolInput) => {
13226
+ logger.file(`[TOOL CALL] ${toolName} | input:`, JSON.stringify(toolInput));
13227
+ try {
13228
+ const result = await toolHandlerInner(toolName, toolInput);
13229
+ logger.file(`[TOOL RESULT] ${toolName} | result:`, typeof result === "string" ? result.slice(0, 4e3) : JSON.stringify(result).slice(0, 4e3));
13230
+ return result;
13231
+ } catch (err) {
13232
+ logger.file(`[TOOL ERROR] ${toolName} | error:`, err instanceof Error ? err.message : String(err));
13233
+ throw err;
13234
+ }
13235
+ };
13160
13236
  let text = "";
13161
13237
  const firstIterationToolChoice = entitySearchActive ? { type: "tool", name: "resolve_entities" } : (this.forkMode || this.editMode) && this.scriptingEnabled ? { type: "any" } : void 0;
13238
+ const conceptToolRegistered = this.externalTools.some((t) => t.id === "concept");
13239
+ const secondIterationToolChoice = conceptToolRegistered ? { type: "tool", name: "concept" } : void 0;
13162
13240
  try {
13163
13241
  text = await LLM.streamWithTools(
13164
13242
  {
@@ -13177,7 +13255,8 @@ var MainAgent = class {
13177
13255
  apiKey: apiKey || this.config.apiKey,
13178
13256
  partial: streamCallback,
13179
13257
  signal,
13180
- ...firstIterationToolChoice ? { firstIterationToolChoice } : {}
13258
+ ...firstIterationToolChoice ? { firstIterationToolChoice } : {},
13259
+ ...secondIterationToolChoice ? { secondIterationToolChoice } : {}
13181
13260
  },
13182
13261
  this.config.maxIterations
13183
13262
  );
@@ -13348,24 +13427,6 @@ ${catalog}`,
13348
13427
  const hardcodedComponentText = findHardcodedComponentText(body);
13349
13428
  const hardcodedEntityNames = findHardcodedEntityNames(body, this.resolvedEntities.map((e) => e.displayName));
13350
13429
  const componentViolations = [...hardcodedAggregates, ...hardcodedComponentText, ...hardcodedEntityNames];
13351
- if (componentViolations.length > 0) {
13352
- const named = componentViolations.map((v) => `"${v.text}"`).join(", ");
13353
- const repeatedTexts = componentViolations.map((v) => v.text).filter((t) => this.lastComponentsViolationTexts.includes(t));
13354
- this.lastComponentsViolationTexts = componentViolations.map((v) => v.text);
13355
- logger.warn(`[MainAgent] write_components rejected \u2014 ${componentViolations.length} hardcoded value(s): ${named}`);
13356
- const lines2 = componentViolations.map((v) => ` \u2022 "${v.text}" \u2014 ${v.reason}`).join("\n");
13357
- let message = `write_components rejected: getComponents hardcodes ${componentViolations.length} value(s) that belong to this run's data, not fixed text:
13358
- ${lines2}
13359
-
13360
- 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.`;
13361
- if (repeatedTexts.length > 0) {
13362
- const repeatedList = repeatedTexts.map((t) => `"${t}"`).join(", ");
13363
- message += `
13364
-
13365
- 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.`;
13366
- }
13367
- return message;
13368
- }
13369
13430
  const recipeId = this.scriptState.recipeId;
13370
13431
  const draft = recipeId ? await this.scriptStore.get(recipeId) : null;
13371
13432
  if (!recipeId || !draft) return "Draft was lost from the store. Call write_script again.";
@@ -13826,12 +13887,19 @@ The verified script is intact. You can still call write_analysis and write_compo
13826
13887
  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:
13827
13888
  ${lines.join("\n")}`;
13828
13889
  }
13829
- /** Dedupe-and-retain helper shared by resolveConceptsForTurn and handleResolveConcepts. */
13830
- retainResolvedConcepts(resolved) {
13890
+ /**
13891
+ * Dedupe-and-retain helper shared by loadConceptInvariants and the generic
13892
+ * direct-tool `resolvedConcepts` opt-in hook. `alwaysInclude` marks entries
13893
+ * that buildResolvedConceptsBlock must attach to EVERY dispatch regardless
13894
+ * of that dispatch's `concepts` param — true only for invariants, which are
13895
+ * meant to apply everywhere by definition. Concepts retained without it
13896
+ * (e.g. via the `concept` tool's `get`) are scoped per-dispatch instead.
13897
+ */
13898
+ retainResolvedConcepts(resolved, opts) {
13831
13899
  for (const c of resolved) {
13832
13900
  if (!c?.name || !c?.body) continue;
13833
13901
  if (this.resolvedConcepts.some((existing) => existing.name === c.name)) continue;
13834
- this.resolvedConcepts.push({ name: String(c.name), kind: String(c.kind ?? ""), body: String(c.body) });
13902
+ this.resolvedConcepts.push({ name: String(c.name), kind: String(c.kind ?? ""), body: String(c.body), alwaysInclude: !!opts?.alwaysInclude });
13835
13903
  }
13836
13904
  }
13837
13905
  /**
@@ -13839,10 +13907,29 @@ ${lines.join("\n")}`;
13839
13907
  * entity ids — appended to the dispatch intent so SourceAgent's own,
13840
13908
  * independent LLM call receives the resolved definitions verbatim rather
13841
13909
  * than only MainAgent's paraphrased intent text.
13910
+ *
13911
+ * Unlike buildResolvedEntitiesBlock, non-invariant entries here are SCOPED
13912
+ * to `names` (this dispatch's SourceToolInput.concepts) rather than
13913
+ * dumping the whole-turn `this.resolvedConcepts` accumulator into every
13914
+ * dispatch. A concept body is phrased as an authoritative override
13915
+ * ("overrides the schema... your own reasoning") — fetching several via
13916
+ * `get` to compare/discard, or a turn needing different concepts for
13917
+ * different sub-questions, must not leak an irrelevant concept's filters
13918
+ * into an unrelated dispatch. An entry not named in `names` and not
13919
+ * `alwaysInclude` is left out — a concept the model forgot to name is a
13920
+ * visible, fixable miss; one that leaks in uninvited is not.
13921
+ *
13922
+ * `alwaysInclude` entries (invariants — see loadConceptInvariants) bypass
13923
+ * `names` entirely and attach to every dispatch regardless, same as before
13924
+ * this method took a `names` param — they are meant to apply everywhere,
13925
+ * not just when the model happens to ask for them.
13842
13926
  */
13843
- buildResolvedConceptsBlock() {
13927
+ buildResolvedConceptsBlock(names) {
13844
13928
  if (!this.resolvedConcepts.length) return "";
13845
- const lines = this.resolvedConcepts.map((c) => `- ${c.name}${c.kind ? ` (${c.kind})` : ""}: ${c.body}`);
13929
+ const wanted = new Set(names ?? []);
13930
+ const scoped = this.resolvedConcepts.filter((c) => c.alwaysInclude || wanted.has(c.name));
13931
+ if (!scoped.length) return "";
13932
+ const lines = scoped.map((c) => `- ${c.name}${c.kind ? ` (${c.kind})` : ""}: ${c.body}`);
13846
13933
  return `
13847
13934
 
13848
13935
  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:
@@ -13857,11 +13944,21 @@ ${lines.join("\n")}`;
13857
13944
  return !!this.config.collections?.["concepts"]?.["resolve"];
13858
13945
  }
13859
13946
  /**
13860
- * Concept names for the system prompt. Names ONLY — the body comes back from
13947
+ * The concept catalog for the system prompt: one line per concept — name,
13948
+ * aliases, and what it is for. No bodies — the body comes back from
13861
13949
  * resolve_concepts once the LLM has named one, exactly as with the entity
13862
13950
  * catalog. Putting bodies here would rebuild the always-on context problem
13863
13951
  * concepts exist to solve.
13864
13952
  *
13953
+ * The description is what makes the list usable. It used to be names only,
13954
+ * which asked the model to judge from a name alone whether `ref-data null
13955
+ * sentinel` bears on the question in front of it — and a wrong judgement is
13956
+ * silent, since it simply does not fetch. A line of prose per concept costs
13957
+ * a few tokens and is read rather than matched.
13958
+ *
13959
+ * Aliases stay, because they are how a user is likely to phrase it, and
13960
+ * showing them is what stops the LLM inventing a near-miss name.
13961
+ *
13865
13962
  * Not cached, for the same reason loadEntityCatalog isn't: unchanged rows
13866
13963
  * render byte-identical text so the prompt-cache prefix still hits, and a
13867
13964
  * process cache would only buy a window where a new concept is invisible.
@@ -13874,7 +13971,8 @@ ${lines.join("\n")}`;
13874
13971
  const rows = res?.data || [];
13875
13972
  const text = rows.map((r) => {
13876
13973
  const aka = r.aliases?.length ? ` (also: ${r.aliases.join(", ")})` : "";
13877
- return `- ${r.name}${aka}`;
13974
+ const what = r.description?.trim() ? ` \u2014 ${r.description.trim()}` : "";
13975
+ return `- ${r.name}${aka}${what}`;
13878
13976
  }).join("\n");
13879
13977
  return { text, count: rows.length };
13880
13978
  } catch (err) {
@@ -13892,10 +13990,14 @@ ${lines.join("\n")}`;
13892
13990
  * Bodies ARE included here — unlike the catalog — because there is nothing to
13893
13991
  * request: being unrequestable is what makes them invariants.
13894
13992
  *
13895
- * Also retained into resolvedConcepts (same as an explicit resolve_concepts
13896
- * call) so these bodies reach SourceAgent's dispatch too — an invariant is
13897
- * meant to apply everywhere, not just wherever the model happens to ask for
13898
- * something whose `requires` chain reaches it.
13993
+ * Also retained into resolvedConcepts so these bodies reach SourceAgent's
13994
+ * dispatch too — an invariant is meant to apply everywhere, not just
13995
+ * wherever the model happens to ask for something whose `requires` chain
13996
+ * reaches it. This is now the ONLY way a concept body reaches
13997
+ * resolvedConcepts/SourceAgent's dispatch automatically — a non-invariant
13998
+ * concept fetched through a project's own concept-lookup tool (if any) is
13999
+ * NOT retained here, and will not reach a later SourceAgent dispatch unless
14000
+ * the model itself restates it in the intent it writes.
13899
14001
  */
13900
14002
  async loadConceptInvariants() {
13901
14003
  const fn = this.config.collections?.["concepts"]?.["invariants"];
@@ -13903,7 +14005,7 @@ ${lines.join("\n")}`;
13903
14005
  try {
13904
14006
  const res = await fn({});
13905
14007
  const rows = res?.data || [];
13906
- this.retainResolvedConcepts(rows);
14008
+ this.retainResolvedConcepts(rows, { alwaysInclude: true });
13907
14009
  const text = rows.map((r) => `### ${r.name}
13908
14010
  ${r.body}`).join("\n\n");
13909
14011
  return { text, count: rows.length };
@@ -13912,161 +14014,11 @@ ${r.body}`).join("\n\n");
13912
14014
  return { text: "", count: 0 };
13913
14015
  }
13914
14016
  }
13915
- /**
13916
- * Resolve the concepts for a turn, with a SERVER-SIDE FLOOR.
13917
- *
13918
- * Pinning a tool guarantees it is called; it does not guarantee the model
13919
- * fills in the arguments. Observed: a question containing the exact words
13920
- * "revenue forecast" produced a call with no concepts at all, and the agent
13921
- * then used an incomplete copy of the formula from the knowledge base — the
13922
- * resulting query filtered a customer id against a project column and
13923
- * returned zero rows on every attempt.
13924
- *
13925
- * So when the model names nothing, scan the question itself for catalog names
13926
- * and aliases on word boundaries. Precise (a literal name is evidence, not a
13927
- * guess) and asymmetric in the safe direction: an unneeded definition costs a
13928
- * few hundred ignorable tokens, a missing one costs a wrong answer.
13929
- */
13930
- /**
13931
- * Returns the tool-facing fields to spread into the resolve_entities JSON
13932
- * response, PLUS a `_trace` field with everything the trace needs that the
13933
- * model does not (asked/viaFloor/unmatched). Callers must pull `_trace` out
13934
- * before spreading the rest into what the model sees — see handleResolveEntities.
13935
- */
13936
- async resolveConceptsForTurn(asked, userPrompt) {
13937
- const resolveFn = this.config.collections?.["concepts"]?.["resolve"];
13938
- if (!resolveFn) return {};
13939
- let names = [...asked];
13940
- let viaFloor = [];
13941
- if (!names.length && userPrompt) {
13942
- try {
13943
- const catFn = this.config.collections?.["concepts"]?.["catalog"];
13944
- const cat = catFn ? await catFn({}) : null;
13945
- const haystack = ` ${userPrompt.toLowerCase().replace(/\s+/g, " ")} `;
13946
- for (const row of cat?.data || []) {
13947
- const labels = [row.name, ...row.aliases || []].filter(Boolean).sort((a, b) => b.length - a.length);
13948
- for (const label of labels) {
13949
- const l = String(label).toLowerCase().trim();
13950
- if (l.length < 4) continue;
13951
- if (haystack.includes(` ${l} `) || haystack.includes(` ${l},`) || haystack.includes(` ${l}?`) || haystack.includes(` ${l}.`)) {
13952
- names.push(row.name);
13953
- viaFloor.push(row.name);
13954
- break;
13955
- }
13956
- }
13957
- }
13958
- } catch (err) {
13959
- logger.warn(`[MainAgent] concept floor scan failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
13960
- }
13961
- }
13962
- if (!names.length) return {};
13963
- try {
13964
- const res = await resolveFn({ names });
13965
- const resolved = res?.resolved || [];
13966
- const impliedKeys = new Set((res?.implied || []).map((s) => s.toLowerCase()));
13967
- this.retainResolvedConcepts(resolved);
13968
- logger.info(
13969
- `[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(", ")}` : "")
13970
- );
13971
- const _trace = {
13972
- asked,
13973
- viaFloor,
13974
- returned: resolved.map((c) => ({
13975
- name: c.name,
13976
- kind: c.kind,
13977
- definition: c.body,
13978
- ...c.returns && Object.keys(c.returns).length ? { returns: c.returns } : {},
13979
- ...impliedKeys.has(String(c.name).toLowerCase()) ? { viaRequires: true } : {}
13980
- })),
13981
- implied: res?.implied || [],
13982
- unmatched: res?.unmatched || [],
13983
- corrected: res?.corrected || [],
13984
- suggestions: res?.suggestions || []
13985
- };
13986
- if (!resolved.length) return { _trace };
13987
- return {
13988
- concepts: resolved.map((c) => ({
13989
- name: c.name,
13990
- kind: c.kind,
13991
- definition: c.body,
13992
- ...c.returns && Object.keys(c.returns).length ? { returns: c.returns } : {},
13993
- ...impliedKeys.has(String(c.name).toLowerCase()) ? { viaRequires: true } : {}
13994
- })),
13995
- ...res?.corrected?.length ? { conceptsCorrected: res.corrected } : {},
13996
- ...res?.suggestions?.length ? { conceptsDidYouMean: res.suggestions } : {},
13997
- 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(", ")}.` : ""),
13998
- _trace
13999
- };
14000
- } catch (err) {
14001
- logger.warn(`[MainAgent] concept resolution inside resolve_entities failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`);
14002
- return { _trace: { asked, viaFloor, error: err instanceof Error ? err.message : String(err) } };
14003
- }
14004
- }
14005
14017
  /**
14006
14018
  * Never throws: a concept-store outage must degrade to "no definition found"
14007
14019
  * and leave the agent deriving the formula as it does today, not take the
14008
14020
  * whole answer down.
14009
14021
  */
14010
- async handleResolveConcepts(toolInput) {
14011
- const fn = this.config.collections?.["concepts"]?.["resolve"];
14012
- const names = Array.isArray(toolInput?.names) ? toolInput.names.filter(Boolean) : [];
14013
- if (!fn) {
14014
- return JSON.stringify({ concepts: [], unmatched: names, note: "Concept definitions are not available; derive from the schema and say so." });
14015
- }
14016
- if (!names.length) {
14017
- return JSON.stringify({ concepts: [], unmatched: [], note: "No concept names given \u2014 continue." });
14018
- }
14019
- try {
14020
- const res = await fn({ names });
14021
- const resolved = res?.resolved || [];
14022
- const impliedKeys = new Set((res?.implied || []).map((s) => s.toLowerCase()));
14023
- this.retainResolvedConcepts(resolved);
14024
- logger.info(
14025
- `[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(", ")}` : "")
14026
- );
14027
- const suggestions = res?.suggestions || [];
14028
- const corrected = res?.corrected || [];
14029
- const notes = [];
14030
- 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.");
14031
- 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.`);
14032
- 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.`);
14033
- if (!resolved.length && !suggestions.length) notes.push("No definition on file \u2014 derive it yourself and say so in your answer.");
14034
- const seenByLLM = {
14035
- concepts: resolved.map((c) => ({
14036
- name: c.name,
14037
- kind: c.kind,
14038
- definition: c.body,
14039
- ...c.returns && Object.keys(c.returns).length ? { returns: c.returns } : {},
14040
- ...impliedKeys.has(String(c.name).toLowerCase()) ? { viaRequires: true } : {}
14041
- })),
14042
- ...corrected.length ? { corrected } : {},
14043
- ...suggestions.length ? { didYouMean: suggestions } : {},
14044
- unmatched: res?.unmatched || [],
14045
- note: notes.join(" ")
14046
- };
14047
- try {
14048
- if (this.config.trace) {
14049
- this.config.trace.concepts.calls.push({
14050
- via: "resolve_concepts",
14051
- asked: names,
14052
- implied: res?.implied || [],
14053
- returned: seenByLLM.concepts,
14054
- ...corrected.length ? { corrected } : {},
14055
- ...suggestions.length ? { suggestions } : {},
14056
- unmatched: res?.unmatched || [],
14057
- note: notes.join(" ")
14058
- });
14059
- }
14060
- } catch (traceErr) {
14061
- logger.warn(`[MainAgent] Trace capture failed for resolve_concepts (non-blocking): ${traceErr}`);
14062
- }
14063
- return JSON.stringify(seenByLLM);
14064
- } catch (err) {
14065
- const msg = err instanceof Error ? err.message : String(err);
14066
- logger.warn(`[MainAgent] resolve_concepts failed (non-blocking): ${msg}`);
14067
- return JSON.stringify({ concepts: [], unmatched: names, note: "Concept lookup failed; derive from the schema and say so." });
14068
- }
14069
- }
14070
14022
  /**
14071
14023
  * Compact catalog (type + gloss) for the system prompt. This is what lets the
14072
14024
  * LLM NAME a concept instead of guessing at our vocabulary — it can infer
@@ -14097,20 +14049,18 @@ ${r.body}`).join("\n\n");
14097
14049
  * Never throws: a resolver outage must degrade to "nothing resolved" and
14098
14050
  * leave the agent exactly as it was, not take the whole answer down.
14099
14051
  */
14100
- async handleResolveEntities(toolInput, userPrompt) {
14052
+ async handleResolveEntities(toolInput) {
14101
14053
  const fn = this.config.collections?.["entity-search"]?.["resolveEntities"];
14102
14054
  const mentions = Array.isArray(toolInput?.mentions) ? toolInput.mentions.filter((m) => m?.text) : [];
14103
14055
  const entityTypes = Array.isArray(toolInput?.entityTypes) ? toolInput.entityTypes.filter(Boolean) : [];
14104
- const askedConcepts = Array.isArray(toolInput?.concepts) ? toolInput.concepts.filter(Boolean) : [];
14105
- const { _trace: conceptTrace, ...conceptBlock } = await this.resolveConceptsForTurn(askedConcepts, userPrompt);
14106
- if (conceptTrace) {
14107
- try {
14108
- if (this.config.trace) {
14109
- this.config.trace.concepts.calls.push({ via: "resolve_entities", ...conceptTrace });
14110
- }
14111
- } catch (traceErr) {
14112
- logger.warn(`[MainAgent] Trace capture failed for concepts-in-resolve_entities (non-blocking): ${traceErr}`);
14113
- }
14056
+ if (this.streamBuffer.hasCallback()) {
14057
+ const named = mentions.map((m) => `"${m.text}"`).join(", ");
14058
+ this.streamBuffer.write(`
14059
+
14060
+ \u{1F50D} **Resolving entities${named ? `: ${named}` : ""}...**
14061
+
14062
+ `);
14063
+ await streamDelay();
14114
14064
  }
14115
14065
  const traceEarlyReturn = (note) => {
14116
14066
  try {
@@ -14123,23 +14073,48 @@ ${r.body}`).join("\n\n");
14123
14073
  };
14124
14074
  if (!fn) {
14125
14075
  traceEarlyReturn("Entity search unavailable");
14126
- return JSON.stringify({ resolved: [], unresolved: [], entityMap: {}, ...conceptBlock, note: "Entity search is not available; proceed using the schema." });
14076
+ if (this.streamBuffer.hasCallback()) {
14077
+ this.streamBuffer.write(`\u26A0\uFE0F **Entity search unavailable** \u2014 proceeding on schema alone.
14078
+
14079
+ `);
14080
+ }
14081
+ return JSON.stringify({ resolved: [], unresolved: [], entityMap: {}, note: "Entity search is not available; proceed using the schema." });
14127
14082
  }
14128
14083
  if (!mentions.length && !entityTypes.length) {
14129
14084
  traceEarlyReturn("Nothing named in the question to resolve");
14085
+ if (this.streamBuffer.hasCallback()) {
14086
+ this.streamBuffer.write(`\u2705 **Entity search:** nothing to resolve for this question.
14087
+
14088
+ `);
14089
+ }
14130
14090
  return JSON.stringify({
14131
14091
  resolved: [],
14132
14092
  unresolved: [],
14133
14093
  entityMap: {},
14134
- ...conceptBlock,
14135
- 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."
14094
+ note: "Nothing to resolve for this question \u2014 continue."
14136
14095
  });
14137
14096
  }
14138
14097
  try {
14139
14098
  const res = await fn({ mentions, entityTypes });
14140
14099
  logger.info(
14141
- `[MainAgent] resolve_entities: ${res?.resolved?.length ?? 0} resolved, ${res?.unresolved?.length ?? 0} unresolved, ${Object.keys(res?.entityMap || {}).length} concept(s)`
14100
+ `[MainAgent] resolve_entities: ${res?.resolved?.length ?? 0} resolved, ${res?.unresolved?.length ?? 0} unresolved, ${Object.keys(res?.entityMap || {}).length} type(s)`
14142
14101
  );
14102
+ if (this.streamBuffer.hasCallback()) {
14103
+ const resolvedCount = res?.resolved?.length ?? 0;
14104
+ const unresolvedCount = res?.unresolved?.length ?? 0;
14105
+ const names = (Array.isArray(res?.resolved) ? res.resolved : []).map((r) => {
14106
+ const label = r?.displayName || r?.mention;
14107
+ if (!label) return null;
14108
+ return r?.entityType ? `${label} (${r.entityType})` : label;
14109
+ }).filter(Boolean).join(", ");
14110
+ const unresolvedNames = (Array.isArray(res?.unresolved) ? res.unresolved : []).map((u) => typeof u === "string" ? u : u?.text).filter(Boolean).join(", ");
14111
+ let line = `\u2705 **Entity search:** ${resolvedCount} resolved`;
14112
+ if (names) line += ` (${names})`;
14113
+ if (unresolvedCount) line += `, ${unresolvedCount} unresolved${unresolvedNames ? ` (${unresolvedNames})` : ""}`;
14114
+ this.streamBuffer.write(`${line}
14115
+
14116
+ `);
14117
+ }
14143
14118
  const TIE_EPSILON = 0.05;
14144
14119
  const key = (e) => `${e.entityType}::${e.instanceId}`;
14145
14120
  const add = (mention, c) => {
@@ -14175,7 +14150,7 @@ ${r.body}`).join("\n\n");
14175
14150
  attrs: r.attrs && typeof r.attrs === "object" ? r.attrs : void 0
14176
14151
  });
14177
14152
  }
14178
- const seenByLLM = { ...res, ...conceptBlock };
14153
+ const seenByLLM = { ...res };
14179
14154
  try {
14180
14155
  if (this.config.trace) {
14181
14156
  this.config.trace.entityResolution.calls.push({ mentions, entityTypes, ...seenByLLM });
@@ -14187,6 +14162,13 @@ ${r.body}`).join("\n\n");
14187
14162
  } catch (err) {
14188
14163
  const msg = err instanceof Error ? err.message : String(err);
14189
14164
  logger.warn(`[MainAgent] resolve_entities failed (non-blocking): ${msg}`);
14165
+ if (this.streamBuffer.hasCallback()) {
14166
+ this.streamBuffer.write(`
14167
+
14168
+ \u274C **Entity search failed:** ${msg}
14169
+
14170
+ `);
14171
+ }
14190
14172
  try {
14191
14173
  if (this.config.trace) {
14192
14174
  this.config.trace.entityResolution.calls.push({
@@ -14205,8 +14187,7 @@ ${r.body}`).join("\n\n");
14205
14187
  resolved: [],
14206
14188
  entityMap: {},
14207
14189
  error: msg,
14208
- unresolved: mentions.map((m) => m.text),
14209
- ...conceptBlock
14190
+ unresolved: mentions.map((m) => m.text)
14210
14191
  });
14211
14192
  }
14212
14193
  }
@@ -14299,7 +14280,7 @@ ${lines.join("\n")}`;
14299
14280
  if (this.streamBuffer.hasCallback()) {
14300
14281
  this.streamBuffer.write(`
14301
14282
 
14302
- \u{1F527} **Running ${tool.name}...**
14283
+ \u{1F527} **Running ${describeDirectToolCall(tool, toolInput)}...**
14303
14284
 
14304
14285
  `);
14305
14286
  await streamDelay();
@@ -14310,19 +14291,24 @@ ${lines.join("\n")}`;
14310
14291
  if (result && result.error) {
14311
14292
  const errorMsg = typeof result.error === "string" ? result.error : JSON.stringify(result.error);
14312
14293
  logger.warn(`[MainAgent] Direct tool "${tool.name}" returned error: ${errorMsg}`);
14294
+ this.config.trace?.directTools.calls.push({ toolId: tool.id, input: toolInput, error: errorMsg, durationMs: executionTimeMs });
14313
14295
  return `\u274C Tool "${tool.name}" error: ${errorMsg}`;
14314
14296
  }
14297
+ if (Array.isArray(result?.resolvedConcepts)) {
14298
+ this.retainResolvedConcepts(result.resolvedConcepts, { alwaysInclude: true });
14299
+ }
14315
14300
  const resultData = result.data || result;
14316
14301
  const rowCount = Array.isArray(resultData) ? resultData.length : 1;
14317
14302
  if (this.streamBuffer.hasCallback()) {
14318
- this.streamBuffer.write(`\u2705 **${tool.name}** completed (${rowCount} results, ${executionTimeMs}ms)
14303
+ this.streamBuffer.write(`\u2705 **${describeDirectToolCall(tool, toolInput)}** completed (${rowCount} results, ${executionTimeMs}ms)
14319
14304
 
14320
14305
  `);
14321
14306
  }
14307
+ const isConceptTool = tool.id === "concept";
14322
14308
  const formattedResult = formatToolResultForLLM(result, {
14323
14309
  toolName: tool.name,
14324
- maxRows: 5,
14325
- maxCharsPerField: 200
14310
+ maxRows: isConceptTool ? 25 : 5,
14311
+ maxCharsPerField: isConceptTool ? 4e3 : 200
14326
14312
  });
14327
14313
  const sampleData = Array.isArray(resultData) ? resultData.slice(0, TOOL_TRACKING_SAMPLE_ROWS) : Array.isArray(formattedResult.data) ? formattedResult.data.slice(0, TOOL_TRACKING_SAMPLE_ROWS) : [];
14328
14314
  executedTools.push({
@@ -14338,6 +14324,7 @@ ${lines.join("\n")}`;
14338
14324
  outputSchema: tool.outputSchema
14339
14325
  });
14340
14326
  const formatted = typeof formattedResult === "string" ? formattedResult : JSON.stringify(formattedResult);
14327
+ this.config.trace?.directTools.calls.push({ toolId: tool.id, input: toolInput, output: result, durationMs: executionTimeMs });
14341
14328
  return `\u2705 Tool "${tool.name}" executed successfully. ${rowCount} results returned.
14342
14329
 
14343
14330
  ${formatted}`;
@@ -14348,10 +14335,11 @@ ${formatted}`;
14348
14335
  if (this.streamBuffer.hasCallback()) {
14349
14336
  this.streamBuffer.write(`
14350
14337
 
14351
- \u274C **${tool.name} failed:** ${errorMsg}
14338
+ \u274C **${describeDirectToolCall(tool, toolInput)} failed:** ${errorMsg}
14352
14339
 
14353
14340
  `);
14354
14341
  }
14342
+ this.config.trace?.directTools.calls.push({ toolId: tool.id, input: toolInput, error: errorMsg, durationMs: executionTimeMs });
14355
14343
  return `\u274C Tool "${tool.name}" failed: ${errorMsg}`;
14356
14344
  }
14357
14345
  }
@@ -14504,6 +14492,11 @@ ${propLines}` : ""
14504
14492
  type: "string",
14505
14493
  enum: ["raw", "pre-aggregate", "summary"],
14506
14494
  description: 'How to return data. "pre-aggregate": use GROUP BY/SUM/COUNT for totals. "summary": high-level metrics. "raw": individual records.'
14495
+ },
14496
+ concepts: {
14497
+ type: "array",
14498
+ items: { type: "string" },
14499
+ 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.`
14507
14500
  }
14508
14501
  },
14509
14502
  required: ["intent"]
@@ -14727,6 +14720,22 @@ ${scopeWarning} The script re-runs them, so you can include more than one \`ctx.
14727
14720
  return buildSourceSummaries(this.externalTools);
14728
14721
  }
14729
14722
  };
14723
+ function describeDirectToolCall(tool, toolInput) {
14724
+ const method = toolInput?.method;
14725
+ if (typeof method !== "string") return tool.name;
14726
+ switch (method) {
14727
+ case "find":
14728
+ return typeof toolInput.query === "string" ? `${tool.name} (find: "${toolInput.query}")` : `${tool.name} (find)`;
14729
+ case "get": {
14730
+ const names = Array.isArray(toolInput.names) ? toolInput.names.join(", ") : void 0;
14731
+ return names ? `${tool.name} (get: ${names})` : `${tool.name} (get)`;
14732
+ }
14733
+ case "run":
14734
+ return typeof toolInput.name === "string" ? `${tool.name} (run: ${toolInput.name})` : `${tool.name} (run)`;
14735
+ default:
14736
+ return `${tool.name} (${method})`;
14737
+ }
14738
+ }
14730
14739
  function extractTablesFromSQL(sqls) {
14731
14740
  const tables = /* @__PURE__ */ new Set();
14732
14741
  const pattern = /(?:FROM|JOIN)\s+"?([a-zA-Z_][\w.]*)"?/gi;
@@ -14750,6 +14759,7 @@ function createRunTrace() {
14750
14759
  scriptMatcher: null,
14751
14760
  entityResolution: { calls: [] },
14752
14761
  concepts: { calls: [] },
14762
+ directTools: { calls: [] },
14753
14763
  componentGen: []
14754
14764
  };
14755
14765
  }
@@ -16044,12 +16054,12 @@ function answerCatalogForBinding(components) {
16044
16054
  var import_dotenv = __toESM(require("dotenv"));
16045
16055
 
16046
16056
  // src/userResponse/schema.ts
16047
- var import_path4 = __toESM(require("path"));
16057
+ var import_path5 = __toESM(require("path"));
16048
16058
  var import_fs5 = __toESM(require("fs"));
16049
16059
  var Schema = class {
16050
16060
  constructor(schemaFilePath) {
16051
16061
  this.cachedSchema = null;
16052
- this.schemaFilePath = schemaFilePath || import_path4.default.join(process.cwd(), "../analysis/data/schema.json");
16062
+ this.schemaFilePath = schemaFilePath || import_path5.default.join(process.cwd(), "../analysis/data/schema.json");
16053
16063
  }
16054
16064
  /**
16055
16065
  * Gets the database schema from the schema file
@@ -16057,7 +16067,7 @@ var Schema = class {
16057
16067
  */
16058
16068
  getDatabaseSchema() {
16059
16069
  try {
16060
- const dir = import_path4.default.dirname(this.schemaFilePath);
16070
+ const dir = import_path5.default.dirname(this.schemaFilePath);
16061
16071
  if (!import_fs5.default.existsSync(dir)) {
16062
16072
  logger.info(`Creating directory structure: ${dir}`);
16063
16073
  import_fs5.default.mkdirSync(dir, { recursive: true });
@@ -17661,7 +17671,7 @@ ${executedToolsText}`);
17661
17671
  */
17662
17672
  async handleUserRequest(userPrompt, components, apiKey, conversationHistory, responseMode = "text", streamCallback, collections, externalTools, userId) {
17663
17673
  const startTime = Date.now();
17664
- logger.clearFile();
17674
+ logger.clearFile(`legacy-${userPrompt}`);
17665
17675
  logger.logLLMPrompt("handleUserRequest", "user", `User Prompt: ${userPrompt}`);
17666
17676
  try {
17667
17677
  const conversationMatch = await conversation_search_default.searchConversationsWithReranking({
@@ -18194,13 +18204,28 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
18194
18204
  let runTrace;
18195
18205
  const answerComps = answerComponents(components);
18196
18206
  const bindingComps = projectForDatasetBinding(answerComps);
18197
- logger.clearFile();
18207
+ logger.clearFile(`chat-${prompt}`);
18198
18208
  logger.logLLMPrompt("agentUserResponse", "user", `User Prompt: ${prompt}`);
18199
18209
  logger.info(`[AgentFlow] Starting | provider: ${provider} | prompt: "${prompt.substring(0, 50)}..."`);
18200
18210
  try {
18201
18211
  const cached = await replayExactMatch({ prompt, collections, userId, streamCallback, method: `${provider}-agent-exact-match`, startTime });
18202
18212
  if (cached?.response) return cached.response;
18203
- const directReplay = cached?.directReplay;
18213
+ let directReplay = cached?.directReplay;
18214
+ if (!cached) {
18215
+ const supersededMatch = await conversation_search_default.findExactMatch({
18216
+ userPrompt: prompt,
18217
+ collections,
18218
+ userId,
18219
+ statuses: ["superseded"]
18220
+ });
18221
+ const boundBinding = supersededMatch?.uiBlock?.scriptBinding;
18222
+ if (boundBinding?.recipeId) {
18223
+ directReplay = { recipeId: boundBinding.recipeId, params: boundBinding.params || {} };
18224
+ logger.info(
18225
+ `[AgentFlow] Exact match found a SUPERSEDED row (recipe edited since) \u2014 will re-run current recipe ${directReplay.recipeId} directly (skip matcher LLM)`
18226
+ );
18227
+ }
18228
+ }
18204
18229
  logger.info(`[AgentFlow] No exact match \u2014 proceeding with agent`);
18205
18230
  runTrace = createRunTrace();
18206
18231
  const apiKey = (() => {
@@ -18285,7 +18310,7 @@ var get_agent_user_response = async (prompt, components, anthropicApiKey, groqAp
18285
18310
  const boundRecipe = await scriptStore.get(directReplay.recipeId);
18286
18311
  if (boundRecipe) {
18287
18312
  scriptMatch = { tier: "high", recipe: boundRecipe, extractedParams: directReplay.params };
18288
- logger.info(`[AgentFlow] TTL-expired exact match \u2014 replaying bound recipe "${boundRecipe.name}" directly (skipped ScriptMatcher LLM)`);
18313
+ logger.info(`[AgentFlow] Direct replay \u2014 running current recipe "${boundRecipe.name}" directly (skipped ScriptMatcher LLM)`);
18289
18314
  } else {
18290
18315
  logger.info(`[AgentFlow] Bound recipe ${directReplay.recipeId} no longer in store \u2014 falling back to ScriptMatcher LLM`);
18291
18316
  }
@@ -18637,6 +18662,19 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${scriptMatch
18637
18662
  }
18638
18663
  };
18639
18664
  }
18665
+ if (isDisplayEdit) {
18666
+ try {
18667
+ const invalidate = collections?.["conversation-history"]?.["invalidateByScriptRecipe"];
18668
+ if (invalidate) {
18669
+ const n = await invalidate({ recipeId: scriptMatch.recipe.id, userId });
18670
+ logger.info(`[AgentFlow] Superseded ${n?.count ?? n ?? 0} cached answer(s) bound to display-edited recipe ${scriptMatch.recipe.id}`);
18671
+ } else {
18672
+ logger.warn(`[AgentFlow] conversation-history.invalidateByScriptRecipe not wired \u2014 cached answers for recipe ${scriptMatch.recipe.id} will replay the PRE-EDIT component set`);
18673
+ }
18674
+ } catch (invErr) {
18675
+ logger.warn(`[AgentFlow] Conversation invalidation failed (non-blocking): ${invErr}`);
18676
+ }
18677
+ }
18640
18678
  const elapsedTime2 = Date.now() - startTime;
18641
18679
  logger.info(`[AgentFlow] Script flow complete | ${scriptResult.executedQueries.length} queries | ${matchedComponents2.length} components | ${elapsedTime2}ms`);
18642
18680
  return {
@@ -18792,6 +18830,67 @@ BEGIN NOW. Your VERY FIRST action MUST be a \`write_script\` tool call containin
18792
18830
  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, "");
18793
18831
  streamBuffer.flush();
18794
18832
  if (signal?.aborted) throw new RequestAbortedError();
18833
+ if (agentResponse.savedScript && editContext) {
18834
+ try {
18835
+ const authored = agentResponse.savedScript;
18836
+ if (runTrace) {
18837
+ try {
18838
+ runTrace.script = {
18839
+ reused: false,
18840
+ recipeId: authored.recipeId,
18841
+ body: authored.scriptBody,
18842
+ reasoning: editContext.instruction ?? null,
18843
+ executionResult: authored.executionResult ?? null,
18844
+ failedAttempts: agentResponse.failedScriptAttempts
18845
+ };
18846
+ } catch (traceErr) {
18847
+ logger.warn(`[AgentFlow] Trace capture failed for edited script (non-blocking): ${traceErr}`);
18848
+ }
18849
+ }
18850
+ const summaryLine = (agentResponse.text || "").split("\n").map((l) => l.trim()).find((l) => l.startsWith("\u270F\uFE0F"));
18851
+ const changeSummary = (summaryLine || editContext.instruction).replace(/^✏️\s*/, "").replace(/^\**\s*Updated the script:\s*/i, "").replace(/\*\*/g, "").replace(/\s+/g, " ").trim().slice(0, 300);
18852
+ const committed = await scriptStore.commitEdit(
18853
+ editContext.recipeId,
18854
+ {
18855
+ recipeId: authored.recipeId,
18856
+ name: authored.name,
18857
+ intentDescription: authored.intentDescription,
18858
+ tags: authored.tags,
18859
+ parameters: authored.parameters,
18860
+ scriptBody: authored.scriptBody,
18861
+ sourceIds: authored.sourceIds,
18862
+ tables: authored.tables
18863
+ },
18864
+ { instruction: editContext.instruction, changeSummary, editedBy: userId }
18865
+ );
18866
+ if (committed) {
18867
+ agentResponse.savedScript.recipeId = committed.id;
18868
+ scriptBinding = {
18869
+ recipeId: committed.id,
18870
+ version: committed.version,
18871
+ params: {},
18872
+ name: committed.name,
18873
+ columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
18874
+ };
18875
+ try {
18876
+ const invalidate = collections?.["conversation-history"]?.["invalidateByScriptRecipe"];
18877
+ if (invalidate) {
18878
+ const n = await invalidate({ recipeId: committed.id, userId });
18879
+ logger.info(`[AgentFlow] Superseded ${n?.count ?? n ?? 0} cached answer(s) bound to edited recipe ${committed.id}`);
18880
+ } else {
18881
+ logger.warn(`[AgentFlow] conversation-history.invalidateByScriptRecipe not wired \u2014 cached answers for recipe ${committed.id} will replay the PRE-EDIT result`);
18882
+ }
18883
+ } catch (invErr) {
18884
+ logger.warn(`[AgentFlow] Conversation invalidation failed (non-blocking): ${invErr}`);
18885
+ }
18886
+ } else {
18887
+ logger.warn(`[AgentFlow] commitEdit failed for "${editedRecipeName}" \u2014 original recipe left unchanged`);
18888
+ }
18889
+ } catch (err) {
18890
+ const msg = err instanceof Error ? err.message : String(err);
18891
+ logger.warn(`[AgentFlow] Failed to commit script edit (non-blocking): ${msg}`);
18892
+ }
18893
+ }
18795
18894
  if (agentResponse.executedTools.length === 0 && agentResponse.savedScript?.executedQueries && agentResponse.savedScript.executedQueries.length > 0) {
18796
18895
  for (const q of agentResponse.savedScript.executedQueries) {
18797
18896
  agentResponse.executedTools.push({
@@ -19065,66 +19164,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${carriedRequ
19065
19164
  }
19066
19165
  };
19067
19166
  }
19068
- if (agentResponse.savedScript && editContext) {
19069
- try {
19070
- const authored = agentResponse.savedScript;
19071
- if (runTrace) {
19072
- try {
19073
- runTrace.script = {
19074
- reused: false,
19075
- recipeId: authored.recipeId,
19076
- body: authored.scriptBody,
19077
- reasoning: editContext.instruction ?? null,
19078
- executionResult: authored.executionResult ?? null,
19079
- failedAttempts: agentResponse.failedScriptAttempts
19080
- };
19081
- } catch (traceErr) {
19082
- logger.warn(`[AgentFlow] Trace capture failed for edited script (non-blocking): ${traceErr}`);
19083
- }
19084
- }
19085
- const summaryLine = (agentResponse.text || "").split("\n").map((l) => l.trim()).find((l) => l.startsWith("\u270F\uFE0F"));
19086
- const changeSummary = (summaryLine || editContext.instruction).replace(/^✏️\s*/, "").replace(/^\**\s*Updated the script:\s*/i, "").replace(/\*\*/g, "").replace(/\s+/g, " ").trim().slice(0, 300);
19087
- const committed = await scriptStore.commitEdit(
19088
- editContext.recipeId,
19089
- {
19090
- recipeId: authored.recipeId,
19091
- name: authored.name,
19092
- intentDescription: authored.intentDescription,
19093
- tags: authored.tags,
19094
- parameters: authored.parameters,
19095
- scriptBody: authored.scriptBody,
19096
- sourceIds: authored.sourceIds,
19097
- tables: authored.tables
19098
- },
19099
- { instruction: editContext.instruction, changeSummary, editedBy: userId }
19100
- );
19101
- if (committed) {
19102
- scriptBinding = {
19103
- recipeId: committed.id,
19104
- version: committed.version,
19105
- params: {},
19106
- name: committed.name,
19107
- columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
19108
- };
19109
- try {
19110
- const invalidate = collections?.["conversation-history"]?.["invalidateByScriptRecipe"];
19111
- if (invalidate) {
19112
- const n = await invalidate({ recipeId: committed.id, userId });
19113
- logger.info(`[AgentFlow] Superseded ${n?.count ?? n ?? 0} cached answer(s) bound to edited recipe ${committed.id}`);
19114
- } else {
19115
- logger.warn(`[AgentFlow] conversation-history.invalidateByScriptRecipe not wired \u2014 cached answers for recipe ${committed.id} will replay the PRE-EDIT result`);
19116
- }
19117
- } catch (invErr) {
19118
- logger.warn(`[AgentFlow] Conversation invalidation failed (non-blocking): ${invErr}`);
19119
- }
19120
- } else {
19121
- logger.warn(`[AgentFlow] commitEdit failed for "${editedRecipeName}" \u2014 original recipe left unchanged`);
19122
- }
19123
- } catch (err) {
19124
- const msg = err instanceof Error ? err.message : String(err);
19125
- logger.warn(`[AgentFlow] Failed to commit script edit (non-blocking): ${msg}`);
19126
- }
19127
- } else if (agentResponse.savedScript) {
19167
+ if (agentResponse.savedScript && !editContext) {
19128
19168
  try {
19129
19169
  const authored = agentResponse.savedScript;
19130
19170
  if (runTrace) {
@@ -19353,7 +19393,8 @@ function transformUIBlockForDB(uiblock, userPrompt, uiBlockId) {
19353
19393
  };
19354
19394
  }
19355
19395
  function hasTraceContent(trace) {
19356
- 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;
19396
+ if (!trace || typeof trace !== "object") return false;
19397
+ 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;
19357
19398
  }
19358
19399
  async function saveConversation(params) {
19359
19400
  const { userId, userPrompt, uiblock, uiBlockId, threadId, collections, status = "success", error: turnError, trace } = params;
@@ -19499,6 +19540,18 @@ var CONTEXT_CONFIG = {
19499
19540
  function stripStreamMarkers(text) {
19500
19541
  return text.replace(/__TEXT_COMPLETE__[\s\S]*/g, "").replace(/__ANSWER_COMPONENT_START__[\s\S]*?__ANSWER_COMPONENT_END__/g, "").replace(/__COMPONENT_GENERATION_START__/g, "").trim();
19501
19542
  }
19543
+ function promptContextFrom(raw) {
19544
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
19545
+ const source = raw;
19546
+ const context = {};
19547
+ if (typeof source.dashboardId === "string" && source.dashboardId.trim()) {
19548
+ context.dashboardId = source.dashboardId.trim();
19549
+ }
19550
+ if (source.filters && typeof source.filters === "object" && !Array.isArray(source.filters)) {
19551
+ context.filters = source.filters;
19552
+ }
19553
+ return Object.keys(context).length ? context : void 0;
19554
+ }
19502
19555
  var inFlightAborts = /* @__PURE__ */ new Map();
19503
19556
  var recentlyAborted = /* @__PURE__ */ new Map();
19504
19557
  var ABORT_TOMBSTONE_TTL_MS = 3e4;
@@ -19562,6 +19615,10 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19562
19615
  const userConfig = payload.config;
19563
19616
  const SA_RUNTIME = payload.SA_RUNTIME;
19564
19617
  const wsId = userPromptRequest.from.id || "unknown";
19618
+ const reported = (response) => ({
19619
+ model: response?.data?.model,
19620
+ usage: response?.data?.usage
19621
+ });
19565
19622
  const promptContext = `User Prompt: ${prompt?.substring(0, 50)}${(prompt?.length || 0) > 50 ? "..." : ""}`;
19566
19623
  llmUsageLogger.resetLogFile(promptContext);
19567
19624
  userPromptErrorLogger.resetLogFile(promptContext);
@@ -19685,7 +19742,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19685
19742
  conversationHistory,
19686
19743
  onStream: streamCallback,
19687
19744
  onNarration: narrationCallback,
19688
- signal: abortController.signal
19745
+ signal: abortController.signal,
19746
+ context: promptContextFrom(payload.context)
19689
19747
  }) : await get_agent_user_response(
19690
19748
  prompt,
19691
19749
  components,
@@ -19742,7 +19800,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19742
19800
  }
19743
19801
  if (userId) {
19744
19802
  const latencyMs = Date.now() - requestStartTime;
19745
- const model = mainAgentModel || "unknown";
19803
+ const turn = reported(userResponse);
19804
+ const model = turn.model || mainAgentModel || "unknown";
19746
19805
  const event = analyticsClient.buildEvent({
19747
19806
  type: "chat_agent",
19748
19807
  userId,
@@ -19755,7 +19814,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19755
19814
  model,
19756
19815
  errorMessage: "aborted",
19757
19816
  conversationId: savedConversationId2,
19758
- response: savedResponse2
19817
+ response: savedResponse2,
19818
+ usage: turn.usage
19759
19819
  });
19760
19820
  analyticsClient.track(event).catch(() => {
19761
19821
  });
@@ -19809,7 +19869,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19809
19869
  }
19810
19870
  if (userId) {
19811
19871
  const latencyMs = Date.now() - requestStartTime;
19812
- const model = mainAgentModel || "unknown";
19872
+ const turn = reported(userResponse);
19873
+ const model = turn.model || mainAgentModel || "unknown";
19813
19874
  const event = analyticsClient.buildEvent({
19814
19875
  type: "chat_agent",
19815
19876
  userId,
@@ -19822,7 +19883,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19822
19883
  model,
19823
19884
  errorMessage,
19824
19885
  conversationId: savedConversationId2,
19825
- response: savedResponse2
19886
+ response: savedResponse2,
19887
+ usage: turn.usage
19826
19888
  });
19827
19889
  analyticsClient.track(event).catch(() => {
19828
19890
  });
@@ -19906,7 +19968,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19906
19968
  llmUsageLogger.logSessionSummary(prompt?.substring(0, 50));
19907
19969
  if (userId) {
19908
19970
  const latencyMs = Date.now() - requestStartTime;
19909
- const model = mainAgentModel || "unknown";
19971
+ const turn = reported(userResponse);
19972
+ const model = turn.model || mainAgentModel || "unknown";
19910
19973
  const componentConfig = userResponse.data?.component?.props?.config;
19911
19974
  const queryMap = componentConfig?._queryMap || {};
19912
19975
  const sqlStatements = Object.values(queryMap).filter((v) => typeof v === "string");
@@ -19938,7 +20001,8 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
19938
20001
  sourcesUsed: sourcesUsed.length > 0 ? sourcesUsed : void 0,
19939
20002
  sqlGenerated: sqlStatements.length > 0 ? sqlStatements.join(";\n") : void 0,
19940
20003
  conversationId: savedConversationId,
19941
- response: savedResponse
20004
+ response: savedResponse,
20005
+ usage: turn.usage
19942
20006
  });
19943
20007
  analyticsClient.track(event).catch(() => {
19944
20008
  });
@@ -19961,7 +20025,7 @@ async function handleUserPromptRequest(data, components, sendMessage, anthropicA
19961
20025
  id: `comp_${Math.random().toString(36).substring(2, 8)}`
19962
20026
  }));
19963
20027
  }
19964
- const { trace: _trace, ...dataWithoutTrace } = response.data ?? {};
20028
+ const { trace: _trace, usage: _usage, model: _model, ...dataWithoutTrace } = response.data ?? {};
19965
20029
  sendDataResponse4(
19966
20030
  response.id || data.id,
19967
20031
  {
@@ -24051,7 +24115,7 @@ var processDashCompRequest = async (data, components, _sendMessage, anthropicApi
24051
24115
  const conversationHistory = dashboardId ? dashboardConversationHistory.getHistory(dashboardId, userId) : void 0;
24052
24116
  const promptContext = `DASH_COMP[${reqType}]: ${prompt?.substring(0, 50)}${(prompt?.length || 0) > 50 ? "..." : ""}`;
24053
24117
  llmUsageLogger.resetLogFile(promptContext);
24054
- logger.clearFile();
24118
+ logger.clearFile(promptContext);
24055
24119
  if (!prompt) {
24056
24120
  errors.push("Prompt is required");
24057
24121
  }
@@ -24582,6 +24646,9 @@ ${marker}
24582
24646
  }
24583
24647
 
24584
24648
  // src/codingAgent/drivers/pi.ts
24649
+ var crypto3 = __toESM(require("crypto"));
24650
+ var fs8 = __toESM(require("fs"));
24651
+ var path9 = __toESM(require("path"));
24585
24652
  var dynamicImport2 = new Function("specifier", "return import(specifier)");
24586
24653
  var piModulePromise = null;
24587
24654
  function loadPi() {
@@ -24593,46 +24660,111 @@ function loadPi() {
24593
24660
  }
24594
24661
  return piModulePromise;
24595
24662
  }
24596
- var authAndRegistry = null;
24597
- function getAuthAndRegistry() {
24598
- if (!authAndRegistry) {
24599
- authAndRegistry = loadPi().then(({ AuthStorage, ModelRegistry }) => {
24600
- const authStorage = AuthStorage.create();
24601
- return { authStorage, modelRegistry: ModelRegistry.create(authStorage) };
24663
+ var runtimePromise = null;
24664
+ function modelRuntime(pi) {
24665
+ if (!runtimePromise) {
24666
+ const agentDir = pi.getAgentDir();
24667
+ runtimePromise = pi.ModelRuntime.create({
24668
+ authPath: `${agentDir}/auth.json`,
24669
+ modelsStorePath: `${agentDir}/models-store.json`,
24670
+ allowModelNetwork: true
24671
+ }).catch((err) => {
24672
+ runtimePromise = null;
24673
+ throw err;
24602
24674
  });
24603
24675
  }
24604
- return authAndRegistry;
24676
+ return runtimePromise;
24605
24677
  }
24606
- var DEFAULT_PROVIDER = "openai-codex";
24607
- var DEFAULT_MODEL_ID = process.env.PI_AGENT_MODEL || "gpt-5.5";
24678
+ var DEFAULT_PROVIDER = process.env.PI_PROVIDER || "openai-codex";
24679
+ var DEFAULT_MODEL_ID = process.env.PI_MODEL || process.env.PI_AGENT_MODEL || "gpt-5.6-luna";
24608
24680
  var KNOWN_UNUSABLE_MODEL_IDS = /* @__PURE__ */ new Set(["gpt-5.3-codex-spark"]);
24609
- async function resolveModel(modelRegistry, spec) {
24610
- const [provider, modelId] = spec?.includes("/") ? [spec.slice(0, spec.indexOf("/")), spec.slice(spec.indexOf("/") + 1)] : [spec || DEFAULT_PROVIDER, void 0];
24611
- const available = modelRegistry.getAvailable().filter((m) => m.provider === provider);
24612
- const wanted = modelId ?? (spec ? void 0 : DEFAULT_MODEL_ID);
24613
- const exact = wanted && (modelRegistry.find(provider, wanted) ?? available.find((m) => m.id === wanted));
24614
- if (exact && modelRegistry.hasConfiguredAuth(exact)) return exact;
24615
- const usable = available.filter((m) => !KNOWN_UNUSABLE_MODEL_IDS.has(m.id));
24616
- if (usable.length) return usable[0];
24617
- if (available.length) return available[0];
24618
- throw new Error(
24619
- `No model available from "${provider}". Authorise it in a terminal on this machine: run \`pi\`, then \`/login\`.`
24620
- );
24681
+ async function resolveModel(runtime, spec) {
24682
+ const slash = spec?.indexOf("/") ?? -1;
24683
+ const provider = slash > 0 ? spec.slice(0, slash) : DEFAULT_PROVIDER;
24684
+ const wanted = slash > 0 ? spec.slice(slash + 1) : spec || DEFAULT_MODEL_ID;
24685
+ const available = await runtime.getAvailable(provider);
24686
+ const exact = available.find((m) => m.id === wanted);
24687
+ if (exact) return exact;
24688
+ const fallback = available.find((m) => !KNOWN_UNUSABLE_MODEL_IDS.has(m.id)) ?? available[0];
24689
+ if (fallback) {
24690
+ console.warn(`[pi] model ${provider}/${wanted} unavailable \u2014 using ${fallback.provider}/${fallback.id}`);
24691
+ return fallback;
24692
+ }
24693
+ throw new Error(`No model available from "${provider}". Authorise it in a terminal on this machine: run \`pi\`, then \`/login\`.`);
24694
+ }
24695
+ var SHELL_ENV_ALLOWLIST = [
24696
+ "PATH",
24697
+ "Path",
24698
+ "HOME",
24699
+ "USER",
24700
+ "USERNAME",
24701
+ "LOGNAME",
24702
+ "SHELL",
24703
+ "LANG",
24704
+ "LC_ALL",
24705
+ "TERM",
24706
+ "TMPDIR",
24707
+ "TEMP",
24708
+ "TMP",
24709
+ "NODE_EXTRA_CA_CERTS",
24710
+ // Windows: without these a shell cannot start programs.
24711
+ "SystemRoot",
24712
+ "SYSTEMROOT",
24713
+ "windir",
24714
+ "ComSpec",
24715
+ "COMSPEC",
24716
+ "PATHEXT",
24717
+ "USERPROFILE",
24718
+ "APPDATA",
24719
+ "LOCALAPPDATA",
24720
+ "HOMEDRIVE",
24721
+ "HOMEPATH",
24722
+ "ProgramFiles",
24723
+ "ProgramData",
24724
+ "MSYSTEM"
24725
+ ];
24726
+ function shellEnv(run, inherit) {
24727
+ if (inherit) return { ...process.env, ...run };
24728
+ const env = {};
24729
+ for (const key of SHELL_ENV_ALLOWLIST) {
24730
+ if (process.env[key] !== void 0) env[key] = process.env[key];
24731
+ }
24732
+ return { ...env, ...run };
24733
+ }
24734
+ function bashTool(pi, cwd, env) {
24735
+ const local = pi.createLocalBashOperations();
24736
+ const definition = pi.createBashToolDefinition(cwd, {
24737
+ operations: {
24738
+ exec: (command, dir, options) => local.exec(command, dir, { ...options, timeout: void 0, env: env() })
24739
+ }
24740
+ });
24741
+ const parameters = definition.parameters;
24742
+ return {
24743
+ ...definition,
24744
+ description: definition.description.replace(/\s*Optionally provide a timeout in seconds\./, ""),
24745
+ parameters: { ...parameters, properties: { command: parameters.properties.command }, required: ["command"] }
24746
+ };
24621
24747
  }
24622
24748
  function emptyUsage2() {
24623
24749
  return { inputTokens: 0, outputTokens: 0, reasoningTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, toolCalls: 0 };
24624
24750
  }
24625
24751
  var BUFFER_CAP2 = 64e3;
24626
24752
  var EVENT_CAP2 = 600;
24627
- function normEvent2(ev) {
24628
- switch (ev.type) {
24629
- case "tool_execution_start":
24630
- return { kind: "command", id: ev.toolCallId, command: ev.toolName ? `${ev.toolName} ${jsonPreview(ev.args)}` : void 0, status: "in_progress" };
24631
- case "tool_execution_end":
24632
- return { kind: "command", id: ev.toolCallId, status: ev.isError ? "failed" : "completed", done: true, output: textOf(ev.result) };
24633
- default:
24634
- return null;
24753
+ function textOf(content) {
24754
+ const blocks = content?.content ?? content;
24755
+ if (!Array.isArray(blocks)) return typeof content === "string" ? content : "";
24756
+ return blocks.filter((b) => b?.type === "text").map((b) => String(b.text ?? "")).join("").trim();
24757
+ }
24758
+ function assistantParts(message) {
24759
+ const record = message;
24760
+ if (record?.role !== "assistant" || !Array.isArray(record.content)) return { text: "", thinking: "" };
24761
+ let text = "";
24762
+ let thinking = "";
24763
+ for (const block of record.content) {
24764
+ if (block?.type === "text") text += String(block.text ?? "");
24765
+ else if (block?.type === "thinking" && !block.redacted) thinking += String(block.thinking ?? "");
24635
24766
  }
24767
+ return { text, thinking };
24636
24768
  }
24637
24769
  function jsonPreview(v) {
24638
24770
  if (v == null) return "";
@@ -24642,10 +24774,10 @@ function jsonPreview(v) {
24642
24774
  return "";
24643
24775
  }
24644
24776
  }
24645
- function textOf(content) {
24646
- const blocks = content?.content ?? content;
24647
- if (!Array.isArray(blocks)) return typeof content === "string" ? content : "";
24648
- return blocks.filter((b) => b?.type === "text").map((b) => String(b.text ?? "")).join("").trim();
24777
+ function toolEvent(name, id, args) {
24778
+ if (name === "bash") return { kind: "command", id, command: String(args?.command ?? "") };
24779
+ if (name === "write" || name === "edit") return { kind: "file", id, text: String(args?.path ?? args?.file_path ?? "") };
24780
+ return { kind: "tool", id, tool: { name: name ?? "tool", args } };
24649
24781
  }
24650
24782
  var TRANSCRIPT_CAP = 400;
24651
24783
  var sessionListeners = /* @__PURE__ */ new Set();
@@ -24663,9 +24795,6 @@ function publish(sessionKey, live, entry) {
24663
24795
  for (const listener of sessionListeners) listener(sessionKey, entry);
24664
24796
  }
24665
24797
  var liveSessions = /* @__PURE__ */ new Map();
24666
- function newSessionId() {
24667
- return `pi_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
24668
- }
24669
24798
  var IDLE_TIMEOUT_MS = Number(process.env.PI_AGENT_IDLE_MS ?? 30 * 60 * 1e3);
24670
24799
  var sweeper = null;
24671
24800
  function startSweeper() {
@@ -24684,29 +24813,76 @@ function startSweeper() {
24684
24813
  }, 6e4);
24685
24814
  sweeper.unref?.();
24686
24815
  }
24816
+ function readInstructions(opts) {
24817
+ if (!opts.instructionsFile) return void 0;
24818
+ const file = path9.resolve(opts.cwd, opts.instructionsFile);
24819
+ try {
24820
+ return { file, content: fs8.readFileSync(file, "utf-8") };
24821
+ } catch {
24822
+ return void 0;
24823
+ }
24824
+ }
24825
+ var hash = (s) => crypto3.createHash("sha256").update(s).digest("hex");
24687
24826
  function createPiSession(opts) {
24688
24827
  let running = false;
24689
- let sessionKey = opts.resumeId && liveSessions.has(opts.resumeId) ? opts.resumeId : void 0;
24690
- async function ensureSession() {
24691
- const existing = sessionKey ? liveSessions.get(sessionKey) : void 0;
24692
- if (existing) return existing;
24693
- const { createAgentSession, SessionManager } = await loadPi();
24694
- const { authStorage, modelRegistry } = await getAuthAndRegistry();
24695
- const model = await resolveModel(modelRegistry, opts.model);
24696
- const { session } = await createAgentSession({
24828
+ let sessionKey = opts.resumeId;
24829
+ async function open(resumeFile) {
24830
+ const pi = await loadPi();
24831
+ const runtime = await modelRuntime(pi);
24832
+ const model = await resolveModel(runtime, opts.model);
24833
+ const agentDir = pi.getAgentDir();
24834
+ const instructions = readInstructions(opts);
24835
+ const resourceLoader = new pi.DefaultResourceLoader({
24836
+ cwd: opts.cwd,
24837
+ agentDir,
24838
+ // The workspace's own instructions, and only them — not whatever
24839
+ // AGENTS.md files the machine's user keeps in ~/.pi or a parent folder.
24840
+ agentsFilesOverride: (base) => instructions ? { agentsFiles: [{ path: instructions.file, content: instructions.content }] } : base
24841
+ });
24842
+ await resourceLoader.reload();
24843
+ const sessionManager = resumeFile && fs8.existsSync(resumeFile) ? pi.SessionManager.open(resumeFile) : pi.SessionManager.create(opts.cwd);
24844
+ const live = {
24845
+ running: false,
24846
+ transcript: [],
24847
+ lastActive: Date.now(),
24848
+ eventLog: [],
24849
+ buf: "",
24850
+ env: opts.env,
24851
+ inheritEnv: opts.inheritEnv,
24852
+ instructions: hash(instructions?.content ?? "")
24853
+ };
24854
+ const { session } = await pi.createAgentSession({
24697
24855
  cwd: opts.cwd,
24698
- sessionManager: SessionManager.inMemory(),
24699
- authStorage,
24700
- modelRegistry,
24856
+ agentDir,
24857
+ modelRuntime: runtime,
24701
24858
  model,
24702
- tools: opts.tools ?? ["bash"]
24859
+ ...opts.tools ? { tools: opts.tools } : {},
24860
+ customTools: [bashTool(pi, opts.cwd, () => shellEnv(live.env, live.inheritEnv))],
24861
+ resourceLoader,
24862
+ sessionManager
24703
24863
  });
24704
- sessionKey = newSessionId();
24705
- const live = { session, running: false, transcript: [], lastActive: Date.now(), eventLog: [], buf: "" };
24706
- liveSessions.set(sessionKey, live);
24864
+ live.session = session;
24865
+ const file = sessionManager.getSessionFile();
24866
+ if (!file) throw new Error("pi did not give this session a file, so it could not be resumed later.");
24867
+ sessionKey = file;
24868
+ liveSessions.set(file, live);
24707
24869
  startSweeper();
24708
24870
  return live;
24709
24871
  }
24872
+ async function ensureSession() {
24873
+ const existing = sessionKey ? liveSessions.get(sessionKey) : void 0;
24874
+ if (existing) {
24875
+ if (existing.instructions !== hash(readInstructions(opts)?.content ?? "") && !existing.running) {
24876
+ existing.session.dispose();
24877
+ liveSessions.delete(sessionKey);
24878
+ return open(sessionKey);
24879
+ }
24880
+ existing.env = opts.env;
24881
+ existing.inheritEnv = opts.inheritEnv;
24882
+ return existing;
24883
+ }
24884
+ return open(sessionKey);
24885
+ }
24710
24886
  async function runTurn(prompt, handlers, signal) {
24711
24887
  const startedAt = Date.now();
24712
24888
  const usage = emptyUsage2();
@@ -24738,29 +24914,70 @@ function createPiSession(opts) {
24738
24914
  live.buf = (live.buf + chunk).slice(-BUFFER_CAP2);
24739
24915
  handlers?.onOutput?.(chunk);
24740
24916
  };
24917
+ const send = (event) => {
24918
+ live.eventLog.push(event);
24919
+ if (live.eventLog.length > EVENT_CAP2) live.eventLog.shift();
24920
+ handlers?.onEvent?.(event);
24921
+ publish(key, live, { at: Date.now(), kind: "activity", event });
24922
+ };
24923
+ let streamed = 0;
24924
+ let messageSeq = 0;
24925
+ const started = /* @__PURE__ */ new Map();
24741
24926
  const unsubscribe = live.session.subscribe((ev) => {
24742
- if (ev.type === "message_update" && ev.assistantMessageEvent?.type === "text_delta") {
24743
- const delta = ev.assistantMessageEvent.delta;
24744
- if (delta) {
24745
- emit(delta);
24746
- handlers?.onNarration?.(delta);
24927
+ switch (ev.type) {
24928
+ case "turn_start":
24929
+ send({ kind: "turn" });
24930
+ return;
24931
+ case "message_start":
24932
+ streamed = 0;
24933
+ return;
24934
+ case "message_update": {
24935
+ const { text } = assistantParts(ev.message);
24936
+ if (text.length > streamed) {
24937
+ const delta = text.slice(streamed);
24938
+ streamed = text.length;
24939
+ emit(delta);
24940
+ handlers?.onNarration?.(delta);
24941
+ }
24942
+ return;
24747
24943
  }
24748
- return;
24749
- }
24750
- if (ev.type === "tool_execution_start" || ev.type === "tool_execution_end") {
24751
- if (ev.type === "tool_execution_end") usage.toolCalls++;
24752
- const event = normEvent2(ev);
24753
- if (!event) return;
24754
- live.eventLog.push(event);
24755
- if (live.eventLog.length > EVENT_CAP2) live.eventLog.shift();
24756
- handlers?.onEvent?.(event);
24757
- publish(key, live, { at: Date.now(), kind: "activity", event });
24758
- if (ev.type === "tool_execution_start") emit(`
24944
+ case "message_end": {
24945
+ const { text, thinking } = assistantParts(ev.message);
24946
+ const id = `msg_${++messageSeq}`;
24947
+ if (thinking.trim()) send({ kind: "reasoning", id: `${id}_r`, text: thinking.trim(), status: "completed", done: true });
24948
+ if (text.trim()) send({ kind: "message", id, text: text.trim(), status: "completed", done: true });
24949
+ streamed = 0;
24950
+ return;
24951
+ }
24952
+ case "tool_execution_start": {
24953
+ const event = { ...toolEvent(ev.toolName, ev.toolCallId, ev.args), status: "in_progress" };
24954
+ if (ev.toolCallId) started.set(ev.toolCallId, event);
24955
+ send(event);
24956
+ emit(`
24759
24957
  \u2192 ${ev.toolName ?? "tool"} ${jsonPreview(ev.args)}
24760
24958
  `);
24761
- return;
24959
+ return;
24960
+ }
24961
+ case "tool_execution_end": {
24962
+ usage.toolCalls++;
24963
+ const start = ev.toolCallId && started.get(ev.toolCallId) || toolEvent(ev.toolName, ev.toolCallId, void 0);
24964
+ send({ ...start, status: ev.isError ? "failed" : "completed", done: true, output: textOf(ev.result) });
24965
+ return;
24966
+ }
24967
+ default:
24968
+ return;
24762
24969
  }
24763
24970
  });
24971
+ let early = false;
24972
+ const poll = handlers?.doneWhen ? setInterval(async () => {
24973
+ try {
24974
+ if (!early && await handlers.doneWhen()) {
24975
+ early = true;
24976
+ void live.session.abort();
24977
+ }
24978
+ } catch {
24979
+ }
24980
+ }, 1e3) : null;
24764
24981
  try {
24765
24982
  if (signal?.aborted) {
24766
24983
  status = "aborted";
@@ -24768,16 +24985,20 @@ function createPiSession(opts) {
24768
24985
  }
24769
24986
  await live.session.prompt(prompt);
24770
24987
  const err = live.session.agent.state.errorMessage;
24771
- if (err) {
24772
- status = signal?.aborted ? "aborted" : "error";
24988
+ if (early) status = "early";
24989
+ else if (signal?.aborted) status = "aborted";
24990
+ else if (err) {
24991
+ status = "error";
24773
24992
  error = err;
24774
- } else if (signal?.aborted) {
24775
- status = "aborted";
24776
24993
  }
24777
24994
  } catch (err) {
24778
- status = signal?.aborted ? "aborted" : "error";
24779
- error = err?.message ?? String(err);
24995
+ if (early) status = "early";
24996
+ else {
24997
+ status = signal?.aborted ? "aborted" : "error";
24998
+ error = err?.message ?? String(err);
24999
+ }
24780
25000
  } finally {
25001
+ if (poll) clearInterval(poll);
24781
25002
  unsubscribe();
24782
25003
  signal?.removeEventListener("abort", onAbort);
24783
25004
  live.running = false;
@@ -24818,8 +25039,8 @@ function createPiSession(opts) {
24818
25039
  }
24819
25040
  sessionKey = void 0;
24820
25041
  },
24821
- // Local-only, same as the codex driver: the live session stays in the
24822
- // module map so a later run with this sessionId can still resume it.
25042
+ // Local-only, same as the codex driver: the live session stays in the map
25043
+ // so a later run with this sessionId resumes it.
24823
25044
  dispose: () => {
24824
25045
  }
24825
25046
  };
@@ -24838,11 +25059,11 @@ function createCodingAgentSession(harness, opts) {
24838
25059
  }
24839
25060
 
24840
25061
  // src/codingAgent/registry.ts
24841
- var path8 = __toESM(require("path"));
25062
+ var path10 = __toESM(require("path"));
24842
25063
  var agents = /* @__PURE__ */ new Map();
24843
25064
  var config = {
24844
- promptRoot: path8.join(process.cwd(), ".prompts", "coding-agents"),
24845
- runtimeDir: path8.join(process.cwd(), ".coding-agent-runtime"),
25065
+ promptRoot: path10.join(process.cwd(), ".prompts", "coding-agents"),
25066
+ runtimeDir: path10.join(process.cwd(), ".coding-agent-runtime"),
24846
25067
  harness: "codex",
24847
25068
  keepRuns: 20,
24848
25069
  keepThreads: 200
@@ -24881,13 +25102,13 @@ function getCodingAgentsConfig() {
24881
25102
  return config;
24882
25103
  }
24883
25104
  function resolvePromptDir(def) {
24884
- return def.promptDir || path8.join(config.promptRoot, def.id);
25105
+ return def.promptDir || path10.join(config.promptRoot, def.id);
24885
25106
  }
24886
25107
 
24887
25108
  // src/codingAgent/runner.ts
24888
- var fs8 = __toESM(require("fs"));
25109
+ var fs9 = __toESM(require("fs"));
24889
25110
  var fsp = __toESM(require("fs/promises"));
24890
- var path9 = __toESM(require("path"));
25111
+ var path11 = __toESM(require("path"));
24891
25112
  var DEFAULT_TIMEOUT_MS = 15 * 60 * 1e3;
24892
25113
  var inFlight = /* @__PURE__ */ new Set();
24893
25114
  function slug(v) {
@@ -24897,11 +25118,11 @@ function resolveScope(def, opts) {
24897
25118
  const scope = def.scope ?? (def.mode === "chat" ? "thread" : "project");
24898
25119
  if (scope === "thread") {
24899
25120
  const id = slug(opts.threadId || "default");
24900
- return { key: `thread:${id}`, dirName: path9.join("threads", id) };
25121
+ return { key: `thread:${id}`, dirName: path11.join("threads", id) };
24901
25122
  }
24902
25123
  if (scope === "user") {
24903
25124
  const id = slug(opts.userId || "anonymous");
24904
- return { key: `user:${id}`, dirName: path9.join("users", id) };
25125
+ return { key: `user:${id}`, dirName: path11.join("users", id) };
24905
25126
  }
24906
25127
  return { key: "project", dirName: "_project" };
24907
25128
  }
@@ -24910,30 +25131,30 @@ function makeRunId() {
24910
25131
  return `${t}-${Math.random().toString(36).slice(2, 8)}`;
24911
25132
  }
24912
25133
  function statePath(scopeDir) {
24913
- return path9.join(scopeDir, "_state.json");
25134
+ return path11.join(scopeDir, "_state.json");
24914
25135
  }
24915
25136
  function loadState(scopeDir) {
24916
25137
  try {
24917
- return JSON.parse(fs8.readFileSync(statePath(scopeDir), "utf-8"));
25138
+ return JSON.parse(fs9.readFileSync(statePath(scopeDir), "utf-8"));
24918
25139
  } catch {
24919
25140
  return { turns: 0, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
24920
25141
  }
24921
25142
  }
24922
25143
  function saveState(scopeDir, state) {
24923
25144
  try {
24924
- fs8.mkdirSync(scopeDir, { recursive: true });
24925
- fs8.writeFileSync(statePath(scopeDir), JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
25145
+ fs9.mkdirSync(scopeDir, { recursive: true });
25146
+ fs9.writeFileSync(statePath(scopeDir), JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
24926
25147
  } catch (err) {
24927
25148
  logger.warn(`[CodingAgent] failed to persist session state: ${err?.message ?? err}`);
24928
25149
  }
24929
25150
  }
24930
25151
  async function clearTransient(dir) {
24931
- await fsp.rm(path9.join(dir, "input"), { recursive: true, force: true });
25152
+ await fsp.rm(path11.join(dir, "input"), { recursive: true, force: true });
24932
25153
  }
24933
25154
  function createWorkspace(dir, runId) {
24934
- const abs = (rel) => path9.join(dir, rel);
25155
+ const abs = (rel) => path11.join(dir, rel);
24935
25156
  const ensureDir = async (rel) => {
24936
- await fsp.mkdir(path9.dirname(abs(rel)), { recursive: true });
25157
+ await fsp.mkdir(path11.dirname(abs(rel)), { recursive: true });
24937
25158
  };
24938
25159
  return {
24939
25160
  dir,
@@ -24962,7 +25183,7 @@ function createWorkspace(dir, runId) {
24962
25183
  }
24963
25184
  var INSTRUCTIONS_FILE = "system.md";
24964
25185
  async function installInstructions(def, ws) {
24965
- const src = path9.join(resolvePromptDir(def), "system.md");
25186
+ const src = path11.join(resolvePromptDir(def), "system.md");
24966
25187
  try {
24967
25188
  await ws.write(INSTRUCTIONS_FILE, await fsp.readFile(src, "utf-8"));
24968
25189
  } catch {
@@ -24976,7 +25197,7 @@ async function pruneThreads(agentId, threadsDir, keep) {
24976
25197
  const dirs = (await fsp.readdir(threadsDir, { withFileTypes: true })).filter((e) => e.isDirectory());
24977
25198
  if (dirs.length <= keep) return;
24978
25199
  const aged = await Promise.all(dirs.map(async (e) => {
24979
- const dir = path9.join(threadsDir, e.name);
25200
+ const dir = path11.join(threadsDir, e.name);
24980
25201
  const stat2 = await fsp.stat(statePath(dir)).catch(() => fsp.stat(dir));
24981
25202
  return { name: e.name, dir, used: stat2.mtimeMs };
24982
25203
  }));
@@ -24996,7 +25217,7 @@ async function pruneRuns(agentDir, keep) {
24996
25217
  try {
24997
25218
  const entries = (await fsp.readdir(agentDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
24998
25219
  for (const name of entries.slice(0, Math.max(0, entries.length - keep))) {
24999
- await fsp.rm(path9.join(agentDir, name), { recursive: true, force: true });
25220
+ await fsp.rm(path11.join(agentDir, name), { recursive: true, force: true });
25000
25221
  }
25001
25222
  } catch {
25002
25223
  }
@@ -25011,17 +25232,17 @@ async function runCodingAgent(agentId, options = {}) {
25011
25232
  }
25012
25233
  const cfg = getCodingAgentsConfig();
25013
25234
  const runId = makeRunId();
25014
- const agentDir = path9.join(cfg.runtimeDir, def.id);
25235
+ const agentDir = path11.join(cfg.runtimeDir, def.id);
25015
25236
  const policy = def.session ?? {};
25016
25237
  const persistentWorkspace = policy.workspace === "persistent";
25017
25238
  const persistentSession = policy.scope === "persistent";
25018
25239
  const runAsJob = !(options.chat === true && def.mode === "task");
25019
25240
  const { key: scopeKey, dirName } = resolveScope(def, { userId: options.userId, threadId: options.threadId });
25020
- const scopeDir = path9.join(agentDir, dirName);
25021
- const dir = options.workspaceDir || (persistentWorkspace ? scopeDir : path9.join(scopeDir, "runs", runId));
25022
- fs8.mkdirSync(dir, { recursive: true });
25241
+ const scopeDir = path11.join(agentDir, dirName);
25242
+ const dir = options.workspaceDir || (persistentWorkspace ? scopeDir : path11.join(scopeDir, "runs", runId));
25243
+ fs9.mkdirSync(dir, { recursive: true });
25023
25244
  if (persistentWorkspace && !options.workspaceDir) {
25024
- fs8.mkdirSync(path9.join(dir, "memory"), { recursive: true });
25245
+ fs9.mkdirSync(path11.join(dir, "memory"), { recursive: true });
25025
25246
  if (runAsJob) await clearTransient(dir);
25026
25247
  }
25027
25248
  const lockKey = persistentWorkspace || persistentSession ? `${def.id}:${scopeKey}` : `dir:${dir}`;
@@ -25122,7 +25343,7 @@ async function runCodingAgent(agentId, options = {}) {
25122
25343
  onUsage: ctx.onUsage,
25123
25344
  // Opt-in only: see stopOnDeliverable. Letting the turn end naturally
25124
25345
  // is what makes usage reporting work at all.
25125
- ...outputPath && def.stopOnDeliverable ? { doneWhen: () => fs8.existsSync(outputPath) } : {}
25346
+ ...outputPath && def.stopOnDeliverable ? { doneWhen: () => fs9.existsSync(outputPath) } : {}
25126
25347
  },
25127
25348
  timeout.signal
25128
25349
  );
@@ -25226,9 +25447,9 @@ Fix ONLY that, and rewrite the same file at the same path. Do not re-run any que
25226
25447
  logger.info(
25227
25448
  `[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}"` : "")
25228
25449
  );
25229
- if (!options.workspaceDir && !persistentWorkspace) await pruneRuns(path9.join(scopeDir, "runs"), cfg.keepRuns);
25230
- if (!options.workspaceDir && dirName.startsWith(`threads${path9.sep}`)) {
25231
- await pruneThreads(def.id, path9.join(agentDir, "threads"), cfg.keepThreads);
25450
+ if (!options.workspaceDir && !persistentWorkspace) await pruneRuns(path11.join(scopeDir, "runs"), cfg.keepRuns);
25451
+ if (!options.workspaceDir && dirName.startsWith(`threads${path11.sep}`)) {
25452
+ await pruneThreads(def.id, path11.join(agentDir, "threads"), cfg.keepThreads);
25232
25453
  }
25233
25454
  }
25234
25455
  }
@@ -25288,14 +25509,14 @@ ${assertReadOnlySql.toString().replace("export function", "function")}
25288
25509
  `;
25289
25510
 
25290
25511
  // src/codingAgent/tools/shims.ts
25291
- var fs9 = __toESM(require("fs"));
25292
- var path10 = __toESM(require("path"));
25293
- var resolve2 = (target, name) => typeof target === "string" ? path10.join(target, name) : target.pathTo(name);
25512
+ var fs10 = __toESM(require("fs"));
25513
+ var path12 = __toESM(require("path"));
25514
+ var resolve3 = (target, name) => typeof target === "string" ? path12.join(target, name) : target.pathTo(name);
25294
25515
  function writeShims(defs, target) {
25295
25516
  for (const def of defs) {
25296
- const file = resolve2(target, def.name);
25297
- fs9.writeFileSync(file, def.body, "utf-8");
25298
- fs9.chmodSync(file, 493);
25517
+ const file = resolve3(target, def.name);
25518
+ fs10.writeFileSync(file, def.body, "utf-8");
25519
+ fs10.chmodSync(file, 493);
25299
25520
  }
25300
25521
  return defs.map((def) => ({ name: `./${def.name}`, summary: def.summary }));
25301
25522
  }
@@ -25554,17 +25775,17 @@ console.log(${JSON.stringify(
25554
25775
  }
25555
25776
 
25556
25777
  // src/codingAgent/triggers.ts
25557
- var fs10 = __toESM(require("fs"));
25558
- var path11 = __toESM(require("path"));
25778
+ var fs11 = __toESM(require("fs"));
25779
+ var path13 = __toESM(require("path"));
25559
25780
  var EVENT_BURST_CAP = 200;
25560
25781
  var capEvents = (events) => events.length > EVENT_BURST_CAP ? events.slice(-EVENT_BURST_CAP) : events;
25561
25782
  function statePath2(agentId) {
25562
- return path11.join(getCodingAgentsConfig().runtimeDir, agentId, "_triggers.json");
25783
+ return path13.join(getCodingAgentsConfig().runtimeDir, agentId, "_triggers.json");
25563
25784
  }
25564
25785
  function loadTriggerState(agentId) {
25565
25786
  let raw;
25566
25787
  try {
25567
- raw = JSON.parse(fs10.readFileSync(statePath2(agentId), "utf-8"));
25788
+ raw = JSON.parse(fs11.readFileSync(statePath2(agentId), "utf-8"));
25568
25789
  } catch {
25569
25790
  return { counts: {}, pending: {} };
25570
25791
  }
@@ -25584,8 +25805,8 @@ function loadTriggerState(agentId) {
25584
25805
  function saveTriggerState(agentId, state) {
25585
25806
  try {
25586
25807
  const p = statePath2(agentId);
25587
- fs10.mkdirSync(path11.dirname(p), { recursive: true });
25588
- fs10.writeFileSync(p, JSON.stringify(state, null, 2));
25808
+ fs11.mkdirSync(path13.dirname(p), { recursive: true });
25809
+ fs11.writeFileSync(p, JSON.stringify(state, null, 2));
25589
25810
  } catch (err) {
25590
25811
  logger.warn(`[CodingAgent:${agentId}] failed to persist trigger state: ${err?.message ?? err}`);
25591
25812
  }
@@ -25721,13 +25942,13 @@ function describeTriggers() {
25721
25942
  }
25722
25943
 
25723
25944
  // src/codingAgent/collection.ts
25724
- var fs11 = __toESM(require("fs"));
25725
- var path12 = __toESM(require("path"));
25945
+ var fs12 = __toESM(require("fs"));
25946
+ var path14 = __toESM(require("path"));
25726
25947
  function isChattable(agent) {
25727
25948
  return agent.mode !== "task";
25728
25949
  }
25729
25950
  function registerCodingAgentCollection(sdk, config2 = {}) {
25730
- const runtimeDir = config2.runtimeDir || process.env.CODING_AGENT_RUNTIME_DIR || path12.join(process.cwd(), ".coding-agent-runtime");
25951
+ const runtimeDir = config2.runtimeDir || process.env.CODING_AGENT_RUNTIME_DIR || path14.join(process.cwd(), ".coding-agent-runtime");
25731
25952
  const defaultHarness = config2.harness || process.env.CODING_AGENT_HARNESS || "codex";
25732
25953
  const sessions = /* @__PURE__ */ new Map();
25733
25954
  const running = /* @__PURE__ */ new Set();
@@ -25787,8 +26008,8 @@ function registerCodingAgentCollection(sdk, config2 = {}) {
25787
26008
  ...record.status === "skipped" ? { error: `${def.title} is in the middle of a run \u2014 try again in a moment.` } : record.error ? { error: record.error } : {}
25788
26009
  };
25789
26010
  }
25790
- const cwd = path12.join(runtimeDir, "threads", threadId);
25791
- fs11.mkdirSync(cwd, { recursive: true });
26011
+ const cwd = path14.join(runtimeDir, "threads", threadId);
26012
+ fs12.mkdirSync(cwd, { recursive: true });
25792
26013
  let session = sessions.get(threadId);
25793
26014
  if (!session) {
25794
26015
  session = createCodingAgentSession(harness || defaultHarness, {
@@ -27068,7 +27289,7 @@ function getLLMProviders() {
27068
27289
 
27069
27290
  // src/auth/user-manager.ts
27070
27291
  var import_fs6 = __toESM(require("fs"));
27071
- var import_path5 = __toESM(require("path"));
27292
+ var import_path6 = __toESM(require("path"));
27072
27293
  var import_os = __toESM(require("os"));
27073
27294
  var UserManager = class {
27074
27295
  /**
@@ -27081,7 +27302,7 @@ var UserManager = class {
27081
27302
  this.hasChanged = false;
27082
27303
  this.syncInterval = null;
27083
27304
  this.isInitialized = false;
27084
- this.filePath = import_path5.default.join(import_os.default.homedir(), ".superatom", "projects", projectId, "users.json");
27305
+ this.filePath = import_path6.default.join(import_os.default.homedir(), ".superatom", "projects", projectId, "users.json");
27085
27306
  this.syncIntervalMs = syncIntervalMs;
27086
27307
  }
27087
27308
  /**
@@ -27106,7 +27327,7 @@ var UserManager = class {
27106
27327
  */
27107
27328
  async loadUsersFromFile() {
27108
27329
  try {
27109
- const dir = import_path5.default.dirname(this.filePath);
27330
+ const dir = import_path6.default.dirname(this.filePath);
27110
27331
  if (!import_fs6.default.existsSync(dir)) {
27111
27332
  logger.info(`Creating directory structure: ${dir}`);
27112
27333
  import_fs6.default.mkdirSync(dir, { recursive: true });
@@ -27138,7 +27359,7 @@ var UserManager = class {
27138
27359
  return;
27139
27360
  }
27140
27361
  try {
27141
- const dir = import_path5.default.dirname(this.filePath);
27362
+ const dir = import_path6.default.dirname(this.filePath);
27142
27363
  if (!import_fs6.default.existsSync(dir)) {
27143
27364
  import_fs6.default.mkdirSync(dir, { recursive: true });
27144
27365
  }
@@ -27366,7 +27587,7 @@ var UserManager = class {
27366
27587
 
27367
27588
  // src/dashboards/dashboard-manager.ts
27368
27589
  var import_fs7 = __toESM(require("fs"));
27369
- var import_path6 = __toESM(require("path"));
27590
+ var import_path7 = __toESM(require("path"));
27370
27591
  var import_os2 = __toESM(require("os"));
27371
27592
  var DashboardManager = class {
27372
27593
  /**
@@ -27375,7 +27596,7 @@ var DashboardManager = class {
27375
27596
  */
27376
27597
  constructor(projectId = "snowflake-dataset") {
27377
27598
  this.projectId = projectId;
27378
- this.dashboardsBasePath = import_path6.default.join(
27599
+ this.dashboardsBasePath = import_path7.default.join(
27379
27600
  import_os2.default.homedir(),
27380
27601
  ".superatom",
27381
27602
  "projects",
@@ -27389,7 +27610,7 @@ var DashboardManager = class {
27389
27610
  * @returns Full path to dashboard data.json file
27390
27611
  */
27391
27612
  getDashboardPath(dashboardId) {
27392
- return import_path6.default.join(this.dashboardsBasePath, dashboardId, "data.json");
27613
+ return import_path7.default.join(this.dashboardsBasePath, dashboardId, "data.json");
27393
27614
  }
27394
27615
  /**
27395
27616
  * Create a new dashboard
@@ -27399,7 +27620,7 @@ var DashboardManager = class {
27399
27620
  */
27400
27621
  createDashboard(dashboardId, dashboard) {
27401
27622
  const dashboardPath = this.getDashboardPath(dashboardId);
27402
- const dashboardDir = import_path6.default.dirname(dashboardPath);
27623
+ const dashboardDir = import_path7.default.dirname(dashboardPath);
27403
27624
  if (import_fs7.default.existsSync(dashboardPath)) {
27404
27625
  throw new Error(`Dashboard '${dashboardId}' already exists`);
27405
27626
  }
@@ -27487,7 +27708,7 @@ var DashboardManager = class {
27487
27708
  */
27488
27709
  deleteDashboard(dashboardId) {
27489
27710
  const dashboardPath = this.getDashboardPath(dashboardId);
27490
- const dashboardDir = import_path6.default.dirname(dashboardPath);
27711
+ const dashboardDir = import_path7.default.dirname(dashboardPath);
27491
27712
  if (!import_fs7.default.existsSync(dashboardPath)) {
27492
27713
  logger.warn(`Dashboard not found for deletion: ${dashboardId}`);
27493
27714
  return false;
@@ -27533,7 +27754,7 @@ var DashboardManager = class {
27533
27754
 
27534
27755
  // src/reports/report-manager.ts
27535
27756
  var import_fs8 = __toESM(require("fs"));
27536
- var import_path7 = __toESM(require("path"));
27757
+ var import_path8 = __toESM(require("path"));
27537
27758
  var import_os3 = __toESM(require("os"));
27538
27759
  var ReportManager = class {
27539
27760
  /**
@@ -27542,7 +27763,7 @@ var ReportManager = class {
27542
27763
  */
27543
27764
  constructor(projectId = "snowflake-dataset") {
27544
27765
  this.projectId = projectId;
27545
- this.reportsBasePath = import_path7.default.join(
27766
+ this.reportsBasePath = import_path8.default.join(
27546
27767
  import_os3.default.homedir(),
27547
27768
  ".superatom",
27548
27769
  "projects",
@@ -27556,7 +27777,7 @@ var ReportManager = class {
27556
27777
  * @returns Full path to report data.json file
27557
27778
  */
27558
27779
  getReportPath(reportId) {
27559
- return import_path7.default.join(this.reportsBasePath, reportId, "data.json");
27780
+ return import_path8.default.join(this.reportsBasePath, reportId, "data.json");
27560
27781
  }
27561
27782
  /**
27562
27783
  * Create a new report
@@ -27566,7 +27787,7 @@ var ReportManager = class {
27566
27787
  */
27567
27788
  createReport(reportId, report) {
27568
27789
  const reportPath = this.getReportPath(reportId);
27569
- const reportDir = import_path7.default.dirname(reportPath);
27790
+ const reportDir = import_path8.default.dirname(reportPath);
27570
27791
  if (import_fs8.default.existsSync(reportPath)) {
27571
27792
  throw new Error(`Report '${reportId}' already exists`);
27572
27793
  }
@@ -27654,7 +27875,7 @@ var ReportManager = class {
27654
27875
  */
27655
27876
  deleteReport(reportId) {
27656
27877
  const reportPath = this.getReportPath(reportId);
27657
- const reportDir = import_path7.default.dirname(reportPath);
27878
+ const reportDir = import_path8.default.dirname(reportPath);
27658
27879
  if (!import_fs8.default.existsSync(reportPath)) {
27659
27880
  logger.warn(`Report not found for deletion: ${reportId}`);
27660
27881
  return false;
@@ -28183,7 +28404,7 @@ var CleanupService = class _CleanupService {
28183
28404
  };
28184
28405
 
28185
28406
  // src/dashboardAgent/collection.ts
28186
- var import_path8 = __toESM(require("path"));
28407
+ var import_path9 = __toESM(require("path"));
28187
28408
  var import_fs9 = __toESM(require("fs"));
28188
28409
 
28189
28410
  // src/dashboardAgent/tools.ts
@@ -28396,12 +28617,16 @@ function loadPi2() {
28396
28617
  if (!piModulePromise2) piModulePromise2 = dynamicImport3("@earendil-works/pi-coding-agent");
28397
28618
  return piModulePromise2;
28398
28619
  }
28399
- var authAndRegistry2 = null;
28400
- function getAuthAndRegistry2() {
28401
- if (!authAndRegistry2) {
28402
- authAndRegistry2 = loadPi2().then(({ AuthStorage, ModelRegistry }) => {
28403
- const authStorage = AuthStorage.create();
28404
- const modelRegistry = ModelRegistry.create(authStorage);
28620
+ var authAndRegistry = null;
28621
+ function getAuthAndRegistry() {
28622
+ if (!authAndRegistry) {
28623
+ authAndRegistry = loadPi2().then(async ({ ModelRuntime, getAgentDir }) => {
28624
+ const agentDir = getAgentDir();
28625
+ const modelRegistry = await ModelRuntime.create({
28626
+ authPath: `${agentDir}/auth.json`,
28627
+ modelsStorePath: `${agentDir}/models-store.json`,
28628
+ allowModelNetwork: true
28629
+ });
28405
28630
  const proxyRoot = process.env.SUPERATOM_LLM_PROXY_URL;
28406
28631
  if (proxyRoot) {
28407
28632
  modelRegistry.registerProvider("openrouter", {
@@ -28413,10 +28638,10 @@ function getAuthAndRegistry2() {
28413
28638
  '[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.'
28414
28639
  );
28415
28640
  }
28416
- return { authStorage, modelRegistry };
28641
+ return { modelRuntime: modelRegistry };
28417
28642
  });
28418
28643
  }
28419
- return authAndRegistry2;
28644
+ return authAndRegistry;
28420
28645
  }
28421
28646
  async function preResolveSchema(sdk, tool, intent, projectId) {
28422
28647
  const tier = tool.schemaTier;
@@ -28512,12 +28737,12 @@ function textDelta(event) {
28512
28737
  }
28513
28738
  var runningDashboards = /* @__PURE__ */ new Set();
28514
28739
  function registerDashboardAgentCollection(sdk, config2 = {}) {
28515
- const piCwd = config2.cwd || process.env.PI_AGENT_DIR || import_path8.default.join(process.cwd(), ".prompts", "dashboard-agent");
28516
- const runtimeDir = config2.runtimeDir || process.env.PI_AGENT_RUNTIME_DIR || import_path8.default.join(process.cwd(), ".pi-dashboard-agent-runtime");
28740
+ const piCwd = config2.cwd || process.env.PI_AGENT_DIR || import_path9.default.join(process.cwd(), ".prompts", "dashboard-agent");
28741
+ const runtimeDir = config2.runtimeDir || process.env.PI_AGENT_RUNTIME_DIR || import_path9.default.join(process.cwd(), ".pi-dashboard-agent-runtime");
28517
28742
  const freshSession = config2.freshSession ?? process.env.PI_AGENT_FRESH_SESSION !== "false";
28518
- const SESSIONS_FILE = import_path8.default.join(runtimeDir, ".pi-dashboard-sessions.json");
28519
- const DRAFTS_DIR = import_path8.default.join(runtimeDir, "drafts");
28520
- const DASHBOARD_LOG_FILE = import_path8.default.join(runtimeDir, "dashboard.log");
28743
+ const SESSIONS_FILE = import_path9.default.join(runtimeDir, ".pi-dashboard-sessions.json");
28744
+ const DRAFTS_DIR = import_path9.default.join(runtimeDir, "drafts");
28745
+ const DASHBOARD_LOG_FILE = import_path9.default.join(runtimeDir, "dashboard.log");
28521
28746
  function appendLog(text) {
28522
28747
  try {
28523
28748
  import_fs9.default.mkdirSync(runtimeDir, { recursive: true });
@@ -28541,7 +28766,7 @@ ${fullPrompt}
28541
28766
  }
28542
28767
  function readDraft(dashboardId) {
28543
28768
  try {
28544
- const raw = import_fs9.default.readFileSync(import_path8.default.join(DRAFTS_DIR, `${dashboardId}.json`), "utf-8");
28769
+ const raw = import_fs9.default.readFileSync(import_path9.default.join(DRAFTS_DIR, `${dashboardId}.json`), "utf-8");
28545
28770
  return JSON.parse(raw);
28546
28771
  } catch (err) {
28547
28772
  console.warn(`[DashboardAgent] Failed to read draft for ${dashboardId}: ${err?.message ?? err}`);
@@ -28630,14 +28855,14 @@ ${fullPrompt}
28630
28855
  }
28631
28856
  try {
28632
28857
  const { createAgentSession, SessionManager } = await loadPi2();
28633
- const { authStorage, modelRegistry } = await getAuthAndRegistry2();
28858
+ const { modelRuntime: modelRuntime2 } = await getAuthAndRegistry();
28634
28859
  let model;
28635
28860
  const explicitProvider = config2.provider || process.env.PI_AGENT_PROVIDER;
28636
28861
  const explicitModel = config2.model || process.env.PI_AGENT_MODEL;
28637
28862
  if (explicitProvider || explicitModel) {
28638
28863
  const providerName = explicitProvider || "openrouter";
28639
28864
  const modelId = explicitModel || "anthropic/claude-sonnet-4.5";
28640
- model = modelRegistry.find(providerName, modelId);
28865
+ model = modelRuntime2.getModel(providerName, modelId);
28641
28866
  if (!model) {
28642
28867
  return { success: false, error: `Model not found: ${providerName}/${modelId}. Check the configured provider/model and that its API key is configured.` };
28643
28868
  }
@@ -28650,8 +28875,8 @@ ${fullPrompt}
28650
28875
  { provider: "groq", modelId: "llama-3.3-70b-versatile" }
28651
28876
  ];
28652
28877
  for (const candidate of FALLBACK_MODELS) {
28653
- const found = modelRegistry.find(candidate.provider, candidate.modelId);
28654
- if (found && modelRegistry.hasConfiguredAuth(found)) {
28878
+ const found = modelRuntime2.getModel(candidate.provider, candidate.modelId);
28879
+ if (found && modelRuntime2.hasConfiguredAuth(found.provider)) {
28655
28880
  model = found;
28656
28881
  break;
28657
28882
  }
@@ -28736,7 +28961,7 @@ ${turn.responseText ?? ""}`).join("\n\n");
28736
28961
  console.warn(`[DashboardAgent] Failed to fetch previous responses for ${dashboardId}: ${err?.message ?? err}`);
28737
28962
  }
28738
28963
  }
28739
- const draftPath = import_path8.default.join(DRAFTS_DIR, `${dashboardId}.json`);
28964
+ const draftPath = import_path9.default.join(DRAFTS_DIR, `${dashboardId}.json`);
28740
28965
  const fullPrompt = [
28741
28966
  `Dashboard ID: ${dashboardId}.`,
28742
28967
  projectId ? `Project ID: ${projectId}.` : null,
@@ -28785,8 +29010,7 @@ ${previousResponseText}` : null,
28785
29010
  const created = await createAgentSession({
28786
29011
  cwd: piCwd,
28787
29012
  sessionManager,
28788
- authStorage,
28789
- modelRegistry,
29013
+ modelRuntime: modelRuntime2,
28790
29014
  model,
28791
29015
  customTools
28792
29016
  });
@@ -28796,8 +29020,7 @@ ${previousResponseText}` : null,
28796
29020
  const created = await createAgentSession({
28797
29021
  cwd: piCwd,
28798
29022
  sessionManager: SessionManager.create(piCwd, runtimeDir),
28799
- authStorage,
28800
- modelRegistry,
29023
+ modelRuntime: modelRuntime2,
28801
29024
  model,
28802
29025
  customTools
28803
29026
  });
@@ -29069,6 +29292,9 @@ var SuperatomSDK = class {
29069
29292
  if (config2.queryCacheTTL !== void 0) {
29070
29293
  queryCache.setTTL(config2.queryCacheTTL);
29071
29294
  }
29295
+ if (config2.queryResultCache === false) {
29296
+ queryCache.disableResultCache();
29297
+ }
29072
29298
  if (config2.dashboardHistoryTTL !== void 0) {
29073
29299
  dashboardConversationHistory.setTTL(config2.dashboardHistoryTTL);
29074
29300
  }
@@ -29153,7 +29379,7 @@ var SuperatomSDK = class {
29153
29379
  logger.info("Already connected to WebSocket");
29154
29380
  return Promise.resolve();
29155
29381
  }
29156
- return new Promise((resolve3, reject) => {
29382
+ return new Promise((resolve4, reject) => {
29157
29383
  try {
29158
29384
  const url = new URL(this.url);
29159
29385
  if (this.apiKey) {
@@ -29169,7 +29395,7 @@ var SuperatomSDK = class {
29169
29395
  logger.info("WebSocket connected successfully");
29170
29396
  this.startHeartbeat();
29171
29397
  this.flushPendingOutbox();
29172
- resolve3();
29398
+ resolve4();
29173
29399
  });
29174
29400
  this.ws.addEventListener("message", (event) => {
29175
29401
  this.handleMessage(event.data);