72 lines
2.7 KiB
TypeScript
72 lines
2.7 KiB
TypeScript
/**
|
|
* Durable projection state for dynamic runtime context.
|
|
* @module @deepseek-ai/dsh-agent-loop/runtime-context
|
|
*/
|
|
|
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
|
|
import { isReplacementSurfaceEvent } from '@deepseek-ai/dsh-session'
|
|
import type { Context } from 'cordis'
|
|
|
|
const SOURCE = '@deepseek-ai/dsh-system-prompt'
|
|
const CLEARED = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'
|
|
|
|
function isOwned(message: UserMessage): boolean {
|
|
return message.source.kind === 'plugin' && message.source.plugin === SOURCE
|
|
}
|
|
|
|
function textOf(message: UserMessage): string | undefined {
|
|
const [block] = message.content
|
|
return message.content.length === 1 && block?.type === 'text' ? block.text : undefined
|
|
}
|
|
|
|
/** Tracks the last retained runtime-context snapshot without owning its commit. */
|
|
export class RuntimeContextProjection {
|
|
/** `undefined` means no snapshot ever existed; `null` means none is retained. */
|
|
private retained: { seq: number; text: string | undefined } | null | undefined
|
|
|
|
/**
|
|
* Restore projection state once, then follow authoritative session events.
|
|
* @param ctx - agent-scoped event context.
|
|
* @param session - session receiving projected messages.
|
|
*/
|
|
constructor(ctx: Context, session: Session) {
|
|
const surface = new Set(session.surface.nodes)
|
|
for (let index = session.events.length - 1; index >= 0; index -= 1) {
|
|
const event = session.events[index]
|
|
if (event?.type !== 'user/message' || !isOwned(event.data)) continue
|
|
this.retained ??= null
|
|
if (surface.has(event.seq)) {
|
|
this.retained = { seq: event.seq, text: textOf(event.data) }
|
|
break
|
|
}
|
|
}
|
|
|
|
ctx.on('session/event', (subject, event) => {
|
|
if (subject !== session) return
|
|
if (event.type === 'user/message' && isOwned(event.data)) {
|
|
this.retained = { seq: event.seq, text: textOf(event.data) }
|
|
} else if (this.retained
|
|
&& isReplacementSurfaceEvent(event)
|
|
&& event.sourceEventSeqs?.includes(this.retained.seq) === true) {
|
|
this.retained = null
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Create an uncommitted snapshot only when the retained value differs.
|
|
* @param current - fully rendered dynamic context.
|
|
* @returns a candidate user message, or `undefined` when no update is needed.
|
|
*/
|
|
project(current: string): UserMessage | undefined {
|
|
if (this.retained === undefined && current.length === 0) return
|
|
const snapshot = current.length === 0 ? CLEARED : current
|
|
if (this.retained?.text === snapshot) return
|
|
return createUserMessage({
|
|
content: [{ type: 'text', text: snapshot }],
|
|
source: { kind: 'plugin', plugin: SOURCE },
|
|
})
|
|
}
|
|
}
|