@pikku/mongodb 0.12.20 → 0.12.22

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.
@@ -1,632 +0,0 @@
1
- import type {
2
- AgentMessage,
3
- AgentRunState,
4
- AgentThread,
5
- } from '@pikku/core/agent'
6
- import type { AgentRunScore } from '@pikku/core/agent-scorer'
7
- import type {
8
- AgentRunStateService,
9
- AgentStorageService,
10
- CreateRunInput,
11
- SaveScoreInput,
12
- } from '@pikku/core/services'
13
- import type { Db, Collection } from 'mongodb'
14
-
15
- interface AgentThreadDoc {
16
- _id: string
17
- resourceId: string
18
- title: string | null
19
- metadata: any | null
20
- createdAt: Date
21
- updatedAt: Date
22
- }
23
-
24
- interface AgentMessageDoc {
25
- _id: string
26
- threadId: string
27
- role: string
28
- content: any | null
29
- createdAt: Date
30
- }
31
-
32
- interface AgentToolCallDoc {
33
- _id: string
34
- threadId: string
35
- messageId: string
36
- runId: string | null
37
- toolName: string
38
- args: any
39
- result: string | null
40
- approvalStatus: string | null
41
- approvalType: string | null
42
- agentRunId: string | null
43
- displayToolName: string | null
44
- displayArgs: any | null
45
- createdAt: Date
46
- }
47
-
48
- interface AgentWorkingMemoryDoc {
49
- id: string
50
- scope: string
51
- data: any
52
- updatedAt: Date
53
- }
54
-
55
- interface AgentRunScoreDoc {
56
- _id: string
57
- runId: string
58
- scorerName: string
59
- score: number
60
- reason: string | null
61
- metadata: any | null
62
- createdAt: Date
63
- }
64
-
65
- interface AgentRunDoc {
66
- _id: string
67
- agentName: string
68
- threadId: string
69
- resourceId: string
70
- status: string
71
- errorMessage: string | null
72
- suspendReason: string | null
73
- missingRpcs: any | null
74
- usageInputTokens: number
75
- usageOutputTokens: number
76
- usageModel: string
77
- createdAt: Date
78
- updatedAt: Date
79
- }
80
-
81
- export class MongoDBAgentStorageService
82
- implements AgentStorageService, AgentRunStateService
83
- {
84
- private initialized = false
85
- private threads!: Collection<AgentThreadDoc>
86
- private agentMessages!: Collection<AgentMessageDoc>
87
- private toolCalls!: Collection<AgentToolCallDoc>
88
- private workingMemory!: Collection<AgentWorkingMemoryDoc>
89
- private agentRuns!: Collection<AgentRunDoc>
90
- private agentRunScores!: Collection<AgentRunScoreDoc>
91
-
92
- constructor(private db: Db) {}
93
-
94
- public async init(): Promise<void> {
95
- if (this.initialized) return
96
-
97
- this.threads = this.db.collection<AgentThreadDoc>('agent_threads')
98
- this.agentMessages = this.db.collection<AgentMessageDoc>('agent_message')
99
- this.toolCalls = this.db.collection<AgentToolCallDoc>('agent_tool_call')
100
- this.workingMemory = this.db.collection<AgentWorkingMemoryDoc>(
101
- 'agent_working_memory'
102
- )
103
- this.agentRuns = this.db.collection<AgentRunDoc>('agent_run')
104
- this.agentRunScores =
105
- this.db.collection<AgentRunScoreDoc>('agent_run_score')
106
-
107
- await this.threads.createIndex({ resourceId: 1 })
108
-
109
- await this.agentMessages.createIndex({ threadId: 1, createdAt: 1 })
110
-
111
- await this.toolCalls.createIndex({ threadId: 1 })
112
- await this.toolCalls.createIndex({ messageId: 1 })
113
-
114
- await this.workingMemory.createIndex({ id: 1, scope: 1 }, { unique: true })
115
-
116
- await this.agentRuns.createIndex({ threadId: 1, createdAt: 1 })
117
-
118
- await this.agentRunScores.createIndex({ runId: 1, createdAt: 1 })
119
-
120
- this.initialized = true
121
- }
122
-
123
- async createThread(
124
- resourceId: string,
125
- options?: {
126
- threadId?: string
127
- title?: string
128
- metadata?: Record<string, unknown>
129
- }
130
- ): Promise<AgentThread> {
131
- const id = options?.threadId ?? crypto.randomUUID()
132
- const now = new Date()
133
-
134
- await this.threads.insertOne({
135
- _id: id,
136
- resourceId,
137
- title: options?.title ?? null,
138
- metadata: options?.metadata ?? null,
139
- createdAt: now,
140
- updatedAt: now,
141
- })
142
-
143
- return {
144
- id,
145
- resourceId,
146
- title: options?.title,
147
- metadata: options?.metadata,
148
- createdAt: now,
149
- updatedAt: now,
150
- }
151
- }
152
-
153
- async getThread(threadId: string): Promise<AgentThread> {
154
- const row = await this.threads.findOne({ _id: threadId })
155
-
156
- if (!row) {
157
- throw new Error(`Thread not found: ${threadId}`)
158
- }
159
-
160
- return {
161
- id: row._id,
162
- resourceId: row.resourceId,
163
- title: row.title ?? undefined,
164
- metadata: row.metadata ?? undefined,
165
- createdAt: new Date(row.createdAt),
166
- updatedAt: new Date(row.updatedAt),
167
- }
168
- }
169
-
170
- async getThreads(resourceId: string): Promise<AgentThread[]> {
171
- const result = await this.threads
172
- .find({ resourceId })
173
- .sort({ updatedAt: -1 })
174
- .toArray()
175
-
176
- return result.map((row) => ({
177
- id: row._id,
178
- resourceId: row.resourceId,
179
- title: row.title ?? undefined,
180
- metadata: row.metadata ?? undefined,
181
- createdAt: new Date(row.createdAt),
182
- updatedAt: new Date(row.updatedAt),
183
- }))
184
- }
185
-
186
- async deleteThread(threadId: string): Promise<void> {
187
- await this.toolCalls.deleteMany({ threadId })
188
- await this.agentMessages.deleteMany({ threadId })
189
- await this.agentRuns.deleteMany({ threadId })
190
- await this.threads.deleteOne({ _id: threadId })
191
- }
192
-
193
- async getMessages(
194
- threadId: string,
195
- options?: { lastN?: number; cursor?: string }
196
- ): Promise<AgentMessage[]> {
197
- let filter: Record<string, any> = { threadId }
198
-
199
- if (options?.cursor) {
200
- const cursorRow = await this.agentMessages.findOne({
201
- _id: options.cursor,
202
- })
203
- if (cursorRow) {
204
- filter.createdAt = { $lt: cursorRow.createdAt }
205
- }
206
- }
207
-
208
- let msgResult
209
- if (options?.cursor || options?.lastN) {
210
- const innerResult = await this.agentMessages
211
- .find(filter)
212
- .sort({ createdAt: -1 })
213
- .limit(options?.lastN ?? 50)
214
- .toArray()
215
- innerResult.reverse()
216
- msgResult = innerResult
217
- } else {
218
- msgResult = await this.agentMessages
219
- .find(filter)
220
- .sort({ createdAt: 1 })
221
- .toArray()
222
- }
223
-
224
- const tcResult = await this.toolCalls
225
- .find({ threadId })
226
- .sort({ createdAt: 1 })
227
- .toArray()
228
-
229
- const tcByMessage = new Map<string, (typeof tcResult)[number][]>()
230
- for (const tc of tcResult) {
231
- const msgId = tc.messageId
232
- if (!tcByMessage.has(msgId)) tcByMessage.set(msgId, [])
233
- tcByMessage.get(msgId)!.push(tc)
234
- }
235
-
236
- const messages: AgentMessage[] = []
237
- for (const row of msgResult) {
238
- let parsedContent: AgentMessage['content'] = row.content ?? undefined
239
- if (typeof row.content === 'string') {
240
- try {
241
- const parsed = JSON.parse(row.content)
242
- if (Array.isArray(parsed)) {
243
- parsedContent = parsed
244
- } else if (typeof parsed === 'string') {
245
- parsedContent = parsed
246
- }
247
- } catch {
248
- // Not JSON, use raw string
249
- }
250
- }
251
-
252
- const msg: AgentMessage = {
253
- id: row._id,
254
- role: row.role as AgentMessage['role'],
255
- content: parsedContent,
256
- createdAt: new Date(row.createdAt),
257
- }
258
-
259
- const tcs = tcByMessage.get(msg.id)
260
- if (tcs?.length) {
261
- msg.toolCalls = tcs.map((tc) => ({
262
- id: tc._id,
263
- name: tc.toolName,
264
- args: tc.args as Record<string, unknown>,
265
- }))
266
-
267
- const completed = tcs.filter((tc) => tc.result != null)
268
- if (completed.length) {
269
- messages.push(msg)
270
- messages.push({
271
- id: `tool-results-${msg.id}`,
272
- role: 'tool',
273
- toolResults: completed.map((tc) => ({
274
- id: tc._id,
275
- name: tc.toolName,
276
- result: tc.result!,
277
- })),
278
- createdAt: msg.createdAt,
279
- })
280
- continue
281
- }
282
- }
283
-
284
- messages.push(msg)
285
- }
286
-
287
- return messages
288
- }
289
-
290
- async saveMessages(
291
- threadId: string,
292
- messages: AgentMessage[]
293
- ): Promise<void> {
294
- if (messages.length === 0) return
295
-
296
- const toolMessages = messages.filter((m) => m.role === 'tool')
297
- const nonToolMessages = messages.filter((m) => m.role !== 'tool')
298
-
299
- if (nonToolMessages.length > 0) {
300
- await this.agentMessages.insertMany(
301
- nonToolMessages.map((msg) => ({
302
- _id: msg.id,
303
- threadId,
304
- role: msg.role,
305
- content: msg.content != null ? JSON.stringify(msg.content) : null,
306
- createdAt: msg.createdAt ?? new Date(),
307
- }))
308
- )
309
- }
310
-
311
- const toolCallDocs = nonToolMessages.flatMap(
312
- (msg) => msg.toolCalls?.map((tc) => ({ ...tc, messageId: msg.id })) ?? []
313
- )
314
- if (toolCallDocs.length > 0) {
315
- await this.toolCalls.insertMany(
316
- toolCallDocs.map((tc) => ({
317
- _id: tc.id,
318
- threadId,
319
- messageId: tc.messageId,
320
- runId: null,
321
- toolName: tc.name,
322
- args: tc.args,
323
- result: null,
324
- approvalStatus: null,
325
- approvalType: null,
326
- agentRunId: null,
327
- displayToolName: null,
328
- displayArgs: null,
329
- createdAt: new Date(),
330
- }))
331
- )
332
- }
333
-
334
- for (const toolMsg of toolMessages) {
335
- if (!toolMsg.toolResults) continue
336
- for (const tr of toolMsg.toolResults) {
337
- await this.toolCalls.updateOne(
338
- { _id: tr.id },
339
- { $set: { result: tr.result } }
340
- )
341
- }
342
- }
343
-
344
- await this.threads.updateOne(
345
- { _id: threadId },
346
- { $set: { updatedAt: new Date() } }
347
- )
348
- }
349
-
350
- async getWorkingMemory(
351
- id: string,
352
- scope: 'resource' | 'thread'
353
- ): Promise<Record<string, unknown> | null> {
354
- const row = await this.workingMemory.findOne({ id, scope })
355
- if (!row) return null
356
- return row.data
357
- }
358
-
359
- async saveWorkingMemory(
360
- id: string,
361
- scope: 'resource' | 'thread',
362
- data: Record<string, unknown>
363
- ): Promise<void> {
364
- await this.workingMemory.updateOne(
365
- { id, scope },
366
- {
367
- $set: {
368
- data,
369
- updatedAt: new Date(),
370
- },
371
- $setOnInsert: {
372
- id,
373
- scope,
374
- },
375
- },
376
- { upsert: true }
377
- )
378
- }
379
-
380
- async createRun(run: CreateRunInput): Promise<string> {
381
- const runId = crypto.randomUUID()
382
-
383
- await this.agentRuns.insertOne({
384
- _id: runId,
385
- agentName: run.agentName,
386
- threadId: run.threadId,
387
- resourceId: run.resourceId,
388
- status: run.status,
389
- errorMessage: run.errorMessage ?? null,
390
- suspendReason: run.suspendReason ?? null,
391
- missingRpcs: run.missingRpcs ?? null,
392
- usageInputTokens: run.usage.inputTokens,
393
- usageOutputTokens: run.usage.outputTokens,
394
- usageModel: run.usage.model,
395
- createdAt: run.createdAt,
396
- updatedAt: run.updatedAt,
397
- })
398
-
399
- if (run.pendingApprovals?.length) {
400
- await this.setApprovals(runId, run.pendingApprovals)
401
- }
402
-
403
- return runId
404
- }
405
-
406
- async updateRun(
407
- runId: string,
408
- updates: Partial<AgentRunState>
409
- ): Promise<void> {
410
- const setValues: Record<string, any> = { updatedAt: new Date() }
411
-
412
- if (updates.status !== undefined) {
413
- setValues.status = updates.status
414
- }
415
- if (updates.errorMessage !== undefined) {
416
- setValues.errorMessage = updates.errorMessage
417
- }
418
- if (updates.suspendReason !== undefined) {
419
- setValues.suspendReason = updates.suspendReason
420
- }
421
- if (updates.missingRpcs !== undefined) {
422
- setValues.missingRpcs = updates.missingRpcs
423
- }
424
- if (updates.usage !== undefined) {
425
- setValues.usageInputTokens = updates.usage.inputTokens
426
- setValues.usageOutputTokens = updates.usage.outputTokens
427
- setValues.usageModel = updates.usage.model
428
- }
429
-
430
- await this.agentRuns.updateOne({ _id: runId }, { $set: setValues })
431
-
432
- if (updates.pendingApprovals !== undefined) {
433
- await this.toolCalls.updateMany(
434
- { runId, approvalStatus: { $ne: null } },
435
- {
436
- $set: {
437
- approvalStatus: null,
438
- runId: null,
439
- approvalType: null,
440
- agentRunId: null,
441
- displayToolName: null,
442
- displayArgs: null,
443
- },
444
- }
445
- )
446
-
447
- if (updates.pendingApprovals.length) {
448
- await this.setApprovals(runId, updates.pendingApprovals)
449
- }
450
- }
451
- }
452
-
453
- async getRun(runId: string): Promise<AgentRunState | null> {
454
- const row = await this.agentRuns.findOne({ _id: runId })
455
- if (!row) return null
456
-
457
- const approvals = await this.toolCalls
458
- .find({ runId, approvalStatus: 'pending' })
459
- .toArray()
460
-
461
- return this.mapRunRow(row, approvals)
462
- }
463
-
464
- async getRunsByThread(threadId: string): Promise<AgentRunState[]> {
465
- const result = await this.agentRuns
466
- .find({ threadId })
467
- .sort({ createdAt: -1 })
468
- .toArray()
469
-
470
- const runs: AgentRunState[] = []
471
- for (const row of result) {
472
- const approvals = await this.toolCalls
473
- .find({ runId: row._id, approvalStatus: 'pending' })
474
- .toArray()
475
- runs.push(this.mapRunRow(row, approvals))
476
- }
477
- return runs
478
- }
479
-
480
- private async setApprovals(
481
- runId: string,
482
- approvals: NonNullable<AgentRunState['pendingApprovals']>
483
- ): Promise<void> {
484
- for (const a of approvals) {
485
- if (a.type === 'agent-call') {
486
- await this.toolCalls.updateOne(
487
- { _id: a.toolCallId },
488
- {
489
- $set: {
490
- approvalStatus: 'pending',
491
- runId,
492
- approvalType: 'agent-call',
493
- agentRunId: a.agentRunId,
494
- displayToolName: a.displayToolName,
495
- displayArgs: a.displayArgs,
496
- },
497
- }
498
- )
499
- } else {
500
- await this.toolCalls.updateOne(
501
- { _id: a.toolCallId },
502
- {
503
- $set: {
504
- approvalStatus: 'pending',
505
- runId,
506
- approvalType: 'tool-call',
507
- },
508
- }
509
- )
510
- }
511
- }
512
- }
513
-
514
- private mapRunRow(row: any, approvalRows?: any[]): AgentRunState {
515
- const pendingApprovals = approvalRows?.length
516
- ? approvalRows.map((a: any) => {
517
- if (a.approvalType === 'agent-call') {
518
- return {
519
- type: 'agent-call' as const,
520
- toolCallId: a._id as string,
521
- agentName: a.toolName as string,
522
- agentRunId: a.agentRunId as string,
523
- displayToolName: a.displayToolName as string,
524
- displayArgs: a.displayArgs as unknown,
525
- }
526
- }
527
- return {
528
- type: 'tool-call' as const,
529
- toolCallId: a._id as string,
530
- toolName: a.toolName as string,
531
- args: a.args as unknown,
532
- }
533
- })
534
- : undefined
535
-
536
- return {
537
- runId: row._id as string,
538
- agentName: row.agentName as string,
539
- threadId: row.threadId as string,
540
- resourceId: row.resourceId as string,
541
- status: row.status as AgentRunState['status'],
542
- errorMessage: row.errorMessage ?? undefined,
543
- suspendReason: row.suspendReason as AgentRunState['suspendReason'],
544
- missingRpcs: row.missingRpcs ?? undefined,
545
- pendingApprovals,
546
- usage: {
547
- inputTokens: Number(row.usageInputTokens),
548
- outputTokens: Number(row.usageOutputTokens),
549
- model: row.usageModel as string,
550
- },
551
- createdAt: new Date(row.createdAt as string),
552
- updatedAt: new Date(row.updatedAt as string),
553
- }
554
- }
555
-
556
- async findRunByToolCallId(toolCallId: string): Promise<{
557
- run: AgentRunState
558
- approval: NonNullable<AgentRunState['pendingApprovals']>[number]
559
- } | null> {
560
- const tc = await this.toolCalls.findOne({
561
- _id: toolCallId,
562
- approvalStatus: 'pending',
563
- })
564
-
565
- if (!tc || !tc.runId) return null
566
-
567
- const run = await this.getRun(tc.runId)
568
- if (!run) return null
569
-
570
- let approval: NonNullable<AgentRunState['pendingApprovals']>[number]
571
- if (tc.approvalType === 'agent-call') {
572
- approval = {
573
- type: 'agent-call',
574
- toolCallId: tc._id,
575
- agentName: tc.toolName,
576
- agentRunId: tc.agentRunId!,
577
- displayToolName: tc.displayToolName!,
578
- displayArgs: tc.displayArgs as unknown,
579
- }
580
- } else {
581
- approval = {
582
- type: 'tool-call',
583
- toolCallId: tc._id,
584
- toolName: tc.toolName,
585
- args: tc.args as unknown,
586
- }
587
- }
588
-
589
- return { run, approval }
590
- }
591
-
592
- async resolveApproval(
593
- toolCallId: string,
594
- status: 'approved' | 'denied'
595
- ): Promise<boolean> {
596
- // Only the caller that moves the tool call off 'pending' has claimed it.
597
- const result = await this.toolCalls.updateOne(
598
- { _id: toolCallId, approvalStatus: 'pending' },
599
- { $set: { approvalStatus: status } }
600
- )
601
- return result.modifiedCount > 0
602
- }
603
-
604
- async saveScore(score: SaveScoreInput): Promise<void> {
605
- await this.agentRunScores.insertOne({
606
- _id: crypto.randomUUID(),
607
- runId: score.runId,
608
- scorerName: score.scorerName,
609
- score: score.score,
610
- reason: score.reason ?? null,
611
- metadata: score.metadata ?? null,
612
- createdAt: new Date(),
613
- })
614
- }
615
-
616
- async getScores(runId: string): Promise<AgentRunScore[]> {
617
- const rows = await this.agentRunScores
618
- .find({ runId })
619
- .sort({ createdAt: 1 })
620
- .toArray()
621
- return rows.map((row) => ({
622
- runId: row.runId,
623
- scorerName: row.scorerName,
624
- score: row.score,
625
- reason: row.reason ?? undefined,
626
- metadata: row.metadata ?? undefined,
627
- createdAt: row.createdAt,
628
- }))
629
- }
630
-
631
- public async close(): Promise<void> {}
632
- }