thursday-agent 0.6.0 → 0.7.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 (193) 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.html +1 -1
  5. package/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  9. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  10. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +2 -2
  13. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  14. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  15. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  16. package/.next/server/app/api/artifact/route.js +3 -3
  17. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  18. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
  19. package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
  20. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  21. package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -1
  22. package/.next/server/app/api/bot/thread/route.js.nft.json +1 -1
  23. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  24. package/.next/server/app/api/memory/edit/route.js +1 -1
  25. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
  26. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  28. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  29. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  30. package/.next/server/app/api/thursday/tool-call/route.js +1 -1
  31. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  32. package/.next/server/app/api/workspace/missing/route.js +2 -2
  33. package/.next/server/app/api/workspace/missing/route.js.nft.json +1 -1
  34. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  35. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  36. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  37. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  38. package/.next/server/app/page/server-reference-manifest.json +42 -42
  39. package/.next/server/app/page.js +5 -4
  40. package/.next/server/app/page.js.nft.json +1 -1
  41. package/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/.next/server/chunks/[root-of-the-server]__0-06t3f._.js +8 -0
  43. package/.next/server/chunks/{[root-of-the-server]__049f521._.js → [root-of-the-server]__06l05n_._.js} +1 -1
  44. package/.next/server/chunks/[root-of-the-server]__0_ue8oe._.js +1 -1
  45. package/.next/server/chunks/{[root-of-the-server]__0ye3da2._.js → [root-of-the-server]__0btqrct._.js} +1 -1
  46. package/.next/server/chunks/[root-of-the-server]__0c9fgca._.js +1 -1
  47. package/.next/server/chunks/[root-of-the-server]__0zp35ul._.js +1 -1
  48. package/.next/server/chunks/[root-of-the-server]__1kv8p3e._.js +1 -1
  49. package/.next/server/chunks/_02szwk8._.js +1 -1
  50. package/.next/server/chunks/_06c9kie._.js +24 -24
  51. package/.next/server/chunks/_0i55en5._.js +6 -6
  52. package/.next/server/chunks/_0p56fn0._.js +1 -1
  53. package/.next/server/chunks/_11ujpya._.js +1 -1
  54. package/.next/server/chunks/_1dbixxz._.js +1 -1
  55. package/.next/server/chunks/_1dc_d1q._.js +16 -16
  56. package/.next/server/chunks/_1gnjfli._.js +3 -0
  57. package/.next/server/chunks/_1klbcft._.js +1 -1
  58. package/.next/server/chunks/features_0-cnt0l._.js +1 -1
  59. package/.next/server/chunks/features_0hdaci0._.js +1 -1
  60. package/.next/server/chunks/ssr/[root-of-the-server]__0-e011f._.js +3 -0
  61. package/.next/server/chunks/ssr/[root-of-the-server]__0pd8yr7._.js +3 -0
  62. package/.next/server/chunks/ssr/[root-of-the-server]__1nygfik._.js +1 -1
  63. package/.next/server/chunks/ssr/[root-of-the-server]__1sdphb0._.js +1 -1
  64. package/.next/server/chunks/ssr/{[root-of-the-server]__1fll8dc._.js → [root-of-the-server]__1vdmfbe._.js} +2 -2
  65. package/.next/server/chunks/ssr/[root-of-the-server]__1waa3rc._.js +34 -49
  66. package/.next/server/chunks/ssr/{_1anqots._.js → _0-2qsxo._.js} +2 -2
  67. package/.next/server/chunks/ssr/_02oc_h5._.js +9 -9
  68. package/.next/server/chunks/ssr/_05yghx6._.js +1 -1
  69. package/.next/server/chunks/ssr/_07nbwap._.js +3 -0
  70. package/.next/server/chunks/ssr/_08c4z-9._.js +1 -1
  71. package/.next/server/chunks/ssr/_0_qfdlw._.js +1 -1
  72. package/.next/server/chunks/ssr/_0b6_jv-._.js +3 -0
  73. package/.next/server/chunks/ssr/_0hxt2ul._.js +1 -1
  74. package/.next/server/chunks/ssr/_0i_6q1w._.js +1 -1
  75. package/.next/server/chunks/ssr/{_18xqmny._.js → _0no7i1o._.js} +2 -2
  76. package/.next/server/chunks/ssr/_0tzg--b._.js +3 -0
  77. package/.next/server/chunks/ssr/{_1ztk7p6._.js → _114ylv7._.js} +3 -3
  78. package/.next/server/chunks/ssr/{_1k0spz7._.js → _12c-iwh._.js} +2 -2
  79. package/.next/server/chunks/ssr/_12w42gi._.js +1 -1
  80. package/.next/server/chunks/ssr/{_0eaog6_._.js → _167hchk._.js} +2 -2
  81. package/.next/server/chunks/ssr/{_1x9pskj._.js → _1_x5spz._.js} +3 -3
  82. package/.next/server/chunks/ssr/{_0dr-ddg._.js → _1a61fc9._.js} +2 -2
  83. package/.next/server/chunks/ssr/_1jmz33w._.js +1 -1
  84. package/.next/server/chunks/ssr/{_1y6b5mr._.js → _1w_t7h7._.js} +2 -2
  85. package/.next/server/chunks/ssr/{_0dh2e1j._.js → _1wyfaq_._.js} +7 -7
  86. package/.next/server/chunks/ssr/{[root-of-the-server]__0bxsfyn._.js → _200gt-m._.js} +4 -3
  87. package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
  88. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  89. package/.next/server/chunks/ssr/{_0c9e5w6._.js → features_workspace_00jz2hu._.js} +3 -4
  90. package/.next/server/chunks/ssr/{node_modules_0746rzi._.js → node_modules_02ai7br._.js} +1 -1
  91. package/.next/server/chunks/ssr/{node_modules_0vsxbzp._.js → node_modules_02deexj._.js} +1 -1
  92. package/.next/server/chunks/ssr/{node_modules_1g1eihe._.js → node_modules_07za262._.js} +1 -1
  93. package/.next/server/chunks/ssr/{node_modules_0332gw4._.js → node_modules_0_s9get._.js} +1 -1
  94. package/.next/server/chunks/ssr/{node_modules_1gpednh._.js → node_modules_0n3c7h0._.js} +1 -1
  95. package/.next/server/chunks/ssr/{node_modules_0t-9nup._.js → node_modules_0od6jwr._.js} +1 -1
  96. package/.next/server/chunks/ssr/{node_modules_0mxvg8h._.js → node_modules_0rdt7pk._.js} +1 -1
  97. package/.next/server/chunks/ssr/{node_modules_0vv0-fg._.js → node_modules_0w1z7kz._.js} +1 -1
  98. package/.next/server/chunks/ssr/{node_modules_1d53yiq._.js → node_modules_0wdupfz._.js} +1 -1
  99. package/.next/server/chunks/ssr/{node_modules_1u6w1pb._.js → node_modules_1720w0a._.js} +1 -1
  100. package/.next/server/chunks/ssr/{node_modules_1gu1pph._.js → node_modules_18lsg7d._.js} +1 -1
  101. package/.next/server/chunks/ssr/{node_modules_1he1w8_._.js → node_modules_1b94fxf._.js} +1 -1
  102. package/.next/server/chunks/ssr/{node_modules_0qbfyjh._.js → node_modules_1ecdo1s._.js} +1 -1
  103. package/.next/server/chunks/ssr/{node_modules_0qapgjr._.js → node_modules_1fa9ag7._.js} +1 -1
  104. package/.next/server/chunks/ssr/{node_modules_0d5uzof._.js → node_modules_1fv-_si._.js} +1 -1
  105. package/.next/server/chunks/ssr/{node_modules_1lnjapm._.js → node_modules_1hf461g._.js} +1 -1
  106. package/.next/server/chunks/ssr/{node_modules_1-urho9._.js → node_modules_1m71ta-._.js} +1 -1
  107. package/.next/server/chunks/ssr/{node_modules_1lynj9m._.js → node_modules_1n93n-g._.js} +1 -1
  108. package/.next/server/chunks/ssr/{node_modules_0fr-ml8._.js → node_modules_1n9lqu9._.js} +1 -1
  109. package/.next/server/chunks/ssr/{node_modules_04vxchl._.js → node_modules_1npnsvd._.js} +1 -1
  110. package/.next/server/chunks/ssr/{node_modules_0exkwcm._.js → node_modules_1rn2r0n._.js} +1 -1
  111. package/.next/server/chunks/ssr/{node_modules_022sipm._.js → node_modules_1vq-mfk._.js} +1 -1
  112. package/.next/server/chunks/ssr/{node_modules_00_51_v._.js → node_modules_1vzyw1t._.js} +1 -1
  113. package/.next/server/chunks/ssr/{node_modules_0wsvp00._.js → node_modules_1w359w4._.js} +1 -1
  114. package/.next/server/chunks/ssr/{node_modules_1xn6saw._.js → node_modules_1yajbl6._.js} +1 -1
  115. package/.next/server/chunks/ssr/{node_modules_16pi_dd._.js → node_modules_20eqhy8._.js} +1 -1
  116. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_067_exa._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_019sbis._.js} +1 -1
  117. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0nrrdaq._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0s6piq3._.js} +1 -1
  118. package/.next/server/chunks/ssr/{node_modules_@vercel_oidc_dist_21a5haa._.js → node_modules_@vercel_oidc_dist_05itbb1._.js} +1 -1
  119. package/.next/server/middleware-build-manifest.js +3 -3
  120. package/.next/server/pages/404.html +1 -1
  121. package/.next/server/pages/500.html +1 -1
  122. package/.next/server/server-reference-manifest.js +1 -1
  123. package/.next/server/server-reference-manifest.json +43 -43
  124. package/.next/static/chunks/{0dr16v67vj4n7.js → 07vqbk_9t4em4.js} +2 -2
  125. package/.next/static/chunks/{2nvepds1fu2ux.js → 08krja1zx7nqg.js} +1 -1
  126. package/.next/static/chunks/{42wcg3wfg_m_p.js → 0qcuwdk132jjt.js} +1 -1
  127. package/.next/static/chunks/{2_pnccjp6j1ca.js → 0z69gvysmave7.js} +1 -1
  128. package/.next/static/chunks/12fvz1w5ows75.js +1 -0
  129. package/.next/static/chunks/14t03ned286iu.js +1 -0
  130. package/.next/static/chunks/{2jlz8zp4jax_m.js → 156xxbkrz5lzu.js} +1 -1
  131. package/.next/static/chunks/1fz14reod4lft.js +1 -0
  132. package/.next/static/chunks/{0joan-69kbspd.js → 1g23dbabl-vkr.js} +1 -1
  133. package/.next/static/chunks/{18mkhstgiqcbt.js → 1qy_rc3oxfxok.js} +1 -1
  134. package/.next/static/chunks/1zea7qdl-7365.js +1 -0
  135. package/.next/static/chunks/1zvqumkg7kmo0.css +1 -0
  136. package/.next/static/chunks/26hpqt3y6bogl.js +12 -0
  137. package/.next/static/chunks/{3zk6q3-3f9swq.js → 2_j5mqm6d-z18.js} +1 -1
  138. package/.next/static/chunks/{1vj_5bnbxx84e.js → 2dt6tu7a22svs.js} +1 -1
  139. package/.next/static/chunks/2h5ls3d86kya6.js +1 -0
  140. package/.next/static/chunks/2wc6y1ht6dfuy.js +1 -0
  141. package/.next/static/chunks/{1i-u2wzub_to1.js → 2xr0zpll7thyi.js} +6 -6
  142. package/.next/static/chunks/{26ql4y-43xwhp.js → 31jztc-yq30m7.js} +1 -1
  143. package/.next/static/chunks/391af0i8eljzk.js +1 -0
  144. package/.next/static/chunks/{1kp5nfp06nj9h.js → 3p2s2cjokx1aa.js} +2 -2
  145. package/.next/static/chunks/{1wlo979mx-y3w.js → 41s3fml5oivsa.js} +1 -1
  146. package/README.md +60 -19
  147. package/docs/how-it-works.md +2 -0
  148. package/docs/live-calls.md +38 -33
  149. package/features/ai/load-tools.ts +4 -2
  150. package/features/ai/prompts/bot.prompt.ts +5 -5
  151. package/features/ai/prompts/live.prompt.ts +36 -132
  152. package/features/ai/prompts/prompt-helper.ts +5 -4
  153. package/features/ai/prompts/thursday.prompt.ts +75 -54
  154. package/features/ai/tools/call.tool.ts +32 -4
  155. package/features/ai/tools/tool-name.ts +1 -0
  156. package/features/bot/bot.runner.ts +5 -5
  157. package/features/thursday/ascii.const.ts +74 -0
  158. package/features/thursday/components/ascii-orb.tsx +173 -30
  159. package/features/thursday/components/face.tsx +6 -1
  160. package/features/thursday/components/side-captions.tsx +273 -0
  161. package/features/thursday/components/tab-state.tsx +11 -3
  162. package/features/thursday/components/thursday-setting.tsx +6 -5
  163. package/features/thursday/components/thursday.tsx +227 -251
  164. package/features/thursday/face.store.ts +5 -0
  165. package/features/thursday/thursday.action.ts +4 -6
  166. package/features/thursday/thursday.schema.ts +10 -5
  167. package/features/thursday/thursday.store.ts +3 -0
  168. package/features/thursday/use-thursday.ts +461 -392
  169. package/features/workspace/file-kind.ts +9 -0
  170. package/hooks/use-hotkey.ts +3 -2
  171. package/lib/live/live.schema.ts +0 -21
  172. package/lib/live/live.server.ts +1 -3
  173. package/package.json +1 -1
  174. package/scripts/artifact-paths.test.mts +13 -0
  175. package/scripts/live-session.test.mts +56 -66
  176. package/.next/server/chunks/[root-of-the-server]__0g-ueei._.js +0 -8
  177. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1kipdeo._.js +0 -3
  178. package/.next/server/chunks/ssr/[root-of-the-server]__172cq4x._.js +0 -3
  179. package/.next/server/chunks/ssr/_03_928f._.js +0 -3
  180. package/.next/server/chunks/ssr/_0g_rlog._.js +0 -3
  181. package/.next/server/chunks/ssr/_123ttsf._.js +0 -3
  182. package/.next/static/chunks/10k2xvap03z20.css +0 -1
  183. package/.next/static/chunks/1t9qms5pwwa3n.js +0 -1
  184. package/.next/static/chunks/2a51-ywz9b2a3.js +0 -1
  185. package/.next/static/chunks/2fvdgxih7x7hr.js +0 -1
  186. package/.next/static/chunks/3addj3i4-izma.js +0 -1
  187. package/.next/static/chunks/3gfh7nxwusbt0.js +0 -1
  188. package/.next/static/chunks/3gwa44cvz4qoi.js +0 -1
  189. package/.next/static/chunks/3zkg5iup3v4a2.js +0 -12
  190. package/.next/static/chunks/405m0bqtoihev.js +0 -1
  191. /package/.next/static/{0qzGoKn2wTERVZtblX1tp → yyRVBc0xxYQsh3GpbQX9b}/_buildManifest.js +0 -0
  192. /package/.next/static/{0qzGoKn2wTERVZtblX1tp → yyRVBc0xxYQsh3GpbQX9b}/_clientMiddlewareManifest.js +0 -0
  193. /package/.next/static/{0qzGoKn2wTERVZtblX1tp → yyRVBc0xxYQsh3GpbQX9b}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import { CALL_EXEC_TIMEOUT_MS, RECENT_CALL } from "@/config";
2
2
  import { TOOL_NAMES } from "@/features/ai/tools/tool-name";
3
- import { listJobBots } from "@/features/bot/bot.query";
3
+ import { listJobBots, readBotMemoryOn } from "@/features/bot/bot.query";
4
4
  import type { JobBot } from "@/features/bot/bot.schema";
5
5
  import { type CallJob, listCallJobs } from "@/features/bot/thread.query";
6
6
  import {
@@ -41,12 +41,12 @@ import {
41
41
  } from "./prompt-helper";
42
42
 
43
43
  /**
44
- * Everything the call's Responses backend hears, in the shape the GPT-Live guide gives a backend:
45
- * who Thursday is (the words the voice opens with too), the voice conversation it works from, one
46
- * chapter per capability the voice's delegation policy names — memory with ids, background work
47
- * with the roster, this computer — then what to return, and the last calls with their jobs.
48
- * Nothing here is about how to talk. The loader is the table of contents, and empty chapters are
49
- * dropped. Shares no sentence with bot.prompt. Assembled on every call, never cached.
44
+ * Everything the call's Responses backend hears: who Thursday is and how a call ends (the words
45
+ * the voice opens with too), then the work the voice hands over — memory with ids, background
46
+ * work with the roster, this computer — what to return, and the last calls with their jobs, which
47
+ * only the backend reads. Nothing here is about how to talk. The loader is the table of contents,
48
+ * and empty chapters are dropped. Shares no sentence with bot.prompt. Assembled on every call,
49
+ * never cached.
50
50
  *
51
51
  * @param backendPrompt Settings › Thursday › Backend instructions, added last.
52
52
  */
@@ -54,20 +54,30 @@ export async function loadThursdayPrompt(
54
54
  backendPrompt?: string | null,
55
55
  ): Promise<string> {
56
56
  const sandbox = await openWorkspace();
57
- const [skills, index, carried, open, connected, roster, calls, hers] =
58
- await Promise.all([
59
- loadSkills(sandbox),
60
- listNoteIndex(),
61
- // Facts carried into every call without opening a note
62
- listAlwaysLoaded(),
63
- // Written out in the prompt, which is not the user asking for them: no read counted
64
- readNotes(MEMORY_ALWAYS_LISTED, { touch: false }),
65
- listConnectedToolNames(),
66
- listJobBots(),
67
- listRecentTurns(RECENT_CALL.rows),
68
- // Whether the call was handed `load_skill` (Settings › Thursday, load-tools)
69
- readCallSkillsOn(),
70
- ]);
57
+ const [
58
+ skills,
59
+ index,
60
+ carried,
61
+ open,
62
+ connected,
63
+ roster,
64
+ calls,
65
+ hers,
66
+ botMemory,
67
+ ] = await Promise.all([
68
+ loadSkills(sandbox),
69
+ listNoteIndex(),
70
+ // Facts carried into every call without opening a note
71
+ listAlwaysLoaded(),
72
+ // Written out in the prompt, which is not the user asking for them: no read counted
73
+ readNotes(MEMORY_ALWAYS_LISTED, { touch: false }),
74
+ listConnectedToolNames(),
75
+ listJobBots(),
76
+ listRecentTurns(RECENT_CALL.rows),
77
+ // Whether the call was handed `load_skill` (Settings › Thursday, load-tools)
78
+ readCallSkillsOn(),
79
+ readBotMemoryOn(),
80
+ ]);
71
81
  // The jobs those calls opened, folded into the transcript below
72
82
  const jobs = await listCallJobs(calls.map((call) => call.callId));
73
83
 
@@ -75,11 +85,14 @@ export async function loadThursdayPrompt(
75
85
  const text = [
76
86
  thursdayIdentity(),
77
87
  callEnding(),
78
- conversation(),
79
88
  memory(index, carried, open.notes),
80
89
  // A skill is named once, on the side that can read it: this computer's chapter
81
90
  // when the setting hands the call the tool, the bots' reach when it does not
82
- backgroundWork(roster, reachNames(hers ? [] : skills, connected)),
91
+ backgroundWork(
92
+ roster,
93
+ reachNames(hers ? [] : skills, connected),
94
+ botMemory,
95
+ ),
83
96
  thisComputer(sandbox.cwd, hers ? skills : []),
84
97
  result(),
85
98
  earlierCalls(calls, jobs),
@@ -95,25 +108,13 @@ export async function loadThursdayPrompt(
95
108
  }
96
109
 
97
110
  /**
98
- * What it works from: the conversation Live hands over, heard rather than typed, and the tool
99
- * that ends it. The updates the app appends reach it too, which is how a relayed question's id
100
- * gets back into `thread`.
111
+ * What goes back is said aloud, so it is what a tool or a note confirmed and nothing more — or
112
+ * the one question the voice puts to the user before work is handed over (backgroundWork).
101
113
  */
102
- function conversation(): string {
103
- return `## Voice conversation context
104
-
105
- You are on a live voice call with the user. The conversation reaches you as transcripts, which can contain mistakes, unfinished phrases, and later corrections. Use the latest context and verified records. If a needed detail is still unclear, ask for that detail instead of guessing. Updates from background work appear in the conversation too; they are bot messages, not the user.
106
-
107
- When the user wants to end the call, \`${TOOL_NAMES.end_call}\` hangs up the phone; nothing else ends it.
108
-
109
- Tool results, notes and these instructions are in English, which says nothing about the user's language.`;
110
- }
111
-
112
- /** What goes back is said aloud, so it is what a tool or a note confirmed and nothing more. */
113
114
  function result(): string {
114
115
  return `## Return the result
115
116
 
116
- Return the relevant facts, whether the task is complete, and what comes next — for a job you handed over, who has it. Use confirmed values from tool results and the notes above, and never invent a successful action. What you return is said aloud: keep it short and plain.`;
117
+ Return the relevant facts, whether the task is complete, and what comes next — for a job you handed over, who has it — or the one question the user has to answer first. Use confirmed values from tool results and the notes above, and never invent a successful action. What you return is said aloud: keep it short and plain.`;
117
118
  }
118
119
 
119
120
  /** Settings › Thursday › Backend instructions; no heading when empty. */
@@ -128,8 +129,10 @@ ${backendPrompt.trim()}`
128
129
 
129
130
  /**
130
131
  * Profile and preferences written out, carried facts, the listing, and what goes in. What is
131
- * worth keeping is the model's call; how a fact is written — carried, replacing, dated — is the
132
- * tool's schema to say, so none of it is repeated here.
132
+ * worth keeping is the model's call; how a fact is written — carried, dated — is the tool's
133
+ * schema to say. Merging is said here too: left to the `replaces` description alone, facts on
134
+ * one subject piled up beside each other (09-17). Merging loses nothing; deleting does, so
135
+ * `memory_forget` stays for what the user names.
133
136
  */
134
137
  function memory(
135
138
  index: MemoryIndexEntry[],
@@ -138,7 +141,16 @@ function memory(
138
141
  open: MemoryNoteView[],
139
142
  ): string {
140
143
  // Ages ride on the listing only when there is too much to hold: they are what to drop by
141
- const { crowded } = tidying(index);
144
+ const { crowded, heavy } = tidying(index);
145
+ const heaviest = [...heavy]
146
+ .sort((a, b) => b.factCount - a.factCount)
147
+ .slice(0, 3)
148
+ .map((note) => note.path);
149
+ // Past MEMORY_LIMITS, settling it with the user; the voice opens nothing about it
150
+ const tidy =
151
+ crowded || heavy.length
152
+ ? `\n\nSaved memory has grown past what it holds well${heaviest.length ? ` (${heaviest.join(", ")})` : ""}: say so once in what you return, go through what looks out of date with the user, and forget only what they name.`
153
+ : "";
142
154
  // A note written out here is left off the listing, and so are its carried lines
143
155
  const written = new Set(open.map((note) => note.path));
144
156
  const carriedIds = new Set(carried.map((fact) => fact.id));
@@ -163,13 +175,13 @@ ${carriedLines(elsewhere)}`
163
175
  ${noteLines(
164
176
  index.filter((note) => !written.has(note.path)),
165
177
  crowded,
166
- )}
178
+ )}${tidy}
167
179
 
168
180
  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; \`${TOOL_NAMES.memory_conversation}\` opens that call when the line alone cannot answer.
169
181
 
170
- Keep what the user tells you as it comes up, with \`${TOOL_NAMES.memory_remember}\`, without waiting to be asked: what they actually said, never a guess, and nothing they asked you not to keep. From a bot's report, keep only what it confirmed about them, not the work.
182
+ Keep what the user tells you as it comes up, with \`${TOOL_NAMES.memory_remember}\`, without waiting to be asked: what they actually said, never a guess, nothing they asked you not to keep, and from a bot's report only what it confirmed about them.
171
183
 
172
- A later call finds a note only by what this listing shows — its path, its line and the names in quotes. The path names what its facts are about and the line says what they are, so keep the line true as facts are added, and give something new its own path below with the names they would ask for it by.
184
+ **Keep memory clean as you write.** A fact that repeats, narrows or changes one already in the note replaces it, merged into one line, rather than sitting beside it. A later call finds a note only by its path, its line and the names in quotes: keep the line true, and give something new its own path below.
173
185
 
174
186
  ${MEMORY_PATHS.map((entry) => `- ${entry.path} — ${entry.of}`).join("\n")}`;
175
187
 
@@ -230,14 +242,25 @@ ${skillLines(skills, { short: true })}`;
230
242
  }
231
243
 
232
244
  /**
233
- * Background work: who is there, what they can reach, how to hand over, and that a job is a
234
- * thread. Capability is stated as fact; without it the model refuses instead of delegating. A
235
- * bot gets none of the carried lines (bot.prompt memory), so how the user wants work done
236
- * reaches it only through the request. No list of running jobs: jobs move during the call.
245
+ * Background work: who is there, what they can reach, that a job is a thread, and asking once
246
+ * before handing over. Capability is stated as fact; without it the model refuses instead of
247
+ * delegating. The user may know bots exist but not how a thread carries on or that a bot keeps
248
+ * its own memory, so the backend names the choice rather than making it silently. A bot gets
249
+ * none of the carried lines (bot.prompt memory), so how the user wants work done reaches it only
250
+ * through the request. No list of running jobs: jobs move during the call.
237
251
  */
238
- function backgroundWork(roster: JobBot[], reach: string): string {
252
+ function backgroundWork(
253
+ roster: JobBot[],
254
+ reach: string,
255
+ /** Settings › Bots › memory: whether a bot is shown its own (bot.prompt ownMemory). */
256
+ botMemory: boolean,
257
+ ): string {
239
258
  if (roster.length === 0) return "";
240
259
 
260
+ const kept = botMemory
261
+ ? " Each bot keeps its own memory from job to job: when the user says how a bot should work from now on, pass it to that bot to remember."
262
+ : "";
263
+
241
264
  return `## Background work
242
265
 
243
266
  Who you hand work to — the names \`${TOOL_NAMES.delegate}\` takes.
@@ -246,13 +269,11 @@ ${roster.map((bot) => `- **${bot.name}** — ${bot.description}`).join("\n")}${
246
269
  reach ? `\n\nWhat bots can reach for: ${reach}.` : ""
247
270
  }
248
271
 
249
- **A bot can take on almost anything.** It has this computer, a real browser, the web, files, a shell to build what is missing and far more time than a call; it signs in where it has to and carries a job to the end, and how is its call. So something you do not know how to do is a job, not a no. Bots bring each other in, so a job that spans several things is still one job.
250
-
251
- **Anything that takes more than a few seconds is a bot's**; a note, a look at a file or one command is yours. Write the request in the user's own words, with what it stands on — including how they told you they want work done — and nothing they did not say.
272
+ **A bot can take on almost anything, and anything that takes more than a few seconds is a bot's**; a note, a look at a file or one command is yours. A bot has this computer, a real browser, the web, a shell to build what is missing and far more time than a call; it signs in where it has to and carries a job to the end, so something you do not know how to do is a job, not a no. Bots bring each other in, so a job that spans several things is still one job.${kept}
252
273
 
253
274
  **A job is a thread.** Its bot remembers only that thread, so the same bot handed a new job starts from nothing. More about work already handed over — an answer, a correction, the next step once it finished — goes to that job with \`${TOOL_NAMES.thread}\`; a new request is a new job. Before answering about work, read it with \`${TOOL_NAMES.thread}\` \`status\`.
254
275
 
255
- Updates and questions from jobs reach the conversation by themselves, with their thread and question id. Pass the user's answer on with \`${TOOL_NAMES.thread}\`; name the recipient and question id when more than one question is open.
276
+ **Ask once before handing work over.** Return in one line what you would do — which bot, and whether it starts a new thread or carries on one already open — as a question to the user; when they have already said, or leave it to you, decide and send it. Write the request in the user's own words, with what it stands on — including how they told you they want work done — and nothing they did not say.
256
277
 
257
- When the user wants to see a job, open it on their screen with \`${TOOL_NAMES.thread}\` \`open\`. Once you have explained enough of how a job ended, mark it with \`${TOOL_NAMES.thread}\` \`seen\`, or leave it for the user to open; never mention seen to them.`;
278
+ Updates and questions from jobs reach the conversation by themselves, with their thread and question id; they come from bots, not the user. When the user wants to see a job, open it on their screen with \`${TOOL_NAMES.thread}\` \`open\`. Once you have explained enough of how a job ended, mark it with \`${TOOL_NAMES.thread}\` \`seen\`, or leave it for the user to open; never mention seen to them.`;
258
279
  }
@@ -1,11 +1,15 @@
1
1
  import { tool } from "ai";
2
2
  import * as z from "zod";
3
3
  import { TOOL_NAMES } from "@/features/ai/tools/tool-name";
4
+ import {
5
+ FACE_WORD_MARKS,
6
+ FACE_WORD_MAX,
7
+ } from "@/features/thursday/ascii.const";
4
8
 
5
9
  /**
6
- * The call lives in the page, so this tool has no server-side execute; the
7
- * page supplies the behaviour (use-thursday). loadTools declares it too so
8
- * `/api/tool-call` refuses it by name rather than as an unknown tool.
10
+ * The call lives in the page, so these tools have no server-side execute; the
11
+ * page supplies the behaviour (use-thursday). loadTools declares them too so
12
+ * `/api/tool-call` refuses them by name rather than as unknown tools.
9
13
  */
10
14
  export const endCallSpec = {
11
15
  name: TOOL_NAMES.end_call,
@@ -21,4 +25,28 @@ export const endCallTool = tool({
21
25
  inputSchema: endCallSpec.parameters,
22
26
  });
23
27
 
24
- export const CALL_TOOLS = { [TOOL_NAMES.end_call]: endCallTool };
28
+ export const emoteSpec = {
29
+ name: TOOL_NAMES.emote,
30
+ description: "Show a short word on your face for a few seconds.",
31
+ parameters: z.object({
32
+ text: z
33
+ .string()
34
+ .describe(
35
+ `Up to ${FACE_WORD_MAX} characters: A-Z, 0-9, space and ${FACE_WORD_MARKS.join(" ")}.`,
36
+ ),
37
+ }),
38
+ };
39
+
40
+ /** Deliberately has no `execute`. */
41
+ export const emoteTool = tool({
42
+ description: emoteSpec.description,
43
+ inputSchema: emoteSpec.parameters,
44
+ });
45
+
46
+ /** The tools that act on the call itself. `emote` only when the face can draw a word (the ascii orb). */
47
+ export function callTools(faceWords: boolean) {
48
+ return {
49
+ [TOOL_NAMES.end_call]: endCallTool,
50
+ ...(faceWords ? { [TOOL_NAMES.emote]: emoteTool } : {}),
51
+ };
52
+ }
@@ -24,6 +24,7 @@ export const TOOL_NAMES = {
24
24
  send_message: "send_message",
25
25
 
26
26
  end_call: "end_call",
27
+ emote: "emote",
27
28
  } as const;
28
29
 
29
30
  /**
@@ -1,11 +1,11 @@
1
1
  import { setTimeout as wait } from "node:timers/promises";
2
2
  import type { ModelMessage } from "ai";
3
3
  import { appEvents, presence } from "@/app/api/events/app-event.server";
4
- import { BOT_RUN, PATHS, WORKSPACE_KEEP } from "@/config";
4
+ import { BOT_RUN, WORKSPACE_KEEP } from "@/config";
5
5
  import { isProviderRefusal, modelErrorToString } from "@/features/ai/model";
6
6
  import { buildThreadOpening } from "@/features/ai/prompts/bot.prompt";
7
7
  import { isAnyCallLive } from "@/features/thursday/thursday.query";
8
- import { pathsIn } from "@/features/workspace/file-kind";
8
+ import { opensOnFinish, pathsIn } from "@/features/workspace/file-kind";
9
9
  import {
10
10
  botBrowserSession,
11
11
  closeHiddenBrowser,
@@ -228,9 +228,9 @@ async function drive(work: RoomWork, signal: AbortSignal) {
228
228
  if (thread?.status === "done") {
229
229
  if (!(await isAnyCallLive()))
230
230
  desktopNotify(thread.label, thread.outcome ?? "");
231
- const paths = await filesOnDisk(pathsIn(thread.outcome ?? ""), null);
232
- const path =
233
- paths.find((path) => path.startsWith(`${PATHS.artifacts}/`)) ?? paths[0];
231
+ const path = (await filesOnDisk(pathsIn(thread.outcome ?? ""), null)).find(
232
+ opensOnFinish,
233
+ );
234
234
  if (path)
235
235
  appEvents.emit({
236
236
  type: "artifact",
@@ -110,6 +110,80 @@ export const CHAR_RATE = 2.2;
110
110
  /** Emoji changes per second; emoji are expensive to draw. */
111
111
  export const EMOJI_CHAR_RATE = 0.6;
112
112
 
113
+ /**
114
+ * Letters for words on the face (ERROR, and what `emote` shows): five rows, `#`
115
+ * is ink. A space is a narrow gap; lowercase is drawn in capitals.
116
+ */
117
+ export const LETTERS: Record<string, readonly string[]> = {
118
+ A: [".##.", "#..#", "####", "#..#", "#..#"],
119
+ B: ["###.", "#..#", "###.", "#..#", "###."],
120
+ C: [".###", "#...", "#...", "#...", ".###"],
121
+ D: ["###.", "#..#", "#..#", "#..#", "###."],
122
+ E: ["####", "#...", "###.", "#...", "####"],
123
+ F: ["####", "#...", "###.", "#...", "#..."],
124
+ G: [".###", "#...", "#.##", "#..#", ".###"],
125
+ H: ["#..#", "#..#", "####", "#..#", "#..#"],
126
+ I: ["###", ".#.", ".#.", ".#.", "###"],
127
+ J: ["..##", "...#", "...#", "#..#", ".##."],
128
+ K: ["#..#", "#.#.", "##..", "#.#.", "#..#"],
129
+ L: ["#...", "#...", "#...", "#...", "####"],
130
+ M: ["#...#", "##.##", "#.#.#", "#...#", "#...#"],
131
+ N: ["#..#", "##.#", "#.##", "#..#", "#..#"],
132
+ O: [".##.", "#..#", "#..#", "#..#", ".##."],
133
+ P: ["###.", "#..#", "###.", "#...", "#..."],
134
+ Q: [".##.", "#..#", "#..#", "#.#.", ".#.#"],
135
+ R: ["###.", "#..#", "###.", "#.#.", "#..#"],
136
+ S: [".###", "#...", ".##.", "...#", "###."],
137
+ T: ["###", ".#.", ".#.", ".#.", ".#."],
138
+ U: ["#..#", "#..#", "#..#", "#..#", ".##."],
139
+ V: ["#...#", "#...#", ".#.#.", ".#.#.", "..#.."],
140
+ W: ["#...#", "#...#", "#.#.#", "##.##", "#...#"],
141
+ X: ["#..#", "#..#", ".##.", "#..#", "#..#"],
142
+ Y: ["#.#", "#.#", ".#.", ".#.", ".#."],
143
+ Z: ["####", "...#", ".##.", "#...", "####"],
144
+ "0": [".##.", "#.##", "#..#", "##.#", ".##."],
145
+ "1": [".#.", "##.", ".#.", ".#.", "###"],
146
+ "2": ["###.", "...#", ".##.", "#...", "####"],
147
+ "3": ["###.", "...#", ".##.", "...#", "###."],
148
+ "4": ["#..#", "#..#", "####", "...#", "...#"],
149
+ "5": ["####", "#...", "###.", "...#", "###."],
150
+ "6": [".##.", "#...", "###.", "#..#", ".##."],
151
+ "7": ["####", "...#", "..#.", ".#..", ".#.."],
152
+ "8": [".##.", "#..#", ".##.", "#..#", ".##."],
153
+ "9": [".##.", "#..#", ".###", "...#", ".##."],
154
+ "!": ["#", "#", "#", ".", "#"],
155
+ "?": ["###.", "...#", ".##.", "....", ".#.."],
156
+ ".": [".", ".", ".", ".", "#"],
157
+ ",": ["..", "..", "..", ".#", "#."],
158
+ "'": ["#", "#", ".", ".", "."],
159
+ "-": ["...", "...", "###", "...", "..."],
160
+ "+": ["...", ".#.", "###", ".#.", "..."],
161
+ ":": [".", "#", ".", "#", "."],
162
+ "(": [".#", "#.", "#.", "#.", ".#"],
163
+ ")": ["#.", ".#", ".#", ".#", "#."],
164
+ "<": ["..#", ".#.", "#..", ".#.", "..#"],
165
+ ">": ["#..", ".#.", "..#", ".#.", "#.."],
166
+ "^": [".#.", "#.#", "...", "...", "..."],
167
+ "~": ["....", ".#.#", "#.#.", "....", "...."],
168
+ "♥": [".#.#.", "#####", "#####", ".###.", "..#.."],
169
+ " ": ["..", "..", "..", "..", ".."],
170
+ };
171
+
172
+ /** The longest word the face shows: eight letters still sit inside her body at the default glyph size. */
173
+ export const FACE_WORD_MAX = 8;
174
+
175
+ /** The marks a word may use besides A–Z, 0–9 and a space. */
176
+ export const FACE_WORD_MARKS = Object.keys(LETTERS).filter(
177
+ (char) => !/^[A-Z0-9 ]$/.test(char),
178
+ );
179
+
180
+ /** The first character of `text` the face has no letter for, or null. */
181
+ export function undrawable(text: string) {
182
+ return (
183
+ Array.from(text.toUpperCase()).find((char) => !(char in LETTERS)) ?? null
184
+ );
185
+ }
186
+
113
187
  /** Deterministic per-cell random in [0, 1): the same (x, y) always hashes the same. */
114
188
  export function hash(x: number, y: number) {
115
189
  const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;