返回源码地图

packages/core/agent-loop/src/runtime-context.ts

main snapshot · da00f7f5358f · 正文引用章节 06 / 06;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/**
2 * Durable projection state for the two loop-owned surface messages the system
3 * prompt plugin forms: the system prompt (surface node 0 and any in-history
4 * replacement) and the dynamic runtime-context snapshot.
5 * @module @deepseek-ai/dsh-agent-loop/runtime-context
6 */
7
8import { createSystemMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
9import type { ContextFormed, ContextSnapshotSection, Message } from '@deepseek-ai/dsh-llm'
10import type { Session, SessionEvent, SessionSeq, SurfaceIntent, SystemMessage, UserMessage } from '@deepseek-ai/dsh-session'
11import { isReplacementSurfaceEvent } from '@deepseek-ai/dsh-session'
12import type { Context } from '@deepseek-ai/cordis'
13declare module '@deepseek-ai/dsh-llm' {
14 interface MessageSourceMap {
15 'runtime-context': { kind: 'runtime-context' } & ContextFormed
16 }
17}
18
19const SOURCE = 'runtime-context'
20const CLEARED = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'
21
22function isOwned(message: UserMessage): boolean {
23 return message.source.kind === SOURCE
24}
25
26function textOf(message: Message): string | undefined {
27 const [block] = message.content
28 return message.content.length === 1 && block?.type === 'text' ? block.text : undefined
29}
30
31/** One uncommitted system-prompt surface operation for request admission or reconciliation. */
32export interface SystemPromptCommit {
33 /** Rendered prompt or empty content: an empty head records no prompt; empty tails are dormant. */
34 message: SystemMessage
35 /** `append` for a new system node, otherwise a replacement of one surviving system node. */
36 intent: SurfaceIntent<'system/message'>
37}
38
39/** The request-series facts one prompt decision is made under. */
40export interface SystemPromptDecisionInput {
41 /** Whether the prepared route for this attempt reads a later `system` message as the effective prompt. */
42 inHistory: boolean
43 /**
44 * Whether prompt admission must consolidate: a pre-step listener declared a
45 * new series, the surface changed since the last request, or assembled tools
46 * changed on a route without tool-update support.
47 */
48 startsSeries: boolean
49}
50
51/** Committed events from the newest backward; the restore scans stop at the first match. */
52function 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}
56
57/**
58 * Decides how a rendered system prompt reaches the surface without owning the
59 * commit. The first prompt, even empty, reserves surface node 0.
60 * A capable continuing series appends changed nonempty text after the
61 * cached history. An incapable route, broken series, or cleared prompt instead
62 * normalizes the first system node and empties later active nodes. Dormant empty
63 * tails do not supply effective text or require repeated replacements.
64 */
65export class SystemPromptProjection {
66 constructor(private readonly session: Session) {}
67
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') continue
75 const content = event.data.message.content
76 const text = content.length === 0 ? '' : textOf(event.data.message)
77 nodes.push({ seq, text })
78 }
79 return nodes
80 }
81
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 !== '') ?? head
95 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 updates
100 }
101 if (latest.text === rendered) return []
102 return [{ message: createSystemMessage(rendered), intent: { surfaceOp: 'append' } }]
103 }
104
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}
112
113/** Tracks the last retained runtime-context snapshot without owning its commit. */
114export class RuntimeContextProjection {
115 /** `undefined` means no snapshot ever existed; `null` means none is retained. */
116 private retained: { seq: SessionSeq; text: string | undefined } | null | undefined
117
118 /**
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)) continue
127 this.retained ??= null
128 if (surface.has(event.seq)) {
129 this.retained = { seq: event.seq, text: textOf(event.data) }
130 break
131 }
132 }
133
134 ctx.on('session/event', (subject, event) => {
135 if (subject !== session) return
136 if (event.type === 'user/message' && isOwned(event.data)) {
137 this.retained = { seq: event.seq, text: textOf(event.data) }
138 } else if (this.retained
139 && isReplacementSurfaceEvent(event)
140 && event.sourceEventSeqs?.includes(this.retained.seq) === true) {
141 this.retained = null
142 }
143 })
144 }
145
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) return
154 const snapshot = current.length === 0 ? CLEARED : current
155 if (this.retained?.text === snapshot) return
156 return createUserMessage({
157 content: [{ type: 'text', text: snapshot }],
158 // The cleared marker has no contributions left to attribute.
159 source: sections.length === 0
160 ? { kind: SOURCE }
161 : { kind: SOURCE, form: 'snapshot', sections },
162 })
163 }
164}