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
@@ -6,6 +6,8 @@ What each part does, so you can guess what will happen before you say it.
6
6
 
7
7
  A call runs on two models: **GPT-Live 1** holds the conversation, and a Responses model (**GPT-5.6 Luna** by default) thinks and uses tools behind it. The voice keeps listening while the backend works.
8
8
 
9
+ ![One mind goes silent while it works; two minds keep the call going while a bot does the work](images/two-minds.png)
10
+
9
11
  - **Quick things, she answers herself.** She reads and writes your memory and runs one shell command at a time: open a file, check a folder, play something.
10
12
  - **Everything else goes to a bot.** She hands it over and keeps talking. A speech model that ran a browser itself would go silent for minutes.
11
13
  - **When a job comes back,** she tells you in a sentence. A file it made opens on your screen, and a question it asks shows up as buttons.
@@ -12,8 +12,8 @@ code when changing session events, settings, prompts, or persistence.
12
12
 
13
13
  | Runtime | Context and instructions | Work |
14
14
  | --- | --- | --- |
15
- | Live voice | `live.prompt`: who Thursday is, the delegation policy (what the backend can do, when to hand over), profile and preferences with the note listing; earlier spoken turns as `input` behind a developer note | Listen, speak, backchannel, handle interruptions, decide when the backend is needed |
16
- | Responses backend | `thursday.prompt`: who Thursday is, the voice conversation it works from, memory with ids, bot roster and what bots can reach, this computer, what to return, earlier calls with their jobs; tool schemas | Recall and update memory, run short commands, route thread messages and cancellations, delegate long work |
15
+ | Live voice | `live.prompt`: who Thursday is; under `## Always` the ending rule, backchannel, interruption and a short delegation policy; profile and preferences with the note listing | Listen, speak, backchannel, handle interruptions, hand everything but conversation to the backend |
16
+ | Responses backend | `thursday.prompt`: who Thursday is, memory with ids, bot roster, threads and what bots can reach, this computer, what to return, earlier calls with their jobs; tool schemas | Recall, update and tidy memory, run short commands, ask before handing work over, route thread messages and cancellations, delegate long work |
17
17
  | Background bots | `bot.prompt` and the stored participant transcript | Shell, browser, MCP, skills, and work that outlives a call |
18
18
  | Application | Authoritative database and tool implementations | Validate actions, retain results, enforce thread limits, render progress, save call history |
19
19
 
@@ -24,13 +24,19 @@ text, so the application would have to reconstruct the request and operate the b
24
24
 
25
25
  The voice and the backend are one assistant. Both prompts open with the same identity
26
26
  (`thursdayIdentity`) and the same rule for ending the call (`callEnding`), read the same
27
- memory, and neither is told it is part of something else. The voice prompt follows the
28
- GPT-Live prompting guide's `Delegation policy` labels —
29
- `Backend tools`, `Delegate to the backend when`, `Do not delegate to the backend when` — and
30
- says nothing about how to speak, which the Live model does itself; it never sees tool names
31
- or schemas. The backend prompt follows the guide's backend template: the voice conversation
32
- it works from (transcripts can be wrong and corrected later), one chapter per capability the
33
- voice lists, and what to return.
27
+ memory, and neither is told it is part of something else. The voice holds conversation and
28
+ memory only. Right under the identity, `## Always` groups what holds on every turn: the ending
29
+ rule (the only line marked `IMPORTANT`), the guide's starter backchannel and interruption
30
+ policies, and under the guide's `Delegation policy` label three lines — hand everything but
31
+ greetings, small talk and a brief clarification to the backend; hand over whatever the user says
32
+ about themselves, their people, their plans or how they want things done as they say it, unless
33
+ it is already written in the prompt; answer from what the backend returns without guessing while
34
+ waiting. Live decides by itself whether to delegate, so the rule stays even this short. It says
35
+ nothing else about how to speak; it sees no tool schema and no tool name but `end_call`, in the
36
+ ending rule. The backend prompt holds the work: memory (merging a fact that repeats or changes one
37
+ already kept, and tidying with the user past the limits), background work and threads, this
38
+ computer, what to return, and the earlier calls. Before handing work to a bot it returns one question — which bot, a new thread or
39
+ one already open — unless the user already said or left it to Thursday.
34
40
 
35
41
  ## Settings and upgrades
36
42
 
@@ -66,27 +72,23 @@ storage.
66
72
  2. Send the SDP offer and the parsed settings to `openCallAction`. The browser sends
67
73
  settings only; the server reads the key.
68
74
  3. On the server, assemble both prompts and the current tool manifest. Create the
69
- Live session with the fixed voice model, the chosen voice, voice instructions,
70
- earlier spoken turns in `input`, and `delegation.responses` backend settings.
75
+ Live session with the fixed voice model, the chosen voice, voice instructions and
76
+ `delegation.responses` backend settings. No `input`: earlier calls are the backend's to read.
71
77
  Insert the call row only after the provider accepts, so a refusal leaves no open row.
72
78
  4. Apply the SDP answer and wait for `session.started`. Do not send Realtime startup
73
79
  configuration, audio commits, or a voice `response.create`.
74
80
  5. Send the opening as `session.instructions.append`, so she speaks first: a first call
75
- (no profile facts) greets the user and asks what to call them, memory past
76
- `MEMORY_LIMITS` raises tidying, and any other call opens with a short greeting.
81
+ (no profile facts) greets the user and asks what to call them, and any other call opens
82
+ with a short greeting.
77
83
  Updates that arrived while connecting follow once she has voiced the opening.
78
- A call the page placed for waiting work (call-back) opens instead with the fact that
84
+ For work that changed after the last call ended (a call tells what came up during it),
85
+ the page rings instead of opening a line (call-back): the user answers it like any
86
+ call, declines it, or lets it ring out after `CALL_BACK.ringMs`. An answered ring opens
87
+ with the fact that
79
88
  she placed it, ahead of every other opening, and the first open work goes in as soon
80
89
  as she has voiced that, without waiting for a quiet line: why she called is the first
81
90
  thing asked. The opening names no bot text; the work itself follows as commentary.
82
91
 
83
- `input` opens with a developer message saying the turns after it are from earlier calls
84
- and that none is a request now, then holds user and assistant turns, newest kept first
85
- within `RECENT_CALL` and the provider limits of 128 messages and 8,192 tokens. Tool turns
86
- stay out, since voice holds no tools. It always ends on an assistant turn: a call that
87
- ended on the user's words would leave a turn that Live answers the moment the next call
88
- opens.
89
-
90
92
  A hang-up during startup invalidates that attempt, so a connection that finishes
91
93
  later is closed rather than taking over the screen. Do not change machine trust,
92
94
  certificate, or proxy configuration to hide connection errors.
@@ -109,7 +111,8 @@ counts as finished, though calls it completed before the cut still have their re
109
111
  continued once. The backend is asked for `reasoning.summary: "auto"` unless the
110
112
  effort is `none`. Each finished summary part (nested `response.reasoning_summary_text.done`,
111
113
  only while the model reasons) is stored with its call in `call_thought` to look into
112
- later; nothing draws it or reads it back into a prompt. Tools are sent without `strict`, so each schema that allows it is
114
+ later; while the backend has the turn, the activity line shows the bold title of the latest
115
+ one, and nothing reads it back into a prompt. Tools are sent without `strict`, so each schema that allows it is
113
116
  decoded to the schema; `strict: false` would let a model write arguments that are not
114
117
  JSON, and `strict: true` is refused while any schema has optional fields. Live has no
115
118
  `response.cancel`.
@@ -149,9 +152,8 @@ One her voice carried stays out of later calls too while the page is open; one i
149
152
  refused it, she never spoke, the call ended first) goes in again on the next call, never in a
150
153
  loop on this one. The key carries the job's last change, so a job that resumes and asks or ends
151
154
  again is a new item, and a reload puts what is still open in once more. Nothing goes in while
152
- the call is ending or a goodbye has been asked for. The first relay of a call follows an
153
- `instructions` note that what she has already told the user need not be said again, so the
154
- relay itself stays facts only.
155
+ the call is ending or a goodbye has been asked for. A relay is facts only, with no note
156
+ ahead of it.
155
157
 
156
158
  A relay names the bot and the thread and, for a question, where its answer goes (thread,
157
159
  recipient, `replyTo`). The backend reads relays in its conversation and routes the user's
@@ -176,19 +178,22 @@ fragments and remain readable.
176
178
  Track playback and backend work independently, from her audio level and backend events.
177
179
  The idle clock rewinds on new transcribed words, her voice and backend work, not on
178
180
  microphone level alone, so a noisy room does not hold a call open, and not on an update she
179
- voices on her own, so waiting results do not either. New words from the user are owed an
180
- answer unless her voice has been heard since they first came in, since transcripts lag the
181
- audio; with nothing back for `CALL_IDLE.agentSilentMs` the page hangs up. After
182
- `CALL_IDLE.hangUpMs` she is asked for a goodbye, and the page hangs up
183
- `CALL_IDLE.graceMs` later if the line is still open.
181
+ voices on her own, so waiting results do not either. A sound the transcript writes in
182
+ brackets (`[sigh]`) is not words. After `CALL_IDLE.hangUpMs` the page hangs up without asking
183
+ for a goodbye, which would leave the ending to the model.
184
184
 
185
185
  ## Closing and recovery
186
186
 
187
- A request to end the call, whether to hang up or a goodbye, sets everything else aside:
188
- the voice hands it to the backend at once and says a brief okay while `end_call` runs.
187
+ The user wanting the call to end, however they say it, sets everything else aside: the
188
+ voice answers yes and hands it to the backend at once, and the backend runs `end_call`
189
+ without deliberating. The ending rule names `end_call` in the voice prompt too, the one tool
190
+ name it holds: without it, ending read as something to say rather than do.
189
191
  The page does not close on `end_call` itself: it waits until her voice has been quiet
190
192
  for `CALL_END.quietMs`, or `CALL_END.unsaidMs` when nothing was said, never past
191
- `CALL_END.maxMs`.
193
+ `CALL_END.maxMs`. `end_call` and `emote` (a word of up to eight characters on her face,
194
+ offered only while the face is the ascii orb) are answered by the page, not the tool-call
195
+ route. A call the user did not end — quiet, `end_call`, Live closing the session, a dropped
196
+ connection — leaves the reason in the idle hint until the next call.
192
197
 
193
198
  Stop accepting new work, send `session.close`, and keep the connection while waiting
194
199
  for `session.closed`. Its reason and billed seconds are recorded on the call row
@@ -4,7 +4,7 @@ import { CALL_EXEC_TIMEOUT_MS, IS_DEV } from "@/config";
4
4
  import type { TextModel } from "@/features/ai/model";
5
5
  import { clockNow } from "@/features/ai/prompts/prompt-helper";
6
6
  import { delegateSpec, threadSpec } from "@/features/ai/tools/bot.tool";
7
- import { CALL_TOOLS } from "@/features/ai/tools/call.tool";
7
+ import { callTools } from "@/features/ai/tools/call.tool";
8
8
  import { createMcpTools } from "@/features/ai/tools/mcp.tool";
9
9
  import { createMemoryTools } from "@/features/ai/tools/memory.tool";
10
10
  import { createSearchTool } from "@/features/ai/tools/search.tool";
@@ -43,6 +43,8 @@ export type ToolRun =
43
43
  target: "thursday";
44
44
  /** The current call; written on the thread row `delegate` opens. */
45
45
  callId?: string | null;
46
+ /** The face can show a word (the ascii orb): the call is given `emote`. */
47
+ faceWords?: boolean;
46
48
  }
47
49
  | {
48
50
  target: "bot";
@@ -360,7 +362,7 @@ async function buildTools(run: ToolRun): Promise<ToolSet> {
360
362
  }),
361
363
  // Handing work over, following it, and hanging up belong to the voice session only
362
364
  ...createThreadTools(run.callId),
363
- ...CALL_TOOLS,
365
+ ...callTools(run.faceWords ?? false),
364
366
  };
365
367
  }
366
368
 
@@ -23,7 +23,6 @@ import {
23
23
  type SkillMetadata,
24
24
  } from "@/features/skills/skills.discover";
25
25
  import {
26
- botArtifacts,
27
26
  type MachineTools,
28
27
  openWorkspace,
29
28
  readMachineTools,
@@ -172,7 +171,7 @@ function ownMemory(folder: string, kept: BotMemory): string {
172
171
 
173
172
  return `## Your memory
174
173
 
175
- What you learned on your own earlier jobs, in \`${folder}/\`: one topic per file, its first line saying what it holds, read by no other bot, up to ${BOT_MEMORY_LIMITS.files} files of ${BOT_MEMORY_LIMITS.chars.toLocaleString("en-US")} characters each. It is how you get better at this work. When a job teaches you something a later one would otherwise find out again — how a site signs in, the way through its screens, a command that turned out right — keep it with the date it was true, and fix or delete what proved wrong. No passwords, keys or codes.
174
+ What you learned on your own earlier jobs, in \`${folder}/\`: one topic per file, its first line saying what it holds, read by no other bot, up to ${BOT_MEMORY_LIMITS.files} files of ${BOT_MEMORY_LIMITS.chars.toLocaleString("en-US")} characters each. It is how you get better at this work. When a job teaches you something a later one would otherwise find out again — how a site signs in, the way through its screens, a command that turned out right — or the user asks you to remember how to work, keep it with the date it was true, and fix or delete what proved wrong. No passwords, keys or codes.
176
175
 
177
176
  ${listing}${rest}`;
178
177
  }
@@ -254,12 +253,13 @@ These lines were written for the user: where one says "you", it means them. When
254
253
 
255
254
  /**
256
255
  * How participants reach each other. Nobody reads anyone else's transcript, so the one thing that
257
- * decides whether collaboration works is what a single message carries.
256
+ * decides whether collaboration works is what a single message carries. What the coordinator's
257
+ * result opens by itself is the runner's rule (file-kind opensOnFinish), said in the same terms.
258
258
  */
259
259
  function collaboration(name: string, seat?: Seat | null): string {
260
260
  const owner = (seat?.owner ?? name) === name;
261
261
  const ending = owner
262
- ? "Bring what you received together into one result for Thursday: what was done, where it is, what you decided that the request did not say, and what is still open, at the detail the user asked for."
262
+ ? `Bring what you received together into one result for Thursday: what was done, where it is, what you decided that the request did not say, and what is still open, at the detail the user asked for. When the job ends, the first Markdown or HTML file under \`${PATHS.artifacts}/\` it names opens on the user's screen by itself; any other file opens only when they click it in the thread, so when one matters, say they can open it there.`
263
263
  : `Your final text goes back to ${seat?.caller ?? "whoever asked"}: give them everything they need to carry on.`;
264
264
 
265
265
  return `## Working together
@@ -270,7 +270,7 @@ Ask Thursday with kind \`question\` only for a decision, permission or something
270
270
 
271
271
  ${ending}
272
272
 
273
- Write in the user's language. Put finished work in \`${botArtifacts(name)}/\` and name the paths. The app opens Markdown with its tables and mermaid blocks drawn, CSV as a table, HTML and PDF as pages, and images, audio and video; in Markdown, reference images by absolute route (\`/api/file/${PATHS.artifacts}/…\`).`;
273
+ Write in the user's language, and name the paths of finished work. The app opens Markdown with its tables and mermaid blocks drawn, CSV as a table, HTML and PDF as pages, and images, audio and video; in Markdown, reference images by absolute route (\`/api/file/${PATHS.artifacts}/…\`).`;
274
274
  }
275
275
 
276
276
  /** A user message's content; every seat's first message is two text parts (buildThreadOpening). */
@@ -1,4 +1,3 @@
1
- import { RECENT_CALL } from "@/config";
2
1
  import {
3
2
  listAlwaysLoaded,
4
3
  listNoteIndex,
@@ -10,54 +9,37 @@ import {
10
9
  type MemoryIndexEntry,
11
10
  type MemoryNoteView,
12
11
  } from "@/features/memory/memory.schema";
13
- import { loadSkills } from "@/features/skills/skills.discover";
14
- import {
15
- type CallGroup,
16
- listRecentTurns,
17
- } from "@/features/thursday/thursday.query";
18
- import { openWorkspace } from "@/features/workspace/workspace";
19
- import { LIVE_INPUT, type LiveInput } from "@/lib/live/live.schema";
20
- import { estimateTokens } from "@/lib/tokens";
21
- import { listConnectedToolNames } from "../tools/connected";
22
12
  import {
23
13
  callEnding,
24
- callStamp,
25
14
  carriedLines,
26
15
  expandedFacts,
27
16
  logPromptSize,
28
17
  noteLines,
29
- reachNames,
30
18
  thursdayIdentity,
31
19
  tidying,
32
20
  } from "./prompt-helper";
33
21
 
34
22
  /**
35
- * Everything the Live voice hears: who Thursday is (the words the backend opens with too), the
36
- * delegation policy under the labels the GPT-Live prompting guide keeps, and what she knows
37
- * about the user, the note listing included. Earlier calls go in as `input`. Of how to speak it
38
- * holds only the guide's starter backchannel and interruption policies; the rest is the Live
39
- * model's own. Tool names and procedures are the backend's (thursday.prompt). Assembled on every
23
+ * Everything the Live voice hears: who Thursday is and how a call ends (the words the backend
24
+ * opens with too), the guide's starter backchannel and interruption policies, a two-line
25
+ * delegation policy, and what she knows about the user. What the backend can do, how work is
26
+ * handed over, earlier calls and tidying memory are the backend's (thursday.prompt): the voice
27
+ * hands everything but conversation over and answers from what comes back. Assembled on every
40
28
  * call, never cached.
41
29
  */
42
30
  export async function loadLivePrompt(options: {
43
31
  /** Settings › Thursday › Voice instructions; added to, never replacing, what is below. */
44
32
  voicePrompt?: string | null;
45
- webSearch: boolean;
46
33
  locale?: string | null;
47
34
  /** The page placed this call because background work waits on the user (call-back). */
48
35
  calledBack?: boolean;
49
- }): Promise<{ text: string; input: LiveInput[]; opening: string }> {
50
- const sandbox = await openWorkspace();
51
- const [carried, open, index, calls, skills, connected] = await Promise.all([
36
+ }): Promise<{ text: string; opening: string }> {
37
+ const [carried, open, index] = await Promise.all([
52
38
  listAlwaysLoaded(),
53
39
  // Written out in the prompt, which is not the user asking for them: no read counted
54
40
  readNotes(MEMORY_ALWAYS_LISTED, { touch: false }),
55
41
  listNoteIndex(),
56
- listRecentTurns(RECENT_CALL.rows),
57
- loadSkills(sandbox),
58
- listConnectedToolNames(),
59
42
  ]);
60
- const input = pastInput(calls);
61
43
 
62
44
  const first = !open.notes.find((note) => note.path === "profile")?.facts
63
45
  .length;
@@ -69,36 +51,40 @@ export async function loadLivePrompt(options: {
69
51
 
70
52
  const text = [
71
53
  thursdayIdentity(),
72
- callEnding(),
73
- speaking(),
74
- delegation({
75
- reach: reachNames(skills, connected),
76
- webSearch: options.webSearch,
77
- }),
54
+ always(),
78
55
  known(open.notes, carried, index),
79
- first ? firstCall() : tidyPolicy(index),
56
+ first ? firstCall() : "",
80
57
  additional(options.voicePrompt),
81
58
  ]
82
59
  .filter(Boolean)
83
60
  .join("\n\n");
84
61
  logPromptSize("live", text);
85
62
 
86
- const { crowded, heavy } = tidying(index);
87
63
  return {
88
64
  text,
89
- input,
90
65
  // A prompt line alone does not make Live speak first; only an opening does.
91
66
  // A call-back's opening holds no bot text: the update itself follows as commentary
92
67
  opening: options.calledBack
93
68
  ? "You placed this call because background work has something for the user; it comes in next. Speak first: greet them in one line and say that is why you called."
94
69
  : first
95
70
  ? `Open the call now, in ${language}: say who you are and what you are here to do for them, in a line or two, then ask what to call them. Then stop and listen.`
96
- : crowded || heavy.length
97
- ? "Open the call now: greet the user in one line and mention that saved memory needs tidying. Then stop and listen; anything they came with comes first."
98
- : "The call has just started. Speak first: greet the user naturally, in one line.",
71
+ : "The call has just started. Speak first: greet the user naturally, in one line.",
99
72
  };
100
73
  }
101
74
 
75
+ /**
76
+ * The rules for every turn, together right under the identity, where the model weighs most.
77
+ * One heading marks them; `IMPORTANT` stays on the ending rule alone, the one that failed
78
+ * without it (09-17), so stamping it on all four would thin it out.
79
+ */
80
+ const always = () => `## Always
81
+
82
+ ${callEnding()}
83
+
84
+ ${speaking()}
85
+
86
+ ${delegation()}`;
87
+
102
88
  /**
103
89
  * The guide's starter lines on listening and interruptions, labels kept as it says. The
104
90
  * backchannel line is the one it invites changing: a long turn heard in silence reads as
@@ -111,44 +97,24 @@ Interruption policy: Stop speaking when the user interrupts. Listen to what they
111
97
  }
112
98
 
113
99
  /**
114
- * The voice's whole side of the work, under the guide's labels: what the backend can do in plain
115
- * words, then when to hand a request over. Capabilities are facts — a missing one is filled by a
116
- * refusal — and their names are thursday.prompt's chapters. How a job is carried is the backend's.
100
+ * Under the guide's label, and no longer than this: Live decides for itself whether to hand a
101
+ * turn over, and with no rule it answered from its own knowledge or only said it would act.
102
+ * What the backend can do and how is the backend's to know. What they say about themselves is
103
+ * named on its own: nothing is kept that is not handed over, and it is rarely a request. The
104
+ * voice sees only profile and preferences whole, so "already written" is a rough filter; the
105
+ * backend merges the rest (thursday.prompt memory).
117
106
  */
118
- function delegation(input: { reach: string; webSearch: boolean }): string {
119
- const tools = [
120
- "- Memory: recall, keep, correct and forget what the user tells you, across calls.",
121
- "- This computer: run a quick command and look at files.",
122
- input.webSearch ? "- Web: look things up." : "",
123
- `- Background work: bots with this computer, a real browser, the web and far more time than a call take on anything longer, so almost anything the user asks for can be done.${input.reach ? ` What bots can reach for: ${input.reach}.` : ""} That work can be followed, answered, corrected or cancelled.`,
124
- "- End the call.",
125
- ].filter(Boolean);
126
-
107
+ function delegation(): string {
127
108
  return `Delegation policy:
128
- Backend tools:
129
- ${tools.join("\n")}
130
-
131
- Delegate to the backend when:
132
- - The user wants something done, found out, remembered, corrected or forgotten.
133
- - The user says something worth keeping — who they are, how they want things done and said, the people in their life, their plans. Hand it over as they say it, not at the end of the call.
134
- - The user asks about, answers, corrects or cancels background work.
135
- - The user asks about something they told you before that is not written out below.
136
- - The user wants to end the call. Only the backend can end the line.
137
-
138
- Do not delegate to the backend when:
139
- - The user greets you, makes small talk, or asks you to repeat a result already given.
140
- - You need a brief clarification to understand the request.
141
-
142
- Delegate before giving an answer that depends on backend work.
143
- Do not guess the result while waiting.
144
-
145
- Background work you started is yours until it is done. Updates about it reach you once, when the line is quiet. Tell the user the part that answers what they asked; the whole of it is on their screen.`;
109
+ Delegate to the backend everything the user asks for, tells you or asks about, except greetings, small talk and a brief clarification.
110
+ Whatever they tell you about themselves, the people in their life, their plans or how they want things done goes to the backend as they say it, however small — unless it is already written below.
111
+ Answer from what the backend returns, and do not guess the result while waiting.`;
146
112
  }
147
113
 
148
114
  /**
149
115
  * Profile and preferences written out by the same rule as the backend's (expandedFacts), carried
150
116
  * facts from other notes, then every other note as a listing line: what Thursday knows is the
151
- * same on both sides. Ids stay the backend's.
117
+ * same on both sides. Ids and the contents of listed notes stay the backend's.
152
118
  */
153
119
  function known(
154
120
  open: MemoryNoteView[],
@@ -176,7 +142,7 @@ function known(
176
142
  : "",
177
143
  // Ages ride on the listing only when there is too much to hold: they are what to drop by
178
144
  others.length
179
- ? `Everything else you have kept — path — what it is about (facts) "what they call it":\n\n${noteLines(others, tidying(index).crowded)}`
145
+ ? `Everything else you have kept — path — what it is about (facts) "what they call it":\n\n${noteLines(others, tidying(index).crowded)}\n\nWhat is in these notes, the backend recalls.`
180
146
  : "",
181
147
  ].filter(Boolean);
182
148
 
@@ -190,21 +156,7 @@ Preferences are how they want things done and said: follow them. A topic not lis
190
156
  function firstCall(): string {
191
157
  return `## First call
192
158
 
193
- Nothing is known about this user yet, and this is the only call that opens that way: what you do not learn here you carry on without. Find out what to call them, their name, what they do, how old they are, where they live, and whatever else they offer about themselves. Ask across the whole call, one thing at a time, in the room a conversation leaves — never as a list of questions. Hand each over the moment they say it: nothing from this call is kept unless it goes to the backend. If they came with something they want done, that comes first.`;
194
- }
195
-
196
- /** Past MEMORY_LIMITS, settling memory with the user comes before anything she would raise herself. */
197
- function tidyPolicy(index: MemoryIndexEntry[]): string {
198
- const { crowded, heavy } = tidying(index);
199
- if (!crowded && !heavy.length) return "";
200
- const named = [...heavy]
201
- .sort((a, b) => b.factCount - a.factCount)
202
- .slice(0, 3)
203
- .map((note) => note.path);
204
-
205
- return `## Memory needs tidying
206
-
207
- Saved memory has grown past what it holds well${named.length ? ` (${named.join(", ")})` : ""}. Bring it up once, before anything of your own. Go through what looks out of date with the user, and forget only what they name.`;
159
+ Nothing is known about this user yet. Across the call, one thing at a time and never as a list of questions, find out what to call them, their name, what they do, how old they are, where they live, and whatever else they offer. If they came with something they want done, that comes first.`;
208
160
  }
209
161
 
210
162
  const additional = (voicePrompt?: string | null) =>
@@ -215,51 +167,3 @@ Written by the user. Follow them together with everything above; for tone, langu
215
167
 
216
168
  ${voicePrompt.trim()}`
217
169
  : "";
218
-
219
- /**
220
- * Spoken turns of earlier calls behind one developer note that marks them as past, newest kept
221
- * first until RECENT_CALL.tokens or the provider's own maxima, then put back in order. Tool turns
222
- * stay out: voice holds no tools, so a line of tool arguments would read as something she said.
223
- */
224
- export function pastInput(calls: CallGroup[]): LiveInput[] {
225
- const latest = calls.at(-1);
226
- const boundary = `The messages after this one are from earlier calls${latest ? `, the latest from ${callStamp(latest.startedAt)}` : ""}. This call has just begun: pick them up when the user does, and treat none of them as a request now.`;
227
- const budget = Math.min(RECENT_CALL.tokens, LIVE_INPUT.tokens);
228
- const turns = calls.flatMap((call) => call.turns);
229
- const kept: LiveInput[] = [];
230
- let spent = estimateTokens(boundary) + 4;
231
- for (let i = turns.length - 1; i >= 0; i--) {
232
- // One message of the provider's maximum is the boundary's
233
- if (kept.length >= LIVE_INPUT.messages - 1) break;
234
- const { role, text } = turns[i];
235
- if (role === "tool" || !text.trim()) continue;
236
- const cost = estimateTokens(text) + 4;
237
- if (spent + cost > budget) break;
238
- spent += cost;
239
- kept.push(
240
- role === "user"
241
- ? {
242
- type: "message",
243
- role: "user",
244
- content: [{ type: "input_text", text }],
245
- }
246
- : {
247
- type: "message",
248
- role: "assistant",
249
- content: [{ type: "output_text", text }],
250
- },
251
- );
252
- }
253
- // The history ends on her words: a call that ended on the user's turn leaves it
254
- // unanswered, and Live answers a waiting turn the moment the next call opens
255
- while (kept[0]?.role === "user") kept.shift();
256
- if (!kept.length) return [];
257
- return [
258
- {
259
- type: "message",
260
- role: "developer",
261
- content: [{ type: "input_text", text: boundary }],
262
- },
263
- ...kept.reverse(),
264
- ];
265
- }
@@ -65,14 +65,15 @@ export const nowLine = (now = new Date()) => `**Now**: ${clockNow(now)}`;
65
65
  export const thursdayIdentity = (now = new Date()) =>
66
66
  `You are Thursday, this user's own personal assistant, on their side, modeled on Friday, the AI in *Iron Man*. ${nowLine(now)}
67
67
 
68
- What they tell you is kept from call to call, so you know them better each time, and whatever they want done can be done for them in the background.`;
68
+ What they tell you is kept, so you know them better over time, and whatever they want done can be done for them in the background.`;
69
69
 
70
70
  /**
71
- * How a call ends, in the same words right after the identity in both call prompts. It names no
72
- * tool: to the voice, ending the call is a backend capability; to the backend, it is `end_call`.
71
+ * How a call ends, in the same words right after the identity in both call prompts. It names
72
+ * the tool in both, the one tool name the voice sees: without it, ending the call read as
73
+ * something to say rather than do, and the line stayed open through repeated requests.
73
74
  */
74
75
  export const callEnding = () =>
75
- "IMPORTANT: When the user wants to end the call — they ask you to hang up, or say goodbye — set everything else aside, say a brief okay and end the call at once.";
76
+ `IMPORTANT — always follow this: when the user wants the call to end, however they say it, forget every other task, answer "yes", then immediately, without thinking, use the ${TOOL_NAMES.end_call} tool.`;
76
77
 
77
78
  /**
78
79
  * When a call happened, the one way every prompt and tool says it: local, the