1
/**2
* Registry for ordered system sections, dynamic context, tool schemas, and prompt variables.3
*4
* @module @deepseek-ai/dsh-system-prompt5
*/7
import { Context, Service } from '@deepseek-ai/cordis'8
import z from '@deepseek-ai/schemastery'9
import { AnonymousEntries, NamedEntries, ScopedLayers, scopeTarget } from '@deepseek-ai/dsh-scope'10
import type { ScopeKey, ScopeLayer, Scoped } from '@deepseek-ai/dsh-scope'11
import type { ContextSnapshotSection, ToolSchema } from '@deepseek-ai/dsh-llm'13
declare module '@deepseek-ai/cordis' {14
interface Context {15
systemPrompt: SystemPrompt16
}18
interface Events {19
/**20
* Expert waterfall over the assembled sections, contexts, tools, and variables.21
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners22
* receive only that scope's assemblies. The returned value is authoritative.23
* A supplied signal controls only this explicit assembly request and must not24
* be retained to control later turns. A registered complete section is25
* restored after this waterfall, so listeners cannot add to or replace26
* that scope's system prompt.27
* @param assembly - the mutable assembly built from registered providers.28
* @param context - the caller's per-assembly context.29
* @mode waterfall30
*/31
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>32
/**33
* Emitted when any prompt provider changes. This registry notification is34
* unfiltered because a global change affects every scope.35
* @mode emit36
*/37
'system-prompt/change'(): void38
}39
}41
/** Merge-extensible context for one prompt assembly. */42
export interface AssembleContext {43
/**44
* Scope whose providers and waterfall listeners participate. When absent,45
* only global providers and subject-less listeners participate.46
*/47
scope?: ScopeKey48
/** Explicit control signal for the turn that requested this assembly, when any. */49
signal?: AbortSignal50
}52
/** One contributed section of the system prompt (registry input). */53
export interface PromptSection {54
/** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */55
readonly name: string56
/**57
* Sections are concatenated in ascending order. Equal orders use code-unit58
* name order.59
*/60
readonly order: number61
/**62
* Static text or a provider evaluated at each assembly with that assembly's63
* {@link AssembleContext}. The text may reference `{{variable}}`s — they are64
* interpolated later, by {@link renderPrompt}, unless `interpolate` is false.65
*/66
readonly text: string | ((context: AssembleContext) => string)67
/** Whether to interpolate prompt variables. Defaults to true; false preserves literal text. */68
readonly interpolate?: boolean69
/**70
* Treat this contribution as the complete system prompt. Assembly still71
* runs the cooperative waterfall so tools, contexts, and variables can be72
* resolved, then restores this exact section as the sole prompt section.73
* More than one effective complete section makes assembly fail.74
*/75
readonly complete?: boolean76
}78
/** Dynamic model context materialized as a durable user-role snapshot. */79
export interface PromptContext {80
/** Unique name — a duplicate registration throws (see {@link SystemPrompt.context}). */81
readonly name: string82
/** Contexts are joined in ascending order. */83
readonly order: number84
/** Static text or a provider evaluated for each assembly. Empty text contributes nothing. */85
readonly text: string | ((context: AssembleContext) => string)86
}88
/** One section of an assembly: {@link PromptSection} with its text resolved. */89
export interface AssembledSection {90
/** The contributing section's unique name. */91
name: string92
/** The resolved (but not yet interpolated) section text. */93
text: string94
/** Whether to interpolate prompt variables. Defaults to true; false preserves literal text. */95
interpolate?: boolean96
}98
/** One resolved dynamic context contribution. */99
export interface AssembledContext {100
/** The contributing context's unique name. */101
name: string102
/** The resolved text before variable interpolation. */103
text: string104
}106
/** Tool schemas visible in one assembly and their pre-restriction name set. */107
export interface ToolProviderResult {108
/** The schemas this provider contributes to THIS assembly. */109
readonly schemas: readonly ToolSchema[]110
/** The pre-restriction name universe for config validation (defaults to `schemas`' names). */111
readonly knownNames?: readonly string[]112
}114
/**115
* Merge-extensible assembled model input. Sections and contexts remain116
* uninterpolated until rendered; tools are already in canonical order.117
*/118
export interface PromptAssembly {119
sections: AssembledSection[]120
contexts: AssembledContext[]121
tools: ToolSchema[]122
variables: Record<string, string | undefined>123
}125
const SECTION_ORDERS = {126
HARNESS_IDENTITY: -1000,127
DEPLOYMENT_PERSONA_PREFIX: 0,128
PLAN_POLICY: 500,129
TEAM_POLICY: 600,130
PTC_ONLY: 800,131
FILE_REFERENCE: 900,132
TOOL_BASH: 1000,133
TOOL_PWSH: 1010,134
TOOL_READ: 1100,135
TOOL_WRITE: 1200,136
TOOL_EDIT: 1300,137
TOOL_GLOB: 1400,138
TOOL_GREP: 1500,139
TOOL_JOBS: 1600,140
TOOL_PTY: 1700,141
TOOL_WEB_SEARCH: 2000,142
TOOL_WEB_FETCH: 2100,143
TOOL_LSP: 2200,144
TOOL_SESSION_QUERY: 2300,145
TOOL_GOAL: 2400,146
TOOL_WORKFLOW: 2600,147
TOOL_RALPH: 2700,148
TOOL_SUBAGENT: 2800,149
TOOL_REPORT: 2900,150
TOOL_COMPUTER_USE: 3000,151
MCP_SERVERS: 3100,152
TOOLS_SDK: 5000,153
DELIVERABLE_FILE_REFERENCES: 9000,154
STRUCTURED_OUTPUT: 9900,155
// Local paths and endpoints follow reusable instructions.156
HARNESS_SOURCE: 10000,157
WEB_SURFACE: 10100,158
DEPLOYMENT_PERSONA_SUFFIX: 10200,159
} as const161
/** Name of a centrally allocated prompt-section position. */162
export type PromptSectionOrderName = keyof typeof SECTION_ORDERS164
const CONTEXT_ORDERS = {165
SANDBOX_POLICY: 110,166
APPROVAL_POLICY: 115,167
SUBAGENT_DELEGATION: 120,168
} as const170
/** Name of a centrally allocated runtime-context position. */171
export type PromptContextOrderName = keyof typeof CONTEXT_ORDERS173
/**174
* The deployment persona prefix's section name. Exported because a175
* composition can replace this slot — an agent preset shadows the176
* deployment's persona with its own — and both sides naming the same section177
* is what makes the replacement work rather than duplicate.178
*/179
export const PERSONA_PREFIX_SECTION = 'deployment:persona-prefix'181
/** Deployment persona suffix section name shared by global and scoped contributions. */182
export const PERSONA_SUFFIX_SECTION = 'deployment:persona-suffix'184
/** Valid variable names: how they are written between the braces. */185
const VARIABLE_NAME = /^[a-z][a-z0-9_]*$/187
/** A complete `{{...}}` reference group at the scan position (validated after). */188
const GROUP_AT = /^\{\{([^{}]*)\}\}/190
/** Reserved {@link Config.toolOrder} marker for unlisted tools. */191
export const TOOL_ORDER_REST = '<unlisted-tools>'193
/**194
* Validate duplicate names and the required {@link TOOL_ORDER_REST} marker.195
* Registered names are checked later because plugins have not loaded yet.196
*/197
function validateToolOrder(toolOrder: string[] | undefined): string[] | undefined {198
if (toolOrder === undefined) return undefined199
const seen = new Set<string>()200
for (const name of toolOrder) {201
if (seen.has(name)) throw new Error(`toolOrder lists "${name}" more than once`)202
seen.add(name)203
}204
if (!seen.has(TOOL_ORDER_REST)) {205
throw new Error(`toolOrder must contain the "${TOOL_ORDER_REST}" rest entry (where unlisted tools are inserted)`)206
}207
return toolOrder208
}210
/**211
* Apply configured tool order, inserting unlisted tools lexicographically at212
* {@link TOOL_ORDER_REST}. Unknown configured names fail; known but restricted213
* names may be absent.214
*/215
function orderTools(tools: ToolSchema[], toolOrder: string[] | undefined, knownNames: ReadonlySet<string>): ToolSchema[] {216
const reserved = tools.find(tool => tool.name === TOOL_ORDER_REST)217
if (reserved !== undefined) {218
throw new Error(`tool provider returned reserved tool name "${TOOL_ORDER_REST}" (reserved for toolOrder's rest entry)`)219
}220
if (toolOrder === undefined) return tools.sort(compareToolNames)221
const unknown = toolOrder.filter(name => name !== TOOL_ORDER_REST && !knownNames.has(name))222
if (unknown.length > 0) {223
throw new Error(`toolOrder lists unregistered tool${unknown.length > 1 ? 's' : ''} ${unknown.map(name => `"${name}"`).join(', ')}; known tools: ${[...knownNames].sort().join(', ') || '(none)'}`)224
}225
const listed = new Set(toolOrder)226
const rest = tools.filter(tool => !listed.has(tool.name)).sort(compareToolNames)227
return toolOrder.flatMap(name =>228
name === TOOL_ORDER_REST ? rest : tools.filter(tool => tool.name === name))229
}231
/** Code-unit name comparison — locale-independent, so the order is identical on every machine. */232
function compareNames(a: string, b: string): number {233
return a < b ? -1 : a > b ? 1 : 0234
}236
/** Order prompt sections by their explicit placement, then deterministically by name. */237
function comparePromptSections(a: PromptSection, b: PromptSection): number {238
return a.order - b.order || compareNames(a.name, b.name)239
}241
/** Order tool schemas lexicographically by name. */242
function compareToolNames(a: ToolSchema, b: ToolSchema): number {243
return compareNames(a.name, b.name)244
}246
/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */247
export interface Config {248
/** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */249
includeHarnessIdentity?: boolean250
/** Include dynamic runtime-context snapshots in model history (default true). */251
includeRuntimeContext?: boolean252
/**253
* Deployment-wide persona prefix template before first-party guidance. A scoped section named254
* `deployment:persona-prefix` shadows it; `{{variable}}` references are strict.255
*/256
personaPrefix?: string257
/**258
* Persona suffix template after first-party guidance. A scoped `deployment:persona-suffix`259
* section shadows it; `{{variable}}` references are strict. Defaults to empty.260
*/261
personaSuffix?: string262
/**263
* Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once.264
* Invalid fields fail at load and unknown names fail at assembly; known names265
* hidden in one scope may be absent there. Omitted means lexicographic order.266
*/267
toolOrder?: string[]268
}270
/**271
* Interpolate strict `{{variable}}` references, drop empty sections, and join272
* the rest with blank lines. Sections with `interpolate: false` retain literal273
* text. Malformed, unknown, or undefined references in other sections throw;274
* a lone `{{` without any later `}}` is literal prose, and substituted values275
* are not scanned again.276
* @param assembly - the assembly whose sections and variables to render.277
* @returns the rendered prompt, or `''` when all sections are empty.278
*/279
export function renderPrompt(assembly: PromptAssembly): string {280
return assembly.sections281
.map(section => section.interpolate === false ? section.text : interpolate(section, assembly.variables, 'section'))282
.filter(text => text.length > 0)283
.join('\n\n')284
}286
/**287
* Render the complete dynamic context snapshot.288
* @param assembly - the assembly whose contexts and variables to render.289
* @returns the current full snapshot, or `''` when no context is active.290
*/291
export function renderContextSnapshot(assembly: PromptAssembly): string {292
return joinContextSections(renderContextSections(assembly))293
}295
/**296
* The model-facing snapshot text for an already-rendered section list.297
*298
* A caller that also needs the sections renders them once and joins here, so a299
* request does not interpolate every context twice.300
* @param sections - sections from {@link renderContextSections}.301
* @returns the current full snapshot, or `''` when no context is active.302
*/303
export function joinContextSections(sections: readonly ContextSnapshotSection[]): string {304
const body = sections.map(section => section.text).join('\n\n')305
if (body.length === 0) return ''306
return `Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\n${body}`307
}309
/**310
* The same snapshot, kept as the named contributions it was assembled from.311
*312
* {@link renderContextSnapshot} joins these for the model; a consumer that313
* presents the snapshot uses them to attribute each part to the subsystem that314
* contributed it, without re-splitting the joined prose.315
* @param assembly - the assembly whose contexts and variables to render.316
* @returns one entry per contributing context that rendered to non-empty text.317
*/318
export function renderContextSections(assembly: PromptAssembly): ContextSnapshotSection[] {319
return assembly.contexts320
.map(context => ({ name: context.name, text: interpolate(context, assembly.variables, 'context') }))321
.filter(section => section.text.length > 0)322
}324
/** Interpolate one section or context and attribute diagnostics to its owning input. */325
function interpolate(326
input: AssembledSection | AssembledContext,327
variables: Record<string, string | undefined>,328
kind: 'section' | 'context',329
): string {330
const text = input.text331
let result = ''332
let last = 0333
for (let open = text.indexOf('{{'); open >= 0; open = text.indexOf('{{', last)) {334
const group = GROUP_AT.exec(text.slice(open))335
if (group === null) {336
// A later closing brace makes this malformed; otherwise it is literal prose.337
if (text.indexOf('}}', open + 2) >= 0) {338
throw new Error(`malformed prompt variable reference at "${text.slice(open, open + 16)}…" in ${kind} "${input.name}" (references are complete simple {{name}} groups)`)339
}340
result += text.slice(last, open + 2)341
last = open + 2342
continue343
}344
// `{{}}` yields an empty name and follows the malformed-reference path.345
const name = group[0].slice(2, -2)346
if (!VARIABLE_NAME.test(name)) {347
throw new Error(`malformed prompt variable reference "{{${name}}}" in ${kind} "${input.name}" (variable names match ${String(VARIABLE_NAME)})`)348
}349
// Do not resolve unregistered names through Object.prototype.350
if (!Object.hasOwn(variables, name)) {351
const known = Object.keys(variables)352
throw new Error(`unknown prompt variable "{{${name}}}" in ${kind} "${input.name}"; registered variables: ${known.length > 0 ? known.join(', ') : '(none)'}`)353
}354
const value = variables[name]355
if (value === undefined) {356
throw new Error(`prompt variable "{{${name}}}" has no value for this assembly (${kind} "${input.name}")`)357
}358
result += text.slice(last, open) + value359
last = open + group[0].length360
}361
return result + text.slice(last)362
}364
/** One tool-schema provider stored in a prompt layer. */365
type ToolProvider = (context: AssembleContext) => ToolProviderResult367
/** One prompt-variable provider stored in a prompt layer. */368
type VariableProvider = (context: AssembleContext) => string | undefined370
/** All prompt registrations owned by one global or scoped layer. */371
class PromptLayer implements ScopeLayer {372
readonly sections: NamedEntries<PromptSection>373
readonly contexts: NamedEntries<PromptContext>374
readonly runtimeContextSuppressors = new AnonymousEntries<true>()375
readonly toolProviders = new AnonymousEntries<ToolProvider>()376
readonly variables: NamedEntries<VariableProvider>378
/**379
* Create one prompt layer with diagnostics specific to its ownership scope.380
* @param scope - the scoped owner, or `undefined` for global registrations.381
*/382
constructor(scope: ScopeKey | undefined) {383
this.sections = new NamedEntries(name => new Error(scope === undefined384
? `prompt section "${name}" is already registered (for a per-agent override, register through that agent's \`agent.ctx\` instead)`385
: `prompt section "${name}" is already registered in this scope`))386
this.contexts = new NamedEntries(name => new Error(scope === undefined387
? `prompt context "${name}" is already registered (for a per-agent override, register through that agent's \`agent.ctx\` instead)`388
: `prompt context "${name}" is already registered in this scope`))389
this.variables = new NamedEntries(name => new Error(scope === undefined390
? `prompt variable "${name}" is already registered (for a per-agent value, register through that agent's \`agent.ctx\` instead)`391
: `prompt variable "${name}" is already registered in this scope`))392
}394
/** @returns whether this layer owns no prompt registrations. */395
isEmpty(): boolean {396
return this.sections.isEmpty()397
&& this.contexts.isEmpty()398
&& this.runtimeContextSuppressors.isEmpty()399
&& this.toolProviders.isEmpty()400
&& this.variables.isEmpty()401
}402
}404
/** Registry service for the prompt inputs assembled before each model step. */405
export class SystemPrompt extends Service {406
static Config: z<Config> = z.object({407
includeHarnessIdentity: z.boolean().default(true),408
includeRuntimeContext: z.boolean().default(true),409
personaPrefix: z.string().default(''),410
personaSuffix: z.string().default(''),411
// Preserve omission because an explicit empty order lacks the rest marker.412
toolOrder: z.array(z.string()).default(undefined as unknown as string[]),413
})415
private readonly layers = new ScopedLayers(416
scope => new PromptLayer(scope),417
() => { this.ctx.emit('system-prompt/change') },418
)419
private readonly toolOrder: string[] | undefined421
constructor(ctx: Context, config: Config) {422
super(ctx, 'systemPrompt')423
this.toolOrder = validateToolOrder(config.toolOrder)424
// Keep harness-owned openers independent of the selected loop plugin.425
if (config.includeHarnessIdentity ?? true) {426
this.section({427
name: 'harness:identity',428
order: this.getSectionOrder('HARNESS_IDENTITY'),429
text: 'You are an AI agent powered by DeepSeek Harness.',430
})431
}432
this.section({433
name: PERSONA_PREFIX_SECTION,434
order: this.getSectionOrder('DEPLOYMENT_PERSONA_PREFIX'),435
// The fallback narrows the optional input type; the schema already defaults it.436
text: config.personaPrefix ?? '',437
})438
this.section({439
name: PERSONA_SUFFIX_SECTION,440
order: this.getSectionOrder('DEPLOYMENT_PERSONA_SUFFIX'),441
text: config.personaSuffix ?? '',442
})443
if (!(config.includeRuntimeContext ?? true)) this.suppressRuntimeContext()444
}446
/**447
* Register an ordered prompt section in the calling context's scope. A scoped448
* section shadows a global section with the same name; duplicates within one449
* layer and non-finite orders throw. Registration and disposal emit450
* `system-prompt/change`.451
* @param section - the section to register.452
* @returns the exact Cordis effect disposer.453
*/454
section(section: PromptSection): () => void {455
if (!Number.isFinite(section.order)) {456
throw new TypeError(`prompt section "${section.name}" order must be a finite number`)457
}458
return this.layers.effect(459
this.ctx,460
layer => layer.sections.insert(section.name, section),461
{ label: 'systemPrompt.section()' },462
)463
}465
/**466
* Resolve the centrally owned placement of a repository prompt section.467
* @param name - stable section placement name.468
* @returns the section's numeric sort order.469
*/470
getSectionOrder(name: PromptSectionOrderName): number {471
return SECTION_ORDERS[name]472
}474
/**475
* Resolve the centrally owned placement of a repository runtime context.476
* @param name - stable context placement name.477
* @returns the context's numeric sort order.478
*/479
getContextOrder(name: PromptContextOrderName): number {480
return CONTEXT_ORDERS[name]481
}483
/**484
* Register ordered dynamic context in the calling context's scope. Scoped485
* entries shadow global entries with the same name.486
* @param context - the context contribution to register.487
* @returns the exact Cordis effect disposer.488
*/489
context(context: PromptContext): () => void {490
if (!Number.isFinite(context.order)) {491
throw new TypeError(`prompt context "${context.name}" order must be a finite number`)492
}493
return this.layers.effect(494
this.ctx,495
layer => layer.contexts.insert(context.name, context),496
{ label: 'systemPrompt.context()' },497
)498
}500
/**501
* Suppress every dynamic runtime-context contribution in the calling502
* context's scope without changing the services that own or enforce those503
* facts. Multiple suppressors remain independently disposable.504
* @returns the exact Cordis effect disposer.505
*/506
suppressRuntimeContext(): () => void {507
return this.layers.effect(508
this.ctx,509
layer => layer.runtimeContextSuppressors.append(true),510
{ label: 'systemPrompt.suppressRuntimeContext()' },511
)512
}514
/**515
* Register a tool-schema provider in the calling context's scope. Global and516
* matching scoped providers both contribute; returning the reserved517
* {@link TOOL_ORDER_REST} name makes assembly fail.518
* @param provider - evaluated for each assembly with its context.519
* @returns the exact Cordis effect disposer.520
*/521
tools(provider: (context: AssembleContext) => ToolProviderResult): () => void {522
return this.layers.effect(523
this.ctx,524
layer => layer.toolProviders.append(provider),525
{ label: 'systemPrompt.tools()' },526
)527
}529
/**530
* Register a prompt variable in the calling context's scope. Scoped values531
* shadow globals; invalid or duplicate names throw. A provider may return532
* `undefined`, but rendering a section that references that value then fails.533
* @param name - the `[a-z][a-z0-9_]*` reference name.534
* @param provider - evaluated for each assembly.535
* @returns the exact Cordis effect disposer.536
*/537
variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void {538
if (!VARIABLE_NAME.test(name)) {539
throw new Error(`invalid prompt variable name "${name}" (must match ${String(VARIABLE_NAME)})`)540
}541
return this.layers.effect(542
this.ctx,543
layer => layer.variables.insert(name, provider),544
{ label: 'systemPrompt.variable()' },545
)546
}548
/**549
* Assemble global and scoped providers, detach tool parameters, apply550
* canonical ordering, then run the assembly waterfall. Scoped sections and551
* variables shadow globals. The returned waterfall value is authoritative552
* except that an effective complete section is restored afterwards as the553
* sole prompt section.554
* @param context - the optional scope and plugin-defined assembly fields.555
* @returns the post-waterfall assembly with any complete prompt enforced.556
*/557
// Keep configuration failures on the declared asynchronous error path.558
async assemble(context: AssembleContext = {}): Promise<PromptAssembly> {559
const scope = context.scope560
const scopeLayers = this.layers.chainLayers(scope)561
const runtimeContextSuppressed = !this.layers.global.runtimeContextSuppressors.isEmpty()562
|| scopeLayers.some(layer => !layer.runtimeContextSuppressors.isEmpty())563
// Scoped variables shadow globals.564
const variables: Record<string, string | undefined> = {}565
for (const [name, provider] of this.layers.global.variables.entries()) {566
variables[name] = provider(context)567
}568
// Scope-chain variables, farthest first, so the nearest scope wins a name.569
for (const layer of scopeLayers) {570
for (const [name, provider] of layer.variables.entries()) {571
variables[name] = provider(context)572
}573
}574
// Scoped sections shadow globals before the deterministic order sort.575
const sectionByName = this.layers.merge(scope, layer => layer.sections)576
const contextByName = this.layers.merge(scope, layer => layer.contexts)577
// Validate order against pre-restriction names while collecting visible schemas.578
const providers = [579
...this.layers.global.toolProviders.values(),580
...scopeLayers.flatMap(layer => [...layer.toolProviders.values()]),581
]582
const collected: ToolSchema[] = []583
const knownNames = new Set<string>()584
for (const provider of providers) {585
const result = provider(context)586
const schemas = result.schemas.map(({ name, description, parameters, deferLoading }): ToolSchema => ({587
name,588
description,589
parameters: structuredClone(parameters),590
...deferLoading === true ? { deferLoading } : {},591
}))592
const acceptedKnownNames = result.knownNames ?? schemas.map(tool => tool.name)593
collected.push(...schemas)594
for (const name of acceptedKnownNames) knownNames.add(name)595
}596
const sectionDefinitions = [...sectionByName.values()].sort(comparePromptSections)597
const completeSections = sectionDefinitions.filter(section => section.complete === true)598
if (completeSections.length > 1) {599
throw new Error(`multiple complete prompt sections are active: ${completeSections.map(section => JSON.stringify(section.name)).join(', ')}`)600
}601
let completeSection: AssembledSection | undefined602
const sections = sectionDefinitions603
.map((section) => {604
const assembled = {605
name: section.name,606
text: typeof section.text === 'function' ? section.text(context) : section.text,607
...section.interpolate !== undefined ? { interpolate: section.interpolate } : {},608
}609
if (section.complete === true) completeSection = { ...assembled }610
return assembled611
})612
const assembly: PromptAssembly = {613
sections,614
contexts: runtimeContextSuppressed615
? []616
: [...contextByName.values()]617
.sort((a, b) => a.order - b.order)618
.map(entry => ({619
name: entry.name,620
text: typeof entry.text === 'function' ? entry.text(context) : entry.text,621
})),622
tools: orderTools(collected, this.toolOrder, knownNames),623
variables,624
}625
const transformed = await this.ctx.waterfall(626
scopeTarget(this, scope), 'system-prompt/assemble', assembly, context,627
() => Promise.resolve(assembly),628
)629
if (completeSection === undefined && !runtimeContextSuppressed) return transformed630
return {631
...transformed,632
sections: completeSection === undefined ? transformed.sections : [completeSection],633
contexts: runtimeContextSuppressed ? [] : transformed.contexts,634
}635
}636
}638
export default SystemPrompt