dsh-diary-memory 0.1.1 → 0.1.2
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 +23 -10
- package/lib/client/choices.d.ts +53 -0
- package/lib/client/choices.js +51 -0
- package/lib/client/choices.js.map +1 -0
- package/lib/client/controller.d.ts +11 -3
- package/lib/client/controller.js +45 -6
- package/lib/client/controller.js.map +1 -1
- package/lib/client/index.js +5 -2
- package/lib/client/index.js.map +1 -1
- package/lib/client/page.js +6 -1
- package/lib/client/page.js.map +1 -1
- package/lib/client.js +165 -9
- package/lib/client.js.map +3 -3
- package/lib/config.d.ts +5 -1
- package/lib/config.js +7 -1
- package/lib/config.js.map +1 -1
- package/lib/diary-runtime.d.ts +10 -0
- package/lib/diary-runtime.js +65 -0
- package/lib/diary-runtime.js.map +1 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/memory-store.d.ts +6 -3
- package/lib/memory-store.js +6 -3
- package/lib/memory-store.js.map +1 -1
- package/lib/orchestrator.js +4 -66
- package/lib/orchestrator.js.map +1 -1
- package/lib/session-memory.d.ts +20 -0
- package/lib/session-memory.js +161 -0
- package/lib/session-memory.js.map +1 -0
- package/lib/session-selection.d.ts +1 -0
- package/lib/session-selection.js +6 -0
- package/lib/session-selection.js.map +1 -0
- package/lib/sqlite.d.ts +25 -0
- package/lib/sqlite.js +64 -0
- package/lib/sqlite.js.map +1 -0
- package/lib/tools.d.ts +1 -0
- package/lib/tools.js +3 -0
- package/lib/tools.js.map +1 -1
- package/package.json +21 -4
- package/src/client/choices.ts +66 -0
- package/src/client/controller.ts +34 -7
- package/src/client/index.ts +7 -2
- package/src/client/page.tsx +32 -0
- package/src/config.ts +7 -1
- package/src/diary-runtime.ts +62 -0
- package/src/index.ts +2 -0
- package/src/memory-store.ts +10 -6
- package/src/orchestrator.ts +4 -57
- package/src/session-memory.ts +151 -0
- package/src/session-selection.ts +4 -0
- package/src/sqlite.ts +78 -0
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
import { symbols } from '@deepseek-ai/cordis'
|
|
3
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
5
|
+
import type { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic'
|
|
6
|
+
import type {} from '@deepseek-ai/dsh-agent-preset-registry'
|
|
7
|
+
import type { Message } from '@deepseek-ai/dsh-llm'
|
|
8
|
+
import type { DiarySummary } from './diary.js'
|
|
9
|
+
import { selectRoundRange } from './diary.js'
|
|
10
|
+
import { appendRecalledContext, prepareDiarySummary } from './diary-runtime.js'
|
|
11
|
+
import { DiaryOrchestrator } from './orchestrator.js'
|
|
12
|
+
import type { RetrievalService } from './retrieval-service.js'
|
|
13
|
+
import { registerRecall } from './tools.js'
|
|
14
|
+
import { createScope, scopeOf } from '@deepseek-ai/dsh-scope'
|
|
15
|
+
import type { Scope } from '@deepseek-ai/dsh-scope'
|
|
16
|
+
|
|
17
|
+
type NativeEngine = Pick<BasicCompactionEngine, 'compactIfNeeded' | 'compactRegion' | 'compactNow'> & {
|
|
18
|
+
readonly ctx: Context
|
|
19
|
+
summarize(input: { readonly messages: readonly Message[] }, agent: Agent, signal?: AbortSignal): Promise<DiarySummary>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Route the existing native engine by Session, without rebinding its preset. */
|
|
23
|
+
export class SessionMemory {
|
|
24
|
+
private readonly restorers = new Map<object, () => void>()
|
|
25
|
+
private readonly operations = new AsyncLocalStorage<{ agent: Agent; enabled: boolean }>()
|
|
26
|
+
private readonly jobs = new Set<Promise<unknown>>()
|
|
27
|
+
private readonly lifetime = new AbortController()
|
|
28
|
+
private closed = false
|
|
29
|
+
private readonly recallScopes = new Map<Agent, Scope>()
|
|
30
|
+
|
|
31
|
+
constructor(private readonly ctx: Context, private readonly memory: RetrievalService) {
|
|
32
|
+
ctx.effect(() => () => this.dispose(), 'diary-memory: existing sessions')
|
|
33
|
+
ctx.on('agent/created', async ({ agent }) => { this.attach(agent) })
|
|
34
|
+
ctx.on('agent/disposed', async ({ agent }) => {
|
|
35
|
+
const scope = this.recallScopes.get(agent)
|
|
36
|
+
this.recallScopes.delete(agent)
|
|
37
|
+
await scope?.dispose()
|
|
38
|
+
})
|
|
39
|
+
ctx.on('agent/pre-step', async ({ agent, messages, signal }, next) => {
|
|
40
|
+
const enabled = this.selected(agent)
|
|
41
|
+
const engine = this.attach(agent)
|
|
42
|
+
if (enabled && engine && !(engine instanceof DiaryOrchestrator) && !this.recallScopes.has(agent)) {
|
|
43
|
+
const key = scopeOf(agent.ctx)
|
|
44
|
+
if (!key) throw new Error('指定会话缺少原生 Agent scope')
|
|
45
|
+
const scope = createScope(ctx, key)
|
|
46
|
+
registerRecall(scope.ctx)
|
|
47
|
+
this.recallScopes.set(agent, scope)
|
|
48
|
+
}
|
|
49
|
+
if (!enabled) {
|
|
50
|
+
const scope = this.recallScopes.get(agent)
|
|
51
|
+
this.recallScopes.delete(agent)
|
|
52
|
+
await scope?.dispose()
|
|
53
|
+
}
|
|
54
|
+
const decision = await next()
|
|
55
|
+
// The companion preset already owns its recall middleware.
|
|
56
|
+
if (!enabled || engine instanceof DiaryOrchestrator) return decision
|
|
57
|
+
return appendRecalledContext(ctx, memory, decision, messages, signal)
|
|
58
|
+
}, { prepend: true })
|
|
59
|
+
for (const agent of ctx.agents.list()) this.attach(agent)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private selected(agent: Agent): boolean { return !this.closed && this.memory.config.sessionIds.includes(agent.session.id) }
|
|
63
|
+
|
|
64
|
+
attach(agent: Agent): BasicCompactionEngine | undefined {
|
|
65
|
+
const provider = this.ctx.agentPresets.serviceFor(agent, 'compaction')
|
|
66
|
+
if (!provider) {
|
|
67
|
+
if (this.selected(agent)) throw new Error('指定会话的预设没有原生压缩器,无法接入日记记忆')
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
if (this.closed) return provider as BasicCompactionEngine
|
|
71
|
+
// Cordis proxies expose their original instance through this public symbol.
|
|
72
|
+
const engine = ((provider as unknown as Record<symbol, unknown>)[symbols.original] ?? provider) as NativeEngine
|
|
73
|
+
if (engine instanceof DiaryOrchestrator || this.restorers.has(engine)) return provider as BasicCompactionEngine
|
|
74
|
+
const descriptors = new Map<string, PropertyDescriptor | undefined>()
|
|
75
|
+
const replacements = new Map<string, unknown>()
|
|
76
|
+
const bridge = this
|
|
77
|
+
const enabled = (owner: Agent) => {
|
|
78
|
+
const operation = bridge.operations.getStore()
|
|
79
|
+
return operation?.agent === owner ? operation.enabled : bridge.selected(owner)
|
|
80
|
+
}
|
|
81
|
+
const replace = <K extends 'compactIfNeeded' | 'compactRegion' | 'compactNow' | 'summarize'>(name: K, method: NativeEngine[K]) => {
|
|
82
|
+
descriptors.set(name, Object.getOwnPropertyDescriptor(engine, name))
|
|
83
|
+
replacements.set(name, method)
|
|
84
|
+
Object.defineProperty(engine, name, { configurable: true, writable: true, value: method })
|
|
85
|
+
}
|
|
86
|
+
const summarize = engine.summarize
|
|
87
|
+
replace('summarize', function (this: NativeEngine, input, owner, signal) {
|
|
88
|
+
if (!enabled(owner)) return summarize.call(this, input, owner, signal)
|
|
89
|
+
return prepareDiarySummary(this.ctx, bridge.memory, input, owner, AbortSignal.any([bridge.lifetime.signal, ...signal ? [signal] : []]))
|
|
90
|
+
})
|
|
91
|
+
const compactIfNeeded = engine.compactIfNeeded
|
|
92
|
+
replace('compactIfNeeded', function (this: NativeEngine, owner, trigger, signal) {
|
|
93
|
+
const selected = enabled(owner)
|
|
94
|
+
return bridge.run(owner, selected, async () => {
|
|
95
|
+
if (!selected) return compactIfNeeded.call(this, owner, trigger, signal)
|
|
96
|
+
await bridge.memory.reconcile(owner.session)
|
|
97
|
+
signal.throwIfAborted()
|
|
98
|
+
const config = bridge.memory.config
|
|
99
|
+
if (trigger === 'pressure' && bridge.memory.stateOf(owner.session).completedRounds >= config.compressionRounds) {
|
|
100
|
+
const range = selectRoundRange(owner.session, config.retainRounds)
|
|
101
|
+
if (range) return this.compactRegion(range.start, range.end, owner, signal)
|
|
102
|
+
}
|
|
103
|
+
return compactIfNeeded.call(this, owner, trigger, signal)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
const compactRegion = engine.compactRegion
|
|
107
|
+
replace('compactRegion', function (this: NativeEngine, start, end, owner, signal) {
|
|
108
|
+
const selected = enabled(owner)
|
|
109
|
+
return bridge.run(owner, selected, async () => {
|
|
110
|
+
if (!selected) return compactRegion.call(this, start, end, owner, signal)
|
|
111
|
+
try { return await compactRegion.call(this, start, end, owner, signal) }
|
|
112
|
+
finally { await bridge.memory.reconcile(owner.session) }
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
const compactNow = engine.compactNow
|
|
116
|
+
replace('compactNow', function (this: NativeEngine, owner, signal, sourceCommandId) {
|
|
117
|
+
const selected = enabled(owner)
|
|
118
|
+
return bridge.run(owner, selected, async () => {
|
|
119
|
+
if (!selected) return compactNow.call(this, owner, signal, sourceCommandId)
|
|
120
|
+
try { return await compactNow.call(this, owner, signal, sourceCommandId) }
|
|
121
|
+
finally { await bridge.memory.reconcile(owner.session) }
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
this.restorers.set(engine, () => {
|
|
125
|
+
for (const [name, descriptor] of descriptors) {
|
|
126
|
+
if (Object.getOwnPropertyDescriptor(engine, name)?.value !== replacements.get(name)) continue
|
|
127
|
+
if (descriptor) Object.defineProperty(engine, name, descriptor)
|
|
128
|
+
else Reflect.deleteProperty(engine, name)
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
return provider as BasicCompactionEngine
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private run<T>(agent: Agent, enabled: boolean, operation: () => Promise<T>): Promise<T> {
|
|
135
|
+
const job = this.memory.track(() => this.operations.run({ agent, enabled }, operation))
|
|
136
|
+
this.jobs.add(job)
|
|
137
|
+
void job.then(() => this.jobs.delete(job), () => this.jobs.delete(job))
|
|
138
|
+
return job
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async dispose(): Promise<void> {
|
|
142
|
+
if (this.closed) return
|
|
143
|
+
this.closed = true
|
|
144
|
+
this.lifetime.abort(new Error('指定会话的日记记忆已关闭'))
|
|
145
|
+
await Promise.allSettled([...this.jobs])
|
|
146
|
+
for (const restore of this.restorers.values()) restore()
|
|
147
|
+
this.restorers.clear()
|
|
148
|
+
await Promise.allSettled([...this.recallScopes.values()].map(scope => scope.dispose()))
|
|
149
|
+
this.recallScopes.clear()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export function validateSessionIds(value: unknown): string[] {
|
|
2
|
+
if (!Array.isArray(value) || value.length > 1000 || value.some(id => typeof id !== 'string' || !id.length || id.length > 256 || /[\r\n\0]/.test(id))) throw new Error('指定会话必须是至多 1000 个有效会话 ID 的数组')
|
|
3
|
+
return [...new Set(value as string[])]
|
|
4
|
+
}
|
package/src/sqlite.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import type { DatabaseSync, SQLInputValue } from 'node:sqlite'
|
|
3
|
+
|
|
4
|
+
export type SqliteDriver = 'auto' | 'builtin' | 'native'
|
|
5
|
+
export interface SqliteStatement {
|
|
6
|
+
run(...parameters: SQLInputValue[]): { changes: number | bigint; lastInsertRowid: number | bigint }
|
|
7
|
+
get(...parameters: SQLInputValue[]): Record<string, unknown> | undefined
|
|
8
|
+
all(...parameters: SQLInputValue[]): Record<string, unknown>[]
|
|
9
|
+
iterate(...parameters: SQLInputValue[]): IterableIterator<Record<string, unknown>>
|
|
10
|
+
}
|
|
11
|
+
export interface SqliteConnection {
|
|
12
|
+
readonly open: boolean
|
|
13
|
+
exec(sql: string): unknown
|
|
14
|
+
prepare(sql: string): SqliteStatement
|
|
15
|
+
pragma(sql: string, options?: { simple: boolean }): unknown
|
|
16
|
+
transaction<T>(operation: () => T): () => T
|
|
17
|
+
close(): unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url)
|
|
21
|
+
|
|
22
|
+
/** Use the same SQLite file format without requiring an Electron-specific addon. */
|
|
23
|
+
function builtinDatabase(filename: string): SqliteConnection {
|
|
24
|
+
const { DatabaseSync: Database } = require('node:sqlite') as { DatabaseSync: typeof DatabaseSync }
|
|
25
|
+
const database = new Database(filename)
|
|
26
|
+
let open = true
|
|
27
|
+
let transactionId = 0
|
|
28
|
+
const normalize = (row: Record<string, unknown> | undefined) => row === undefined ? undefined
|
|
29
|
+
: Object.fromEntries(Object.entries(row).map(([key, value]) => [key, value instanceof Uint8Array ? Buffer.from(value) : value]))
|
|
30
|
+
return {
|
|
31
|
+
get open() { return open },
|
|
32
|
+
exec: sql => database.exec(sql),
|
|
33
|
+
prepare(sql) {
|
|
34
|
+
const statement = database.prepare(sql)
|
|
35
|
+
return {
|
|
36
|
+
run: (...parameters) => statement.run(...parameters),
|
|
37
|
+
get: (...parameters) => normalize(statement.get(...parameters)),
|
|
38
|
+
all: (...parameters) => statement.all(...parameters).map(row => normalize(row)!),
|
|
39
|
+
*iterate(...parameters) { for (const row of statement.iterate(...parameters)) yield normalize(row)! },
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
pragma(sql, options) {
|
|
43
|
+
const rows = database.prepare(`PRAGMA ${sql}`).all()
|
|
44
|
+
return options?.simple ? Object.values(rows[0] ?? {})[0] : rows
|
|
45
|
+
},
|
|
46
|
+
transaction: operation => () => {
|
|
47
|
+
// Savepoints also give nested synchronous operations rollback isolation.
|
|
48
|
+
const point = `diary_${++transactionId}`
|
|
49
|
+
database.exec(`SAVEPOINT ${point}`)
|
|
50
|
+
try {
|
|
51
|
+
const result = operation()
|
|
52
|
+
if (result instanceof Promise) throw new TypeError('SQLite 事务必须同步完成')
|
|
53
|
+
database.exec(`RELEASE ${point}`)
|
|
54
|
+
return result
|
|
55
|
+
} catch (error) {
|
|
56
|
+
database.exec(`ROLLBACK TO ${point}; RELEASE ${point}`)
|
|
57
|
+
throw error
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
close() { database.close(); open = false },
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function openDatabase(filename: string, driver: SqliteDriver = 'auto'): { db: SqliteConnection; backend: 'better-sqlite3' | 'node:sqlite' } {
|
|
65
|
+
if (driver !== 'builtin') {
|
|
66
|
+
try {
|
|
67
|
+
const Database = require('better-sqlite3') as typeof import('better-sqlite3')
|
|
68
|
+
return { db: new Database(filename) as unknown as SqliteConnection, backend: 'better-sqlite3' }
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const failure = error as NodeJS.ErrnoException
|
|
71
|
+
const unavailable = failure.code === 'MODULE_NOT_FOUND' || failure.code === 'ERR_DLOPEN_FAILED'
|
|
72
|
+
|| failure.message.startsWith('Could not locate the bindings file.')
|
|
73
|
+
// Disk, SQL, permission and corruption failures must never select another backend.
|
|
74
|
+
if (driver === 'native' || !unavailable) throw error
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { db: builtinDatabase(filename), backend: 'node:sqlite' }
|
|
78
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -6,6 +6,10 @@ export const inject = ['tools', 'diaryMemory']
|
|
|
6
6
|
|
|
7
7
|
/** Model-visible read-only recall, scoped by the companion preset and namespace. */
|
|
8
8
|
export function apply(ctx: Context): void {
|
|
9
|
+
registerRecall(ctx)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function registerRecall(ctx: Context): void {
|
|
9
13
|
ctx.tools.register({
|
|
10
14
|
name: 'diary_recall',
|
|
11
15
|
description: '想起我们之前的经历、称呼、偏好、情绪变化、约定或我自己的反思。输入自然语言,会检索历史日记并按相关性重排。日记中的主观感受和不确定解释不等于已确认事实。',
|