@thewaifu/dsh-bot 1.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 +304 -0
- package/bin/dsh-bot-web.js +33 -0
- package/client.js +13691 -0
- package/cordis.patch.yml +17 -0
- package/index.js +57 -0
- package/package.json +80 -0
- package/src/host/agents.js +186 -0
- package/src/host/approvals.js +63 -0
- package/src/host/browser.js +258 -0
- package/src/host/channel.js +342 -0
- package/src/host/chat-index.js +293 -0
- package/src/host/connectors.js +169 -0
- package/src/host/constants.js +65 -0
- package/src/host/delivery.js +71 -0
- package/src/host/diagnostics.js +249 -0
- package/src/host/dsh-compat.js +22 -0
- package/src/host/dsh-update.js +170 -0
- package/src/host/groups.js +592 -0
- package/src/host/home-lock.js +97 -0
- package/src/host/images.js +211 -0
- package/src/host/memory.js +1362 -0
- package/src/host/models.js +231 -0
- package/src/host/parts.js +976 -0
- package/src/host/permissions.js +48 -0
- package/src/host/prompt.js +71 -0
- package/src/host/roster.js +471 -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 +344 -0
- package/src/host/text.js +47 -0
- package/src/host/time-context.js +46 -0
- package/src/host/tools.js +425 -0
- package/src/host/turns.js +388 -0
- package/src/host/update.js +237 -0
- package/src/host/usage.js +591 -0
- package/src/host/waifu.js +58 -0
- package/src/shared/mentions.js +65 -0
- package/src/shared/web-port.js +26 -0
- package/web-search.js +764 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// Team tools: the roster, bot-to-bot messages, group chat settings and posts, and the
|
|
2
|
+
// question card. Group rounds and reading chats are in groups.js.
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
5
|
+
import { COLORS, EXTRA_COLORS, MAX_HOPS, ROOM_BOT_ROUNDS, ROOM_MODES } from './constants.js'
|
|
6
|
+
import { unquote } from './text.js'
|
|
7
|
+
|
|
8
|
+
export const output = { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] }
|
|
9
|
+
export const GROUP_REF = { type: 'string', required: true, description: 'Exact group chat name from list_bots' }
|
|
10
|
+
const MODE_PARAM = {
|
|
11
|
+
type: 'string',
|
|
12
|
+
enum: ROOM_MODES,
|
|
13
|
+
description: '"everyone": every member answers a plain message in turn, admin first. "admin" (the default): a plain message goes to the admin, who calls others with @Name. @mentions always choose who answers.',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function install(rt) {
|
|
17
|
+
const { ctx, state } = rt
|
|
18
|
+
|
|
19
|
+
const callerBot = (exec) => rt.botOf(rt.selfOf(exec.agent?.id))
|
|
20
|
+
|
|
21
|
+
const roomLine = (room, self) => {
|
|
22
|
+
const members = room.members.map(rt.botOf).filter(Boolean)
|
|
23
|
+
.map(bot => `${bot.name}${bot.id === room.admin ? ' (admin)' : ''}${bot.id === self ? ' (you)' : ''}`)
|
|
24
|
+
const parts = [
|
|
25
|
+
`members: ${members.join(', ')}`,
|
|
26
|
+
room.mode === 'admin' && room.admin ? 'plain messages go to the admin first' : 'every member replies in turn',
|
|
27
|
+
room.notice ? `notice: ${room.notice.split('\n')[0].slice(0, 100)}` : '',
|
|
28
|
+
].filter(Boolean)
|
|
29
|
+
return `- "${room.name}" — ${parts.join('; ')}`
|
|
30
|
+
}
|
|
31
|
+
// Main Bots see every group; other Bots see the groups they are in.
|
|
32
|
+
const visibleRooms = self => Object.values(state.rooms).filter(room => rt.isMain(self) || room.members.includes(self))
|
|
33
|
+
|
|
34
|
+
ctx.tools.register(defineTool({
|
|
35
|
+
name: 'list_bots',
|
|
36
|
+
description: 'List every Bot on the user\'s team with its role and what it does, and the group chats you can see.',
|
|
37
|
+
parameters: {},
|
|
38
|
+
output,
|
|
39
|
+
async execute(_args, exec) {
|
|
40
|
+
await rt.load()
|
|
41
|
+
const self = rt.selfOf(exec.agent?.id)
|
|
42
|
+
const lines = Object.values(state.bots).map(bot => {
|
|
43
|
+
const model = bot.appliedModel ?? bot.model
|
|
44
|
+
const tags = [bot.role, model ? `model ${model}` : '', rt.isMain(bot.id) ? 'Main Bot' : '', bot.id === self ? 'you' : ''].filter(Boolean).join(', ')
|
|
45
|
+
const job = bot.instructions ? ` — ${bot.instructions.split('\n')[0].slice(0, 120)}` : ''
|
|
46
|
+
return `- ${bot.name}${tags ? ` (${tags})` : ''}${job}`
|
|
47
|
+
})
|
|
48
|
+
if (lines.length === 0) return 'No Bots yet.'
|
|
49
|
+
const rooms = visibleRooms(self)
|
|
50
|
+
return [
|
|
51
|
+
'Bots:',
|
|
52
|
+
...lines,
|
|
53
|
+
'',
|
|
54
|
+
rooms.length === 0 ? 'Group chats: none you can see.' : 'Group chats:',
|
|
55
|
+
...rooms.map(room => roomLine(room, self)),
|
|
56
|
+
].join('\n')
|
|
57
|
+
},
|
|
58
|
+
}))
|
|
59
|
+
|
|
60
|
+
// Sessions a Bot is running in right now: its own chat plus each group Session
|
|
61
|
+
// with a live turn. An idle Bot reports just its own chat.
|
|
62
|
+
const activeSessions = (botId) => {
|
|
63
|
+
const running = [...new Set([rt.chatOf(botId), ...rt.groupSessionsOf(botId)])]
|
|
64
|
+
.filter(sessionId => rt.live.get(sessionId)?.turn !== undefined)
|
|
65
|
+
return { running, idle: running.length === 0 }
|
|
66
|
+
}
|
|
67
|
+
const whereOf = (sessionId) => {
|
|
68
|
+
const room = rt.roomOf(rt.groupOwners.get(sessionId)?.roomId)
|
|
69
|
+
return room ? `group "${room.name}"` : 'its own chat'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ctx.tools.register(defineTool({
|
|
73
|
+
name: 'message_bot',
|
|
74
|
+
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.',
|
|
75
|
+
parameters: {
|
|
76
|
+
to: { type: 'string', required: true, description: 'Exact Bot name from list_bots' },
|
|
77
|
+
message: { type: 'string', required: true, description: 'Self-contained message; the other Bot cannot see this conversation' },
|
|
78
|
+
reply_to: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
enum: ['me', 'user'],
|
|
81
|
+
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.',
|
|
82
|
+
},
|
|
83
|
+
when: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
enum: ['now', 'queue'],
|
|
86
|
+
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.',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
output,
|
|
90
|
+
async execute(args, exec) {
|
|
91
|
+
await rt.load()
|
|
92
|
+
const sender = callerBot(exec)
|
|
93
|
+
if (sender === undefined) return 'Only Bots on the team can use message_bot.'
|
|
94
|
+
const target = rt.findBot(args.to)
|
|
95
|
+
if (target === undefined) return `No Bot named ${args.to}. Team: ${Object.values(state.bots).map(bot => bot.name).join(', ')}`
|
|
96
|
+
if (target.id === sender.id) return 'You cannot message yourself.'
|
|
97
|
+
const turn = rt.live.get(exec.agent?.id)
|
|
98
|
+
const hop = Math.max(turn?.info?.hop ?? 0, ...(turn?.joined ?? []).map(joined => joined.info?.hop ?? 0))
|
|
99
|
+
if (hop >= MAX_HOPS) return 'This chain of Bot messages is already long. Stop here and report to the user instead.'
|
|
100
|
+
const toUser = args.reply_to === 'user'
|
|
101
|
+
const working = rt.live.get(rt.chatOf(target.id))?.turn !== undefined
|
|
102
|
+
const steer = args.when !== 'queue' && !toUser
|
|
103
|
+
await rt.deliver({ fromId: sender.id, toId: target.id, text: args.message, role: 'request', hop, steer, ...(toUser ? { audience: 'user' } : {}) })
|
|
104
|
+
const sent = toUser
|
|
105
|
+
? `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.`
|
|
106
|
+
: working && steer
|
|
107
|
+
? `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.`
|
|
108
|
+
: working
|
|
109
|
+
? `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.`
|
|
110
|
+
: `Delivered to ${target.name}. Its reply will arrive later as a new message; do not wait for it.`
|
|
111
|
+
// Messages reach a Bot's own chat only; work in a group goes on without them.
|
|
112
|
+
const groups = activeSessions(target.id).running.filter(sessionId => sessionId !== rt.chatOf(target.id))
|
|
113
|
+
if (groups.length === 0) return sent
|
|
114
|
+
const stop = rt.isMain(sender.id) ? ' If it must not go on there, use stop_bot.' : ''
|
|
115
|
+
return `${sent} ${target.name} is also working in ${groups.map(whereOf).join(' and ')}; your message does not reach that work.${stop}`
|
|
116
|
+
},
|
|
117
|
+
}))
|
|
118
|
+
|
|
119
|
+
// stop_bot throws when it does nothing, so the chat shows the call as failed
|
|
120
|
+
// instead of "Stopped".
|
|
121
|
+
const refuse = (message) => { throw new Error(message) }
|
|
122
|
+
|
|
123
|
+
ctx.tools.register(defineTool({
|
|
124
|
+
name: 'stop_bot',
|
|
125
|
+
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.',
|
|
126
|
+
parameters: {
|
|
127
|
+
bot: { type: 'string', required: true, description: 'Exact Bot name from list_bots' },
|
|
128
|
+
reason: { type: 'string', description: 'Why it was stopped; it reads this afterwards' },
|
|
129
|
+
},
|
|
130
|
+
output,
|
|
131
|
+
async execute(args, exec) {
|
|
132
|
+
await rt.load()
|
|
133
|
+
const sender = callerBot(exec)
|
|
134
|
+
if (sender === undefined || !rt.isMain(sender.id)) refuse('Only a Main Bot can stop other Bots.')
|
|
135
|
+
const target = rt.findBot(args.bot)
|
|
136
|
+
if (target === undefined) refuse(`No Bot named ${args.bot}. Team: ${Object.values(state.bots).map(bot => bot.name).join(', ')}`)
|
|
137
|
+
if (target.id === sender.id) refuse('You cannot stop yourself.')
|
|
138
|
+
const { running } = activeSessions(target.id)
|
|
139
|
+
if (running.length === 0) refuse(`${target.name} is not running right now, so there is nothing to stop. Use message_bot to reach it.`)
|
|
140
|
+
for (const sessionId of running) {
|
|
141
|
+
const agent = await rt.resolveAgent(sessionId)
|
|
142
|
+
agent.cancel({ kind: 'hook', reason: 'ds-bot/stopped' })
|
|
143
|
+
}
|
|
144
|
+
const why = String(args.reason ?? '').trim() || 'Stop what you are doing; the previous turn was ended on purpose. Acknowledge briefly.'
|
|
145
|
+
await rt.deliver({ fromId: sender.id, toId: target.id, text: why, role: 'stopped' })
|
|
146
|
+
return `Stopped ${target.name} in ${running.map(whereOf).join(' and ')}. It is told why in a new turn, whose reply comes back to you.`
|
|
147
|
+
},
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
ctx.tools.register(defineTool({
|
|
151
|
+
name: 'create_bot',
|
|
152
|
+
description: 'Main Bot only. Create a new Bot on the user\'s team. It appears in the sidebar and greets the user.',
|
|
153
|
+
parameters: {
|
|
154
|
+
name: { type: 'string', required: true, description: 'Short display name, e.g. "Researcher" or "Editor"' },
|
|
155
|
+
role: { type: 'string', required: true, description: 'Two-to-four character role label shown under the name' },
|
|
156
|
+
instructions: { type: 'string', required: true, description: 'What this Bot is responsible for and how it should work' },
|
|
157
|
+
brief: { type: 'string', description: 'Why the user wants it, passed to the new Bot as its first context' },
|
|
158
|
+
model: { type: 'string', description: 'Model the Bot runs on, as provider/model or a model id. Only when the user names one; otherwise the DSH default model.' },
|
|
159
|
+
},
|
|
160
|
+
output,
|
|
161
|
+
async execute(args, exec) {
|
|
162
|
+
await rt.load()
|
|
163
|
+
if (!rt.isMain(rt.selfOf(exec.agent?.id))) return 'Only the Main Bot can create Bots. Ask the Main Bot instead.'
|
|
164
|
+
try {
|
|
165
|
+
const bot = await rt.createBot({ ...args, createdBy: rt.selfOf(exec.agent.id) })
|
|
166
|
+
return `Created ${bot.name}. It is greeting the user now.`
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return `Could not create the Bot: ${error.message}`
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
}))
|
|
172
|
+
|
|
173
|
+
ctx.tools.register(defineTool({
|
|
174
|
+
name: 'update_bot',
|
|
175
|
+
description: 'Change a Bot\'s name, role, color, model, or instructions. Every Bot may change itself; only a Main Bot may change another Bot.',
|
|
176
|
+
parameters: {
|
|
177
|
+
bot: { type: 'string', description: 'Current name of the Bot to change; leave out to change yourself' },
|
|
178
|
+
name: { type: 'string', description: 'New name' },
|
|
179
|
+
role: { type: 'string', description: 'New role label' },
|
|
180
|
+
instructions: { type: 'string', description: 'Replacement instructions' },
|
|
181
|
+
color: { type: 'string', description: `One of ${[...COLORS, ...EXTRA_COLORS].join(', ')}` },
|
|
182
|
+
model: { type: 'string', description: 'Move the Bot to another model, as provider/model or a model id' },
|
|
183
|
+
},
|
|
184
|
+
output,
|
|
185
|
+
async execute(args, exec) {
|
|
186
|
+
await rt.load()
|
|
187
|
+
const self = rt.selfOf(exec.agent?.id)
|
|
188
|
+
const caller = rt.botOf(self)
|
|
189
|
+
if (caller === undefined) return 'Only Bots on the team can use update_bot.'
|
|
190
|
+
const bot = args.bot ? rt.findBot(args.bot) : caller
|
|
191
|
+
if (bot === undefined) return `No Bot named ${args.bot}.`
|
|
192
|
+
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.'
|
|
193
|
+
try {
|
|
194
|
+
const { name: newName, role, instructions, color, model } = args
|
|
195
|
+
const next = await rt.updateBot(bot.id, { name: newName, role, instructions, color, ...(model ? { model } : {}) })
|
|
196
|
+
return `Updated ${next.name}.`
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return `Could not update the Bot: ${error.message}`
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
}))
|
|
202
|
+
|
|
203
|
+
ctx.tools.register(defineTool({
|
|
204
|
+
name: 'list_models',
|
|
205
|
+
description: 'List the models a Bot can run on, as provider/model refs for create_bot and update_bot.',
|
|
206
|
+
parameters: {},
|
|
207
|
+
output,
|
|
208
|
+
async execute() {
|
|
209
|
+
const models = await rt.catalog()
|
|
210
|
+
if (models.length === 0) return 'No models are configured. Ask the user to add one in Settings → Models.'
|
|
211
|
+
const fallback = rt.defaultRef()
|
|
212
|
+
return models.map(entry => `- ${entry.ref}${entry.name !== entry.id ? ` (${entry.name})` : ''}${entry.input?.includes('image') ? ', reads images' : ''}${entry.ref === fallback ? ', default' : ''}`).join('\n')
|
|
213
|
+
},
|
|
214
|
+
}))
|
|
215
|
+
|
|
216
|
+
// Group tools take Bot names as an array, or as one comma-separated string from
|
|
217
|
+
// models that flatten it.
|
|
218
|
+
const nameList = value => (Array.isArray(value) ? value : typeof value === 'string' ? value.split(/[,,、]/) : [])
|
|
219
|
+
.map(item => String(item ?? '').trim()).filter(Boolean)
|
|
220
|
+
const resolveBots = (refs) => {
|
|
221
|
+
const bots = []
|
|
222
|
+
const unknown = []
|
|
223
|
+
for (const ref of nameList(refs)) {
|
|
224
|
+
const bot = rt.findBot(ref)
|
|
225
|
+
if (bot === undefined) unknown.push(ref)
|
|
226
|
+
else if (!bots.includes(bot)) bots.push(bot)
|
|
227
|
+
}
|
|
228
|
+
return { bots, unknown }
|
|
229
|
+
}
|
|
230
|
+
const teamNames = () => Object.values(state.bots).map(bot => bot.name).join(', ')
|
|
231
|
+
const groupNames = self => visibleRooms(self).map(room => `"${room.name}"`).join(', ') || 'none'
|
|
232
|
+
const pickRoom = (ref, self) => {
|
|
233
|
+
const matches = rt.findRooms(ref).filter(room => visibleRooms(self).includes(room))
|
|
234
|
+
if (matches.length === 1) return { room: matches[0] }
|
|
235
|
+
if (matches.length > 1) return { error: `Several group chats are called "${unquote(ref)}". Rename one first.` }
|
|
236
|
+
return { error: `No group chat called "${unquote(ref)}" that you can see. Groups: ${groupNames(self)}.` }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
ctx.tools.register(defineTool({
|
|
240
|
+
name: 'create_group',
|
|
241
|
+
description: 'Main Bot only. Create a group chat with the user and some Bots. You join it as its admin unless you name another member.',
|
|
242
|
+
parameters: {
|
|
243
|
+
members: { type: 'array', required: true, items: { type: 'string' }, description: 'Exact names of the other Bots to add; you are added automatically' },
|
|
244
|
+
name: { type: 'string', description: 'Group name; defaults to the member names' },
|
|
245
|
+
notice: { type: 'string', description: 'Shared instructions every member reads before speaking in this group' },
|
|
246
|
+
admin: { type: 'string', description: 'Member who leads the group; defaults to you' },
|
|
247
|
+
mode: MODE_PARAM,
|
|
248
|
+
},
|
|
249
|
+
output,
|
|
250
|
+
async execute(args, exec) {
|
|
251
|
+
await rt.load()
|
|
252
|
+
const self = rt.selfOf(exec.agent?.id)
|
|
253
|
+
if (!rt.isMain(self)) return 'Only a Main Bot can create group chats. Ask a Main Bot instead.'
|
|
254
|
+
const { bots, unknown } = resolveBots(args.members)
|
|
255
|
+
if (unknown.length > 0) return `No Bot named ${unknown.join(', ')}. Team: ${teamNames()}`
|
|
256
|
+
const admin = args.admin ? rt.findBot(args.admin) : rt.botOf(self)
|
|
257
|
+
if (admin === undefined) return `No Bot named ${args.admin}. Team: ${teamNames()}`
|
|
258
|
+
const members = [...new Set([self, ...bots.map(bot => bot.id), admin.id])]
|
|
259
|
+
try {
|
|
260
|
+
const room = await rt.createRoom({ members, name: args.name, notice: args.notice, mode: args.mode, admin: admin.id, createdBy: self })
|
|
261
|
+
return `Created group "${room.name}" with ${room.members.map(id => rt.botOf(id).name).join(', ')}. ${admin.id === self ? 'You are' : `${admin.name} is`} its admin. It is in the user's sidebar; use post_to_group to start a discussion there.`
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return `Could not create the group: ${error.message}`
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
}))
|
|
267
|
+
|
|
268
|
+
ctx.tools.register(defineTool({
|
|
269
|
+
name: 'update_group',
|
|
270
|
+
description: 'Change a group chat: rename it, set its notice, add or remove members, change its admin or reply mode. Main Bots may change any group; a group\'s admin may change its own group.',
|
|
271
|
+
parameters: {
|
|
272
|
+
group: GROUP_REF,
|
|
273
|
+
name: { type: 'string', description: 'New group name; an empty string names it after its members again' },
|
|
274
|
+
notice: { type: 'string', description: 'Replacement notice, the shared instructions every member reads; an empty string clears it' },
|
|
275
|
+
add_members: { type: 'array', items: { type: 'string' }, description: 'Exact names of Bots to add' },
|
|
276
|
+
remove_members: { type: 'array', items: { type: 'string' }, description: 'Exact names of Bots to remove' },
|
|
277
|
+
admin: { type: 'string', description: 'Member to make the admin; "none" leaves the group without one (Main Bots only)' },
|
|
278
|
+
mode: MODE_PARAM,
|
|
279
|
+
},
|
|
280
|
+
output,
|
|
281
|
+
async execute(args, exec) {
|
|
282
|
+
await rt.load()
|
|
283
|
+
const self = rt.selfOf(exec.agent?.id)
|
|
284
|
+
if (rt.botOf(self) === undefined) return 'Only Bots on the team can use update_group.'
|
|
285
|
+
const { room, error } = pickRoom(args.group, self)
|
|
286
|
+
if (error) return error
|
|
287
|
+
const add = resolveBots(args.add_members)
|
|
288
|
+
const remove = resolveBots(args.remove_members)
|
|
289
|
+
const unknown = [...add.unknown, ...remove.unknown]
|
|
290
|
+
if (unknown.length > 0) return `No Bot named ${unknown.join(', ')}. Team: ${teamNames()}`
|
|
291
|
+
let admin
|
|
292
|
+
if (args.admin !== undefined && args.admin !== '') {
|
|
293
|
+
if (/^(none|nobody|null|无|没有)$/i.test(String(args.admin).trim())) admin = null
|
|
294
|
+
else {
|
|
295
|
+
admin = rt.findBot(args.admin)?.id
|
|
296
|
+
if (admin === undefined) return `No Bot named ${args.admin}. Team: ${teamNames()}`
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const { room: next, changes } = await rt.updateRoom(room.id, {
|
|
301
|
+
name: args.name,
|
|
302
|
+
notice: args.notice,
|
|
303
|
+
mode: args.mode,
|
|
304
|
+
admin,
|
|
305
|
+
add: add.bots.map(bot => bot.id),
|
|
306
|
+
remove: remove.bots.map(bot => bot.id),
|
|
307
|
+
}, self)
|
|
308
|
+
return changes.length === 0 ? `Nothing changed in "${next.name}".` : `Updated "${next.name}": ${changes.join('; ')}.`
|
|
309
|
+
} catch (failure) {
|
|
310
|
+
return `Could not update "${room.name}": ${failure.message}`
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
}))
|
|
314
|
+
|
|
315
|
+
ctx.tools.register(defineTool({
|
|
316
|
+
name: 'delete_group',
|
|
317
|
+
description: 'Main Bot only. Delete a group chat; its history is archived. Only when the user asks.',
|
|
318
|
+
parameters: { group: GROUP_REF },
|
|
319
|
+
output,
|
|
320
|
+
async execute(args, exec) {
|
|
321
|
+
await rt.load()
|
|
322
|
+
const self = rt.selfOf(exec.agent?.id)
|
|
323
|
+
if (!rt.isMain(self)) return 'Only a Main Bot can delete group chats.'
|
|
324
|
+
const { room, error } = pickRoom(args.group, self)
|
|
325
|
+
if (error) return error
|
|
326
|
+
if (rt.roomBusy(room.id)) return `"${room.name}" is in the middle of a round. Delete it after the round ends.`
|
|
327
|
+
await rt.deleteRoom(room.id)
|
|
328
|
+
return `Deleted "${room.name}". Its history is archived.`
|
|
329
|
+
},
|
|
330
|
+
}))
|
|
331
|
+
|
|
332
|
+
ctx.tools.register(defineTool({
|
|
333
|
+
name: 'post_to_group',
|
|
334
|
+
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.',
|
|
335
|
+
parameters: {
|
|
336
|
+
group: GROUP_REF,
|
|
337
|
+
message: { type: 'string', required: true, description: 'What to say; write "@Name" to choose who answers' },
|
|
338
|
+
report_back: { type: 'boolean', description: 'Also send the members\' replies back to you as a new message when the round ends' },
|
|
339
|
+
},
|
|
340
|
+
output,
|
|
341
|
+
async execute(args, exec) {
|
|
342
|
+
await rt.load()
|
|
343
|
+
const self = rt.selfOf(exec.agent?.id)
|
|
344
|
+
const sender = rt.botOf(self)
|
|
345
|
+
if (sender === undefined) return 'Only Bots on the team can use post_to_group.'
|
|
346
|
+
const { room, error } = pickRoom(args.group, self)
|
|
347
|
+
if (error) return error
|
|
348
|
+
if (!rt.isMain(self) && !rt.isAdminOf(room, self)) return `Only a Main Bot or the admin of "${room.name}" can post there. Use message_bot to reach its members.`
|
|
349
|
+
const info = rt.live.get(exec.agent?.id)?.info
|
|
350
|
+
if (info?.role === 'room' && info.roomId === room.id) return `You are speaking in "${room.name}" right now. Write your message as your reply instead.`
|
|
351
|
+
if ((info?.hop ?? 0) >= MAX_HOPS) return 'This chain of Bot messages is already long. Stop here and report to the user instead.'
|
|
352
|
+
const text = String(args.message ?? '').trim()
|
|
353
|
+
if (text === '') return 'Write a message to post.'
|
|
354
|
+
if ((room.botRounds ?? 0) >= ROOM_BOT_ROUNDS) return `Bots already started ${ROOM_BOT_ROUNDS} rounds in "${room.name}" since the user last spoke there. Wait for the user.`
|
|
355
|
+
room.botRounds = (room.botRounds ?? 0) + 1
|
|
356
|
+
await rt.save()
|
|
357
|
+
const reportBack = args.report_back === true
|
|
358
|
+
try {
|
|
359
|
+
await rt.postToRoom(room, sender, text, { hop: info?.hop ?? 0, ...(reportBack ? { reportBack: true } : {}) })
|
|
360
|
+
} catch (failure) {
|
|
361
|
+
return `Could not post in "${room.name}": ${failure.message}`
|
|
362
|
+
}
|
|
363
|
+
return `Posted in "${room.name}". Members reply there, where the user reads them${reportBack ? '; their replies also come back to you as a new message when the round ends' : ', and nothing comes back to you'}.`
|
|
364
|
+
},
|
|
365
|
+
}))
|
|
366
|
+
|
|
367
|
+
ctx.tools.register(defineTool({
|
|
368
|
+
name: 'ask_user',
|
|
369
|
+
description: 'Show the user a short multiple-choice question card in the chat (the user can also type their own answer). Calling it ends your turn; the chosen answer arrives later as the user\'s next message.',
|
|
370
|
+
parameters: {
|
|
371
|
+
question: { type: 'string', required: true, description: 'One short question' },
|
|
372
|
+
options: {
|
|
373
|
+
type: 'array',
|
|
374
|
+
required: true,
|
|
375
|
+
description: 'Two to four answers',
|
|
376
|
+
items: {
|
|
377
|
+
type: 'object',
|
|
378
|
+
additionalProperties: false,
|
|
379
|
+
properties: {
|
|
380
|
+
label: { type: 'string', required: true, description: 'Short answer shown on the option' },
|
|
381
|
+
description: { type: 'string', description: 'Optional one-line explanation under the label' },
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
detail: { type: 'string', description: 'Optional one-line context shown under the question' },
|
|
386
|
+
multiSelect: { type: 'boolean', description: 'Allow choosing several options' },
|
|
387
|
+
allowCustom: { type: 'boolean', description: 'Also offer a field for an answer in the user\'s own words' },
|
|
388
|
+
},
|
|
389
|
+
output,
|
|
390
|
+
async execute(args, exec) {
|
|
391
|
+
await rt.load()
|
|
392
|
+
const id = exec.agent?.id
|
|
393
|
+
if (rt.groupOwners.has(id)) return 'Question cards only work in your own chat. In a group, ask in your message instead.'
|
|
394
|
+
if (!rt.chatOwners.has(id)) return 'Only Bots on the team can use ask_user.'
|
|
395
|
+
const options = (Array.isArray(args.options) ? args.options : [])
|
|
396
|
+
.map(option => (typeof option === 'string' ? { label: option } : option ?? {}))
|
|
397
|
+
.map(option => ({
|
|
398
|
+
label: String(option.label ?? '').trim(),
|
|
399
|
+
...(option.description ? { description: String(option.description).trim() } : {}),
|
|
400
|
+
}))
|
|
401
|
+
.filter(option => option.label !== '')
|
|
402
|
+
.slice(0, 6)
|
|
403
|
+
if (options.length === 0) return 'Give at least one option.'
|
|
404
|
+
// A hidden Bot-to-Bot turn shows none of its calls, so its card goes to the
|
|
405
|
+
// composer dock, which takes questions without a call.
|
|
406
|
+
const turn = rt.live.get(id)?.turn
|
|
407
|
+
const hidden = turn !== undefined && (state.exchangeTurns[id] ?? []).includes(turn)
|
|
408
|
+
state.questions[id] = {
|
|
409
|
+
id: randomUUID(),
|
|
410
|
+
...(exec.callId && !hidden ? { callId: exec.callId } : {}),
|
|
411
|
+
question: String(args.question ?? '').trim(),
|
|
412
|
+
...(args.detail ? { detail: String(args.detail).trim() } : {}),
|
|
413
|
+
options,
|
|
414
|
+
multiSelect: args.multiSelect === true,
|
|
415
|
+
...(args.allowCustom === true ? { allowCustom: true } : {}),
|
|
416
|
+
askedAt: Date.now(),
|
|
417
|
+
}
|
|
418
|
+
await rt.save()
|
|
419
|
+
exec.concludeTurn?.()
|
|
420
|
+
return 'The card is on screen; the user\'s answer arrives as their next message.'
|
|
421
|
+
},
|
|
422
|
+
}))
|
|
423
|
+
|
|
424
|
+
Object.assign(rt, { pickRoom })
|
|
425
|
+
}
|