ds-bot 0.1.2 → 0.1.4
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 +3 -3
- package/client.js +2164 -1102
- package/index.js +10 -1
- package/package.json +6 -4
- package/src/host/approvals.js +69 -0
- package/src/host/capabilities.js +521 -0
- package/src/host/channel.js +26 -4
- package/src/host/constants.js +12 -1
- package/src/host/delivery.js +34 -8
- package/src/host/diagnostics.js +7 -5
- package/src/host/groups.js +296 -59
- 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 +94 -21
- package/src/host/prompt.js +7 -3
- package/src/host/resume.js +247 -0
- package/src/host/roster.js +110 -56
- package/src/host/secrets.js +9 -1
- package/src/host/store.js +129 -31
- package/src/host/time-context.js +46 -0
- package/src/host/tools.js +182 -29
- package/src/host/turns.js +263 -39
- package/src/host/update.js +5 -1
- package/src/shared/mentions.js +65 -0
- package/web-search.js +23 -7
package/src/host/tools.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// Team tools: the roster, bot-to-bot messages, group chat settings and posts, and the
|
|
2
2
|
// question card. Group rounds and reading chats are in groups.js.
|
|
3
3
|
import { randomUUID } from 'node:crypto'
|
|
4
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
5
4
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
6
|
-
import { COLORS, EXTRA_COLORS, MAX_HOPS, ROOM_BOT_ROUNDS, ROOM_MODES
|
|
5
|
+
import { COLORS, EXTRA_COLORS, MAX_HOPS, ROOM_BOT_ROUNDS, ROOM_MODES } from './constants.js'
|
|
7
6
|
import { unquote } from './text.js'
|
|
8
7
|
|
|
9
8
|
export const output = { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] }
|
|
@@ -18,6 +17,10 @@ export function install(rt) {
|
|
|
18
17
|
const { ctx, state } = rt
|
|
19
18
|
|
|
20
19
|
const callerBot = (exec) => rt.botOf(rt.selfOf(exec.agent?.id))
|
|
20
|
+
const callerRoom = (exec) => {
|
|
21
|
+
const roomId = rt.groupOwners.get(exec.agent?.id)?.roomId
|
|
22
|
+
return roomId ? { fromRoomId: roomId } : {}
|
|
23
|
+
}
|
|
21
24
|
|
|
22
25
|
const roomLine = (room, self) => {
|
|
23
26
|
const members = room.members.map(rt.botOf).filter(Boolean)
|
|
@@ -58,9 +61,21 @@ export function install(rt) {
|
|
|
58
61
|
},
|
|
59
62
|
}))
|
|
60
63
|
|
|
64
|
+
// Sessions a Bot is running in right now: its own chat plus each group Session
|
|
65
|
+
// with a live turn. An idle Bot reports just its own chat.
|
|
66
|
+
const activeSessions = (botId) => {
|
|
67
|
+
const running = [...new Set([rt.chatOf(botId), ...rt.groupSessionsOf(botId)])]
|
|
68
|
+
.filter(sessionId => rt.live.get(sessionId)?.turn !== undefined)
|
|
69
|
+
return { running, idle: running.length === 0 }
|
|
70
|
+
}
|
|
71
|
+
const whereOf = (sessionId) => {
|
|
72
|
+
const room = rt.roomOf(rt.groupOwners.get(sessionId)?.roomId)
|
|
73
|
+
return room ? `group "${room.name}"` : 'its own chat'
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
ctx.tools.register(defineTool({
|
|
62
77
|
name: 'message_bot',
|
|
63
|
-
description: 'Send
|
|
78
|
+
description: 'Send a message to another Bot on the team. If that Bot is working, it reads your message at its next step and can change course, then answers once that work is done; if it is idle, it starts on it right away. Its reply arrives later as a new message to you; with reply_to "user" it answers the user in its own chat instead.',
|
|
64
79
|
parameters: {
|
|
65
80
|
to: { type: 'string', required: true, description: 'Exact Bot name from list_bots' },
|
|
66
81
|
message: { type: 'string', required: true, description: 'Self-contained message; the other Bot cannot see this conversation' },
|
|
@@ -69,6 +84,11 @@ export function install(rt) {
|
|
|
69
84
|
enum: ['me', 'user'],
|
|
70
85
|
description: '"me" (default): the reply comes back to you and the user does not see it. "user": the Bot answers the user in its own chat, where the user reads it, and nothing comes back to you. Use "user" whenever the user wants to hear from that Bot directly or asks Bots to say something in their own chats.',
|
|
71
86
|
},
|
|
87
|
+
when: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
enum: ['now', 'queue'],
|
|
90
|
+
description: '"now" (default): a working Bot reads your message at its next step, between its current actions, and answers once its current work is done; use it for questions, information it needs, corrections and warnings. "queue": it reads your message only after its current work is done, as a separate task; use it only for a new task unrelated to what it is doing now. With reply_to "user" the message always queues.',
|
|
91
|
+
},
|
|
72
92
|
},
|
|
73
93
|
output,
|
|
74
94
|
async execute(args, exec) {
|
|
@@ -78,13 +98,56 @@ export function install(rt) {
|
|
|
78
98
|
const target = rt.findBot(args.to)
|
|
79
99
|
if (target === undefined) return `No Bot named ${args.to}. Team: ${Object.values(state.bots).map(bot => bot.name).join(', ')}`
|
|
80
100
|
if (target.id === sender.id) return 'You cannot message yourself.'
|
|
81
|
-
const
|
|
101
|
+
const turn = rt.live.get(exec.agent?.id)
|
|
102
|
+
const hop = Math.max(turn?.info?.hop ?? 0, ...(turn?.joined ?? []).map(joined => joined.info?.hop ?? 0))
|
|
82
103
|
if (hop >= MAX_HOPS) return 'This chain of Bot messages is already long. Stop here and report to the user instead.'
|
|
83
104
|
const toUser = args.reply_to === 'user'
|
|
84
|
-
|
|
85
|
-
|
|
105
|
+
const working = rt.live.get(rt.chatOf(target.id))?.turn !== undefined
|
|
106
|
+
const steer = args.when !== 'queue' && !toUser
|
|
107
|
+
await rt.deliver({ fromId: sender.id, toId: target.id, text: args.message, role: 'request', hop, steer, ...(toUser ? { audience: 'user' } : callerRoom(exec)) })
|
|
108
|
+
const sent = toUser
|
|
86
109
|
? `Delivered to ${target.name}. It will answer the user in its own chat; nothing comes back to you, so point the user to ${target.name}'s chat instead of relaying.`
|
|
87
|
-
:
|
|
110
|
+
: working && steer
|
|
111
|
+
? `Delivered to ${target.name} while it is working: it reads your message at its next step and answers once that work is done. Its reply will arrive later as a new message; do not wait for it.`
|
|
112
|
+
: working
|
|
113
|
+
? `Queued for ${target.name}: it reads your message after its current work. Its reply will arrive later as a new message; do not wait for it.`
|
|
114
|
+
: `Delivered to ${target.name}. Its reply will arrive later as a new message; do not wait for it.`
|
|
115
|
+
// Messages reach a Bot's own chat only; work in a group goes on without them.
|
|
116
|
+
const groups = activeSessions(target.id).running.filter(sessionId => sessionId !== rt.chatOf(target.id))
|
|
117
|
+
if (groups.length === 0) return sent
|
|
118
|
+
const stop = rt.isMain(sender.id) ? ' If it must not go on there, use stop_bot.' : ''
|
|
119
|
+
return `${sent} ${target.name} is also working in ${groups.map(whereOf).join(' and ')}; your message does not reach that work.${stop}`
|
|
120
|
+
},
|
|
121
|
+
}))
|
|
122
|
+
|
|
123
|
+
// stop_bot throws when it does nothing, so the chat shows the call as failed
|
|
124
|
+
// instead of "Stopped".
|
|
125
|
+
const refuse = (message) => { throw new Error(message) }
|
|
126
|
+
|
|
127
|
+
ctx.tools.register(defineTool({
|
|
128
|
+
name: 'stop_bot',
|
|
129
|
+
description: 'Main Bot only. Stop another Bot\'s running turn immediately — when it must not finish a dangerous action and a message_bot warning is not enough. Its queued messages are dropped with the turn. It is then told why in a new turn, whose reply comes back to you.',
|
|
130
|
+
parameters: {
|
|
131
|
+
bot: { type: 'string', required: true, description: 'Exact Bot name from list_bots' },
|
|
132
|
+
reason: { type: 'string', description: 'Why it was stopped; it reads this afterwards' },
|
|
133
|
+
},
|
|
134
|
+
output,
|
|
135
|
+
async execute(args, exec) {
|
|
136
|
+
await rt.load()
|
|
137
|
+
const sender = callerBot(exec)
|
|
138
|
+
if (sender === undefined || !rt.isMain(sender.id)) refuse('Only a Main Bot can stop other Bots.')
|
|
139
|
+
const target = rt.findBot(args.bot)
|
|
140
|
+
if (target === undefined) refuse(`No Bot named ${args.bot}. Team: ${Object.values(state.bots).map(bot => bot.name).join(', ')}`)
|
|
141
|
+
if (target.id === sender.id) refuse('You cannot stop yourself.')
|
|
142
|
+
const { running } = activeSessions(target.id)
|
|
143
|
+
if (running.length === 0) refuse(`${target.name} is not running right now, so there is nothing to stop. Use message_bot to reach it.`)
|
|
144
|
+
for (const sessionId of running) {
|
|
145
|
+
const agent = await rt.resolveAgent(sessionId)
|
|
146
|
+
agent.cancel({ kind: 'hook', reason: 'ds-bot/stopped' })
|
|
147
|
+
}
|
|
148
|
+
const why = String(args.reason ?? '').trim() || 'Stop what you are doing; the previous turn was ended on purpose. Acknowledge briefly.'
|
|
149
|
+
await rt.deliver({ fromId: sender.id, toId: target.id, text: why, role: 'stopped', ...callerRoom(exec) })
|
|
150
|
+
return `Stopped ${target.name} in ${running.map(whereOf).join(' and ')}. It is told why in a new turn, whose reply comes back to you.`
|
|
88
151
|
},
|
|
89
152
|
}))
|
|
90
153
|
|
|
@@ -113,9 +176,9 @@ export function install(rt) {
|
|
|
113
176
|
|
|
114
177
|
ctx.tools.register(defineTool({
|
|
115
178
|
name: 'update_bot',
|
|
116
|
-
description: '
|
|
179
|
+
description: 'Change a Bot\'s name, role, color, model, or instructions. Every Bot may change itself; only a Main Bot may change another Bot.',
|
|
117
180
|
parameters: {
|
|
118
|
-
bot: { type: 'string',
|
|
181
|
+
bot: { type: 'string', description: 'Current name of the Bot to change; leave out to change yourself' },
|
|
119
182
|
name: { type: 'string', description: 'New name' },
|
|
120
183
|
role: { type: 'string', description: 'New role label' },
|
|
121
184
|
instructions: { type: 'string', description: 'Replacement instructions' },
|
|
@@ -125,9 +188,12 @@ export function install(rt) {
|
|
|
125
188
|
output,
|
|
126
189
|
async execute(args, exec) {
|
|
127
190
|
await rt.load()
|
|
128
|
-
|
|
129
|
-
const
|
|
191
|
+
const self = rt.selfOf(exec.agent?.id)
|
|
192
|
+
const caller = rt.botOf(self)
|
|
193
|
+
if (caller === undefined) return 'Only Bots on the team can use update_bot.'
|
|
194
|
+
const bot = args.bot ? rt.findBot(args.bot) : caller
|
|
130
195
|
if (bot === undefined) return `No Bot named ${args.bot}.`
|
|
196
|
+
if (bot.id !== caller.id && !rt.isMain(self)) return 'Only a Main Bot can change another Bot. Leave out bot to change yourself, or ask a Main Bot with message_bot.'
|
|
131
197
|
try {
|
|
132
198
|
const { name: newName, role, instructions, color, model } = args
|
|
133
199
|
const next = await rt.updateBot(bot.id, { name: newName, role, instructions, color, ...(model ? { model } : {}) })
|
|
@@ -272,7 +338,7 @@ export function install(rt) {
|
|
|
272
338
|
description: 'Post a message into a group chat as yourself. It starts a round: members reply in the group, where the user reads them. Main Bots may post into any group; an admin into its own group.',
|
|
273
339
|
parameters: {
|
|
274
340
|
group: GROUP_REF,
|
|
275
|
-
message: { type: 'string', required: true, description: 'What to say; write "@Name" to choose who answers' },
|
|
341
|
+
message: { type: 'string', required: true, description: 'What to say; write "@Name" to choose who answers, and a name without "@" to only mention a member' },
|
|
276
342
|
report_back: { type: 'boolean', description: 'Also send the members\' replies back to you as a new message when the round ends' },
|
|
277
343
|
},
|
|
278
344
|
output,
|
|
@@ -294,22 +360,7 @@ export function install(rt) {
|
|
|
294
360
|
await rt.save()
|
|
295
361
|
const reportBack = args.report_back === true
|
|
296
362
|
try {
|
|
297
|
-
|
|
298
|
-
agent.followup(createUserMessage({
|
|
299
|
-
content: [{ type: 'text', text: `[Group post from ${sender.name}]\n${text}` }],
|
|
300
|
-
source: {
|
|
301
|
-
kind: SOURCE_KIND,
|
|
302
|
-
form: 'relay',
|
|
303
|
-
role: 'post',
|
|
304
|
-
exchangeId: randomUUID(),
|
|
305
|
-
senderName: sender.name,
|
|
306
|
-
senderSessionId: sender.id,
|
|
307
|
-
roomId: room.id,
|
|
308
|
-
hop: info?.hop ?? 0,
|
|
309
|
-
...(reportBack ? { reportBack: true } : {}),
|
|
310
|
-
},
|
|
311
|
-
}))
|
|
312
|
-
await ctx.sessions.flush(agent.session)
|
|
363
|
+
await rt.postToRoom(room, sender, text, { hop: info?.hop ?? 0, ...(reportBack ? { reportBack: true } : {}) })
|
|
313
364
|
} catch (failure) {
|
|
314
365
|
return `Could not post in "${room.name}": ${failure.message}`
|
|
315
366
|
}
|
|
@@ -354,9 +405,13 @@ export function install(rt) {
|
|
|
354
405
|
.filter(option => option.label !== '')
|
|
355
406
|
.slice(0, 6)
|
|
356
407
|
if (options.length === 0) return 'Give at least one option.'
|
|
408
|
+
// A hidden Bot-to-Bot turn shows none of its calls, so its card goes to the
|
|
409
|
+
// composer dock, which takes questions without a call.
|
|
410
|
+
const turn = rt.live.get(id)?.turn
|
|
411
|
+
const hidden = turn !== undefined && (state.exchangeTurns[id] ?? []).includes(turn)
|
|
357
412
|
state.questions[id] = {
|
|
358
413
|
id: randomUUID(),
|
|
359
|
-
...(exec.callId ? { callId: exec.callId } : {}),
|
|
414
|
+
...(exec.callId && !hidden ? { callId: exec.callId } : {}),
|
|
360
415
|
question: String(args.question ?? '').trim(),
|
|
361
416
|
...(args.detail ? { detail: String(args.detail).trim() } : {}),
|
|
362
417
|
options,
|
|
@@ -370,5 +425,103 @@ export function install(rt) {
|
|
|
370
425
|
},
|
|
371
426
|
}))
|
|
372
427
|
|
|
428
|
+
ctx.tools.register(defineTool({
|
|
429
|
+
name: 'list_capabilities',
|
|
430
|
+
description: 'List shared MCP servers, skills, and packs, plus the capabilities attached to you.',
|
|
431
|
+
parameters: {},
|
|
432
|
+
output,
|
|
433
|
+
async execute(_args, exec) {
|
|
434
|
+
await rt.load()
|
|
435
|
+
const self = rt.selfOf(exec.agent?.id)
|
|
436
|
+
const bot = rt.botOf(self)
|
|
437
|
+
if (!bot) return 'Only Bots on the team can use list_capabilities.'
|
|
438
|
+
const items = Object.values(state.capabilities?.items ?? {})
|
|
439
|
+
if (items.length === 0) return 'No shared capabilities are saved yet.'
|
|
440
|
+
const statuses = rt.capabilityStatusesView()[bot.id] ?? {}
|
|
441
|
+
const lines = items.map(item => {
|
|
442
|
+
const extra = item.kind === 'pack' ? `; includes ${(item.items ?? []).join(', ') || 'nothing'}`
|
|
443
|
+
: item.kind === 'mcp' && item.transport === 'stdio' ? `; ${item.approved ? 'allowed' : 'needs user approval'}` : ''
|
|
444
|
+
const itemStatus = statuses[item.name]
|
|
445
|
+
const state = itemStatus ? ` (${itemStatus.state}${itemStatus.error ? `: ${itemStatus.error}` : ''})` : ''
|
|
446
|
+
const status = bot.capabilities?.includes(item.name) ? `; attached to you${state}` : ''
|
|
447
|
+
return `- ${item.name} (${item.kind})${item.description ? ` — ${item.description}` : ''}${extra}${status}`
|
|
448
|
+
})
|
|
449
|
+
return ['Shared capabilities:', ...lines].join('\n')
|
|
450
|
+
},
|
|
451
|
+
}))
|
|
452
|
+
ctx.tools.register(defineTool({
|
|
453
|
+
name: 'save_capability',
|
|
454
|
+
description: 'Create or update a shared MCP server, skill, or pack. Changes apply from your next message.',
|
|
455
|
+
parameters: {
|
|
456
|
+
name: { type: 'string', required: true, description: 'Lowercase letters, digits, and single hyphens, up to 32 characters; "github" is reserved' },
|
|
457
|
+
kind: { type: 'string', required: true, enum: ['mcp', 'skill', 'pack'] },
|
|
458
|
+
description: { type: 'string', description: 'What this capability provides' },
|
|
459
|
+
transport: { type: 'string', enum: ['stdio', 'streamable-http'], description: 'How to reach an MCP server: a local command or an HTTP URL' },
|
|
460
|
+
command: { type: 'string', description: 'Program that starts a local MCP server' },
|
|
461
|
+
args: { type: 'array', items: { type: 'string' }, description: 'Arguments for command' },
|
|
462
|
+
env: { type: 'object', additionalProperties: true, description: 'Environment variables for command; write $DSH_SECRET_<NAME> to insert a saved secret instead of the value' },
|
|
463
|
+
url: { type: 'string', description: 'MCP server URL' },
|
|
464
|
+
headers: { type: 'object', additionalProperties: true, description: 'HTTP headers; write $DSH_SECRET_<NAME> to insert a saved secret instead of the value' },
|
|
465
|
+
content: { type: 'string', description: 'Skill instructions' },
|
|
466
|
+
items: { type: 'array', items: { type: 'string' }, description: 'MCP server and skill names in the pack' },
|
|
467
|
+
},
|
|
468
|
+
output,
|
|
469
|
+
async execute(args, exec) {
|
|
470
|
+
await rt.load()
|
|
471
|
+
const self = rt.selfOf(exec.agent?.id)
|
|
472
|
+
if (!rt.botOf(self)) return 'Only Bots on the team can use save_capability.'
|
|
473
|
+
try {
|
|
474
|
+
const item = await rt.capabilitySave(args, { botId: self })
|
|
475
|
+
const approval = item.kind === 'mcp' && item.transport === 'stdio' && !item.approved
|
|
476
|
+
? ' The user must allow this MCP server in DS Bot settings before it can work.'
|
|
477
|
+
: ''
|
|
478
|
+
return `Saved ${item.name}. Bots with it attached use the change from their next message.${approval}`
|
|
479
|
+
} catch (error) { return error.message }
|
|
480
|
+
},
|
|
481
|
+
}))
|
|
482
|
+
ctx.tools.register(defineTool({
|
|
483
|
+
name: 'delete_capability',
|
|
484
|
+
description: 'Remove a shared capability and detach it from every Bot and pack.',
|
|
485
|
+
parameters: { name: { type: 'string', required: true } },
|
|
486
|
+
output,
|
|
487
|
+
async execute(args, exec) {
|
|
488
|
+
await rt.load()
|
|
489
|
+
const self = rt.selfOf(exec.agent?.id)
|
|
490
|
+
if (!rt.botOf(self)) return 'Only Bots on the team can use delete_capability.'
|
|
491
|
+
try {
|
|
492
|
+
await rt.capabilityDelete(args.name, { botId: self })
|
|
493
|
+
return `Deleted ${args.name}; it was removed from every Bot and pack. The change applies from their next message.`
|
|
494
|
+
} catch (error) { return error.message }
|
|
495
|
+
},
|
|
496
|
+
}))
|
|
497
|
+
for (const [name, endpoint, verb] of [['attach_capability', 'attach', 'Attached'], ['detach_capability', 'detach', 'Detached']]) {
|
|
498
|
+
ctx.tools.register(defineTool({
|
|
499
|
+
name,
|
|
500
|
+
description: `${verb} a shared capability to yourself, or to another Bot if you are a Main Bot.`,
|
|
501
|
+
parameters: {
|
|
502
|
+
name: { type: 'string', required: true, description: 'Capability name from list_capabilities' },
|
|
503
|
+
bot: { type: 'string', description: 'Optional Bot name; only a Main Bot may change another Bot' },
|
|
504
|
+
},
|
|
505
|
+
output,
|
|
506
|
+
async execute(args, exec) {
|
|
507
|
+
await rt.load()
|
|
508
|
+
const self = rt.selfOf(exec.agent?.id)
|
|
509
|
+
if (!rt.botOf(self)) return `Only Bots on the team can use ${name}.`
|
|
510
|
+
try {
|
|
511
|
+
const result = await rt.changeCapabilityAttachment(endpoint, args, self)
|
|
512
|
+
const attached = state.capabilities.items[args.name]
|
|
513
|
+
const members = attached?.kind === 'pack'
|
|
514
|
+
? (attached.items ?? []).map(child => state.capabilities.items[child]).filter(Boolean)
|
|
515
|
+
: attached ? [attached] : []
|
|
516
|
+
const approvals = members
|
|
517
|
+
.filter(item => item.kind === 'mcp' && item.transport === 'stdio' && item.approved !== true)
|
|
518
|
+
.map(item => ` The user must allow ${item.name} in DS Bot settings before it can work.`)
|
|
519
|
+
.join('')
|
|
520
|
+
return `${verb} ${args.name} ${args.bot ? `to ${result.bot}` : 'to yourself'}. Changes apply from ${result.bot}'s next message.${endpoint === 'attach' ? approvals : ''}`
|
|
521
|
+
} catch (error) { return error.message }
|
|
522
|
+
},
|
|
523
|
+
}))
|
|
524
|
+
}
|
|
525
|
+
|
|
373
526
|
Object.assign(rt, { pickRoom })
|
|
374
527
|
}
|