返回源码地图

packages/core/system-prompt/src/index.ts

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

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

1/**
2 * Registry for ordered system sections, dynamic context, tool schemas, and prompt variables.
3 *
4 * @module @deepseek-ai/dsh-system-prompt
5 */
6
7import { Context, Service } from '@deepseek-ai/cordis'
8import z from '@deepseek-ai/schemastery'
9import { AnonymousEntries, NamedEntries, ScopedLayers, scopeTarget } from '@deepseek-ai/dsh-scope'
10import type { ScopeKey, ScopeLayer, Scoped } from '@deepseek-ai/dsh-scope'
11import type { ContextSnapshotSection, ToolSchema } from '@deepseek-ai/dsh-llm'
12
13declare module '@deepseek-ai/cordis' {
14 interface Context {
15 systemPrompt: SystemPrompt
16 }
17
18 interface Events {
19 /**
20 * Expert waterfall over the assembled sections, contexts, tools, and variables.
21 * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
22 * receive only that scope's assemblies. The returned value is authoritative.
23 * A supplied signal controls only this explicit assembly request and must not
24 * be retained to control later turns. A registered complete section is
25 * restored after this waterfall, so listeners cannot add to or replace
26 * 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 waterfall
30 */
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 is
34 * unfiltered because a global change affects every scope.
35 * @mode emit
36 */
37 'system-prompt/change'(): void
38 }
39}
40
41/** Merge-extensible context for one prompt assembly. */
42export 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?: ScopeKey
48 /** Explicit control signal for the turn that requested this assembly, when any. */
49 signal?: AbortSignal
50}
51
52/** One contributed section of the system prompt (registry input). */
53export interface PromptSection {
54 /** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */
55 readonly name: string
56 /**
57 * Sections are concatenated in ascending order. Equal orders use code-unit
58 * name order.
59 */
60 readonly order: number
61 /**
62 * Static text or a provider evaluated at each assembly with that assembly's
63 * {@link AssembleContext}. The text may reference `{{variable}}`s — they are
64 * 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?: boolean
69 /**
70 * Treat this contribution as the complete system prompt. Assembly still
71 * runs the cooperative waterfall so tools, contexts, and variables can be
72 * 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?: boolean
76}
77
78/** Dynamic model context materialized as a durable user-role snapshot. */
79export interface PromptContext {
80 /** Unique name — a duplicate registration throws (see {@link SystemPrompt.context}). */
81 readonly name: string
82 /** Contexts are joined in ascending order. */
83 readonly order: number
84 /** Static text or a provider evaluated for each assembly. Empty text contributes nothing. */
85 readonly text: string | ((context: AssembleContext) => string)
86}
87
88/** One section of an assembly: {@link PromptSection} with its text resolved. */
89export interface AssembledSection {
90 /** The contributing section's unique name. */
91 name: string
92 /** The resolved (but not yet interpolated) section text. */
93 text: string
94 /** Whether to interpolate prompt variables. Defaults to true; false preserves literal text. */
95 interpolate?: boolean
96}
97
98/** One resolved dynamic context contribution. */
99export interface AssembledContext {
100 /** The contributing context's unique name. */
101 name: string
102 /** The resolved text before variable interpolation. */
103 text: string
104}
105
106/** Tool schemas visible in one assembly and their pre-restriction name set. */
107export 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}
113
114/**
115 * Merge-extensible assembled model input. Sections and contexts remain
116 * uninterpolated until rendered; tools are already in canonical order.
117 */
118export interface PromptAssembly {
119 sections: AssembledSection[]
120 contexts: AssembledContext[]
121 tools: ToolSchema[]
122 variables: Record<string, string | undefined>
123}
124
125const 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 const
160
161/** Name of a centrally allocated prompt-section position. */
162export type PromptSectionOrderName = keyof typeof SECTION_ORDERS
163
164const CONTEXT_ORDERS = {
165 SANDBOX_POLICY: 110,
166 APPROVAL_POLICY: 115,
167 SUBAGENT_DELEGATION: 120,
168} as const
169
170/** Name of a centrally allocated runtime-context position. */
171export type PromptContextOrderName = keyof typeof CONTEXT_ORDERS
172
173/**
174 * The deployment persona prefix's section name. Exported because a
175 * composition can replace this slot — an agent preset shadows the
176 * deployment's persona with its own — and both sides naming the same section
177 * is what makes the replacement work rather than duplicate.
178 */
179export const PERSONA_PREFIX_SECTION = 'deployment:persona-prefix'
180
181/** Deployment persona suffix section name shared by global and scoped contributions. */
182export const PERSONA_SUFFIX_SECTION = 'deployment:persona-suffix'
183
184/** Valid variable names: how they are written between the braces. */
185const VARIABLE_NAME = /^[a-z][a-z0-9_]*$/
186
187/** A complete `{{...}}` reference group at the scan position (validated after). */
188const GROUP_AT = /^\{\{([^{}]*)\}\}/
189
190/** Reserved {@link Config.toolOrder} marker for unlisted tools. */
191export const TOOL_ORDER_REST = '<unlisted-tools>'
192
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 */
197function validateToolOrder(toolOrder: string[] | undefined): string[] | undefined {
198 if (toolOrder === undefined) return undefined
199 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 toolOrder
208}
209
210/**
211 * Apply configured tool order, inserting unlisted tools lexicographically at
212 * {@link TOOL_ORDER_REST}. Unknown configured names fail; known but restricted
213 * names may be absent.
214 */
215function 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}
230
231/** Code-unit name comparison — locale-independent, so the order is identical on every machine. */
232function compareNames(a: string, b: string): number {
233 return a < b ? -1 : a > b ? 1 : 0
234}
235
236/** Order prompt sections by their explicit placement, then deterministically by name. */
237function comparePromptSections(a: PromptSection, b: PromptSection): number {
238 return a.order - b.order || compareNames(a.name, b.name)
239}
240
241/** Order tool schemas lexicographically by name. */
242function compareToolNames(a: ToolSchema, b: ToolSchema): number {
243 return compareNames(a.name, b.name)
244}
245
246/** Plugin config: the deployment-authored fragment of the system prompt (see {@link Config.personaPrefix} for its contract). */
247export interface Config {
248 /** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */
249 includeHarnessIdentity?: boolean
250 /** Include dynamic runtime-context snapshots in model history (default true). */
251 includeRuntimeContext?: boolean
252 /**
253 * Deployment-wide persona prefix template before first-party guidance. A scoped section named
254 * `deployment:persona-prefix` shadows it; `{{variable}}` references are strict.
255 */
256 personaPrefix?: string
257 /**
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?: string
262 /**
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 names
265 * hidden in one scope may be absent there. Omitted means lexicographic order.
266 */
267 toolOrder?: string[]
268}
269
270/**
271 * Interpolate strict `{{variable}}` references, drop empty sections, and join
272 * the rest with blank lines. Sections with `interpolate: false` retain literal
273 * text. Malformed, unknown, or undefined references in other sections throw;
274 * a lone `{{` without any later `}}` is literal prose, and substituted values
275 * 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 */
279export function renderPrompt(assembly: PromptAssembly): string {
280 return assembly.sections
281 .map(section => section.interpolate === false ? section.text : interpolate(section, assembly.variables, 'section'))
282 .filter(text => text.length > 0)
283 .join('\n\n')
284}
285
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 */
291export function renderContextSnapshot(assembly: PromptAssembly): string {
292 return joinContextSections(renderContextSections(assembly))
293}
294
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 a
299 * 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 */
303export 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}
308
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 that
313 * presents the snapshot uses them to attribute each part to the subsystem that
314 * 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 */
318export function renderContextSections(assembly: PromptAssembly): ContextSnapshotSection[] {
319 return assembly.contexts
320 .map(context => ({ name: context.name, text: interpolate(context, assembly.variables, 'context') }))
321 .filter(section => section.text.length > 0)
322}
323
324/** Interpolate one section or context and attribute diagnostics to its owning input. */
325function interpolate(
326 input: AssembledSection | AssembledContext,
327 variables: Record<string, string | undefined>,
328 kind: 'section' | 'context',
329): string {
330 const text = input.text
331 let result = ''
332 let last = 0
333 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 + 2
342 continue
343 }
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) + value
359 last = open + group[0].length
360 }
361 return result + text.slice(last)
362}
363
364/** One tool-schema provider stored in a prompt layer. */
365type ToolProvider = (context: AssembleContext) => ToolProviderResult
366
367/** One prompt-variable provider stored in a prompt layer. */
368type VariableProvider = (context: AssembleContext) => string | undefined
369
370/** All prompt registrations owned by one global or scoped layer. */
371class 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>
377
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 === undefined
384 ? `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 === undefined
387 ? `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 === undefined
390 ? `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 }
393
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}
403
404/** Registry service for the prompt inputs assembled before each model step. */
405export 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 })
414
415 private readonly layers = new ScopedLayers(
416 scope => new PromptLayer(scope),
417 () => { this.ctx.emit('system-prompt/change') },
418 )
419 private readonly toolOrder: string[] | undefined
420
421 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 }
445
446 /**
447 * Register an ordered prompt section in the calling context's scope. A scoped
448 * section shadows a global section with the same name; duplicates within one
449 * layer and non-finite orders throw. Registration and disposal emit
450 * `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 }
464
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 }
473
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 }
482
483 /**
484 * Register ordered dynamic context in the calling context's scope. Scoped
485 * 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 }
499
500 /**
501 * Suppress every dynamic runtime-context contribution in the calling
502 * context's scope without changing the services that own or enforce those
503 * 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 }
513
514 /**
515 * Register a tool-schema provider in the calling context's scope. Global and
516 * matching scoped providers both contribute; returning the reserved
517 * {@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 }
528
529 /**
530 * Register a prompt variable in the calling context's scope. Scoped values
531 * shadow globals; invalid or duplicate names throw. A provider may return
532 * `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 }
547
548 /**
549 * Assemble global and scoped providers, detach tool parameters, apply
550 * canonical ordering, then run the assembly waterfall. Scoped sections and
551 * variables shadow globals. The returned waterfall value is authoritative
552 * except that an effective complete section is restored afterwards as the
553 * 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.scope
560 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 | undefined
602 const sections = sectionDefinitions
603 .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 assembled
611 })
612 const assembly: PromptAssembly = {
613 sections,
614 contexts: runtimeContextSuppressed
615 ? []
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 transformed
630 return {
631 ...transformed,
632 sections: completeSection === undefined ? transformed.sections : [completeSection],
633 contexts: runtimeContextSuppressed ? [] : transformed.contexts,
634 }
635 }
636}
637
638export default SystemPrompt