refactor(core): simplify tools prompts and trusted services

This commit is contained in:
Tianyi Cui
2026-07-12 22:39:01 +08:00
parent 28e04ff4fb
commit 02ca71db57
24 changed files with 636 additions and 2695 deletions

View File

@@ -1,8 +1,8 @@
/**
* System prompt assembly registry. Plugins contribute ordered text sections,
* tool schema providers, named prompt variables, and owner-final named
* protections; `assemble(context)` collates them through a waterfall that
* runs once per step, restores protected contributions, and `renderPrompt`
* tool schema providers, and named prompt variables; protocol contributions
* may declare themselves owner-final. `assemble(context)` collates them through a waterfall that
* runs once per step, restores owner-final contributions, and `renderPrompt`
* interpolates `{{variable}}` references into the final text.
*
* The harness-owned prompt openers live here too: this plugin registers the
@@ -18,7 +18,6 @@ import z from 'schemastery'
import { scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope'
import type { ScopeKey, Scoped } from '@deepseek-ai/dsh-scope'
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
declare module 'cordis' {
interface Context {
@@ -45,7 +44,7 @@ declare module 'cordis' {
*/
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
/**
* A section, tool provider, variable provider, or protection was registered
* A section, tool provider, or variable provider was registered
* or unregistered (the assembly inputs changed — possibly for one scope
* only). An UNFILTERED registry-subject notification, deliberately not
* scope-filtered dispatch: a global change concerns every agent's next
@@ -81,19 +80,25 @@ export interface AssembleContext {
/** One contributed section of the system prompt (registry input). */
export interface PromptSection {
/** Unique name — a duplicate registration throws (see {@link SystemPrompt.section}). */
name: string
readonly name: string
/**
* Sections are concatenated in ascending order. Convention: `-100` is the
* harness identity, `0` the deployment persona, tool guidance uses 100199;
* other negative orders also render before the persona.
*/
order: number
readonly order: number
/**
* Static text or a provider evaluated at each assembly with that assembly's
* {@link AssembleContext}. The text may reference `{{variable}}`s — they are
* interpolated later, by {@link renderPrompt}.
*/
text: string | ((context: AssembleContext) => string)
readonly text: string | ((context: AssembleContext) => string)
/**
* Whether this section's canonical presence and definition survive the
* complete assembly waterfall. Use this only for owner-required protocol
* instructions; ordinary sections remain transformable.
*/
readonly ownerFinal?: boolean
}
/** One section of an assembly: {@link PromptSection} with its text resolved. */
@@ -118,30 +123,15 @@ export interface AssembledSection {
*/
export interface ToolProviderResult {
/** The schemas this provider contributes to THIS assembly. */
schemas: ToolSchema[]
readonly schemas: readonly ToolSchema[]
/** The pre-restriction name universe for config validation (defaults to `schemas`' names). */
knownNames?: readonly string[]
}
/**
* Canonical prompt contributions that survive the assembly waterfall.
*
* Protection is declarative by contribution name rather than an ordered
* callback: after every `system-prompt/assemble` listener has finished, the
* service restores each protected name to the exact presence and definition
* produced by its registries before the waterfall. Restored entries keep
* canonical order with one another and anchor before their first surviving
* later unprotected canonical neighbor (or at the end); the service does not
* undo a listener's reordering of unprotected entries. A name absent from that
* canonical assembly is removed from the result. This makes mode-dependent
* absence protectable too (for example, a native tool that intentionally stays
* off the wire in Code Mode).
*/
export interface PromptProtection {
/** Section names whose canonical presence and definition are restored after the waterfall. */
sections?: readonly string[]
/** Tool names whose canonical presence and definition are restored after the waterfall. */
tools?: readonly string[]
readonly knownNames?: readonly string[]
/**
* Tool names this provider owns finally. The names need not be present in
* `schemas`: naming a mode-hidden tool makes its canonical absence final, so
* an assembly listener cannot fabricate it onto the wire.
*/
readonly ownerFinalNames?: readonly string[]
}
/**
@@ -234,76 +224,28 @@ function orderTools(tools: ToolSchema[], toolOrder: string[] | undefined, knownN
name === TOOL_ORDER_REST ? rest : tools.filter(tool => tool.name === name))
}
/** Snapshot one waterfall-produced named entry with a stable, own data `name`. */
function snapshotNamedEntry<T extends { name: string }>(entry: T): { entry: T; name: string } {
// Read the name exactly once before protection matching. The waterfall owns
// its output and may return accessor-backed records; retaining such an entry
// would let a getter answer "unprotected" during filtering and the protected
// name later when a consumer reads the final assembly.
const name = entry.name
const snapshot: Record<string, unknown> = {}
Object.defineProperty(snapshot, 'name', {
value: name,
enumerable: true,
configurable: true,
writable: true,
})
// Copy every other enumerable field once while deliberately skipping name.
// defineProperty keeps a literal "__proto__" extension field ordinary data.
for (const key of Object.keys(entry)) {
if (key === 'name') continue
Object.defineProperty(snapshot, key, {
value: (entry as unknown as Record<string, unknown>)[key],
enumerable: true,
configurable: true,
writable: true,
})
}
return { entry: snapshot as T, name }
}
/** Restore protected named entries from `canonical`, anchored before their next unprotected canonical neighbor. */
function restoreProtected<T extends { name: string }>(
canonical: readonly T[], result: readonly T[], protectedNames: ReadonlySet<string>,
/** Restore owner-final entries from `canonical`, anchored before their next ordinary canonical neighbor. */
function restoreOwnerFinal<T extends { name: string }>(
canonical: readonly T[], result: readonly T[], ownerFinalNames: ReadonlySet<string>,
): T[] {
const restored = result
.map(snapshotNamedEntry)
.filter(record => !protectedNames.has(record.name))
const restored = result.filter(entry => !ownerFinalNames.has(entry.name))
for (const [index, entry] of canonical.entries()) {
if (!protectedNames.has(entry.name)) continue
if (!ownerFinalNames.has(entry.name)) continue
// Protected entries are inserted in canonical order. Anchor each one
// before the first later UNPROTECTED canonical neighbor that survived the
// waterfall; if none survived, it belongs at the end. Looking only at
// unprotected neighbors avoids reversing adjacent protected entries.
// ordinary neighbors avoids reversing adjacent owner-final entries.
const following = new Set(
canonical.slice(index + 1)
.filter(candidate => !protectedNames.has(candidate.name))
.filter(candidate => !ownerFinalNames.has(candidate.name))
.map(candidate => candidate.name),
)
const next = restored.findIndex(candidate => following.has(candidate.name))
restored.splice(next < 0 ? restored.length : next, 0, {
entry: structuredClone(entry),
name: entry.name,
})
// `canonical` is an owned snapshot made before the waterfall; no second
// clone is needed when moving its entries into the finalized assembly.
restored.splice(next < 0 ? restored.length : next, 0, entry)
}
return restored.map(record => record.entry)
}
/** Validate and detach one protection-name array without rereading an element. */
function snapshotProtectionNames(value: unknown, field: 'sections' | 'tools'): readonly string[] {
if (!Array.isArray(value)) {
throw new TypeError(`systemPrompt.protect() ${field} must be an array of strings`)
}
const names: string[] = []
const length = value.length
for (let index = 0; index < length; index += 1) {
const name: unknown = value[index]
if (typeof name !== 'string') {
throw new TypeError(`systemPrompt.protect() ${field} must be an array of strings`)
}
names.push(name)
}
return Object.freeze([...new Set(names)])
return restored
}
/** Lexicographic (code-unit) name comparison — locale-independent, so the order is identical on every machine. */
@@ -423,7 +365,7 @@ function interpolate(section: AssembledSection, variables: Record<string, string
/**
* Registry service (`ctx.systemPrompt`): plugins contribute ordered text
* sections, tool-schema providers, named prompt variables, and owner-final
* contribution protections; the agent loop calls `assemble(context)` once per
* contributions; the agent loop calls `assemble(context)` once per
* step. Registers the harness-owned `harness:identity` and
* `deployment:persona` sections itself (see {@link Config.persona}).
*/
@@ -442,12 +384,10 @@ export class SystemPrompt extends Service {
private sections: PromptSection[] = []
private toolProviders: ((context: AssembleContext) => ToolProviderResult)[] = []
private variableProviders = new Map<string, (context: AssembleContext) => string | undefined>()
private protections: PromptProtection[] = []
/** Per-scope layers (`@deepseek-ai/dsh-scope`); entries drop when a layer empties, so a disposed scope leaves no residue. */
private scopedSections = new Map<ScopeKey, PromptSection[]>()
private scopedToolProviders = new Map<ScopeKey, ((context: AssembleContext) => ToolProviderResult)[]>()
private scopedVariableProviders = new Map<ScopeKey, Map<string, (context: AssembleContext) => string | undefined>>()
private scopedProtections = new Map<ScopeKey, PromptProtection[]>()
private readonly toolOrder: string[] | undefined
constructor(ctx: Context, public config: Config) {
@@ -480,13 +420,11 @@ export class SystemPrompt extends Service {
* scoped context (`agent.ctx`) contributes to that scope alone — and a
* scoped section SHADOWS a same-named global section for that scope's
* assemblies (most-specific-wins; this is how a per-agent persona overrides
* `deployment:persona`) unless that global name is protected: global
* protection reserves its section name against scoped shadows so the
* `deployment:persona`) unless that global contribution is owner-final: it
* reserves its section name against scoped shadows so the
* registration owner—not a later scope—defines the canonical value. The
* registry reads `name`, `order`, and `text` once, validates their fixed
* string/finite-number/string-or-function types, and stores only that
* accepted record, so later caller-object mutation cannot rename or reshape
* a contribution. Throws if the SAME layer already has the name (a
* readonly typed contribution is borrowed until disposal; only the semantic
* finite-order rule is checked at runtime. Throws if the SAME layer already has the name (a
* duplicate would silently double prompt text — e.g. a double-loaded tool
* plugin; the global-duplicate message names `agent.ctx` as the per-agent
* alternative). Removed when the calling fiber is disposed. Emits
@@ -497,25 +435,20 @@ export class SystemPrompt extends Service {
* yield it directly — exact identity nests the teardown in order.
*/
section(section: PromptSection): () => Promise<void> | void {
const input: unknown = section
if (typeof input !== 'object' || input === null) {
throw new TypeError('systemPrompt.section() requires a section object')
}
const accepted = input as PromptSection
const name = accepted.name
const order = accepted.order
const text = accepted.text
if (typeof name !== 'string') throw new TypeError('prompt section name must be a string')
if (typeof order !== 'number' || !Number.isFinite(order)) {
throw new TypeError(`prompt section "${name}" order must be a finite number`)
}
if (typeof text !== 'string' && typeof text !== 'function') {
throw new TypeError(`prompt section "${name}" text must be a string or function`)
if (!Number.isFinite(section.order)) {
throw new TypeError(`prompt section "${section.name}" order must be a finite number`)
}
const scope = scopeOf(this.ctx)
const snapshot: PromptSection = { name, order, text }
if (scope !== undefined && this.protections.some(record => record.sections?.includes(snapshot.name))) {
throw new Error(`prompt section "${snapshot.name}" is globally protected and cannot be shadowed in an agent scope`)
if (scope !== undefined
&& this.sections.some(global => global.name === section.name && global.ownerFinal === true)) {
throw new Error(`prompt section "${section.name}" is globally owner-final and cannot be shadowed in an agent scope`)
}
if (scope === undefined && section.ownerFinal === true) {
const hasScopedShadow = [...this.scopedSections.values()]
.some(layer => layer.some(scoped => scoped.name === section.name))
if (hasScopedShadow) {
throw new Error(`owner-final prompt section "${section.name}" cannot be registered while a scoped shadow exists`)
}
}
const dispose = this.ctx.effect(function* (this: SystemPrompt) {
const layer = scope === undefined
@@ -525,18 +458,18 @@ export class SystemPrompt extends Service {
this.scopedSections.set(scope, created)
return created
})()
if (layer.some(existing => existing.name === snapshot.name)) {
if (layer.some(existing => existing.name === section.name)) {
throw new Error(scope === undefined
? `prompt section "${snapshot.name}" is already registered (for a per-agent override, register through that agent's \`agent.ctx\` instead)`
: `prompt section "${snapshot.name}" is already registered in this scope`)
? `prompt section "${section.name}" is already registered (for a per-agent override, register through that agent's \`agent.ctx\` instead)`
: `prompt section "${section.name}" is already registered in this scope`)
}
layer.push(snapshot)
layer.push(section)
// Yield the rollback BEFORE emitting `system-prompt/change`: a generator
// effect collects each yielded disposer before the next step runs, so a
// throwing change listener removes the section instead of leaking it into
// every future assembly.
yield () => {
const index = layer.indexOf(snapshot)
const index = layer.indexOf(section)
/* v8 ignore next 3 -- defensive: section was registered, so indexOf is guaranteed >= 0 */
if (index >= 0) layer.splice(index, 1)
if (scope !== undefined && layer.length === 0) this.scopedSections.delete(scope)
@@ -560,8 +493,7 @@ export class SystemPrompt extends Service {
* `schemas`/`knownNames` split). The layer is decided by the calling
* context: a scoped provider (registered through `agent.ctx`) is consulted
* only for that scope's assemblies. Removed when the calling fiber is
* disposed. A non-function provider is rejected before any effect is stored.
* A provider must not return a schema named
* disposed. A provider must not return a schema named
* {@link TOOL_ORDER_REST}; that name is reserved for
* {@link Config.toolOrder}'s rest entry and rejects the assembly. Emits
* `system-prompt/change`.
@@ -571,9 +503,6 @@ export class SystemPrompt extends Service {
* yield it directly — exact identity nests the teardown in order.
*/
tools(provider: (context: AssembleContext) => ToolProviderResult): () => Promise<void> | void {
if (typeof provider !== 'function') {
throw new TypeError('system prompt tool provider must be a function')
}
const scope = scopeOf(this.ctx)
const dispose = this.ctx.effect(function* (this: SystemPrompt) {
const layer = scope === undefined
@@ -611,8 +540,7 @@ export class SystemPrompt extends Service {
* deployment must not claim facts it does not have). The layer is decided
* by the calling context: a scoped variable (registered through
* `agent.ctx`) resolves only for that scope's assemblies and SHADOWS a
* same-named global variable there. The fixed name and callback types are
* validated before effect storage. Throws on a name that does not match
* same-named global variable there. Throws on a name that does not match
* `[a-z][a-z0-9_]*` (it could never be referenced) or one already registered
* in the SAME layer. Removed when the calling fiber is disposed; emits
* `system-prompt/change` on register/unregister.
@@ -623,13 +551,8 @@ export class SystemPrompt extends Service {
* yield it directly — exact identity nests the teardown in order.
*/
variable(name: string, provider: (context: AssembleContext) => string | undefined): () => Promise<void> | void {
const inputName: unknown = name
if (typeof inputName !== 'string') throw new TypeError('prompt variable name must be a string')
if (!VARIABLE_NAME.test(inputName)) {
throw new Error(`invalid prompt variable name "${inputName}" (must match ${String(VARIABLE_NAME)})`)
}
if (typeof provider !== 'function') {
throw new TypeError(`prompt variable "${inputName}" provider must be a function`)
if (!VARIABLE_NAME.test(name)) {
throw new Error(`invalid prompt variable name "${name}" (must match ${String(VARIABLE_NAME)})`)
}
const scope = scopeOf(this.ctx)
const dispose = this.ctx.effect(function* (this: SystemPrompt) {
@@ -663,91 +586,6 @@ export class SystemPrompt extends Service {
return dispose
}
/**
* Protect named section/tool contributions from the assembly waterfall.
* The layer is decided by the calling context: a global protection applies
* to every assembly, while one registered through `agent.ctx` applies only
* to that agent's scope. The name's canonical registry/provider output is
* restored AFTER the whole waterfall, so listener registration order cannot
* strip, replace, duplicate, or fabricate it. Canonical absence is restored
* too: if the protected name is intentionally absent for an assembly, a
* listener-injected entry with that name is removed. Each optional field and
* array slot is read once; non-array fields or non-string names reject before
* effect storage, and the accepted deduplicated arrays are frozen. During
* finalization each waterfall-produced entry name is likewise read once into
* an owned data record, so a stateful getter cannot look unprotected during
* filtering and later impersonate a protected name. An empty protection
* throws because it cannot affect output.
* Removed with the calling fiber and emits `system-prompt/change` on
* registration/unregistration. A global section protection also reserves the
* name against scoped section shadows; registering protection when such a
* shadow already exists fails loudly instead of protecting the wrong owner.
* @param protection - section and/or tool names whose canonical presence and definitions are restored after the waterfall.
* @returns the exact Cordis effect disposer that removes the protection.
*/
protect(protection: PromptProtection): () => Promise<void> | void {
const input: unknown = protection
if (typeof input !== 'object' || input === null) {
throw new TypeError('systemPrompt.protect() requires a protection object')
}
const accepted = input as PromptProtection
const inputSections = accepted.sections
const inputTools = accepted.tools
const sections = inputSections === undefined
? undefined
: snapshotProtectionNames(inputSections, 'sections')
const tools = inputTools === undefined
? undefined
: snapshotProtectionNames(inputTools, 'tools')
const scope = scopeOf(this.ctx)
const snapshot: PromptProtection = Object.freeze({
...sections !== undefined ? { sections } : {},
...tools !== undefined ? { tools } : {},
})
if ((snapshot.sections?.length ?? 0) === 0 && (snapshot.tools?.length ?? 0) === 0) {
throw new Error('systemPrompt.protect() requires at least one section or tool name')
}
if (scope === undefined && snapshot.sections !== undefined) {
const protectedSections = new Set(snapshot.sections)
const conflicts = [...this.scopedSections.values()]
.flatMap(layer => layer.filter(section => protectedSections.has(section.name)).map(section => section.name))
if (conflicts.length > 0) {
throw new Error(`systemPrompt.protect() cannot globally protect section${conflicts.length > 1 ? 's' : ''} ${[...new Set(conflicts)].map(name => `"${name}"`).join(', ')} while scoped shadows are registered`)
}
}
const dispose = this.ctx.effect(function* (this: SystemPrompt) {
const layer = scope === undefined
? this.protections
: this.scopedProtections.get(scope) ?? (() => {
const created: PromptProtection[] = []
this.scopedProtections.set(scope, created)
return created
})()
layer.push(snapshot)
yield () => {
const index = layer.indexOf(snapshot)
/* v8 ignore next 3 -- defensive: protection was registered, so indexOf is guaranteed >= 0 */
if (index >= 0) layer.splice(index, 1)
if (scope !== undefined && layer.length === 0) this.scopedProtections.delete(scope)
this.ctx.emit('system-prompt/change')
}
this.ctx.emit('system-prompt/change')
}.bind(this), 'systemPrompt.protect()')
return dispose
}
/** Resolve the owner-final names registered for one assembly scope. */
private protectedNames(scope: ScopeKey | undefined): { sections: Set<string>; tools: Set<string> } {
const records = [
...this.protections,
...(scope === undefined ? [] : this.scopedProtections.get(scope)) ?? [],
]
return {
sections: new Set(records.flatMap(record => record.sections ?? [])),
tools: new Set(records.flatMap(record => record.tools ?? [])),
}
}
/**
* Assemble the current prompt for one caller: the global layer merged with
* {@link AssembleContext.scope}'s layer (scoped sections/variables SHADOW
@@ -760,14 +598,12 @@ export class SystemPrompt extends Service {
* the providers' `knownNames` universe rejects the assembly, while a known
* name restricted away for this scope is a normal absence), and every
* visible variable resolved against `context` into `assembly.variables`.
* Each provider result and schema field is read once; those same captured
* names drive both `toolOrder` validation and the model-visible collection.
* Tool schemas are deep-cloned because adapters and request waterfalls may
* mutate schema objects. Runs through the `system-prompt/assemble`
* Tool schemas are detached because assembly waterfalls may mutate them.
* Runs through the `system-prompt/assemble`
* waterfall, giving listeners the opportunity to mutate or replace the
* assembly, then restores every visible {@link PromptProtection} from the
* pre-waterfall canonical assembly. Like the sections' `order` sort, tool
* canonicalization happens on the initial assembly; unprotected listener
* assembly, then restores every contribution whose owner declared it final
* from the pre-waterfall canonical assembly. Like the sections' `order` sort, tool
* canonicalization happens on the initial assembly; ordinary listener
* output owns its own determinism. Await the result before reading the
* assembly values — waterfall listeners may be async.
* Interpolation happens later, in {@link renderPrompt}.
@@ -780,10 +616,6 @@ export class SystemPrompt extends Service {
// (`assemble().catch(...)` would miss it).
async assemble(context: AssembleContext = {}): Promise<PromptAssembly> {
const scope = context.scope
// Protection is a registry input too: snapshot which names are protected
// at assembly start. Registrations that land while an async waterfall is
// in flight affect the NEXT assembly, matching the other registries.
const protectedNames = this.protectedNames(scope)
// Variables: global layer first, then the scope's layer OVERWRITES
// same-named entries (shadowing — a per-agent value wins for that agent).
const variables: Record<string, string | undefined> = {}
@@ -803,6 +635,11 @@ export class SystemPrompt extends Service {
for (const section of (scope === undefined ? [] : this.scopedSections.get(scope)) ?? []) {
sectionByName.set(section.name, section)
}
const ownerFinalSections = new Set(
[...sectionByName.values()]
.filter(section => section.ownerFinal === true)
.map(section => section.name),
)
// Tools: consult the global providers plus the scope's, each with this
// assembly's context. `schemas` are what the model may see (already
// post-restriction, per provider); `knownNames` (defaulting to the
@@ -815,45 +652,18 @@ export class SystemPrompt extends Service {
]
const collected: ToolSchema[] = []
const knownNames = new Set<string>()
const ownerFinalTools = new Set<string>()
for (const provider of providers) {
const result = provider(context)
// One provider result snapshot: `schemas`, `knownNames`, and each schema
// field may be accessor-backed. The same captured names must drive both
// toolOrder validation and the model-visible collection.
const inputSchemas = result.schemas
const inputKnownNames = result.knownNames
const schemas = inputSchemas.map((tool, index): ToolSchema => {
const name = tool.name
const description = tool.description
const inputParameters = tool.parameters
if (typeof name !== 'string') {
throw new TypeError(`system prompt tool schema at index ${index} name must be a string`)
}
if (typeof description !== 'string') {
throw new TypeError(`system prompt tool "${name}" description must be a string`)
}
const parameters = snapshotJsonValue(inputParameters)
if (parameters === undefined) {
throw new TypeError(`system prompt tool "${name}" parameters must be losslessly JSON-serializable`)
}
return { name, description, parameters }
})
let acceptedKnownNames: string[]
if (inputKnownNames === undefined) {
acceptedKnownNames = schemas.map(tool => tool.name)
} else {
if (!Array.isArray(inputKnownNames)) {
throw new TypeError('system prompt tool provider knownNames must be an array of strings')
}
acceptedKnownNames = Array.from(inputKnownNames, (name) => {
if (typeof name !== 'string') {
throw new TypeError('system prompt tool provider knownNames must be an array of strings')
}
return name
})
}
const schemas = result.schemas.map(({ name, description, parameters }): ToolSchema => ({
name,
description,
parameters: structuredClone(parameters),
}))
const acceptedKnownNames = result.knownNames ?? schemas.map(tool => tool.name)
collected.push(...schemas)
for (const name of acceptedKnownNames) knownNames.add(name)
for (const name of result.ownerFinalNames ?? []) ownerFinalTools.add(name)
}
const assembly: PromptAssembly = {
sections: [...sectionByName.values()]
@@ -866,11 +676,11 @@ export class SystemPrompt extends Service {
tools: orderTools(collected, this.toolOrder, knownNames),
variables,
}
// Snapshot only the fields protection can restore. The waterfall receives
// Snapshot only the owner-final fields. The waterfall receives
// `assembly` by reference and may mutate it or return a replacement; these
// independent snapshots remain the authoritative registry product.
const canonicalSections = protectedNames.sections.size > 0 ? structuredClone(assembly.sections) : undefined
const canonicalTools = protectedNames.tools.size > 0 ? structuredClone(assembly.tools) : undefined
const canonicalSections = ownerFinalSections.size > 0 ? structuredClone(assembly.sections) : undefined
const canonicalTools = ownerFinalTools.size > 0 ? structuredClone(assembly.tools) : undefined
const result = await this.ctx.waterfall(
scopeTarget(this, scope), 'system-prompt/assemble', assembly, context,
() => Promise.resolve(assembly),
@@ -881,10 +691,10 @@ export class SystemPrompt extends Service {
return {
...result,
...canonicalSections !== undefined
? { sections: restoreProtected(canonicalSections, result.sections, protectedNames.sections) }
? { sections: restoreOwnerFinal(canonicalSections, result.sections, ownerFinalSections) }
: {},
...canonicalTools !== undefined
? { tools: restoreProtected(canonicalTools, result.tools, protectedNames.tools) }
? { tools: restoreOwnerFinal(canonicalTools, result.tools, ownerFinalTools) }
: {},
}
}