turbollm 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/cli.js +783 -112
  2. package/dist/webdist/assets/{AgentEditPage-DRzR4iGs.js → AgentEditPage-DrcwQcJE.js} +1 -1
  3. package/dist/webdist/assets/{ChatScreen-cMenh5vH.js → ChatScreen-BkpwUk-O.js} +15 -15
  4. package/dist/webdist/assets/{CodeComposer-WSBYcLfh.js → CodeComposer-DG4lgSoA.js} +1 -1
  5. package/dist/webdist/assets/{CodeHomeScreen-DLYhNrPE.js → CodeHomeScreen-Cqv5c0nM.js} +1 -1
  6. package/dist/webdist/assets/{CodeSessionScreen-DiOU2TWi.js → CodeSessionScreen-aQesvCPO.js} +1 -1
  7. package/dist/webdist/assets/{CustomizeScreen-lEyan0c2.js → CustomizeScreen-DRrw2hs7.js} +3 -3
  8. package/dist/webdist/assets/{DeveloperScreen-BYWhhQGb.js → DeveloperScreen-pTaZZ6r5.js} +1 -1
  9. package/dist/webdist/assets/{EnginesScreen-C5nINY73.js → EnginesScreen-BvZtUKd0.js} +2 -2
  10. package/dist/webdist/assets/{FsBrowser-BCP84FiV.js → FsBrowser-D0rDep29.js} +1 -1
  11. package/dist/webdist/assets/{ModelLoadMenu-DGZLiX2S.js → ModelLoadMenu-K1Qolm9B.js} +1 -1
  12. package/dist/webdist/assets/{ModelsScreen-D-FCPIeG.js → ModelsScreen-HfLjpGmf.js} +1 -1
  13. package/dist/webdist/assets/{SettingsScreen-X5-09oZy.js → SettingsScreen-z-UuH8MI.js} +1 -1
  14. package/dist/webdist/assets/{SkillEditPage-CgXB_6Nl.js → SkillEditPage-W3ofhEQn.js} +1 -1
  15. package/dist/webdist/assets/{TokensScreen-NrRIm4cC.js → TokensScreen-dLb-UDmL.js} +1 -1
  16. package/dist/webdist/assets/{ToolApprovalBar-tOnZJ73i.js → ToolApprovalBar-BGRW2hFN.js} +15 -15
  17. package/dist/webdist/assets/WorkspaceScreen-C5R7gzUi.js +1 -0
  18. package/dist/webdist/assets/{agent-api-CUKcfOzd.js → agent-api-C48x10WZ.js} +1 -1
  19. package/dist/webdist/assets/{arc-CvnQcBmG.js → arc-B6W63qO_.js} +1 -1
  20. package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-Pm9wMCDM.js → architectureDiagram-3BPJPVTR-BNxjHCm7.js} +1 -1
  21. package/dist/webdist/assets/{arrow-left-OByItXF5.js → arrow-left-DmGW0W4g.js} +1 -1
  22. package/dist/webdist/assets/{badge-DJWQwk9P.js → badge-IsOHcYvf.js} +1 -1
  23. package/dist/webdist/assets/{blockDiagram-GPEHLZMM-DzLgLYNm.js → blockDiagram-GPEHLZMM-DrPlDIMe.js} +1 -1
  24. package/dist/webdist/assets/{bot-BNdnf7ne.js → bot-DAa4xIIF.js} +1 -1
  25. package/dist/webdist/assets/{c4Diagram-AAUBKEIU-qsqnYbgY.js → c4Diagram-AAUBKEIU-BKtn6SbN.js} +1 -1
  26. package/dist/webdist/assets/channel-CDs60xNM.js +1 -0
  27. package/dist/webdist/assets/{chat-api-BPIB2oYY.js → chat-api-CtbwBsSY.js} +1 -1
  28. package/dist/webdist/assets/{chat-queries-C37A8E8t.js → chat-queries-BANbggZ1.js} +1 -1
  29. package/dist/webdist/assets/check-DeJoIInG.js +1 -0
  30. package/dist/webdist/assets/{chevron-left-vAm2-1DH.js → chevron-left-DScqk7lr.js} +1 -1
  31. package/dist/webdist/assets/{chevron-right-A1qanud_.js → chevron-right-DNc_yz-y.js} +1 -1
  32. package/dist/webdist/assets/{chunk-2J33WTMH-C2ZqERw1.js → chunk-2J33WTMH-B97vCEvd.js} +1 -1
  33. package/dist/webdist/assets/{chunk-4BX2VUAB-C59qC9gw.js → chunk-4BX2VUAB-B5mAgaq1.js} +1 -1
  34. package/dist/webdist/assets/{chunk-55IACEB6-Bj5dZNjh.js → chunk-55IACEB6-CCZRTh0d.js} +1 -1
  35. package/dist/webdist/assets/{chunk-727SXJPM-DDibr_98.js → chunk-727SXJPM-tZT9XNMs.js} +1 -1
  36. package/dist/webdist/assets/{chunk-AQP2D5EJ-DR5l9iQa.js → chunk-AQP2D5EJ-CfIdLt8G.js} +1 -1
  37. package/dist/webdist/assets/{chunk-FMBD7UC4-Br0rQBb-.js → chunk-FMBD7UC4-BRdScA0W.js} +1 -1
  38. package/dist/webdist/assets/{chunk-ND2GUHAM-BLhxelYC.js → chunk-ND2GUHAM-BedHAIQl.js} +1 -1
  39. package/dist/webdist/assets/{chunk-QZHKN3VN-C2hJC_sd.js → chunk-QZHKN3VN-ByAw17Nt.js} +1 -1
  40. package/dist/webdist/assets/{circle-check-BFahLD_z.js → circle-check-BNIKoJDg.js} +1 -1
  41. package/dist/webdist/assets/{circle-x-B34XrT4_.js → circle-x-DfIf4ChY.js} +1 -1
  42. package/dist/webdist/assets/classDiagram-4FO5ZUOK-BLIfDCvc.js +1 -0
  43. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-BLIfDCvc.js +1 -0
  44. package/dist/webdist/assets/{common-Cd9MCcBN.js → common-BwXsHduN.js} +1 -1
  45. package/dist/webdist/assets/{copy-button-Ctl1GqEo.js → copy-button-BcFHHH9b.js} +1 -1
  46. package/dist/webdist/assets/{cose-bilkent-S5V4N54A-lgljnskt.js → cose-bilkent-S5V4N54A--x5WjVCv.js} +1 -1
  47. package/dist/webdist/assets/{dagre-BM42HDAG-CieWJzRR.js → dagre-BM42HDAG-DWQy_ulr.js} +1 -1
  48. package/dist/webdist/assets/{diagram-2AECGRRQ-CbASwXb9.js → diagram-2AECGRRQ-DQo4yJeJ.js} +1 -1
  49. package/dist/webdist/assets/{diagram-5GNKFQAL-dH7h3v4f.js → diagram-5GNKFQAL-DwbynoQ2.js} +1 -1
  50. package/dist/webdist/assets/{diagram-KO2AKTUF-CshsiuQd.js → diagram-KO2AKTUF-CjKV0fFm.js} +1 -1
  51. package/dist/webdist/assets/{diagram-LMA3HP47-ZjRHTqGM.js → diagram-LMA3HP47-CXfjg5Fe.js} +1 -1
  52. package/dist/webdist/assets/{diagram-OG6HWLK6-BiqwWdGR.js → diagram-OG6HWLK6-CKEjI4NC.js} +1 -1
  53. package/dist/webdist/assets/{dialog-CmUNO7nv.js → dialog-uY3tk3E9.js} +1 -1
  54. package/dist/webdist/assets/{erDiagram-TEJ5UH35-57UMKMnk.js → erDiagram-TEJ5UH35-D43kYg84.js} +1 -1
  55. package/dist/webdist/assets/{external-link-BYRaM4A8.js → external-link-jnEQCJGd.js} +1 -1
  56. package/dist/webdist/assets/{flowDiagram-I6XJVG4X-yjW2Ak0W.js → flowDiagram-I6XJVG4X-qGS0J-De.js} +1 -1
  57. package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-XCpB83hN.js → ganttDiagram-6RSMTGT7-3Ud_YPtk.js} +1 -1
  58. package/dist/webdist/assets/{git-branch-Dch9ecuo.js → git-branch-BZW2_OwW.js} +1 -1
  59. package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-CR2LNCc5.js → gitGraphDiagram-PVQCEYII-BhOiYdR2.js} +1 -1
  60. package/dist/webdist/assets/index-DffknE3S.css +1 -0
  61. package/dist/webdist/assets/{index-DNrwllG_.js → index-rezSzJo3.js} +2 -2
  62. package/dist/webdist/assets/{infoDiagram-5YYISTIA-BcOPgHgK.js → infoDiagram-5YYISTIA-D2Yo845I.js} +1 -1
  63. package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-C4eg0Z0x.js → ishikawaDiagram-YF4QCWOH-BoTTMzTX.js} +1 -1
  64. package/dist/webdist/assets/{journeyDiagram-JHISSGLW-DZYiwT4f.js → journeyDiagram-JHISSGLW-B30FNQqG.js} +1 -1
  65. package/dist/webdist/assets/{kanban-definition-UN3LZRKU-pOnJphWm.js → kanban-definition-UN3LZRKU-DwuHMcyp.js} +1 -1
  66. package/dist/webdist/assets/{layers-cWfyYdPT.js → layers-BbMBWfEB.js} +1 -1
  67. package/dist/webdist/assets/{linear-VlBvT_0X.js → linear-BI-xSqy2.js} +1 -1
  68. package/dist/webdist/assets/{mermaid.core-BI2Q_Wx8.js → mermaid.core-DjBtB3Ob.js} +4 -4
  69. package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-BC5CLMVT.js → mindmap-definition-RKZ34NQL-P0Blz3ba.js} +1 -1
  70. package/dist/webdist/assets/{pencil-CLmcV9uS.js → pencil-pd4qdmIY.js} +1 -1
  71. package/dist/webdist/assets/{personas-CqrWwpby.js → personas-DGNsw4JH.js} +36 -12
  72. package/dist/webdist/assets/{pieDiagram-4H26LBE5-DAknqfMh.js → pieDiagram-4H26LBE5-D_RWWNYB.js} +1 -1
  73. package/dist/webdist/assets/{plus-CvK7zibA.js → plus-Co-23h9C.js} +1 -1
  74. package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-9LBpUvz1.js → quadrantDiagram-W4KKPZXB-0UW-qXoo.js} +1 -1
  75. package/dist/webdist/assets/{refresh-cw-BMXeVLNx.js → refresh-cw-DBHReAx0.js} +1 -1
  76. package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-CIg6A5rC.js → requirementDiagram-4Y6WPE33-DQPViUOU.js} +1 -1
  77. package/dist/webdist/assets/{rotate-ccw-B_2DOZJc.js → rotate-ccw-L6hm18l-.js} +1 -1
  78. package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-BX3yi4pm.js → sankeyDiagram-5OEKKPKP-CoFKXFxN.js} +1 -1
  79. package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-Do0XLaou.js → sequenceDiagram-3UESZ5HK-CTKbZzaz.js} +1 -1
  80. package/dist/webdist/assets/{skeleton-BN6_VQVb.js → skeleton-CTqhU4nE.js} +1 -1
  81. package/dist/webdist/assets/{sliders-horizontal-BO-2FJXO.js → sliders-horizontal-CqA4xQwh.js} +1 -1
  82. package/dist/webdist/assets/{star-gNSRckrW.js → star-C4xvUEne.js} +1 -1
  83. package/dist/webdist/assets/{stateDiagram-AJRCARHV-644YDLF8.js → stateDiagram-AJRCARHV-Bbgyioxb.js} +1 -1
  84. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-DBMc27o_.js +1 -0
  85. package/dist/webdist/assets/{switch-B_Lxu9_l.js → switch-vVu6UIJB.js} +1 -1
  86. package/dist/webdist/assets/{terminal-coo1NYsn.js → terminal-BqY0B6tG.js} +1 -1
  87. package/dist/webdist/assets/{timeline-definition-PNZ67QCA-C4ocqXD4.js → timeline-definition-PNZ67QCA-CNuDMLf0.js} +1 -1
  88. package/dist/webdist/assets/{trash-2-CaTm58vg.js → trash-2-CI-4vgjV.js} +1 -1
  89. package/dist/webdist/assets/{useIsDesktop--92FD_Wr.js → useIsDesktop-B-UPxY57.js} +1 -1
  90. package/dist/webdist/assets/{vennDiagram-CIIHVFJN-D1j0WATg.js → vennDiagram-CIIHVFJN-5KfL6f8c.js} +1 -1
  91. package/dist/webdist/assets/{wardley-L42UT6IY-CVMZBlcB.js → wardley-L42UT6IY-KSQpq69w.js} +1 -1
  92. package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-BXbaN4Ls.js → wardleyDiagram-YWT4CUSO-CRDC9-52.js} +1 -1
  93. package/dist/webdist/assets/{wrench-cQ16tDks.js → wrench-B6O2XFxJ.js} +1 -1
  94. package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-DWd8i6R6.js → xychartDiagram-2RQKCTM6-CFJUnPty.js} +1 -1
  95. package/dist/webdist/index.html +2 -2
  96. package/package.json +1 -1
  97. package/dist/webdist/assets/WorkspaceScreen-CpTRJunA.js +0 -1
  98. package/dist/webdist/assets/channel-D1GgHB0B.js +0 -1
  99. package/dist/webdist/assets/check-Bv_bnX7C.js +0 -1
  100. package/dist/webdist/assets/classDiagram-4FO5ZUOK-BiLDame3.js +0 -1
  101. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-BiLDame3.js +0 -1
  102. package/dist/webdist/assets/index-D3nTAqcF.css +0 -1
  103. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-CqPYkk1X.js +0 -1
package/dist/cli.js CHANGED
@@ -3677,7 +3677,7 @@ function enumWindowsGpus() {
3677
3677
  var DISPLAY_CLASS_GUID = "{4d36e968-e325-11ce-bfc1-08002be10318}";
3678
3678
  function readWindowsVramRegistry() {
3679
3679
  try {
3680
- const ps = `Get-ChildItem 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\${DISPLAY_CLASS_GUID}' -ErrorAction SilentlyContinue | ForEach-Object { $p = Get-ItemProperty -Path $_.PSPath -ErrorAction SilentlyContinue; if ($p.'HardwareInformation.qwMemorySize') { "$($p.DriverDesc)|$($p.'HardwareInformation.qwMemorySize')" } }`;
3680
+ const ps = `Get-ChildItem 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\${DISPLAY_CLASS_GUID}' -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match '^\\d{4}$' } | ForEach-Object { try { $p = Get-ItemProperty -Path $_.PSPath -ErrorAction Stop; if ($p.'HardwareInformation.qwMemorySize') { "$($p.DriverDesc)|$($p.'HardwareInformation.qwMemorySize')" } } catch { } }; exit 0`;
3681
3681
  const out = execFileSync2("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], {
3682
3682
  timeout: 8e3,
3683
3683
  windowsHide: true
@@ -4134,18 +4134,37 @@ function skipValue(r, t) {
4134
4134
  }
4135
4135
  r.skip(scalarSize(t));
4136
4136
  }
4137
- function readNumberOrMax(r, t) {
4137
+ function readNumberList(r, t) {
4138
4138
  if (t === T_ARRAY) {
4139
4139
  const elemType = r.u32();
4140
4140
  const count = r.u64();
4141
- let max = 0;
4142
- for (let i = 0; i < count; i++) {
4143
- const v = Number(readScalar(r, elemType));
4144
- if (v > max) max = v;
4145
- }
4146
- return max;
4141
+ const values = [];
4142
+ for (let i = 0; i < count; i++) values.push(Number(readScalar(r, elemType)));
4143
+ return { values, isArray: true };
4147
4144
  }
4148
- return Number(readScalar(r, t));
4145
+ return { values: [Number(readScalar(r, t))], isArray: false };
4146
+ }
4147
+ function readNumberOrMax(r, t) {
4148
+ return maxOf(readNumberList(r, t).values);
4149
+ }
4150
+ function maxOf(values) {
4151
+ let max = 0;
4152
+ for (const v of values) if (v > max) max = v;
4153
+ return max;
4154
+ }
4155
+ function readBoolArray(r, t) {
4156
+ if (t !== T_ARRAY) {
4157
+ skipValue(r, t);
4158
+ return [];
4159
+ }
4160
+ const elemType = r.u32();
4161
+ const count = r.u64();
4162
+ const out = [];
4163
+ for (let i = 0; i < count; i++) {
4164
+ const v = readScalar(r, elemType);
4165
+ out.push(typeof v === "string" ? v.toLowerCase() === "true" : Boolean(v));
4166
+ }
4167
+ return out;
4149
4168
  }
4150
4169
  function parseGguf(path) {
4151
4170
  const fd = openSync2(path, "r");
@@ -4159,6 +4178,8 @@ function parseGguf(path) {
4159
4178
  const m = { hasChatTemplate: false };
4160
4179
  let keyLength;
4161
4180
  let valueLength;
4181
+ let keyLengthSwa;
4182
+ let valueLengthSwa;
4162
4183
  for (let i = 0; i < kvCount; i++) {
4163
4184
  const key = r.str();
4164
4185
  const t = r.u32();
@@ -4169,9 +4190,20 @@ function parseGguf(path) {
4169
4190
  else if (key.endsWith(".context_length")) m.nativeCtx = readNumberOrMax(r, t);
4170
4191
  else if (key.endsWith(".block_count")) m.blockCount = readNumberOrMax(r, t);
4171
4192
  else if (key.endsWith(".embedding_length")) m.embedLen = readNumberOrMax(r, t);
4172
- else if (key.endsWith(".attention.head_count_kv")) m.headCountKv = readNumberOrMax(r, t);
4173
- else if (key.endsWith(".attention.key_length")) keyLength = readNumberOrMax(r, t);
4193
+ else if (key.endsWith(".attention.head_count_kv")) {
4194
+ const kv = readNumberList(r, t);
4195
+ m.headCountKv = maxOf(kv.values);
4196
+ if (kv.isArray) m.headCountKvPerLayer = kv.values;
4197
+ } else if (key.endsWith(".attention.key_length")) keyLength = readNumberOrMax(r, t);
4174
4198
  else if (key.endsWith(".attention.value_length")) valueLength = readNumberOrMax(r, t);
4199
+ else if (key.endsWith(".attention.key_length_swa")) keyLengthSwa = readNumberOrMax(r, t);
4200
+ else if (key.endsWith(".attention.value_length_swa")) valueLengthSwa = readNumberOrMax(r, t);
4201
+ else if (key.endsWith(".attention.sliding_window")) m.slidingWindow = readNumberOrMax(r, t);
4202
+ else if (key.endsWith(".attention.sliding_window_pattern")) m.slidingWindowPattern = readBoolArray(r, t);
4203
+ else if (key.endsWith(".full_attention_interval")) m.fullAttentionInterval = readNumberOrMax(r, t);
4204
+ else if (key.endsWith(".ssm.inner_size")) m.ssmInnerSize = readNumberOrMax(r, t);
4205
+ else if (key.endsWith(".ssm.state_size")) m.ssmStateSize = readNumberOrMax(r, t);
4206
+ else if (key.endsWith(".ssm.conv_kernel")) m.ssmConvKernel = readNumberOrMax(r, t);
4175
4207
  else if (key.endsWith(".expert_count")) m.expertCount = readNumberOrMax(r, t);
4176
4208
  else if (key.endsWith(".nextn_predict_layers")) m.nextnLayers = readNumberOrMax(r, t);
4177
4209
  else if (key === "tokenizer.chat_template") {
@@ -4193,7 +4225,15 @@ function parseGguf(path) {
4193
4225
  headDim: keyLength ?? valueLength ?? 0,
4194
4226
  expertCount: m.expertCount ?? 0,
4195
4227
  nextnLayers: m.nextnLayers ?? 0,
4196
- hasChatTemplate: m.hasChatTemplate ?? false
4228
+ hasChatTemplate: m.hasChatTemplate ?? false,
4229
+ slidingWindow: m.slidingWindow ?? 0,
4230
+ slidingWindowPattern: m.slidingWindowPattern ?? [],
4231
+ headDimSwa: keyLengthSwa ?? valueLengthSwa ?? 0,
4232
+ headCountKvPerLayer: m.headCountKvPerLayer ?? [],
4233
+ fullAttentionInterval: m.fullAttentionInterval ?? 0,
4234
+ ssmInnerSize: m.ssmInnerSize ?? 0,
4235
+ ssmStateSize: m.ssmStateSize ?? 0,
4236
+ ssmConvKernel: m.ssmConvKernel ?? 0
4197
4237
  };
4198
4238
  } finally {
4199
4239
  closeSync2(fd);
@@ -4205,7 +4245,20 @@ function quantFromName(filename) {
4205
4245
  }
4206
4246
 
4207
4247
  // src/models/scanner.ts
4208
- var CACHE_VERSION = 3;
4248
+ var CACHE_VERSION = 4;
4249
+ function attentionLayoutOf(meta) {
4250
+ const a = meta ?? {};
4251
+ const out = {};
4252
+ if (a.slidingWindow) out.slidingWindow = a.slidingWindow;
4253
+ if (a.slidingWindowPattern?.length) out.slidingWindowPattern = a.slidingWindowPattern;
4254
+ if (a.headDimSwa) out.headDimSwa = a.headDimSwa;
4255
+ if (a.headCountKvPerLayer?.length) out.headCountKvPerLayer = a.headCountKvPerLayer;
4256
+ if (a.fullAttentionInterval) out.fullAttentionInterval = a.fullAttentionInterval;
4257
+ if (a.ssmInnerSize) out.ssmInnerSize = a.ssmInnerSize;
4258
+ if (a.ssmStateSize) out.ssmStateSize = a.ssmStateSize;
4259
+ if (a.ssmConvKernel) out.ssmConvKernel = a.ssmConvKernel;
4260
+ return out;
4261
+ }
4209
4262
  var SPLIT_RE = /^(.*)-(\d{5})-of-(\d{5})\.gguf$/i;
4210
4263
  var EMBED_ARCHS = /* @__PURE__ */ new Set([
4211
4264
  "bert",
@@ -4392,6 +4445,9 @@ var Scanner = class {
4392
4445
  blockCount: meta?.blockCount ?? 0,
4393
4446
  headCountKv: meta?.headCountKv ?? 0,
4394
4447
  headDim: meta?.headDim ?? 0,
4448
+ // Only present on the handful of models that declare a non-uniform attention
4449
+ // layout; everything else keeps the shape (and the KV estimate) it had before.
4450
+ ...attentionLayoutOf(meta),
4395
4451
  moe: (meta?.expertCount ?? 0) > 0,
4396
4452
  expertCount: meta?.expertCount ?? 0,
4397
4453
  nextnLayers: meta?.nextnLayers ?? 0,
@@ -4689,6 +4745,52 @@ function kvBytesPerElem(t) {
4689
4745
  function defaultSampling() {
4690
4746
  return { temp: 0.8, topP: 0.95, topK: 40, minP: 0.05, repeatPenalty: 1, presencePenalty: 0, frequencyPenalty: 0, stop: [] };
4691
4747
  }
4748
+ function isFullAttentionLayer(i, interval) {
4749
+ return (i + 1) % interval === 0;
4750
+ }
4751
+ function hybridInterval(m, blocks) {
4752
+ const interval = m.fullAttentionInterval ?? 0;
4753
+ if (!Number.isInteger(interval) || interval <= 1 || interval > blocks) return 0;
4754
+ return interval;
4755
+ }
4756
+ function linearLayerCount(m) {
4757
+ const blocks = m.blockCount || 1;
4758
+ const interval = hybridInterval(m, blocks);
4759
+ if (interval === 0) return 0;
4760
+ let n = 0;
4761
+ for (let i = 0; i < blocks; i++) if (!isFullAttentionLayer(i, interval)) n++;
4762
+ return n;
4763
+ }
4764
+ function kvCacheElems(m, ctx) {
4765
+ const blocks = m.blockCount || 1;
4766
+ const kvHeads = m.headCountKv || 8;
4767
+ const headDim = m.headDim || HEAD_DIM;
4768
+ const perLayerHeads = m.headCountKvPerLayer?.length === blocks ? m.headCountKvPerLayer : void 0;
4769
+ const window = m.slidingWindow ?? 0;
4770
+ const swaPattern = window > 0 && m.slidingWindowPattern?.length === blocks ? m.slidingWindowPattern : void 0;
4771
+ const swaHeadDim = (m.headDimSwa ?? 0) > 0 ? m.headDimSwa : headDim;
4772
+ const interval = hybridInterval(m, blocks);
4773
+ const hybrid = interval > 0 && swaPattern === void 0;
4774
+ if (swaPattern === void 0 && !hybrid) return 2 * blocks * ctx * kvHeads * headDim;
4775
+ let elems = 0;
4776
+ for (let i = 0; i < blocks; i++) {
4777
+ if (hybrid && !isFullAttentionLayer(i, interval)) continue;
4778
+ const sliding = swaPattern?.[i] === true;
4779
+ const tokens = sliding ? Math.min(ctx, window) : ctx;
4780
+ const heads = perLayerHeads?.[i] || kvHeads;
4781
+ const dim = sliding ? swaHeadDim : headDim;
4782
+ elems += 2 * tokens * heads * dim;
4783
+ }
4784
+ return elems;
4785
+ }
4786
+ function ssmStateElems(m) {
4787
+ const layers = linearLayerCount(m);
4788
+ const inner = m.ssmInnerSize ?? 0;
4789
+ const state = m.ssmStateSize ?? 0;
4790
+ if (layers === 0 || inner <= 0 || state <= 0) return 0;
4791
+ const conv = Math.max(1, m.ssmConvKernel ?? 1);
4792
+ return layers * inner * (state + conv - 1);
4793
+ }
4692
4794
  function gpuBudgetMb(sys, p) {
4693
4795
  if (sys.gpus.length === 0) return 0;
4694
4796
  if (p?.gpu?.splitMode === "none") {
@@ -4704,10 +4806,9 @@ function estimateVram(p, m, sys) {
4704
4806
  const blocks = m.blockCount || 1;
4705
4807
  const gpuFrac = m.moe ? 1 - 0.85 * (p.nCpuMoe / blocks) : Math.min(p.ngl, blocks) / blocks;
4706
4808
  const weightsMb = sizeMb * Math.max(0, Math.min(1, gpuFrac));
4707
- const kvHeads = m.headCountKv || 8;
4708
- const headDim = m.headDim || HEAD_DIM;
4709
- const kvElems = 2 * blocks * p.ctx * kvHeads * headDim;
4710
- const kvMb = p.kvOffload === false ? 0 : kvElems * kvBytesPerElem(p.kvTypeK) / 1e6 * (p.kvUnified ? 1 : Math.max(1, p.parallel));
4809
+ const kvElems = kvCacheElems(m, p.ctx);
4810
+ const ssmMb = ssmStateElems(m) * 4 / 1e6;
4811
+ const kvMb = p.kvOffload === false ? 0 : (kvElems * kvBytesPerElem(p.kvTypeK) / 1e6 + ssmMb) * (p.kvUnified ? 1 : Math.max(1, p.parallel));
4711
4812
  const mmprojMb = p.useMmproj && p.mmprojGpu && m.mmprojPath ? m.mmprojSizeBytes / 1e6 * 1.15 : 0;
4712
4813
  const estMb = Math.round(weightsMb + kvMb + 800 + mmprojMb);
4713
4814
  const pct = estMb / totalVramMb;
@@ -8291,6 +8392,18 @@ async function checkSsrf(url) {
8291
8392
 
8292
8393
  // src/tools/search-providers.ts
8293
8394
  var SEARCH_TIMEOUT_MS = 2e4;
8395
+ var FUTURE_SKEW_MS = 2 * 24 * 60 * 60 * 1e3;
8396
+ function normalizePublishedDate(raw) {
8397
+ if (typeof raw !== "string") return void 0;
8398
+ const trimmed = raw.trim();
8399
+ if (!trimmed) return void 0;
8400
+ const ms = Date.parse(trimmed);
8401
+ if (!Number.isFinite(ms)) return void 0;
8402
+ if (ms > Date.now() + FUTURE_SKEW_MS) return void 0;
8403
+ const iso = new Date(ms).toISOString();
8404
+ if (Number(iso.slice(0, 4)) < 1990) return void 0;
8405
+ return iso.slice(0, 10);
8406
+ }
8294
8407
  function searchConfigured(cfg2) {
8295
8408
  if (!cfg2) return false;
8296
8409
  switch (cfg2.provider) {
@@ -8325,7 +8438,11 @@ var TavilyClient = class {
8325
8438
  key;
8326
8439
  fetchImpl;
8327
8440
  provider = "tavily";
8328
- async search(query, maxResults) {
8441
+ // `time_range` is honoured on the default (general) topic and meaningfully shifts the
8442
+ // mix toward recent pages. It does NOT make Tavily return `published_date` — only
8443
+ // `topic:'news'` does that, and that collapses results to news articles, so we don't
8444
+ // send it. Tavily therefore filters by recency but rarely supplies a date here.
8445
+ async search(query, maxResults, opts) {
8329
8446
  const resp = await this.fetchImpl("https://api.tavily.com/search", {
8330
8447
  method: "POST",
8331
8448
  headers: { "Content-Type": "application/json" },
@@ -8333,18 +8450,23 @@ var TavilyClient = class {
8333
8450
  api_key: this.key,
8334
8451
  query,
8335
8452
  max_results: maxResults,
8336
- search_depth: "advanced"
8453
+ search_depth: "advanced",
8454
+ ...opts?.recent ? { time_range: "month" } : {}
8337
8455
  }),
8338
8456
  signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
8339
8457
  });
8340
8458
  if (!resp.ok) throw new Error(await httpError("Tavily", resp));
8341
8459
  const data = await resp.json();
8342
- return (data.results ?? []).map((r) => ({
8343
- title: r.title ?? "",
8344
- url: r.url ?? "",
8345
- content: r.content ?? "",
8346
- score: r.score
8347
- }));
8460
+ return (data.results ?? []).map((r) => {
8461
+ const publishedDate = normalizePublishedDate(r.published_date);
8462
+ return {
8463
+ title: r.title ?? "",
8464
+ url: r.url ?? "",
8465
+ content: r.content ?? "",
8466
+ score: r.score,
8467
+ ...publishedDate ? { publishedDate } : {}
8468
+ };
8469
+ });
8348
8470
  }
8349
8471
  };
8350
8472
  var KagiClient = class {
@@ -8355,6 +8477,10 @@ var KagiClient = class {
8355
8477
  key;
8356
8478
  fetchImpl;
8357
8479
  provider = "kagi";
8480
+ // No `opts` arg: the v0 search endpoint this adapter targets documents only `q` and
8481
+ // `limit` — it has no recency parameter, so `recent` cannot be honoured here.
8482
+ // (Kagi's newer POST /search endpoint does support recency via lens/filters, but it
8483
+ // has a different request and response shape and would need a full adapter rewrite.)
8358
8484
  async search(query, maxResults) {
8359
8485
  const url = `https://kagi.com/api/v0/search?q=${encodeURIComponent(query)}&limit=${maxResults}`;
8360
8486
  const resp = await this.fetchImpl(url, {
@@ -8364,11 +8490,15 @@ var KagiClient = class {
8364
8490
  });
8365
8491
  if (!resp.ok) throw new Error(await httpError("Kagi", resp));
8366
8492
  const data = await resp.json();
8367
- return (data.data ?? []).filter((r) => r.t === 0 && r.url).map((r) => ({
8368
- title: r.title ?? "",
8369
- url: r.url ?? "",
8370
- content: r.snippet ?? ""
8371
- }));
8493
+ return (data.data ?? []).filter((r) => r.t === 0 && r.url).map((r) => {
8494
+ const publishedDate = normalizePublishedDate(r.published);
8495
+ return {
8496
+ title: r.title ?? "",
8497
+ url: r.url ?? "",
8498
+ content: r.snippet ?? "",
8499
+ ...publishedDate ? { publishedDate } : {}
8500
+ };
8501
+ });
8372
8502
  }
8373
8503
  };
8374
8504
  var SearxngClient = class {
@@ -8379,21 +8509,26 @@ var SearxngClient = class {
8379
8509
  baseUrl;
8380
8510
  fetchImpl;
8381
8511
  provider = "searxng";
8382
- async search(query, maxResults) {
8512
+ async search(query, maxResults, opts) {
8383
8513
  const base2 = this.baseUrl.replace(/\/+$/, "");
8384
- const url = `${base2}/search?q=${encodeURIComponent(query)}&format=json&categories=general`;
8514
+ let url = `${base2}/search?q=${encodeURIComponent(query)}&format=json&categories=general`;
8515
+ if (opts?.recent) url += "&time_range=month";
8385
8516
  const resp = await this.fetchImpl(url, {
8386
8517
  method: "GET",
8387
8518
  signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
8388
8519
  });
8389
8520
  if (!resp.ok) throw new Error(await httpError("SearXNG", resp));
8390
8521
  const data = await resp.json();
8391
- return (data.results ?? []).slice(0, maxResults).map((r) => ({
8392
- title: r.title ?? "",
8393
- url: r.url ?? "",
8394
- content: r.content ?? "",
8395
- score: r.score
8396
- }));
8522
+ return (data.results ?? []).slice(0, maxResults).map((r) => {
8523
+ const publishedDate = normalizePublishedDate(r.publishedDate);
8524
+ return {
8525
+ title: r.title ?? "",
8526
+ url: r.url ?? "",
8527
+ content: r.content ?? "",
8528
+ score: r.score,
8529
+ ...publishedDate ? { publishedDate } : {}
8530
+ };
8531
+ });
8397
8532
  }
8398
8533
  };
8399
8534
  async function httpError(name, resp) {
@@ -8403,8 +8538,10 @@ async function httpError(name, resp) {
8403
8538
 
8404
8539
  // src/tools/research-service.ts
8405
8540
  var MIN_RELEVANCE_SCORE = 0.4;
8406
- var MAX_RESULTS = 5;
8541
+ var MAX_RESULTS = 8;
8407
8542
  var MAX_SEARCH_RESULTS = 10;
8543
+ var FRESH_MAX_AGE_DAYS = 90;
8544
+ var DATED_MIN_AGE_DAYS = 3 * 365;
8408
8545
  var TRUSTED_DOMAINS = [
8409
8546
  [/\.(edu|ac\.[a-z]{2})$/, 0.9],
8410
8547
  [/\.gov(\.[a-z]{2})?$/, 0.9],
@@ -8511,58 +8648,385 @@ function keywordOverlap(query, text) {
8511
8648
  const bigramScore = bigramTotal > 0 ? bigramHits / bigramTotal : 0;
8512
8649
  return Math.min(1, unigramScore * 0.7 + bigramScore * 0.3);
8513
8650
  }
8651
+ function clamp(n, lo, hi) {
8652
+ return Math.min(hi, Math.max(lo, n));
8653
+ }
8654
+ var GENERIC_URL_LABELS = /* @__PURE__ */ new Set([
8655
+ "best",
8656
+ "top",
8657
+ "guide",
8658
+ "guides",
8659
+ "review",
8660
+ "reviews",
8661
+ "news",
8662
+ "blog",
8663
+ "blogs",
8664
+ "shop",
8665
+ "store",
8666
+ "buy",
8667
+ "price",
8668
+ "prices",
8669
+ "deal",
8670
+ "deals",
8671
+ "compare",
8672
+ "ranking",
8673
+ "rankings",
8674
+ "list",
8675
+ "lists",
8676
+ "info",
8677
+ "online",
8678
+ "free",
8679
+ "home",
8680
+ "site",
8681
+ "daily",
8682
+ "today",
8683
+ "live",
8684
+ "tips",
8685
+ "world",
8686
+ "expert",
8687
+ "experts",
8688
+ "hub",
8689
+ "zone",
8690
+ "central",
8691
+ "article",
8692
+ "articles",
8693
+ "post",
8694
+ "posts",
8695
+ "page",
8696
+ "pages",
8697
+ "category",
8698
+ "tag",
8699
+ "tags",
8700
+ "search",
8701
+ "wiki",
8702
+ "forum",
8703
+ "forums",
8704
+ "topic",
8705
+ "topics"
8706
+ ]);
8707
+ var SUFFIX_LABELS = /* @__PURE__ */ new Set(["www", "co", "com", "org", "net", "ac", "gov", "edu"]);
8708
+ function entityStem(s) {
8709
+ return s.replace(/\d+$/, "");
8710
+ }
8711
+ function entityTerms(query) {
8712
+ const terms = new Set(
8713
+ tokenize(query).filter((t) => t.length >= 4 && !GENERIC_URL_LABELS.has(t))
8714
+ );
8715
+ if (query !== query.toUpperCase()) {
8716
+ for (const m of query.match(/\b[A-Z]{3,6}\b/g) ?? []) {
8717
+ const lower = m.toLowerCase();
8718
+ if (!GENERIC_URL_LABELS.has(lower)) terms.add(lower);
8719
+ }
8720
+ }
8721
+ return terms;
8722
+ }
8723
+ function officialSourceBoost(query, url) {
8724
+ let host2;
8725
+ let segments;
8726
+ try {
8727
+ const u = new URL(url.startsWith("http") ? url : `https://${url}`);
8728
+ host2 = u.hostname.toLowerCase();
8729
+ segments = u.pathname.split("/").filter(Boolean).map((s) => s.toLowerCase().replace(/[^a-z0-9]/g, ""));
8730
+ } catch {
8731
+ return 0;
8732
+ }
8733
+ const terms = entityTerms(query);
8734
+ if (terms.size === 0) return 0;
8735
+ const matches = (candidate) => {
8736
+ if (candidate.length < 3 || GENERIC_URL_LABELS.has(candidate)) return false;
8737
+ const stem = entityStem(candidate);
8738
+ for (const t of terms) {
8739
+ if (t === candidate) return true;
8740
+ if (stem.length >= 4 && entityStem(t) === stem) return true;
8741
+ }
8742
+ return false;
8743
+ };
8744
+ const isConcatenatedName = (candidate) => {
8745
+ if (candidate.length < 8) return false;
8746
+ let hits = 0;
8747
+ let covered = 0;
8748
+ for (const t of terms) {
8749
+ if (t.length >= 4 && candidate.includes(t)) {
8750
+ hits++;
8751
+ covered += t.length;
8752
+ }
8753
+ }
8754
+ return hits >= 2 && covered >= candidate.length * 0.6;
8755
+ };
8756
+ const labels = host2.split(".");
8757
+ for (let i = 0; i < labels.length - 1; i++) {
8758
+ if (SUFFIX_LABELS.has(labels[i])) continue;
8759
+ if (matches(labels[i])) return 0.25;
8760
+ if (isConcatenatedName(labels[i])) return 0.22;
8761
+ }
8762
+ if (segments.length !== 1) return 0;
8763
+ if (labels.some((l) => GENERIC_URL_LABELS.has(l))) return 0;
8764
+ return matches(segments[0]) ? 0.15 : 0;
8765
+ }
8766
+ function officialDocsBoost(url) {
8767
+ try {
8768
+ const host2 = new URL(url.startsWith("http") ? url : `https://${url}`).hostname.toLowerCase();
8769
+ return /^(docs?|developers?|support|help)\./.test(host2) ? 0.1 : 0;
8770
+ } catch {
8771
+ return 0;
8772
+ }
8773
+ }
8774
+ var AGGREGATOR_BAIT = [
8775
+ /\bbest\b/,
8776
+ /\btop\s+\d+\b/,
8777
+ /\b\d+\s+(best|top|ways|reasons|tips|things|ideas|alternatives)\b/,
8778
+ /\bultimate\s+guide\b/,
8779
+ /\branked\b/,
8780
+ /\breviews?\b|\breviewed\b/,
8781
+ /\bround\s?up\b/,
8782
+ /\b(vs|versus)\b/
8783
+ ];
8784
+ var AGGREGATOR_PATTERNS = [
8785
+ ...AGGREGATOR_BAIT,
8786
+ /\bwhich\b[\s\S]*\bshould\s+you\b/
8787
+ ];
8788
+ var AGGREGATOR_EXEMPTIONS = [
8789
+ /\bbest\s+practices?\b/,
8790
+ /\b(systematic|literature|scoping|narrative|meta)\b/,
8791
+ /\bpeer\s+reviewed\b/
8792
+ ];
8793
+ function aggregatorPenalty(title, url, intent) {
8794
+ if (intent === "comparison") return 0;
8795
+ let slug = "";
8796
+ try {
8797
+ slug = new URL(url.startsWith("http") ? url : `https://${url}`).pathname;
8798
+ } catch {
8799
+ slug = "";
8800
+ }
8801
+ const text = `${title} ${slug.replace(/[^a-zA-Z0-9]+/g, " ")}`.toLowerCase();
8802
+ if (AGGREGATOR_EXEMPTIONS.some((re) => re.test(text))) return 0;
8803
+ if (!AGGREGATOR_PATTERNS.some((re) => re.test(text))) return 0;
8804
+ return /\b(19|20)\d{2}\b/.test(text) ? 0.2 : 0.15;
8805
+ }
8806
+ function substantiveTermCount(text) {
8807
+ return text.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((t) => t.length >= 2 && !STOP_WORDS.has(t)).length;
8808
+ }
8809
+ var QUERY_ONLY_BAIT = [
8810
+ /\bwhich\b/gi,
8811
+ /\bshould\s+(you|i|we)\b/gi,
8812
+ /\b(19|20)\d{2}\b/g
8813
+ ];
8814
+ function debaitQuery(query) {
8815
+ if (typeof query !== "string" || !query.trim()) return null;
8816
+ const lower = query.toLowerCase();
8817
+ if (AGGREGATOR_EXEMPTIONS.some((re) => re.test(lower))) return null;
8818
+ if (!AGGREGATOR_PATTERNS.some((re) => re.test(lower))) return null;
8819
+ let out = query;
8820
+ for (const re of AGGREGATOR_BAIT) {
8821
+ out = out.replace(new RegExp(re.source, "gi"), " ");
8822
+ }
8823
+ for (const re of QUERY_ONLY_BAIT) out = out.replace(re, " ");
8824
+ out = out.replace(/\s+/g, " ").trim().replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "").trim();
8825
+ if (!out || out.toLowerCase() === query.trim().toLowerCase()) return null;
8826
+ if (substantiveTermCount(out) < 2) return null;
8827
+ return out;
8828
+ }
8829
+ function sourceQuality(query, title, url, intent) {
8830
+ let base2;
8831
+ try {
8832
+ base2 = domainScore(url);
8833
+ } catch {
8834
+ base2 = 0.5;
8835
+ }
8836
+ return clamp(
8837
+ base2 + officialSourceBoost(query, url) + officialDocsBoost(url) - aggregatorPenalty(title, url, intent),
8838
+ 0.1,
8839
+ 1
8840
+ );
8841
+ }
8842
+ function passagePriority(sentence) {
8843
+ const s = sentence.trim();
8844
+ let rank = 0;
8845
+ if (/^#{1,6}\s/.test(s)) rank -= 2;
8846
+ if (/\b(19|20)\d{2}\b/.test(s)) rank += 1;
8847
+ return rank;
8848
+ }
8849
+ var MAX_PASSAGE_CHARS = 1200;
8850
+ var REFERENTIAL_OPENER = /^(in addition|as a result|for example|for instance|it|this|that|these|those|they|he|she|its|their|his|her|there|such|however|but|and|so|then|also|additionally|moreover|furthermore|instead|meanwhile|therefore|thus|yet|still)\b/i;
8851
+ function needsPrecedingContext(sentence) {
8852
+ const s = sentence.trim();
8853
+ return s.length < 120 || REFERENTIAL_OPENER.test(s) || /^[a-z]/.test(s);
8854
+ }
8514
8855
  function extractPassage(query, content) {
8515
8856
  if (!content) return "";
8516
8857
  const sentences = content.split(/(?<=[.?!])\s+/).filter((s) => s.trim().length > 0);
8517
8858
  if (sentences.length <= 1) {
8518
- return content.slice(0, 200);
8859
+ return content.slice(0, MAX_PASSAGE_CHARS);
8519
8860
  }
8520
- let bestSentence = sentences[0];
8861
+ let bestIdx = 0;
8521
8862
  let bestScore = keywordOverlap(query, sentences[0]);
8522
8863
  for (let i = 1; i < sentences.length; i++) {
8523
8864
  const s = keywordOverlap(query, sentences[i]);
8524
- if (s > bestScore) {
8865
+ if (s > bestScore || s === bestScore && passagePriority(sentences[i]) > passagePriority(sentences[bestIdx])) {
8525
8866
  bestScore = s;
8526
- bestSentence = sentences[i];
8527
- }
8867
+ bestIdx = i;
8868
+ }
8869
+ }
8870
+ const best = sentences[bestIdx].trim();
8871
+ if (best.length >= MAX_PASSAGE_CHARS) return best.slice(0, MAX_PASSAGE_CHARS);
8872
+ let start = bestIdx;
8873
+ let end = bestIdx;
8874
+ let total = best.length;
8875
+ const fits = (s) => total + s.length + 1 <= MAX_PASSAGE_CHARS;
8876
+ if (bestIdx > 0 && needsPrecedingContext(best)) {
8877
+ const prev = sentences[bestIdx - 1].trim();
8878
+ if (fits(prev)) {
8879
+ start = bestIdx - 1;
8880
+ total += prev.length + 1;
8881
+ }
8882
+ }
8883
+ while (end + 1 < sentences.length) {
8884
+ const next = sentences[end + 1].trim();
8885
+ if (!fits(next)) break;
8886
+ end++;
8887
+ total += next.length + 1;
8888
+ }
8889
+ while (start > 0) {
8890
+ const prev = sentences[start - 1].trim();
8891
+ if (!fits(prev)) break;
8892
+ start--;
8893
+ total += prev.length + 1;
8894
+ }
8895
+ return sentences.slice(start, end + 1).map((s) => s.trim()).join(" ");
8896
+ }
8897
+ function normalizePublishedDate2(raw) {
8898
+ if (typeof raw !== "string") return void 0;
8899
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw.trim());
8900
+ if (!m) return void 0;
8901
+ const [, y, mo, d] = m;
8902
+ const ms = Date.UTC(Number(y), Number(mo) - 1, Number(d));
8903
+ const dt = new Date(ms);
8904
+ if (dt.getUTCFullYear() !== Number(y) || dt.getUTCMonth() !== Number(mo) - 1 || dt.getUTCDate() !== Number(d)) {
8905
+ return void 0;
8528
8906
  }
8529
- return bestSentence.trim();
8907
+ return `${y}-${mo}-${d}`;
8530
8908
  }
8531
- function freshnessSignal(content, freshness) {
8532
- if (!freshness || freshness === "any") return "unknown";
8533
- const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
8909
+ function ageInDays(isoDate, now = Date.now()) {
8910
+ const normalized = normalizePublishedDate2(isoDate);
8911
+ if (!normalized) return void 0;
8912
+ const published = Date.parse(`${normalized}T00:00:00Z`);
8913
+ if (Number.isNaN(published)) return void 0;
8914
+ return Math.max(0, Math.floor((now - published) / 864e5));
8915
+ }
8916
+ function yearMentionHint(content) {
8534
8917
  const yearMatches = content.match(/\b(20\d{2})\b/g);
8535
8918
  if (!yearMatches) return "unknown";
8536
- const years = yearMatches.map(Number);
8537
- const maxYear = Math.max(...years);
8919
+ const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
8920
+ const maxYear = Math.max(...yearMatches.map(Number));
8538
8921
  if (maxYear >= currentYear - 1) return "recent";
8539
8922
  if (maxYear <= currentYear - 3) return "dated";
8540
8923
  return "unknown";
8541
8924
  }
8542
- function freshnessScore(signal, freshness) {
8543
- if (!freshness || freshness === "any") return 0.5;
8544
- if (signal === "recent") return 0.9;
8545
- if (signal === "dated") return 0.2;
8925
+ function currentnessMultiplier(ageDays) {
8926
+ if (ageDays <= FRESH_MAX_AGE_DAYS) return 1;
8927
+ if (ageDays <= 365) return 0.85;
8928
+ if (ageDays <= DATED_MIN_AGE_DAYS) return 0.65;
8929
+ return 0.45;
8930
+ }
8931
+ function freshnessSignal(ageDays) {
8932
+ if (ageDays === void 0) return "unknown";
8933
+ if (ageDays <= FRESH_MAX_AGE_DAYS) return "recent";
8934
+ if (ageDays >= DATED_MIN_AGE_DAYS) return "dated";
8935
+ return "unknown";
8936
+ }
8937
+ function freshnessScore(ageDays, content, freshness) {
8938
+ if (freshness !== "current") return 0.5;
8939
+ if (ageDays !== void 0) {
8940
+ if (ageDays <= FRESH_MAX_AGE_DAYS) return 1;
8941
+ if (ageDays <= 180) return 0.6;
8942
+ if (ageDays <= 365) return 0.4;
8943
+ if (ageDays < DATED_MIN_AGE_DAYS) return 0.2;
8944
+ return 0.1;
8945
+ }
8946
+ const hint = yearMentionHint(content);
8947
+ if (hint === "recent") return 0.6;
8948
+ if (hint === "dated") return 0.45;
8546
8949
  return 0.5;
8547
8950
  }
8951
+ var TRACKING_PARAMS = /^(utm_[a-z0-9_]+|fbclid|gclid|dclid|msclkid|mc_cid|mc_eid|igshid|ref|referrer|source)$/i;
8952
+ function normalizeUrlIdentity(url) {
8953
+ const raw = String(url ?? "").trim();
8954
+ try {
8955
+ const u = new URL(raw.startsWith("http") ? raw : `https://${raw}`);
8956
+ for (const key of [...u.searchParams.keys()]) {
8957
+ if (TRACKING_PARAMS.test(key)) u.searchParams.delete(key);
8958
+ }
8959
+ u.searchParams.sort();
8960
+ const host2 = u.hostname.toLowerCase().replace(/^www\./, "");
8961
+ const path = u.pathname.replace(/\/+$/, "");
8962
+ const qs = u.searchParams.toString();
8963
+ return `${host2}${path}${qs ? `?${qs}` : ""}`;
8964
+ } catch {
8965
+ return raw.toLowerCase();
8966
+ }
8967
+ }
8968
+ function normalizeTitleIdentity(title) {
8969
+ return String(title ?? "").trim().replace(/\s+/g, " ").toLowerCase();
8970
+ }
8971
+ function isBetterDuplicate(candidate, incumbent) {
8972
+ const candidateDated = !!candidate.publishedDate;
8973
+ const incumbentDated = !!incumbent.publishedDate;
8974
+ if (candidateDated !== incumbentDated) return candidateDated;
8975
+ return (candidate.content?.length ?? 0) > (incumbent.content?.length ?? 0);
8976
+ }
8977
+ function collapseBy(results, keyOf) {
8978
+ const seenAt = /* @__PURE__ */ new Map();
8979
+ const out = [];
8980
+ for (const r of results) {
8981
+ const key = keyOf(r);
8982
+ if (key === null) {
8983
+ out.push(r);
8984
+ continue;
8985
+ }
8986
+ const at = seenAt.get(key);
8987
+ if (at === void 0) {
8988
+ seenAt.set(key, out.length);
8989
+ out.push(r);
8990
+ } else if (isBetterDuplicate(r, out[at])) {
8991
+ out[at] = r;
8992
+ }
8993
+ }
8994
+ return out;
8995
+ }
8996
+ function dedupeSearchResults(results) {
8997
+ const byUrl = collapseBy(results, (r) => r?.url ? normalizeUrlIdentity(r.url) : null);
8998
+ return collapseBy(byUrl, (r) => {
8999
+ const title = normalizeTitleIdentity(r?.title ?? "");
9000
+ if (!title) return null;
9001
+ return `${extractDomain(r.url ?? "")}\0${title}`;
9002
+ });
9003
+ }
8548
9004
  async function research(q, cfg2, fetchImpl) {
8549
9005
  if (!searchConfigured(cfg2)) return [];
8550
9006
  const client = searchProviderClient(cfg2, fetchImpl);
8551
9007
  if (!client) return [];
8552
- let raw;
8553
- try {
8554
- raw = await client.search(q.query, MAX_SEARCH_RESULTS);
8555
- } catch {
8556
- return [];
8557
- }
9008
+ const opts = q.freshness === "current" ? { recent: true } : void 0;
9009
+ const debaited = debaitQuery(q.query);
9010
+ const primaryCall = client.search(q.query, MAX_SEARCH_RESULTS, opts).then(
9011
+ (r) => r,
9012
+ () => null
9013
+ // null distinguishes "the search failed" from "the search found nothing".
9014
+ );
9015
+ const extraCall = debaited ? client.search(debaited, MAX_SEARCH_RESULTS, opts).catch(() => []) : Promise.resolve([]);
9016
+ const [primaryRaw, extraRaw] = await Promise.all([primaryCall, extraCall]);
9017
+ if (primaryRaw === null) return [];
9018
+ const raw = dedupeSearchResults([...primaryRaw, ...extraRaw]);
9019
+ const now = Date.now();
8558
9020
  const scored = raw.map((r) => {
8559
9021
  const domain = extractDomain(r.url);
8560
9022
  const text = `${r.title} ${r.content}`;
8561
9023
  const kwScore = keywordOverlap(q.query, text);
8562
- const ds = domainScore(r.url);
8563
- const signal = freshnessSignal(r.content, q.freshness);
8564
- const fs2 = freshnessScore(signal, q.freshness);
8565
- const relevanceScore = 0.5 * kwScore + 0.3 * ds + 0.2 * fs2;
9024
+ const sq = sourceQuality(q.query, r.title, r.url, q.intent);
9025
+ const publishedDate = normalizePublishedDate2(r.publishedDate);
9026
+ const ageDays = publishedDate ? ageInDays(publishedDate, now) : void 0;
9027
+ const signal = freshnessSignal(ageDays);
9028
+ const fs2 = freshnessScore(ageDays, r.content, q.freshness);
9029
+ const relevanceScore = 0.35 * kwScore + 0.45 * sq + 0.2 * fs2;
8566
9030
  const passage = extractPassage(q.query, r.content);
8567
9031
  return {
8568
9032
  url: r.url,
@@ -8570,40 +9034,45 @@ async function research(q, cfg2, fetchImpl) {
8570
9034
  passage,
8571
9035
  relevanceScore: Math.round(relevanceScore * 1e3) / 1e3,
8572
9036
  freshnessSignal: signal,
8573
- domain
9037
+ domain,
9038
+ publishedDate,
9039
+ ageDays
8574
9040
  };
8575
9041
  });
8576
9042
  return scored.filter((r) => r.relevanceScore >= MIN_RELEVANCE_SCORE).sort((a, b) => b.relevanceScore - a.relevanceScore).slice(0, MAX_RESULTS);
8577
9043
  }
8578
9044
 
8579
9045
  // src/tools/builtin.ts
8580
- var WEB_SEARCH_TOOL = {
8581
- type: "function",
8582
- function: {
8583
- name: "web_search",
8584
- description: "Search the web for real-time information. Call this BEFORE answering any question that depends on current events, recent data, prices, specific facts, or anything your training data may not cover accurately. Formulate a precise, keyword-focused query \u2014 include names, dates, or version numbers when relevant. Run multiple focused searches rather than one broad one for complex questions. Results are pre-ranked by relevance \u2014 each entry includes a relevanceScore (0\u20131), a key passage, and a source URL.",
8585
- parameters: {
8586
- type: "object",
8587
- properties: {
8588
- query: {
8589
- type: "string",
8590
- description: 'A precise, specific search query. Use key terms and identifiers. Good: "Python 3.13 release date new features". Bad: "Python new stuff". Good: "NVIDIA RTX 5090 benchmark 2025". Bad: "new GPU benchmarks".'
8591
- },
8592
- intent: {
8593
- type: "string",
8594
- enum: ["factual", "recent_news", "comparison", "how_to"],
8595
- description: "Optional: the type of answer needed. Helps the retrieval service weight results appropriately."
9046
+ function webSearchTool(today = todayIso()) {
9047
+ const year = today.slice(0, 4);
9048
+ return {
9049
+ type: "function",
9050
+ function: {
9051
+ name: "web_search",
9052
+ description: `Search the web for real-time information. TODAY IS ${today}. Call this BEFORE answering any question that depends on current events, recent data, prices, specific facts, or anything your training data may not cover accurately. DISCOVERY BEFORE RECALL: when you need what is best/current/latest in some category, your FIRST search must NOT name a specific product, model, version, brand or person you are recalling from memory \u2014 what you remember may have been replaced, and a search only returns what you ask about. Search the category itself plus the current year (e.g. "<category> ${year}", "latest <category> released"), read which names actually exist now, and only THEN search those names. Run multiple focused searches rather than one broad one for complex questions. Returns up to 8 pre-ranked results. Each entry carries a substantial excerpt from the page \u2014 not a one-line snippet \u2014 plus a relevanceScore (0\u20131), a source URL, and the publication date when the source reports one. Judge sources by authority, not only by how closely their wording echoes the question: prefer primary and official sources \u2014 the organisation's own site, official documentation, the original announcement, report, filing, or paper \u2014 over roundups, listicles and "best of" articles, which are often re-titled stale content dressed up as current. When an excerpt reads as authoritative but is too thin to answer from, call fetch_url on that result to read the full page rather than filling the gap from memory.`,
9053
+ parameters: {
9054
+ type: "object",
9055
+ properties: {
9056
+ query: {
9057
+ type: "string",
9058
+ description: `A precise, specific search query. Use key terms and identifiers. When recency matters use ${year} \u2014 never a year you recall from training, which is how a search silently returns correct answers about superseded things. Good: "Python 3.13 release date new features". Bad: "Python new stuff". Good: "NVIDIA RTX 5090 benchmark ${year}". Bad: "new GPU benchmarks".`
9059
+ },
9060
+ intent: {
9061
+ type: "string",
9062
+ enum: ["factual", "recent_news", "comparison", "how_to"],
9063
+ description: "Optional: the type of answer needed. Helps the retrieval service weight results appropriately."
9064
+ },
9065
+ freshness: {
9066
+ type: "string",
9067
+ enum: ["current", "any"],
9068
+ description: 'Optional: "current" asks the search provider for recent results and penalises sources published more than 90 days ago. Pass "current" whenever the answer can go stale: questions using latest/newest/current/right now/this year, software releases and version numbers, prices, benchmarks, rankings, ongoing events, and news. Omit it (or pass "any") for timeless background facts such as definitions, history, or how something works.'
9069
+ }
8596
9070
  },
8597
- freshness: {
8598
- type: "string",
8599
- enum: ["current", "any"],
8600
- description: 'Optional: "current" penalises results older than 90 days. Use for breaking news or time-sensitive queries.'
8601
- }
8602
- },
8603
- required: ["query"]
9071
+ required: ["query"]
9072
+ }
8604
9073
  }
8605
- }
8606
- };
9074
+ };
9075
+ }
8607
9076
  var FETCH_URL_TOOL = {
8608
9077
  type: "function",
8609
9078
  function: {
@@ -8632,11 +9101,92 @@ var RUN_CODE_TOOL = {
8632
9101
  }
8633
9102
  }
8634
9103
  };
9104
+ function todayIso() {
9105
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9106
+ }
9107
+ function normalizeIsoDate(raw) {
9108
+ if (!raw) return void 0;
9109
+ const match = /\d{4}-\d{2}-\d{2}/.exec(raw);
9110
+ if (!match) return void 0;
9111
+ const iso = match[0];
9112
+ const ms = Date.parse(`${iso}T00:00:00Z`);
9113
+ if (Number.isNaN(ms)) return void 0;
9114
+ if (new Date(ms).toISOString().slice(0, 10) !== iso) return void 0;
9115
+ const year = Number(iso.slice(0, 4));
9116
+ if (year < 1990 || year > (/* @__PURE__ */ new Date()).getUTCFullYear() + 1) return void 0;
9117
+ return iso;
9118
+ }
9119
+ function oneLine(s) {
9120
+ return s.replace(/[\u200b\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "").replace(/[\s\u0085]+/g, " ").trim();
9121
+ }
9122
+ function publishedLabel(r) {
9123
+ const iso = normalizeIsoDate(r.publishedDate);
9124
+ if (!iso) return "unknown";
9125
+ const age = r.ageDays;
9126
+ if (typeof age !== "number" || !Number.isFinite(age) || age < 0) return iso;
9127
+ if (age === 0) return `${iso} (today)`;
9128
+ return `${iso} (${age === 1 ? "1 day" : `${age} days`} ago)`;
9129
+ }
8635
9130
  function resolveSearchQuery(args) {
8636
9131
  if (typeof args.query === "string") return args.query;
8637
9132
  if (Array.isArray(args.queries) && typeof args.queries[0] === "string") return args.queries[0];
8638
9133
  return "";
8639
9134
  }
9135
+ var DATE_PROBE_MAX_BYTES = 96e3;
9136
+ var DATE_PROBE_TIMEOUT_MS = 4e3;
9137
+ function sourceAdvisory(query, results) {
9138
+ if (results.length === 0) return null;
9139
+ const roundups = results.filter((r) => aggregatorPenalty(r.title, r.url) > 0).length;
9140
+ const primaries = results.filter(
9141
+ (r) => officialSourceBoost(query, r.url) > 0 || officialDocsBoost(r.url) > 0
9142
+ ).length;
9143
+ if (primaries > 0 || roundups < Math.ceil(results.length / 2)) return null;
9144
+ return `CAUTION: ${roundups} of these ${results.length} results are roundup/"best of" pages and none is a primary or official source. Pages like these are frequently re-published with a new date and stale contents, so they establish which names EXIST, not which is current or correct. Treat every name below as a CANDIDATE, not an answer: before recommending one, search that name against its own official source (the maker/organisation/authority behind it) and confirm the specifics there. If you cannot confirm a candidate that way, say so rather than repeating the roundup.`;
9145
+ }
9146
+ async function backfillPublishedDates(results) {
9147
+ const missing = results.filter((r) => !r.publishedDate);
9148
+ if (missing.length === 0) return;
9149
+ await Promise.allSettled(
9150
+ missing.map(async (r) => {
9151
+ if (await checkSsrf(r.url)) return;
9152
+ let resp;
9153
+ try {
9154
+ resp = await fetch(r.url, {
9155
+ headers: { "User-Agent": "TurboLLM/0.7 (tool-fetch)" },
9156
+ signal: AbortSignal.timeout(DATE_PROBE_TIMEOUT_MS)
9157
+ });
9158
+ } catch {
9159
+ return;
9160
+ }
9161
+ if (!resp.ok || !resp.body) return;
9162
+ let html = "";
9163
+ try {
9164
+ const reader = resp.body.getReader();
9165
+ const decoder = new TextDecoder();
9166
+ let read = 0;
9167
+ while (read < DATE_PROBE_MAX_BYTES) {
9168
+ const { done, value } = await reader.read();
9169
+ if (done) break;
9170
+ read += value.byteLength;
9171
+ html += decoder.decode(value, { stream: true });
9172
+ }
9173
+ await reader.cancel().catch(() => {
9174
+ });
9175
+ } catch {
9176
+ return;
9177
+ }
9178
+ const iso = extractPublishedDate(html);
9179
+ if (!iso) return;
9180
+ r.publishedDate = iso;
9181
+ r.ageDays = ageInDays(iso);
9182
+ r.freshnessSignal = freshnessSignal(r.ageDays);
9183
+ })
9184
+ );
9185
+ }
9186
+ function webSearchUnavailableMessage(reason) {
9187
+ const cause = reason === "not_configured" ? "No web-search provider is configured (Settings \u2192 Tools)." : "Web search is not available in this conversation \u2014 the active engine did not receive the tool list.";
9188
+ return `${cause} You CANNOT search the web on this turn, and no results will arrive however many times you ask. Answer from your own knowledge only, and say so plainly at the top of your reply: state that you could not search, that what follows is from training data which may be out of date, and flag anything that is likely to have changed since. Do not claim or imply that you searched, do not invent sources, citations, URLs or dates, and do not emit a confidence line.`;
9189
+ }
8640
9190
  async function execWebSearch(args, searchCfg) {
8641
9191
  const query = resolveSearchQuery(args);
8642
9192
  if (!query.trim()) return "Error: query is required.";
@@ -8649,16 +9199,75 @@ async function execWebSearch(args, searchCfg) {
8649
9199
  return `Error: could not reach the ${searchCfg.provider} search provider \u2014 ${e.message}`;
8650
9200
  }
8651
9201
  if (results.length === 0) return "No results found.";
8652
- const lines = [`RESEARCH RESULTS (${results.length} ranked results for "${query}"):`];
9202
+ await backfillPublishedDates(results);
9203
+ if (freshness === "current") {
9204
+ for (const r of results) {
9205
+ if (typeof r.ageDays === "number") {
9206
+ r.relevanceScore = Math.round(r.relevanceScore * currentnessMultiplier(r.ageDays) * 1e3) / 1e3;
9207
+ }
9208
+ }
9209
+ results.sort((a, b) => b.relevanceScore - a.relevanceScore);
9210
+ }
9211
+ const retrieved = todayIso();
9212
+ const lines = [
9213
+ `RESEARCH RESULTS (${results.length} ranked results for "${query.replace(/\s+/g, " ")}"):`,
9214
+ `Retrieved: ${retrieved} \u2014 this search ran today, so treat ${retrieved} as the current date. Weigh each result against its Published date; "Published: unknown" means the source reported no date, not that it is current.`
9215
+ ];
9216
+ const advisory = sourceAdvisory(query, results);
9217
+ if (advisory) lines.push(advisory);
8653
9218
  for (const [i, r] of results.entries()) {
8654
9219
  lines.push(`
8655
- [${i + 1}] ${r.title}`);
9220
+ [${i + 1}] ${oneLine(r.title)}`);
8656
9221
  lines.push(`Source: ${r.url}`);
8657
- lines.push(`Domain: ${r.domain} | Relevance: ${r.relevanceScore.toFixed(2)} | Freshness: ${r.freshnessSignal}`);
8658
- lines.push(`Key passage: ${r.passage}`);
9222
+ lines.push(`Domain: ${r.domain} | Relevance: ${r.relevanceScore.toFixed(2)} | Freshness: ${r.freshnessSignal} | Published: ${publishedLabel(r)} | Retrieved: ${retrieved}`);
9223
+ lines.push(`Key passage: ${oneLine(r.passage)}`);
8659
9224
  }
8660
9225
  return lines.join("\n").trim();
8661
9226
  }
9227
+ var DATE_META_KEYS = /* @__PURE__ */ new Set([
9228
+ "article:published_time",
9229
+ "og:article:published_time",
9230
+ "og:published_time",
9231
+ "datepublished",
9232
+ "date",
9233
+ "dc.date",
9234
+ "dc.date.issued",
9235
+ "pubdate",
9236
+ "publish-date"
9237
+ ]);
9238
+ function extractPublishedDate(html) {
9239
+ for (const block of html.matchAll(/<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)) {
9240
+ const iso = normalizeIsoDate(/"datePublished"\s*:\s*"([^"]{1,64})"/i.exec(block[1])?.[1]);
9241
+ if (iso) return iso;
9242
+ }
9243
+ for (const tag of html.matchAll(/<meta\s[^>]*>/gi)) {
9244
+ const key = /\b(?:property|name|itemprop)\s*=\s*["']([^"']{1,64})["']/i.exec(tag[0])?.[1];
9245
+ if (!key || !DATE_META_KEYS.has(key.trim().toLowerCase())) continue;
9246
+ const iso = normalizeIsoDate(/\bcontent\s*=\s*["']([^"']{1,64})["']/i.exec(tag[0])?.[1]);
9247
+ if (iso) return iso;
9248
+ }
9249
+ for (const tag of html.matchAll(/<time\s[^>]*>/gi)) {
9250
+ const iso = normalizeIsoDate(/\bdatetime\s*=\s*["']([^"']{1,64})["']/i.exec(tag[0])?.[1]);
9251
+ if (iso) return iso;
9252
+ }
9253
+ return void 0;
9254
+ }
9255
+ var MODIFIED_META_KEYS = /* @__PURE__ */ new Set(["article:modified_time", "og:updated_time", "datemodified", "last-modified", "dc.date.modified"]);
9256
+ function extractModifiedDate(html) {
9257
+ for (const block of html.matchAll(/<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)) {
9258
+ const iso = normalizeIsoDate(/"dateModified"\s*:\s*"([^"]{1,64})"/i.exec(block[1])?.[1]);
9259
+ if (iso) return iso;
9260
+ }
9261
+ for (const tag of html.matchAll(/<meta\s[^>]*>/gi)) {
9262
+ const key = /\b(?:property|name|itemprop)\s*=\s*["']([^"']{1,64})["']/i.exec(tag[0])?.[1];
9263
+ if (!key || !MODIFIED_META_KEYS.has(key.trim().toLowerCase())) continue;
9264
+ const iso = normalizeIsoDate(/\bcontent\s*=\s*["']([^"']{1,64})["']/i.exec(tag[0])?.[1]);
9265
+ if (iso) return iso;
9266
+ }
9267
+ return void 0;
9268
+ }
9269
+ var FETCH_URL_MAX_CHARS = 2e4;
9270
+ var FETCH_URL_TRUNCATION_MARKER = "\n[truncated: this page was longer than the fetch limit and is cut off here. If the answer is not above, fetch a more specific URL on this site rather than assuming the page lacks it.]";
8662
9271
  async function execFetchUrl(args) {
8663
9272
  const url = String(args.url ?? "").trim();
8664
9273
  if (!url) return "Error: url is required.";
@@ -8677,13 +9286,20 @@ async function execFetchUrl(args) {
8677
9286
  const contentType2 = resp.headers.get("content-type") ?? "";
8678
9287
  const text = await resp.text().catch(() => "");
8679
9288
  let content;
9289
+ let published;
9290
+ let modified;
8680
9291
  if (contentType2.includes("text/html")) {
9292
+ published = extractPublishedDate(text);
9293
+ modified = extractModifiedDate(text);
8681
9294
  content = text.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\s{2,}/g, " ").trim();
8682
9295
  } else {
8683
9296
  content = text.trim();
8684
9297
  }
8685
- if (content.length > 4e3) content = content.slice(0, 4e3) + "\n[truncated]";
8686
- return content || "(empty response)";
9298
+ const header = modified && modified > (published ?? "") ? `Published: ${published ?? "unknown"} | Updated: ${modified} | Retrieved: ${todayIso()}` : `Published: ${published ?? "unknown"} | Retrieved: ${todayIso()}`;
9299
+ const budget = FETCH_URL_MAX_CHARS - header.length - 1 - FETCH_URL_TRUNCATION_MARKER.length;
9300
+ if (content.length > budget) content = content.slice(0, budget) + FETCH_URL_TRUNCATION_MARKER;
9301
+ return `${header}
9302
+ ${content || "(empty response)"}`;
8687
9303
  }
8688
9304
  function execRunCode(args) {
8689
9305
  const code = String(args.code ?? "").trim();
@@ -9007,7 +9623,7 @@ var ToolRegistry = class {
9007
9623
  /** Build the full tools array to send to the engine. Returns [] when no tools are available. */
9008
9624
  async buildToolDefinitions() {
9009
9625
  const defs = [];
9010
- if (searchConfigured(this.toolsCfg.search)) defs.push(WEB_SEARCH_TOOL);
9626
+ if (searchConfigured(this.toolsCfg.search)) defs.push(webSearchTool());
9011
9627
  defs.push(FETCH_URL_TOOL);
9012
9628
  defs.push(RUN_CODE_TOOL);
9013
9629
  for (const client of this.mcpClients.values()) {
@@ -9034,7 +9650,7 @@ var ToolRegistry = class {
9034
9650
  const args = call.args;
9035
9651
  if (name === "web_search") {
9036
9652
  if (!searchConfigured(this.toolsCfg.search)) {
9037
- return "Error: no web-search provider configured. Add one in Settings \u2192 Tools.";
9653
+ return `Error: ${webSearchUnavailableMessage("not_configured")}`;
9038
9654
  }
9039
9655
  return execWebSearch(args, this.toolsCfg.search);
9040
9656
  }
@@ -9058,6 +9674,13 @@ var ToolRegistry = class {
9058
9674
  }
9059
9675
  return `Error: unknown tool "${name}"`;
9060
9676
  }
9677
+ /** Whether web_search can actually execute (a provider is configured). Distinct from whether
9678
+ * the model can REACH it — the tool list may still be suppressed downstream (see the
9679
+ * engine-kind gate in chat-routes.ts). Callers that need to explain a failed research turn
9680
+ * use this to pick between the 'not_configured' and 'tools_unreachable' reasons. */
9681
+ searchAvailable() {
9682
+ return searchConfigured(this.toolsCfg.search);
9683
+ }
9061
9684
  /** Whether any tools are currently available (determines if tools should be sent to engine). */
9062
9685
  hasTools() {
9063
9686
  if (searchConfigured(this.toolsCfg.search)) return true;
@@ -10317,7 +10940,7 @@ var BUILTIN_SKILLS = [
10317
10940
  }
10318
10941
  ];
10319
10942
  function toSkillMd(skill) {
10320
- const fm = [`name: ${skill.name}`, `description: ${oneLine(skill.description)}`];
10943
+ const fm = [`name: ${skill.name}`, `description: ${oneLine2(skill.description)}`];
10321
10944
  if (skill.tools.length) fm.push(`tools: ${skill.tools.join(", ")}`);
10322
10945
  return `---
10323
10946
  ${fm.join("\n")}
@@ -10345,7 +10968,7 @@ function fromSkillMd(id, raw) {
10345
10968
  function unquote(s) {
10346
10969
  return s.replace(/^["']|["']$/g, "").trim();
10347
10970
  }
10348
- function oneLine(s) {
10971
+ function oneLine2(s) {
10349
10972
  return s.replace(/\s*\n\s*/g, " ").trim();
10350
10973
  }
10351
10974
  var SkillStore = class {
@@ -10628,6 +11251,19 @@ async function body(c) {
10628
11251
  return {};
10629
11252
  }
10630
11253
  }
11254
+ var BAKED_DATE_LINE = /^Today['’]s date is [^\n]{0,140}$\n?/gm;
11255
+ function currentDateLine(now) {
11256
+ const y = now.getFullYear();
11257
+ const m = String(now.getMonth() + 1).padStart(2, "0");
11258
+ const d = String(now.getDate()).padStart(2, "0");
11259
+ return `Today's date is ${y}-${m}-${d}. Use it only when the request depends on the current date; otherwise ignore it.`;
11260
+ }
11261
+ function withCurrentDate(systemPrompt, now = /* @__PURE__ */ new Date()) {
11262
+ const base2 = systemPrompt.replace(BAKED_DATE_LINE, "").replace(/\n{3,}/g, "\n\n").trim();
11263
+ return base2 ? `${base2}
11264
+
11265
+ ${currentDateLine(now)}` : currentDateLine(now);
11266
+ }
10631
11267
  function registerChatRoutes(app2, d) {
10632
11268
  const { db: db2 } = d;
10633
11269
  app2.get("/api/v1/folders", (c) => {
@@ -10775,7 +11411,7 @@ function registerChatRoutes(app2, d) {
10775
11411
  await stream.writeSSE({ event: "meta", data: JSON.stringify({ userMessageId: userMsg.id, assistantMessageId: assistantMsg.id }) });
10776
11412
  const allMsgs = (conv.messages ?? []).filter((m) => m.id !== assistantMsg.id);
10777
11413
  const engineMessages = [];
10778
- if (conv.systemPrompt) engineMessages.push({ role: "system", content: conv.systemPrompt });
11414
+ if (conv.systemPrompt) engineMessages.push({ role: "system", content: withCurrentDate(conv.systemPrompt) });
10779
11415
  for (const m of allMsgs) {
10780
11416
  const content2 = conv.preserveThinking && m.role === "assistant" && m.reasoning?.trim() ? `<think>
10781
11417
  ${m.reasoning}
@@ -10821,7 +11457,7 @@ ${content}` : b.docContext : content;
10821
11457
  await stream.writeSSE({ event: "meta", data: JSON.stringify({ userMessageId: lastUser.id, assistantMessageId: assistantMsg.id }) });
10822
11458
  const allMsgs = (conv.messages ?? []).filter((m) => m.id !== assistantMsg.id);
10823
11459
  const engineMessages = [];
10824
- if (conv.systemPrompt) engineMessages.push({ role: "system", content: conv.systemPrompt });
11460
+ if (conv.systemPrompt) engineMessages.push({ role: "system", content: withCurrentDate(conv.systemPrompt) });
10825
11461
  for (const m of allMsgs) {
10826
11462
  const content = conv.preserveThinking && m.role === "assistant" && m.reasoning?.trim() ? `<think>
10827
11463
  ${m.reasoning}
@@ -11065,7 +11701,11 @@ ${existing}`.trim() };
11065
11701
  }
11066
11702
  }
11067
11703
  }
11068
- const CONFIDENCE_INSTRUCTION = "\n\nAfter reviewing the search results, include a confidence assessment on a line by itself before your final answer: `[confidence: 0.XX]` where XX is your confidence (0.0\u20131.0) that your answer is accurate and current. If your confidence is below 0.8, call web_search again with a more specific query first. Maximum 3 search calls per response.";
11704
+ const MAX_SEARCH_CALLS = 6;
11705
+ const MAX_CONFIDENCE_RETRIES = 2;
11706
+ const CONFIDENCE_INSTRUCTION = `
11707
+
11708
+ After reviewing the search results, include a confidence assessment on a line by itself before your final answer: \`[confidence: 0.XX]\` where XX is your confidence (0.0\u20131.0) that your answer is accurate and current. If your confidence is below 0.8, call web_search again with a more specific query first. Maximum ${MAX_SEARCH_CALLS} search calls per response.`;
11069
11709
  if (conv.toolPolicy === "force_web_search") {
11070
11710
  const sysIdx = iterMessages.findIndex((m) => m.role === "system");
11071
11711
  if (sysIdx >= 0) {
@@ -11075,9 +11715,10 @@ ${existing}`.trim() };
11075
11715
  iterMessages.unshift({ role: "system", content: CONFIDENCE_INSTRUCTION.trim() });
11076
11716
  }
11077
11717
  }
11078
- const MAX_TOOL_ITER = 10;
11718
+ const MAX_TOOL_ITER = 16;
11079
11719
  let toolIter = 0;
11080
11720
  let searchCallCount = 0;
11721
+ let confidenceRetries = 0;
11081
11722
  const allResearchSources = [];
11082
11723
  let parsedConfidence;
11083
11724
  let fullContent = "";
@@ -11295,7 +11936,7 @@ ${existing}`.trim() };
11295
11936
  if (tc.name === "web_search" && !callError) {
11296
11937
  searchCallCount++;
11297
11938
  try {
11298
- const sourceMatches = [...result.matchAll(/\[(\d+)\] (.+?)\nSource: (\S+)\nDomain: (\S+) \| Relevance: ([\d.]+) \| Freshness: (\w+)\nKey passage: ([\s\S]+?)(?=\n\[|\s*$)/g)];
11939
+ const sourceMatches = [...result.matchAll(/\[(\d+)\] (.+?)\nSource: (\S+)\nDomain: (\S+) \| Relevance: ([\d.]+) \| Freshness: (\w+)(?: \| Published: ([^|\n]*?))?(?: \| Retrieved: ([\d-]+))?\nKey passage: ([\s\S]+?)(?=\n\[|\s*$)/g)];
11299
11940
  for (const m of sourceMatches) {
11300
11941
  allResearchSources.push({
11301
11942
  title: m[2].trim(),
@@ -11303,7 +11944,7 @@ ${existing}`.trim() };
11303
11944
  domain: m[4].trim(),
11304
11945
  relevanceScore: parseFloat(m[5]),
11305
11946
  freshnessSignal: m[6].trim(),
11306
- passage: m[7].trim()
11947
+ passage: m[9].trim()
11307
11948
  });
11308
11949
  }
11309
11950
  } catch {
@@ -11320,8 +11961,9 @@ ${existing}`.trim() };
11320
11961
  const conf = parseFloat(confMatch[1]);
11321
11962
  parsedConfidence = conf;
11322
11963
  fullContent = fullContent.replace(/\[confidence:\s*[\d.]+\]\s*/gi, "").trim();
11323
- if (conf < 0.8 && searchCallCount < 3) {
11324
- console.log(`[chat] F-021: confidence ${conf} < 0.8 (searches: ${searchCallCount}/3) \u2014 re-entering search loop`);
11964
+ if (conf < 0.8 && searchCallCount < MAX_SEARCH_CALLS && confidenceRetries < MAX_CONFIDENCE_RETRIES) {
11965
+ confidenceRetries++;
11966
+ console.log(`[chat] F-021: confidence ${conf} < 0.8 (searches: ${searchCallCount}/${MAX_SEARCH_CALLS}, retry ${confidenceRetries}/${MAX_CONFIDENCE_RETRIES}) \u2014 re-entering search loop`);
11325
11967
  const toolDefs2 = await d.tools.buildToolDefinitions();
11326
11968
  if (toolDefs2.some((t) => t.function.name === "web_search")) {
11327
11969
  iterMessages.push({ role: "assistant", content: fullContent });
@@ -15208,6 +15850,33 @@ function keepRecentTokensFor(messages, baseKeepRecentTokens) {
15208
15850
  }
15209
15851
  var MUTATING_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "bash"]);
15210
15852
  var PATH_TOOLS = /* @__PURE__ */ new Set(["read", "edit", "write", "grep", "find", "ls"]);
15853
+ var LOOP_BREAK_AFTER = 3;
15854
+ function toolCallSignature(name, input) {
15855
+ const stable = (v) => {
15856
+ if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "undefined";
15857
+ if (Array.isArray(v)) return `[${v.map(stable).join(",")}]`;
15858
+ const o = v;
15859
+ return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${stable(o[k])}`).join(",")}}`;
15860
+ };
15861
+ return `${name}\0${stable(input)}`;
15862
+ }
15863
+ var ToolLoopTracker = class {
15864
+ lastSig = null;
15865
+ count = 0;
15866
+ record(name, input) {
15867
+ const sig = toolCallSignature(name, input);
15868
+ if (sig === this.lastSig) this.count += 1;
15869
+ else {
15870
+ this.lastSig = sig;
15871
+ this.count = 1;
15872
+ }
15873
+ return this.count;
15874
+ }
15875
+ reset() {
15876
+ this.lastSig = null;
15877
+ this.count = 0;
15878
+ }
15879
+ };
15211
15880
  var DEPENDENCY_ADD_PATTERNS = [
15212
15881
  /\bnpm\s+(i|install|add)\s+\S/,
15213
15882
  /\byarn\s+add\s+\S/,
@@ -15291,6 +15960,7 @@ async function runCodeSession(params) {
15291
15960
  };
15292
15961
  let consecutiveToolFailures = 0;
15293
15962
  let hasSearchedWebThisTurn = false;
15963
+ const toolLoop = new ToolLoopTracker();
15294
15964
  const getLspClient = (spec) => {
15295
15965
  let clients = lspClientsByConv.get(convId);
15296
15966
  if (!clients) {
@@ -15452,15 +16122,16 @@ async function runCodeSession(params) {
15452
16122
  pi.on("after_provider_response", () => {
15453
16123
  releaseGate();
15454
16124
  });
15455
- pi.on("turn_start", () => {
15456
- consecutiveToolFailures = 0;
15457
- hasSearchedWebThisTurn = false;
15458
- });
15459
16125
  pi.on("tool_call", async (event) => {
15460
16126
  const toolName = event.toolName;
15461
16127
  const toolCallId = event.toolCallId;
15462
16128
  const input = event.input;
15463
16129
  await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "pending" } });
16130
+ if (toolLoop.record(toolName, input) > LOOP_BREAK_AFTER) {
16131
+ const reason = `[SYSTEM: you have called \`${toolName}\` with identical arguments too many times in a row and it is not making progress \u2014 this call was NOT executed. Stop repeating it: take a different action, call it with different arguments, or, if you already have what you need, give your final answer now.]`;
16132
+ await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
16133
+ return { block: true, reason };
16134
+ }
15464
16135
  if (PATH_TOOLS.has(toolName)) {
15465
16136
  const p = input.path;
15466
16137
  const pathRequired = toolName === "read" || toolName === "edit" || toolName === "write";