ds-bot 0.1.1 → 0.1.3
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 +5 -5
- package/client.js +1311 -466
- package/index.js +3 -1
- package/package.json +6 -7
- package/src/host/approvals.js +63 -0
- package/src/host/channel.js +14 -3
- package/src/host/constants.js +4 -1
- package/src/host/delivery.js +19 -7
- package/src/host/diagnostics.js +7 -5
- package/src/host/dsh-update.js +8 -6
- package/src/host/groups.js +195 -47
- package/src/host/home-lock.js +16 -10
- package/src/host/images.js +37 -0
- package/src/host/memory.js +27 -16
- package/src/host/parts.js +92 -21
- package/src/host/prompt.js +6 -2
- package/src/host/roster.js +109 -56
- package/src/host/store.js +113 -28
- package/src/host/time-context.js +46 -0
- package/src/host/tools.js +79 -28
- package/src/host/turns.js +176 -38
- package/src/host/update.js +31 -4
- package/src/shared/mentions.js +65 -0
- package/web-search.js +23 -7
package/index.js
CHANGED
|
@@ -12,9 +12,11 @@ import * as delivery from './src/host/delivery.js'
|
|
|
12
12
|
import * as roster from './src/host/roster.js'
|
|
13
13
|
import * as agents from './src/host/agents.js'
|
|
14
14
|
import * as prompt from './src/host/prompt.js'
|
|
15
|
+
import * as timeContext from './src/host/time-context.js'
|
|
15
16
|
import * as tools from './src/host/tools.js'
|
|
16
17
|
import * as chatIndex from './src/host/chat-index.js'
|
|
17
18
|
import * as groups from './src/host/groups.js'
|
|
19
|
+
import * as approvals from './src/host/approvals.js'
|
|
18
20
|
import * as memory from './src/host/memory.js'
|
|
19
21
|
import * as parts from './src/host/parts.js'
|
|
20
22
|
import * as images from './src/host/images.js'
|
|
@@ -36,7 +38,7 @@ export const inject = ['llm', 'tools', 'systemPrompt', 'sessionController', 'ses
|
|
|
36
38
|
// the functions other modules call; they call them through `rt` at run time, never during
|
|
37
39
|
// install. Listeners run and tools are listed in registration order, so this order holds:
|
|
38
40
|
// usage meters model requests after every listener that rewrites them.
|
|
39
|
-
const MODULES = [diagnostics, store, models, permissions, turns, delivery, roster, agents, prompt, tools, chatIndex, groups, memory, parts, images, secrets, connectors, schedules, browser, usage, update, dshUpdate, channel]
|
|
41
|
+
const MODULES = [diagnostics, store, models, permissions, turns, delivery, roster, agents, prompt, timeContext, tools, chatIndex, groups, approvals, memory, parts, images, secrets, connectors, schedules, browser, usage, update, dshUpdate, channel]
|
|
40
42
|
|
|
41
43
|
export function apply(ctx, config = {}) {
|
|
42
44
|
const home = config.home ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'bot')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ds-bot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "DS Bot: a team of long-lived Bots for DeepSeek Harness, with Main Bots, Bot-to-Bot messages and group chats",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"multi-agent",
|
|
12
12
|
"agents"
|
|
13
13
|
],
|
|
14
|
-
"homepage": "https://github.com/
|
|
14
|
+
"homepage": "https://github.com/Fei2411/DeepSeek-Bot#readme",
|
|
15
15
|
"bugs": {
|
|
16
|
-
"url": "https://github.com/
|
|
16
|
+
"url": "https://github.com/Fei2411/DeepSeek-Bot/issues"
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/
|
|
20
|
+
"url": "git+https://github.com/Fei2411/DeepSeek-Bot.git",
|
|
21
21
|
"directory": "ds-bot"
|
|
22
22
|
},
|
|
23
23
|
"license": "Apache-2.0",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"files": [
|
|
33
33
|
"index.js",
|
|
34
34
|
"src/host/",
|
|
35
|
+
"src/shared/",
|
|
35
36
|
"client.js",
|
|
36
37
|
"web-search.js",
|
|
37
38
|
"cordis.patch.yml",
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"build": "node scripts/build-client.mjs",
|
|
43
44
|
"test": "node --import ./test/harness-resolve.mjs --test \"test/*.test.js\"",
|
|
44
45
|
"lint": "node scripts/lint.mjs",
|
|
46
|
+
"harness": "npm install --prefix dev/harness --legacy-peer-deps=false --no-package-lock --no-fund --no-audit",
|
|
45
47
|
"check": "sh dev/check.sh"
|
|
46
48
|
},
|
|
47
49
|
"peerDependencies": {
|
|
@@ -58,9 +60,6 @@
|
|
|
58
60
|
"patch": "./cordis.patch.yml"
|
|
59
61
|
}
|
|
60
62
|
},
|
|
61
|
-
"dsBot": {
|
|
62
|
-
"verifiedDsh": "0.2.1-alpha.1"
|
|
63
|
-
},
|
|
64
63
|
"devDependencies": {
|
|
65
64
|
"esbuild": "0.28.2"
|
|
66
65
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Approvals. A member's group Session is hidden, so DSH's approval card never shows
|
|
2
|
+
// there and the turn would wait forever: DS Bot answers those requests itself, from
|
|
3
|
+
// the group chat. A Bot's own chat keeps DSH's card; its requests are only noted, so
|
|
4
|
+
// the team view can say who is waiting.
|
|
5
|
+
import { randomUUID } from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
const OUTCOMES = { allow: 'allowed-once', reject: 'rejected' }
|
|
8
|
+
|
|
9
|
+
export function install(rt) {
|
|
10
|
+
const { ctx } = rt
|
|
11
|
+
/** @type {Map<string, {id:string, sessionId:string, botId:string, roomId?:string, tool:string, reason:string, detail:{text?:string, code?:string}, at:number, answer?:(outcome:string)=>void}>} */
|
|
12
|
+
const pending = new Map()
|
|
13
|
+
|
|
14
|
+
// Ahead of DSH's own answerer, which would hold a hidden Session's request unseen.
|
|
15
|
+
ctx.on('approval/request', (request, next) => {
|
|
16
|
+
const sessionId = request.agent?.session?.id ?? request.agent?.id
|
|
17
|
+
const owner = sessionId === undefined ? undefined : rt.groupOwners.get(sessionId)
|
|
18
|
+
const botId = owner?.botId ?? (sessionId === undefined ? undefined : rt.chatOwners.get(sessionId))
|
|
19
|
+
if (botId === undefined) return next()
|
|
20
|
+
const id = randomUUID()
|
|
21
|
+
const entry = {
|
|
22
|
+
id,
|
|
23
|
+
sessionId,
|
|
24
|
+
botId,
|
|
25
|
+
...(owner ? { roomId: owner.roomId } : {}),
|
|
26
|
+
tool: String(request.toolName ?? ''),
|
|
27
|
+
reason: String(request.reason ?? '').slice(0, 500),
|
|
28
|
+
detail: rt.callDetail?.(sessionId, request.callId) ?? {},
|
|
29
|
+
at: Date.now(),
|
|
30
|
+
}
|
|
31
|
+
pending.set(id, entry)
|
|
32
|
+
if (owner === undefined) return Promise.resolve(next()).finally(() => pending.delete(id))
|
|
33
|
+
rt.yieldRoomTurn?.(sessionId)
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
entry.answer = (outcome) => {
|
|
36
|
+
if (!pending.delete(id)) return
|
|
37
|
+
resolve(outcome)
|
|
38
|
+
}
|
|
39
|
+
if (request.signal?.aborted) entry.answer('cancelled')
|
|
40
|
+
request.signal?.addEventListener('abort', () => entry.answer('cancelled'), { once: true })
|
|
41
|
+
})
|
|
42
|
+
}, { prepend: true })
|
|
43
|
+
|
|
44
|
+
const approvalsView = () => [...pending.values()].map(({ answer, ...entry }) => ({ ...entry, answerable: answer !== undefined }))
|
|
45
|
+
const waitsForApproval = sessionId => [...pending.values()].some(entry => entry.sessionId === sessionId)
|
|
46
|
+
|
|
47
|
+
function answerApproval(id, choice) {
|
|
48
|
+
const entry = pending.get(String(id ?? ''))
|
|
49
|
+
if (entry?.answer === undefined) throw Object.assign(new Error('This approval is no longer open'), { code: 'bot/stale' })
|
|
50
|
+
const outcome = OUTCOMES[choice]
|
|
51
|
+
if (outcome === undefined) throw Object.assign(new Error('Choose allow or reject'), { code: 'bot/invalid' })
|
|
52
|
+
entry.answer(outcome)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A group Session that leaves its group has no card left to answer from.
|
|
56
|
+
const cancelApprovals = (sessionId) => {
|
|
57
|
+
for (const entry of pending.values()) {
|
|
58
|
+
if (entry.sessionId === sessionId) entry.answer?.('cancelled')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Object.assign(rt, { approvalsView, waitsForApproval, answerApproval, cancelApprovals })
|
|
63
|
+
}
|
package/src/host/channel.js
CHANGED
|
@@ -67,11 +67,17 @@ export function install(rt) {
|
|
|
67
67
|
})
|
|
68
68
|
// What changes without a new revision, sent with every state answer. `memoryNews`:
|
|
69
69
|
// per Bot, its latest memory change; `memoryReviews`: memory reviews waiting or
|
|
70
|
-
// running.
|
|
71
|
-
const live = () => ({
|
|
70
|
+
// running; `work`: per Bot, the turn it runs and its tool, and the approvals waiting.
|
|
71
|
+
const live = () => ({
|
|
72
|
+
activity: rt.activityView(),
|
|
73
|
+
work: { bots: rt.workView(), approvals: rt.approvalsView() },
|
|
74
|
+
memoryNews: rt.memoryNews(),
|
|
75
|
+
memoryReviews: rt.memoryReviews(),
|
|
76
|
+
connectors: rt.connectorsView(),
|
|
77
|
+
})
|
|
72
78
|
// What a host that only reads the team may still answer.
|
|
73
79
|
// Updates change the dsh profile, not the team, so a reading host may run them too.
|
|
74
|
-
const READS = new Set(['state', 'models', 'efforts', 'earlier', 'room-progress', 'exchange', 'usage', 'schedules', 'memory-view', 'browser-wait', 'browser-result', 'diagnostics', 'diagnostics-log', 'diagnostics-clear', 'update-check', 'update-install', 'dsh-update-install'])
|
|
80
|
+
const READS = new Set(['state', 'models', 'efforts', 'earlier', 'room-progress', 'exchange', 'usage', 'schedules', 'memory-view', 'browser-wait', 'browser-result', 'diagnostics', 'diagnostics-log', 'diagnostics-clear', 'update-check', 'update-install', 'dsh-update-install', 'repo-stars', 'approval-answer'])
|
|
75
81
|
|
|
76
82
|
function setPrefs(body) {
|
|
77
83
|
if (body.theme !== undefined) {
|
|
@@ -196,6 +202,9 @@ export function install(rt) {
|
|
|
196
202
|
await rt.load()
|
|
197
203
|
await rt.deleteRoom(body.id)
|
|
198
204
|
return ok(view())
|
|
205
|
+
case 'approval-answer':
|
|
206
|
+
rt.answerApproval(body.id, body.choice)
|
|
207
|
+
return ok(true)
|
|
199
208
|
case 'dismiss-question':
|
|
200
209
|
await rt.load()
|
|
201
210
|
if (state.questions[body.sessionId] !== undefined) {
|
|
@@ -270,6 +279,8 @@ export function install(rt) {
|
|
|
270
279
|
return ok(rt.updateStart())
|
|
271
280
|
case 'dsh-update-install':
|
|
272
281
|
return ok(rt.dshUpdateStart?.())
|
|
282
|
+
case 'repo-stars':
|
|
283
|
+
return ok(await rt.repoStars())
|
|
273
284
|
// The Desktop window's long poll for read_browser, and its answers.
|
|
274
285
|
case 'browser-wait': {
|
|
275
286
|
if (rt.browserChannel === undefined) return fail('Bot browsers are off on this host', 'bot/browser-off')
|
package/src/host/constants.js
CHANGED
|
@@ -26,6 +26,9 @@ export const ROOM_REPLY_TIMEOUT_MS = 240_000
|
|
|
26
26
|
export const SOURCE_KIND = 'bot'
|
|
27
27
|
// An answer from a question card, in the shape DSH's own late question replies use.
|
|
28
28
|
export const REPLY_KIND = 'user-question-reply'
|
|
29
|
+
// DSH's snapshot of a referenced conversation, logged right after the user's message
|
|
30
|
+
// that cites it.
|
|
31
|
+
export const REFERENCE_KIND = 'session-reference'
|
|
29
32
|
// `everyone`: every member answers a plain message in turn, admin first. `admin`: a
|
|
30
33
|
// plain message goes to the admin, who calls others with @mentions. New groups start
|
|
31
34
|
// in `admin` mode, because every extra speaker re-reads its whole context; a group
|
|
@@ -42,7 +45,7 @@ export const AGENT_SESSION_TITLE_MAX = 80
|
|
|
42
45
|
// Every tool this plugin registers for Bots. `read_browser` only exists in the
|
|
43
46
|
// Desktop profile, so a restriction filters this list down to registered names.
|
|
44
47
|
export const TEAM_TOOLS = [
|
|
45
|
-
'list_bots', 'message_bot', 'create_bot', 'update_bot', 'list_models',
|
|
48
|
+
'list_bots', 'message_bot', 'stop_bot', 'create_bot', 'update_bot', 'list_models',
|
|
46
49
|
'create_group', 'update_group', 'delete_group', 'post_to_group', 'ask_user',
|
|
47
50
|
'request_secret', 'list_secrets', 'group_relay', 'read_own_chat',
|
|
48
51
|
'read_group_chat', 'remember', 'forget', 'recall', 'read_browser',
|
package/src/host/delivery.js
CHANGED
|
@@ -12,8 +12,10 @@ export function install(rt) {
|
|
|
12
12
|
return resolved.agent
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const headerFor = (role, fromName, roomName, audience, note = 'new message') => {
|
|
15
|
+
const headerFor = (role, fromName, roomName, audience, note = 'new message', deferred = false) => {
|
|
16
|
+
if (deferred) return `[Message from ${fromName} · you read it during your last task; answer it now]`
|
|
16
17
|
if (role === 'reply') return `[Reply from ${fromName}]`
|
|
18
|
+
if (role === 'stopped') return `[Stopped by ${fromName}]`
|
|
17
19
|
if (role === 'room') return `[Group chat "${roomName}" · ${note}]`
|
|
18
20
|
if (role === 'report') return `[Group chat "${roomName}" · replies to your post]`
|
|
19
21
|
if (role === 'kickoff') return '[Bot team setup]'
|
|
@@ -22,8 +24,13 @@ export function install(rt) {
|
|
|
22
24
|
return `[Message from ${fromName}]`
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
// Roles recorded as team exchanges.
|
|
28
|
+
const EXCHANGE_ROLES = new Set(['request', 'reply', 'stopped'])
|
|
29
|
+
|
|
25
30
|
// `sessionId` defaults to the recipient's own chat; group turns name its group Session.
|
|
26
|
-
|
|
31
|
+
// `deferred`: a request the Bot already read mid-turn, handed back to answer in a
|
|
32
|
+
// turn of its own; its exchange was recorded when it first arrived.
|
|
33
|
+
async function deliver({ fromId, toId, sessionId, text, role, hop = 0, replyTo, roomId, audience, note, exchangeId = randomUUID(), steer = false, deferred = false }) {
|
|
27
34
|
const from = rt.botOf(fromId)
|
|
28
35
|
const to = rt.botOf(toId)
|
|
29
36
|
if (to === undefined) throw new Error(`Unknown Bot ${toId}`)
|
|
@@ -41,16 +48,21 @@ export function install(rt) {
|
|
|
41
48
|
...(replyTo ? { replyTo } : {}),
|
|
42
49
|
...(roomId ? { roomId } : {}),
|
|
43
50
|
...(audience === 'user' ? { audience } : {}),
|
|
51
|
+
...(deferred ? { deferred: true } : {}),
|
|
44
52
|
}
|
|
45
|
-
if (role
|
|
53
|
+
if (EXCHANGE_ROLES.has(role) && !deferred) {
|
|
46
54
|
state.exchanges.push({ id: exchangeId, from: fromId, to: toId, text, role, time: Date.now(), ...(replyTo ? { replyTo } : {}), ...(audience === 'user' ? { audience } : {}) })
|
|
47
|
-
void rt.save()
|
|
55
|
+
void rt.save().catch(() => {})
|
|
48
56
|
}
|
|
49
57
|
const agent = await resolveAgent(sessionId ?? rt.chatOf(toId))
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
// A steered message cuts into a running turn at its next step; a follow-up
|
|
59
|
+
// queues for the next turn (see turns.js for how each is routed).
|
|
60
|
+
const message = createUserMessage({
|
|
61
|
+
content: [{ type: 'text', text: `${headerFor(role, fromName, room?.name, audience, note, deferred)}\n${text}` }],
|
|
52
62
|
source,
|
|
53
|
-
})
|
|
63
|
+
})
|
|
64
|
+
if (steer) agent.steer(message)
|
|
65
|
+
else agent.followup(message)
|
|
54
66
|
await ctx.sessions.flush(agent.session)
|
|
55
67
|
return exchangeId
|
|
56
68
|
}
|
package/src/host/diagnostics.js
CHANGED
|
@@ -16,7 +16,7 @@ const LOG_MAX_BYTES = 1 << 20
|
|
|
16
16
|
const STACK_LINES = 8
|
|
17
17
|
const TEXT_MAX = 2000
|
|
18
18
|
export const REPORT_ENTRIES = 20
|
|
19
|
-
export const ISSUES_URL = 'https://github.com/
|
|
19
|
+
export const ISSUES_URL = 'https://github.com/Fei2411/DeepSeek-Bot/issues/new'
|
|
20
20
|
|
|
21
21
|
const quote = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
22
22
|
|
|
@@ -144,7 +144,7 @@ export function install(rt) {
|
|
|
144
144
|
}).catch((error) => {
|
|
145
145
|
if (fileFailed) return
|
|
146
146
|
fileFailed = true
|
|
147
|
-
console.error('[ds-bot] diagnostics log not written:', error?.code ?? error?.message ?? error)
|
|
147
|
+
console.error('[ds-bot] diagnostics log not written:', error?.code ?? clean(error?.message ?? String(error)))
|
|
148
148
|
})
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -164,13 +164,15 @@ export function install(rt) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
const warn = (template, ...args) => {
|
|
167
|
-
ctx.logger.warn(template, ...args)
|
|
168
|
-
console.error(`[ds-bot] ${template}`, ...args.map(arg => arg?.stack ?? arg))
|
|
169
167
|
const error = args.find(arg => arg instanceof Error)
|
|
168
|
+
const line = clean(format(template, ...args.map(arg => (arg instanceof Error ? errorText(arg) : arg))))
|
|
169
|
+
const frames = typeof error?.stack === 'string' ? error.stack.split('\n').filter(item => /^\s+at /.test(item)).slice(0, STACK_LINES).map(item => item.trim()) : []
|
|
170
|
+
ctx.logger.warn('%s', line)
|
|
171
|
+
console.error(`[ds-bot] ${line}${frames.length > 0 ? `\n${clean(frames.join('\n'))}` : ''}`)
|
|
170
172
|
note({
|
|
171
173
|
level: 'warn',
|
|
172
174
|
source: 'host',
|
|
173
|
-
text:
|
|
175
|
+
text: line.replace(/^ds-bot: /, ''),
|
|
174
176
|
code: typeof error?.code === 'string' ? error.code : undefined,
|
|
175
177
|
stack: error?.stack,
|
|
176
178
|
})
|
package/src/host/dsh-update.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
// DSH updates:
|
|
2
|
-
// against (`dsBot.verifiedDsh` in package.json)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// `npm i -g`; a packaged app's
|
|
6
|
-
//
|
|
1
|
+
// DSH updates: a released DS Bot may name the DSH release it was verified
|
|
2
|
+
// against (`dsBot.verifiedDsh` in package.json); a release that names none
|
|
3
|
+
// offers nothing. The newest released bot wins: offering that version and
|
|
4
|
+
// nothing newer keeps the update path away from DSH releases we have not
|
|
5
|
+
// checked. A DSH installed from npm swaps with `npm i -g`; a packaged app's
|
|
6
|
+
// runtime lives inside its bundle and only its own updater moves it, to the
|
|
7
|
+
// newest release it finds, so the client offers it only when that release is
|
|
8
|
+
// the verified one.
|
|
7
9
|
import { spawn } from 'node:child_process'
|
|
8
10
|
import { readFileSync } from 'node:fs'
|
|
9
11
|
import { dirname, join } from 'node:path'
|