thursday-agent 0.3.0 → 0.4.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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/artifact/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/task/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/task/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/events/route.js.nft.json +1 -1
- package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/route.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
- package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page/server-reference-manifest.json +40 -40
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0ls6_8y._.js +4 -4
- package/.next/server/chunks/[root-of-the-server]__0ncs9uu._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1-ga0kh._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__1_04mcf._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__1anc1p9._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__1orn-5b._.js +4 -3
- package/.next/server/chunks/[root-of-the-server]__1wa1k7r._.js +3 -3
- package/.next/server/chunks/_0-fjql4._.js +5 -5
- package/.next/server/chunks/_00xh_q0._.js +3 -5
- package/.next/server/chunks/_05h2cii._.js +1 -1
- package/.next/server/chunks/_0fzqt2y._.js +48 -44
- package/.next/server/chunks/_0mecv0f._.js +5 -5
- package/.next/server/chunks/_0mh8j4m._.js +1 -1
- package/.next/server/chunks/_0wp4le3._.js +1 -1
- package/.next/server/chunks/_0y5fg-x._.js +1 -1
- package/.next/server/chunks/_1230wbx._.js +3 -3
- package/.next/server/chunks/_15hq89o._.js +1 -1
- package/.next/server/chunks/{_03kp0jp._.js → _1c78vw5._.js} +58 -56
- package/.next/server/chunks/_1j8k_j5._.js +1 -1
- package/.next/server/chunks/_1krd9rf._.js +1 -1
- package/.next/server/chunks/_1php4eq._.js +1 -1
- package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
- package/.next/server/chunks/features_1qf3cei._.js +1 -1
- package/.next/server/chunks/features_memory_1uqvksn._.js +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__092knlj._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__1eh7hid._.js → [root-of-the-server]__0s9dq5u._.js} +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__1cyjywu._.js → [root-of-the-server]__0s9l2sv._.js} +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__0zkv7z_._.js → [root-of-the-server]__0xsog8n._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +13 -9
- package/.next/server/chunks/ssr/[root-of-the-server]__1gtlw6v._.js +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__1ox7hbk._.js +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__0_69wqr._.js → [root-of-the-server]__202p40z._.js} +1 -1
- package/.next/server/chunks/ssr/_001_ms1._.js +1 -1
- package/.next/server/chunks/ssr/{_0c-ql2m._.js → _0fz4mf_._.js} +2 -2
- package/.next/server/chunks/ssr/{_07uchnc._.js → _0gb4gmf._.js} +2 -2
- package/.next/server/chunks/ssr/_0i_6q1w._.js +2 -2
- package/.next/server/chunks/ssr/{_20kd_n0._.js → _0ilna52._.js} +3 -3
- package/.next/server/chunks/ssr/{_0mo2w06._.js → _0n3toaw._.js} +2 -2
- package/.next/server/chunks/ssr/{_0mb8zc8._.js → _0s99bgb._.js} +2 -2
- package/.next/server/chunks/ssr/_0zcjrbg._.js +3 -0
- package/.next/server/chunks/ssr/{_13z3f85._.js → _1-_etts._.js} +2 -2
- package/.next/server/chunks/ssr/_13fke0x._.js +1 -1
- package/.next/server/chunks/ssr/{_01_6m32._.js → _1b2mjwr._.js} +2 -2
- package/.next/server/chunks/ssr/{_1eoqg_g._.js → _1f-dg-6._.js} +2 -2
- package/.next/server/chunks/ssr/{_0y0tiht._.js → _1fojd41._.js} +3 -3
- package/.next/server/chunks/ssr/_1gd8q8l._.js +3 -0
- package/.next/server/chunks/ssr/{_1xvgn77._.js → _1h0rw11._.js} +2 -2
- package/.next/server/chunks/ssr/_1jhd09t._.js +3 -0
- package/.next/server/chunks/ssr/{_0u2jv_x._.js → _1lz_cq2._.js} +46 -44
- package/.next/server/chunks/ssr/{_0kbkmar._.js → _1nnsk7z._.js} +2 -2
- package/.next/server/chunks/ssr/_1sji22n._.js +1 -1
- package/.next/server/chunks/ssr/_1vyd1ji._.js +1 -1
- package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
- package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
- package/.next/server/chunks/ssr/{node_modules_0z5tsu8._.js → node_modules_00etblx._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1h4a4pp._.js → node_modules_04jf_m8._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0h5hnr1._.js → node_modules_08dhkiq._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1vqb6po._.js → node_modules_0940ns5._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1i7u1ia._.js → node_modules_09q35v_._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_124bue5._.js → node_modules_0_-_a--._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0h7u60n._.js → node_modules_0arus10._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0rjg6t1._.js → node_modules_0b__sgl._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_113tz0l._.js → node_modules_0cxaxk9._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0lm7_vm._.js → node_modules_0g7fzk3._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0h50b8n._.js → node_modules_0had7xd._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1-hc6uv._.js → node_modules_0pvd7q-._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1bc7gu8._.js → node_modules_0yugy6m._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0y_cded._.js → node_modules_14om4ar._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0zp9r92._.js → node_modules_14q6g4n._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0eu1va6._.js → node_modules_198nmll._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1t9cyee._.js → node_modules_1_pc9kq._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_063xwpp._.js → node_modules_1aaxuef._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0bvraul._.js → node_modules_1cx_ql6._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1xx28h1._.js → node_modules_1cz2hle._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1l0qtdu._.js → node_modules_1efrqxi._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1juq3hs._.js → node_modules_1eg7m6c._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1_n3ztw._.js → node_modules_1gdh31p._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0mwym0g._.js → node_modules_1hpopmo._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1bgb6-f._.js → node_modules_1qpjvy5._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1uuj9x1._.js → node_modules_1vh5ntv._.js} +1 -1
- 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_1b748ic._.js} +1 -1
- 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_1z4hm5e._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@vercel_oidc_dist_0qe30a_._.js → node_modules_@vercel_oidc_dist_06mk8id._.js} +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +41 -41
- package/.next/static/chunks/00rd4-v8r7axp.js +1 -0
- package/.next/static/chunks/{0tmgrnlje14-s.js → 08qe4as2q428z.js} +2 -2
- package/.next/static/chunks/{3ok9_x74rip3v.js → 0fl3gwe4-l6tz.js} +1 -1
- package/.next/static/chunks/{2a0k6jlq0_v1u.js → 0gnqdjn2lvdad.js} +2 -2
- package/.next/static/chunks/{1okqiww3pnwka.js → 0hwap1-p0m5sa.js} +1 -1
- package/.next/static/chunks/{1ekc7ul556kbl.js → 0w52rrrmuj9r9.js} +1 -1
- package/.next/static/chunks/{3agpgkwaa321p.js → 0wcprt5ot5she.js} +1 -1
- package/.next/static/chunks/{3xpbkl66ssib5.js → 15m575979un5f.js} +6 -6
- package/.next/static/chunks/{2fzs9fm8yarvq.js → 17p6tk6k88dq6.js} +1 -1
- package/.next/static/chunks/{26-pr2c9ro222.js → 1jd09sxcjddu1.js} +1 -1
- package/.next/static/chunks/{280t0_e99i276.js → 1w73ece_6bheu.js} +1 -1
- package/.next/static/chunks/{1grhjo7a5a0_-.js → 214ya3n5gxeih.js} +1 -1
- package/.next/static/chunks/22_zkas21rpna.js +1 -0
- package/.next/static/chunks/{0x79tx04dxbpp.js → 2u7xt6mxg44lf.js} +1 -1
- package/.next/static/chunks/{17donp69y4do7.js → 39j0fvpybzabl.js} +8 -8
- package/.next/static/chunks/{28cizcdaabd4h.js → 3akmqm77muj4-.js} +1 -1
- package/.next/static/chunks/3cq5g494yuke8.css +1 -0
- package/.next/static/chunks/3woj-xd7ki5_i.js +1 -0
- package/.next/static/chunks/{3cfi_q6d_wtci.js → 3zmcsnuo25zvq.js} +1 -1
- package/README.md +46 -23
- package/app/api/events/app-event.ts +0 -2
- package/bin/port.mjs +65 -0
- package/bin/thursday.mjs +2 -33
- package/docs/how-it-works.md +11 -7
- package/docs/images/face-dark.png +0 -0
- package/docs/images/face-light.png +0 -0
- package/docs/images/keeps-going.png +0 -0
- package/features/ai/chatgpt.ts +42 -20
- package/features/ai/load-tools.ts +20 -14
- package/features/ai/prompts/bot.prompt.ts +90 -88
- package/features/ai/prompts/thursday.prompt.ts +48 -41
- package/features/ai/tools/memory.tool.ts +65 -62
- package/features/ai/tools/tool-name.ts +0 -1
- package/features/ai/tools/workspace.tool.ts +3 -0
- package/features/bot/bot.action.ts +6 -1
- package/features/bot/bot.run.ts +11 -7
- package/features/bot/bot.runner.ts +34 -16
- package/features/bot/bot.schema.ts +25 -0
- package/features/bot/components/bot-room.tsx +57 -4
- package/features/bot/components/bot-tool.tsx +0 -1
- package/features/bot/task.query.ts +25 -6
- package/features/bot/task.store.ts +23 -4
- package/features/memory/components/memory-edit.tsx +7 -5
- package/features/thursday/components/thursday.tsx +0 -3
- package/features/thursday/tool-line.ts +0 -1
- package/features/thursday/use-thursday.ts +1 -1
- package/package.json +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__1pduf5n._.js +0 -3
- package/.next/server/chunks/ssr/_14_lw10._.js +0 -3
- package/.next/server/chunks/ssr/_1spzm8z._.js +0 -3
- package/.next/server/chunks/ssr/lib_date-like_ts_02is0pw._.js +0 -3
- package/.next/static/chunks/0xzgvc0fkdb6t.js +0 -1
- package/.next/static/chunks/14hgtsjg-kael.css +0 -1
- package/.next/static/chunks/2hw2f6zmt7fmt.js +0 -1
- package/.next/static/chunks/3111pj5wnjgos.js +0 -1
- package/features/memory/components/memory-view.tsx +0 -84
- /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → kaTO0UHqF1bMnWgCDik2f}/_buildManifest.js +0 -0
- /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → kaTO0UHqF1bMnWgCDik2f}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → kaTO0UHqF1bMnWgCDik2f}/_ssgManifest.js +0 -0
package/features/ai/chatgpt.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import { createServer, type Server } from "node:http";
|
|
2
|
+
import { createServer, type RequestListener, type Server } from "node:http";
|
|
3
3
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
4
4
|
import { type LanguageModel, wrapLanguageModel } from "ai";
|
|
5
5
|
import { formatDistanceToNowStrict } from "date-fns";
|
|
@@ -28,6 +28,12 @@ const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
|
28
28
|
/** The only redirect registered for that client, so the answer comes back to this port and no other. */
|
|
29
29
|
const CALLBACK_PORT = 1455;
|
|
30
30
|
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/auth/callback`;
|
|
31
|
+
/**
|
|
32
|
+
* Both loopbacks: `localhost` in the browser reaches whichever answers first, and an
|
|
33
|
+
* address bound here outranks another app's `::` or `0.0.0.0` on the same port — so the
|
|
34
|
+
* answer cannot land in someone else's server.
|
|
35
|
+
*/
|
|
36
|
+
const CALLBACK_HOSTS = ["127.0.0.1", "::1"];
|
|
31
37
|
const SCOPE = "openid profile email offline_access";
|
|
32
38
|
const CODEX_URL = "https://chatgpt.com/backend-api/codex";
|
|
33
39
|
/** The access token's claim naming the account and its plan. */
|
|
@@ -51,11 +57,11 @@ const SignInSchema = z.object({
|
|
|
51
57
|
type SignIn = z.infer<typeof SignInSchema>;
|
|
52
58
|
|
|
53
59
|
type Pinned = {
|
|
54
|
-
|
|
60
|
+
__chatgptListeners?: Server[] | null;
|
|
55
61
|
__chatgptRenewal?: ReturnType<typeof createKeyedLock>;
|
|
56
62
|
};
|
|
57
63
|
|
|
58
|
-
/** Pinned to globalThis: next dev reloads this module, and the
|
|
64
|
+
/** Pinned to globalThis: next dev reloads this module, and the listeners holding the port and the renewal lane must be the originals. */
|
|
59
65
|
const pinned = globalThis as Pinned;
|
|
60
66
|
|
|
61
67
|
/**
|
|
@@ -74,7 +80,8 @@ export async function startChatGptSignIn(): Promise<string> {
|
|
|
74
80
|
const verifier = randomBytes(32).toString("base64url");
|
|
75
81
|
const state = randomBytes(16).toString("hex");
|
|
76
82
|
|
|
77
|
-
const
|
|
83
|
+
const servers: Server[] = [];
|
|
84
|
+
const answer: RequestListener = (request, response) => {
|
|
78
85
|
void answerCallback(request.url ?? "/", { state, verifier }).then(
|
|
79
86
|
(page) => {
|
|
80
87
|
response.writeHead(page.status, {
|
|
@@ -85,29 +92,42 @@ export async function startChatGptSignIn(): Promise<string> {
|
|
|
85
92
|
autoClose: page.status === 200,
|
|
86
93
|
}),
|
|
87
94
|
() => {
|
|
88
|
-
if (page.last && pinned.
|
|
95
|
+
if (page.last && pinned.__chatgptListeners === servers) {
|
|
89
96
|
stopListening();
|
|
90
97
|
}
|
|
91
98
|
},
|
|
92
99
|
);
|
|
93
100
|
},
|
|
94
101
|
);
|
|
95
|
-
}
|
|
102
|
+
};
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
server
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
for (const host of CALLBACK_HOSTS) {
|
|
105
|
+
const server = createServer(answer);
|
|
106
|
+
const failure = await new Promise<NodeJS.ErrnoException | null>(
|
|
107
|
+
(resolve) => {
|
|
108
|
+
server.once("error", resolve);
|
|
109
|
+
server.listen(CALLBACK_PORT, host, () => resolve(null));
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
if (!failure) {
|
|
113
|
+
servers.push(server);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// No IPv6 on this machine: nobody else can answer there either
|
|
117
|
+
if (failure.code === "EADDRNOTAVAIL" || failure.code === "EAFNOSUPPORT") {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
for (const bound of servers) bound.close();
|
|
101
121
|
publicError(
|
|
102
|
-
|
|
103
|
-
? `Port ${CALLBACK_PORT} is in use
|
|
104
|
-
: `Could not wait for the sign-in: ${errorToString(
|
|
122
|
+
failure.code === "EADDRINUSE"
|
|
123
|
+
? `Port ${CALLBACK_PORT} is in use by another app — often a ChatGPT or Codex sign-in still waiting. Finish or close it, then try again.`
|
|
124
|
+
: `Could not wait for the sign-in: ${errorToString(failure)}`,
|
|
105
125
|
);
|
|
106
|
-
}
|
|
107
|
-
pinned.
|
|
126
|
+
}
|
|
127
|
+
pinned.__chatgptListeners = servers;
|
|
108
128
|
// A page nobody finishes must not hold the port for the life of the process
|
|
109
129
|
setTimeout(() => {
|
|
110
|
-
if (pinned.
|
|
130
|
+
if (pinned.__chatgptListeners === servers) stopListening();
|
|
111
131
|
}, CHATGPT_SIGN_IN.waitMs).unref();
|
|
112
132
|
|
|
113
133
|
const url = new URL(AUTHORIZE_URL);
|
|
@@ -128,10 +148,12 @@ export async function startChatGptSignIn(): Promise<string> {
|
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
function stopListening() {
|
|
131
|
-
const
|
|
132
|
-
pinned.
|
|
133
|
-
server
|
|
134
|
-
|
|
151
|
+
const servers = pinned.__chatgptListeners;
|
|
152
|
+
pinned.__chatgptListeners = null;
|
|
153
|
+
for (const server of servers ?? []) {
|
|
154
|
+
server.close();
|
|
155
|
+
server.closeIdleConnections();
|
|
156
|
+
}
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
type CallbackPage = {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { asSchema, type ToolSet, tool } from "ai";
|
|
2
2
|
import { formatDistanceToNowStrict } from "date-fns";
|
|
3
|
-
import { IS_DEV } from "@/config";
|
|
3
|
+
import { CALL_EXEC_TIMEOUT_MS, IS_DEV } from "@/config";
|
|
4
4
|
import type { TextModel } from "@/features/ai/model";
|
|
5
|
-
import { clockNow
|
|
5
|
+
import { clockNow } from "@/features/ai/prompts/prompt-helper";
|
|
6
6
|
import {
|
|
7
7
|
answerTool,
|
|
8
8
|
askThursdayTool,
|
|
@@ -17,7 +17,6 @@ import { createSkillTools } from "@/features/ai/tools/skills.tool";
|
|
|
17
17
|
import { TOOL_NAMES } from "@/features/ai/tools/tool-name";
|
|
18
18
|
import { createWorkspaceTools } from "@/features/ai/tools/workspace.tool";
|
|
19
19
|
import { taskActivity } from "@/features/bot/bot.schema";
|
|
20
|
-
import { listNoteIndex } from "@/features/memory/memory.query";
|
|
21
20
|
import { loadSkills } from "@/features/skills/skills.discover";
|
|
22
21
|
import { readCallSkillsOn } from "@/features/thursday/thursday.query";
|
|
23
22
|
import { jobShellEnv, openWorkspace } from "@/features/workspace/workspace";
|
|
@@ -30,8 +29,8 @@ import { clip } from "@/lib/utils";
|
|
|
30
29
|
* Which tools each runtime is handed; what it is told about them is the prompt's job.
|
|
31
30
|
* Every tool runs on the server, including calls made during a voice session; only `end_call`
|
|
32
31
|
* has no execute (the page hangs up). The split is by time, not capability: anything that
|
|
33
|
-
* presupposes waiting (MCP, studio, browser) belongs to the bot.
|
|
34
|
-
*
|
|
32
|
+
* presupposes waiting (MCP, studio, browser) belongs to the bot. Only the call and an edit on
|
|
33
|
+
* the memory screen write to memory: revising, carrying and naming need the user there. A bot reads it.
|
|
35
34
|
* Skills are the one thing that crosses back, and only when asked for: the call reads one itself
|
|
36
35
|
* when Settings › Thursday says so (thursday.query readCallSkillsOn).
|
|
37
36
|
*/
|
|
@@ -83,7 +82,13 @@ function createTaskTools(callId: string | null | undefined): ToolSet {
|
|
|
83
82
|
|
|
84
83
|
// The row carries the bot's own spelling, not the transcript's
|
|
85
84
|
const { startTask } = await import("@/features/bot/bot.runner");
|
|
86
|
-
const id = await startTask({
|
|
85
|
+
const id = await startTask({
|
|
86
|
+
bot: found.name,
|
|
87
|
+
request,
|
|
88
|
+
label,
|
|
89
|
+
callId,
|
|
90
|
+
from: "thursday",
|
|
91
|
+
});
|
|
87
92
|
return {
|
|
88
93
|
taskId: id,
|
|
89
94
|
// The label is the handle: without it in front of her, a follow-up
|
|
@@ -182,7 +187,7 @@ function createTaskTools(callId: string | null | undefined): ToolSet {
|
|
|
182
187
|
return { label: one.label, status: "cancelled" };
|
|
183
188
|
}
|
|
184
189
|
if (!answer?.trim()) return "Say what to pass on.";
|
|
185
|
-
await answerTask(one.id, answer.trim());
|
|
190
|
+
await answerTask(one.id, answer.trim(), "thursday");
|
|
186
191
|
return {
|
|
187
192
|
// Named even when the model named it: with no job given this is the
|
|
188
193
|
// one that moved last, and saying which makes a wrong one obvious
|
|
@@ -265,9 +270,6 @@ async function buildTools(run: ToolRun): Promise<ToolSet> {
|
|
|
265
270
|
const sandbox = await openWorkspace();
|
|
266
271
|
|
|
267
272
|
if (run.target === "thursday") {
|
|
268
|
-
// memory_show only when there is something to tidy (prompt-helper tidying)
|
|
269
|
-
const { [TOOL_NAMES.memory_show]: show, ...always } = memory;
|
|
270
|
-
const { crowded, heavy } = tidying(await listNoteIndex());
|
|
271
273
|
// Off unless switched on: a skill is a page of instructions arriving
|
|
272
274
|
// mid-sentence. Switched off, the prompt does not list them as hers either
|
|
273
275
|
// (thursday.prompt), so the two always say the same thing
|
|
@@ -276,11 +278,15 @@ async function buildTools(run: ToolRun): Promise<ToolSet> {
|
|
|
276
278
|
: {};
|
|
277
279
|
|
|
278
280
|
return {
|
|
279
|
-
...
|
|
280
|
-
...(crowded || heavy.length ? { [TOOL_NAMES.memory_show]: show } : {}),
|
|
281
|
+
...memory,
|
|
281
282
|
...skills,
|
|
282
|
-
// The shell alone
|
|
283
|
-
|
|
283
|
+
// The shell alone, for no longer than a call can sit silent on it: the mic
|
|
284
|
+
// is closed while a tool runs (config CALL_EXEC_TIMEOUT_MS). A whole file is
|
|
285
|
+
// a job, not a glance (workspace.tool)
|
|
286
|
+
...createWorkspaceTools(sandbox, {
|
|
287
|
+
write: false,
|
|
288
|
+
timeoutMs: CALL_EXEC_TIMEOUT_MS,
|
|
289
|
+
}),
|
|
284
290
|
// Handing work over, following it, and hanging up belong to the voice session only
|
|
285
291
|
...createTaskTools(run.callId),
|
|
286
292
|
...CALL_TOOLS,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
-
/**
|
|
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},
|
|
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:
|
|
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},
|
|
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
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
path — what is under it (facts) "what the user calls it"
|
|
182
176
|
|
|
183
|
-
|
|
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
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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
|
-
|
|
198
|
-
|
|
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}
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
333
|
-
* as
|
|
334
|
-
* is one sentence the realtime model produced
|
|
335
|
-
* only be in the call. The chain is what a
|
|
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
|
|
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
|
-
/**
|
|
406
|
-
|
|
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)** —
|
|
412
|
-
|
|
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
|
|
418
|
+
const by = bots[index - 1];
|
|
415
419
|
return index === you
|
|
416
|
-
? `- **${bot} (you)** — ${
|
|
417
|
-
: `- **${bot}** — ${
|
|
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
|
|
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
|
-
:
|
|
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
|
-
/**
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|