|
@@ -1,5 +1,5 @@
|
|
|
/**
|
|
/**
|
|
|
- * Registry for ordered prompt sections, tool schemas, and prompt variables.
|
|
|
|
|
|
|
+ * Registry for ordered system sections, dynamic context, tool schemas, and prompt variables.
|
|
|
*
|
|
*
|
|
|
* @module @deepseek-ai/dsh-system-prompt
|
|
* @module @deepseek-ai/dsh-system-prompt
|
|
|
*/
|
|
*/
|
|
@@ -17,7 +17,7 @@ declare module 'cordis' {
|
|
|
|
|
|
|
|
interface Events {
|
|
interface Events {
|
|
|
/**
|
|
/**
|
|
|
- * Expert waterfall over the assembled sections, tools, and variables.
|
|
|
|
|
|
|
+ * Expert waterfall over the assembled sections, contexts, tools, and variables.
|
|
|
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
|
|
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
|
|
|
* receive only that scope's assemblies. The returned value is authoritative.
|
|
* receive only that scope's assemblies. The returned value is authoritative.
|
|
|
* A supplied signal controls only this explicit assembly request and must not
|
|
* A supplied signal controls only this explicit assembly request and must not
|
|
@@ -65,6 +65,16 @@ export interface PromptSection {
|
|
|
readonly text: string | ((context: AssembleContext) => string)
|
|
readonly text: string | ((context: AssembleContext) => string)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** Dynamic model context materialized as a durable user-role snapshot. */
|
|
|
|
|
+export interface PromptContext {
|
|
|
|
|
+ /** Unique name — a duplicate registration throws (see {@link SystemPrompt.context}). */
|
|
|
|
|
+ readonly name: string
|
|
|
|
|
+ /** Contexts are joined in ascending order. */
|
|
|
|
|
+ readonly order: number
|
|
|
|
|
+ /** Static text or a provider evaluated for each assembly. Empty text contributes nothing. */
|
|
|
|
|
+ readonly text: string | ((context: AssembleContext) => string)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** One section of an assembly: {@link PromptSection} with its text resolved. */
|
|
/** One section of an assembly: {@link PromptSection} with its text resolved. */
|
|
|
export interface AssembledSection {
|
|
export interface AssembledSection {
|
|
|
/** The contributing section's unique name. */
|
|
/** The contributing section's unique name. */
|
|
@@ -73,6 +83,14 @@ export interface AssembledSection {
|
|
|
text: string
|
|
text: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** One resolved dynamic context contribution. */
|
|
|
|
|
+export interface AssembledContext {
|
|
|
|
|
+ /** The contributing context's unique name. */
|
|
|
|
|
+ name: string
|
|
|
|
|
+ /** The resolved text before variable interpolation. */
|
|
|
|
|
+ text: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** Tool schemas visible in one assembly and their pre-restriction name set. */
|
|
/** Tool schemas visible in one assembly and their pre-restriction name set. */
|
|
|
export interface ToolProviderResult {
|
|
export interface ToolProviderResult {
|
|
|
/** The schemas this provider contributes to THIS assembly. */
|
|
/** The schemas this provider contributes to THIS assembly. */
|
|
@@ -82,11 +100,12 @@ export interface ToolProviderResult {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Merge-extensible assembled prompt. Sections remain uninterpolated until
|
|
|
|
|
- * {@link renderPrompt}; tools are already in canonical model-facing order.
|
|
|
|
|
|
|
+ * Merge-extensible assembled model input. Sections and contexts remain
|
|
|
|
|
+ * uninterpolated until rendered; tools are already in canonical order.
|
|
|
*/
|
|
*/
|
|
|
export interface PromptAssembly {
|
|
export interface PromptAssembly {
|
|
|
sections: AssembledSection[]
|
|
sections: AssembledSection[]
|
|
|
|
|
+ contexts: AssembledContext[]
|
|
|
tools: ToolSchema[]
|
|
tools: ToolSchema[]
|
|
|
variables: Record<string, string | undefined>
|
|
variables: Record<string, string | undefined>
|
|
|
}
|
|
}
|
|
@@ -170,14 +189,32 @@ export interface Config {
|
|
|
*/
|
|
*/
|
|
|
export function renderPrompt(assembly: PromptAssembly): string {
|
|
export function renderPrompt(assembly: PromptAssembly): string {
|
|
|
return assembly.sections
|
|
return assembly.sections
|
|
|
- .map(section => interpolate(section, assembly.variables))
|
|
|
|
|
|
|
+ .map(section => interpolate(section, assembly.variables, 'section'))
|
|
|
|
|
+ .filter(text => text.length > 0)
|
|
|
|
|
+ .join('\n\n')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Render the complete dynamic context snapshot.
|
|
|
|
|
+ * @param assembly - the assembly whose contexts and variables to render.
|
|
|
|
|
+ * @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)
|
|
.filter(text => text.length > 0)
|
|
|
.join('\n\n')
|
|
.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's `{{variable}}` references (see {@link renderPrompt}). */
|
|
|
|
|
-function interpolate(section: AssembledSection, variables: Record<string, string | undefined>): string {
|
|
|
|
|
- const text = section.text
|
|
|
|
|
|
|
+/** Interpolate one section or context and attribute diagnostics to its owner. */
|
|
|
|
|
+function interpolate(
|
|
|
|
|
+ input: AssembledSection | AssembledContext,
|
|
|
|
|
+ variables: Record<string, string | undefined>,
|
|
|
|
|
+ kind: 'section' | 'context',
|
|
|
|
|
+): string {
|
|
|
|
|
+ const text = input.text
|
|
|
let result = ''
|
|
let result = ''
|
|
|
let last = 0
|
|
let last = 0
|
|
|
for (let open = text.indexOf('{{'); open >= 0; open = text.indexOf('{{', last)) {
|
|
for (let open = text.indexOf('{{'); open >= 0; open = text.indexOf('{{', last)) {
|
|
@@ -185,7 +222,7 @@ function interpolate(section: AssembledSection, variables: Record<string, string
|
|
|
if (group === null) {
|
|
if (group === null) {
|
|
|
// A later closing brace makes this malformed; otherwise it is literal prose.
|
|
// A later closing brace makes this malformed; otherwise it is literal prose.
|
|
|
if (text.indexOf('}}', open + 2) >= 0) {
|
|
if (text.indexOf('}}', open + 2) >= 0) {
|
|
|
- throw new Error(`malformed prompt variable reference at "${text.slice(open, open + 16)}…" in section "${section.name}" (references are complete simple {{name}} groups)`)
|
|
|
|
|
|
|
+ throw new Error(`malformed prompt variable reference at "${text.slice(open, open + 16)}…" in ${kind} "${input.name}" (references are complete simple {{name}} groups)`)
|
|
|
}
|
|
}
|
|
|
result += text.slice(last, open + 2)
|
|
result += text.slice(last, open + 2)
|
|
|
last = open + 2
|
|
last = open + 2
|
|
@@ -194,16 +231,16 @@ function interpolate(section: AssembledSection, variables: Record<string, string
|
|
|
// `{{}}` yields an empty name and follows the malformed-reference path.
|
|
// `{{}}` yields an empty name and follows the malformed-reference path.
|
|
|
const name = group[0].slice(2, -2)
|
|
const name = group[0].slice(2, -2)
|
|
|
if (!VARIABLE_NAME.test(name)) {
|
|
if (!VARIABLE_NAME.test(name)) {
|
|
|
- throw new Error(`malformed prompt variable reference "{{${name}}}" in section "${section.name}" (variable names match ${String(VARIABLE_NAME)})`)
|
|
|
|
|
|
|
+ throw new Error(`malformed prompt variable reference "{{${name}}}" in ${kind} "${input.name}" (variable names match ${String(VARIABLE_NAME)})`)
|
|
|
}
|
|
}
|
|
|
// Do not resolve unregistered names through Object.prototype.
|
|
// Do not resolve unregistered names through Object.prototype.
|
|
|
if (!Object.hasOwn(variables, name)) {
|
|
if (!Object.hasOwn(variables, name)) {
|
|
|
const known = Object.keys(variables)
|
|
const known = Object.keys(variables)
|
|
|
- throw new Error(`unknown prompt variable "{{${name}}}" in section "${section.name}"; registered variables: ${known.length > 0 ? known.join(', ') : '(none)'}`)
|
|
|
|
|
|
|
+ throw new Error(`unknown prompt variable "{{${name}}}" in ${kind} "${input.name}"; registered variables: ${known.length > 0 ? known.join(', ') : '(none)'}`)
|
|
|
}
|
|
}
|
|
|
const value = variables[name]
|
|
const value = variables[name]
|
|
|
if (value === undefined) {
|
|
if (value === undefined) {
|
|
|
- throw new Error(`prompt variable "{{${name}}}" has no value for this assembly (section "${section.name}")`)
|
|
|
|
|
|
|
+ throw new Error(`prompt variable "{{${name}}}" has no value for this assembly (${kind} "${input.name}")`)
|
|
|
}
|
|
}
|
|
|
result += text.slice(last, open) + value
|
|
result += text.slice(last, open) + value
|
|
|
last = open + group[0].length
|
|
last = open + group[0].length
|
|
@@ -220,6 +257,7 @@ type VariableProvider = (context: AssembleContext) => string | undefined
|
|
|
/** All prompt registrations owned by one global or scoped layer. */
|
|
/** All prompt registrations owned by one global or scoped layer. */
|
|
|
class PromptLayer implements ScopeLayer {
|
|
class PromptLayer implements ScopeLayer {
|
|
|
readonly sections: NamedEntries<PromptSection>
|
|
readonly sections: NamedEntries<PromptSection>
|
|
|
|
|
+ readonly contexts: NamedEntries<PromptContext>
|
|
|
readonly toolProviders = new AnonymousEntries<ToolProvider>()
|
|
readonly toolProviders = new AnonymousEntries<ToolProvider>()
|
|
|
readonly variables: NamedEntries<VariableProvider>
|
|
readonly variables: NamedEntries<VariableProvider>
|
|
|
|
|
|
|
@@ -231,6 +269,9 @@ class PromptLayer implements ScopeLayer {
|
|
|
this.sections = new NamedEntries(name => new Error(scope === undefined
|
|
this.sections = new NamedEntries(name => new Error(scope === undefined
|
|
|
? `prompt section "${name}" is already registered (for a per-agent override, register through that agent's \`agent.ctx\` instead)`
|
|
? `prompt section "${name}" is already registered (for a per-agent override, register through that agent's \`agent.ctx\` instead)`
|
|
|
: `prompt section "${name}" is already registered in this scope`))
|
|
: `prompt section "${name}" is already registered in this scope`))
|
|
|
|
|
+ this.contexts = new NamedEntries(name => new Error(scope === undefined
|
|
|
|
|
+ ? `prompt context "${name}" is already registered (for a per-agent override, register through that agent's \`agent.ctx\` instead)`
|
|
|
|
|
+ : `prompt context "${name}" is already registered in this scope`))
|
|
|
this.variables = new NamedEntries(name => new Error(scope === undefined
|
|
this.variables = new NamedEntries(name => new Error(scope === undefined
|
|
|
? `prompt variable "${name}" is already registered (for a per-agent value, register through that agent's \`agent.ctx\` instead)`
|
|
? `prompt variable "${name}" is already registered (for a per-agent value, register through that agent's \`agent.ctx\` instead)`
|
|
|
: `prompt variable "${name}" is already registered in this scope`))
|
|
: `prompt variable "${name}" is already registered in this scope`))
|
|
@@ -239,6 +280,7 @@ class PromptLayer implements ScopeLayer {
|
|
|
/** @returns whether this layer owns no prompt registrations. */
|
|
/** @returns whether this layer owns no prompt registrations. */
|
|
|
isEmpty(): boolean {
|
|
isEmpty(): boolean {
|
|
|
return this.sections.isEmpty()
|
|
return this.sections.isEmpty()
|
|
|
|
|
+ && this.contexts.isEmpty()
|
|
|
&& this.toolProviders.isEmpty()
|
|
&& this.toolProviders.isEmpty()
|
|
|
&& this.variables.isEmpty()
|
|
&& this.variables.isEmpty()
|
|
|
}
|
|
}
|
|
@@ -297,6 +339,23 @@ export class SystemPrompt extends Service {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Register ordered dynamic context in the calling context's scope. Scoped
|
|
|
|
|
+ * entries shadow global entries with the same name.
|
|
|
|
|
+ * @param context - the context contribution to register.
|
|
|
|
|
+ * @returns the exact Cordis effect disposer.
|
|
|
|
|
+ */
|
|
|
|
|
+ context(context: PromptContext): () => void {
|
|
|
|
|
+ if (!Number.isFinite(context.order)) {
|
|
|
|
|
+ throw new TypeError(`prompt context "${context.name}" order must be a finite number`)
|
|
|
|
|
+ }
|
|
|
|
|
+ return this.layers.effect(
|
|
|
|
|
+ this.ctx,
|
|
|
|
|
+ layer => layer.contexts.insert(context.name, context),
|
|
|
|
|
+ { label: 'systemPrompt.context()' },
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Register a tool-schema provider in the calling context's scope. Global and
|
|
* Register a tool-schema provider in the calling context's scope. Global and
|
|
|
* matching scoped providers both contribute; returning the reserved
|
|
* matching scoped providers both contribute; returning the reserved
|
|
@@ -352,6 +411,7 @@ export class SystemPrompt extends Service {
|
|
|
}
|
|
}
|
|
|
// Scoped sections shadow globals before the stable order sort.
|
|
// Scoped sections shadow globals before the stable order sort.
|
|
|
const sectionByName = this.layers.merge(scope, layer => layer.sections)
|
|
const sectionByName = this.layers.merge(scope, layer => layer.sections)
|
|
|
|
|
+ const contextByName = this.layers.merge(scope, layer => layer.contexts)
|
|
|
// Validate order against pre-restriction names while collecting visible schemas.
|
|
// Validate order against pre-restriction names while collecting visible schemas.
|
|
|
const providers = [
|
|
const providers = [
|
|
|
...this.layers.global.toolProviders.values(),
|
|
...this.layers.global.toolProviders.values(),
|
|
@@ -377,6 +437,12 @@ export class SystemPrompt extends Service {
|
|
|
name: section.name,
|
|
name: section.name,
|
|
|
text: typeof section.text === 'function' ? section.text(context) : section.text,
|
|
text: typeof section.text === 'function' ? section.text(context) : section.text,
|
|
|
})),
|
|
})),
|
|
|
|
|
+ contexts: [...contextByName.values()]
|
|
|
|
|
+ .sort((a, b) => a.order - b.order)
|
|
|
|
|
+ .map(entry => ({
|
|
|
|
|
+ name: entry.name,
|
|
|
|
|
+ text: typeof entry.text === 'function' ? entry.text(context) : entry.text,
|
|
|
|
|
+ })),
|
|
|
tools: orderTools(collected, this.toolOrder, knownNames),
|
|
tools: orderTools(collected, this.toolOrder, knownNames),
|
|
|
variables,
|
|
variables,
|
|
|
}
|
|
}
|