ds-bot 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +236 -3
- package/client.js +12618 -0
- package/cordis.patch.yml +17 -0
- package/index.js +54 -0
- package/package.json +67 -6
- package/src/host/agents.js +186 -0
- package/src/host/browser.js +258 -0
- package/src/host/channel.js +320 -0
- package/src/host/chat-index.js +293 -0
- package/src/host/connectors.js +169 -0
- package/src/host/constants.js +62 -0
- package/src/host/delivery.js +59 -0
- package/src/host/diagnostics.js +247 -0
- package/src/host/dsh-update.js +168 -0
- package/src/host/groups.js +444 -0
- package/src/host/home-lock.js +91 -0
- package/src/host/images.js +174 -0
- package/src/host/memory.js +1351 -0
- package/src/host/models.js +202 -0
- package/src/host/parts.js +902 -0
- package/src/host/permissions.js +42 -0
- package/src/host/prompt.js +67 -0
- package/src/host/roster.js +410 -0
- package/src/host/schedules.js +151 -0
- package/src/host/secrets.js +530 -0
- package/src/host/steward.js +164 -0
- package/src/host/store.js +259 -0
- package/src/host/text.js +47 -0
- package/src/host/tools.js +374 -0
- package/src/host/turns.js +250 -0
- package/src/host/update.js +210 -0
- package/src/host/usage.js +594 -0
- package/web-search.js +748 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Models. Every Bot runs on one model of its own, fixed when it is created and changed
|
|
2
|
+
// only in its settings or by a Main Bot. The team's first Main Bot has none until the
|
|
3
|
+
// user picks one in its chat. The Session keeps the selection durably.
|
|
4
|
+
import { normalize, splitRef } from './text.js'
|
|
5
|
+
|
|
6
|
+
const CATALOG_TTL_MS = 10_000
|
|
7
|
+
const ROUTE_WAIT_MS = 30_000
|
|
8
|
+
|
|
9
|
+
export function install(rt) {
|
|
10
|
+
const { ctx, state } = rt
|
|
11
|
+
|
|
12
|
+
let modelsSynced = null
|
|
13
|
+
let catalogCache = { at: 0, value: [] }
|
|
14
|
+
// A model's thinking levels, resolved once and kept for a short while: they change
|
|
15
|
+
// only with the provider, which also drops the catalog cache.
|
|
16
|
+
const reasoningCache = new Map()
|
|
17
|
+
ctx.on?.('llm/adapters-updated', () => { catalogCache = { at: 0, value: [] }; reasoningCache.clear() })
|
|
18
|
+
|
|
19
|
+
// The thinking levels a model serves: `{efforts: [{id, name, description?}],
|
|
20
|
+
// defaultEffort?}`, or undefined when it has none or the provider cannot say.
|
|
21
|
+
async function reasoningOf(ref) {
|
|
22
|
+
const cached = reasoningCache.get(ref)
|
|
23
|
+
if (cached !== undefined && Date.now() - cached.at < CATALOG_TTL_MS) return cached.value
|
|
24
|
+
const route = splitRef(ref)
|
|
25
|
+
const info = route === undefined ? undefined : await ctx.llm.resolveModelInfo?.(route.provider, route.model).catch(() => undefined)
|
|
26
|
+
const value = info?.reasoning === undefined ? undefined : {
|
|
27
|
+
efforts: (info.reasoning.efforts ?? []).map(effort => ({ id: effort.id, name: effort.name ?? effort.id, ...(effort.description ? { description: effort.description } : {}) })),
|
|
28
|
+
...(info.reasoning.defaultEffort === undefined ? {} : { defaultEffort: info.reasoning.defaultEffort }),
|
|
29
|
+
}
|
|
30
|
+
reasoningCache.set(ref, { at: Date.now(), value })
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A provider's model list can change in DSH settings without any event, so a picker
|
|
35
|
+
// that is about to be used asks for a `fresh` one. Entries for models a Bot uses
|
|
36
|
+
// also carry their `reasoning`, so the picker can offer thinking levels.
|
|
37
|
+
async function catalog({ fresh = false } = {}) {
|
|
38
|
+
if (!fresh && Date.now() - catalogCache.at < CATALOG_TTL_MS && catalogCache.value.length > 0) return catalogCache.value
|
|
39
|
+
const entries = []
|
|
40
|
+
for (const provider of ctx.llm.listProviders()) {
|
|
41
|
+
let models = []
|
|
42
|
+
try { models = await ctx.llm.listModels(provider.id) } catch { continue }
|
|
43
|
+
for (const model of models) {
|
|
44
|
+
entries.push({
|
|
45
|
+
ref: `${provider.id}/${model.id}`,
|
|
46
|
+
provider: provider.id,
|
|
47
|
+
providerName: provider.name ?? provider.id,
|
|
48
|
+
id: model.id,
|
|
49
|
+
name: model.name ?? model.id,
|
|
50
|
+
...(model.inputModalities ? { input: [...model.inputModalities] } : {}),
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const used = new Set(Object.values(state.bots ?? {}).flatMap(bot => [bot.model, bot.appliedModel].filter(Boolean)))
|
|
55
|
+
await Promise.all(entries.filter(entry => used.has(entry.ref)).map(async (entry) => {
|
|
56
|
+
const reasoning = await reasoningOf(entry.ref)
|
|
57
|
+
if (reasoning !== undefined) entry.reasoning = reasoning
|
|
58
|
+
}))
|
|
59
|
+
catalogCache = { at: Date.now(), value: entries }
|
|
60
|
+
return entries
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The profile default model of DSH as set, served or not.
|
|
64
|
+
const defaultRef = () => {
|
|
65
|
+
const selection = ctx.get?.('agentDefaultModel')?.currentSelection?.()
|
|
66
|
+
return selection?.provider && selection?.model ? `${selection.provider}/${selection.model}` : undefined
|
|
67
|
+
}
|
|
68
|
+
// The profile default model of DSH, when it is served; else the first served model.
|
|
69
|
+
function pickDefault(entries) {
|
|
70
|
+
const ref = defaultRef()
|
|
71
|
+
return entries.find(entry => entry.ref === ref)?.ref ?? entries[0]?.ref
|
|
72
|
+
}
|
|
73
|
+
const defaultModel = async () => pickDefault(await catalog())
|
|
74
|
+
|
|
75
|
+
// A full `provider/model` ref, or a model id or name that only one provider serves.
|
|
76
|
+
async function resolveModel(text) {
|
|
77
|
+
const entries = await catalog({ fresh: true })
|
|
78
|
+
const wanted = String(text ?? '').trim()
|
|
79
|
+
const exact = entries.find(entry => entry.ref === wanted)
|
|
80
|
+
if (exact) return exact.ref
|
|
81
|
+
const key = normalize(wanted)
|
|
82
|
+
const named = entries.filter(entry => normalize(entry.id) === key || normalize(entry.name) === key)
|
|
83
|
+
if (named.length === 1) return named[0].ref
|
|
84
|
+
const known = entries.map(entry => entry.ref).join(', ') || 'none'
|
|
85
|
+
throw new Error(named.length > 1
|
|
86
|
+
? `Model ${wanted} is served by more than one provider; use one of ${named.map(entry => entry.ref).join(', ')}`
|
|
87
|
+
: `Model ${wanted} is not available. Available models: ${known}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A Bot with no model yet waits for the user to pick one and changes nothing. A
|
|
91
|
+
// model that is no longer served falls back to the last one it ran on, then the default.
|
|
92
|
+
async function desiredModel(bot) {
|
|
93
|
+
if (!bot.model) return undefined
|
|
94
|
+
const entries = await catalog()
|
|
95
|
+
const served = new Set(entries.map(entry => entry.ref))
|
|
96
|
+
return [bot.model, bot.appliedModel].find(ref => ref && served.has(ref)) ?? pickDefault(entries)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The thinking level a Bot asked for, when the model it runs on serves it; else the
|
|
100
|
+
// model's own default applies. A level the model does not have is ignored, never
|
|
101
|
+
// sent: an explicit effort a provider does not know is a hard refusal there.
|
|
102
|
+
async function desiredEffort(bot, ref) {
|
|
103
|
+
if (bot.effort === undefined || ref === undefined) return undefined
|
|
104
|
+
const reasoning = await reasoningOf(ref)
|
|
105
|
+
return reasoning?.efforts?.some(effort => effort.id === bot.effort) ? bot.effort : undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A thinking level id, or a name only one level of the model has, like resolveModel.
|
|
109
|
+
async function resolveEffort(ref, text) {
|
|
110
|
+
const wanted = String(text ?? '').trim()
|
|
111
|
+
if (wanted === '') return undefined
|
|
112
|
+
const reasoning = await reasoningOf(ref)
|
|
113
|
+
const efforts = reasoning?.efforts ?? []
|
|
114
|
+
if (efforts.length === 0) throw new Error(`Model ${ref} has no thinking levels`)
|
|
115
|
+
const key = normalize(wanted)
|
|
116
|
+
const named = efforts.filter(effort => normalize(effort.id) === key || normalize(effort.name) === key)
|
|
117
|
+
if (named.length === 1) return named[0].id
|
|
118
|
+
throw new Error(`Thinking level ${wanted} is not available for ${ref}. Available: ${efforts.map(effort => effort.name).join(', ')}`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// DSH's selectModel also saves the selection as the profile default model, in the
|
|
122
|
+
// background. A Bot's model is its own, so the user's default is written back after
|
|
123
|
+
// it. Selections run one at a time and wait for the write-back: profile writes land in
|
|
124
|
+
// submission order, so the next one reads the user's default, not a Bot's.
|
|
125
|
+
let selecting = Promise.resolve()
|
|
126
|
+
function selectSessionModel(sessionId, { provider, model, reasoningEffort }) {
|
|
127
|
+
const run = selecting.then(async () => {
|
|
128
|
+
const defaults = ctx.get?.('agentDefaultModel')
|
|
129
|
+
const before = defaults?.currentSelection?.()
|
|
130
|
+
const result = await ctx.sessionController.selectModel({
|
|
131
|
+
sessionId, provider, model,
|
|
132
|
+
...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
133
|
+
})
|
|
134
|
+
const saved = result?.selected ?? { provider, model, ...(reasoningEffort === undefined ? {} : { reasoningEffort }) }
|
|
135
|
+
if (before?.provider && before?.model && !sameSelection(before, saved)) {
|
|
136
|
+
await defaults.saveSelection(before).catch(error => rt.warn('ds-bot: the default model was not restored: %s', error))
|
|
137
|
+
}
|
|
138
|
+
return result
|
|
139
|
+
})
|
|
140
|
+
selecting = run.catch(() => {})
|
|
141
|
+
return run
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const sameSelection = (a, b) => a.provider === b.provider && a.model === b.model && a.reasoningEffort === b.reasoningEffort
|
|
145
|
+
|
|
146
|
+
// `force` re-selects even when the record says it is applied: the composer's own
|
|
147
|
+
// model picker can move the Session without telling this plugin.
|
|
148
|
+
async function applyModel(bot, { force = false } = {}) {
|
|
149
|
+
const ref = await desiredModel(bot)
|
|
150
|
+
if (ref === undefined) return ref
|
|
151
|
+
const effort = await desiredEffort(bot, ref)
|
|
152
|
+
if (!force && bot.appliedModel === ref && (bot.appliedEffort ?? undefined) === effort) return ref
|
|
153
|
+
const route = { ...splitRef(ref), ...(effort === undefined ? {} : { reasoningEffort: effort }) }
|
|
154
|
+
await selectSessionModel(rt.chatOf(bot.id), route)
|
|
155
|
+
for (const sessionId of rt.groupSessionsOf(bot.id)) {
|
|
156
|
+
await selectSessionModel(sessionId, route)
|
|
157
|
+
.catch(error => rt.warn('ds-bot: model for %s in a group not applied: %s', bot.name, error))
|
|
158
|
+
}
|
|
159
|
+
bot.appliedModel = ref
|
|
160
|
+
if (effort === undefined) delete bot.appliedEffort
|
|
161
|
+
else bot.appliedEffort = effort
|
|
162
|
+
await rt.save()
|
|
163
|
+
return ref
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// A provider may register after the plugin starts; a greeting waits for its Bot's
|
|
167
|
+
// model instead of failing with "no adapter registered".
|
|
168
|
+
async function waitForModel(bot, ms = ROUTE_WAIT_MS) {
|
|
169
|
+
const deadline = Date.now() + ms
|
|
170
|
+
for (;;) {
|
|
171
|
+
if (!bot.model) return undefined
|
|
172
|
+
catalogCache = { at: 0, value: [] }
|
|
173
|
+
const served = new Set((await catalog()).map(entry => entry.ref))
|
|
174
|
+
if (served.has(bot.model) || Date.now() >= deadline) return desiredModel(bot)
|
|
175
|
+
await new Promise((resolve) => {
|
|
176
|
+
const timer = setTimeout(done, Math.min(2000, deadline - Date.now()))
|
|
177
|
+
const off = ctx.on?.('llm/adapters-updated', done)
|
|
178
|
+
function done() { clearTimeout(timer); off?.(); resolve() }
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Sequential, and again whenever the served models change: a provider that
|
|
184
|
+
// registers late still gets its Bots' models selected.
|
|
185
|
+
let syncedKey
|
|
186
|
+
const syncModels = () => {
|
|
187
|
+
const key = catalogCache.value.map(entry => entry.ref).join(',')
|
|
188
|
+
if (key !== syncedKey) { syncedKey = key; modelsSynced = modelsSynced?.then(syncAll, syncAll) ?? syncAll() }
|
|
189
|
+
return modelsSynced
|
|
190
|
+
}
|
|
191
|
+
async function syncAll() {
|
|
192
|
+
for (const bot of Object.values(state.bots)) {
|
|
193
|
+
await applyModel(bot).catch(error => rt.warn('ds-bot: model for %s not applied: %s', bot.name, error))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
Object.assign(rt, {
|
|
198
|
+
catalog, cachedCatalog: () => catalogCache.value, cachedDefault: () => pickDefault(catalogCache.value),
|
|
199
|
+
defaultRef, defaultModel, resolveModel, desiredModel, reasoningOf, resolveEffort, desiredEffort,
|
|
200
|
+
selectSessionModel, applyModel, waitForModel, syncModels,
|
|
201
|
+
})
|
|
202
|
+
}
|