ds-bot 0.1.4 → 0.1.5
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/README.md +4 -3
- package/client.js +1558 -1296
- package/index.js +5 -3
- package/package.json +1 -1
- package/src/host/channel.js +9 -3
- package/src/host/constants.js +3 -0
- package/src/host/delivery.js +1 -0
- package/src/host/folders.js +135 -0
- package/src/host/groups.js +4 -3
- package/src/host/images.js +2 -2
- package/src/host/memory.js +31 -8
- package/src/host/parts.js +98 -32
- package/src/host/prompt.js +4 -4
- package/src/host/roster.js +40 -10
- package/src/host/tools.js +88 -16
- package/src/host/turns.js +33 -7
- package/src/host/usage.js +4 -1
package/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// DS Bot: a team of long-lived Bots on top of ordinary dsh root Sessions.
|
|
2
2
|
// Every Bot is one root Session; the roster, rooms, and the bot-to-bot exchange log live
|
|
3
3
|
// in one JSON file under DSH_HOME so they survive restarts without new Session event types.
|
|
4
|
-
import { homedir } from 'node:os'
|
|
5
4
|
import { join } from 'node:path'
|
|
6
5
|
import * as diagnostics from './src/host/diagnostics.js'
|
|
7
6
|
import * as store from './src/host/store.js'
|
|
@@ -10,6 +9,7 @@ import * as permissions from './src/host/permissions.js'
|
|
|
10
9
|
import * as turns from './src/host/turns.js'
|
|
11
10
|
import * as delivery from './src/host/delivery.js'
|
|
12
11
|
import * as roster from './src/host/roster.js'
|
|
12
|
+
import * as folders from './src/host/folders.js'
|
|
13
13
|
import * as agents from './src/host/agents.js'
|
|
14
14
|
import * as prompt from './src/host/prompt.js'
|
|
15
15
|
import * as timeContext from './src/host/time-context.js'
|
|
@@ -40,17 +40,19 @@ export const inject = ['llm', 'tools', 'systemPrompt', 'sessionController', 'ses
|
|
|
40
40
|
// the functions other modules call; they call them through `rt` at run time, never during
|
|
41
41
|
// install. Listeners run and tools are listed in registration order, so this order holds:
|
|
42
42
|
// usage meters model requests after every listener that rewrites them.
|
|
43
|
-
const MODULES = [diagnostics, store, models, permissions, turns, delivery, roster, agents, prompt, timeContext, tools, chatIndex, groups, approvals, resume, memory, parts, images, secrets, connectors, capabilities, schedules, browser, usage, update, dshUpdate, channel]
|
|
43
|
+
const MODULES = [diagnostics, store, models, permissions, turns, delivery, roster, folders, agents, prompt, timeContext, tools, chatIndex, groups, approvals, resume, memory, parts, images, secrets, connectors, capabilities, schedules, browser, usage, update, dshUpdate, channel]
|
|
44
44
|
|
|
45
45
|
export function apply(ctx, config = {}) {
|
|
46
46
|
createRuntime(ctx, config)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function createRuntime(ctx, config = {}) {
|
|
50
|
-
const
|
|
50
|
+
const dshHome = folders.dshHomeOf(process.env.DSH_HOME)
|
|
51
|
+
const home = config.home ?? join(dshHome, 'bot')
|
|
51
52
|
const rt = {
|
|
52
53
|
ctx,
|
|
53
54
|
config,
|
|
55
|
+
dshHome,
|
|
54
56
|
home,
|
|
55
57
|
statePath: join(home, 'state.json'),
|
|
56
58
|
defaults: {
|
package/package.json
CHANGED
package/src/host/channel.js
CHANGED
|
@@ -100,7 +100,7 @@ export function install(rt) {
|
|
|
100
100
|
return rt.save()
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
const handle = async (endpoint, payload) => {
|
|
103
|
+
const handle = async (endpoint, payload, signal) => {
|
|
104
104
|
try {
|
|
105
105
|
const body = payload ?? {}
|
|
106
106
|
await rt.refresh()
|
|
@@ -141,7 +141,7 @@ export function install(rt) {
|
|
|
141
141
|
return ok([...rt.roomProgress.entries()].filter(([, entry]) => entry.roomId === body.roomId).map(([callId, entry]) => ({ callId, replies: entry.replies, speaking: entry.speaking ?? null })))
|
|
142
142
|
case 'create-bot': {
|
|
143
143
|
await rt.bootstrap()
|
|
144
|
-
const bot = await rt.createBot({ name: body.name, role: body.role, color: body.color, instructions: body.instructions, brief: body.brief, avatar: body.avatar, model: body.model, effort: body.effort })
|
|
144
|
+
const bot = await rt.createBot({ name: body.name, role: body.role, color: body.color, instructions: body.instructions, brief: body.brief, avatar: body.avatar, model: body.model, effort: body.effort, workDir: body.workDir })
|
|
145
145
|
return ok(bot)
|
|
146
146
|
}
|
|
147
147
|
case 'update-bot':
|
|
@@ -174,9 +174,15 @@ export function install(rt) {
|
|
|
174
174
|
members: Array.isArray(body.members) ? body.members : undefined,
|
|
175
175
|
add: Array.isArray(body.add) ? body.add : undefined,
|
|
176
176
|
remove: Array.isArray(body.remove) ? body.remove : undefined,
|
|
177
|
+
workDir: body.workDir,
|
|
177
178
|
})
|
|
178
179
|
return ok({ room: view().rooms.find(entry => entry.id === room.id), changes })
|
|
179
180
|
}
|
|
181
|
+
case 'folders':
|
|
182
|
+
case 'folder-pick':
|
|
183
|
+
case 'folder-list':
|
|
184
|
+
await rt.load()
|
|
185
|
+
return ok(await rt.folderEndpoint(endpoint, body, signal))
|
|
180
186
|
case 'add-agent':
|
|
181
187
|
await rt.load()
|
|
182
188
|
return ok(await rt.addAgent())
|
|
@@ -332,7 +338,7 @@ export function install(rt) {
|
|
|
332
338
|
fetch: async (request) => {
|
|
333
339
|
let body
|
|
334
340
|
try { body = await request.json() } catch { body = {} }
|
|
335
|
-
const result = await handle(String(body?.endpoint ?? ''), body?.payload)
|
|
341
|
+
const result = await handle(String(body?.endpoint ?? ''), body?.payload, request.signal)
|
|
336
342
|
return new Response(JSON.stringify(result), { status: 200, headers: { 'content-type': 'application/json' } })
|
|
337
343
|
},
|
|
338
344
|
}), 'ds-bot: api route')
|
package/src/host/constants.js
CHANGED
|
@@ -21,6 +21,9 @@ export const OCR_CACHE_MAX = 500
|
|
|
21
21
|
export const MAX_AVATAR_IMAGE = 160_000
|
|
22
22
|
export const ROUTE = '/api/bot'
|
|
23
23
|
export const MAX_HOPS = 6
|
|
24
|
+
// Bot requests a Bot's own chat may hold unread; past this the Bot is most likely
|
|
25
|
+
// stuck, and more messages only pile up behind it.
|
|
26
|
+
export const MAX_UNREAD_REQUESTS = 10
|
|
24
27
|
export const MAX_EXCHANGES = 600
|
|
25
28
|
export const ROOM_REPLY_TIMEOUT_MS = 240_000
|
|
26
29
|
export const SOURCE_KIND = 'bot'
|
package/src/host/delivery.js
CHANGED
|
@@ -9,6 +9,7 @@ export function install(rt) {
|
|
|
9
9
|
const resolveAgent = async (sessionId) => {
|
|
10
10
|
const resolved = await ctx.sessionController.resolveAgent(sessionId)
|
|
11
11
|
if ('error' in resolved) throw new Error(resolved.error?.message ?? 'Session unavailable')
|
|
12
|
+
rt.hideBlockingQuestion(resolved.agent)
|
|
12
13
|
return resolved.agent
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Working folders: where a Bot's Sessions run. A group's folder holds for every member
|
|
2
|
+
// there, else a Bot works in its own folder, else in the team workspace. A Session's
|
|
3
|
+
// folder is fixed when it is made, so a change moves the conversation on to a fresh
|
|
4
|
+
// part (parts.js).
|
|
5
|
+
import { realpath, stat } from 'node:fs/promises'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { basename, isAbsolute, join, resolve, sep } from 'node:path'
|
|
8
|
+
import { unquote } from './text.js'
|
|
9
|
+
|
|
10
|
+
const PICK_TIMEOUT_MS = 10 * 60_000
|
|
11
|
+
|
|
12
|
+
export const within = (inner, outer) => inner === outer || inner.startsWith(outer.endsWith(sep) ? outer : outer + sep)
|
|
13
|
+
|
|
14
|
+
const expandHome = text => (text === '~' ? homedir() : /^~[\\/]/.test(text) ? join(homedir(), text.slice(2)) : text)
|
|
15
|
+
|
|
16
|
+
// Read as DSH reads it, so the team's data and the guarded folder are where DSH keeps
|
|
17
|
+
// its own: a blank value counts as unset.
|
|
18
|
+
export const dshHomeOf = value => resolve(expandHome(value?.trim() ? value : join(homedir(), '.dsh')))
|
|
19
|
+
|
|
20
|
+
export function install(rt) {
|
|
21
|
+
const { ctx, state } = rt
|
|
22
|
+
|
|
23
|
+
const teamFolder = () => ctx.workspaceRegistry.get?.(state.workspaceId)?.path
|
|
24
|
+
const workDirOf = ({ botId, roomId } = {}) => rt.roomOf(roomId)?.workDir ?? rt.botOf(botId)?.workDir
|
|
25
|
+
const isFolder = path => stat(path).then(info => info.isDirectory(), () => false)
|
|
26
|
+
const real = path => realpath(path).catch(() => resolve(path))
|
|
27
|
+
|
|
28
|
+
// A Bot that could write where DSH or the team keeps its data could change its own
|
|
29
|
+
// permissions or the team, so neither those folders nor one that holds them is allowed.
|
|
30
|
+
async function guarded(path) {
|
|
31
|
+
const team = teamFolder()
|
|
32
|
+
for (const root of [rt.dshHome, rt.home]) {
|
|
33
|
+
const data = await real(root)
|
|
34
|
+
if (within(data, path)) return true
|
|
35
|
+
if (within(path, data) && !(team !== undefined && within(path, team))) return true
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Saved as the real path, which is what a Session's cwd shows. Undefined means the
|
|
41
|
+
// team workspace.
|
|
42
|
+
async function cleanWorkDir(input) {
|
|
43
|
+
const text = unquote(input)
|
|
44
|
+
if (text === '') return undefined
|
|
45
|
+
const expanded = expandHome(text)
|
|
46
|
+
if (!isAbsolute(expanded)) throw new Error(`Give the full path of the folder, not ${text}`)
|
|
47
|
+
let path
|
|
48
|
+
try {
|
|
49
|
+
path = await realpath(expanded)
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error(`There is no folder at ${text}`)
|
|
52
|
+
}
|
|
53
|
+
if (!(await isFolder(path))) throw new Error(`${text} is a file, not a folder`)
|
|
54
|
+
if (await guarded(path)) throw new Error(`${path} holds DeepSeek Harness data, so Bots cannot work there`)
|
|
55
|
+
return path === teamFolder() ? undefined : path
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Every Session of a Bot: its own chat and its conversation in each of its groups.
|
|
59
|
+
const sessionsOfBot = botId => [rt.chatOf(botId), ...Object.values(state.rooms)
|
|
60
|
+
.filter(room => room.members.includes(botId)).map(room => room.sessions?.[botId])].filter(Boolean)
|
|
61
|
+
|
|
62
|
+
// A folder outside the team workspace widens where a Bot may write, so a Bot that
|
|
63
|
+
// sets one for Bots whose writes are bounded by their folder asks the user first.
|
|
64
|
+
// `sessions` are the Sessions that would work there; the default policy counts too,
|
|
65
|
+
// since the Sessions made there later start under it. Throws when it is not allowed.
|
|
66
|
+
async function approveFolder(exec, { path, sessions = [], who }) {
|
|
67
|
+
const team = teamFolder()
|
|
68
|
+
const policy = ctx.get?.('sandboxPolicy')
|
|
69
|
+
if (path === undefined || (team !== undefined && within(path, team)) || policy === undefined) return
|
|
70
|
+
const agents = await Promise.all(sessions.map(id => rt.resolveAgent(id)))
|
|
71
|
+
const modes = [policy.resolve(), ...agents.map(agent => policy.resolve({ session: agent.session }))].map(entry => entry.mode)
|
|
72
|
+
if (!modes.includes('workspace-write')) return
|
|
73
|
+
const approval = ctx.get?.('approval')
|
|
74
|
+
const agent = exec.agent
|
|
75
|
+
if (approval === undefined || agent?.session === undefined) throw new Error(`Working in ${path} needs the user's approval, and there is no way to ask for it here, so nothing changed. The user can make this change in the settings.`)
|
|
76
|
+
const outcome = await approval.request({
|
|
77
|
+
agent,
|
|
78
|
+
toolName: exec.name,
|
|
79
|
+
...(exec.callId !== undefined ? { callId: exec.callId } : {}),
|
|
80
|
+
reason: `Let ${who.en} work in ${path}? Files there can then be changed.`,
|
|
81
|
+
displayReason: { en: `Let ${who.en} work in ${path}? Files there can then be changed.`, zh: `允许${who.zh}在 ${path} 中工作吗?之后可以修改这个文件夹里的文件。` },
|
|
82
|
+
...(exec.signal ? { signal: exec.signal } : {}),
|
|
83
|
+
})
|
|
84
|
+
if (outcome === 'allowed-once') return
|
|
85
|
+
// An approval policy that never asks answers 'rejected' as well.
|
|
86
|
+
throw new Error(outcome === 'rejected' ? `Working in ${path} was not allowed, so nothing changed. The user can make this change in the settings.`
|
|
87
|
+
: outcome === 'cancelled' ? 'The approval was cancelled, so nothing changed.'
|
|
88
|
+
: `Working in ${path} needs the user's approval, and no one could answer, so nothing changed. The user can make this change in the settings.`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const picker = () => {
|
|
92
|
+
try {
|
|
93
|
+
return ctx.get?.('directoryPicker')?.capability()
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function folderEndpoint(endpoint, body, signal) {
|
|
100
|
+
switch (endpoint) {
|
|
101
|
+
case 'folders': {
|
|
102
|
+
const team = teamFolder() ?? null
|
|
103
|
+
const known = new Map()
|
|
104
|
+
for (const workspace of ctx.workspaceRegistry.list?.() ?? []) {
|
|
105
|
+
if (workspace.path !== team) known.set(workspace.path, workspace.title)
|
|
106
|
+
}
|
|
107
|
+
for (const entry of [...Object.values(state.bots), ...Object.values(state.rooms)]) {
|
|
108
|
+
if (entry.workDir && !known.has(entry.workDir)) known.set(entry.workDir, basename(entry.workDir) || entry.workDir)
|
|
109
|
+
}
|
|
110
|
+
const present = await Promise.all([...known].map(async ([path, title]) => (await isFolder(path) ? { path, title } : undefined)))
|
|
111
|
+
const kind = picker()?.kind
|
|
112
|
+
return {
|
|
113
|
+
team,
|
|
114
|
+
picker: kind === 'native' || kind === 'browse' ? kind : null,
|
|
115
|
+
folders: present.filter(Boolean),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
case 'folder-pick': {
|
|
119
|
+
const native = picker()
|
|
120
|
+
if (native?.kind !== 'native') throw new Error('This DeepSeek Harness has no folder chooser')
|
|
121
|
+
return await native.pick(AbortSignal.any([signal, AbortSignal.timeout(PICK_TIMEOUT_MS)].filter(Boolean)))
|
|
122
|
+
}
|
|
123
|
+
case 'folder-list': {
|
|
124
|
+
const browse = picker()
|
|
125
|
+
if (browse?.kind !== 'browse') throw new Error('This DeepSeek Harness cannot list folders')
|
|
126
|
+
const listing = await browse.list(typeof body.path === 'string' && body.path !== '' ? body.path : undefined, signal)
|
|
127
|
+
return { path: listing.path, home: listing.home, crumbs: listing.crumbs, entries: listing.entries, truncated: listing.truncated }
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
throw new Error(`Unknown endpoint ${endpoint}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Object.assign(rt, { teamFolder, workDirOf, isFolder, cleanWorkDir, sessionsOfBot, approveFolder, folderEndpoint })
|
|
135
|
+
}
|
package/src/host/groups.js
CHANGED
|
@@ -124,7 +124,7 @@ export function install(rt) {
|
|
|
124
124
|
await rt.ensureCapabilities(known)
|
|
125
125
|
return { id: known, brief: undefined }
|
|
126
126
|
}
|
|
127
|
-
const id = await rt.createSession(`${member.name} · ${room.name}
|
|
127
|
+
const id = await rt.createSession(`${member.name} · ${room.name}`, { dir: rt.workDirOf({ botId: member.id, roomId: room.id }) })
|
|
128
128
|
room.sessions[member.id] = id
|
|
129
129
|
rt.groupOwners.set(id, { botId: member.id, roomId: room.id })
|
|
130
130
|
delete room.seen[member.id]
|
|
@@ -651,9 +651,10 @@ export function install(rt) {
|
|
|
651
651
|
|
|
652
652
|
// A group chat's own Session never consults the model: models drift from "only call
|
|
653
653
|
// group_relay" (they message one member directly when the user names it), so the
|
|
654
|
-
// relay step is synthesized here. Auxiliary calls such as titles
|
|
654
|
+
// relay step is synthesized here. Auxiliary calls such as titles, and DS Bot's own
|
|
655
|
+
// calls for the room such as image readings, still reach the model.
|
|
655
656
|
ctx.on('llm/stream', (options, next) => {
|
|
656
|
-
if (options.sessionId === undefined || options.purpose !== undefined) return next()
|
|
657
|
+
if (options.sessionId === undefined || options.purpose !== undefined || rt.ownRequest(options)) return next()
|
|
657
658
|
return (async function* relayOrPass() {
|
|
658
659
|
await rt.load()
|
|
659
660
|
if (rt.roomOf(options.sessionId) === undefined) {
|
package/src/host/images.js
CHANGED
|
@@ -91,10 +91,10 @@ export function install(rt) {
|
|
|
91
91
|
const route = await ocrRoute()
|
|
92
92
|
if (route === undefined) throw new Error('no vision model is available')
|
|
93
93
|
const assembler = new BlockAssembler()
|
|
94
|
-
// No sessionId: this call is not a Session request, so the hook below lets it through.
|
|
95
94
|
for await (const chunk of ctx.llm.stream({
|
|
96
95
|
provider: route.provider,
|
|
97
96
|
model: route.model,
|
|
97
|
+
sessionId,
|
|
98
98
|
messages: rt.tagUsage([{ role: 'user', content: [{ type: 'image', attachment }, { type: 'text', text: OCR_PROMPT }] }], { sessionId, kind: 'image' }),
|
|
99
99
|
maxTokens: 8192,
|
|
100
100
|
signal: AbortSignal.timeout(OCR_TIMEOUT_MS),
|
|
@@ -176,7 +176,7 @@ export function install(rt) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
ctx.on('llm/stream', (options, next) => {
|
|
179
|
-
if (options.sessionId === undefined || !(options.messages ?? []).some(liveImages)) return next()
|
|
179
|
+
if (options.sessionId === undefined || rt.ownRequest(options) || !(options.messages ?? []).some(liveImages)) return next()
|
|
180
180
|
return (async function* imagesAsTextOrPass() {
|
|
181
181
|
await rt.load()
|
|
182
182
|
if (rt.roomOf(options.sessionId) !== undefined || await readsImages(options.provider, options.model)) {
|
package/src/host/memory.js
CHANGED
|
@@ -919,6 +919,7 @@ export function install(rt) {
|
|
|
919
919
|
for await (const chunk of ctx.llm.stream({
|
|
920
920
|
provider: route.provider,
|
|
921
921
|
model: route.model,
|
|
922
|
+
...(tag?.sessionId === undefined ? {} : { sessionId: tag.sessionId }),
|
|
922
923
|
...(effort === undefined ? {} : { reasoningEffort: effort }),
|
|
923
924
|
system,
|
|
924
925
|
messages,
|
|
@@ -936,6 +937,25 @@ export function install(rt) {
|
|
|
936
937
|
throw new Error(`The model's reasoning used up the output budget (${SUMMARY_MAX_TOKENS} and ${SUMMARY_RETRY_MAX_TOKENS} tokens); no complete answer was written`)
|
|
937
938
|
}
|
|
938
939
|
|
|
940
|
+
// A served model can still refuse the call (a key without access, say), and
|
|
941
|
+
// desiredModel only falls back for models that are not served; so a failed call
|
|
942
|
+
// is tried once more on the profile default.
|
|
943
|
+
async function botShot(bot, system, text, tag) {
|
|
944
|
+
const own = await modelFor(bot)
|
|
945
|
+
try {
|
|
946
|
+
return await oneShot(own, system, text, tag)
|
|
947
|
+
} catch (error) {
|
|
948
|
+
const fallback = splitRef(await rt.defaultModel())
|
|
949
|
+
if (fallback === undefined || (fallback.provider === own.provider && fallback.model === own.model)) throw error
|
|
950
|
+
try {
|
|
951
|
+
return await oneShot(fallback, system, text, tag)
|
|
952
|
+
} catch (again) {
|
|
953
|
+
const said = failure => failure?.message ?? String(failure)
|
|
954
|
+
throw new Error(`${own.provider}/${own.model}: ${said(error)}; ${fallback.provider}/${fallback.model}: ${said(again)}`)
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
939
959
|
/** @type {Map<string, {hash:string, text:string, at:number}>} */
|
|
940
960
|
const summaries = new Map()
|
|
941
961
|
const summarizing = new Map()
|
|
@@ -955,21 +975,23 @@ export function install(rt) {
|
|
|
955
975
|
const linesFor = files => allEntries(files).map(entry => `- ${entry.file.topic ? `[[${entry.file.topic}]] ` : ''}${entry.text}`)
|
|
956
976
|
|
|
957
977
|
function startSummary(bot, own, team, language, hash) {
|
|
978
|
+
const model = bot.model
|
|
958
979
|
const job = (async () => {
|
|
959
|
-
const text = await
|
|
980
|
+
const text = await botShot(bot, SUMMARY_SYSTEM, summaryRequest({ name: bot.name, own: linesFor(own), team: linesFor(team), language }), { sessionId: rt.chatOf(bot.id), kind: 'memory' })
|
|
960
981
|
const value = { hash, text: text.slice(0, SUMMARY_TEXT_MAX), at: Date.now() }
|
|
961
982
|
summaries.set(bot.id, value)
|
|
962
983
|
summaryFailures.delete(bot.id)
|
|
963
984
|
if (rt.readOnly() === undefined) await writeText(summaryPath(bot.id), JSON.stringify(value, null, 2))
|
|
964
985
|
})().catch((error) => {
|
|
965
|
-
summaryFailures.set(bot.id, { hash, message: error?.message ?? String(error) })
|
|
986
|
+
summaryFailures.set(bot.id, { hash, model, message: error?.message ?? String(error) })
|
|
966
987
|
rt.warn('ds-bot: memory summary for %s failed: %s', bot.name, error)
|
|
967
988
|
}).finally(() => summarizing.delete(bot.id))
|
|
968
989
|
summarizing.set(bot.id, job)
|
|
969
990
|
}
|
|
970
991
|
|
|
971
992
|
// The summary is written again when the entries or the interface language change;
|
|
972
|
-
// a failed one waits for `regenerate
|
|
993
|
+
// a failed one waits for `regenerate`, or a new model for the Bot, instead of
|
|
994
|
+
// retrying on every poll.
|
|
973
995
|
function memoryView(body) {
|
|
974
996
|
const bot = rt.botOf(String(body.botId ?? ''))
|
|
975
997
|
if (bot === undefined) throw new Error('Unknown Bot')
|
|
@@ -979,8 +1001,9 @@ export function install(rt) {
|
|
|
979
1001
|
const language = languageNow()
|
|
980
1002
|
const hash = createHash('sha256').update(JSON.stringify([language, linesFor(own), linesFor(team)])).digest('hex')
|
|
981
1003
|
const saved = savedSummary(bot.id)
|
|
982
|
-
const
|
|
983
|
-
const
|
|
1004
|
+
const prior = summaryFailures.get(bot.id)
|
|
1005
|
+
const failed = prior?.hash === hash && prior.model === bot.model ? prior : undefined
|
|
1006
|
+
const wanted = body.regenerate === true || (body.summarize === true && saved?.hash !== hash && failed === undefined)
|
|
984
1007
|
if (wanted && entries.length > 0 && !summarizing.has(bot.id)) startSummary(bot, own, team, language, hash)
|
|
985
1008
|
return {
|
|
986
1009
|
botId: bot.id,
|
|
@@ -990,7 +1013,7 @@ export function install(rt) {
|
|
|
990
1013
|
summary: entries.length > 0 && saved !== undefined ? { text: saved.text, at: saved.at } : null,
|
|
991
1014
|
fresh: entries.length === 0 || saved?.hash === hash,
|
|
992
1015
|
summarizing: summarizing.has(bot.id),
|
|
993
|
-
error: failed
|
|
1016
|
+
error: failed !== undefined && !summarizing.has(bot.id) ? failed.message : null,
|
|
994
1017
|
}
|
|
995
1018
|
}
|
|
996
1019
|
|
|
@@ -1003,7 +1026,7 @@ export function install(rt) {
|
|
|
1003
1026
|
...allEntries(loadScope(ownScope(bot.id))).map(entry => ({ ...entry, scope: 'own' })),
|
|
1004
1027
|
...allEntries(loadScope(TEAM)).map(entry => ({ ...entry, scope: 'team' })),
|
|
1005
1028
|
]
|
|
1006
|
-
const answer = parseAsk(await
|
|
1029
|
+
const answer = parseAsk(await botShot(bot, ASK_SYSTEM, askRequest({ name: bot.name, numbered, language: languageNow(), text }), { sessionId: rt.chatOf(bot.id), kind: 'memory' }))
|
|
1007
1030
|
const meta = { source: 'memory panel', by: 'user', added: today() }
|
|
1008
1031
|
const changes = []
|
|
1009
1032
|
for (const change of answer.changes.slice(0, ASK_CHANGES_MAX)) {
|
|
@@ -1236,7 +1259,7 @@ export function install(rt) {
|
|
|
1236
1259
|
const sizes = { own: ownFiles[0].content.length, team: teamFiles[0].content.length }
|
|
1237
1260
|
const earlier = batch.earlier.map(line => reviewLine(line.who, line.text, bot))
|
|
1238
1261
|
const language = writtenIn(read.filter(line => line.who === 'user').map(line => line.text.replace(/^User: /, '')), languageNow())
|
|
1239
|
-
const reply = await
|
|
1262
|
+
const reply = await botShot(bot, reviewSystem(writesTeam(bot.id)), reviewRequest({ bot, numbered, left, sizes, where: batch.where, earlier, fresh: fresh.texts, more, language }), { sessionId: rt.chatOf(bot.id), kind: 'memory' })
|
|
1240
1263
|
const changes = parseReview(reply)
|
|
1241
1264
|
if (changes === undefined) throw new Error(`the model's answer is not a list of changes: ${clip(reply.replace(/\s+/g, ' '), 120)}`)
|
|
1242
1265
|
const unknown = changes.find(change => change.op !== 'add' && numbered[Number(change.entry) - 1] === undefined)
|