feat(system-prompt): cache dynamic policy context
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
LlmError,
|
||||
assertNever,
|
||||
createAssistantMessage,
|
||||
createUserMessage,
|
||||
deepFreeze,
|
||||
errorChain,
|
||||
freezeMessage,
|
||||
@@ -45,7 +46,7 @@ import {
|
||||
import type { GenerateOptions, LlmCallConfig, LlmFailure, Message, PreparedLlmCall, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
|
||||
import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session'
|
||||
import type { AssistantMessage, Session, SessionId, TurnEndReason, TurnTrigger, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { renderContextSnapshot, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import { executeToolCalls } from './tool-calls.ts'
|
||||
|
||||
@@ -54,6 +55,34 @@ type StepOutcome =
|
||||
| { kind: 'completed'; continueTurn: boolean; concluded: boolean; maxTokens: boolean }
|
||||
| { kind: 'request-failed'; error: RequestError; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined }
|
||||
|
||||
const RUNTIME_CONTEXT_SOURCE = '@deepseek-ai/dsh-system-prompt'
|
||||
const CLEARED_RUNTIME_CONTEXT = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'
|
||||
|
||||
/** Latest retained cache-safe runtime-context snapshot, excluding compacted-away history. */
|
||||
function retainedRuntimeContext(session: Session): string | undefined {
|
||||
for (const message of [...session.deriveMessages()].reverse()) {
|
||||
if (message.role !== 'user'
|
||||
|| message.source.kind !== 'plugin'
|
||||
|| message.source.plugin !== RUNTIME_CONTEXT_SOURCE) continue
|
||||
const [block] = message.content
|
||||
if (message.content.length === 1 && block?.type === 'text') return block.text
|
||||
return undefined
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Append a full current snapshot only when it changed or compaction removed it. */
|
||||
function materializeRuntimeContext(session: Session, current: string): void {
|
||||
const previous = retainedRuntimeContext(session)
|
||||
if (previous === undefined && current.length === 0) return
|
||||
const snapshot = current.length === 0 ? CLEARED_RUNTIME_CONTEXT : current
|
||||
if (previous === snapshot) return
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: snapshot }],
|
||||
source: { kind: 'plugin', plugin: RUNTIME_CONTEXT_SOURCE },
|
||||
}), { surfaceOp: 'append' })
|
||||
}
|
||||
|
||||
/**
|
||||
* The concrete {@link Agent}: each `run()` owns one turn and repeats model
|
||||
* steps while tools or steering require another request.
|
||||
@@ -511,10 +540,13 @@ export class ReactLoopAgent implements Agent {
|
||||
// this request together.
|
||||
this.drainOutbox(turn)
|
||||
|
||||
// Assemble the system prompt fresh each step (it may depend on log state).
|
||||
// Assemble request-owned prompt inputs fresh each step. Dynamic context is
|
||||
// committed at the tail before deriving history, preserving the stable
|
||||
// system/history cache prefix while keeping every model-visible byte logged.
|
||||
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
|
||||
signal.throwIfAborted()
|
||||
const system = renderPrompt(assembly)
|
||||
materializeRuntimeContext(session, renderContextSnapshot(assembly))
|
||||
|
||||
// Snapshot the exact log prefix: the reconstruction boundary. Appends
|
||||
// after this synchronous snapshot join the next request.
|
||||
|
||||
Reference in New Issue
Block a user