Merge remote-tracking branch 'origin/master' into feat/todo-multi-in-progress
# Conflicts: # examples/acp-agent/tests/snapshots/todo-write/session.jsonl # packages/client/ui-conversation/README.i18n.yaml
This commit is contained in:
@@ -27,7 +27,7 @@ import type { Scope } from '@deepseek-ai/dsh-scope'
|
||||
import { createScope } from '@deepseek-ai/dsh-scope'
|
||||
import type { EpochHeader, RequestContext, Session, SessionId, TurnEndReason, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session'
|
||||
import { renderContextSnapshot, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { joinContextSections, renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { Context } from 'cordis'
|
||||
import { RuntimeContextProjection } from './runtime-context.ts'
|
||||
@@ -202,7 +202,8 @@ export class ReactLoopAgent implements Agent {
|
||||
const claimed = this.inbox.claim(target, position.turn)
|
||||
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
|
||||
signal.throwIfAborted()
|
||||
const context = this.runtimeContext.project(renderContextSnapshot(assembly))
|
||||
const sections = renderContextSections(assembly)
|
||||
const context = this.runtimeContext.project(joinContextSections(sections), sections)
|
||||
const decision = await agentEvents(this.loopCtx, this).waterfall(
|
||||
'agent/pre-step', claimed, { ...position, signal },
|
||||
() => Promise.resolve({
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContextSnapshotSection } 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'
|
||||
@@ -57,15 +58,19 @@ export class RuntimeContextProjection {
|
||||
/**
|
||||
* Create an uncommitted snapshot only when the retained value differs.
|
||||
* @param current - fully rendered dynamic context.
|
||||
* @param sections - named contributions that formed the current snapshot.
|
||||
* @returns a candidate user message, or `undefined` when no update is needed.
|
||||
*/
|
||||
project(current: string): UserMessage | undefined {
|
||||
project(current: string, sections: readonly ContextSnapshotSection[]): 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 },
|
||||
// The cleared marker has no contributions left to attribute.
|
||||
source: sections.length === 0
|
||||
? { kind: 'plugin', plugin: SOURCE }
|
||||
: { kind: 'plugin', plugin: SOURCE, form: 'snapshot', sections },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,16 @@ describe('RuntimeContextProjection', () => {
|
||||
|
||||
const projection = new RuntimeContextProjection(ctx, session)
|
||||
expect(session.surface.nodes).toContain(retained.seq)
|
||||
expect(projection.project('retained')).toBeUndefined()
|
||||
expect(projection.project('retained', [])).toBeUndefined()
|
||||
expect(projection.project('next', [{ name: 'sandbox:policy', text: 'policy' }])?.source).toEqual({
|
||||
kind: 'plugin',
|
||||
plugin: SOURCE,
|
||||
form: 'snapshot',
|
||||
sections: [{ name: 'sandbox:policy', text: 'policy' }],
|
||||
})
|
||||
|
||||
const other = ctx.sessions.create(SessionId('runtime-context-other'))
|
||||
other.append('user/message', contextMessage('other'), { surfaceOp: 'append' })
|
||||
expect(projection.project('retained')).toBeUndefined()
|
||||
expect(projection.project('retained', [])).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Context, Service } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import { AnonymousEntries, NamedEntries, ScopedLayers, scopeTarget } from '@deepseek-ai/dsh-scope'
|
||||
import type { ScopeKey, ScopeLayer, Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContextSnapshotSection, ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
@@ -200,15 +200,39 @@ export function renderPrompt(assembly: PromptAssembly): string {
|
||||
* @returns the current full snapshot, or `''` when no context is active.
|
||||
*/
|
||||
export function renderContextSnapshot(assembly: PromptAssembly): string {
|
||||
const body = assembly.contexts
|
||||
.map(context => interpolate(context, assembly.variables, 'context'))
|
||||
.filter(text => text.length > 0)
|
||||
.join('\n\n')
|
||||
return joinContextSections(renderContextSections(assembly))
|
||||
}
|
||||
|
||||
/**
|
||||
* The model-facing snapshot text for an already-rendered section list.
|
||||
*
|
||||
* A caller that also needs the sections renders them once and joins here, so a
|
||||
* request does not interpolate every context twice.
|
||||
* @param sections - sections from {@link renderContextSections}.
|
||||
* @returns the current full snapshot, or `''` when no context is active.
|
||||
*/
|
||||
export function joinContextSections(sections: readonly ContextSnapshotSection[]): string {
|
||||
const body = sections.map(section => section.text).join('\n\n')
|
||||
if (body.length === 0) return ''
|
||||
return `Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\n${body}`
|
||||
}
|
||||
|
||||
/** Interpolate one section or context and attribute diagnostics to its owner. */
|
||||
/**
|
||||
* The same snapshot, kept as the named contributions it was assembled from.
|
||||
*
|
||||
* {@link renderContextSnapshot} joins these for the model; a consumer that
|
||||
* presents the snapshot uses them to attribute each part to the subsystem that
|
||||
* contributed it, without re-splitting the joined prose.
|
||||
* @param assembly - the assembly whose contexts and variables to render.
|
||||
* @returns one entry per contributing context that rendered to non-empty text.
|
||||
*/
|
||||
export function renderContextSections(assembly: PromptAssembly): ContextSnapshotSection[] {
|
||||
return assembly.contexts
|
||||
.map(context => ({ name: context.name, text: interpolate(context, assembly.variables, 'context') }))
|
||||
.filter(section => section.text.length > 0)
|
||||
}
|
||||
|
||||
/** Interpolate one section or context and attribute diagnostics to its owning input. */
|
||||
function interpolate(
|
||||
input: AssembledSection | AssembledContext,
|
||||
variables: Record<string, string | undefined>,
|
||||
|
||||
Reference in New Issue
Block a user