ds-bot 0.1.3 → 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 +5 -4
- package/client.js +1948 -1269
- package/index.js +12 -3
- package/package.json +1 -1
- package/src/host/approvals.js +8 -2
- package/src/host/capabilities.js +521 -0
- package/src/host/channel.js +25 -6
- package/src/host/constants.js +11 -0
- package/src/host/delivery.js +21 -6
- package/src/host/folders.js +135 -0
- package/src/host/groups.js +123 -33
- package/src/host/images.js +2 -2
- package/src/host/memory.js +31 -8
- package/src/host/parts.js +100 -32
- package/src/host/prompt.js +5 -5
- package/src/host/resume.js +247 -0
- package/src/host/roster.js +41 -10
- package/src/host/secrets.js +9 -1
- package/src/host/store.js +16 -3
- package/src/host/tools.js +193 -19
- package/src/host/turns.js +126 -14
- package/src/host/usage.js +4 -1
package/src/host/turns.js
CHANGED
|
@@ -31,9 +31,45 @@ export function attachReference(queued, source, text) {
|
|
|
31
31
|
post.references = [...(post.references ?? []), { labels, text }]
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// A tool call some models write into their text instead of making it (the XML-like
|
|
35
|
+
// form of their own chat template).
|
|
36
|
+
const WRITTEN_CALL = /<tool_call>\s*<function=|<function=[\w.-]+>\s*<parameter=/
|
|
37
|
+
export const writesToolCall = text => typeof text === 'string' && WRITTEN_CALL.test(text)
|
|
38
|
+
export const stripWrittenCalls = text => text
|
|
39
|
+
.replace(/<tool_call>[\s\S]*?(?:<\/tool_call>|$)/g, '')
|
|
40
|
+
.replace(/<function=[\w.-]+>[\s\S]*?(?:<\/function>|$)/g, '')
|
|
41
|
+
.trim()
|
|
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
|
+
|
|
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.'
|
|
48
|
+
|
|
34
49
|
export function install(rt) {
|
|
35
50
|
const { ctx, state } = rt
|
|
36
51
|
|
|
52
|
+
// The retry goes where the turn's trigger came from: a group round's waiter or a
|
|
53
|
+
// Bot's request keeps its exchange; anything else is a nudge in the Bot's own chat.
|
|
54
|
+
const retryWrittenCall = (id, info) => {
|
|
55
|
+
const keeps = info !== undefined && (info.role === 'room' || expectsReply(info))
|
|
56
|
+
return rt.deliver({
|
|
57
|
+
...(keeps && info.senderSessionId ? { fromId: info.senderSessionId } : {}),
|
|
58
|
+
toId: rt.selfOf(id),
|
|
59
|
+
sessionId: id,
|
|
60
|
+
role: keeps ? info.role : 'retry',
|
|
61
|
+
...(keeps && info.roomId ? { roomId: info.roomId } : {}),
|
|
62
|
+
hop: keeps ? info.hop ?? 0 : 0,
|
|
63
|
+
...(keeps && info.exchangeId ? { exchangeId: info.exchangeId } : {}),
|
|
64
|
+
...(keeps && info.audience ? { audience: info.audience } : {}),
|
|
65
|
+
...(keeps && info.askedBy ? { askedBy: info.askedBy } : {}),
|
|
66
|
+
...(keeps && info.senderRoomId ? { fromRoomId: info.senderRoomId } : {}),
|
|
67
|
+
note: 'your tool call did not run',
|
|
68
|
+
retried: true,
|
|
69
|
+
text: RETRY_TEXT,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
37
73
|
/** @type {Map<string, {turn?:number, pending?:any, info?:any, texts:string[], opened?:boolean, joined?:{info:any, from:number}[]}>} */
|
|
38
74
|
const live = new Map()
|
|
39
75
|
/** @type {Map<string, {sessionId:string, resolve:(text:string|null)=>void, fail:(failure:any)=>void, touch:()=>void, approval:()=>void}>} member turn waiters keyed by exchange id */
|
|
@@ -88,6 +124,22 @@ export function install(rt) {
|
|
|
88
124
|
else next[id] = value
|
|
89
125
|
activity = next
|
|
90
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
|
+
}
|
|
91
143
|
|
|
92
144
|
const markExchangeTurn = (sessionId, turn) => {
|
|
93
145
|
const turns = state.exchangeTurns[sessionId] ??= []
|
|
@@ -157,11 +209,29 @@ export function install(rt) {
|
|
|
157
209
|
return bots
|
|
158
210
|
}
|
|
159
211
|
|
|
212
|
+
// Per group, the members whose turn there is running; the round's current speaker is
|
|
213
|
+
// marked, since its round shows it.
|
|
214
|
+
const roomWorkView = () => {
|
|
215
|
+
const speaking = new Set([...roomProgress.values()].map(round => `${round.roomId}:${round.speaking}`))
|
|
216
|
+
const rooms = {}
|
|
217
|
+
for (const [id, entry] of live) {
|
|
218
|
+
const owner = rt.groupOwners.get(id)
|
|
219
|
+
if (entry.turn === undefined || owner === undefined) continue
|
|
220
|
+
rooms[owner.roomId] = [...(rooms[owner.roomId] ?? []), {
|
|
221
|
+
botId: owner.botId,
|
|
222
|
+
since: entry.since,
|
|
223
|
+
...(entry.tool ? { tool: entry.tool } : {}),
|
|
224
|
+
...(speaking.has(`${owner.roomId}:${owner.botId}`) ? { speaking: true } : {}),
|
|
225
|
+
}]
|
|
226
|
+
}
|
|
227
|
+
return rooms
|
|
228
|
+
}
|
|
229
|
+
|
|
160
230
|
// What a running tool call is about, for an approval card: its own description and
|
|
161
231
|
// the command, path or address it acts on.
|
|
162
232
|
const callDetail = (sessionId, callId) => {
|
|
163
233
|
let args
|
|
164
|
-
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 = {} }
|
|
165
235
|
const first = keys => keys.map(key => args?.[key]).find(value => typeof value === 'string' && value.trim() !== '')
|
|
166
236
|
const text = first(['description', 'justification'])
|
|
167
237
|
const code = first(['command', 'cmd', 'file_path', 'path', 'url', 'query'])
|
|
@@ -195,15 +265,22 @@ export function install(rt) {
|
|
|
195
265
|
switch (event.type) {
|
|
196
266
|
case 'tool/call': {
|
|
197
267
|
const name = String(event.data?.name ?? '')
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (
|
|
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 ?? {}))
|
|
276
|
+
if (rt.groupOwners.has(id)) rt.workRoomTurn?.(id, name)
|
|
201
277
|
break
|
|
202
278
|
}
|
|
279
|
+
case 'tool/ptc-dispatch':
|
|
280
|
+
endCall(id, entry, event.data?.subCallId)
|
|
281
|
+
break
|
|
203
282
|
case 'tool/result':
|
|
204
|
-
|
|
205
|
-
entry.tool = undefined
|
|
206
|
-
entry.calls?.delete(event.data?.callId)
|
|
283
|
+
endCall(id, entry, event.data?.callId)
|
|
207
284
|
break
|
|
208
285
|
case 'turn/start':
|
|
209
286
|
setActivity(id, 'thinking')
|
|
@@ -255,6 +332,10 @@ export function install(rt) {
|
|
|
255
332
|
...(source.late ? { late: true } : {}),
|
|
256
333
|
...(source.quiet ? { quiet: true } : {}),
|
|
257
334
|
...(source.failed ? { failed: source.failed } : {}),
|
|
335
|
+
...(source.resumed ? { resumed: true } : {}),
|
|
336
|
+
...(source.again ? { again: true } : {}),
|
|
337
|
+
...(source.stopped ? { stopped: true } : {}),
|
|
338
|
+
...(source.askedBy ? { askedBy: source.askedBy } : {}),
|
|
258
339
|
})
|
|
259
340
|
} else if (source?.kind === SOURCE_KIND && source.role === 'kickoff' && rt.roomOf(id)) {
|
|
260
341
|
queueRoomMessage(id, { text: '', kickoff: true })
|
|
@@ -295,8 +376,11 @@ export function install(rt) {
|
|
|
295
376
|
break
|
|
296
377
|
}
|
|
297
378
|
case 'assistant/message': {
|
|
298
|
-
const
|
|
379
|
+
const content = event.data?.message?.content
|
|
380
|
+
const text = textOf(content)
|
|
299
381
|
if (text !== '') entry.texts.push(text)
|
|
382
|
+
entry.lastText = text
|
|
383
|
+
entry.lastCalled = Array.isArray(content) && content.some(block => block?.type === 'tool-call')
|
|
300
384
|
break
|
|
301
385
|
}
|
|
302
386
|
case 'turn/end': {
|
|
@@ -304,6 +388,7 @@ export function install(rt) {
|
|
|
304
388
|
const info = entry.info
|
|
305
389
|
const joined = entry.joined ?? []
|
|
306
390
|
const texts = entry.texts
|
|
391
|
+
const { lastText, lastCalled } = entry
|
|
307
392
|
// A turn stopped for a switch goes on in the next part, which answers instead.
|
|
308
393
|
const switched = entry.switched === true
|
|
309
394
|
entry.turn = undefined
|
|
@@ -314,20 +399,37 @@ export function install(rt) {
|
|
|
314
399
|
entry.since = undefined
|
|
315
400
|
entry.tool = undefined
|
|
316
401
|
entry.calls = undefined
|
|
402
|
+
entry.lastText = undefined
|
|
403
|
+
entry.lastCalled = undefined
|
|
317
404
|
const reason = event.data?.reason
|
|
318
405
|
const failure = reason?.kind === 'error' && !switched ? turnFailure(reason.error) : undefined
|
|
319
406
|
if (failure !== undefined) noteTurnFailure(id, failure)
|
|
320
407
|
if (switched) break
|
|
408
|
+
// A model that writes its tool call as text ends the turn as if it were done,
|
|
409
|
+
// though nothing ran. It gets one more turn on the same trigger, so a waiting
|
|
410
|
+
// round or sender gets the real answer; a second time, the text goes on without
|
|
411
|
+
// the call.
|
|
412
|
+
const wroteCall = failure === undefined && reason?.kind === 'completed' && lastCalled !== true && writesToolCall(lastText)
|
|
413
|
+
const retry = wroteCall && info?.retried !== true && !(info?.role === 'room' && stoppedRoomTurns.has(info.exchangeId))
|
|
414
|
+
if (retry) void retryWrittenCall(id, info).catch(error => rt.warn('ds-bot: asking %s to run its written tool call failed: %s', id, error))
|
|
415
|
+
const replyText = () => {
|
|
416
|
+
const reply = (wroteCall ? texts.map(stripWrittenCalls) : texts).join('\n\n').trim()
|
|
417
|
+
return wroteCall && reply === '' ? 'Tried to run a tool but wrote the call as text, so it did not run.' : reply
|
|
418
|
+
}
|
|
321
419
|
// Whoever the turn answers gets the same three-way rule: a failure notice,
|
|
322
420
|
// the turn's text (or a stopped-before-answer line), or nothing.
|
|
323
421
|
const answer = (source, reply) => {
|
|
324
422
|
if (!expectsReply(source) || answersUser(source)) return
|
|
325
423
|
const name = rt.botOf(rt.selfOf(id))?.name ?? 'The other Bot'
|
|
424
|
+
// A sender that wrote from a group gets the reply in its Session there, while
|
|
425
|
+
// it is still a member.
|
|
426
|
+
const back = rt.roomOf(source.senderRoomId)?.sessions?.[source.senderSessionId]
|
|
326
427
|
if (failure !== undefined) {
|
|
327
428
|
// The sender was told a reply comes later; without this it would wait forever.
|
|
328
429
|
void rt.deliver({
|
|
329
430
|
fromId: rt.selfOf(id),
|
|
330
431
|
toId: source.senderSessionId,
|
|
432
|
+
...(back ? { sessionId: back } : {}),
|
|
331
433
|
text: `${name} could not answer your message: its model call failed (${failureLine(failure)}). Tell the user if it matters; do not retry right away.`,
|
|
332
434
|
role: 'reply',
|
|
333
435
|
hop: (source.hop ?? 0) + 1,
|
|
@@ -339,6 +441,7 @@ export function install(rt) {
|
|
|
339
441
|
void rt.deliver({
|
|
340
442
|
fromId: rt.selfOf(id),
|
|
341
443
|
toId: source.senderSessionId,
|
|
444
|
+
...(back ? { sessionId: back } : {}),
|
|
342
445
|
text: reply !== '' ? reply : `${name} was stopped before it could answer your message.`,
|
|
343
446
|
role: 'reply',
|
|
344
447
|
hop: (source.hop ?? 0) + 1,
|
|
@@ -346,20 +449,28 @@ export function install(rt) {
|
|
|
346
449
|
}).catch(error => rt.warn('ds-bot: reply delivery failed: %s', error))
|
|
347
450
|
}
|
|
348
451
|
}
|
|
349
|
-
|
|
452
|
+
// A retried turn answers the same waiter or sender, so this one passes nothing on.
|
|
453
|
+
if (!retry && info?.role === 'room') {
|
|
350
454
|
const waiter = roomWaiters.get(info.exchangeId)
|
|
351
455
|
roomWaiters.delete(info.exchangeId)
|
|
352
456
|
const stopped = stoppedRoomTurns.delete(info.exchangeId)
|
|
353
457
|
if (waiter !== undefined) {
|
|
354
458
|
if (failure !== undefined) waiter.fail(failure)
|
|
355
|
-
else waiter.resolve(reason?.kind === 'aborted' ? null :
|
|
459
|
+
else waiter.resolve(reason?.kind === 'aborted' ? null : replyText())
|
|
356
460
|
} else if (!stopped && reason?.kind !== 'aborted') {
|
|
357
461
|
// The round moved on without this member; how its turn ends still reaches the group.
|
|
358
462
|
if (failure !== undefined) void rt.lateRoomFailure?.(id, info, failure)
|
|
359
|
-
else void rt.lateRoomReply?.(id, info,
|
|
463
|
+
else void rt.lateRoomReply?.(id, info, replyText())
|
|
464
|
+
}
|
|
465
|
+
} else if (!retry && info !== undefined) {
|
|
466
|
+
// A group Session speaks only in its group: the turn that takes a reply to a
|
|
467
|
+
// message it sent from there posts what it ends with.
|
|
468
|
+
if (rt.groupOwners.has(id) && (info.role === 'reply' || info.role === 'retry')) {
|
|
469
|
+
if (failure !== undefined) void rt.lateRoomFailure?.(id, info, failure)
|
|
470
|
+
else if (reason?.kind !== 'aborted') void rt.lateRoomReply?.(id, info, replyText())
|
|
471
|
+
} else {
|
|
472
|
+
answer(info, replyText())
|
|
360
473
|
}
|
|
361
|
-
} else if (info !== undefined) {
|
|
362
|
-
answer(info, texts.join('\n\n').trim())
|
|
363
474
|
}
|
|
364
475
|
for (const request of joined) {
|
|
365
476
|
// A failed or stopped turn closes the request like any other; a finished
|
|
@@ -376,6 +487,7 @@ export function install(rt) {
|
|
|
376
487
|
role: 'request',
|
|
377
488
|
hop: request.info.hop ?? 0,
|
|
378
489
|
exchangeId: request.info.exchangeId,
|
|
490
|
+
...(request.info.senderRoomId ? { fromRoomId: request.info.senderRoomId } : {}),
|
|
379
491
|
deferred: true,
|
|
380
492
|
}).catch(error => rt.warn('ds-bot: deferred request delivery failed: %s', error))
|
|
381
493
|
}
|
|
@@ -384,5 +496,5 @@ export function install(rt) {
|
|
|
384
496
|
}
|
|
385
497
|
})
|
|
386
498
|
|
|
387
|
-
Object.assign(rt, { live, liveOf, roomWaiters, roomProgress, roomInbox, activityView, workView, callDetail, stopRoomTurn })
|
|
499
|
+
Object.assign(rt, { live, liveOf, roomWaiters, roomProgress, roomInbox, activityView, workView, roomWorkView, callDetail, stopRoomTurn })
|
|
388
500
|
}
|
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
|
}
|