Reorganize packages into a modular hierarchy
Move the 18 flat packages/<name> packages into role-grouped dirs: core/, llm/, bash/, session-persistence/, ui/, support/. Group dirs are pure containers; each package keeps its @deepseek-ai/dsh-* name. Collapse the per-package tsconfig paths maps (base + typecheck) into one @deepseek-ai/dsh-* wildcard with a candidate per group, and derive the publint list from the hierarchy. Update all depth-coupled globs/configs (workspace, tsdown, vitest, eslint, knip, tsconfig includes/refs, per-package tsconfigs, generators, doc-script scopes, type-equiv manifest) and the cross-package/script relative imports in tests. Fix doc-typecheck's workspacePaths() to parse tsconfig JSONC via the TypeScript API instead of a regex comment-strip, which corrupted the new wildcard `/*/` path candidates. WIP: doc cross-links and package/RFC docs still to update.
This commit is contained in:
148
packages/core/system-prompt/src/index.ts
Normal file
148
packages/core/system-prompt/src/index.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* System prompt assembly registry. Plugins contribute ordered text sections and
|
||||
* tool schema providers; `assemble()` collates them through a waterfall that
|
||||
* runs once per step.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-system-prompt
|
||||
*/
|
||||
|
||||
import { Context, Service } from 'cordis'
|
||||
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
systemPrompt: SystemPrompt
|
||||
}
|
||||
|
||||
interface Events {
|
||||
/**
|
||||
* Waterfall around prompt assembly — mutate or extend the
|
||||
* {@link PromptAssembly} (sections + tool schemas) before it is rendered.
|
||||
* Bound to the {@link SystemPrompt} service; call `next()` to delegate.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'system-prompt/assemble'(this: SystemPrompt, assembly: PromptAssembly, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
|
||||
/**
|
||||
* A section or tool provider was registered or unregistered (the assembly
|
||||
* inputs changed).
|
||||
* @mode emit
|
||||
*/
|
||||
'system-prompt/change'(): void
|
||||
}
|
||||
}
|
||||
|
||||
/** One contributed section of the system prompt. */
|
||||
export interface PromptSection {
|
||||
/** Unique name (diagnostics / dedup). */
|
||||
name: string
|
||||
/** Sections are concatenated in ascending order. */
|
||||
order: number
|
||||
/** Static text or a provider evaluated at each assembly. */
|
||||
text: string | (() => string)
|
||||
}
|
||||
|
||||
/**
|
||||
* The assembled prompt.
|
||||
*
|
||||
* Tool schemas are part of the assembly by design: "what the model is told it
|
||||
* can do" is one coherent thing managed here, even though adapters transmit
|
||||
* `tools` as a separate wire field rather than prompt text.
|
||||
*
|
||||
* Merge-extensible: plugins can declare extra fields on this interface.
|
||||
*/
|
||||
export interface PromptAssembly {
|
||||
sections: PromptSection[]
|
||||
tools: ToolSchema[]
|
||||
}
|
||||
|
||||
/** Renders the text part of an assembly (sections joined by blank lines). */
|
||||
export function renderPrompt(assembly: PromptAssembly): string {
|
||||
return assembly.sections
|
||||
.map(section => typeof section.text === 'function' ? section.text() : section.text)
|
||||
.filter(text => text.length > 0)
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry service (`ctx.systemPrompt`): plugins contribute ordered text
|
||||
* sections and tool-schema providers; the agent loop calls `assemble()` once
|
||||
* per step.
|
||||
*/
|
||||
export class SystemPrompt extends Service {
|
||||
private sections: PromptSection[] = []
|
||||
private toolProviders: (() => ToolSchema[])[] = []
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'systemPrompt')
|
||||
}
|
||||
|
||||
/**
|
||||
* Contribute a text section to the system prompt. Order is determined by
|
||||
* `section.order` (ascending). The section is removed when the calling
|
||||
* fiber is disposed. Emits `system-prompt/change` on register/unregister.
|
||||
*/
|
||||
section(section: PromptSection): () => void {
|
||||
const dispose = this.ctx.effect(function* (this: SystemPrompt) {
|
||||
this.sections.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 = this.sections.indexOf(section)
|
||||
/* v8 ignore next 3 -- defensive: section was registered, so indexOf is guaranteed >= 0 */
|
||||
if (index >= 0) this.sections.splice(index, 1)
|
||||
this.ctx.emit('system-prompt/change')
|
||||
}
|
||||
this.ctx.emit('system-prompt/change')
|
||||
}.bind(this), 'systemPrompt.section()')
|
||||
// ctx.effect's disposer returns Promise<void>; our disposer API is
|
||||
// synchronous fire-and-forget — discard the (always-resolved) promise.
|
||||
return () => void dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Contribute a tool-schema provider that is evaluated at each assembly
|
||||
* call (so it can reflect the live registry state). The provider is
|
||||
* removed when the calling fiber is disposed. Emits `system-prompt/change`.
|
||||
*/
|
||||
tools(provider: () => ToolSchema[]): () => void {
|
||||
const dispose = this.ctx.effect(function* (this: SystemPrompt) {
|
||||
this.toolProviders.push(provider)
|
||||
// Yield the rollback BEFORE emitting `system-prompt/change` (see section()).
|
||||
yield () => {
|
||||
const index = this.toolProviders.indexOf(provider)
|
||||
/* v8 ignore next 3 -- defensive: provider was registered, so indexOf is guaranteed >= 0 */
|
||||
if (index >= 0) this.toolProviders.splice(index, 1)
|
||||
this.ctx.emit('system-prompt/change')
|
||||
}
|
||||
this.ctx.emit('system-prompt/change')
|
||||
}.bind(this), 'systemPrompt.tools()')
|
||||
// ctx.effect's disposer returns Promise<void>; our disposer API is
|
||||
// synchronous fire-and-forget — discard the (always-resolved) promise.
|
||||
return () => void dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the current prompt (sections sorted by order, tools collected
|
||||
* from all providers). Section records are top-level clones (the `text`
|
||||
* provider may be a function and is intentionally shared); tool schemas are
|
||||
* deep-cloned because adapters and request waterfalls may mutate schema
|
||||
* objects. Runs through the `system-prompt/assemble` waterfall, giving
|
||||
* listeners the opportunity to mutate or replace the assembly before it
|
||||
* reaches the model. Await the result before reading the assembly values —
|
||||
* waterfall listeners may be async.
|
||||
*/
|
||||
assemble(): Promise<PromptAssembly> {
|
||||
const assembly: PromptAssembly = {
|
||||
sections: this.sections
|
||||
.map(section => ({ ...section }))
|
||||
.sort((a, b) => a.order - b.order),
|
||||
tools: this.toolProviders.flatMap(provider =>
|
||||
provider().map(tool => ({ ...tool, parameters: structuredClone(tool.parameters) }))),
|
||||
}
|
||||
return this.ctx.waterfall(this, 'system-prompt/assemble', assembly, () => Promise.resolve(assembly))
|
||||
}
|
||||
}
|
||||
|
||||
export default SystemPrompt
|
||||
Reference in New Issue
Block a user