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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { tool } from "ai";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
-
import { appEvents } from "@/app/api/events/app-event.server";
|
|
4
3
|
import { MEMORY_CONVERSATION_PAGE, MEMORY_LIMITS } from "@/config";
|
|
5
4
|
import {
|
|
6
5
|
callStamp,
|
|
@@ -57,9 +56,9 @@ const noConversation = (source: MemorySource | null) =>
|
|
|
57
56
|
/**
|
|
58
57
|
* Said when a note has outgrown the recommended size, and only then — the write
|
|
59
58
|
* itself always goes through. It names no tool: this one answers the call and a
|
|
60
|
-
* bot, and
|
|
61
|
-
*
|
|
62
|
-
*
|
|
59
|
+
* bot, and how to settle it with the user is the call's to say (prompts/thursday.prompt);
|
|
60
|
+
* the ask is the same either way, because a bot that turned up a fact can hand
|
|
61
|
+
* the same request back with it.
|
|
63
62
|
*/
|
|
64
63
|
const overSize = (count: number) =>
|
|
65
64
|
count > MEMORY_LIMITS.factsPerNote
|
|
@@ -114,7 +113,9 @@ export const createMemoryTools = (
|
|
|
114
113
|
.object({
|
|
115
114
|
text: z
|
|
116
115
|
.string()
|
|
117
|
-
.describe(
|
|
116
|
+
.describe(
|
|
117
|
+
'One statement that stands on its own later — a date as a date, never "next week".',
|
|
118
|
+
),
|
|
118
119
|
replaces: z
|
|
119
120
|
.number()
|
|
120
121
|
.int()
|
|
@@ -126,7 +127,7 @@ export const createMemoryTools = (
|
|
|
126
127
|
.boolean()
|
|
127
128
|
.nullish()
|
|
128
129
|
.describe(
|
|
129
|
-
`True carries this line into every call's instructions without opening its note — for what to call them, their language and register
|
|
130
|
+
`True carries this line into every call's instructions without opening its note — for what every call needs before any note is opened: what to call them, their language and register. At most ${MEMORY_LIMITS.carried}. Null leaves it; false makes a carried line an ordinary fact.`,
|
|
130
131
|
),
|
|
131
132
|
})
|
|
132
133
|
.array()
|
|
@@ -175,23 +176,23 @@ export const createMemoryTools = (
|
|
|
175
176
|
};
|
|
176
177
|
}
|
|
177
178
|
// A new path outside the convention goes to inbox, which keeps its own line and takes no aliases
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
179
|
+
const toInbox = !known && !isMemoryPath(said);
|
|
180
|
+
const filed = toInbox
|
|
181
|
+
? {
|
|
182
|
+
path: MEMORY_INBOX,
|
|
183
|
+
description: appNoteLine(MEMORY_INBOX),
|
|
184
|
+
facts,
|
|
185
|
+
}
|
|
186
|
+
: { path: target, description, aliases, facts };
|
|
186
187
|
|
|
187
188
|
const write = await writeNotes([filed], source, callId);
|
|
188
189
|
|
|
189
190
|
// The write already succeeded; what follows are requests, not failures.
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
// Only a path that landed in inbox is said: one reached by a name the
|
|
192
|
+
// listing carries is where it was asked for, however it was spelled.
|
|
193
|
+
const elsewhere = toInbox
|
|
194
|
+
? ` Filed under ${filed.path}: "${said}" is not a path this listing can carry. If that is not where it belongs, write it again under one that is.`
|
|
195
|
+
: "";
|
|
195
196
|
|
|
196
197
|
const unnamed = write.unnamed.length
|
|
197
198
|
? ` ${write.unnamed[0]} is new and has no line yet — the listing shows its first fact instead. Call again with \`description\` (one line about what it is) and \`aliases\` (the names they say for it).`
|
|
@@ -265,59 +266,61 @@ export const createMemoryTools = (
|
|
|
265
266
|
},
|
|
266
267
|
}),
|
|
267
268
|
|
|
268
|
-
[TOOL_NAMES.memory_show]: tool({
|
|
269
|
-
description: `Put a note on the user's screen so they can see it while you
|
|
270
|
-
talk about it, and take it away again. They cannot edit it there — they say
|
|
271
|
-
what to drop and you drop it.`,
|
|
272
|
-
inputSchema: z.object({
|
|
273
|
-
path: z
|
|
274
|
-
.string()
|
|
275
|
-
.nullable()
|
|
276
|
-
.describe(
|
|
277
|
-
"The note to put on screen, exactly as the listing writes it. Null takes it away.",
|
|
278
|
-
),
|
|
279
|
-
}),
|
|
280
|
-
execute: async ({ path }) => {
|
|
281
|
-
if (!path?.trim()) {
|
|
282
|
-
appEvents.emit({ type: "memory-view", path: null });
|
|
283
|
-
return { note: "Taken off the screen." };
|
|
284
|
-
}
|
|
285
|
-
// The screen gets only the path and reads facts over GET, so deletions show up on their own
|
|
286
|
-
const { notes } = await readNotes([path], { touch: false });
|
|
287
|
-
const note = notes[0];
|
|
288
|
-
if (!note) {
|
|
289
|
-
return { note: `Nothing on the listing called ${path.trim()}.` };
|
|
290
|
-
}
|
|
291
|
-
appEvents.emit({ type: "memory-view", path: note.path });
|
|
292
|
-
return {
|
|
293
|
-
...note,
|
|
294
|
-
note: "It is on their screen now, in this order. Ask what they no longer need, and forget only what they name.",
|
|
295
|
-
};
|
|
296
|
-
},
|
|
297
|
-
}),
|
|
298
|
-
|
|
299
269
|
[TOOL_NAMES.memory_forget]: tool({
|
|
300
|
-
description: "Delete
|
|
270
|
+
description: "Delete facts for good.",
|
|
301
271
|
inputSchema: z.object({
|
|
302
|
-
|
|
272
|
+
factIds: z
|
|
303
273
|
.number()
|
|
304
274
|
.int()
|
|
305
|
-
.
|
|
275
|
+
.array()
|
|
276
|
+
.min(1)
|
|
277
|
+
.describe(
|
|
278
|
+
"The ids that came with the facts when their notes were opened — every one to delete, in one call.",
|
|
279
|
+
),
|
|
306
280
|
}),
|
|
307
|
-
execute: async ({
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
281
|
+
execute: async ({ factIds }) => {
|
|
282
|
+
const missing: number[] = [];
|
|
283
|
+
/** Notes that lost their last fact, and went with it (memory.query forgetFactById). */
|
|
284
|
+
const gone: string[] = [];
|
|
285
|
+
/** Notes that still hold something, handed back as they are now. */
|
|
286
|
+
const left = new Set<string>();
|
|
287
|
+
for (const id of new Set(factIds)) {
|
|
288
|
+
const forgotten = await forgetFactById(id);
|
|
289
|
+
if (!forgotten) {
|
|
290
|
+
missing.push(id);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (forgotten.noteGone) {
|
|
294
|
+
left.delete(forgotten.path);
|
|
295
|
+
gone.push(forgotten.path);
|
|
296
|
+
} else {
|
|
297
|
+
left.add(forgotten.path);
|
|
298
|
+
}
|
|
311
299
|
}
|
|
312
300
|
|
|
313
|
-
|
|
314
|
-
if (forgotten.noteGone) {
|
|
301
|
+
if (!gone.length && !left.size) {
|
|
315
302
|
return {
|
|
316
|
-
note:
|
|
303
|
+
note: `No fact with id ${missing.join(", ")}. Nothing was deleted.`,
|
|
317
304
|
};
|
|
318
305
|
}
|
|
319
|
-
|
|
320
|
-
|
|
306
|
+
|
|
307
|
+
// The notes that went are still on the listing until the next session, so say so
|
|
308
|
+
const said = [
|
|
309
|
+
GONE,
|
|
310
|
+
gone.length === 1
|
|
311
|
+
? `That was the last fact under ${gone[0]}, so the note went with it.`
|
|
312
|
+
: gone.length
|
|
313
|
+
? `That was the last of ${gone.join(", ")}, so those notes went with them.`
|
|
314
|
+
: "",
|
|
315
|
+
missing.length
|
|
316
|
+
? `No fact with id ${missing.join(", ")}, so those were skipped.`
|
|
317
|
+
: "",
|
|
318
|
+
]
|
|
319
|
+
.filter(Boolean)
|
|
320
|
+
.join(" ");
|
|
321
|
+
if (!left.size) return { note: said };
|
|
322
|
+
const { notes } = await readNotes([...left], { touch: false });
|
|
323
|
+
return { notes: notes.map(withCount), note: said };
|
|
321
324
|
},
|
|
322
325
|
}),
|
|
323
326
|
});
|
|
@@ -18,6 +18,8 @@ export const createWorkspaceTools = (
|
|
|
18
18
|
env?: Record<string, string>;
|
|
19
19
|
/** Attach the shell guide to this run's first `bash` result (bots only). */
|
|
20
20
|
guide?: boolean;
|
|
21
|
+
/** How long one command may run; unset is the sandbox's own limit (config EXEC_TIMEOUT_MS). */
|
|
22
|
+
timeoutMs?: number;
|
|
21
23
|
},
|
|
22
24
|
): ToolSet => {
|
|
23
25
|
/** Fold absolute sandbox paths against cwd; they still resolve when handed back. */
|
|
@@ -45,6 +47,7 @@ export const createWorkspaceTools = (
|
|
|
45
47
|
const result = await sandbox.exec(command, {
|
|
46
48
|
signal: abortSignal,
|
|
47
49
|
env: options.env,
|
|
50
|
+
timeoutMs: options.timeoutMs,
|
|
48
51
|
});
|
|
49
52
|
if (!owed) return result;
|
|
50
53
|
owed = false;
|
|
@@ -110,7 +110,12 @@ export const startTaskAction = serverAction(
|
|
|
110
110
|
const worker = await findBot(bot);
|
|
111
111
|
if (!worker) publicError(`No bot called "${bot}".`);
|
|
112
112
|
const label = labelFor(said);
|
|
113
|
-
const id = await startTask({
|
|
113
|
+
const id = await startTask({
|
|
114
|
+
bot: worker.name,
|
|
115
|
+
request: said,
|
|
116
|
+
label,
|
|
117
|
+
from: "user",
|
|
118
|
+
});
|
|
114
119
|
return { id, label, bot: worker.name };
|
|
115
120
|
},
|
|
116
121
|
);
|
package/features/bot/bot.run.ts
CHANGED
|
@@ -344,6 +344,7 @@ export async function runBot(
|
|
|
344
344
|
const summary = await compact(model.model, agentTools, messages, {
|
|
345
345
|
signal: options.signal,
|
|
346
346
|
budget,
|
|
347
|
+
instructions: prompt.text,
|
|
347
348
|
}).finally(quiet.release);
|
|
348
349
|
logger.debug(
|
|
349
350
|
`compacted ${messages.length} messages at ${size} tokens (budget ${budget})`,
|
|
@@ -701,17 +702,19 @@ async function filesUnder(
|
|
|
701
702
|
/**
|
|
702
703
|
* The model summarizes its own context and continues from the summary. Tools
|
|
703
704
|
* are passed with calling off: providers refuse a history of tool calls
|
|
704
|
-
* without the tools that made them.
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
705
|
+
* without the tools that made them. The run's own instructions go too: the
|
|
706
|
+
* summary is written by the bot the thread belongs to, and the provider's
|
|
707
|
+
* cached prefix, instructions first, still matches the run's. A context too
|
|
708
|
+
* long to summarise whole — the very thing a compaction is for — is tried once
|
|
709
|
+
* more without the tool calls and results but the last few. A failure throws
|
|
710
|
+
* with its cause kept: the runner parks what a retry or a smaller budget can
|
|
711
|
+
* fix (bot.runner parkTask) and fails the rest; the thread stays.
|
|
709
712
|
*/
|
|
710
713
|
async function compact(
|
|
711
714
|
model: LanguageModel,
|
|
712
715
|
tools: ToolSet,
|
|
713
716
|
messages: ModelMessage[],
|
|
714
|
-
options: { signal?: AbortSignal; budget: number },
|
|
717
|
+
options: { signal?: AbortSignal; budget: number; instructions: string },
|
|
715
718
|
): Promise<{ text: string; usage: TokenUsage }> {
|
|
716
719
|
let failure: unknown;
|
|
717
720
|
try {
|
|
@@ -745,10 +748,11 @@ async function summarize(
|
|
|
745
748
|
model: LanguageModel,
|
|
746
749
|
tools: ToolSet,
|
|
747
750
|
messages: ModelMessage[],
|
|
748
|
-
options: { signal?: AbortSignal; budget: number },
|
|
751
|
+
options: { signal?: AbortSignal; budget: number; instructions: string },
|
|
749
752
|
): Promise<{ text: string; usage: TokenUsage }> {
|
|
750
753
|
const { text, usage } = await generateText({
|
|
751
754
|
model,
|
|
755
|
+
system: options.instructions,
|
|
752
756
|
tools,
|
|
753
757
|
toolChoice: "none",
|
|
754
758
|
messages: [
|
|
@@ -34,7 +34,13 @@ import { logger } from "@/lib/logger";
|
|
|
34
34
|
import { publicError } from "@/lib/public-error";
|
|
35
35
|
import { createKeyedLock } from "@/lib/queue";
|
|
36
36
|
import { resumeThread, runBot, type TaskEvent } from "./bot.run";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
TASK_CONTINUE,
|
|
39
|
+
type TaskPending,
|
|
40
|
+
type TaskSpeaker,
|
|
41
|
+
type TaskStatus,
|
|
42
|
+
tagSpeaker,
|
|
43
|
+
} from "./bot.schema";
|
|
38
44
|
import {
|
|
39
45
|
addTaskUsage,
|
|
40
46
|
countStopsSinceSpoken,
|
|
@@ -49,6 +55,7 @@ import {
|
|
|
49
55
|
listRunningTaskIds,
|
|
50
56
|
listTaskFolders,
|
|
51
57
|
listThread,
|
|
58
|
+
stopNote,
|
|
52
59
|
type TaskMessageInput,
|
|
53
60
|
updateTask,
|
|
54
61
|
upsertMessage,
|
|
@@ -99,24 +106,28 @@ const pausing = ((globalThis as Pinned).__botPausing ??= {
|
|
|
99
106
|
current: Promise.resolve(),
|
|
100
107
|
});
|
|
101
108
|
|
|
102
|
-
/** Opens a job. The opening message — who is
|
|
109
|
+
/** Opens a job. The opening message — who is on it, the job, the call it came from (bot.prompt buildTaskOpening) — is the thread's first row. */
|
|
103
110
|
export async function startTask(input: {
|
|
104
111
|
bot: string;
|
|
105
112
|
request: string;
|
|
106
113
|
label: string;
|
|
107
114
|
callId?: string | null;
|
|
115
|
+
/** Who handed it over: Thursday during a call, or the user on screen. */
|
|
116
|
+
from: TaskSpeaker;
|
|
108
117
|
}) {
|
|
109
|
-
const
|
|
110
|
-
|
|
118
|
+
const { from, ...row } = input;
|
|
119
|
+
const conversation = row.callId
|
|
120
|
+
? await listCallTurns(row.callId, OPENING_TURNS)
|
|
111
121
|
: [];
|
|
112
122
|
const opening = buildTaskOpening({
|
|
113
|
-
bot:
|
|
114
|
-
request:
|
|
123
|
+
bot: row.bot,
|
|
124
|
+
request: row.request,
|
|
115
125
|
conversation,
|
|
126
|
+
from,
|
|
116
127
|
});
|
|
117
|
-
const task = await insertTask({ ...
|
|
128
|
+
const task = await insertTask({ ...row, opening });
|
|
118
129
|
launch(task.id, {
|
|
119
|
-
bot:
|
|
130
|
+
bot: row.bot,
|
|
120
131
|
messages: [{ role: "user", content: opening }],
|
|
121
132
|
});
|
|
122
133
|
return task.id;
|
|
@@ -128,14 +139,25 @@ export async function startTask(input: {
|
|
|
128
139
|
* before its next step; the step in flight finishes), or a follow-up to a
|
|
129
140
|
* finished one. Only when the run has already ended does the running case
|
|
130
141
|
* fall through to stop, append, relaunch.
|
|
142
|
+
*
|
|
143
|
+
* @param from Who said it. The words go on the thread tagged with it (bot.schema
|
|
144
|
+
* tagSpeaker): Thursday passing something on from a call and the user typing on
|
|
145
|
+
* screen reach the bot down this one pipe, and a go-ahead means something
|
|
146
|
+
* different from each. Unset for words that already carry their tags — the notes
|
|
147
|
+
* a finished run left unread (drive).
|
|
131
148
|
*/
|
|
132
|
-
export async function answerTask(
|
|
149
|
+
export async function answerTask(
|
|
150
|
+
id: string,
|
|
151
|
+
answer: string,
|
|
152
|
+
from?: TaskSpeaker,
|
|
153
|
+
) {
|
|
154
|
+
const said = from ? tagSpeaker(from, answer) : answer;
|
|
133
155
|
return taskLock(id, async () => {
|
|
134
156
|
const task = await findTask(id);
|
|
135
157
|
if (!task) publicError("No such job.");
|
|
136
158
|
|
|
137
159
|
const live = running.get(id);
|
|
138
|
-
if (live && task.status === "running" && live.note(
|
|
160
|
+
if (live && task.status === "running" && live.note(said)) {
|
|
139
161
|
logger.debug(`task ${id}: interjection queued`);
|
|
140
162
|
return;
|
|
141
163
|
}
|
|
@@ -147,7 +169,7 @@ export async function answerTask(id: string, answer: string) {
|
|
|
147
169
|
forgetResume(id);
|
|
148
170
|
|
|
149
171
|
const thread = await listThread(id);
|
|
150
|
-
const { reply, as } = joinThread(task,
|
|
172
|
+
const { reply, as } = joinThread(task, said, thread);
|
|
151
173
|
logger.debug(`task ${id}: ${as}`);
|
|
152
174
|
|
|
153
175
|
await upsertMessage(id, (await lastSeq(id)) + 1, {
|
|
@@ -465,7 +487,7 @@ async function parkTask(
|
|
|
465
487
|
const inMs =
|
|
466
488
|
auto && stop.retry ? waits[Math.min(stops, waits.length - 1)] : 0;
|
|
467
489
|
|
|
468
|
-
await writeNote(id,
|
|
490
|
+
await writeNote(id, stopNote(stop.why)).catch((cause) =>
|
|
469
491
|
logger.warn(`task ${id}: stop not written`, cause),
|
|
470
492
|
);
|
|
471
493
|
await writeEnding(id, {
|
|
@@ -485,10 +507,6 @@ async function parkTask(
|
|
|
485
507
|
if (auto) scheduleResume(id, inMs);
|
|
486
508
|
}
|
|
487
509
|
|
|
488
|
-
/** Said to a resumed run after why it stopped. */
|
|
489
|
-
const IN_FLIGHT =
|
|
490
|
-
"Anything that was under way — a command, a page loading, a download — may not have finished: check before relying on it, then carry on.";
|
|
491
|
-
|
|
492
510
|
const spell = (ms: number) =>
|
|
493
511
|
ms < 60_000
|
|
494
512
|
? `${Math.round(ms / 1000)} seconds`
|
|
@@ -205,6 +205,29 @@ export const TASK_HISTORY_PAGE = PAGE_SIZE;
|
|
|
205
205
|
/** The single option offered when the app stopped a job and asks whether to go on. Button text and spoken word alike. */
|
|
206
206
|
export const TASK_CONTINUE = "Continue";
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Who a person's words to a job came from (bot.runner answerTask). Thursday
|
|
210
|
+
* passing something on from a call and the user typing on screen reach a bot
|
|
211
|
+
* down one pipe, so the words carry it in front of them.
|
|
212
|
+
*/
|
|
213
|
+
export type TaskSpeaker = "thursday" | "user";
|
|
214
|
+
|
|
215
|
+
const SPEAKER_TAGS: Record<TaskSpeaker, string> = {
|
|
216
|
+
thursday: "Thursday, on the call:",
|
|
217
|
+
user: "The user, on screen:",
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const tagSpeaker = (from: TaskSpeaker, text: string): string =>
|
|
221
|
+
`${SPEAKER_TAGS[from]} ${text}`;
|
|
222
|
+
|
|
223
|
+
/** The words without their tag, as the screen draws a person's line (task.query linesOf). */
|
|
224
|
+
export const untagSpeaker = (text: string): string => {
|
|
225
|
+
for (const tag of Object.values(SPEAKER_TAGS)) {
|
|
226
|
+
if (text.startsWith(`${tag} `)) return text.slice(tag.length + 1);
|
|
227
|
+
}
|
|
228
|
+
return text;
|
|
229
|
+
};
|
|
230
|
+
|
|
208
231
|
/**
|
|
209
232
|
* What a `waiting` job waits on (database task.pending). `toolCallId` is the
|
|
210
233
|
* `ask_thursday` call the answer resolves, null when the app stopped the run and
|
|
@@ -254,6 +277,8 @@ export const TaskLineSchema = z.discriminatedUnion("kind", [
|
|
|
254
277
|
LineBase.extend({ kind: z.literal("text"), text: z.string() }),
|
|
255
278
|
/** Compaction summary (bot.run compact). The model resumes from here; the screen draws it as a divider. */
|
|
256
279
|
LineBase.extend({ kind: z.literal("note"), text: z.string() }),
|
|
280
|
+
/** Why the app stopped the run (bot.runner parkTask, a run that broke), without what the resumed run is told to check. */
|
|
281
|
+
LineBase.extend({ kind: z.literal("stop"), text: z.string() }),
|
|
257
282
|
LineBase.extend({
|
|
258
283
|
kind: z.literal("tool"),
|
|
259
284
|
callId: z.string(),
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { format } from "date-fns";
|
|
3
4
|
import {
|
|
4
5
|
ArrowUp,
|
|
5
6
|
Check,
|
|
7
|
+
ChevronDown,
|
|
6
8
|
ChevronLeft,
|
|
7
9
|
ChevronRight,
|
|
8
10
|
ChevronsRight,
|
|
@@ -10,6 +12,7 @@ import {
|
|
|
10
12
|
History,
|
|
11
13
|
Loader2,
|
|
12
14
|
Plus,
|
|
15
|
+
RotateCw,
|
|
13
16
|
X,
|
|
14
17
|
} from "lucide-react";
|
|
15
18
|
import {
|
|
@@ -45,7 +48,7 @@ import { TaskReply, useAnswerTask } from "@/features/bot/components/task-reply";
|
|
|
45
48
|
import { openSettings } from "@/features/settings/settings.store";
|
|
46
49
|
import { ThursdayMark } from "@/features/thursday/components/thursday-mark";
|
|
47
50
|
import { FileViewer } from "@/features/workspace/components/file-view";
|
|
48
|
-
import { shortAgo } from "@/lib/date-like";
|
|
51
|
+
import { shortAgo, toDate } from "@/lib/date-like";
|
|
49
52
|
import { useServerAction } from "@/lib/protocol/use-server-action";
|
|
50
53
|
import { revalidate, useServerRoute } from "@/lib/protocol/use-server-route";
|
|
51
54
|
import {
|
|
@@ -1630,6 +1633,8 @@ function Group({ group, task }: { group: ChatterGroup; task: TaskView }) {
|
|
|
1630
1633
|
{runs(group.lines).map((run) =>
|
|
1631
1634
|
run.kind === "tools" ? (
|
|
1632
1635
|
<Steps key={run.key} lines={run.lines} taskId={task.id} />
|
|
1636
|
+
) : run.kind === "stops" ? (
|
|
1637
|
+
<Stops key={run.key} lines={run.lines} />
|
|
1633
1638
|
) : (
|
|
1634
1639
|
run.lines.map((line) => (
|
|
1635
1640
|
<Line key={line.id} line={line} taskId={task.id} />
|
|
@@ -1793,17 +1798,65 @@ function Steps({ lines, taskId }: { lines: Chatter[]; taskId: string }) {
|
|
|
1793
1798
|
);
|
|
1794
1799
|
}
|
|
1795
1800
|
|
|
1796
|
-
/**
|
|
1801
|
+
/**
|
|
1802
|
+
* Where the app stopped the run (bot.runner parkTask): a failed model call, a
|
|
1803
|
+
* restart, a closed browser. Muted and in the bot's turn, since the bot goes on
|
|
1804
|
+
* from here. The same reason in a row is one line with a count; opening it lists
|
|
1805
|
+
* each stop by the time it happened.
|
|
1806
|
+
*/
|
|
1807
|
+
function Stops({ lines }: { lines: Chatter[] }) {
|
|
1808
|
+
return (
|
|
1809
|
+
<details className="group min-w-0 px-1 text-muted-foreground">
|
|
1810
|
+
<summary className="flex cursor-pointer list-none items-center gap-1.5 text-[12.5px] leading-relaxed outline-none [&::-webkit-details-marker]:hidden">
|
|
1811
|
+
<RotateCw className="size-3 shrink-0" />
|
|
1812
|
+
<span className="min-w-0 truncate">{leadOf(lines[0].text)}</span>
|
|
1813
|
+
{lines.length > 1 && (
|
|
1814
|
+
<span className="shrink-0 font-mono text-[10px] text-muted-foreground/70">
|
|
1815
|
+
×{lines.length}
|
|
1816
|
+
</span>
|
|
1817
|
+
)}
|
|
1818
|
+
<ChevronDown className="size-2.5 shrink-0 text-muted-foreground/70 transition-transform group-open:rotate-180" />
|
|
1819
|
+
</summary>
|
|
1820
|
+
<div className="mt-1 space-y-1 pl-4.5 text-[11px] leading-relaxed">
|
|
1821
|
+
{lines.map((line) => (
|
|
1822
|
+
<p key={line.id} className="flex gap-2 break-keep">
|
|
1823
|
+
{line.at && (
|
|
1824
|
+
<span className="shrink-0 font-mono text-muted-foreground/70 tabular-nums">
|
|
1825
|
+
{format(toDate(line.at), "HH:mm:ss")}
|
|
1826
|
+
</span>
|
|
1827
|
+
)}
|
|
1828
|
+
<span className="min-w-0">{line.text}</span>
|
|
1829
|
+
</p>
|
|
1830
|
+
))}
|
|
1831
|
+
</div>
|
|
1832
|
+
</details>
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/** What stopped it, without the words behind it: a stop names those in parentheses (bot.runner parkTask). */
|
|
1837
|
+
const leadOf = (text: string) => text.split(" (")[0].replace(/\.$/, "");
|
|
1838
|
+
|
|
1839
|
+
/** Splits one speaker's lines into runs of tool calls, of stops, and of everything else. */
|
|
1797
1840
|
type Run =
|
|
1798
1841
|
| { kind: "tools"; key: string; lines: Chatter[] }
|
|
1842
|
+
| { kind: "stops"; key: string; lines: Chatter[] }
|
|
1799
1843
|
| { kind: "said"; key: string; lines: Chatter[] };
|
|
1800
1844
|
|
|
1801
1845
|
function runs(lines: Chatter[]): Run[] {
|
|
1802
1846
|
const out: Run[] = [];
|
|
1803
1847
|
for (const line of lines) {
|
|
1804
|
-
const kind =
|
|
1848
|
+
const kind =
|
|
1849
|
+
line.kind === "tool" && line.tool
|
|
1850
|
+
? "tools"
|
|
1851
|
+
: line.kind === "stop"
|
|
1852
|
+
? "stops"
|
|
1853
|
+
: "said";
|
|
1805
1854
|
const open = out.at(-1);
|
|
1806
|
-
|
|
1855
|
+
// Stops fold only while the reason repeats; another reason is a line of its own
|
|
1856
|
+
const same =
|
|
1857
|
+
open?.kind === kind &&
|
|
1858
|
+
(kind !== "stops" || leadOf(open.lines[0].text) === leadOf(line.text));
|
|
1859
|
+
if (open && same) open.lines.push(line);
|
|
1807
1860
|
else out.push({ kind, key: line.id, lines: [line] });
|
|
1808
1861
|
}
|
|
1809
1862
|
return out;
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type TaskPending,
|
|
26
26
|
type TaskStatus,
|
|
27
27
|
type TokenUsage,
|
|
28
|
+
untagSpeaker,
|
|
28
29
|
} from "./bot.schema";
|
|
29
30
|
|
|
30
31
|
// Tasks and their threads. Bots themselves (roster, pinned tools) are bot.query.
|
|
@@ -688,6 +689,13 @@ export async function readToolResult(
|
|
|
688
689
|
return null;
|
|
689
690
|
}
|
|
690
691
|
|
|
692
|
+
/** Said to a resumed run after why it stopped. The screen shows only the why (linesOf). */
|
|
693
|
+
const IN_FLIGHT =
|
|
694
|
+
"Anything that was under way — a command, a page loading, a download — may not have finished: check before relying on it, then carry on.";
|
|
695
|
+
|
|
696
|
+
/** The thread line a stop the app made leaves (bot.runner parkTask). */
|
|
697
|
+
export const stopNote = (why: string) => `${why} ${IN_FLIGHT}`;
|
|
698
|
+
|
|
691
699
|
type StoredMessage = typeof taskMessageTable.$inferSelect;
|
|
692
700
|
|
|
693
701
|
/**
|
|
@@ -710,11 +718,17 @@ function linesOf(message: StoredMessage, owner: string): TaskLine[] {
|
|
|
710
718
|
if (message.seq === 0) return [];
|
|
711
719
|
const text = typeof content === "string" ? content : textOf(content);
|
|
712
720
|
if (!text) return [];
|
|
713
|
-
// The app's own
|
|
714
|
-
//
|
|
715
|
-
if (message.note
|
|
716
|
-
|
|
717
|
-
|
|
721
|
+
// The app's own lines, never the user's words. `compact` alone marks a
|
|
722
|
+
// compaction: rows written before `note` existed carry only that.
|
|
723
|
+
if (message.compact) return [{ ...base, id: id(0), kind: "note", text }];
|
|
724
|
+
if (message.note) {
|
|
725
|
+
const why = text.endsWith(IN_FLIGHT)
|
|
726
|
+
? text.slice(0, -IN_FLIGHT.length).trimEnd()
|
|
727
|
+
: text;
|
|
728
|
+
return [{ ...base, id: id(0), kind: "stop", text: why }];
|
|
729
|
+
}
|
|
730
|
+
// Tagged with who said it for the bot (bot.schema tagSpeaker); the screen draws the words
|
|
731
|
+
return [{ ...base, id: id(0), kind: "user", text: untagSpeaker(text) }];
|
|
718
732
|
}
|
|
719
733
|
|
|
720
734
|
if (message.role === "assistant") {
|
|
@@ -817,7 +831,12 @@ function linesOf(message: StoredMessage, owner: string): TaskLine[] {
|
|
|
817
831
|
const text = resultParts(part.output, RESULT_LINES)
|
|
818
832
|
.flatMap((result) => (result.type === "text" ? [result.text] : []))
|
|
819
833
|
.join("\n");
|
|
820
|
-
lines.push({
|
|
834
|
+
lines.push({
|
|
835
|
+
...base,
|
|
836
|
+
id: id(index),
|
|
837
|
+
kind: "user",
|
|
838
|
+
text: untagSpeaker(text),
|
|
839
|
+
});
|
|
821
840
|
} else if (
|
|
822
841
|
part.toolName === TOOL_NAMES.ask_bot ||
|
|
823
842
|
part.toolName === TOOL_NAMES.ask_back
|
|
@@ -24,7 +24,11 @@ import { errorToString } from "@/lib/utils";
|
|
|
24
24
|
|
|
25
25
|
export type BotRef = { name: string; icon?: BotIcon | null };
|
|
26
26
|
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* `note` and `stop` are the app's markers, not speech. `note` is a compaction
|
|
29
|
+
* summary (bot.run compact), drawn as a divider; `stop` is where the app stopped
|
|
30
|
+
* the run (bot.runner parkTask), drawn muted in the bot's turn.
|
|
31
|
+
*/
|
|
28
32
|
export type ChatterKind =
|
|
29
33
|
| "say"
|
|
30
34
|
| "ask"
|
|
@@ -32,7 +36,8 @@ export type ChatterKind =
|
|
|
32
36
|
| "user"
|
|
33
37
|
| "result"
|
|
34
38
|
| "error"
|
|
35
|
-
| "note"
|
|
39
|
+
| "note"
|
|
40
|
+
| "stop";
|
|
36
41
|
|
|
37
42
|
/** One tool call. `name` picks the renderer (components/bot-tool); `results` arrive after the call. */
|
|
38
43
|
export type ToolUse = {
|
|
@@ -63,6 +68,8 @@ export type Chatter = {
|
|
|
63
68
|
tool?: ToolUse;
|
|
64
69
|
/** Options attached to a question; only on the line where the bot stopped to ask. */
|
|
65
70
|
options?: string[];
|
|
71
|
+
/** When it was written; only for kind `stop`, whose repeats fold into one line. */
|
|
72
|
+
at?: DateLike;
|
|
66
73
|
};
|
|
67
74
|
|
|
68
75
|
/** `waiting`: the bot stopped to ask the user something. */
|
|
@@ -159,6 +166,16 @@ export function taskFromRow(row: Task, bots?: Bot[]): TaskView {
|
|
|
159
166
|
case "note":
|
|
160
167
|
lines.push({ id: line.id, bot, to, text: line.text, kind: "note" });
|
|
161
168
|
break;
|
|
169
|
+
case "stop":
|
|
170
|
+
lines.push({
|
|
171
|
+
id: line.id,
|
|
172
|
+
bot,
|
|
173
|
+
to,
|
|
174
|
+
text: line.text,
|
|
175
|
+
kind: "stop",
|
|
176
|
+
at: line.at,
|
|
177
|
+
});
|
|
178
|
+
break;
|
|
162
179
|
case "tool":
|
|
163
180
|
openLines.set(line.callId, lines.length);
|
|
164
181
|
lines.push({
|
|
@@ -264,7 +281,8 @@ export function taskFromRow(row: Task, bots?: Bot[]): TaskView {
|
|
|
264
281
|
export function lastSaid(task: TaskView): Chatter | null {
|
|
265
282
|
for (let at = task.lines.length - 1; at >= 0; at--) {
|
|
266
283
|
const line = task.lines[at];
|
|
267
|
-
if (line.kind !== "user" && line.kind !== "note"
|
|
284
|
+
if (line.kind !== "user" && line.kind !== "note" && line.kind !== "stop")
|
|
285
|
+
return line;
|
|
268
286
|
}
|
|
269
287
|
return null;
|
|
270
288
|
}
|
|
@@ -412,7 +430,8 @@ export function latestPerBot(list: TaskView[]): BotLine[] {
|
|
|
412
430
|
byBot.set(task.bot.name, { bot: task.bot, task, line: null, open: 0 });
|
|
413
431
|
}
|
|
414
432
|
for (const line of task.lines) {
|
|
415
|
-
if (line.kind === "user" || line.kind === "note")
|
|
433
|
+
if (line.kind === "user" || line.kind === "note" || line.kind === "stop")
|
|
434
|
+
continue;
|
|
416
435
|
byBot.set(line.bot.name, {
|
|
417
436
|
bot: line.bot,
|
|
418
437
|
task,
|