nodal-agents 0.4.0 → 0.4.3

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 (178) hide show
  1. package/README.md +34 -14
  2. package/cli.js +16 -1
  3. package/migrations/0030_agent_fallback_llm_keys.sql +6 -0
  4. package/migrations/0031_llm_key_capabilities.sql +7 -0
  5. package/migrations/0032_drop_llm_key_model_caps.sql +9 -0
  6. package/migrations/0033_agent_fallback_chain.sql +21 -0
  7. package/migrations/meta/_journal.json +28 -0
  8. package/package.json +2 -1
  9. package/runner.js +602 -94
  10. package/web/.next/BUILD_ID +1 -1
  11. package/web/.next/app-path-routes-manifest.json +1 -1
  12. package/web/.next/build-manifest.json +2 -2
  13. package/web/.next/prerender-manifest.json +3 -3
  14. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page.js +3 -3
  15. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page.js.nft.json +1 -1
  16. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page_client-reference-manifest.js +1 -1
  17. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page.js +1 -1
  18. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page.js.nft.json +1 -1
  19. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page_client-reference-manifest.js +1 -1
  20. package/web/.next/server/app/(dashboard)/agents/page.js +2 -2
  21. package/web/.next/server/app/(dashboard)/agents/page.js.nft.json +1 -1
  22. package/web/.next/server/app/(dashboard)/agents/page_client-reference-manifest.js +1 -1
  23. package/web/.next/server/app/(dashboard)/approvals/page.js +2 -2
  24. package/web/.next/server/app/(dashboard)/approvals/page.js.nft.json +1 -1
  25. package/web/.next/server/app/(dashboard)/approvals/page_client-reference-manifest.js +1 -1
  26. package/web/.next/server/app/(dashboard)/automations/page.js +2 -2
  27. package/web/.next/server/app/(dashboard)/automations/page.js.nft.json +1 -1
  28. package/web/.next/server/app/(dashboard)/automations/page_client-reference-manifest.js +1 -1
  29. package/web/.next/server/app/(dashboard)/billing/page.js +1 -1
  30. package/web/.next/server/app/(dashboard)/billing/page.js.nft.json +1 -1
  31. package/web/.next/server/app/(dashboard)/billing/page_client-reference-manifest.js +1 -1
  32. package/web/.next/server/app/(dashboard)/chat/page.js +2 -2
  33. package/web/.next/server/app/(dashboard)/chat/page.js.nft.json +1 -1
  34. package/web/.next/server/app/(dashboard)/chat/page_client-reference-manifest.js +1 -1
  35. package/web/.next/server/app/(dashboard)/connectors/page.js +1 -1
  36. package/web/.next/server/app/(dashboard)/connectors/page.js.nft.json +1 -1
  37. package/web/.next/server/app/(dashboard)/connectors/page_client-reference-manifest.js +1 -1
  38. package/web/.next/server/app/(dashboard)/credentials/page.js +1 -1
  39. package/web/.next/server/app/(dashboard)/credentials/page.js.nft.json +1 -1
  40. package/web/.next/server/app/(dashboard)/credentials/page_client-reference-manifest.js +1 -1
  41. package/web/.next/server/app/(dashboard)/jobs/[id]/page.js +2 -2
  42. package/web/.next/server/app/(dashboard)/jobs/[id]/page.js.nft.json +1 -1
  43. package/web/.next/server/app/(dashboard)/jobs/[id]/page_client-reference-manifest.js +1 -1
  44. package/web/.next/server/app/(dashboard)/jobs/page.js +2 -2
  45. package/web/.next/server/app/(dashboard)/jobs/page.js.nft.json +1 -1
  46. package/web/.next/server/app/(dashboard)/jobs/page_client-reference-manifest.js +1 -1
  47. package/web/.next/server/app/(dashboard)/llm-providers/page.js +2 -2
  48. package/web/.next/server/app/(dashboard)/llm-providers/page.js.nft.json +1 -1
  49. package/web/.next/server/app/(dashboard)/llm-providers/page_client-reference-manifest.js +1 -1
  50. package/web/.next/server/app/(dashboard)/logs/page.js +1 -1
  51. package/web/.next/server/app/(dashboard)/logs/page.js.nft.json +1 -1
  52. package/web/.next/server/app/(dashboard)/logs/page_client-reference-manifest.js +1 -1
  53. package/web/.next/server/app/(dashboard)/mcp/page.js +2 -2
  54. package/web/.next/server/app/(dashboard)/mcp/page.js.nft.json +1 -1
  55. package/web/.next/server/app/(dashboard)/mcp/page_client-reference-manifest.js +1 -1
  56. package/web/.next/server/app/(dashboard)/memories/page.js +2 -2
  57. package/web/.next/server/app/(dashboard)/memories/page.js.nft.json +1 -1
  58. package/web/.next/server/app/(dashboard)/memories/page_client-reference-manifest.js +1 -1
  59. package/web/.next/server/app/(dashboard)/page.js +2 -2
  60. package/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
  61. package/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
  62. package/web/.next/server/app/(dashboard)/settings/page.js +2 -2
  63. package/web/.next/server/app/(dashboard)/settings/page.js.nft.json +1 -1
  64. package/web/.next/server/app/(dashboard)/settings/page_client-reference-manifest.js +1 -1
  65. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page.js +1 -1
  66. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page.js.nft.json +1 -1
  67. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page_client-reference-manifest.js +1 -1
  68. package/web/.next/server/app/(dashboard)/skills/new/page.js +2 -2
  69. package/web/.next/server/app/(dashboard)/skills/new/page.js.nft.json +1 -1
  70. package/web/.next/server/app/(dashboard)/skills/new/page_client-reference-manifest.js +1 -1
  71. package/web/.next/server/app/(dashboard)/skills/page.js +2 -2
  72. package/web/.next/server/app/(dashboard)/skills/page.js.nft.json +1 -1
  73. package/web/.next/server/app/(dashboard)/skills/page_client-reference-manifest.js +1 -1
  74. package/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  75. package/web/.next/server/app/_global-error.html +1 -1
  76. package/web/.next/server/app/_global-error.rsc +2 -2
  77. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  78. package/web/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  79. package/web/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  80. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  81. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +2 -2
  82. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  83. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  84. package/web/.next/server/app/_not-found.html +1 -1
  85. package/web/.next/server/app/_not-found.rsc +3 -3
  86. package/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  87. package/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  88. package/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  89. package/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  90. package/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  91. package/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  92. package/web/.next/server/app/api/oauth/[provider]/callback/route.js +1 -1
  93. package/web/.next/server/app/api/oauth/[provider]/start/route.js +1 -1
  94. package/web/.next/server/app/login/page_client-reference-manifest.js +1 -1
  95. package/web/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
  96. package/web/.next/server/app/onboarding.html +1 -1
  97. package/web/.next/server/app/onboarding.rsc +3 -3
  98. package/web/.next/server/app/onboarding.segments/_full.segment.rsc +3 -3
  99. package/web/.next/server/app/onboarding.segments/_head.segment.rsc +1 -1
  100. package/web/.next/server/app/onboarding.segments/_index.segment.rsc +3 -3
  101. package/web/.next/server/app/onboarding.segments/_tree.segment.rsc +2 -2
  102. package/web/.next/server/app/onboarding.segments/onboarding/__PAGE__.segment.rsc +1 -1
  103. package/web/.next/server/app/onboarding.segments/onboarding.segment.rsc +1 -1
  104. package/web/.next/server/app-paths-manifest.json +1 -1
  105. package/web/.next/server/chunks/3233.js +1 -0
  106. package/web/.next/server/chunks/4574.js +1 -1
  107. package/web/.next/server/chunks/4808.js +1 -0
  108. package/web/.next/server/chunks/{3057.js → 7466.js} +1 -1
  109. package/web/.next/server/chunks/7741.js +3 -3
  110. package/web/.next/server/chunks/8052.js +1 -0
  111. package/web/.next/server/chunks/8766.js +1 -0
  112. package/web/.next/server/chunks/8782.js +1 -0
  113. package/web/.next/server/chunks/9084.js +1 -0
  114. package/web/.next/server/chunks/{7557.js → 9606.js} +2 -2
  115. package/web/.next/server/middleware-build-manifest.js +1 -1
  116. package/web/.next/server/pages/404.html +1 -1
  117. package/web/.next/server/pages/500.html +1 -1
  118. package/web/.next/server/server-reference-manifest.js +1 -1
  119. package/web/.next/server/server-reference-manifest.json +1 -1
  120. package/web/.next/static/chunks/{9060-df7c0c4c6fa27737.js → 2575-e660568bd1a9bcb6.js} +2 -2
  121. package/web/.next/static/chunks/3233-e6efb7fb1fa24591.js +1 -0
  122. package/web/.next/static/chunks/5436-c1006a40e59853ed.js +1 -0
  123. package/web/.next/static/chunks/7025-7afa82fda10bddc4.js +62 -0
  124. package/web/.next/static/chunks/{5801-e411029984b17b8b.js → 8396-f3502b9af3172006.js} +1 -1
  125. package/web/.next/static/chunks/{8503-ced632da5c3fce79.js → 9098-2bfef80a73c706b3.js} +1 -1
  126. package/web/.next/static/chunks/9123-5c5ad180c831baa4.js +1 -0
  127. package/web/.next/static/chunks/{6679-7c76034b83edeb06.js → 9582-fbf7c8d9b2a39101.js} +1 -1
  128. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/edit/page-de6c8fc7cb73a3de.js +2 -0
  129. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/telegram/page-6d4161f1e0b19885.js +1 -0
  130. package/web/.next/static/chunks/app/(dashboard)/agents/page-50005050a3304bee.js +1 -0
  131. package/web/.next/static/chunks/app/(dashboard)/approvals/page-7f4314908d1024f6.js +1 -0
  132. package/web/.next/static/chunks/app/(dashboard)/automations/page-7693601b49363371.js +1 -0
  133. package/web/.next/static/chunks/app/(dashboard)/chat/page-839128f211f63728.js +1 -0
  134. package/web/.next/static/chunks/app/(dashboard)/connectors/page-a6a1d8f0a33d2faf.js +1 -0
  135. package/web/.next/static/chunks/app/(dashboard)/jobs/[id]/page-4fc570c6c1e39edb.js +1 -0
  136. package/web/.next/static/chunks/app/(dashboard)/jobs/page-cf861b235dc54ced.js +1 -0
  137. package/web/.next/static/chunks/app/(dashboard)/layout-769de8a52528194a.js +1 -0
  138. package/web/.next/static/chunks/app/(dashboard)/llm-providers/page-99eab754716f9071.js +1 -0
  139. package/web/.next/static/chunks/app/(dashboard)/mcp/page-082442b4f9ac0f91.js +1 -0
  140. package/web/.next/static/chunks/app/(dashboard)/memories/page-e201633b4bbbdf73.js +1 -0
  141. package/web/.next/static/chunks/app/(dashboard)/page-a42d880f7036e866.js +1 -0
  142. package/web/.next/static/chunks/app/(dashboard)/settings/page-d85cac3728506241.js +1 -0
  143. package/web/.next/static/chunks/app/(dashboard)/skills/[id]/edit/page-ecaf3520da303237.js +1 -0
  144. package/web/.next/static/chunks/app/(dashboard)/skills/new/page-cdbc2aada2be0bfc.js +1 -0
  145. package/web/.next/static/chunks/app/(dashboard)/skills/page-234553540bef945b.js +1 -0
  146. package/web/.next/static/css/78ead23854ab041e.css +3 -0
  147. package/web/.next/server/chunks/1511.js +0 -1
  148. package/web/.next/server/chunks/2103.js +0 -1
  149. package/web/.next/server/chunks/211.js +0 -1
  150. package/web/.next/server/chunks/8178.js +0 -1
  151. package/web/.next/server/chunks/9201.js +0 -1
  152. package/web/.next/server/chunks/9824.js +0 -1
  153. package/web/.next/static/chunks/1165-ec573be2aa63710b.js +0 -1
  154. package/web/.next/static/chunks/2569-6b5e0af9c1f584a4.js +0 -1
  155. package/web/.next/static/chunks/6522-3f865de55adb618d.js +0 -1
  156. package/web/.next/static/chunks/921-f437093debcddbb3.js +0 -1
  157. package/web/.next/static/chunks/9421-d522a48618c4fe37.js +0 -62
  158. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/edit/page-d3724fbf38b71806.js +0 -2
  159. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/telegram/page-e6b35d5f361044a9.js +0 -1
  160. package/web/.next/static/chunks/app/(dashboard)/agents/page-b58294bf588f4581.js +0 -1
  161. package/web/.next/static/chunks/app/(dashboard)/approvals/page-b9e504918d043b6d.js +0 -1
  162. package/web/.next/static/chunks/app/(dashboard)/automations/page-4807e81e2af3030e.js +0 -1
  163. package/web/.next/static/chunks/app/(dashboard)/chat/page-2c8f9571a443f250.js +0 -1
  164. package/web/.next/static/chunks/app/(dashboard)/connectors/page-72ccb0e3a5ed6f2d.js +0 -1
  165. package/web/.next/static/chunks/app/(dashboard)/jobs/[id]/page-40172a14d0b1368f.js +0 -1
  166. package/web/.next/static/chunks/app/(dashboard)/jobs/page-d4a3a16745e02fd1.js +0 -1
  167. package/web/.next/static/chunks/app/(dashboard)/layout-4d5634ba460464d7.js +0 -1
  168. package/web/.next/static/chunks/app/(dashboard)/llm-providers/page-90fb785e2ab32759.js +0 -1
  169. package/web/.next/static/chunks/app/(dashboard)/mcp/page-426478332dfe8313.js +0 -1
  170. package/web/.next/static/chunks/app/(dashboard)/memories/page-aa46f5f7efbfa262.js +0 -1
  171. package/web/.next/static/chunks/app/(dashboard)/page-fc49d7ed8e472118.js +0 -1
  172. package/web/.next/static/chunks/app/(dashboard)/settings/page-1cc10beb46234c7d.js +0 -1
  173. package/web/.next/static/chunks/app/(dashboard)/skills/[id]/edit/page-0b61f21847f4c7a0.js +0 -1
  174. package/web/.next/static/chunks/app/(dashboard)/skills/new/page-9de96e643c361732.js +0 -1
  175. package/web/.next/static/chunks/app/(dashboard)/skills/page-4566512d74e54bfe.js +0 -1
  176. package/web/.next/static/css/0a81480f93d3ab37.css +0 -3
  177. /package/web/.next/static/{9FXcaPSw8KYgjKzjKLpT2 → n6jP_zB4kqJScKY_T2ciu}/_buildManifest.js +0 -0
  178. /package/web/.next/static/{9FXcaPSw8KYgjKzjKLpT2 → n6jP_zB4kqJScKY_T2ciu}/_ssgManifest.js +0 -0
package/runner.js CHANGED
@@ -1102,6 +1102,155 @@ var init_connector_catalog = __esm({
1102
1102
  }
1103
1103
  });
1104
1104
 
1105
+ // ../../packages/shared/src/model-catalog.ts
1106
+ function findModelCatalogEntry(provider, modelId) {
1107
+ return MODEL_CATALOG[provider]?.find((e) => e.modelId === modelId);
1108
+ }
1109
+ var MODEL_CATALOG;
1110
+ var init_model_catalog = __esm({
1111
+ "../../packages/shared/src/model-catalog.ts"() {
1112
+ "use strict";
1113
+ MODEL_CATALOG = {
1114
+ anthropic: [
1115
+ {
1116
+ modelId: "claude-opus-4-8",
1117
+ label: "Claude Opus 4.8",
1118
+ capabilities: { tools: true, forcedToolChoice: true }
1119
+ },
1120
+ {
1121
+ modelId: "claude-sonnet-4-6",
1122
+ label: "Claude Sonnet 4.6",
1123
+ capabilities: { tools: true, forcedToolChoice: true }
1124
+ },
1125
+ {
1126
+ modelId: "claude-haiku-4-5-20251001",
1127
+ label: "Claude Haiku 4.5",
1128
+ capabilities: { tools: true, forcedToolChoice: true }
1129
+ }
1130
+ ],
1131
+ openai: [
1132
+ { modelId: "gpt-5", label: "GPT-5", capabilities: { tools: true, forcedToolChoice: true } },
1133
+ {
1134
+ modelId: "gpt-5-mini",
1135
+ label: "GPT-5 mini",
1136
+ capabilities: { tools: true, forcedToolChoice: true }
1137
+ }
1138
+ ],
1139
+ google: [
1140
+ {
1141
+ modelId: "gemini-2.0-flash",
1142
+ label: "Gemini 2.0 Flash",
1143
+ capabilities: { tools: true, forcedToolChoice: true }
1144
+ },
1145
+ {
1146
+ modelId: "gemini-2.5-pro",
1147
+ label: "Gemini 2.5 Pro",
1148
+ capabilities: { tools: true, forcedToolChoice: true }
1149
+ }
1150
+ ],
1151
+ groq: [
1152
+ {
1153
+ modelId: "llama-3.3-70b-versatile",
1154
+ label: "Llama 3.3 70B",
1155
+ capabilities: { tools: true, forcedToolChoice: true }
1156
+ }
1157
+ ],
1158
+ mistral: [
1159
+ {
1160
+ modelId: "mistral-large-latest",
1161
+ label: "Mistral Large",
1162
+ capabilities: { tools: true, forcedToolChoice: true }
1163
+ }
1164
+ ],
1165
+ // OpenRouter models are namespaced by sub-vendor (anthropic/, deepseek/, …).
1166
+ // The UI groups them by that vendor (see modelGroupLabel). Tested + working
1167
+ // routes. `forcedToolChoice` is per-model: most accept tool_choice:'required';
1168
+ // MiniMax M3 does not (some of its OpenRouter endpoints reject the forced
1169
+ // value), so it runs on 'auto' + the runtime floor.
1170
+ openrouter: [
1171
+ // Anthropic
1172
+ {
1173
+ modelId: "anthropic/claude-haiku-4.5",
1174
+ label: "Claude Haiku 4.5",
1175
+ capabilities: { tools: true, forcedToolChoice: true }
1176
+ },
1177
+ {
1178
+ modelId: "anthropic/claude-opus-4.7",
1179
+ label: "Claude Opus 4.7",
1180
+ capabilities: { tools: true, forcedToolChoice: true }
1181
+ },
1182
+ {
1183
+ modelId: "anthropic/claude-opus-4.7-fast",
1184
+ label: "Claude Opus 4.7 (fast)",
1185
+ capabilities: { tools: true, forcedToolChoice: true }
1186
+ },
1187
+ {
1188
+ modelId: "anthropic/claude-opus-4.8",
1189
+ label: "Claude Opus 4.8",
1190
+ capabilities: { tools: true, forcedToolChoice: true }
1191
+ },
1192
+ {
1193
+ modelId: "anthropic/claude-opus-4.8-fast",
1194
+ label: "Claude Opus 4.8 (fast)",
1195
+ capabilities: { tools: true, forcedToolChoice: true }
1196
+ },
1197
+ {
1198
+ modelId: "anthropic/claude-sonnet-4.6",
1199
+ label: "Claude Sonnet 4.6",
1200
+ capabilities: { tools: true, forcedToolChoice: true }
1201
+ },
1202
+ // DeepSeek
1203
+ {
1204
+ modelId: "deepseek/deepseek-v3.2",
1205
+ label: "DeepSeek V3.2",
1206
+ capabilities: { tools: true, forcedToolChoice: true }
1207
+ },
1208
+ {
1209
+ modelId: "deepseek/deepseek-v4-flash",
1210
+ label: "DeepSeek V4 Flash",
1211
+ capabilities: { tools: true, forcedToolChoice: true }
1212
+ },
1213
+ {
1214
+ modelId: "deepseek/deepseek-v4-pro",
1215
+ label: "DeepSeek V4 Pro",
1216
+ capabilities: { tools: true, forcedToolChoice: true }
1217
+ },
1218
+ // Google
1219
+ {
1220
+ modelId: "google/gemini-3.1-flash-lite-preview",
1221
+ label: "Gemini 3.1 Flash Lite (preview)",
1222
+ capabilities: { tools: true, forcedToolChoice: true }
1223
+ },
1224
+ {
1225
+ modelId: "google/gemini-3.1-pro-preview",
1226
+ label: "Gemini 3.1 Pro (preview)",
1227
+ capabilities: { tools: true, forcedToolChoice: true }
1228
+ },
1229
+ {
1230
+ modelId: "google/gemini-3.5-flash",
1231
+ label: "Gemini 3.5 Flash",
1232
+ capabilities: { tools: true, forcedToolChoice: true }
1233
+ },
1234
+ {
1235
+ modelId: "google/gemma-4-31b-it",
1236
+ label: "Gemma 4 31B-IT",
1237
+ capabilities: { tools: true, forcedToolChoice: true }
1238
+ },
1239
+ // MiniMax
1240
+ {
1241
+ modelId: "minimax/minimax-m3",
1242
+ label: "MiniMax M3",
1243
+ // A reasoning model. Some of its OpenRouter endpoints reject a FORCED
1244
+ // tool_choice ('required') → we send 'auto' (forcedToolChoice:false).
1245
+ // reasoning:true makes the provider return reasoning_details so the runner
1246
+ // can round-trip them across tool-call turns.
1247
+ capabilities: { tools: true, forcedToolChoice: false, reasoning: true }
1248
+ }
1249
+ ]
1250
+ };
1251
+ }
1252
+ });
1253
+
1105
1254
  // ../../packages/shared/src/index.ts
1106
1255
  var init_src = __esm({
1107
1256
  "../../packages/shared/src/index.ts"() {
@@ -1127,6 +1276,7 @@ var init_src = __esm({
1127
1276
  init_providers();
1128
1277
  init_root_agent();
1129
1278
  init_connector_catalog();
1279
+ init_model_catalog();
1130
1280
  }
1131
1281
  });
1132
1282
 
@@ -1262,7 +1412,6 @@ var init_llm_keys = __esm({
1262
1412
  apiKeyLast4: text3("api_key_last4").notNull().default(""),
1263
1413
  baseUrl: text3("base_url"),
1264
1414
  nickname: text3("nickname"),
1265
- defaultModel: text3("default_model"),
1266
1415
  isActive: boolean2("is_active").notNull().default(true),
1267
1416
  createdAt: timestamp3("created_at", { withTimezone: true }).notNull().defaultNow(),
1268
1417
  updatedAt: timestamp3("updated_at", { withTimezone: true }).notNull().defaultNow()
@@ -1281,6 +1430,7 @@ import {
1281
1430
  integer,
1282
1431
  bigint,
1283
1432
  timestamp as timestamp4,
1433
+ jsonb as jsonb2,
1284
1434
  index as index3,
1285
1435
  check as check2
1286
1436
  } from "drizzle-orm/pg-core";
@@ -1301,6 +1451,14 @@ var init_agents = __esm({
1301
1451
  personality: text4("personality").notNull(),
1302
1452
  model: text4("model").default("claude-sonnet-4-6-20260217"),
1303
1453
  llmKeyId: uuid4("llm_key_id").references(() => entityLlmKeys.id, { onDelete: "set null" }),
1454
+ // Ordered LLM-key fallback chain (Guard 2). When the primary key
1455
+ // (llmKeyId) exhausts retries / times out / hits quota mid-job, the runner
1456
+ // fails over to these in order; all-down fails loud (`all_providers_failed`).
1457
+ // Empty = no failover (default). Each link is a (keyId, model) pair so a
1458
+ // fallback runs on a CHOSEN model (empty model ⇒ that provider's catalog
1459
+ // default). FK integrity is enforced in the app layer; a deleted key is
1460
+ // skipped at resolution time.
1461
+ fallbackChain: jsonb2("fallback_chain").$type().default(sql2`'[]'::jsonb`),
1304
1462
  active: boolean3("active").default(true),
1305
1463
  isDefault: boolean3("is_default").default(false),
1306
1464
  role: text4("role").default("agent"),
@@ -1380,7 +1538,7 @@ var init_agents = __esm({
1380
1538
  });
1381
1539
 
1382
1540
  // ../../packages/db/src/schema/jobs.ts
1383
- import { pgTable as pgTable5, text as text5, uuid as uuid5, integer as integer2, jsonb as jsonb2, timestamp as timestamp5, index as index4, check as check3 } from "drizzle-orm/pg-core";
1541
+ import { pgTable as pgTable5, text as text5, uuid as uuid5, integer as integer2, jsonb as jsonb3, timestamp as timestamp5, index as index4, check as check3 } from "drizzle-orm/pg-core";
1384
1542
  import { sql as sql3 } from "drizzle-orm";
1385
1543
  var agentJobs;
1386
1544
  var init_jobs = __esm({
@@ -1400,7 +1558,7 @@ var init_jobs = __esm({
1400
1558
  originalTask: text5("original_task"),
1401
1559
  chatId: text5("chat_id"),
1402
1560
  systemPrompt: text5("system_prompt"),
1403
- messages: jsonb2("messages").default(sql3`'[]'::jsonb`),
1561
+ messages: jsonb3("messages").default(sql3`'[]'::jsonb`),
1404
1562
  toolsUsed: text5("tools_used").array().default(sql3`'{}'::text[]`),
1405
1563
  turn: integer2("turn").default(0),
1406
1564
  result: text5("result"),
@@ -1428,7 +1586,7 @@ var init_jobs = __esm({
1428
1586
  * different specialist (live regression: job `7767a3c1`, 2026-05-19).
1429
1587
  */
1430
1588
  lastFailedDelegationSlug: text5("last_failed_delegation_slug"),
1431
- pendingDelegation: jsonb2("pending_delegation"),
1589
+ pendingDelegation: jsonb3("pending_delegation"),
1432
1590
  completedAt: timestamp5("completed_at", { withTimezone: true }),
1433
1591
  createdAt: timestamp5("created_at", { withTimezone: true }).defaultNow(),
1434
1592
  updatedAt: timestamp5("updated_at", { withTimezone: true }).defaultNow()
@@ -1463,7 +1621,7 @@ import {
1463
1621
  text as text6,
1464
1622
  uuid as uuid6,
1465
1623
  integer as integer3,
1466
- jsonb as jsonb3,
1624
+ jsonb as jsonb4,
1467
1625
  timestamp as timestamp6,
1468
1626
  numeric,
1469
1627
  index as index5,
@@ -1499,7 +1657,7 @@ var init_tasks = __esm({
1499
1657
  outputTokens: integer3("output_tokens").default(0),
1500
1658
  costUsd: numeric("cost_usd", { precision: 10, scale: 6 }).default("0"),
1501
1659
  dependsOn: uuid6("depends_on").array().default(sql4`'{}'::uuid[]`),
1502
- context: jsonb3("context").default(sql4`'{}'::jsonb`),
1660
+ context: jsonb4("context").default(sql4`'{}'::jsonb`),
1503
1661
  rootJobId: uuid6("root_job_id"),
1504
1662
  lockedAt: timestamp6("locked_at", { withTimezone: true }),
1505
1663
  lockedBy: text6("locked_by"),
@@ -1607,7 +1765,7 @@ var init_connectors = __esm({
1607
1765
  });
1608
1766
 
1609
1767
  // ../../packages/db/src/schema/tool_calls.ts
1610
- import { pgTable as pgTable9, text as text9, uuid as uuid9, integer as integer4, jsonb as jsonb4, timestamp as timestamp9, index as index8 } from "drizzle-orm/pg-core";
1768
+ import { pgTable as pgTable9, text as text9, uuid as uuid9, integer as integer4, jsonb as jsonb5, timestamp as timestamp9, index as index8 } from "drizzle-orm/pg-core";
1611
1769
  import { sql as sql7 } from "drizzle-orm";
1612
1770
  var toolCalls;
1613
1771
  var init_tool_calls = __esm({
@@ -1622,7 +1780,7 @@ var init_tool_calls = __esm({
1622
1780
  entityId: uuid9("entity_id").references(() => entities.id, { onDelete: "cascade" }),
1623
1781
  jobId: uuid9("job_id").references(() => agentJobs.id, { onDelete: "cascade" }),
1624
1782
  toolName: text9("tool_name").notNull(),
1625
- toolInput: jsonb4("tool_input"),
1783
+ toolInput: jsonb5("tool_input"),
1626
1784
  toolOutput: text9("tool_output"),
1627
1785
  durationMs: integer4("duration_ms"),
1628
1786
  turn: integer4("turn"),
@@ -1639,7 +1797,7 @@ var init_tool_calls = __esm({
1639
1797
  });
1640
1798
 
1641
1799
  // ../../packages/db/src/schema/approvals.ts
1642
- import { pgTable as pgTable10, text as text10, uuid as uuid10, jsonb as jsonb5, timestamp as timestamp10, index as index9, check as check7 } from "drizzle-orm/pg-core";
1800
+ import { pgTable as pgTable10, text as text10, uuid as uuid10, jsonb as jsonb6, timestamp as timestamp10, index as index9, check as check7 } from "drizzle-orm/pg-core";
1643
1801
  import { sql as sql8 } from "drizzle-orm";
1644
1802
  var approvalRequests, approvalRules;
1645
1803
  var init_approvals = __esm({
@@ -1656,7 +1814,7 @@ var init_approvals = __esm({
1656
1814
  jobId: uuid10("job_id").notNull().references(() => agentJobs.id, { onDelete: "cascade" }),
1657
1815
  agentId: uuid10("agent_id").references(() => agents.id, { onDelete: "cascade" }),
1658
1816
  toolName: text10("tool_name").notNull(),
1659
- toolInput: jsonb5("tool_input").notNull(),
1817
+ toolInput: jsonb6("tool_input").notNull(),
1660
1818
  status: text10("status").default("pending"),
1661
1819
  requestedAt: timestamp10("requested_at", { withTimezone: true }).defaultNow(),
1662
1820
  resolvedAt: timestamp10("resolved_at", { withTimezone: true }),
@@ -1691,7 +1849,7 @@ var init_approvals = __esm({
1691
1849
  agentId: uuid10("agent_id").references(() => agents.id, { onDelete: "cascade" }),
1692
1850
  toolName: text10("tool_name").notNull(),
1693
1851
  action: text10("action").notNull(),
1694
- conditionJson: jsonb5("condition_json").default(sql8`'{}'::jsonb`),
1852
+ conditionJson: jsonb6("condition_json").default(sql8`'{}'::jsonb`),
1695
1853
  createdAt: timestamp10("created_at", { withTimezone: true }).defaultNow(),
1696
1854
  updatedAt: timestamp10("updated_at", { withTimezone: true }).defaultNow()
1697
1855
  },
@@ -1819,7 +1977,7 @@ import {
1819
1977
  uuid as uuid13,
1820
1978
  boolean as boolean7,
1821
1979
  integer as integer7,
1822
- jsonb as jsonb6,
1980
+ jsonb as jsonb7,
1823
1981
  timestamp as timestamp13,
1824
1982
  index as index12
1825
1983
  } from "drizzle-orm/pg-core";
@@ -1843,8 +2001,8 @@ var init_skills = __esm({
1843
2001
  description: text13("description"),
1844
2002
  defaultContent: text13("default_content"),
1845
2003
  contentOverridden: boolean7("content_overridden").default(false),
1846
- requiredConfig: jsonb6("required_config").default(sql10`'[]'::jsonb`),
1847
- operations: jsonb6("operations").default(sql10`'[]'::jsonb`),
2004
+ requiredConfig: jsonb7("required_config").default(sql10`'[]'::jsonb`),
2005
+ operations: jsonb7("operations").default(sql10`'[]'::jsonb`),
1848
2006
  requiredBuiltins: text13("required_builtins").array().notNull().default(sql10`'{}'::text[]`),
1849
2007
  createdAt: timestamp13("created_at", { withTimezone: true }).defaultNow(),
1850
2008
  updatedAt: timestamp13("updated_at", { withTimezone: true }).defaultNow()
@@ -1889,7 +2047,7 @@ var init_skills = __esm({
1889
2047
  entityId: uuid13("entity_id").notNull().references(() => entities.id, { onDelete: "cascade" }),
1890
2048
  agentId: uuid13("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
1891
2049
  skillId: uuid13("skill_id").notNull().references(() => agentSkills.id, { onDelete: "cascade" }),
1892
- approvalOverrides: jsonb6("approval_overrides").default(sql10`'{}'::jsonb`),
2050
+ approvalOverrides: jsonb7("approval_overrides").default(sql10`'{}'::jsonb`),
1893
2051
  useCustomInstructions: boolean7("use_custom_instructions").notNull().default(false),
1894
2052
  enabledOperations: text13("enabled_operations").array(),
1895
2053
  createdAt: timestamp13("created_at", { withTimezone: true }).notNull().defaultNow()
@@ -1999,7 +2157,7 @@ import {
1999
2157
  text as text16,
2000
2158
  uuid as uuid16,
2001
2159
  boolean as boolean10,
2002
- jsonb as jsonb7,
2160
+ jsonb as jsonb8,
2003
2161
  timestamp as timestamp16,
2004
2162
  index as index15,
2005
2163
  uniqueIndex as uniqueIndex2,
@@ -2023,7 +2181,7 @@ var init_mcp = __esm({
2023
2181
  url: text16("url"),
2024
2182
  command: text16("command"),
2025
2183
  args: text16("args").array().default(sql13`'{}'::text[]`),
2026
- envVars: jsonb7("env_vars").default(sql13`'{}'::jsonb`),
2184
+ envVars: jsonb8("env_vars").default(sql13`'{}'::jsonb`),
2027
2185
  // Encrypted (enc:v1: blob) credential for HTTP MCP servers — same pattern
2028
2186
  // as connectors.api_key. NULL for servers that need no auth.
2029
2187
  apiKey: text16("api_key"),
@@ -2036,7 +2194,7 @@ var init_mcp = __esm({
2036
2194
  // The literal header name or query param name (e.g. 'x-api-key', 'api_key').
2037
2195
  authParamName: text16("auth_param_name"),
2038
2196
  active: boolean10("active").default(true),
2039
- availableTools: jsonb7("available_tools"),
2197
+ availableTools: jsonb8("available_tools"),
2040
2198
  createdAt: timestamp16("created_at", { withTimezone: true }).defaultNow(),
2041
2199
  updatedAt: timestamp16("updated_at", { withTimezone: true }).defaultNow()
2042
2200
  },
@@ -2058,7 +2216,7 @@ var init_mcp = __esm({
2058
2216
  entityId: uuid16("entity_id").notNull().references(() => entities.id, { onDelete: "cascade" }),
2059
2217
  agentId: uuid16("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
2060
2218
  mcpServerId: uuid16("mcp_server_id").notNull().references(() => mcpServers.id, { onDelete: "cascade" }),
2061
- enabledTools: jsonb7("enabled_tools"),
2219
+ enabledTools: jsonb8("enabled_tools"),
2062
2220
  createdAt: timestamp16("created_at", { withTimezone: true }).notNull().defaultNow(),
2063
2221
  updatedAt: timestamp16("updated_at", { withTimezone: true }).notNull().defaultNow()
2064
2222
  },
@@ -2077,7 +2235,7 @@ var init_mcp = __esm({
2077
2235
  entityId: uuid16("entity_id").notNull().references(() => entities.id, { onDelete: "cascade" }),
2078
2236
  slug: text16("slug").notNull(),
2079
2237
  active: boolean10("active").notNull().default(true),
2080
- toolConfig: jsonb7("tool_config").notNull().default(sql13`'{}'::jsonb`),
2238
+ toolConfig: jsonb8("tool_config").notNull().default(sql13`'{}'::jsonb`),
2081
2239
  createdAt: timestamp16("created_at", { withTimezone: true }).notNull().defaultNow(),
2082
2240
  updatedAt: timestamp16("updated_at", { withTimezone: true }).notNull().defaultNow()
2083
2241
  },
@@ -2093,7 +2251,7 @@ import {
2093
2251
  uuid as uuid17,
2094
2252
  boolean as boolean11,
2095
2253
  integer as integer9,
2096
- jsonb as jsonb8,
2254
+ jsonb as jsonb9,
2097
2255
  timestamp as timestamp17,
2098
2256
  index as index16,
2099
2257
  check as check12
@@ -2111,7 +2269,7 @@ var init_misc = __esm({
2111
2269
  id: uuid17("id").primaryKey().defaultRandom(),
2112
2270
  entityId: uuid17("entity_id").notNull().references(() => entities.id, { onDelete: "cascade" }),
2113
2271
  agentId: uuid17("agent_id").references(() => agents.id, { onDelete: "cascade" }),
2114
- messages: jsonb8("messages").notNull().default(sql14`'[]'::jsonb`),
2272
+ messages: jsonb9("messages").notNull().default(sql14`'[]'::jsonb`),
2115
2273
  status: text17("status").notNull().default("active"),
2116
2274
  turnCount: integer9("turn_count").notNull().default(0),
2117
2275
  createdAt: timestamp17("created_at", { withTimezone: true }).notNull().defaultNow(),
@@ -2131,7 +2289,7 @@ var init_misc = __esm({
2131
2289
  slug: text17("slug").notNull(),
2132
2290
  description: text17("description"),
2133
2291
  pluginType: text17("plugin_type").notNull(),
2134
- config: jsonb8("config").default(sql14`'{}'::jsonb`),
2292
+ config: jsonb9("config").default(sql14`'{}'::jsonb`),
2135
2293
  active: boolean11("active").default(true),
2136
2294
  hook: text17("hook").notNull(),
2137
2295
  webhookUrl: text17("webhook_url"),
@@ -10307,6 +10465,21 @@ Caused by: ${underlyingCause.stack}`;
10307
10465
  }
10308
10466
  }
10309
10467
  };
10468
+ var AllProvidersFailedError = class extends Error {
10469
+ code = "all_providers_failed";
10470
+ underlyingCause;
10471
+ constructor(providerCount, underlyingCause) {
10472
+ super(
10473
+ `All ${providerCount} LLM providers failed; last: ${formatCauseSummary(underlyingCause)}`
10474
+ );
10475
+ this.name = "AllProvidersFailedError";
10476
+ this.underlyingCause = underlyingCause;
10477
+ if (underlyingCause instanceof Error) {
10478
+ this.stack = `${this.stack}
10479
+ Caused by: ${underlyingCause.stack}`;
10480
+ }
10481
+ }
10482
+ };
10310
10483
  function formatCauseSummary(cause) {
10311
10484
  if (cause instanceof Error) {
10312
10485
  const name = cause.name || "Error";
@@ -10587,6 +10760,48 @@ function sleep(ms) {
10587
10760
  return new Promise((resolve) => setTimeout(resolve, ms));
10588
10761
  }
10589
10762
 
10763
+ // ../../packages/llm/src/tool-choice-floor.ts
10764
+ function isUnsupportedToolChoiceError(err) {
10765
+ let cur = err;
10766
+ for (let depth = 0; depth < 5 && cur; depth++) {
10767
+ if (!(cur instanceof Error)) return false;
10768
+ const parts = [cur.message ?? ""];
10769
+ const body = cur.responseBody;
10770
+ if (typeof body === "string") parts.push(body);
10771
+ const data = cur.data;
10772
+ if (data !== void 0 && data !== null) {
10773
+ try {
10774
+ parts.push(JSON.stringify(data));
10775
+ } catch {
10776
+ }
10777
+ }
10778
+ const text22 = parts.join(" ").toLowerCase();
10779
+ if (text22.includes("tool_choice") && (text22.includes("no endpoints") || text22.includes("not supported") || text22.includes("does not support") || text22.includes("unsupported") || text22.includes("invalid value"))) {
10780
+ return true;
10781
+ }
10782
+ const inner = cur.cause;
10783
+ if (inner === cur) return false;
10784
+ cur = inner;
10785
+ }
10786
+ return false;
10787
+ }
10788
+ async function generateWithToolChoiceFloor(run, toolChoice, label) {
10789
+ try {
10790
+ return await run();
10791
+ } catch (err) {
10792
+ const wasForced = toolChoice !== void 0 && toolChoice !== "auto";
10793
+ if (wasForced && isUnsupportedToolChoiceError(err)) {
10794
+ console.warn(
10795
+ `[tool_choice_relaxed] ${label}: provider rejected tool_choice=${JSON.stringify(
10796
+ toolChoice
10797
+ )} \u2014 retrying with 'auto'`
10798
+ );
10799
+ return run("auto");
10800
+ }
10801
+ throw err;
10802
+ }
10803
+ }
10804
+
10590
10805
  // ../../packages/llm/src/providers/anthropic.ts
10591
10806
  import { createAnthropic } from "@ai-sdk/anthropic";
10592
10807
  function buildAnthropicModel(config) {
@@ -10743,7 +10958,8 @@ function buildGroqModel(config) {
10743
10958
  }
10744
10959
 
10745
10960
  // ../../packages/llm/src/providers/openrouter.ts
10746
- import { createOpenAICompatible as createOpenAICompatible2 } from "@ai-sdk/openai-compatible";
10961
+ init_src();
10962
+ import { createOpenRouter } from "@openrouter/ai-sdk-provider";
10747
10963
  import { wrapLanguageModel } from "ai";
10748
10964
 
10749
10965
  // ../../packages/llm/src/providers/parsers.ts
@@ -10971,15 +11187,21 @@ function buildOpenRouterModel(config) {
10971
11187
  throw new ProviderConfigError("openrouter provider requires an apiKey");
10972
11188
  }
10973
11189
  const baseURL = config.baseURL ?? PROVIDER_PRESETS.openrouter.defaultBaseURL;
10974
- const provider = createOpenAICompatible2({
10975
- name: "openrouter",
10976
- baseURL,
11190
+ const provider = createOpenRouter({
10977
11191
  apiKey: config.apiKey,
11192
+ baseURL,
11193
+ // 'strict' is the documented mode for the first-party OpenRouter API
11194
+ // ('compatible' is for 3rd-party proxies). We hit openrouter.ai directly.
11195
+ compatibility: "strict",
10978
11196
  // Normalise non-spec responses (e.g. DeepSeek V4 returning function.arguments
10979
- // as an object instead of a JSON string) before AI SDK's Zod schema sees them.
11197
+ // as an object instead of a JSON string) before the SDK's Zod schema sees them.
10980
11198
  fetch: createTolerantFetch()
10981
11199
  });
10982
- const base = provider(config.model);
11200
+ const isReasoning = findModelCatalogEntry("openrouter", config.model)?.capabilities.reasoning;
11201
+ const base = provider.chat(
11202
+ config.model,
11203
+ isReasoning ? { extraBody: { reasoning: { enabled: true } } } : void 0
11204
+ );
10983
11205
  const middleware = middlewareForFamily(detectAgenticFamily(config.model));
10984
11206
  if (middleware) {
10985
11207
  return wrapLanguageModel({ model: base, middleware });
@@ -11068,21 +11290,27 @@ function createLlmClient(config) {
11068
11290
  };
11069
11291
  const clientGenerateText = async (args) => {
11070
11292
  validateIfMessages(args);
11071
- return withRetry(
11072
- () => callWithTimeout(
11073
- () => generateText({
11074
- ...args,
11075
- model,
11076
- // AI SDK native timeout via AbortSignal.timeout(). Survives middleware
11077
- // wrapping unlike a passed-in abortSignal which their internal retry
11078
- // can swallow.
11079
- timeout: LLM_TIMEOUT_MS,
11080
- // Disable AI SDK internal retry we own retries via withRetry to
11081
- // preserve typed error handling (Quota/MessageStructure/LLMTimeout).
11082
- maxRetries: 0
11083
- })
11293
+ const toolChoice = args.toolChoice;
11294
+ return generateWithToolChoiceFloor(
11295
+ (override) => withRetry(
11296
+ () => callWithTimeout(
11297
+ () => generateText({
11298
+ ...args,
11299
+ model,
11300
+ ...override ? { toolChoice: override } : {},
11301
+ // AI SDK native timeout via AbortSignal.timeout(). Survives
11302
+ // middleware wrapping unlike a passed-in abortSignal which their
11303
+ // internal retry can swallow.
11304
+ timeout: LLM_TIMEOUT_MS,
11305
+ // Disable AI SDK internal retry — we own retries via withRetry to
11306
+ // preserve typed error handling (Quota/MessageStructure/LLMTimeout).
11307
+ maxRetries: 0
11308
+ })
11309
+ ),
11310
+ retryOpts
11084
11311
  ),
11085
- retryOpts
11312
+ toolChoice,
11313
+ `${config.provider}/${config.model}`
11086
11314
  );
11087
11315
  };
11088
11316
  const clientStreamText = (args) => {
@@ -11112,6 +11340,66 @@ function createLlmClient(config) {
11112
11340
  };
11113
11341
  }
11114
11342
 
11343
+ // ../../packages/llm/src/failover.ts
11344
+ function isFailoverWorthy(err) {
11345
+ return err instanceof RetryExhaustedError || err instanceof LLMTimeoutError || err instanceof QuotaExhaustedError;
11346
+ }
11347
+ function errLabel(err) {
11348
+ return err instanceof Error ? err.name : String(err);
11349
+ }
11350
+ function createFailoverFromClients(clients) {
11351
+ if (clients.length === 0) {
11352
+ throw new ProviderConfigError("failover: at least one client is required");
11353
+ }
11354
+ if (clients.length === 1) return clients[0];
11355
+ let activeIndex = 0;
11356
+ async function runWithFailover(op, label) {
11357
+ let lastErr;
11358
+ for (let i = activeIndex; i < clients.length; i++) {
11359
+ try {
11360
+ const result = await op(clients[i]);
11361
+ activeIndex = i;
11362
+ return result;
11363
+ } catch (err) {
11364
+ lastErr = err;
11365
+ if (!isFailoverWorthy(err)) throw err;
11366
+ const next = i + 1;
11367
+ if (next < clients.length) {
11368
+ console.warn(
11369
+ `[llm-failover] ${label}: ${clients[i].config.provider}/${clients[i].config.model} failed (${errLabel(err)}) \u2014 failing over to ${clients[next].config.provider}/${clients[next].config.model}`
11370
+ );
11371
+ }
11372
+ }
11373
+ }
11374
+ throw new AllProvidersFailedError(clients.length, lastErr);
11375
+ }
11376
+ const primary = clients[0];
11377
+ return {
11378
+ // Surface the primary's identity/capabilities; the chain is homogeneous in
11379
+ // the capability that matters here (tool use). Failover is for outages, not
11380
+ // capability switching.
11381
+ config: primary.config,
11382
+ capabilities: primary.capabilities,
11383
+ generateText: ((args) => runWithFailover(
11384
+ (c) => c.generateText(args),
11385
+ "generateText"
11386
+ )),
11387
+ // Streaming keeps single-provider semantics (the runner loop uses
11388
+ // generateText). Delegate to the currently-active provider.
11389
+ streamText: ((args) => clients[activeIndex].streamText(args)),
11390
+ generateObject: ((args) => runWithFailover(
11391
+ (c) => c.generateObject(args),
11392
+ "generateObject"
11393
+ ))
11394
+ };
11395
+ }
11396
+ function createFailoverLlmClient(configs) {
11397
+ if (configs.length === 0) {
11398
+ throw new ProviderConfigError("failover: at least one provider config is required");
11399
+ }
11400
+ return createFailoverFromClients(configs.map((c) => createLlmClient(c)));
11401
+ }
11402
+
11115
11403
  // ../../packages/llm/src/embeddings.ts
11116
11404
  import { embed } from "ai";
11117
11405
  import { createOllama as createOllama2 } from "ollama-ai-provider-v2";
@@ -11344,7 +11632,10 @@ async function _writeToolCall(ctx, toolName, input, output, durationMs) {
11344
11632
 
11345
11633
  // ../../packages/tools/src/tool-choice.ts
11346
11634
  function computeToolChoice(cfg) {
11347
- const { isOrchestrator, turn, hasAdapterTools } = cfg;
11635
+ const { isOrchestrator, turn, hasAdapterTools, modelSupportsForcedToolChoice = true } = cfg;
11636
+ if (!modelSupportsForcedToolChoice) {
11637
+ return "auto";
11638
+ }
11348
11639
  if (hasAdapterTools && !isOrchestrator) {
11349
11640
  return "required";
11350
11641
  }
@@ -26212,6 +26503,67 @@ async function createMcpTools(opts) {
26212
26503
  return { tools, close: conn.close };
26213
26504
  }
26214
26505
 
26506
+ // src/job/resolve-llm.ts
26507
+ init_src();
26508
+ async function resolveAgentLlmClient(db, agent, onSkip) {
26509
+ if (!agent.llmKeyId) return { ok: false, reason: "agent_no_llm_configured" };
26510
+ const seen = /* @__PURE__ */ new Set();
26511
+ const requested = [];
26512
+ for (const link of [
26513
+ { keyId: agent.llmKeyId, model: agent.model },
26514
+ ...agent.fallbackChain ?? []
26515
+ ]) {
26516
+ if (typeof link.keyId === "string" && link.keyId.length > 0 && !seen.has(link.keyId)) {
26517
+ seen.add(link.keyId);
26518
+ requested.push({ keyId: link.keyId, model: link.model ?? "" });
26519
+ }
26520
+ }
26521
+ const ids = requested.map((r) => r.keyId);
26522
+ const rows = await db.select().from(entityLlmKeys).where(inArray3(entityLlmKeys.id, ids));
26523
+ const byId = new Map(rows.map((r) => [r.id, r]));
26524
+ try {
26525
+ const configs = [];
26526
+ for (const { keyId, model: requestedModel } of requested) {
26527
+ const row = byId.get(keyId);
26528
+ if (!row || !row.isActive) {
26529
+ onSkip?.({ keyId, reason: "missing_or_inactive" });
26530
+ continue;
26531
+ }
26532
+ const model = requestedModel.length > 0 ? requestedModel : MODEL_CATALOG[row.provider]?.[0]?.modelId ?? "";
26533
+ if (!model) {
26534
+ onSkip?.({ keyId, reason: "no_catalog_model" });
26535
+ continue;
26536
+ }
26537
+ const plaintextKey = row.apiKey ? decrypt(row.apiKey) : "";
26538
+ configs.push({
26539
+ provider: row.provider,
26540
+ model,
26541
+ apiKey: plaintextKey || void 0,
26542
+ baseURL: row.baseUrl ?? void 0
26543
+ });
26544
+ }
26545
+ if (configs.length === 0) return { ok: false, reason: "agent_no_llm_configured" };
26546
+ const effectivePrimary = configs[0];
26547
+ const client = configs.length > 1 ? createFailoverLlmClient(configs) : createLlmClient(effectivePrimary);
26548
+ return {
26549
+ ok: true,
26550
+ client,
26551
+ primaryProvider: effectivePrimary.provider,
26552
+ chainLength: configs.length,
26553
+ // Capability comes from the model CATALOG (provider, model of the
26554
+ // effective primary), not a stored column. Unknown/custom models default
26555
+ // to true; the runtime tool_choice floor backstops a wrong guess.
26556
+ primarySupportsForcedToolChoice: findModelCatalogEntry(effectivePrimary.provider, effectivePrimary.model)?.capabilities.forcedToolChoice ?? true
26557
+ };
26558
+ } catch (err) {
26559
+ return {
26560
+ ok: false,
26561
+ reason: "llm_key_invalid",
26562
+ detail: err instanceof Error ? err.message.slice(0, 200) : "llm_key_invalid"
26563
+ };
26564
+ }
26565
+ }
26566
+
26215
26567
  // ../../packages/orchestration/src/errors.ts
26216
26568
  var DelegationPendingError = class extends Error {
26217
26569
  constructor(childJobId, childSlug) {
@@ -26273,7 +26625,16 @@ var DEFAULT_LIMITS = {
26273
26625
  maxDelegationDepth: 3,
26274
26626
  maxTurns: 50,
26275
26627
  // matches Hermes Agent's per-subagent iteration budget; cumulative cap across resumes
26276
- maxConsecutiveDeliveryTurns: 3
26628
+ maxConsecutiveDeliveryTurns: 3,
26629
+ // 1.5M total tokens: a loud backstop well above any legitimate single job
26630
+ // (typical jobs sit in the tens of thousands) yet below the ~2.4M-token
26631
+ // runaway that motivated it. Override per-deployment via MAX_TOTAL_TOKENS_PER_JOB.
26632
+ maxTotalTokensPerJob: 15e5,
26633
+ // 12 identical (toolName+input+output) turns in a row before declaring the job
26634
+ // stuck. Deliberately conservative: a real poll completes (output changes) long
26635
+ // before 12 identical reads, so this only catches genuinely degenerate loops —
26636
+ // and maxTurns (50) is the ultimate backstop above it.
26637
+ maxNoProgressRepeats: 12
26277
26638
  };
26278
26639
  var ChainCounters = class _ChainCounters {
26279
26640
  constructor(limits = DEFAULT_LIMITS) {
@@ -26971,7 +27332,7 @@ function buildJobContextBlock(ctx) {
26971
27332
  if (ctx.telegramChatId) lines.push(`- telegram_chat_id: ${ctx.telegramChatId}`);
26972
27333
  if (ctx.surface === "chat") {
26973
27334
  lines.push(
26974
- '- surface: in-app dashboard chat \u2014 you are talking directly with the user; reply in plain text. For conversation or recalling facts, just reply (your durable facts are loaded below). For ANY action \u2014 using a connector or skill, delegating to your team, sending/fetching/creating/publishing, or (as the workspace ROOT) creating agents, skills, MCP servers, connectors or automations \u2014 call `run_task` with a clear, self-contained instruction: it runs as a tracked job with your FULL toolset. `run_task` is your gateway to everything you can do \u2014 NEVER tell the user you cannot do something that an action could accomplish; escalate it via `run_task` instead. When you call `run_task`, ALSO write a brief one-line acknowledgment in your own voice (e.g. "Je m\'en occupe\u2026") so the user sees you started. Do not call any other named tool on this surface.'
27335
+ '- surface: in-app dashboard chat \u2014 you are talking directly with the user; reply in plain text. For conversation or recalling facts, just reply (your durable facts are loaded below). For ANY action \u2014 using a connector or skill, delegating to your team, sending/fetching/creating/publishing, or (as the workspace ROOT) creating agents, skills, MCP servers, connectors or automations \u2014 you MUST call the `run_task` tool with a clear, self-contained instruction. CRITICAL: writing in text that you will do something (e.g. "Je lance X\u2026") does NOT start anything \u2014 ONLY an actual `run_task` tool call performs the action. If you intend to act, the `run_task` tool call is mandatory; a text-only reply about an action accomplishes nothing. It runs as a tracked job with your FULL toolset. `run_task` is your gateway to everything you can do \u2014 NEVER tell the user you cannot do something that an action could accomplish; escalate it via `run_task` instead. You may add a one-line acknowledgment in your own voice alongside the call, but the `run_task` call is what actually does the work. Do not call any other named tool on this surface.'
26975
27336
  );
26976
27337
  }
26977
27338
  if (ctx.notifyOnSuccess) {
@@ -27312,9 +27673,21 @@ function truncateForContext(value) {
27312
27673
 
27313
27674
  [... truncated: ${dropped} chars dropped (total ${value.length}) ...]`;
27314
27675
  }
27676
+ function stableStringify(value) {
27677
+ return JSON.stringify(value, (_key, val) => {
27678
+ if (val && typeof val === "object" && !Array.isArray(val)) {
27679
+ return Object.keys(val).sort().reduce((acc, k) => {
27680
+ acc[k] = val[k];
27681
+ return acc;
27682
+ }, {});
27683
+ }
27684
+ return val;
27685
+ });
27686
+ }
27315
27687
  async function executeJob(jobId, deps, _runnerEnv) {
27316
27688
  const { db, registry } = deps;
27317
27689
  let llmClient;
27690
+ let modelSupportsForcedToolChoice = true;
27318
27691
  const startedAt = Date.now();
27319
27692
  const trace = (event, data) => {
27320
27693
  console.error(`[exec ${jobId}] ${event}`, data ? JSON.stringify(data) : "");
@@ -27378,28 +27751,27 @@ async function executeJob(jobId, deps, _runnerEnv) {
27378
27751
  return { status: "failed", error: "agent_no_llm_configured" };
27379
27752
  }
27380
27753
  {
27381
- const [keyRow] = await db.select().from(entityLlmKeys).where(eq4(entityLlmKeys.id, agentRow.llmKeyId)).limit(1);
27382
- if (!keyRow || !keyRow.isActive) {
27383
- await failJob(db, jobId, "agent_no_llm_configured", runStats());
27384
- return { status: "failed", error: "agent_no_llm_configured" };
27385
- }
27386
- try {
27387
- const plaintextKey = keyRow.apiKey ? decrypt(keyRow.apiKey) : "";
27388
- llmClient = createLlmClient({
27389
- provider: keyRow.provider,
27390
- model: agent.model,
27391
- apiKey: plaintextKey || void 0,
27392
- baseURL: keyRow.baseUrl ?? void 0
27393
- });
27394
- trace("llm_client_from_key", {
27395
- keyId: keyRow.id,
27396
- provider: keyRow.provider
27397
- });
27398
- } catch (err) {
27399
- const errorCode = err instanceof Error ? err.message.slice(0, 200) : "llm_key_invalid";
27400
- await failJob(db, jobId, `llm_key_invalid:${errorCode}`, runStats());
27401
- return { status: "failed", error: `llm_key_invalid:${errorCode}` };
27402
- }
27754
+ const resolved = await resolveAgentLlmClient(
27755
+ db,
27756
+ {
27757
+ llmKeyId: agentRow.llmKeyId,
27758
+ fallbackChain: agentRow.fallbackChain ?? null,
27759
+ model: agent.model
27760
+ },
27761
+ (info) => trace("fallback_key_skipped", info)
27762
+ );
27763
+ if (!resolved.ok) {
27764
+ const code = resolved.reason === "agent_no_llm_configured" ? "agent_no_llm_configured" : `llm_key_invalid:${resolved.detail}`;
27765
+ await failJob(db, jobId, code, runStats());
27766
+ return { status: "failed", error: code };
27767
+ }
27768
+ llmClient = resolved.client;
27769
+ modelSupportsForcedToolChoice = resolved.primarySupportsForcedToolChoice;
27770
+ trace("llm_client_from_key", {
27771
+ provider: resolved.primaryProvider,
27772
+ chainLength: resolved.chainLength,
27773
+ forcedToolChoice: modelSupportsForcedToolChoice
27774
+ });
27403
27775
  }
27404
27776
  const childRows = await db.select({ id: agents.id, slug: agents.slug, role: agents.role }).from(agentAssignments).innerJoin(agents, eq4(agentAssignments.subAgentId, agents.id)).where(and3(eq4(agentAssignments.orchestratorId, agentRow.id), eq4(agents.active, true)));
27405
27777
  const children = childRows.map((r) => ({
@@ -27734,6 +28106,20 @@ async function executeJob(jobId, deps, _runnerEnv) {
27734
28106
  await setJobStatus(db, jobId, "awaiting_approval");
27735
28107
  return { status: "awaiting_approval" };
27736
28108
  };
28109
+ const maxTotalTokensPerJob = (() => {
28110
+ const raw = process.env["MAX_TOTAL_TOKENS_PER_JOB"];
28111
+ const n = raw ? Number(raw) : NaN;
28112
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_LIMITS.maxTotalTokensPerJob;
28113
+ })();
28114
+ const recentTurnSignatures = [];
28115
+ const maxNoProgressRepeats = (() => {
28116
+ const raw = process.env["MAX_NO_PROGRESS_REPEATS"];
28117
+ const n = raw ? Number(raw) : NaN;
28118
+ return Number.isFinite(n) && n >= 2 ? Math.floor(n) : DEFAULT_LIMITS.maxNoProgressRepeats;
28119
+ })();
28120
+ const unresolvedToolFailures = /* @__PURE__ */ new Set();
28121
+ const MAX_UNRESOLVED_FAILURE_NUDGES = 2;
28122
+ let unresolvedFailureNudges = 0;
27737
28123
  try {
27738
28124
  while (true) {
27739
28125
  turn += 1;
@@ -27749,7 +28135,12 @@ async function executeJob(jobId, deps, _runnerEnv) {
27749
28135
  return { status: "failed", error: "turn_limit_exceeded" };
27750
28136
  }
27751
28137
  validateMessageStructure(messages);
27752
- const toolChoice = computeToolChoice({ isOrchestrator, turn, hasAdapterTools });
28138
+ const toolChoice = computeToolChoice({
28139
+ isOrchestrator,
28140
+ turn,
28141
+ hasAdapterTools,
28142
+ modelSupportsForcedToolChoice
28143
+ });
27753
28144
  const aiSdkTools = {};
27754
28145
  for (const [name, toolDef] of toolMap) {
27755
28146
  const description = authoringToolsSuffix && (name === "create_skill" || name === "update_skill") ? toolDef.description + authoringToolsSuffix : toolDef.description;
@@ -27767,6 +28158,11 @@ async function executeJob(jobId, deps, _runnerEnv) {
27767
28158
  const completionT = Number(usage?.outputTokens ?? 0);
27768
28159
  inputTokens += Number.isFinite(promptT) ? promptT : 0;
27769
28160
  outputTokens += Number.isFinite(completionT) ? completionT : 0;
28161
+ if (inputTokens + outputTokens > maxTotalTokensPerJob) {
28162
+ trace("token_budget_exceeded", { turn, inputTokens, outputTokens, maxTotalTokensPerJob });
28163
+ await failJob(db, jobId, "token_budget_exceeded", runStats());
28164
+ return { status: "failed", error: "token_budget_exceeded" };
28165
+ }
27770
28166
  const rawToolCalls = response.toolCalls ?? [];
27771
28167
  trace("llm_call_done", {
27772
28168
  turn,
@@ -27781,14 +28177,18 @@ async function executeJob(jobId, deps, _runnerEnv) {
27781
28177
  await failJob(db, jobId, "delivery_spam_guard", runStats());
27782
28178
  return { status: "failed", error: "delivery_spam_guard" };
27783
28179
  }
28180
+ const reasoningParts = response.reasoning ?? [];
27784
28181
  const assistantMsg = {
27785
28182
  role: "assistant",
27786
- content: rawToolCalls.length > 0 ? rawToolCalls.map((tc) => ({
27787
- type: "tool-call",
27788
- toolCallId: tc.toolCallId,
27789
- toolName: tc.toolName,
27790
- input: tc.input
27791
- })) : response.text || ""
28183
+ content: rawToolCalls.length > 0 ? [
28184
+ ...reasoningParts,
28185
+ ...rawToolCalls.map((tc) => ({
28186
+ type: "tool-call",
28187
+ toolCallId: tc.toolCallId,
28188
+ toolName: tc.toolName,
28189
+ input: tc.input
28190
+ }))
28191
+ ] : reasoningParts.length > 0 ? [...reasoningParts, { type: "text", text: response.text || "" }] : response.text || ""
27792
28192
  };
27793
28193
  messages = [...messages, assistantMsg];
27794
28194
  const returnResultCall = rawToolCalls.find((tc) => tc.toolName === "return_result");
@@ -28011,6 +28411,11 @@ async function executeJob(jobId, deps, _runnerEnv) {
28011
28411
  if (toolResult.outcome === "success" && DELIVERY_TOOL_NAMES.has(call.name)) {
28012
28412
  telegramDelivered = true;
28013
28413
  }
28414
+ if (toolResult.outcome === "success") {
28415
+ unresolvedToolFailures.delete(call.name);
28416
+ } else {
28417
+ unresolvedToolFailures.add(call.name);
28418
+ }
28014
28419
  toolResultBlocks.push({
28015
28420
  type: "tool-result",
28016
28421
  toolCallId: call.id,
@@ -28059,6 +28464,38 @@ async function executeJob(jobId, deps, _runnerEnv) {
28059
28464
  }
28060
28465
  if (returnResultCall) {
28061
28466
  trace("return_result_branch", { turn });
28467
+ const rrStatus = returnResultCall.input?.status;
28468
+ if (rrStatus === "success" && unresolvedToolFailures.size > 0) {
28469
+ const stuck = [...unresolvedToolFailures];
28470
+ if (unresolvedFailureNudges < MAX_UNRESOLVED_FAILURE_NUDGES) {
28471
+ unresolvedFailureNudges += 1;
28472
+ trace("unresolved_tool_failure_nudge", {
28473
+ turn,
28474
+ attempt: unresolvedFailureNudges,
28475
+ stuck
28476
+ });
28477
+ toolResultBlocks.push({
28478
+ type: "tool-result",
28479
+ toolCallId: returnResultCall.toolCallId,
28480
+ toolName: "return_result",
28481
+ output: toResultOutput({
28482
+ error: "deferred: tu signales success mais ces actions ont \xE9chou\xE9 sans \xEAtre corrig\xE9es (" + stuck.join(", ") + "). R\xE9essaie l'action jusqu'\xE0 r\xE9ussite, ou appelle return_result avec status='blocked'."
28483
+ })
28484
+ });
28485
+ messages = [...messages, { role: "tool", content: toolResultBlocks }];
28486
+ messages = [
28487
+ ...messages,
28488
+ {
28489
+ role: "user",
28490
+ content: "[syst\xE8me] Ne d\xE9clare pas un succ\xE8s qui n'a pas eu lieu. Une action a \xE9chou\xE9 et n'a pas \xE9t\xE9 corrig\xE9e. Corrige-la, ou termine honn\xEAtement avec status='blocked'."
28491
+ }
28492
+ ];
28493
+ continue;
28494
+ }
28495
+ trace("unresolved_tool_failure", { turn, stuck });
28496
+ await failJob(db, jobId, "unresolved_tool_failure", runStats());
28497
+ return { status: "failed", error: "unresolved_tool_failure" };
28498
+ }
28062
28499
  const taskRows = await db.select({ id: agentTasks.id }).from(agentTasks).where(eq4(agentTasks.rootJobId, jobId));
28063
28500
  if (requiresToolDelivery && !telegramDelivered && taskRows.length === 0) {
28064
28501
  if (telegramRedeliveryNudges < MAX_TELEGRAM_REDELIVERY_NUDGES) {
@@ -28118,6 +28555,23 @@ async function executeJob(jobId, deps, _runnerEnv) {
28118
28555
  if (toolResultBlocks.length > 0) {
28119
28556
  messages = [...messages, { role: "tool", content: toolResultBlocks }];
28120
28557
  }
28558
+ const turnSignature = toolResultBlocks.map((b) => {
28559
+ const call = callsToProcess.find((c) => c.id === b.toolCallId);
28560
+ const input = call ? stableStringify(call.input) : "";
28561
+ const output = b.output.type === "text" ? b.output.value : stableStringify(b.output.value ?? null);
28562
+ return `${b.toolName}\0${input}\0${output}`;
28563
+ }).sort().join("\n");
28564
+ if (turnSignature !== "") {
28565
+ recentTurnSignatures.push(turnSignature);
28566
+ if (recentTurnSignatures.length > maxNoProgressRepeats) {
28567
+ recentTurnSignatures.shift();
28568
+ }
28569
+ if (recentTurnSignatures.length === maxNoProgressRepeats && recentTurnSignatures.every((s) => s === turnSignature)) {
28570
+ trace("no_progress_detected", { turn, repeats: recentTurnSignatures.length });
28571
+ await failJob(db, jobId, "no_progress_detected", runStats());
28572
+ return { status: "failed", error: "no_progress_detected" };
28573
+ }
28574
+ }
28121
28575
  await saveCheckpoint(db, jobId, {
28122
28576
  messages,
28123
28577
  turn,
@@ -28148,6 +28602,10 @@ async function executeJob(jobId, deps, _runnerEnv) {
28148
28602
  await failJob(db, jobId, "quota_exhausted", runStats());
28149
28603
  return { status: "failed", error: "quota_exhausted" };
28150
28604
  }
28605
+ if (err instanceof AllProvidersFailedError) {
28606
+ await failJob(db, jobId, err.code, runStats());
28607
+ return { status: "failed", error: err.code };
28608
+ }
28151
28609
  if (err instanceof MessageStructureError) {
28152
28610
  await failJob(db, jobId, `message_structure_invalid:${err.code}`, runStats());
28153
28611
  return { status: "failed", error: `message_structure_invalid:${err.code}` };
@@ -28773,6 +29231,7 @@ var CHAT_TOOLS = {
28773
29231
  inputSchema: z89.object({ instruction: z89.string().min(1).max(4e3) })
28774
29232
  }
28775
29233
  };
29234
+ var ESCALATION_RECHECK = "Re-read your previous reply. If it committed to performing an action \u2014 running, launching, sending, fetching, creating, configuring, delegating, or any task or tool use \u2014 then your text ALONE did nothing: call the run_task tool NOW with a clear, self-contained instruction. If your reply was pure conversation, a question, or simply recalling a fact, do not call any tool \u2014 the conversation is complete.";
28776
29235
  async function runChatTurn(opts) {
28777
29236
  const { deps, entityId, agentId, conversationId, message } = opts;
28778
29237
  const db = deps.db;
@@ -28786,20 +29245,18 @@ async function runChatTurn(opts) {
28786
29245
  const title = message.trim().slice(0, TITLE_MAX) + (message.trim().length > TITLE_MAX ? "\u2026" : "");
28787
29246
  await db.update(conversations).set({ title }).where(eq4(conversations.id, conversationId));
28788
29247
  }
28789
- const [keyRow] = await db.select().from(entityLlmKeys).where(eq4(entityLlmKeys.id, agentRow.llmKeyId)).limit(1);
28790
- if (!keyRow || !keyRow.isActive) return { ok: false, error: "agent_no_llm_configured" };
28791
- let llmClient;
28792
- try {
28793
- const plaintextKey = keyRow.apiKey ? decrypt(keyRow.apiKey) : "";
28794
- llmClient = createLlmClient({
28795
- provider: keyRow.provider,
28796
- model: agentRow.model ?? DEFAULT_MODEL,
28797
- apiKey: plaintextKey || void 0,
28798
- baseURL: keyRow.baseUrl ?? void 0
28799
- });
28800
- } catch {
28801
- return { ok: false, error: "llm_key_invalid" };
29248
+ const resolved = await resolveAgentLlmClient(db, {
29249
+ llmKeyId: agentRow.llmKeyId,
29250
+ fallbackChain: agentRow.fallbackChain ?? null,
29251
+ model: agentRow.model ?? DEFAULT_MODEL
29252
+ });
29253
+ if (!resolved.ok) {
29254
+ return {
29255
+ ok: false,
29256
+ error: resolved.reason === "agent_no_llm_configured" ? "agent_no_llm_configured" : "llm_key_invalid"
29257
+ };
28802
29258
  }
29259
+ const llmClient = resolved.client;
28803
29260
  const agent = {
28804
29261
  id: agentRow.id,
28805
29262
  name: agentRow.name,
@@ -28816,8 +29273,43 @@ async function runChatTurn(opts) {
28816
29273
  origin: "dashboard",
28817
29274
  surface: "chat"
28818
29275
  });
28819
- const rows = await db.select({ role: chatMessages.role, content: chatMessages.content }).from(chatMessages).where(eq4(chatMessages.conversationId, conversationId)).orderBy(desc(chatMessages.createdAt)).limit(HISTORY_LIMIT);
28820
- const messages = rows.reverse().map((r) => ({ role: r.role, content: r.content }));
29276
+ const rows = await db.select({
29277
+ role: chatMessages.role,
29278
+ content: chatMessages.content,
29279
+ jobId: chatMessages.jobId,
29280
+ jobTask: agentJobs.task
29281
+ }).from(chatMessages).leftJoin(agentJobs, eq4(chatMessages.jobId, agentJobs.id)).where(eq4(chatMessages.conversationId, conversationId)).orderBy(desc(chatMessages.createdAt)).limit(HISTORY_LIMIT);
29282
+ const messages = [];
29283
+ for (const r of rows.reverse()) {
29284
+ if (r.role === "assistant" && r.jobId) {
29285
+ const toolCallId = `hist-${r.jobId}`;
29286
+ messages.push({
29287
+ role: "assistant",
29288
+ content: [
29289
+ ...r.content ? [{ type: "text", text: r.content }] : [],
29290
+ {
29291
+ type: "tool-call",
29292
+ toolCallId,
29293
+ toolName: "run_task",
29294
+ input: { instruction: r.jobTask ?? "" }
29295
+ }
29296
+ ]
29297
+ });
29298
+ messages.push({
29299
+ role: "tool",
29300
+ content: [
29301
+ {
29302
+ type: "tool-result",
29303
+ toolCallId,
29304
+ toolName: "run_task",
29305
+ output: { type: "text", value: "Task dispatched." }
29306
+ }
29307
+ ]
29308
+ });
29309
+ } else {
29310
+ messages.push({ role: r.role, content: r.content });
29311
+ }
29312
+ }
28821
29313
  let text22 = "";
28822
29314
  let runTask;
28823
29315
  try {
@@ -28828,7 +29320,23 @@ async function runChatTurn(opts) {
28828
29320
  });
28829
29321
  text22 = (response.text ?? "").trim();
28830
29322
  runTask = (response.toolCalls ?? []).find((tc) => tc.toolName === "run_task");
28831
- } catch {
29323
+ } catch (err) {
29324
+ console.warn(`[run-chat-turn] tools call failed (${agentRow.slug}):`, err.message);
29325
+ }
29326
+ if (!runTask && text22) {
29327
+ try {
29328
+ const recheck = await llmClient.generateText({
29329
+ system: systemPrompt,
29330
+ messages: [
29331
+ ...messages,
29332
+ { role: "assistant", content: text22 },
29333
+ { role: "user", content: ESCALATION_RECHECK }
29334
+ ],
29335
+ tools: CHAT_TOOLS
29336
+ });
29337
+ runTask = (recheck.toolCalls ?? []).find((tc) => tc.toolName === "run_task");
29338
+ } catch {
29339
+ }
28832
29340
  }
28833
29341
  if (runTask) {
28834
29342
  const instruction = String(runTask.input?.instruction ?? "").trim() || message;
@@ -28915,7 +29423,6 @@ async function seedDefaultLlmKey(db, env2) {
28915
29423
  apiKeyLast4: last4(plaintextKey),
28916
29424
  baseUrl: env2.LLM_BASE_URL ?? null,
28917
29425
  nickname: "Default (env)",
28918
- defaultModel: env2.LLM_MODEL,
28919
29426
  isActive: true
28920
29427
  }).returning({ id: entityLlmKeys.id });
28921
29428
  if (!newKey) return;
@@ -30710,7 +31217,7 @@ function createApp(deps, runnerEnv) {
30710
31217
  throw err;
30711
31218
  }
30712
31219
  });
30713
- app.use("/api/approve", async (c, next) => {
31220
+ const bearerOrSession = async (c, next) => {
30714
31221
  const auth2 = c.req.header("authorization") ?? "";
30715
31222
  const bearer = auth2.startsWith("Bearer ") ? auth2.slice(7) : null;
30716
31223
  if (bearer && runnerEnv.WORKER_SECRET && bearer === runnerEnv.WORKER_SECRET) {
@@ -30729,7 +31236,8 @@ function createApp(deps, runnerEnv) {
30729
31236
  }
30730
31237
  throw err;
30731
31238
  }
30732
- });
31239
+ };
31240
+ app.use("/api/approve", bearerOrSession);
30733
31241
  app.get("/api/health", (c) => healthRoute(c, deps));
30734
31242
  app.post("/api/agent", (c) => agentRoute(c, deps, runnerEnv));
30735
31243
  app.post("/api/worker", (c) => workerRoute(c, deps, runnerEnv));