1
/**2
* Durable projection state for the two loop-owned surface messages the system3
* prompt plugin forms: the system prompt (surface node 0 and any in-history4
* replacement) and the dynamic runtime-context snapshot.5
* @module @deepseek-ai/dsh-agent-loop/runtime-context6
*/8
import { createSystemMessage, createUserMessage } from '@deepseek-ai/dsh-llm'9
import type { ContextFormed, ContextSnapshotSection, Message } from '@deepseek-ai/dsh-llm'10
import type { Session, SessionEvent, SessionSeq, SurfaceIntent, SystemMessage, UserMessage } from '@deepseek-ai/dsh-session'11
import { isReplacementSurfaceEvent } from '@deepseek-ai/dsh-session'12
import type { Context } from '@deepseek-ai/cordis'13
declare module '@deepseek-ai/dsh-llm' {14
interface MessageSourceMap {15
'runtime-context': { kind: 'runtime-context' } & ContextFormed16
}17
}19
const SOURCE = 'runtime-context'20
const CLEARED = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'22
function isOwned(message: UserMessage): boolean {23
return message.source.kind === SOURCE24
}26
function textOf(message: Message): string | undefined {27
const [block] = message.content28
return message.content.length === 1 && block?.type === 'text' ? block.text : undefined29
}31
/** One uncommitted system-prompt surface operation for request admission or reconciliation. */32
export interface SystemPromptCommit {33
/** Rendered prompt or empty content: an empty head records no prompt; empty tails are dormant. */34
message: SystemMessage35
/** `append` for a new system node, otherwise a replacement of one surviving system node. */36
intent: SurfaceIntent<'system/message'>37
}39
/** The request-series facts one prompt decision is made under. */40
export interface SystemPromptDecisionInput {41
/** Whether the prepared route for this attempt reads a later `system` message as the effective prompt. */42
inHistory: boolean43
/**44
* Whether prompt admission must consolidate: a pre-step listener declared a45
* new series, the surface changed since the last request, or assembled tools46
* changed on a route without tool-update support.47
*/48
startsSeries: boolean49
}51
/** Committed events from the newest backward; the restore scans stop at the first match. */52
function eventsNewestFirst(session: Session): readonly SessionEvent[] {53
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.54
return session.snapshotEvents().toReversed()55
}57
/**58
* Decides how a rendered system prompt reaches the surface without owning the59
* commit. The first prompt, even empty, reserves surface node 0.60
* A capable continuing series appends changed nonempty text after the61
* cached history. An incapable route, broken series, or cleared prompt instead62
* normalizes the first system node and empties later active nodes. Dormant empty63
* tails do not supply effective text or require repeated replacements.64
*/65
export class SystemPromptProjection {66
constructor(private readonly session: Session) {}68
/** The surviving `system/message` nodes in surface order. */69
private systemNodes(): { seq: SessionSeq; text: string | undefined }[] {70
const nodes: { seq: SessionSeq; text: string | undefined }[] = []71
for (const seq of this.session.surface.nodes) {72
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.73
const event = this.session.eventAt(seq)74
if (event?.type !== 'system/message') continue75
const content = event.data.message.content76
const text = content.length === 0 ? '' : textOf(event.data.message)77
nodes.push({ seq, text })78
}79
return nodes80
}82
/**83
* Reconcile effective text and retained nodes with the prepared route and series.84
* @param rendered - the fully rendered system prompt; `''` when none is active.85
* @param input - the route capability and series facts for this step.86
* @returns ordered per-node updates; an empty list means no update is needed.87
*/88
project(rendered: string, input: SystemPromptDecisionInput): SystemPromptCommit[] {89
const nodes = this.systemNodes()90
const head = nodes[0]91
if (head === undefined) {92
return [{ message: createSystemMessage(rendered), intent: { surfaceOp: 'append' } }]93
}94
const latest = nodes.findLast(node => node.text !== '') ?? head95
if (!input.inHistory || input.startsSeries || rendered.length === 0) {96
const updates = nodes.slice(1).filter(node => node.text !== '')97
.map(node => this.replace(node.seq, ''))98
if (head.text !== rendered) updates.push(this.replace(head.seq, rendered))99
return updates100
}101
if (latest.text === rendered) return []102
return [{ message: createSystemMessage(rendered), intent: { surfaceOp: 'append' } }]103
}105
private replace(seq: SessionSeq, text: string): SystemPromptCommit {106
return {107
message: createSystemMessage(text),108
intent: { surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq }, sourceEventSeqs: [seq] },109
}110
}111
}113
/** Tracks the last retained runtime-context snapshot without owning its commit. */114
export class RuntimeContextProjection {115
/** `undefined` means no snapshot ever existed; `null` means none is retained. */116
private retained: { seq: SessionSeq; text: string | undefined } | null | undefined118
/**119
* Restore projection state once, then follow authoritative session events.120
* @param ctx - agent-scoped event context.121
* @param session - session receiving projected messages.122
*/123
constructor(ctx: Context, session: Session) {124
const surface = new Set(session.surface.nodes)125
for (const event of eventsNewestFirst(session)) {126
if (event.type !== 'user/message' || !isOwned(event.data)) continue127
this.retained ??= null128
if (surface.has(event.seq)) {129
this.retained = { seq: event.seq, text: textOf(event.data) }130
break131
}132
}134
ctx.on('session/event', (subject, event) => {135
if (subject !== session) return136
if (event.type === 'user/message' && isOwned(event.data)) {137
this.retained = { seq: event.seq, text: textOf(event.data) }138
} else if (this.retained139
&& isReplacementSurfaceEvent(event)140
&& event.sourceEventSeqs?.includes(this.retained.seq) === true) {141
this.retained = null142
}143
})144
}146
/**147
* Create an uncommitted snapshot only when the retained value differs.148
* @param current - fully rendered dynamic context.149
* @param sections - named contributions that formed the current snapshot.150
* @returns a candidate user message, or `undefined` when no update is needed.151
*/152
project(current: string, sections: readonly ContextSnapshotSection[]): UserMessage | undefined {153
if (this.retained === undefined && current.length === 0) return154
const snapshot = current.length === 0 ? CLEARED : current155
if (this.retained?.text === snapshot) return156
return createUserMessage({157
content: [{ type: 'text', text: snapshot }],158
// The cleared marker has no contributions left to attribute.159
source: sections.length === 0160
? { kind: SOURCE }161
: { kind: SOURCE, form: 'snapshot', sections },162
})163
}164
}