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/src/host/tools.js
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
// question card. Group rounds and reading chats are in groups.js.
|
|
3
3
|
import { randomUUID } from 'node:crypto'
|
|
4
4
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
5
|
-
import { COLORS, EXTRA_COLORS, MAX_HOPS, ROOM_BOT_ROUNDS, ROOM_MODES } from './constants.js'
|
|
5
|
+
import { COLORS, EXTRA_COLORS, MAX_HOPS, MAX_UNREAD_REQUESTS, ROOM_BOT_ROUNDS, ROOM_MODES, SOURCE_KIND } from './constants.js'
|
|
6
6
|
import { unquote } from './text.js'
|
|
7
7
|
|
|
8
8
|
export const output = { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] }
|
|
9
9
|
export const GROUP_REF = { type: 'string', required: true, description: 'Exact group chat name from list_bots' }
|
|
10
|
+
const BLOCKING_QUESTION_TOOL = 'ask_user_question'
|
|
10
11
|
const MODE_PARAM = {
|
|
11
12
|
type: 'string',
|
|
12
13
|
enum: ROOM_MODES,
|
|
13
14
|
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
|
+
const WORK_DIR_PARAM = { type: 'string', description: 'Full path of the folder to work in, where commands and file tools start. Only when the user asks for it' }
|
|
15
17
|
|
|
16
18
|
export function install(rt) {
|
|
17
19
|
const { ctx, state } = rt
|
|
@@ -29,6 +31,7 @@ export function install(rt) {
|
|
|
29
31
|
`members: ${members.join(', ')}`,
|
|
30
32
|
room.mode === 'admin' && room.admin ? 'plain messages go to the admin first' : 'every member replies in turn',
|
|
31
33
|
room.notice ? `notice: ${room.notice.split('\n')[0].slice(0, 100)}` : '',
|
|
34
|
+
room.workDir ? `members work in ${room.workDir} here` : '',
|
|
32
35
|
].filter(Boolean)
|
|
33
36
|
return `- "${room.name}" — ${parts.join('; ')}`
|
|
34
37
|
}
|
|
@@ -45,7 +48,7 @@ export function install(rt) {
|
|
|
45
48
|
const self = rt.selfOf(exec.agent?.id)
|
|
46
49
|
const lines = Object.values(state.bots).map(bot => {
|
|
47
50
|
const model = bot.appliedModel ?? bot.model
|
|
48
|
-
const tags = [bot.role, model ? `model ${model}` : '', rt.isMain(bot.id) ? 'Main Bot' : '', bot.id === self ? 'you' : ''].filter(Boolean).join(', ')
|
|
51
|
+
const tags = [bot.role, model ? `model ${model}` : '', bot.workDir ? `works in ${bot.workDir}` : '', rt.isMain(bot.id) ? 'Main Bot' : '', bot.id === self ? 'you' : ''].filter(Boolean).join(', ')
|
|
49
52
|
const job = bot.instructions ? ` — ${bot.instructions.split('\n')[0].slice(0, 120)}` : ''
|
|
50
53
|
return `- ${bot.name}${tags ? ` (${tags})` : ''}${job}`
|
|
51
54
|
})
|
|
@@ -72,6 +75,12 @@ export function install(rt) {
|
|
|
72
75
|
const room = rt.roomOf(rt.groupOwners.get(sessionId)?.roomId)
|
|
73
76
|
return room ? `group "${room.name}"` : 'its own chat'
|
|
74
77
|
}
|
|
78
|
+
// Delivered but not read yet: no step or turn has taken them out of the inbox.
|
|
79
|
+
const unreadRequests = agent => [...(agent.inbox?.nextStep ?? []), ...(agent.inbox?.nextTurn ?? [])]
|
|
80
|
+
.filter(message => message.source?.kind === SOURCE_KIND && message.source.role === 'request').length
|
|
81
|
+
|
|
82
|
+
// A tool that throws shows as failed in the chat, instead of as sent or stopped.
|
|
83
|
+
const refuse = (message) => { throw new Error(message) }
|
|
75
84
|
|
|
76
85
|
ctx.tools.register(defineTool({
|
|
77
86
|
name: 'message_bot',
|
|
@@ -101,8 +110,13 @@ export function install(rt) {
|
|
|
101
110
|
const turn = rt.live.get(exec.agent?.id)
|
|
102
111
|
const hop = Math.max(turn?.info?.hop ?? 0, ...(turn?.joined ?? []).map(joined => joined.info?.hop ?? 0))
|
|
103
112
|
if (hop >= MAX_HOPS) return 'This chain of Bot messages is already long. Stop here and report to the user instead.'
|
|
113
|
+
const chat = rt.chatOf(target.id)
|
|
114
|
+
const unread = unreadRequests(await rt.resolveAgent(chat))
|
|
115
|
+
if (unread >= MAX_UNREAD_REQUESTS) {
|
|
116
|
+
refuse(`Not sent: ${target.name} has not read the last ${unread} messages from Bots yet, so it is most likely stuck, for example waiting for the user. Do not send it more; tell the user that ${target.name} is not responding.`)
|
|
117
|
+
}
|
|
104
118
|
const toUser = args.reply_to === 'user'
|
|
105
|
-
const working = rt.live.get(
|
|
119
|
+
const working = rt.live.get(chat)?.turn !== undefined
|
|
106
120
|
const steer = args.when !== 'queue' && !toUser
|
|
107
121
|
await rt.deliver({ fromId: sender.id, toId: target.id, text: args.message, role: 'request', hop, steer, ...(toUser ? { audience: 'user' } : callerRoom(exec)) })
|
|
108
122
|
const sent = toUser
|
|
@@ -112,18 +126,20 @@ export function install(rt) {
|
|
|
112
126
|
: working
|
|
113
127
|
? `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
128
|
: `Delivered to ${target.name}. Its reply will arrive later as a new message; do not wait for it.`
|
|
129
|
+
// "Delivered" only means it is in the inbox; say so when it may sit there a while.
|
|
130
|
+
const notes = [
|
|
131
|
+
rt.waitsForApproval?.(chat) ? `${target.name} is waiting for the user's approval, so it reads your message only after the user answers.` : '',
|
|
132
|
+
unread > 0 ? `${target.name} has not read ${unread === 1 ? 'an earlier message' : `${unread} earlier messages`} from Bots yet.` : '',
|
|
133
|
+
].filter(Boolean)
|
|
134
|
+
const status = notes.length === 0 ? sent : `${sent} ${notes.join(' ')}`
|
|
115
135
|
// 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 !==
|
|
117
|
-
if (groups.length === 0) return
|
|
136
|
+
const groups = activeSessions(target.id).running.filter(sessionId => sessionId !== chat)
|
|
137
|
+
if (groups.length === 0) return status
|
|
118
138
|
const stop = rt.isMain(sender.id) ? ' If it must not go on there, use stop_bot.' : ''
|
|
119
|
-
return `${
|
|
139
|
+
return `${status} ${target.name} is also working in ${groups.map(whereOf).join(' and ')}; your message does not reach that work.${stop}`
|
|
120
140
|
},
|
|
121
141
|
}))
|
|
122
142
|
|
|
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
143
|
ctx.tools.register(defineTool({
|
|
128
144
|
name: 'stop_bot',
|
|
129
145
|
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.',
|
|
@@ -160,14 +176,18 @@ export function install(rt) {
|
|
|
160
176
|
instructions: { type: 'string', required: true, description: 'What this Bot is responsible for and how it should work' },
|
|
161
177
|
brief: { type: 'string', description: 'Why the user wants it, passed to the new Bot as its first context' },
|
|
162
178
|
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.' },
|
|
179
|
+
work_dir: WORK_DIR_PARAM,
|
|
163
180
|
},
|
|
164
181
|
output,
|
|
165
182
|
async execute(args, exec) {
|
|
166
183
|
await rt.load()
|
|
167
184
|
if (!rt.isMain(rt.selfOf(exec.agent?.id))) return 'Only the Main Bot can create Bots. Ask the Main Bot instead.'
|
|
168
185
|
try {
|
|
169
|
-
const
|
|
170
|
-
|
|
186
|
+
const workDir = args.work_dir ? await rt.cleanWorkDir(args.work_dir) : undefined
|
|
187
|
+
const name = String(args.name ?? '').trim()
|
|
188
|
+
await rt.approveFolder(exec, { path: workDir, who: { en: `the new Bot ${name}`, zh: `新 Bot ${name} ` } })
|
|
189
|
+
const bot = await rt.createBot({ ...args, workDir, createdBy: rt.selfOf(exec.agent.id) })
|
|
190
|
+
return `Created ${bot.name}${workDir ? `, working in ${workDir}` : ''}. It is greeting the user now.`
|
|
171
191
|
} catch (error) {
|
|
172
192
|
return `Could not create the Bot: ${error.message}`
|
|
173
193
|
}
|
|
@@ -176,7 +196,7 @@ export function install(rt) {
|
|
|
176
196
|
|
|
177
197
|
ctx.tools.register(defineTool({
|
|
178
198
|
name: 'update_bot',
|
|
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.',
|
|
199
|
+
description: 'Change a Bot\'s name, role, color, model, working folder, or instructions. Every Bot may change itself; only a Main Bot may change another Bot.',
|
|
180
200
|
parameters: {
|
|
181
201
|
bot: { type: 'string', description: 'Current name of the Bot to change; leave out to change yourself' },
|
|
182
202
|
name: { type: 'string', description: 'New name' },
|
|
@@ -184,6 +204,7 @@ export function install(rt) {
|
|
|
184
204
|
instructions: { type: 'string', description: 'Replacement instructions' },
|
|
185
205
|
color: { type: 'string', description: `One of ${[...COLORS, ...EXTRA_COLORS].join(', ')}` },
|
|
186
206
|
model: { type: 'string', description: 'Move the Bot to another model, as provider/model or a model id' },
|
|
207
|
+
work_dir: { ...WORK_DIR_PARAM, description: `${WORK_DIR_PARAM.description}; an empty string returns it to the team workspace` },
|
|
187
208
|
},
|
|
188
209
|
output,
|
|
189
210
|
async execute(args, exec) {
|
|
@@ -196,8 +217,13 @@ export function install(rt) {
|
|
|
196
217
|
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.'
|
|
197
218
|
try {
|
|
198
219
|
const { name: newName, role, instructions, color, model } = args
|
|
199
|
-
const
|
|
200
|
-
|
|
220
|
+
const moving = typeof args.work_dir === 'string'
|
|
221
|
+
const workDir = moving ? await rt.cleanWorkDir(args.work_dir) : undefined
|
|
222
|
+
if (moving && workDir !== bot.workDir) await rt.approveFolder(exec, { path: workDir, sessions: rt.sessionsOfBot(bot.id), who: { en: bot.name, zh: ` ${bot.name} ` } })
|
|
223
|
+
const next = await rt.updateBot(bot.id, { name: newName, role, instructions, color, ...(model ? { model } : {}), ...(moving ? { workDir: workDir ?? '' } : {}) })
|
|
224
|
+
if (!moving) return `Updated ${next.name}.`
|
|
225
|
+
const folder = workDir ?? 'the team workspace'
|
|
226
|
+
return bot.id === caller.id ? `Updated ${next.name}. From your next step on, you work in ${folder}.` : `Updated ${next.name}. It now works in ${folder}.`
|
|
201
227
|
} catch (error) {
|
|
202
228
|
return `Could not update the Bot: ${error.message}`
|
|
203
229
|
}
|
|
@@ -280,6 +306,7 @@ export function install(rt) {
|
|
|
280
306
|
remove_members: { type: 'array', items: { type: 'string' }, description: 'Exact names of Bots to remove' },
|
|
281
307
|
admin: { type: 'string', description: 'Member to make the admin; "none" leaves the group without one (Main Bots only)' },
|
|
282
308
|
mode: MODE_PARAM,
|
|
309
|
+
work_dir: { ...WORK_DIR_PARAM, description: `${WORK_DIR_PARAM.description}. Every member works there in this group; an empty string lets each member work in its own folder again (Main Bots only)` },
|
|
283
310
|
},
|
|
284
311
|
output,
|
|
285
312
|
async execute(args, exec) {
|
|
@@ -288,6 +315,8 @@ export function install(rt) {
|
|
|
288
315
|
if (rt.botOf(self) === undefined) return 'Only Bots on the team can use update_group.'
|
|
289
316
|
const { room, error } = pickRoom(args.group, self)
|
|
290
317
|
if (error) return error
|
|
318
|
+
const moving = typeof args.work_dir === 'string'
|
|
319
|
+
if (moving && !rt.isMain(self)) return `Only a Main Bot can set the folder of "${room.name}".`
|
|
291
320
|
const add = resolveBots(args.add_members)
|
|
292
321
|
const remove = resolveBots(args.remove_members)
|
|
293
322
|
const unknown = [...add.unknown, ...remove.unknown]
|
|
@@ -301,6 +330,16 @@ export function install(rt) {
|
|
|
301
330
|
}
|
|
302
331
|
}
|
|
303
332
|
try {
|
|
333
|
+
const workDir = moving ? await rt.cleanWorkDir(args.work_dir) : undefined
|
|
334
|
+
// A Bot that joins a group with a folder works there from its first turn.
|
|
335
|
+
const joining = add.bots.filter(bot => !room.members.includes(bot.id))
|
|
336
|
+
if (moving && workDir !== room.workDir) {
|
|
337
|
+
const members = [...room.members, ...joining.map(bot => bot.id)]
|
|
338
|
+
await rt.approveFolder(exec, { path: workDir, sessions: members.map(id => room.sessions?.[id]).filter(Boolean), who: { en: `every member of "${room.name}"`, zh: `群聊「${room.name}」的成员` } })
|
|
339
|
+
} else if (joining.length > 0) {
|
|
340
|
+
const names = joining.map(bot => bot.name)
|
|
341
|
+
await rt.approveFolder(exec, { path: room.workDir, sessions: joining.map(bot => room.sessions?.[bot.id]).filter(Boolean), who: { en: names.join(', '), zh: ` ${names.join('、')} ` } })
|
|
342
|
+
}
|
|
304
343
|
const { room: next, changes } = await rt.updateRoom(room.id, {
|
|
305
344
|
name: args.name,
|
|
306
345
|
notice: args.notice,
|
|
@@ -308,6 +347,7 @@ export function install(rt) {
|
|
|
308
347
|
admin,
|
|
309
348
|
add: add.bots.map(bot => bot.id),
|
|
310
349
|
remove: remove.bots.map(bot => bot.id),
|
|
350
|
+
...(moving ? { workDir: workDir ?? '' } : {}),
|
|
311
351
|
}, self)
|
|
312
352
|
return changes.length === 0 ? `Nothing changed in "${next.name}".` : `Updated "${next.name}": ${changes.join('; ')}.`
|
|
313
353
|
} catch (failure) {
|
|
@@ -425,6 +465,38 @@ export function install(rt) {
|
|
|
425
465
|
},
|
|
426
466
|
}))
|
|
427
467
|
|
|
468
|
+
// dsh's own question tool holds the turn open until the user answers, and every
|
|
469
|
+
// message from another Bot or a group round queues behind it. Bot Sessions ask
|
|
470
|
+
// through ask_user instead, so the tool is taken out of their tool list.
|
|
471
|
+
const hiddenQuestion = new WeakSet()
|
|
472
|
+
const hideBlockingQuestion = (agent) => {
|
|
473
|
+
if (agent === undefined || hiddenQuestion.has(agent) || !rt.isBotSession(agent.session?.id)) return
|
|
474
|
+
if (typeof agent.ctx?.tools?.restrict !== 'function') return
|
|
475
|
+
try {
|
|
476
|
+
// restrict() throws on a name this profile does not serve.
|
|
477
|
+
if (!ctx.tools.schemas(agent).some(schema => schema.name === BLOCKING_QUESTION_TOOL)) return
|
|
478
|
+
agent.ctx.tools.restrict({ deny: [BLOCKING_QUESTION_TOOL] })
|
|
479
|
+
hiddenQuestion.add(agent)
|
|
480
|
+
} catch (error) {
|
|
481
|
+
rt.warn('ds-bot: %s stays in the tool list of %s: %s', BLOCKING_QUESTION_TOOL, agent.session?.id, error)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// A new Bot, part, or group Session gets its Agent before ds-bot records it, so the
|
|
485
|
+
// list is only trimmed once ds-bot resolves that Agent again; the guard refuses
|
|
486
|
+
// the tool in any Bot Session meanwhile.
|
|
487
|
+
ctx.tools.guard(exec => (exec.name === BLOCKING_QUESTION_TOOL && rt.isBotSession(exec.agent?.id)
|
|
488
|
+
? `Bots cannot use ${BLOCKING_QUESTION_TOOL}. Ask with ask_user in your own chat, or ask in your reply.`
|
|
489
|
+
: undefined))
|
|
490
|
+
// Creation waits for this listener, so the Agent's first step already lacks the tool.
|
|
491
|
+
ctx.on('agent/created', async ({ agent }) => {
|
|
492
|
+
try {
|
|
493
|
+
await rt.load()
|
|
494
|
+
hideBlockingQuestion(agent)
|
|
495
|
+
} catch (error) {
|
|
496
|
+
rt.warn('ds-bot: hiding %s on agent/created failed: %s', BLOCKING_QUESTION_TOOL, error)
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
|
|
428
500
|
ctx.tools.register(defineTool({
|
|
429
501
|
name: 'list_capabilities',
|
|
430
502
|
description: 'List shared MCP servers, skills, and packs, plus the capabilities attached to you.',
|
|
@@ -523,5 +595,5 @@ export function install(rt) {
|
|
|
523
595
|
}))
|
|
524
596
|
}
|
|
525
597
|
|
|
526
|
-
Object.assign(rt, { pickRoom })
|
|
598
|
+
Object.assign(rt, { pickRoom, hideBlockingQuestion })
|
|
527
599
|
}
|
package/src/host/turns.js
CHANGED
|
@@ -40,6 +40,10 @@ export const stripWrittenCalls = text => text
|
|
|
40
40
|
.replace(/<function=[\w.-]+>[\s\S]*?(?:<\/function>|$)/g, '')
|
|
41
41
|
.trim()
|
|
42
42
|
|
|
43
|
+
// Under DSH's PTC presentation the model calls only this tool; the program's sub-calls
|
|
44
|
+
// are the tools it really runs.
|
|
45
|
+
const PROGRAM_TOOL = 'run_code'
|
|
46
|
+
|
|
43
47
|
const RETRY_TEXT = 'Your last reply wrote a tool call as plain text (<tool_call>…), so it did not run and nothing happened. Call the tool through the tool interface now, then reply as you meant to.'
|
|
44
48
|
|
|
45
49
|
export function install(rt) {
|
|
@@ -120,6 +124,22 @@ export function install(rt) {
|
|
|
120
124
|
else next[id] = value
|
|
121
125
|
activity = next
|
|
122
126
|
}
|
|
127
|
+
// The newest call still running, a program's sub-calls included, is what the Session
|
|
128
|
+
// is doing.
|
|
129
|
+
const showCall = (id, entry) => {
|
|
130
|
+
const name = [...(entry.calls?.values() ?? [])].at(-1)?.name
|
|
131
|
+
entry.tool = name
|
|
132
|
+
if (name !== undefined) setActivity(id, name === 'ask_user' ? undefined : activityOf(name))
|
|
133
|
+
else if (entry.turn !== undefined) setActivity(id, 'thinking')
|
|
134
|
+
}
|
|
135
|
+
const startCall = (id, entry, callId, name, args) => {
|
|
136
|
+
if (typeof callId === 'string') (entry.calls ??= new Map()).set(callId, { name, args })
|
|
137
|
+
showCall(id, entry)
|
|
138
|
+
}
|
|
139
|
+
const endCall = (id, entry, callId) => {
|
|
140
|
+
entry.calls?.delete(callId)
|
|
141
|
+
showCall(id, entry)
|
|
142
|
+
}
|
|
123
143
|
|
|
124
144
|
const markExchangeTurn = (sessionId, turn) => {
|
|
125
145
|
const turns = state.exchangeTurns[sessionId] ??= []
|
|
@@ -211,7 +231,7 @@ export function install(rt) {
|
|
|
211
231
|
// the command, path or address it acts on.
|
|
212
232
|
const callDetail = (sessionId, callId) => {
|
|
213
233
|
let args
|
|
214
|
-
try { args = JSON.parse(live.get(sessionId)?.calls?.get(callId) ?? '{}') } catch { args = {} }
|
|
234
|
+
try { args = JSON.parse(live.get(sessionId)?.calls?.get(callId)?.args ?? '{}') } catch { args = {} }
|
|
215
235
|
const first = keys => keys.map(key => args?.[key]).find(value => typeof value === 'string' && value.trim() !== '')
|
|
216
236
|
const text = first(['description', 'justification'])
|
|
217
237
|
const code = first(['command', 'cmd', 'file_path', 'path', 'url', 'query'])
|
|
@@ -245,16 +265,22 @@ export function install(rt) {
|
|
|
245
265
|
switch (event.type) {
|
|
246
266
|
case 'tool/call': {
|
|
247
267
|
const name = String(event.data?.name ?? '')
|
|
248
|
-
|
|
249
|
-
|
|
268
|
+
startCall(id, entry, event.data?.callId, name, String(event.data?.arguments ?? ''))
|
|
269
|
+
// A program is as quick as the tools it runs, so its sub-calls decide.
|
|
270
|
+
if (rt.groupOwners.has(id) && name !== PROGRAM_TOOL) rt.workRoomTurn?.(id, name)
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
case 'tool/ptc-dispatch-start': {
|
|
274
|
+
const name = String(event.data?.name ?? '')
|
|
275
|
+
startCall(id, entry, event.data?.subCallId, name, JSON.stringify(event.data?.arguments ?? {}))
|
|
250
276
|
if (rt.groupOwners.has(id)) rt.workRoomTurn?.(id, name)
|
|
251
|
-
if (typeof event.data?.callId === 'string') (entry.calls ??= new Map()).set(event.data.callId, String(event.data.arguments ?? ''))
|
|
252
277
|
break
|
|
253
278
|
}
|
|
279
|
+
case 'tool/ptc-dispatch':
|
|
280
|
+
endCall(id, entry, event.data?.subCallId)
|
|
281
|
+
break
|
|
254
282
|
case 'tool/result':
|
|
255
|
-
|
|
256
|
-
entry.tool = undefined
|
|
257
|
-
entry.calls?.delete(event.data?.callId)
|
|
283
|
+
endCall(id, entry, event.data?.callId)
|
|
258
284
|
break
|
|
259
285
|
case 'turn/start':
|
|
260
286
|
setActivity(id, 'thinking')
|
package/src/host/usage.js
CHANGED
|
@@ -523,6 +523,9 @@ export function install(rt) {
|
|
|
523
523
|
const tag = from !== null && typeof from === 'object' ? tags.get(from) : undefined
|
|
524
524
|
if (tag !== undefined) tagUsage(to, tag)
|
|
525
525
|
}
|
|
526
|
+
// DS Bot's own requests name the Session they serve, as some routes require, so
|
|
527
|
+
// `sessionId` alone does not make a request that Session's own round; the tag does.
|
|
528
|
+
const ownRequest = options => tags.has(options?.messages)
|
|
526
529
|
// Owner and group use are journaled with the call itself: a Bot or room that
|
|
527
530
|
// is deleted before the first scan still leaves its calls under its own name.
|
|
528
531
|
const journaled = new Map()
|
|
@@ -590,5 +593,5 @@ export function install(rt) {
|
|
|
590
593
|
})()
|
|
591
594
|
})
|
|
592
595
|
|
|
593
|
-
Object.assign(rt, { usageView, tagUsage, carryUsageTag })
|
|
596
|
+
Object.assign(rt, { usageView, tagUsage, carryUsageTag, ownRequest })
|
|
594
597
|
}
|