@nocobase/app-plugin-agents 0.1.0-beta.1 → 0.1.0-beta.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 (278) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +44 -12
  3. package/dist/client/api/client.d.ts +9 -2
  4. package/dist/client/api/client.d.ts.map +1 -1
  5. package/dist/client/api/client.js +15 -3
  6. package/dist/client/api/client.js.map +1 -1
  7. package/dist/client/chat/message-model.d.ts.map +1 -1
  8. package/dist/client/chat/message-model.js +10 -0
  9. package/dist/client/chat/message-model.js.map +1 -1
  10. package/dist/client/chat/settings-section.d.ts.map +1 -1
  11. package/dist/client/chat/settings-section.js +29 -19
  12. package/dist/client/chat/settings-section.js.map +1 -1
  13. package/dist/client/components/model-entries.d.ts +12 -3
  14. package/dist/client/components/model-entries.d.ts.map +1 -1
  15. package/dist/client/components/model-entries.js +41 -19
  16. package/dist/client/components/model-entries.js.map +1 -1
  17. package/dist/client/components/variables/variable-dialogs.d.ts +6 -1
  18. package/dist/client/components/variables/variable-dialogs.d.ts.map +1 -1
  19. package/dist/client/components/variables/variable-dialogs.js +27 -11
  20. package/dist/client/components/variables/variable-dialogs.js.map +1 -1
  21. package/dist/client/components/variables/variables-panel.d.ts.map +1 -1
  22. package/dist/client/components/variables/variables-panel.js +9 -2
  23. package/dist/client/components/variables/variables-panel.js.map +1 -1
  24. package/dist/client/lib/agents.d.ts +5 -0
  25. package/dist/client/lib/agents.d.ts.map +1 -1
  26. package/dist/client/lib/agents.js +23 -0
  27. package/dist/client/lib/agents.js.map +1 -1
  28. package/dist/client/lib/model-suggestions.d.ts +28 -0
  29. package/dist/client/lib/model-suggestions.d.ts.map +1 -0
  30. package/dist/client/lib/model-suggestions.js +44 -0
  31. package/dist/client/lib/model-suggestions.js.map +1 -0
  32. package/dist/client/locales/chat.en-US.d.ts +7 -0
  33. package/dist/client/locales/chat.en-US.d.ts.map +1 -1
  34. package/dist/client/locales/chat.en-US.js +8 -1
  35. package/dist/client/locales/chat.en-US.js.map +1 -1
  36. package/dist/client/locales/chat.zh-CN.d.ts.map +1 -1
  37. package/dist/client/locales/chat.zh-CN.js +8 -1
  38. package/dist/client/locales/chat.zh-CN.js.map +1 -1
  39. package/dist/client/locales/en-US.d.ts +20 -1
  40. package/dist/client/locales/en-US.d.ts.map +1 -1
  41. package/dist/client/locales/en-US.js +21 -1
  42. package/dist/client/locales/en-US.js.map +1 -1
  43. package/dist/client/locales/runWait.en-US.d.ts +31 -0
  44. package/dist/client/locales/runWait.en-US.d.ts.map +1 -0
  45. package/dist/client/locales/runWait.en-US.js +30 -0
  46. package/dist/client/locales/runWait.en-US.js.map +1 -0
  47. package/dist/client/locales/runWait.zh-CN.d.ts +5 -0
  48. package/dist/client/locales/runWait.zh-CN.d.ts.map +1 -0
  49. package/dist/client/locales/runWait.zh-CN.js +25 -0
  50. package/dist/client/locales/runWait.zh-CN.js.map +1 -0
  51. package/dist/client/locales/runtimes.en-US.d.ts +6 -0
  52. package/dist/client/locales/runtimes.en-US.d.ts.map +1 -1
  53. package/dist/client/locales/runtimes.en-US.js +7 -1
  54. package/dist/client/locales/runtimes.en-US.js.map +1 -1
  55. package/dist/client/locales/runtimes.zh-CN.d.ts.map +1 -1
  56. package/dist/client/locales/runtimes.zh-CN.js +7 -1
  57. package/dist/client/locales/runtimes.zh-CN.js.map +1 -1
  58. package/dist/client/locales/zh-CN.d.ts.map +1 -1
  59. package/dist/client/locales/zh-CN.js +21 -1
  60. package/dist/client/locales/zh-CN.js.map +1 -1
  61. package/dist/client/pages/agents/index.d.ts.map +1 -1
  62. package/dist/client/pages/agents/index.js +6 -6
  63. package/dist/client/pages/agents/index.js.map +1 -1
  64. package/dist/client/pages/runtimes/index.js +2 -2
  65. package/dist/client/pages/runtimes/index.js.map +1 -1
  66. package/dist/client/pages/runtimes/runner-sheet.js +11 -3
  67. package/dist/client/pages/runtimes/runner-sheet.js.map +1 -1
  68. package/dist/client/runs/format-wait.d.ts +21 -0
  69. package/dist/client/runs/format-wait.d.ts.map +1 -0
  70. package/dist/client/runs/format-wait.js +76 -0
  71. package/dist/client/runs/format-wait.js.map +1 -0
  72. package/dist/client/runs.d.ts +2 -0
  73. package/dist/client/runs.d.ts.map +1 -1
  74. package/dist/client/runs.js +2 -0
  75. package/dist/client/runs.js.map +1 -1
  76. package/dist/database/migrations/.manifest.json +20 -0
  77. package/dist/database/migrations/202610090001_ag_add_team_runner_variables.d.ts +9 -0
  78. package/dist/database/migrations/202610090001_ag_add_team_runner_variables.d.ts.map +1 -0
  79. package/dist/database/migrations/202610090001_ag_add_team_runner_variables.js +28 -0
  80. package/dist/database/migrations/202610090001_ag_add_team_runner_variables.js.map +1 -0
  81. package/dist/database/migrations/202610090002_ag_create_run_requests.d.ts +10 -0
  82. package/dist/database/migrations/202610090002_ag_create_run_requests.d.ts.map +1 -0
  83. package/dist/database/migrations/202610090002_ag_create_run_requests.js +105 -0
  84. package/dist/database/migrations/202610090002_ag_create_run_requests.js.map +1 -0
  85. package/dist/database/migrations/202610090003_ag_add_execution_history.d.ts +4 -0
  86. package/dist/database/migrations/202610090003_ag_add_execution_history.d.ts.map +1 -0
  87. package/dist/database/migrations/202610090003_ag_add_execution_history.js +17 -0
  88. package/dist/database/migrations/202610090003_ag_add_execution_history.js.map +1 -0
  89. package/dist/database/migrations/202610090004_ag_add_runner_workspace_usage.d.ts +9 -0
  90. package/dist/database/migrations/202610090004_ag_add_runner_workspace_usage.d.ts.map +1 -0
  91. package/dist/database/migrations/202610090004_ag_add_runner_workspace_usage.js +22 -0
  92. package/dist/database/migrations/202610090004_ag_add_runner_workspace_usage.js.map +1 -0
  93. package/dist/database/migrations/202610090005_ag_add_runner_variables.d.ts +10 -0
  94. package/dist/database/migrations/202610090005_ag_add_runner_variables.d.ts.map +1 -0
  95. package/dist/database/migrations/202610090005_ag_add_runner_variables.js +36 -0
  96. package/dist/database/migrations/202610090005_ag_add_runner_variables.js.map +1 -0
  97. package/dist/package.json +1 -1
  98. package/dist/server/composition.d.ts +12 -2
  99. package/dist/server/composition.d.ts.map +1 -1
  100. package/dist/server/composition.js +16 -3
  101. package/dist/server/composition.js.map +1 -1
  102. package/dist/server/core/agents/agent.service.d.ts.map +1 -1
  103. package/dist/server/core/agents/agent.service.js +4 -1
  104. package/dist/server/core/agents/agent.service.js.map +1 -1
  105. package/dist/server/core/consultations/consultation.service.d.ts.map +1 -1
  106. package/dist/server/core/consultations/consultation.service.js +5 -2
  107. package/dist/server/core/consultations/consultation.service.js.map +1 -1
  108. package/dist/server/core/conversations/availability.d.ts +10 -4
  109. package/dist/server/core/conversations/availability.d.ts.map +1 -1
  110. package/dist/server/core/conversations/availability.js +6 -20
  111. package/dist/server/core/conversations/availability.js.map +1 -1
  112. package/dist/server/core/conversations/chat-settings.d.ts +2 -2
  113. package/dist/server/core/conversations/chat-settings.d.ts.map +1 -1
  114. package/dist/server/core/conversations/chat-settings.js +19 -1
  115. package/dist/server/core/conversations/chat-settings.js.map +1 -1
  116. package/dist/server/core/conversations/conversation.service.d.ts +2 -1
  117. package/dist/server/core/conversations/conversation.service.d.ts.map +1 -1
  118. package/dist/server/core/conversations/conversation.service.js +255 -57
  119. package/dist/server/core/conversations/conversation.service.js.map +1 -1
  120. package/dist/server/core/runs/availability.d.ts +5 -2
  121. package/dist/server/core/runs/availability.d.ts.map +1 -1
  122. package/dist/server/core/runs/availability.js +17 -6
  123. package/dist/server/core/runs/availability.js.map +1 -1
  124. package/dist/server/core/runs/claim.d.ts +16 -2
  125. package/dist/server/core/runs/claim.d.ts.map +1 -1
  126. package/dist/server/core/runs/claim.js +220 -64
  127. package/dist/server/core/runs/claim.js.map +1 -1
  128. package/dist/server/core/runs/eligibility.d.ts +49 -0
  129. package/dist/server/core/runs/eligibility.d.ts.map +1 -0
  130. package/dist/server/core/runs/eligibility.js +44 -0
  131. package/dist/server/core/runs/eligibility.js.map +1 -0
  132. package/dist/server/core/runs/execution-view.d.ts +10 -0
  133. package/dist/server/core/runs/execution-view.d.ts.map +1 -0
  134. package/dist/server/core/runs/execution-view.js +25 -0
  135. package/dist/server/core/runs/execution-view.js.map +1 -0
  136. package/dist/server/core/runs/execution.d.ts +13 -0
  137. package/dist/server/core/runs/execution.d.ts.map +1 -0
  138. package/dist/server/core/runs/execution.js +85 -0
  139. package/dist/server/core/runs/execution.js.map +1 -0
  140. package/dist/server/core/runs/extensions.d.ts +2 -0
  141. package/dist/server/core/runs/extensions.d.ts.map +1 -1
  142. package/dist/server/core/runs/extensions.js.map +1 -1
  143. package/dist/server/core/runs/index.d.ts +6 -3
  144. package/dist/server/core/runs/index.d.ts.map +1 -1
  145. package/dist/server/core/runs/index.js +5 -2
  146. package/dist/server/core/runs/index.js.map +1 -1
  147. package/dist/server/core/runs/lifecycle.d.ts.map +1 -1
  148. package/dist/server/core/runs/lifecycle.js +29 -2
  149. package/dist/server/core/runs/lifecycle.js.map +1 -1
  150. package/dist/server/core/runs/ports.d.ts +20 -0
  151. package/dist/server/core/runs/ports.d.ts.map +1 -1
  152. package/dist/server/core/runs/ports.js.map +1 -1
  153. package/dist/server/core/runs/preview.d.ts.map +1 -1
  154. package/dist/server/core/runs/preview.js +2 -0
  155. package/dist/server/core/runs/preview.js.map +1 -1
  156. package/dist/server/core/runs/run-requests.d.ts +217 -0
  157. package/dist/server/core/runs/run-requests.d.ts.map +1 -0
  158. package/dist/server/core/runs/run-requests.js +471 -0
  159. package/dist/server/core/runs/run-requests.js.map +1 -0
  160. package/dist/server/core/runs/run.service.d.ts +63 -7
  161. package/dist/server/core/runs/run.service.d.ts.map +1 -1
  162. package/dist/server/core/runs/run.service.js +211 -37
  163. package/dist/server/core/runs/run.service.js.map +1 -1
  164. package/dist/server/core/runs/run.store.d.ts +14 -3
  165. package/dist/server/core/runs/run.store.d.ts.map +1 -1
  166. package/dist/server/core/runs/run.store.js +41 -2
  167. package/dist/server/core/runs/run.store.js.map +1 -1
  168. package/dist/server/core/runs/runner-view.d.ts +4 -3
  169. package/dist/server/core/runs/runner-view.d.ts.map +1 -1
  170. package/dist/server/core/runs/runner-view.js +7 -3
  171. package/dist/server/core/runs/runner-view.js.map +1 -1
  172. package/dist/server/core/runs/transitions.d.ts.map +1 -1
  173. package/dist/server/core/runs/transitions.js +4 -0
  174. package/dist/server/core/runs/transitions.js.map +1 -1
  175. package/dist/server/core/runs/workload.d.ts +1 -1
  176. package/dist/server/core/runs/workload.d.ts.map +1 -1
  177. package/dist/server/core/runs/workload.js +44 -13
  178. package/dist/server/core/runs/workload.js.map +1 -1
  179. package/dist/server/core/variables/index.d.ts +1 -1
  180. package/dist/server/core/variables/index.d.ts.map +1 -1
  181. package/dist/server/core/variables/index.js +1 -1
  182. package/dist/server/core/variables/index.js.map +1 -1
  183. package/dist/server/core/variables/variable.service.d.ts +38 -6
  184. package/dist/server/core/variables/variable.service.d.ts.map +1 -1
  185. package/dist/server/core/variables/variable.service.js +201 -30
  186. package/dist/server/core/variables/variable.service.js.map +1 -1
  187. package/dist/server/jobs/index.d.ts +1 -1
  188. package/dist/server/jobs/index.d.ts.map +1 -1
  189. package/dist/server/jobs/index.js +1 -1
  190. package/dist/server/jobs/index.js.map +1 -1
  191. package/dist/server/jobs/job.service.d.ts +2 -0
  192. package/dist/server/jobs/job.service.d.ts.map +1 -1
  193. package/dist/server/jobs/job.service.js +3 -1
  194. package/dist/server/jobs/job.service.js.map +1 -1
  195. package/dist/server/jobs/spec.d.ts +4 -0
  196. package/dist/server/jobs/spec.d.ts.map +1 -1
  197. package/dist/server/jobs/spec.js +6 -0
  198. package/dist/server/jobs/spec.js.map +1 -1
  199. package/dist/server/kernel/errors.d.ts +1 -1
  200. package/dist/server/kernel/errors.d.ts.map +1 -1
  201. package/dist/server/kernel/errors.js +1 -0
  202. package/dist/server/kernel/errors.js.map +1 -1
  203. package/dist/server/kernel/events.d.ts +28 -8
  204. package/dist/server/kernel/events.d.ts.map +1 -1
  205. package/dist/server/kernel/events.js.map +1 -1
  206. package/dist/server/routes/admin.d.ts.map +1 -1
  207. package/dist/server/routes/admin.js +38 -7
  208. package/dist/server/routes/admin.js.map +1 -1
  209. package/dist/server/routes/chat.d.ts.map +1 -1
  210. package/dist/server/routes/chat.js +9 -6
  211. package/dist/server/routes/chat.js.map +1 -1
  212. package/dist/server/routes/index.d.ts.map +1 -1
  213. package/dist/server/routes/index.js +11 -1
  214. package/dist/server/routes/index.js.map +1 -1
  215. package/dist/server/routes/roster.d.ts.map +1 -1
  216. package/dist/server/routes/roster.js +2 -1
  217. package/dist/server/routes/roster.js.map +1 -1
  218. package/dist/server/routes/run-requests.d.ts +6 -0
  219. package/dist/server/routes/run-requests.d.ts.map +1 -0
  220. package/dist/server/routes/run-requests.js +216 -0
  221. package/dist/server/routes/run-requests.js.map +1 -0
  222. package/dist/server/routes/runners/admin.d.ts +9 -1
  223. package/dist/server/routes/runners/admin.d.ts.map +1 -1
  224. package/dist/server/routes/runners/admin.js +62 -24
  225. package/dist/server/routes/runners/admin.js.map +1 -1
  226. package/dist/server/routes/runners/runner.d.ts.map +1 -1
  227. package/dist/server/routes/runners/runner.js +21 -3
  228. package/dist/server/routes/runners/runner.js.map +1 -1
  229. package/dist/server/routes/schemas.d.ts +31 -3
  230. package/dist/server/routes/schemas.d.ts.map +1 -1
  231. package/dist/server/routes/schemas.js +233 -3
  232. package/dist/server/routes/schemas.js.map +1 -1
  233. package/dist/server/runners/index.d.ts +1 -0
  234. package/dist/server/runners/index.d.ts.map +1 -1
  235. package/dist/server/runners/index.js +1 -0
  236. package/dist/server/runners/index.js.map +1 -1
  237. package/dist/server/runners/runner.service.d.ts +10 -5
  238. package/dist/server/runners/runner.service.d.ts.map +1 -1
  239. package/dist/server/runners/runner.service.js +35 -10
  240. package/dist/server/runners/runner.service.js.map +1 -1
  241. package/dist/server/runners/runner.store.d.ts +7 -1
  242. package/dist/server/runners/runner.store.d.ts.map +1 -1
  243. package/dist/server/runners/runner.store.js +69 -4
  244. package/dist/server/runners/runner.store.js.map +1 -1
  245. package/dist/server/runners/sweeper.d.ts +5 -0
  246. package/dist/server/runners/sweeper.d.ts.map +1 -1
  247. package/dist/server/runners/sweeper.js +4 -2
  248. package/dist/server/runners/sweeper.js.map +1 -1
  249. package/dist/server/runners/workspaces.d.ts +30 -0
  250. package/dist/server/runners/workspaces.d.ts.map +1 -0
  251. package/dist/server/runners/workspaces.js +135 -0
  252. package/dist/server/runners/workspaces.js.map +1 -0
  253. package/dist/server/tokens.d.ts +4 -2
  254. package/dist/server/tokens.d.ts.map +1 -1
  255. package/dist/server/tokens.js +2 -0
  256. package/dist/server/tokens.js.map +1 -1
  257. package/dist/shared/agents.d.ts +2 -0
  258. package/dist/shared/agents.d.ts.map +1 -1
  259. package/dist/shared/agents.js.map +1 -1
  260. package/dist/shared/conversations.d.ts +48 -13
  261. package/dist/shared/conversations.d.ts.map +1 -1
  262. package/dist/shared/conversations.js +2 -2
  263. package/dist/shared/conversations.js.map +1 -1
  264. package/dist/shared/runners.d.ts +56 -5
  265. package/dist/shared/runners.d.ts.map +1 -1
  266. package/dist/shared/runners.js.map +1 -1
  267. package/dist/shared/runs.d.ts +145 -1
  268. package/dist/shared/runs.d.ts.map +1 -1
  269. package/dist/shared/runs.js +18 -0
  270. package/dist/shared/runs.js.map +1 -1
  271. package/dist/shared/variables.d.ts +22 -0
  272. package/dist/shared/variables.d.ts.map +1 -1
  273. package/dist/shared/variables.js +9 -1
  274. package/dist/shared/variables.js.map +1 -1
  275. package/package.json +8 -8
  276. package/skills/nocobase-app-plugin-agents/references/extension-points.md +4 -0
  277. package/skills/nocobase-app-plugin-agents/references/runners.md +9 -1
  278. package/skills/nocobase-app-plugin-agents/references/verification-and-troubleshooting.md +1 -1
@@ -6,7 +6,7 @@ import { sampleValue } from '../../../shared/briefs.js';
6
6
  import { forbidden, invalid, notFound, ProtocolError, } from '../../kernel/errors.js';
7
7
  import { asJson } from '../../kernel/values.js';
8
8
  import { findAgent } from '../agents/index.js';
9
- import { eventsAfter, hasSession, lastSummary, onlineEntryOf, openRunsOnEach, } from '../runs/index.js';
9
+ import { eventsAfter, hasSession, lastSummary, onlineEntryOf, openRunsOnEach, queuedRun, } from '../runs/index.js';
10
10
  import { findRunRecord, toRun } from '../runs/run.store.js';
11
11
  import { readAttachmentIds, storedAttachments, } from './attachments.js';
12
12
  import { availabilityOf, wakeable } from './availability.js';
@@ -76,6 +76,10 @@ function noticeText(notice, names) {
76
76
  switch (notice.code) {
77
77
  case 'switchedToDefault':
78
78
  return `This conversation now uses ${name(notice.agentId)}, the default agent, until you switch back to ${name(notice.fromAgentId)}.`;
79
+ case 'switchedToOnline':
80
+ return `${name(notice.fromAgentId)} has no runner for you now, so ${name(notice.agentId)} answers until you switch back.`;
81
+ case 'onlineFallbackUnavailable':
82
+ return `The online fallback agent cannot answer (${notice.reason}). This conversation stays with ${name(notice.fromAgentId)} while it waits for a runner.`;
79
83
  case 'switchedBack':
80
84
  return `This conversation is back with ${name(notice.agentId)}.`;
81
85
  case 'runFailed':
@@ -190,25 +194,81 @@ export function createConversationService(deps) {
190
194
  const agent = await findAgent(conn, defaultAgentId);
191
195
  return wakeable(agent, userId, deps.agents.mayInvoke) ? agent : null;
192
196
  }
193
- async function detailOf(conn, record, runners) {
197
+ /** The configured online agent and whether its default answering model is offered now. */
198
+ async function onlineFallback(conn, userId, catalog, settings) {
199
+ const { onlineFallbackAgentId } = settings ?? (await deps.settings.settings(conn));
200
+ if (!onlineFallbackAgentId)
201
+ return null;
202
+ const agent = await findAgent(conn, onlineFallbackAgentId);
203
+ const online = agent?.type === 'online' ? agent : null;
204
+ return {
205
+ agent: online,
206
+ availability: availabilityOf(online, userId, deps.agents.mayInvoke, 0, catalog),
207
+ };
208
+ }
209
+ /** Reads the same features and variable restrictions as claim, before any write transaction. */
210
+ async function availabilityFor(conn, agent, userId, catalog, wanted, runners) {
211
+ const eligible = agent?.type === 'runner' && wakeable(agent, userId, deps.agents.mayInvoke)
212
+ ? await deps.eligibility.runnersFor(conn, agent, {
213
+ actorUserId: userId,
214
+ ...(runners ? { runners } : {}),
215
+ })
216
+ : [];
217
+ return availabilityOf(agent, userId, deps.agents.mayInvoke, eligible.length, catalog, wanted);
218
+ }
219
+ async function canAnswer(conn, agent, userId, catalog) {
220
+ if (!wakeable(agent, userId, deps.agents.mayInvoke))
221
+ return false;
222
+ return agent.type === 'runner'
223
+ ? deps.eligibility.canClaim(conn, agent, { actorUserId: userId })
224
+ : availabilityOf(agent, userId, deps.agents.mayInvoke, 0, catalog).online;
225
+ }
226
+ async function automaticFallback(conn, agent, userId, catalog, settings) {
227
+ const current = settings ?? (await deps.settings.settings(conn));
228
+ if (!current.onlineFallbackAgentId ||
229
+ agent.type !== 'runner' ||
230
+ !wakeable(agent, userId, deps.agents.mayInvoke))
231
+ return null;
232
+ if (await deps.eligibility.canClaim(conn, agent, { actorUserId: userId }))
233
+ return null;
234
+ return onlineFallback(conn, userId, catalog, current);
235
+ }
236
+ /**
237
+ * The agent `fallback` switches the conversation to: an available system default of the conversation's mode, else,
238
+ * for a runner conversation, the online fallback agent. Null while it already uses another agent in place of its own.
239
+ */
240
+ async function fallbackTarget(conn, record, catalog) {
241
+ if (record.fallbackFromAgentId)
242
+ return null;
243
+ const system = await systemDefault(conn, record.userId);
244
+ if (system &&
245
+ system.id !== record.agentId &&
246
+ system.type === record.mode &&
247
+ (await canAnswer(conn, system, record.userId, catalog)))
248
+ return system;
249
+ if (record.mode !== 'runner')
250
+ return null;
251
+ const online = await onlineFallback(conn, record.userId, catalog);
252
+ return online?.availability.online && online.agent?.id !== record.agentId
253
+ ? online.agent
254
+ : null;
255
+ }
256
+ async function detailOf(conn, record) {
194
257
  const catalog = await deps.models.catalog();
195
258
  const [summary] = await summaries(conn, [record], catalog.defaultModel);
196
259
  const agent = await findAgent(conn, record.agentId);
197
260
  const fallbackFrom = record.fallbackFromAgentId
198
261
  ? await findAgent(conn, record.fallbackFromAgentId)
199
262
  : null;
200
- const system = await systemDefault(conn, record.userId);
263
+ const target = await fallbackTarget(conn, record, catalog);
201
264
  return {
202
265
  ...summary,
203
- availability: availabilityOf(agent, record.userId, deps.agents.mayInvoke, runners ?? (await deps.runners.list()), catalog, chosenModel(record)),
266
+ availability: await availabilityFor(conn, agent, record.userId, catalog, chosenModel(record)),
204
267
  models: record.mode === 'online' && agent
205
268
  ? chatModelChoices(onlineEntriesOrDefault(agent, catalog.defaultModel ?? null), catalog)
206
269
  : [],
207
- canFallback: Boolean(system &&
208
- !record.fallbackFromAgentId &&
209
- system.id !== record.agentId &&
210
- system.type === record.mode),
211
- canRestore: wakeable(fallbackFrom, record.userId, deps.agents.mayInvoke),
270
+ canFallback: Boolean(target),
271
+ canRestore: await canAnswer(conn, fallbackFrom, record.userId, catalog),
212
272
  };
213
273
  }
214
274
  async function detail(userId, id) {
@@ -232,9 +292,13 @@ export function createConversationService(deps) {
232
292
  return appended;
233
293
  }
234
294
  async function notice(unit, record, value, runId = null) {
235
- const agentIds = value.code === 'switchedToDefault' || value.code === 'switchedBack'
295
+ const agentIds = value.code === 'switchedToDefault' ||
296
+ value.code === 'switchedToOnline' ||
297
+ value.code === 'switchedBack'
236
298
  ? [value.agentId, value.fromAgentId]
237
- : [];
299
+ : value.code === 'onlineFallbackUnavailable'
300
+ ? [value.fromAgentId]
301
+ : [];
238
302
  const names = new Map();
239
303
  for (const id of agentIds) {
240
304
  const agent = await findAgent(unit.conn, id);
@@ -251,6 +315,38 @@ export function createConversationService(deps) {
251
315
  async function nameOf(conn, userId) {
252
316
  return (await deps.people.names(conn, [userId])).get(userId) ?? userId;
253
317
  }
318
+ /** Remember the last fallback failure in the transcript, across requests and application restarts. */
319
+ async function fallbackFailureNotice(unit, record, value) {
320
+ const previous = await messagesRepo(unit.conn).findOne({
321
+ filter: (f) => f.and([
322
+ f.string('conversationId').eq(record.id),
323
+ f.string('role').eq('system'),
324
+ f
325
+ .json('metadata')
326
+ .path(['notice', 'code'])
327
+ .eq('onlineFallbackUnavailable'),
328
+ ]),
329
+ sort: (sort) => sort.field('seq').desc(),
330
+ });
331
+ const metadata = previous ? metadataOf(previous) : null;
332
+ const last = metadata?.notice;
333
+ if (value &&
334
+ !metadata?.onlineFallbackResolved &&
335
+ last?.code === 'onlineFallbackUnavailable' &&
336
+ last.fromAgentId === value.fromAgentId &&
337
+ last.reason === value.reason)
338
+ return;
339
+ if (previous && !metadata?.onlineFallbackResolved)
340
+ await messagesRepo(unit.conn).updateMany({
341
+ filter: { id: previous.id },
342
+ values: {
343
+ metadata: asJson({ ...metadata, onlineFallbackResolved: true }),
344
+ updatedAt: now(),
345
+ },
346
+ });
347
+ if (value)
348
+ await notice(unit, record, value);
349
+ }
254
350
  /** The run input a user message becomes: its text, its files, and its page context marked as data. */
255
351
  function inputText(message) {
256
352
  const view = toMessage(message);
@@ -272,7 +368,7 @@ export function createConversationService(deps) {
272
368
  let result = null;
273
369
  for (const message of messages) {
274
370
  const files = storedAttachments(message.attachments);
275
- const enqueued = await deps.runs.enqueue({
371
+ const enqueued = queuedRun(await deps.runs.enqueue({
276
372
  agentId: agent.id,
277
373
  subject: subjectOf(record.id),
278
374
  threadScope: threadScopeOf(record),
@@ -293,22 +389,27 @@ export function createConversationService(deps) {
293
389
  : {}),
294
390
  },
295
391
  },
296
- }, unit);
392
+ }, unit));
297
393
  await updateMessageRun(unit.conn, message, enqueued.runId, { inputId: enqueued.inputId }, now());
298
394
  result = { id: enqueued.runId, outcome: enqueued.outcome };
299
395
  }
300
396
  return result;
301
397
  }
302
398
  /**
303
- * Moves the conversation to `agentId`: its queued work is withdrawn, the thread moves on (a new session), and the
304
- * messages nobody answered go to the new agent.
399
+ * Moves the conversation to `agent`: its queued work is withdrawn, the thread moves on (a new session), and the
400
+ * messages nobody answered go to the new agent. The mode follows the agent's type; a model chosen for an online
401
+ * conversation is dropped when it changes.
305
402
  */
306
- async function rebind(unit, record, agentId, fallbackFromAgentId, value) {
403
+ async function rebind(unit, record, agent, fallbackFromAgentId, value) {
307
404
  const withdrawn = await deps.runs.withdrawQueued({ subject: subjectOf(record.id), byUserId: record.userId }, unit);
308
405
  await conversationsRepo(unit.conn).updateMany({
309
406
  filter: { id: record.id },
310
407
  values: {
311
- agentId,
408
+ agentId: agent.id,
409
+ mode: agent.type,
410
+ ...(agent.type === record.mode
411
+ ? {}
412
+ : { modelService: null, model: null }),
312
413
  fallbackFromAgentId,
313
414
  thread: Number(record.thread) + 1,
314
415
  updatedAt: now(),
@@ -628,13 +729,51 @@ export function createConversationService(deps) {
628
729
  throw invalid('This application takes no files in chat.', {
629
730
  field: 'attachmentIds',
630
731
  });
631
- await owned(tx.read(), userId, id);
732
+ const before = await owned(tx.read(), userId, id);
733
+ const agent = before.mode === 'runner'
734
+ ? await findAgent(tx.read(), before.agentId)
735
+ : null;
736
+ const replacement = agent
737
+ ? await automaticFallback(tx.read(), agent, userId, await deps.models.catalog())
738
+ : null;
632
739
  // Resolved before the transaction: resolvers read through the services of the plugins that own the kinds.
633
740
  const context = request.context
634
741
  ? await deps.contextKinds.resolve(tx.read(), userId, request.context)
635
742
  : null;
636
743
  const sent = await tx.run(async (unit) => {
637
- const record = await lockOwned(unit, userId, id);
744
+ let record = await lockOwned(unit, userId, id);
745
+ let fallbackFailure = null;
746
+ // A concurrent switch owns its new binding; do not apply a decision made for the previous one.
747
+ if (replacement &&
748
+ record.agentId === before.agentId &&
749
+ record.thread === before.thread &&
750
+ record.mode === 'runner' &&
751
+ record.fallbackFromAgentId === before.fallbackFromAgentId) {
752
+ const current = await findAgent(unit.conn, record.agentId);
753
+ const target = replacement.agent
754
+ ? await findAgent(unit.conn, replacement.agent.id)
755
+ : null;
756
+ if (current?.revision === agent?.revision &&
757
+ replacement.availability.online &&
758
+ wakeable(target, userId, deps.agents.mayInvoke) &&
759
+ target.revision === replacement.agent?.revision) {
760
+ await rebind(unit, record, target, record.fallbackFromAgentId ?? record.agentId, {
761
+ code: 'switchedToOnline',
762
+ agentId: target.id,
763
+ fromAgentId: record.agentId,
764
+ });
765
+ record = (await findConversation(unit.conn, id));
766
+ }
767
+ else if (current?.revision === agent?.revision &&
768
+ replacement.availability.reason) {
769
+ fallbackFailure = {
770
+ code: 'onlineFallbackUnavailable',
771
+ fromAgentId: record.agentId,
772
+ reason: replacement.availability.reason,
773
+ };
774
+ }
775
+ }
776
+ await fallbackFailureNotice(unit, record, fallbackFailure);
638
777
  const at = now();
639
778
  if (record.archivedAt || (record.titleSource === 'auto' && !record.title))
640
779
  await conversationsRepo(unit.conn).updateMany({
@@ -742,11 +881,26 @@ export function createConversationService(deps) {
742
881
  async create(userId, request) {
743
882
  const wanted = request.model ?? null;
744
883
  // Read before the transaction, which a read on another connection would wait for.
745
- const fallback = wanted
746
- ? ((await deps.models.catalog()).defaultModel ?? null)
884
+ const catalog = await deps.models.catalog();
885
+ const fallback = catalog.defaultModel ?? null;
886
+ const settings = await deps.settings.settings();
887
+ const picked = await pickAgent(tx.read(), userId, request.agentId);
888
+ const replacement = !wanted
889
+ ? await automaticFallback(tx.read(), picked, userId, catalog, settings)
747
890
  : null;
748
891
  const id = await tx.run(async (unit) => {
749
- const agent = await pickAgent(unit.conn, userId, request.agentId);
892
+ const current = await pickAgent(unit.conn, userId, request.agentId);
893
+ const target = replacement?.agent
894
+ ? await findAgent(unit.conn, replacement.agent.id)
895
+ : null;
896
+ const instead = current.id === picked.id &&
897
+ current.revision === picked.revision &&
898
+ replacement?.availability.online &&
899
+ wakeable(target, userId, deps.agents.mayInvoke) &&
900
+ target.revision === replacement.agent?.revision
901
+ ? target
902
+ : null;
903
+ const agent = instead ?? current;
750
904
  if (wanted &&
751
905
  (agent.type !== 'online' ||
752
906
  !onlineEntriesOrDefault(agent, fallback).some((entry) => sameEntry(entry, wanted))))
@@ -765,7 +919,7 @@ export function createConversationService(deps) {
765
919
  userId,
766
920
  agentId: agent.id,
767
921
  mode: agent.type,
768
- fallbackFromAgentId: null,
922
+ fallbackFromAgentId: instead ? picked.id : null,
769
923
  modelService: wanted?.modelService ?? null,
770
924
  model: wanted?.model ?? null,
771
925
  title,
@@ -784,7 +938,21 @@ export function createConversationService(deps) {
784
938
  updatedAt: at,
785
939
  },
786
940
  });
787
- changed(unit, (await findConversation(unit.conn, conversationId)));
941
+ const record = (await findConversation(unit.conn, conversationId));
942
+ if (instead)
943
+ await notice(unit, record, {
944
+ code: 'switchedToOnline',
945
+ agentId: instead.id,
946
+ fromAgentId: picked.id,
947
+ });
948
+ else if (replacement?.availability.reason)
949
+ await notice(unit, record, {
950
+ code: 'onlineFallbackUnavailable',
951
+ fromAgentId: picked.id,
952
+ reason: replacement.availability.reason,
953
+ });
954
+ else
955
+ changed(unit, record);
788
956
  return conversationId;
789
957
  });
790
958
  return detail(userId, id);
@@ -916,31 +1084,47 @@ export function createConversationService(deps) {
916
1084
  return detail(userId, id);
917
1085
  },
918
1086
  async fallback(userId, id) {
1087
+ const catalog = await deps.models.catalog();
1088
+ const before = await owned(tx.read(), userId, id);
1089
+ const target = await fallbackTarget(tx.read(), before, catalog);
919
1090
  await tx.run(async (unit) => {
920
1091
  const record = await lockOwned(unit, userId, id);
921
- const system = await systemDefault(unit.conn, userId);
922
- if (!system ||
923
- record.fallbackFromAgentId ||
924
- system.id === record.agentId ||
925
- system.type !== record.mode)
926
- throw conversationConflict('noFallback', 'This conversation cannot switch to the default agent.');
927
- await rebind(unit, record, system.id, record.agentId, {
928
- code: 'switchedToDefault',
929
- agentId: system.id,
1092
+ const current = target ? await findAgent(unit.conn, target.id) : null;
1093
+ if (!target ||
1094
+ !wakeable(current, userId, deps.agents.mayInvoke) ||
1095
+ current.revision !== target.revision ||
1096
+ record.agentId !== before.agentId ||
1097
+ record.thread !== before.thread ||
1098
+ record.fallbackFromAgentId !== before.fallbackFromAgentId)
1099
+ throw conversationConflict('noFallback', 'This conversation cannot switch to another agent.');
1100
+ await rebind(unit, record, current, record.agentId, {
1101
+ code: target.type === record.mode
1102
+ ? 'switchedToDefault'
1103
+ : 'switchedToOnline',
1104
+ agentId: target.id,
930
1105
  fromAgentId: record.agentId,
931
1106
  });
932
1107
  });
933
1108
  return detail(userId, id);
934
1109
  },
935
1110
  async restore(userId, id) {
1111
+ const before = await owned(tx.read(), userId, id);
1112
+ const own = before.fallbackFromAgentId
1113
+ ? await findAgent(tx.read(), before.fallbackFromAgentId)
1114
+ : null;
1115
+ const available = await canAnswer(tx.read(), own, userId, await deps.models.catalog());
936
1116
  await tx.run(async (unit) => {
937
1117
  const record = await lockOwned(unit, userId, id);
938
- const own = record.fallbackFromAgentId
939
- ? await findAgent(unit.conn, record.fallbackFromAgentId)
940
- : null;
941
- if (!own || !wakeable(own, userId, deps.agents.mayInvoke))
1118
+ const current = own ? await findAgent(unit.conn, own.id) : null;
1119
+ if (!own ||
1120
+ !available ||
1121
+ !wakeable(current, userId, deps.agents.mayInvoke) ||
1122
+ current.revision !== own.revision ||
1123
+ record.agentId !== before.agentId ||
1124
+ record.thread !== before.thread ||
1125
+ record.fallbackFromAgentId !== own.id)
942
1126
  throw conversationConflict('noRestore', 'This conversation cannot switch back to its own agent now.');
943
- await rebind(unit, record, own.id, null, {
1127
+ await rebind(unit, record, current, null, {
944
1128
  code: 'switchedBack',
945
1129
  agentId: own.id,
946
1130
  fromAgentId: record.agentId,
@@ -1039,14 +1223,14 @@ export function createConversationService(deps) {
1039
1223
  const agent = await findAgent(outer.conn, record.agentId);
1040
1224
  if (!wakeable(agent, record.userId, deps.agents.mayInvoke))
1041
1225
  return null;
1042
- const enqueued = await deps.runs.enqueue({
1226
+ const enqueued = queuedRun(await deps.runs.enqueue({
1043
1227
  agentId: agent.id,
1044
1228
  subject: subjectOf(record.id),
1045
1229
  threadScope: threadScopeOf(record),
1046
1230
  actorUserId: record.userId,
1047
1231
  ownerUserId: record.userId,
1048
1232
  input: news.input,
1049
- }, outer);
1233
+ }, outer));
1050
1234
  return { id: enqueued.runId, outcome: enqueued.outcome };
1051
1235
  },
1052
1236
  lock: async (conn, id) => (await lockConversation(conn, id, now())) !== null,
@@ -1096,28 +1280,42 @@ export function createConversationService(deps) {
1096
1280
  const conn = tx.read();
1097
1281
  const [agents, runners, settings, preferences, catalog] = await Promise.all([
1098
1282
  deps.agents.list(),
1099
- deps.runners.list(),
1283
+ deps.runners
1284
+ .list()
1285
+ .then((all) => all.filter((runner) => runner.status === 'online')),
1100
1286
  deps.settings.settings(conn),
1101
1287
  deps.settings.preferences(userId, conn),
1102
1288
  deps.models.catalog(),
1103
1289
  ]);
1104
- return agents
1290
+ const replacement = await onlineFallback(conn, userId, catalog, settings);
1291
+ const online = replacement?.availability.online
1292
+ ? replacement.agent
1293
+ : null;
1294
+ return Promise.all(agents
1105
1295
  .filter((agent) => deps.agents.mayInvoke(agent, userId))
1106
- .map((agent) => ({
1107
- id: agent.id,
1108
- name: agent.name,
1109
- description: agent.description,
1110
- nameText: agent.nameText,
1111
- descriptionText: agent.descriptionText,
1112
- avatar: agent.avatar,
1113
- type: agent.type,
1114
- models: agent.type === 'online'
1115
- ? chatModelChoices(onlineEntriesOrDefault(agent, catalog.defaultModel ?? null), catalog)
1116
- : [],
1117
- personal: agent.ownerUserId === userId && agent.access === 'ownerOnly',
1118
- isSystemDefault: agent.id === settings.defaultAgentId,
1119
- isMyDefault: agent.id === preferences.defaultAgentId,
1120
- availability: availabilityOf(agent, userId, deps.agents.mayInvoke, runners, catalog),
1296
+ .map(async (agent) => {
1297
+ const availability = await availabilityFor(conn, agent, userId, catalog, undefined, runners);
1298
+ return {
1299
+ id: agent.id,
1300
+ name: agent.name,
1301
+ description: agent.description,
1302
+ nameText: agent.nameText,
1303
+ descriptionText: agent.descriptionText,
1304
+ avatar: agent.avatar,
1305
+ type: agent.type,
1306
+ models: agent.type === 'online'
1307
+ ? chatModelChoices(onlineEntriesOrDefault(agent, catalog.defaultModel ?? null), catalog)
1308
+ : [],
1309
+ personal: agent.ownerUserId === userId && agent.access === 'ownerOnly',
1310
+ isSystemDefault: agent.id === settings.defaultAgentId,
1311
+ isMyDefault: agent.id === preferences.defaultAgentId,
1312
+ availability,
1313
+ fallbackAgentId: online &&
1314
+ agent.type === 'runner' &&
1315
+ availability.reason === 'noRunner'
1316
+ ? online.id
1317
+ : null,
1318
+ };
1121
1319
  }));
1122
1320
  },
1123
1321
  async copyAgent(userId, agentId, request) {