thursday-agent 0.3.0 → 0.5.0

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 (253) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/prerender-manifest.json +3 -3
  4. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/server/app/_not-found.html +1 -1
  13. package/.next/server/app/_not-found.rsc +2 -2
  14. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  15. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  17. package/.next/server/app/api/artifact/route.js +4 -4
  18. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  19. package/.next/server/app/api/bot/memory/files/route.js +3 -3
  20. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
  21. package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
  22. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  23. package/.next/server/app/api/bot/task/[id]/route.js +8 -8
  24. package/.next/server/app/api/bot/task/[id]/route.js.nft.json +1 -1
  25. package/.next/server/app/api/bot/task/route.js +6 -6
  26. package/.next/server/app/api/bot/task/route.js.nft.json +1 -1
  27. package/.next/server/app/api/config/route.js.nft.json +1 -1
  28. package/.next/server/app/api/events/route.js.nft.json +1 -1
  29. package/.next/server/app/api/file/[...path]/route.js +4 -4
  30. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  31. package/.next/server/app/api/llm-model/catalog/route.js +2 -2
  32. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  33. package/.next/server/app/api/llm-model/credits/route.js +2 -2
  34. package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
  35. package/.next/server/app/api/llm-model/route.js +2 -2
  36. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  37. package/.next/server/app/api/llm-model/subscription/route.js +2 -2
  38. package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
  39. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  40. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  41. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  42. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  43. package/.next/server/app/api/memory/edit/route.js +11 -11
  44. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
  45. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  46. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  47. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  48. package/.next/server/app/api/thursday/call/route.js +6 -6
  49. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  50. package/.next/server/app/api/thursday/skills/route.js +6 -6
  51. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  52. package/.next/server/app/api/thursday/tool-call/route.js +8 -8
  53. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  54. package/.next/server/app/api/workspace/route.js +3 -2
  55. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  56. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  57. package/.next/server/app/artifact/[...path]/page.js +2 -2
  58. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  59. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  60. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  61. package/.next/server/app/page/server-reference-manifest.json +40 -40
  62. package/.next/server/app/page.js +3 -3
  63. package/.next/server/app/page.js.nft.json +1 -1
  64. package/.next/server/app/page_client-reference-manifest.js +1 -1
  65. package/.next/server/chunks/[root-of-the-server]__026u77e._.js +5 -0
  66. package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +1 -1
  67. package/.next/server/chunks/{[root-of-the-server]__02xjoto._.js → [root-of-the-server]__050zri4._.js} +1 -1
  68. package/.next/server/chunks/[root-of-the-server]__0bg9710._.js +3 -0
  69. package/.next/server/chunks/[root-of-the-server]__0in-d5s._.js +3 -0
  70. package/.next/server/chunks/[root-of-the-server]__0ncs9uu._.js +1 -1
  71. package/.next/server/chunks/[root-of-the-server]__0pa9k-f._.js +8 -0
  72. package/.next/server/chunks/[root-of-the-server]__0qi_m0e._.js +8 -0
  73. package/.next/server/chunks/[root-of-the-server]__0tq0m_k._.js +3 -0
  74. package/.next/server/chunks/[root-of-the-server]__0xep6j_._.js +3 -0
  75. package/.next/server/chunks/[root-of-the-server]__1-ga0kh._.js +2 -2
  76. package/.next/server/chunks/[root-of-the-server]__144l7qt._.js +8 -0
  77. package/.next/server/chunks/[root-of-the-server]__1_04mcf._.js +2 -2
  78. package/.next/server/chunks/[root-of-the-server]__1anc1p9._.js +1 -1
  79. package/.next/server/chunks/[root-of-the-server]__1jg8t86._.js +8 -0
  80. package/.next/server/chunks/{[root-of-the-server]__12cabg9._.js → [root-of-the-server]__1lldzy9._.js} +1 -1
  81. package/.next/server/chunks/[root-of-the-server]__1orn-5b._.js +4 -3
  82. package/.next/server/chunks/{[root-of-the-server]__01dkp4a._.js → [root-of-the-server]__1peqvsi._.js} +1 -1
  83. package/.next/server/chunks/[root-of-the-server]__1w8gm5h._.js +10 -0
  84. package/.next/server/chunks/[root-of-the-server]__1xt39nn._.js +8 -0
  85. package/.next/server/chunks/[root-of-the-server]__1zb8mfo._.js +8 -0
  86. package/.next/server/chunks/_0-fjql4._.js +11 -6
  87. package/.next/server/chunks/_00xh_q0._.js +4 -6
  88. package/.next/server/chunks/_05h2cii._.js +1 -1
  89. package/.next/server/chunks/{_1_9mjx-._.js → _091bi8d._.js} +1 -1
  90. package/.next/server/chunks/_0fzqt2y._.js +57 -45
  91. package/.next/server/chunks/{_0u4aiy2._.js → _0lr92vd._.js} +1 -1
  92. package/.next/server/chunks/_0mecv0f._.js +11 -6
  93. package/.next/server/chunks/_0mh8j4m._.js +1 -1
  94. package/.next/server/chunks/{_0sbynis._.js → _0si7xzn._.js} +1 -1
  95. package/.next/server/chunks/_0wp4le3._.js +1 -1
  96. package/.next/server/chunks/_0y5fg-x._.js +1 -1
  97. package/.next/server/chunks/_1230wbx._.js +5 -5
  98. package/.next/server/chunks/_15hq89o._.js +1 -1
  99. package/.next/server/chunks/{_0obojvv._.js → _18d7-5p._.js} +1 -1
  100. package/.next/server/chunks/{_03kp0jp._.js → _1c78vw5._.js} +71 -61
  101. package/.next/server/chunks/_1d0j961._.js +2 -2
  102. package/.next/server/chunks/_1j8k_j5._.js +1 -1
  103. package/.next/server/chunks/_1krd9rf._.js +1 -1
  104. package/.next/server/chunks/_1php4eq._.js +1 -1
  105. package/.next/server/chunks/{_03a0897._.js → _1zkfbaa._.js} +1 -1
  106. package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
  107. package/.next/server/chunks/features_1qf3cei._.js +6 -1
  108. package/.next/server/chunks/features_memory_1uqvksn._.js +2 -2
  109. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0r28-sm._.js → node_modules_consola_dist_chunks_prompt_mjs_17k8irj._.js} +1 -1
  110. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_1xoq2rl._.js → node_modules_consola_dist_chunks_prompt_mjs_1jidylq._.js} +1 -1
  111. package/.next/server/chunks/ssr/[root-of-the-server]__0-nob52._.js +4 -0
  112. package/.next/server/chunks/ssr/[root-of-the-server]__092knlj._.js +3 -0
  113. package/.next/server/chunks/ssr/{[root-of-the-server]__0_69wqr._.js → [root-of-the-server]__0s4wb56._.js} +1 -1
  114. package/.next/server/chunks/ssr/{[root-of-the-server]__1eh7hid._.js → [root-of-the-server]__0s9dq5u._.js} +1 -1
  115. package/.next/server/chunks/ssr/{[root-of-the-server]__0zkv7z_._.js → [root-of-the-server]__0sd3izg._.js} +2 -2
  116. package/.next/server/chunks/ssr/{[root-of-the-server]__1gtlw6v._.js → [root-of-the-server]__1-13tcn._.js} +3 -3
  117. package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +13 -9
  118. package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
  119. package/.next/server/chunks/ssr/{[root-of-the-server]__1cyjywu._.js → [root-of-the-server]__1zidpfl._.js} +1 -1
  120. package/.next/server/chunks/ssr/_001_ms1._.js +6 -1
  121. package/.next/server/chunks/ssr/{_07uchnc._.js → _04z2c7b._.js} +2 -2
  122. package/.next/server/chunks/ssr/{_0kbkmar._.js → _06tssji._.js} +2 -2
  123. package/.next/server/chunks/ssr/_0fk4c7d._.js +3 -0
  124. package/.next/server/chunks/ssr/_0i_6q1w._.js +2 -2
  125. package/.next/server/chunks/ssr/{_1xvgn77._.js → _0m2lsd1._.js} +2 -2
  126. package/.next/server/chunks/ssr/_0mggveb._.js +3 -0
  127. package/.next/server/chunks/ssr/_0nbpc_8._.js +3 -0
  128. package/.next/server/chunks/ssr/{_0y0tiht._.js → _0rzucul._.js} +3 -3
  129. package/.next/server/chunks/ssr/{_0mb8zc8._.js → _0s99bgb._.js} +2 -2
  130. package/.next/server/chunks/ssr/_0zcjrbg._.js +3 -0
  131. package/.next/server/chunks/ssr/_1-teee5._.js +4 -4
  132. package/.next/server/chunks/ssr/{_0c-ql2m._.js → _11j92tu._.js} +2 -2
  133. package/.next/server/chunks/ssr/_13fke0x._.js +1 -1
  134. package/.next/server/chunks/ssr/{_01_6m32._.js → _1ebxla1._.js} +2 -2
  135. package/.next/server/chunks/ssr/_1jdgjv9._.js +3 -0
  136. package/.next/server/chunks/ssr/{_0u2jv_x._.js → _1lz_cq2._.js} +60 -50
  137. package/.next/server/chunks/ssr/{_20kd_n0._.js → _1nj4niv._.js} +3 -3
  138. package/.next/server/chunks/ssr/_1ovmc9s._.js +3 -0
  139. package/.next/server/chunks/ssr/_1sji22n._.js +1 -1
  140. package/.next/server/chunks/ssr/_1vyd1ji._.js +1 -1
  141. package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
  142. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  143. package/.next/server/chunks/ssr/{node_modules_1-hc6uv._.js → node_modules_0-dr_o7._.js} +1 -1
  144. package/.next/server/chunks/ssr/{node_modules_124bue5._.js → node_modules_04sksn9._.js} +1 -1
  145. package/.next/server/chunks/ssr/{node_modules_1uuj9x1._.js → node_modules_05-n6dz._.js} +1 -1
  146. package/.next/server/chunks/ssr/{node_modules_1i7u1ia._.js → node_modules_056qgf-._.js} +1 -1
  147. package/.next/server/chunks/ssr/{node_modules_0lm7_vm._.js → node_modules_07dsgkx._.js} +1 -1
  148. package/.next/server/chunks/ssr/{node_modules_1h4a4pp._.js → node_modules_0bvopy2._.js} +1 -1
  149. package/.next/server/chunks/ssr/{node_modules_1bc7gu8._.js → node_modules_0fkeku3._.js} +1 -1
  150. package/.next/server/chunks/ssr/{node_modules_0h50b8n._.js → node_modules_0ifp389._.js} +1 -1
  151. package/.next/server/chunks/ssr/{node_modules_0zp9r92._.js → node_modules_0iur59s._.js} +1 -1
  152. package/.next/server/chunks/ssr/{node_modules_063xwpp._.js → node_modules_0m383r2._.js} +1 -1
  153. package/.next/server/chunks/ssr/{node_modules_1bgb6-f._.js → node_modules_0u9da1h._.js} +1 -1
  154. package/.next/server/chunks/ssr/{node_modules_0bvraul._.js → node_modules_0zeg3x_._.js} +1 -1
  155. package/.next/server/chunks/ssr/{node_modules_1t9cyee._.js → node_modules_1-y7nni._.js} +1 -1
  156. package/.next/server/chunks/ssr/{node_modules_1_n3ztw._.js → node_modules_16mkxuo._.js} +1 -1
  157. package/.next/server/chunks/ssr/{node_modules_0eu1va6._.js → node_modules_1a0kmjl._.js} +1 -1
  158. package/.next/server/chunks/ssr/{node_modules_0z5tsu8._.js → node_modules_1chvq5f._.js} +1 -1
  159. package/.next/server/chunks/ssr/{node_modules_0h7u60n._.js → node_modules_1j8j4pt._.js} +1 -1
  160. package/.next/server/chunks/ssr/{node_modules_0rjg6t1._.js → node_modules_1k6n600._.js} +1 -1
  161. package/.next/server/chunks/ssr/{node_modules_1xx28h1._.js → node_modules_1k7z2j6._.js} +1 -1
  162. package/.next/server/chunks/ssr/{node_modules_0h5hnr1._.js → node_modules_1k8j8n0._.js} +1 -1
  163. package/.next/server/chunks/ssr/{node_modules_0mwym0g._.js → node_modules_1lz0oxc._.js} +1 -1
  164. package/.next/server/chunks/ssr/{node_modules_1vqb6po._.js → node_modules_1t77e4n._.js} +1 -1
  165. package/.next/server/chunks/ssr/{node_modules_1l0qtdu._.js → node_modules_1uyk91w._.js} +1 -1
  166. package/.next/server/chunks/ssr/{node_modules_0y_cded._.js → node_modules_1x0ztf4._.js} +1 -1
  167. package/.next/server/chunks/ssr/{node_modules_113tz0l._.js → node_modules_1y79i83._.js} +1 -1
  168. package/.next/server/chunks/ssr/{node_modules_1juq3hs._.js → node_modules_20c28js._.js} +1 -1
  169. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0d3skpl._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0bkwd0u._.js} +1 -1
  170. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0i44kk5._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0p9a9su._.js} +1 -1
  171. package/.next/server/chunks/ssr/{node_modules_@vercel_oidc_dist_0qe30a_._.js → node_modules_@vercel_oidc_dist_1o553h-._.js} +1 -1
  172. package/.next/server/middleware-build-manifest.js +3 -3
  173. package/.next/server/pages/404.html +1 -1
  174. package/.next/server/pages/500.html +1 -1
  175. package/.next/server/server-reference-manifest.js +1 -1
  176. package/.next/server/server-reference-manifest.json +41 -41
  177. package/.next/static/chunks/{17donp69y4do7.js → 0n3jle9kbqfr2.js} +8 -8
  178. package/.next/static/chunks/{3ok9_x74rip3v.js → 11r2yp_yh9_qn.js} +1 -1
  179. package/.next/static/chunks/{26-pr2c9ro222.js → 126fj4pga_72k.js} +1 -1
  180. package/.next/static/chunks/{2a0k6jlq0_v1u.js → 159udiexf6dom.js} +2 -2
  181. package/.next/static/chunks/18gi7o2ibrgh8.js +3 -0
  182. package/.next/static/chunks/{0x79tx04dxbpp.js → 1q9z1tohil7xn.js} +1 -1
  183. package/.next/static/chunks/{3cfi_q6d_wtci.js → 1sjgfets224re.js} +1 -1
  184. package/.next/static/chunks/{3agpgkwaa321p.js → 1wszsr83t01gh.js} +1 -1
  185. package/.next/static/chunks/22_zkas21rpna.js +1 -0
  186. package/.next/static/chunks/{1ekc7ul556kbl.js → 2szintyibu0s5.js} +1 -1
  187. package/.next/static/chunks/{1okqiww3pnwka.js → 3-b6b7entqazs.js} +1 -1
  188. package/.next/static/chunks/3cq5g494yuke8.css +1 -0
  189. package/.next/static/chunks/{2fzs9fm8yarvq.js → 3dmbhsv2u_8kj.js} +1 -1
  190. package/.next/static/chunks/{1grhjo7a5a0_-.js → 3i0a5px-oufgc.js} +1 -1
  191. package/.next/static/chunks/{280t0_e99i276.js → 3wexk9qo8zuj3.js} +3 -3
  192. package/.next/static/chunks/3xre8bu_pk1p_.js +1 -0
  193. package/.next/static/chunks/3zv6t3i77xhr_.js +1 -0
  194. package/.next/static/chunks/{28cizcdaabd4h.js → 408ueu4hfvcg7.js} +1 -1
  195. package/.next/static/chunks/{3xpbkl66ssib5.js → 435atsnhs7ksu.js} +8 -8
  196. package/README.md +46 -23
  197. package/app/api/events/app-event.ts +0 -2
  198. package/bin/port.mjs +65 -0
  199. package/bin/thursday.mjs +2 -33
  200. package/docs/how-it-works.md +11 -7
  201. package/docs/images/face-dark.png +0 -0
  202. package/docs/images/face-light.png +0 -0
  203. package/docs/images/keeps-going.png +0 -0
  204. package/features/ai/chatgpt.ts +42 -20
  205. package/features/ai/load-tools.ts +20 -14
  206. package/features/ai/prompts/bot.prompt.ts +91 -89
  207. package/features/ai/prompts/thursday.prompt.ts +48 -41
  208. package/features/ai/tools/bot.tool.ts +3 -3
  209. package/features/ai/tools/memory.tool.ts +65 -62
  210. package/features/ai/tools/skills.tool.ts +25 -8
  211. package/features/ai/tools/tool-name.ts +0 -1
  212. package/features/ai/tools/workspace.tool.ts +3 -0
  213. package/features/bot/bot.action.ts +6 -1
  214. package/features/bot/bot.run.ts +195 -80
  215. package/features/bot/bot.runner.ts +82 -39
  216. package/features/bot/bot.schema.ts +25 -0
  217. package/features/bot/components/bot-room.tsx +57 -4
  218. package/features/bot/components/bot-tool.tsx +0 -1
  219. package/features/bot/task.query.ts +132 -10
  220. package/features/bot/task.store.ts +26 -9
  221. package/features/memory/components/memory-edit.tsx +7 -5
  222. package/features/thursday/components/thursday.tsx +0 -3
  223. package/features/thursday/tool-line.ts +0 -1
  224. package/features/thursday/use-thursday.ts +1 -1
  225. package/features/workspace/workspace.ts +27 -20
  226. package/package.json +1 -1
  227. package/skills/browser/SKILL.md +6 -5
  228. package/.next/server/chunks/[root-of-the-server]__04q--0f._.js +0 -8
  229. package/.next/server/chunks/[root-of-the-server]__0bl-e63._.js +0 -3
  230. package/.next/server/chunks/[root-of-the-server]__0jq4ohy._.js +0 -8
  231. package/.next/server/chunks/[root-of-the-server]__0ls6_8y._.js +0 -8
  232. package/.next/server/chunks/[root-of-the-server]__1339jnv._.js +0 -8
  233. package/.next/server/chunks/[root-of-the-server]__18_teo8._.js +0 -8
  234. package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +0 -10
  235. package/.next/server/chunks/[root-of-the-server]__1wa1k7r._.js +0 -8
  236. package/.next/server/chunks/_1zjrdag._.js +0 -3
  237. package/.next/server/chunks/ssr/[root-of-the-server]__1ox7hbk._.js +0 -4
  238. package/.next/server/chunks/ssr/[root-of-the-server]__1pduf5n._.js +0 -3
  239. package/.next/server/chunks/ssr/_0mo2w06._.js +0 -3
  240. package/.next/server/chunks/ssr/_13z3f85._.js +0 -3
  241. package/.next/server/chunks/ssr/_14_lw10._.js +0 -3
  242. package/.next/server/chunks/ssr/_1eoqg_g._.js +0 -3
  243. package/.next/server/chunks/ssr/_1spzm8z._.js +0 -3
  244. package/.next/server/chunks/ssr/lib_date-like_ts_02is0pw._.js +0 -3
  245. package/.next/static/chunks/0tmgrnlje14-s.js +0 -3
  246. package/.next/static/chunks/0xzgvc0fkdb6t.js +0 -1
  247. package/.next/static/chunks/14hgtsjg-kael.css +0 -1
  248. package/.next/static/chunks/2hw2f6zmt7fmt.js +0 -1
  249. package/.next/static/chunks/3111pj5wnjgos.js +0 -1
  250. package/features/memory/components/memory-view.tsx +0 -84
  251. /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → G6-gNoyGDEcgAsq-L4PE1}/_buildManifest.js +0 -0
  252. /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → G6-gNoyGDEcgAsq-L4PE1}/_clientMiddlewareManifest.js +0 -0
  253. /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → G6-gNoyGDEcgAsq-L4PE1}/_ssgManifest.js +0 -0
@@ -1,19 +1,14 @@
1
1
  import type { ModelMessage } from "ai";
2
+ import { format } from "date-fns";
2
3
  import { PATHS, PROMPT_LINE, WORKSPACE_KEEP } from "@/config";
3
4
  import { TOOL_NAMES } from "@/features/ai/tools/tool-name";
4
5
  import { botMemoryFolder, listBotMemory } from "@/features/bot/bot.memory";
5
6
  import { listJobBots, readBotMemoryOn } from "@/features/bot/bot.query";
6
- import type { BotMemory, JobBot } from "@/features/bot/bot.schema";
7
+ import type { BotMemory, JobBot, TaskSpeaker } from "@/features/bot/bot.schema";
7
8
  import { findPinnedTools } from "@/features/connectors/mcp.query";
8
9
  import type { McpToolRef } from "@/features/connectors/mcp.schema";
9
- import {
10
- listAlwaysLoaded,
11
- listNoteIndex,
12
- } from "@/features/memory/memory.query";
13
- import type {
14
- MemoryAlwaysLoaded,
15
- MemoryIndexEntry,
16
- } from "@/features/memory/memory.schema";
10
+ import { listNoteIndex } from "@/features/memory/memory.query";
11
+ import type { MemoryIndexEntry } from "@/features/memory/memory.schema";
17
12
  import {
18
13
  loadSkills,
19
14
  type SkillMetadata,
@@ -23,11 +18,11 @@ import {
23
18
  openWorkspace,
24
19
  readMachineTools,
25
20
  } from "@/features/workspace/workspace";
21
+ import { toDate } from "@/lib/date-like";
26
22
  import { logger } from "@/lib/logger";
27
23
  import { clip } from "@/lib/utils";
28
24
  import { listConnectedToolNames } from "../tools/connected";
29
25
  import {
30
- carriedLines,
31
26
  type LoadedPrompt,
32
27
  logPromptSize,
33
28
  mcpToolLines,
@@ -57,31 +52,21 @@ export async function loadBotPrompt(
57
52
  const sandbox = await openWorkspace();
58
53
  const name = self.trim();
59
54
  const askedBy = seat?.askedBy ?? null;
60
- const [
61
- skills,
62
- index,
63
- carried,
64
- mcpTools,
65
- pinned,
66
- allBots,
67
- kept,
68
- memoryOn,
69
- machine,
70
- ] = await Promise.all([
71
- loadSkills(sandbox),
72
- listNoteIndex(),
73
- listAlwaysLoaded(),
74
- // User-connected servers and the app's studio in one list (tools/connected)
75
- listConnectedToolNames(),
76
- // MCP tools this bot already holds; dropped from the listing below
77
- findPinnedTools(name),
78
- listJobBots(),
79
- // What this bot kept on earlier jobs, read off its own folder (bot.memory)
80
- listBotMemory(name),
81
- readBotMemoryOn(),
82
- // One `command -v` sweep; what is here decides the first command (environment)
83
- readMachineTools(sandbox),
84
- ]);
55
+ const [skills, index, mcpTools, pinned, allBots, kept, memoryOn, machine] =
56
+ await Promise.all([
57
+ loadSkills(sandbox),
58
+ listNoteIndex(),
59
+ // User-connected servers and the app's studio in one list (tools/connected)
60
+ listConnectedToolNames(),
61
+ // MCP tools this bot already holds; dropped from the listing below
62
+ findPinnedTools(name),
63
+ listJobBots(),
64
+ // What this bot kept on earlier jobs, read off its own folder (bot.memory)
65
+ listBotMemory(name),
66
+ readBotMemoryOn(),
67
+ // One `command -v` sweep; what is here decides the first command (environment)
68
+ readMachineTools(sandbox),
69
+ ]);
85
70
 
86
71
  // Drop itself and every bot above it on this job, which are blocked waiting on
87
72
  // it; the last seat the depth allows has no roster at all (config BOT_RUN.depth)
@@ -94,11 +79,12 @@ export async function loadBotPrompt(
94
79
 
95
80
  const text = [
96
81
  askedBy ? borrowedIdentity(name, askedBy) : identity(name),
97
- memory(index, carried),
98
- memoryOn ? ownMemory(botMemoryFolder(name), kept) : "",
82
+ memory(index),
99
83
  connectedTools(mcpTools, pinned),
100
84
  methods(skills),
101
85
  environment(sandbox.cwd, machine, folders),
86
+ // After Environment: its folder is named against the Cwd said there
87
+ memoryOn ? ownMemory(botMemoryFolder(name), kept) : "",
102
88
  roster(peers),
103
89
  askedBy ? askingBack(askedBy) : ASKING,
104
90
  askedBy ? handingUp(askedBy) : ANSWERING,
@@ -118,20 +104,37 @@ export async function loadBotPrompt(
118
104
  };
119
105
  }
120
106
 
121
- /** Who the bot is, what machine it is on, and that guesses are not results. */
107
+ /**
108
+ * The two people every seat works between. Said before anything else, so the
109
+ * rest of the prompt — whose memory, who reads the answer — has someone to refer to.
110
+ */
111
+ const PEOPLE = `- **The user** — the one person all of this is for. They talk with Thursday by voice, and they follow this job on their screen.
112
+ - **Thursday** — their own personal assistant, one to one. She holds the conversation with them, hands bots the work that takes time, and tells them what comes back.`;
113
+
114
+ /**
115
+ * Who is who, what machine it is on, and that guesses are not results. How this
116
+ * job reached it is the first message's to say (buildTaskOpening).
117
+ */
122
118
  function identity(name: string): string {
123
119
  return [
124
120
  // Named, because the owner's prompt may not name it and its own instructions are addressed to it
125
- `You are ${name}, a worker. Thursday handed you a job while she keeps talking to the user — they speak to her and to nobody else, and what she says out of your answer is the only part of this that reaches them. You are not in that conversation and never address the user. This thread is drawn on their screen while you work, though: not written to them, but not private either. ${nowLine()}`,
121
+ `You are ${name}, one of the bots that work for the user. ${nowLine()}
122
+
123
+ ${PEOPLE}
124
+ - **You** — you do the work. Your answer goes to Thursday, not to the user, and she tells them what they need from it.`,
126
125
  MACHINE,
127
126
  NO_GUESSING,
128
127
  ].join("\n\n");
129
128
  }
130
129
 
131
- /** The borrowed seat: above it is the borrowing bot, not Thursday. */
130
+ /** The borrowed seat: between it and Thursday is the bot that borrowed it. */
132
131
  function borrowedIdentity(name: string, askedBy: string): string {
133
132
  return [
134
- `You are ${name}, a worker. ${askedBy} has handed you one part of a job Thursday handed over; your first message says who holds it and how it reached you. The user speaks to Thursday and to nobody else; you never address them, and what you hand back goes into ${askedBy}'s answer as-is. This thread is drawn on their screen while you work: not written to them, but not private either. ${nowLine()}`,
133
+ `You are ${name}, one of the bots that work for the user. ${nowLine()}
134
+
135
+ ${PEOPLE}
136
+ - **${askedBy}** — a bot on this job, who handed you one part of it. They cannot see your thread.
137
+ - **You** — you do that part. What you hand back goes into ${askedBy}'s work as it is, never to Thursday or the user.`,
135
138
  MACHINE,
136
139
  NO_GUESSING,
137
140
  ].join("\n\n");
@@ -159,44 +162,34 @@ Written by the person this bot works for. Where these and anything above disagre
159
162
  ${persona.trim()}`
160
163
  : "";
161
164
 
162
- /** A bot only reads memory; every write, revising and the names the user says are the call's (load-tools), so the chapter is that small. */
163
- function memory(
164
- index: MemoryIndexEntry[],
165
- carried: MemoryAlwaysLoaded[],
166
- ): string {
167
- const head = `## The user's memory
168
-
169
- What Thursday knows about the user, kept as she talks with them. Every bot reads it; none writes to it. Open a note with \`${TOOL_NAMES.memory_recall}\` when the job needs something about them — the address they sign in with, the name to book under — and put what you find out about them in your answer; she decides what to keep. It describes them, not you: how they are named and spoken to is hers to use. What you learn about the work goes in your own memory, below.`;
170
-
171
- const alreadyKnown = carried.length
172
- ? `Already known:
173
-
174
- ${carriedLines(carried)}`
175
- : "";
176
-
177
- const listing = `path — what is under it (facts) "what the user calls it"
165
+ /**
166
+ * Thursday's memory, which a bot only reads (load-tools). The listing alone: the
167
+ * lines she carries into every call are written for her — what to call them, how
168
+ * long an answer runs — and are hers to act on, not a bot's.
169
+ */
170
+ function memory(index: MemoryIndexEntry[]): string {
171
+ return `## Thursday's memory of the user
178
172
 
179
- ${noteLines(index)}
173
+ What Thursday keeps from talking with them. Open a note from the listing with \`${TOOL_NAMES.memory_recall}\` when the job needs something about them; what you find out about them goes in your answer, and she keeps what matters.
180
174
 
181
- A topic not listed is one nobody knows anything about.`;
175
+ path — what is under it (facts) "what the user calls it"
182
176
 
183
- return [head, alreadyKnown, listing].filter(Boolean).join("\n\n");
177
+ ${noteLines(index)}`;
184
178
  }
185
179
 
186
180
  /**
187
- * The bot's own memory (features/bot/bot.memory), listed by each file's first line so what a
188
- * file holds costs one line until a job opens it. Always drawn, so a first job knows it has one.
189
- * Nothing says what usually goes in first: a line like that is what a first job writes, whether
190
- * or not the job taught it anything.
181
+ * The bot's own memory (features/bot/bot.memory), listed by each file's first line and the day
182
+ * it last changed, both read off the disk, so what a file holds costs one line until a job opens
183
+ * it. Always drawn, so a first job knows it has one. Nothing says what usually goes in first: a
184
+ * line like that is what a first job writes, whether or not the job taught it anything.
191
185
  */
192
186
  function ownMemory(folder: string, kept: BotMemory): string {
193
187
  const listing = kept.entries.length
194
188
  ? kept.entries
195
- .map(({ file, line }) =>
196
- line
197
- ? `- ${file} — ${clip(line, PROMPT_LINE.botMemory)}`
198
- : `- ${file}`,
199
- )
189
+ .map(({ file, line, at }) => {
190
+ const said = line ? ` — ${clip(line, PROMPT_LINE.botMemory)}` : "";
191
+ return `- ${file}${said} · ${format(toDate(at), "yyyy-MM-dd")}`;
192
+ })
200
193
  .join("\n")
201
194
  : "Empty.";
202
195
  const rest =
@@ -206,7 +199,7 @@ function ownMemory(folder: string, kept: BotMemory): string {
206
199
 
207
200
  return `## Your memory
208
201
 
209
- What you kept from your own earlier jobs, in \`${folder}/\`: one topic per file, its first line saying what it holds, read by no other bot. Keep what a later job would otherwise have to find out again — how a site signs in, the way through its screens, a command that turned out right — and fix or delete a file that proved wrong. No passwords, keys or codes.
202
+ What you kept from your own earlier jobs, in \`${folder}/\` under the Cwd above: one topic per file, its first line saying what it holds, read by no other bot. Keep what a later job would otherwise have to find out again — how a site signs in, the way through its screens, a command that turned out right — with the date you found it true, and fix or delete a file that proved wrong. No passwords, keys or codes.
210
203
 
211
204
  ${listing}${rest}`;
212
205
  }
@@ -239,7 +232,7 @@ function methods(skills: SkillMetadata[]): string {
239
232
 
240
233
  return `## Skills
241
234
 
242
- Written-down methods. When the job is one of these, read it with \`${TOOL_NAMES.load_skill}\` before starting.
235
+ Written-down methods. Use the instructions already in your conversation. When the job needs a skill whose full instructions are absent, read it with \`${TOOL_NAMES.load_skill}\` before starting.
243
236
 
244
237
  ${skillLines(skills)}`;
245
238
  }
@@ -314,7 +307,7 @@ const ANSWERING = `## Answering
314
307
 
315
308
  Every job ends with \`${TOOL_NAMES.answer}\`. **Answer what was asked, at the size it was asked**: a question ends in its answer — one number is one line — and a thing to make comes back made, an action done, photos in a page, a comparison in a table. Once you have what they asked for the job is done; the next thing you would go and check is theirs to ask for.
316
309
 
317
- You are answering Thursday, not the user: she is on the call and picks what to say out of this. Write it plainly, in the user's language — the thread is on their screen as you write it. Anything past a few lines is a file under \`${PATHS.artifacts}/\`, and the answer names its path — it becomes a link on their screen. In a \`.md\`, images only by absolute route (\`/api/file/${PATHS.artifacts}/…\`).`;
310
+ You are answering Thursday, not the user: she picks what to say out of this. Write it plainly, in the user's language — the thread is on their screen as you write it. Anything past a few lines is a file under \`${PATHS.artifacts}/\`, and the answer names its path — it becomes a link on their screen. In a \`.md\`, images only by absolute route (\`/api/file/${PATHS.artifacts}/…\`).`;
318
311
 
319
312
  const handingUp = (askedBy: string) => `## Answering
320
313
 
@@ -329,19 +322,22 @@ export type OpeningContent = Extract<ModelMessage, { role: "user" }>["content"];
329
322
  /**
330
323
  * The first message of the bot holding a job, stored as the thread's first row
331
324
  * (seq 0, bot.runner startTask) and read whole on every resume and after every
332
- * compaction. Two text parts: who is who on this job, then the chain — the job
333
- * as Thursday handed it and the call it came from, verbatim, because the request
334
- * is one sentence the realtime model produced and a detail the user said may
335
- * only be in the call. The chain is what a borrowed bot inherits (buildHandoff),
336
- * so its headings name people and never say "you".
325
+ * compaction. Two text parts: who is on this job and how it reached them, then
326
+ * the chain — the job as it was handed over and, for a job from a call, that
327
+ * call verbatim, because the request is one sentence the realtime model produced
328
+ * and a detail the user said may only be in the call. The chain is what a
329
+ * borrowed bot inherits (buildHandoff), so its headings name people and never say "you".
337
330
  */
338
331
  export function buildTaskOpening(input: {
339
332
  bot: string;
340
333
  request: string;
341
334
  conversation: { role: "user" | "assistant"; text: string }[];
335
+ /** Who handed the job over: Thursday during a call, or the user on screen. */
336
+ from: TaskSpeaker;
342
337
  }): OpeningContent {
343
338
  const turns = input.conversation.filter((turn) => turn.text.trim());
344
- const job = `## Thursday → ${input.bot}: the job\n\n${input.request.trim()}`;
339
+ const by = input.from === "user" ? "The user" : "Thursday";
340
+ const job = `## ${by} → ${input.bot}: the job\n\n${input.request.trim()}`;
345
341
  const call = turns.length
346
342
  ? `## The call the job came from — its last ${turns.length} turns, verbatim
347
343
 
@@ -355,7 +351,7 @@ ${turns
355
351
  .join("\n")}`
356
352
  : "";
357
353
  return [
358
- { type: "text", text: whoIsWho([input.bot]) },
354
+ { type: "text", text: whoIsWho([input.bot], input.from) },
359
355
  { type: "text", text: [job, call].filter(Boolean).join("\n\n") },
360
356
  ];
361
357
  }
@@ -402,24 +398,30 @@ export function chainOf(first: ModelMessage | undefined): string {
402
398
  return texts.at(-1) ?? "";
403
399
  }
404
400
 
405
- /** Who is who on this job; the last of `bots` is the one reading it. */
406
- function whoIsWho(bots: string[]): string {
401
+ /**
402
+ * The bots on this job, and how it reached the one reading it — the last of
403
+ * `bots`. The user and Thursday are in every seat's prompt (identity); `from` is
404
+ * read only when the reader holds the job.
405
+ */
406
+ function whoIsWho(bots: string[], from?: TaskSpeaker): string {
407
407
  const you = bots.length - 1;
408
408
  const lines = bots.map((bot, index) => {
409
409
  if (index === 0) {
410
410
  return index === you
411
- ? `- **${bot} (you)** — Thursday handed you this job. You answer to Thursday, and your answer is the only part of it that reaches the user.`
412
- : `- **${bot}** — holds the job Thursday handed over, and answers for it.`;
411
+ ? `- **${bot} (you)** — ${
412
+ from === "user"
413
+ ? "the user handed you this job on their screen, not during a call"
414
+ : "Thursday handed you this job during a call"
415
+ }.`
416
+ : `- **${bot}** — holds this job and answers for it.`;
413
417
  }
414
- const from = bots[index - 1];
418
+ const by = bots[index - 1];
415
419
  return index === you
416
- ? `- **${bot} (you)** — ${from} handed you one part of it: the last section below. You answer to ${from}; nobody else hears from you.`
417
- : `- **${bot}** — ${from} handed them one part of it.`;
420
+ ? `- **${bot} (you)** — ${by} handed you one part of it: the last section below. You answer to ${by}; nobody else hears from you.`
421
+ : `- **${bot}** — ${by} handed them one part of it.`;
418
422
  });
419
- return `## Who is who on this job
423
+ return `## Who is on this job
420
424
 
421
- - **The user** — talks only to Thursday, on a voice call. No bot addresses them.
422
- - **Thursday** — the user's assistant on that call. Hands jobs to bots and tells the user what comes back.
423
425
  ${lines.join("\n")}`;
424
426
  }
425
427
 
@@ -95,7 +95,7 @@ export async function loadThursdayPrompt(
95
95
  // Injected as a system item by use-thursday: the realtime model does not open a call from instructions alone
96
96
  opening: blank.some((note) => note.path === "profile")
97
97
  ? opening(locale)
98
- : null,
98
+ : tidyOpening(index),
99
99
  };
100
100
  }
101
101
 
@@ -121,9 +121,43 @@ Their name, their language and how to address them are \`alwaysLoad\`; the rest
121
121
 
122
122
  If they came with something they want done, that comes first — hand it over and pick this up in the gaps. But do not end this call without a name and a language saved.]`;
123
123
 
124
- /** Who Thursday is: the concept, what stays between us, and that this is a call. */
124
+ /**
125
+ * Pushed the same way as the first-call opener when memory has outgrown MEMORY_LIMITS: settling
126
+ * it comes before anything she would raise herself, and a prompt line alone does not make the
127
+ * realtime model speak first. It names a few notes rather than every one it caught — a warning
128
+ * that lists everything is a second listing, and the first is already too long.
129
+ */
130
+ function tidyOpening(index: MemoryIndexEntry[]): string | null {
131
+ const { crowded, heavy } = tidying(index);
132
+ if (!crowded && heavy.length === 0) return null;
133
+
134
+ const full = heavy.length
135
+ ? `${[...heavy]
136
+ .sort((a, b) => b.factCount - a.factCount)
137
+ .slice(0, 3)
138
+ .map((note) => `${note.path} (${note.factCount})`)
139
+ .join(", ")} ${heavy.length > 1 ? "have" : "has"} grown past ${
140
+ MEMORY_LIMITS.factsPerNote
141
+ } facts, more than one note holds well.`
142
+ : "";
143
+ const total = index.reduce((sum, note) => sum + note.factCount, 0);
144
+ const many = crowded
145
+ ? `Memory holds ${total} facts in all, past ${MEMORY_LIMITS.facts}; the coldest notes are ${index
146
+ .slice(-4)
147
+ .map((note) => note.path)
148
+ .join(", ")}.`
149
+ : "";
150
+
151
+ return `[Memory needs tidying — this is not the user speaking.
152
+
153
+ ${[full, many].filter(Boolean).join(" ")}
154
+
155
+ Greet them and bring this up before anything of your own — if they open with something they need, that comes first. Go through it with them: read out what looks out of date a few facts at a time, and forget only what they tell you to drop, a whole note if they say so.]`;
156
+ }
157
+
158
+ /** Who Thursday is: whose she is, what makes her theirs, and that this is a call. */
125
159
  function identity(): string {
126
- return `You are Thursday, a personal voice assistant modeled on Friday, the AI in *Iron Man* — quick, warm, dry, on their side. ${nowLine()}
160
+ return `You are Thursday, this user's own personal assistant — one to one, modeled on Friday, the AI in *Iron Man*: quick, warm, dry, on their side. You work for them alone, and you become more theirs the more you know about them: who they are, the people in their life, what they are in the middle of, how they like things done. ${nowLine()}
127
161
 
128
162
  This is a call, not a chat: one or two sentences a turn. If you did not catch something, say so and ask again.`;
129
163
  }
@@ -138,12 +172,16 @@ Written by the user themselves. Where this and anything above disagree, this win
138
172
  ${persona.trim()}`
139
173
  : "";
140
174
 
141
- /** The note listing plus one sentence on when to write. What to save is the model's call. */
175
+ /**
176
+ * The note listing plus one sentence on what goes in. What is worth keeping is the model's call;
177
+ * how a fact is written — carried, replacing, dated — is the tool's schema to say.
178
+ */
142
179
  function memory(
143
180
  index: MemoryIndexEntry[],
144
181
  carried: MemoryAlwaysLoaded[],
145
182
  ): string {
146
- const { crowded, heavy } = tidying(index);
183
+ // Ages ride on the listing only when there is too much to hold: they are what to drop by
184
+ const { crowded } = tidying(index);
147
185
 
148
186
  const head = `## Memory
149
187
 
@@ -161,44 +199,11 @@ ${noteLines(index, crowded)}
161
199
 
162
200
  Open a note before answering out of it; a topic not listed is one you know nothing about. A fact marked \`said\` came from a call; open that call with \`${TOOL_NAMES.memory_conversation}\` only when the line itself cannot answer — exactly what they said, or why it was saved.
163
201
 
164
- Anything worth knowing next time goes in the moment it comes up, without asking — one fact per line, dates as dates. A rule they lay down for you — their language, how long an answer runs, when to hang up — is \`alwaysLoad\`; unsaved, it dies with the call. What stopped being true is corrected with \`replaces\`, not left standing beside it. Never claim to remember what you did not save.
202
+ Everything about them worth knowing next time goes in with \`${TOOL_NAMES.memory_remember}\` as it comes up, without asking — where it belongs is below. Never claim to remember what you did not save.
165
203
 
166
204
  ${MEMORY_PATHS.map((entry) => `- ${entry.path} — ${entry.of}`).join("\n")}`;
167
205
 
168
- // Only when there is something to tidy, and it names a few notes rather than
169
- // every one it caught: a warning that lists everything is a second listing,
170
- // and what it is warning about is that the first one is already too long.
171
- const full = heavy.length
172
- ? `${[...heavy]
173
- .sort((a, b) => b.factCount - a.factCount)
174
- .slice(0, 3)
175
- .map((note) => `${note.path} (${note.factCount})`)
176
- .join(", ")} ${heavy.length > 1 ? "have" : "has"} grown past ${
177
- MEMORY_LIMITS.factsPerNote
178
- } facts, more than one note holds well. Put ${
179
- heavy.length > 1 ? "each" : "it"
180
- } on their screen with \`${TOOL_NAMES.memory_show}\`, read back what is stale and forget only what they name.`
181
- : "";
182
- const many = crowded
183
- ? `Memory is past ${MEMORY_LIMITS.facts} facts in all. The coldest notes are ${index
184
- .slice(-4)
185
- .map((note) => note.path)
186
- .join(
187
- ", ",
188
- )} — ask whether any of them is still worth keeping, and delete the note if not.`
189
- : "";
190
- // Ahead of anything else she would raise, never ahead of what they came with
191
- const tidy = [full, many].filter(Boolean).join(" ");
192
-
193
- return [
194
- head,
195
- alreadyKnown,
196
- listing,
197
- tidy &&
198
- `${tidy} Get this settled before anything else you would bring up yourself — not before what they came to you with.`,
199
- ]
200
- .filter(Boolean)
201
- .join("\n\n");
206
+ return [head, alreadyKnown, listing].filter(Boolean).join("\n\n");
202
207
  }
203
208
 
204
209
  /** Last calls verbatim, marked as past so the model does not answer as if just asked. Absent on the first call. */
@@ -282,7 +287,9 @@ ${mcpServerLines(input.mcpTools)}`
282
287
  ${hands.join("\n\n")}`
283
288
  : "";
284
289
 
285
- const handingOver = `**Anything that takes more than a few seconds is a bot's** — one note or one file is yours. \`${TOOL_NAMES.delegate}\` answers at once: say who has it and keep talking. Put the request in their own words, with what it stands on and nothing they did not say; if something only they can say is missing — how much, which one, by when — ask that first.
290
+ // A bot does not read what she carries into her calls (bot.prompt memory), so
291
+ // how they want work done reaches it only through the request she writes
292
+ const handingOver = `**Anything that takes more than a few seconds is a bot's** — one note or one file is yours. \`${TOOL_NAMES.delegate}\` answers at once: say who has it and keep talking. Put the request in their own words, with what it stands on, including anything they have told you about how they want work done, and nothing they did not say; if something only they can say is missing — how much, which one, by when — ask that first.
286
293
 
287
294
  **The conversation belongs to the job, not to the bot** — hand the same bot a second job and it starts from nothing, knowing neither what was asked nor what it found. So more about a job you already handed over goes to \`${TOOL_NAMES.task}\` by its name, and the bot wakes with that job's own thread; with no name given it takes the one that moved last.`;
288
295
 
@@ -34,12 +34,12 @@ export const delegateSpec = {
34
34
 
35
35
  /**
36
36
  * `context` carries what the asking bot found, since its thread is not handed over (bot.run).
37
- * `request` is a brief, not a line: the borrowed bot sees nothing else.
37
+ * A returning participant keeps its own thread; `context` adds only what this caller knows.
38
38
  */
39
39
  export const askBotSpec = {
40
40
  name: TOOL_NAMES.ask_bot,
41
41
  description:
42
- "Hand one part of the job you are holding to another bot and wait for what they bring back. The job stays yours.",
42
+ "Send a request to another bot on this job and wait for their reply. The same bot continues its own work and conversation whenever you ask it again. The job stays yours.",
43
43
  parameters: z.object({
44
44
  bot: z.string().describe("A name from the bot list."),
45
45
  request: z
@@ -50,7 +50,7 @@ export const askBotSpec = {
50
50
  context: z
51
51
  .string()
52
52
  .describe(
53
- "What you have done on the job so far and why this part is theirs — exact values, and the paths of files you wrote for them to read. They see this, the part, the job and the call; not your thread or your tool output, so a value you leave out is one they fetch again.",
53
+ "Pass on what you know that they do not: exact values and paths they need. They keep their own previous work and your exchanges, but cannot read your private thread or tool output.",
54
54
  ),
55
55
  }),
56
56
  };
@@ -1,6 +1,5 @@
1
1
  import { tool } from "ai";
2
2
  import * as z from "zod";
3
- import { appEvents } from "@/app/api/events/app-event.server";
4
3
  import { MEMORY_CONVERSATION_PAGE, MEMORY_LIMITS } from "@/config";
5
4
  import {
6
5
  callStamp,
@@ -57,9 +56,9 @@ const noConversation = (source: MemorySource | null) =>
57
56
  /**
58
57
  * Said when a note has outgrown the recommended size, and only then — the write
59
58
  * itself always goes through. It names no tool: this one answers the call and a
60
- * bot, and only one of them has a screen to put a note on. How to do it is the
61
- * call prompt's (prompts/thursday.prompt); the ask is the same either way,
62
- * because a bot that turned up a fact can hand the same request back with it.
59
+ * bot, and how to settle it with the user is the call's to say (prompts/thursday.prompt);
60
+ * the ask is the same either way, because a bot that turned up a fact can hand
61
+ * the same request back with it.
63
62
  */
64
63
  const overSize = (count: number) =>
65
64
  count > MEMORY_LIMITS.factsPerNote
@@ -114,7 +113,9 @@ export const createMemoryTools = (
114
113
  .object({
115
114
  text: z
116
115
  .string()
117
- .describe("One statement that stands on its own later."),
116
+ .describe(
117
+ 'One statement that stands on its own later — a date as a date, never "next week".',
118
+ ),
118
119
  replaces: z
119
120
  .number()
120
121
  .int()
@@ -126,7 +127,7 @@ export const createMemoryTools = (
126
127
  .boolean()
127
128
  .nullish()
128
129
  .describe(
129
- `True carries this line into every call's instructions without opening its note — for what to call them, their language and register, a standing rule. At most ${MEMORY_LIMITS.carried}. Null leaves it; false makes a carried line an ordinary fact.`,
130
+ `True carries this line into every call's instructions without opening its note — for what every call needs before any note is opened: what to call them, their language and register. At most ${MEMORY_LIMITS.carried}. Null leaves it; false makes a carried line an ordinary fact.`,
130
131
  ),
131
132
  })
132
133
  .array()
@@ -175,23 +176,23 @@ export const createMemoryTools = (
175
176
  };
176
177
  }
177
178
  // A new path outside the convention goes to inbox, which keeps its own line and takes no aliases
178
- const filed =
179
- known || isMemoryPath(said)
180
- ? { path: target, description, aliases, facts }
181
- : {
182
- path: MEMORY_INBOX,
183
- description: appNoteLine(MEMORY_INBOX),
184
- facts,
185
- };
179
+ const toInbox = !known && !isMemoryPath(said);
180
+ const filed = toInbox
181
+ ? {
182
+ path: MEMORY_INBOX,
183
+ description: appNoteLine(MEMORY_INBOX),
184
+ facts,
185
+ }
186
+ : { path: target, description, aliases, facts };
186
187
 
187
188
  const write = await writeNotes([filed], source, callId);
188
189
 
189
190
  // The write already succeeded; what follows are requests, not failures.
190
- // A path filed elsewhere must be said, or the model reports it saved where it asked.
191
- const elsewhere =
192
- filed.path !== said
193
- ? ` Filed under ${filed.path}: "${said}" is not a path this listing can carry. If that is not where it belongs, write it again under one that is.`
194
- : "";
191
+ // Only a path that landed in inbox is said: one reached by a name the
192
+ // listing carries is where it was asked for, however it was spelled.
193
+ const elsewhere = toInbox
194
+ ? ` Filed under ${filed.path}: "${said}" is not a path this listing can carry. If that is not where it belongs, write it again under one that is.`
195
+ : "";
195
196
 
196
197
  const unnamed = write.unnamed.length
197
198
  ? ` ${write.unnamed[0]} is new and has no line yet — the listing shows its first fact instead. Call again with \`description\` (one line about what it is) and \`aliases\` (the names they say for it).`
@@ -265,59 +266,61 @@ export const createMemoryTools = (
265
266
  },
266
267
  }),
267
268
 
268
- [TOOL_NAMES.memory_show]: tool({
269
- description: `Put a note on the user's screen so they can see it while you
270
- talk about it, and take it away again. They cannot edit it there — they say
271
- what to drop and you drop it.`,
272
- inputSchema: z.object({
273
- path: z
274
- .string()
275
- .nullable()
276
- .describe(
277
- "The note to put on screen, exactly as the listing writes it. Null takes it away.",
278
- ),
279
- }),
280
- execute: async ({ path }) => {
281
- if (!path?.trim()) {
282
- appEvents.emit({ type: "memory-view", path: null });
283
- return { note: "Taken off the screen." };
284
- }
285
- // The screen gets only the path and reads facts over GET, so deletions show up on their own
286
- const { notes } = await readNotes([path], { touch: false });
287
- const note = notes[0];
288
- if (!note) {
289
- return { note: `Nothing on the listing called ${path.trim()}.` };
290
- }
291
- appEvents.emit({ type: "memory-view", path: note.path });
292
- return {
293
- ...note,
294
- note: "It is on their screen now, in this order. Ask what they no longer need, and forget only what they name.",
295
- };
296
- },
297
- }),
298
-
299
269
  [TOOL_NAMES.memory_forget]: tool({
300
- description: "Delete one fact for good.",
270
+ description: "Delete facts for good.",
301
271
  inputSchema: z.object({
302
- factId: z
272
+ factIds: z
303
273
  .number()
304
274
  .int()
305
- .describe("The id that came with the fact when the note was opened."),
275
+ .array()
276
+ .min(1)
277
+ .describe(
278
+ "The ids that came with the facts when their notes were opened — every one to delete, in one call.",
279
+ ),
306
280
  }),
307
- execute: async ({ factId }) => {
308
- const forgotten = await forgetFactById(factId);
309
- if (!forgotten) {
310
- return { note: `No fact with id ${factId}. Nothing was deleted.` };
281
+ execute: async ({ factIds }) => {
282
+ const missing: number[] = [];
283
+ /** Notes that lost their last fact, and went with it (memory.query forgetFactById). */
284
+ const gone: string[] = [];
285
+ /** Notes that still hold something, handed back as they are now. */
286
+ const left = new Set<string>();
287
+ for (const id of new Set(factIds)) {
288
+ const forgotten = await forgetFactById(id);
289
+ if (!forgotten) {
290
+ missing.push(id);
291
+ continue;
292
+ }
293
+ if (forgotten.noteGone) {
294
+ left.delete(forgotten.path);
295
+ gone.push(forgotten.path);
296
+ } else {
297
+ left.add(forgotten.path);
298
+ }
311
299
  }
312
300
 
313
- // The note goes with its last fact; the listing still shows it until the next session, so say so
314
- if (forgotten.noteGone) {
301
+ if (!gone.length && !left.size) {
315
302
  return {
316
- note: `${GONE} That was the last fact under ${forgotten.path}, so the note went with it.`,
303
+ note: `No fact with id ${missing.join(", ")}. Nothing was deleted.`,
317
304
  };
318
305
  }
319
- const { notes } = await readNotes([forgotten.path], { touch: false });
320
- return { ...notes[0], note: GONE };
306
+
307
+ // The notes that went are still on the listing until the next session, so say so
308
+ const said = [
309
+ GONE,
310
+ gone.length === 1
311
+ ? `That was the last fact under ${gone[0]}, so the note went with it.`
312
+ : gone.length
313
+ ? `That was the last of ${gone.join(", ")}, so those notes went with them.`
314
+ : "",
315
+ missing.length
316
+ ? `No fact with id ${missing.join(", ")}, so those were skipped.`
317
+ : "",
318
+ ]
319
+ .filter(Boolean)
320
+ .join(" ");
321
+ if (!left.size) return { note: said };
322
+ const { notes } = await readNotes([...left], { touch: false });
323
+ return { notes: notes.map(withCount), note: said };
321
324
  },
322
325
  }),
323
326
  });