fix: suppress runtime context in minimal profiles
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/core/system-prompt/README.md
|
||||
README.md: cedda783d549633f5be9765a9a074e968d99500d
|
||||
README.zh.md: ab3dbffa099549cb1d5cc2713d903038e917e695
|
||||
README.md: 6d0b43322ea0ccf4dddef404c4c862e014819696
|
||||
README.zh.md: 08da218ef2fe07ffa63f246dff5382474070c4d9
|
||||
|
||||
@@ -9,6 +9,7 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `includeHarnessIdentity` | `true` | Include the fixed `You are an AI agent powered by the DeepSeek Harness SDK.` order-−100 opener. Set false only when a compatibility deployment owns the complete system prompt. |
|
||||
| `includeRuntimeContext` | `true` | Include ordered dynamic contexts in assembly. When false, context providers are not evaluated and contexts added by `system-prompt/assemble` listeners are discarded after the waterfall; other services and their enforcement remain active. |
|
||||
| `persona` | `''` | The global deployment-persona default: the ONE config-authored prompt fragment, rendered as the order-0 `deployment:persona` section unless an agent-scoped contribution shadows it. A template — complete `{{…}}` groups are interpreted strictly against the registered variables (the shipped loop registers `{{model}}`/`{{cwd}}`), with no escape syntax for literal braces yet. Empty ⇒ the section is dropped at render. |
|
||||
| `toolOrder` | — | Explicit model-facing tool order, as a list of `ToolSchema.name`s with one `'<unlisted-tools>'` rest entry (`TOOL_ORDER_REST`): listed tools take their listed position, unlisted tools land at the rest entry in lexicographic name order. Absent ⇒ plain lexicographic name order. Applied to the collected tools BEFORE the `system-prompt/assemble` waterfall — like the sections' `order` sort, it canonicalizes what the registry contributed (registration order is a plugin-load artifact), and a waterfall listener that mutates the list owns the determinism of what it emits. Misconfiguration fails loud: a list without exactly one rest entry, or with duplicates, throws at load; a listed name with no registered tool rejects every `assemble()`; a tool provider returning the reserved rest-entry name also rejects. Under the shipped loop the turn fails before any model request. Why a central list and not per-plugin weights: [Explicit model-facing tool order](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md). |
|
||||
|
||||
@@ -17,9 +18,11 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem
|
||||
### Public API
|
||||
|
||||
- `ctx.systemPrompt.section(section: PromptSection): () => void` Contribute a section. The layer is the calling context's scope: `agent.ctx` contributes to that agent alone, shadowing a same-named global section there. A `complete: true` section becomes the exact complete prompt after the assembly waterfall; more than one effective complete section rejects assembly. Duplicate names within one layer and non-finite orders throw. Disposed with the calling fiber.
|
||||
- `ctx.systemPrompt.context(context: PromptContext): () => void` Contribute ordered dynamic context for the calling scope. Providers are evaluated for each eligible assembly and become a sourced runtime-context snapshot in model history under the shipped loop.
|
||||
- `ctx.systemPrompt.suppressRuntimeContext(): () => void` Suppress every dynamic-context contribution for the calling scope. Multiple registrations compose independently; disposing the returned effect restores context when no suppressor remains.
|
||||
- `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void` Contribute tool schemas, evaluated at each assembly with that assembly's context. `ToolProviderResult` = `{ schemas, knownNames? }`: `schemas` is the post-restriction visible set; `knownNames` is the pre-restriction universe used by `toolOrder`. A provider must not return a schema named `TOOL_ORDER_REST`. Scoped providers are consulted only for their scope's assemblies. Disposed with the calling fiber.
|
||||
- `ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => void` Contribute a prompt variable, referenced from section text as `{{name}}`. Scoped variables shadow a same-named global for that agent. Duplicate-in-layer or unreferenceable names throw; `undefined` means "no value for this assembly". Disposed with the calling fiber.
|
||||
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>` Assemble the prompt for one caller: the global layer merged with `context.scope`'s layer, with tool schemas detached before the transform waterfall. Runs through the scope-filtered `system-prompt/assemble` waterfall, then restores an effective complete section as the sole prompt section. An optional `context.signal` explicitly controls this assembly request; providers and listeners may cooperate with it but must not retain it for another turn. Rejects for multiple complete sections, when a configured `toolOrder` names a tool outside the providers' `knownNames` universe, or when a provider returns the reserved rest-entry name.
|
||||
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>` Assemble the prompt for one caller: the global layer merged with `context.scope`'s layer, with tool schemas detached before the transform waterfall. Runs through the scope-filtered `system-prompt/assemble` waterfall, then restores an effective complete section as the sole prompt section and enforces any active runtime-context suppressor. An optional `context.signal` explicitly controls this assembly request; providers and listeners may cooperate with it but must not retain it for another turn. Rejects for multiple complete sections, when a configured `toolOrder` names a tool outside the providers' `knownNames` universe, or when a provider returns the reserved rest-entry name.
|
||||
|
||||
### Live events
|
||||
|
||||
@@ -49,7 +52,7 @@ Design rationale: [the prompt-variables Agent Note](../../../.agents/notes/imple
|
||||
|
||||
#### What the model sees
|
||||
|
||||
By default every assembly starts with the harness identity below, then the configured persona and ordered plugin sections after strict variable interpolation. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt and tool schemas unless one effective section declares itself complete; that exact section then becomes the whole system prompt while the waterfall's contexts, tools, and variables remain.
|
||||
By default every assembly starts with the harness identity below, then the configured persona and ordered plugin sections after strict variable interpolation. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt and tool schemas unless one effective section declares itself complete; that exact section then becomes the whole system prompt while the waterfall's contexts, tools, and variables remain. Ordered dynamic contexts are separate from system-prompt sections and become sourced user-role snapshots only when present. `includeRuntimeContext: false` or a scoped suppressor removes all such contexts, including listener additions, without disabling the services that own the underlying policy or state.
|
||||
|
||||
##### Harness identity
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
| 键 | 默认值 | 含义 |
|
||||
|---|---|---|
|
||||
| `includeHarnessIdentity` | `true` | 是否包含顺序为 −100 的固定开场白 `You are an AI agent powered by the DeepSeek Harness SDK.`。仅当兼容部署拥有完整系统提示词时设为 false。 |
|
||||
| `includeRuntimeContext` | `true` | 是否在组装中包含有序动态上下文。设为 false 时不会求值上下文提供方,并会在 waterfall 后丢弃 `system-prompt/assemble` 监听器添加的上下文;其他服务及其强制机制仍然生效。 |
|
||||
| `persona` | `''` | 全局部署 persona 默认值:唯一由配置提供的提示词片段,渲染为顺序为 0 的 `deployment:persona` 段,除非 agent 作用域的贡献将其遮蔽。它是模板,完整的 `{{…}}` 组会严格按已注册变量解释(随附循环注册 `{{model}}`/`{{cwd}}`),目前没有表达字面量花括号的转义语法。为空 ⇒ 渲染时删除该段。 |
|
||||
| `toolOrder` | 无 | 显式的面向模型工具顺序:一个 `ToolSchema.name` 列表,包含一个 `'<unlisted-tools>'` 其余项(`TOOL_ORDER_REST`)。已列工具占据列出的位置;未列工具按名称字典序落在其余项位置。缺席 ⇒ 直接按名称字典序排列。在 `system-prompt/assemble` waterfall(瀑布式事件)之前应用于已收集工具;与段的 `order` 排序一样,它会规范化注册表贡献的内容(注册顺序是插件加载产物),而修改列表的 waterfall 监听器拥有其输出的确定性。配置错误会明确失败:列表没有恰好一个其余项或存在重复项,会在加载时抛出;已列名称没有对应已注册工具,会使每次 `assemble()` 被拒绝;工具提供方返回保留的其余项名称也会被拒绝。在随附循环下,轮次会在任何模型请求前失败。为何采用中心列表而非每插件权重,见[显式面向模型工具顺序](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md)。 |
|
||||
|
||||
@@ -17,9 +18,11 @@
|
||||
### 公开 API
|
||||
|
||||
- `ctx.systemPrompt.section(section: PromptSection): () => void`:贡献一个段。层由调用上下文的作用域决定:`agent.ctx` 只为该 agent 贡献,并在该处遮蔽同名全局段。一个 `complete: true` 段会在组装 waterfall 之后成为精确的完整提示词;有效 complete 段超过一个时,组装会被拒绝。同一层中的重复名称和非有限顺序会抛出。随调用 fiber 一并 dispose(资源释放)。
|
||||
- `ctx.systemPrompt.context(context: PromptContext): () => void`:为调用作用域贡献有序动态上下文。每次符合条件的组装都会求值提供方,并在随附循环下成为模型历史中带来源的 runtime-context 快照。
|
||||
- `ctx.systemPrompt.suppressRuntimeContext(): () => void`:抑制调用作用域的所有动态上下文贡献。多个注册会独立组合;只有当不再存在抑制器时,dispose 返回的 effect 才会恢复上下文。
|
||||
- `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void`:贡献工具 schema;每次组装时使用该次组装的上下文求值。`ToolProviderResult` = `{ schemas, knownNames? }`:`schemas` 是限制后的可见集合;`knownNames` 是限制前由 `toolOrder` 使用的全集。提供方不得返回名为 `TOOL_ORDER_REST` 的 schema。带作用域提供方只在其作用域的组装中查询。随调用 fiber 一并 dispose。
|
||||
- `ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => void`:贡献提示词变量,在段文本中以 `{{name}}` 引用。带作用域变量会为该 agent 遮蔽同名全局变量。同层重复或无法引用的名称会抛出;`undefined` 表示「本次组装没有值」。随调用 fiber 一并 dispose。
|
||||
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>`:为一个调用方组装提示词:将全局层与 `context.scope` 的层合并,并在变换 waterfall 前分离工具 schema。它经过按作用域筛选的 `system-prompt/assemble` waterfall,之后将一个有效的 complete 段恢复为唯一的提示词段落。可选的 `context.signal` 显式控制本次组装请求;提供方与监听器可以配合该信号,但不得将它保留给另一轮次。存在多个 complete 段、已配置的 `toolOrder` 指名提供方 `knownNames` 全集以外的工具,或提供方返回保留的其余项名称时,调用会被拒绝。
|
||||
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>`:为一个调用方组装提示词:将全局层与 `context.scope` 的层合并,并在变换 waterfall 前分离工具 schema。它经过按作用域筛选的 `system-prompt/assemble` waterfall,之后将一个有效的 complete 段恢复为唯一的提示词段落,并实施任何活动的 runtime-context 抑制器。可选的 `context.signal` 显式控制本次组装请求;提供方与监听器可以配合该信号,但不得将它保留给另一轮次。存在多个 complete 段、已配置的 `toolOrder` 指名提供方 `knownNames` 全集以外的工具,或提供方返回保留的其余项名称时,调用会被拒绝。
|
||||
|
||||
<a id="live-events"></a>
|
||||
|
||||
@@ -51,7 +54,7 @@
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
默认情况下,每次组装都从下方 harness 身份开始,然后在严格变量插值后追加已配置 persona 与有序插件段。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段和变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词与工具 schema,除非一个有效段声明自身为 complete;此时,该确切段落会成为完整的系统提示词,而 waterfall 得到的上下文、工具和变量保持不变。
|
||||
默认情况下,每次组装都从下方 harness 身份开始,然后在严格变量插值后追加已配置 persona 与有序插件段。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段和变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词与工具 schema,除非一个有效段声明自身为 complete;此时,该确切段落会成为完整的系统提示词,而 waterfall 得到的上下文、工具和变量保持不变。有序动态上下文与系统提示词段落分离,只在存在时才会成为带来源的 user 角色快照。`includeRuntimeContext: false` 或带作用域的抑制器会移除所有这类上下文,包括监听器添加的内容,但不会禁用拥有底层策略或状态的服务。
|
||||
|
||||
##### harness 身份
|
||||
|
||||
|
||||
@@ -186,6 +186,8 @@ function compareToolNames(a: ToolSchema, b: ToolSchema): number {
|
||||
export interface Config {
|
||||
/** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */
|
||||
includeHarnessIdentity?: boolean
|
||||
/** Include dynamic runtime-context snapshots in model history (default true). */
|
||||
includeRuntimeContext?: boolean
|
||||
/**
|
||||
* Deployment-wide order-0 persona template. A scoped section named
|
||||
* `deployment:persona` shadows it; `{{variable}}` references are strict.
|
||||
@@ -302,6 +304,7 @@ type VariableProvider = (context: AssembleContext) => string | undefined
|
||||
class PromptLayer implements ScopeLayer {
|
||||
readonly sections: NamedEntries<PromptSection>
|
||||
readonly contexts: NamedEntries<PromptContext>
|
||||
readonly runtimeContextSuppressors = new AnonymousEntries<true>()
|
||||
readonly toolProviders = new AnonymousEntries<ToolProvider>()
|
||||
readonly variables: NamedEntries<VariableProvider>
|
||||
|
||||
@@ -325,6 +328,7 @@ class PromptLayer implements ScopeLayer {
|
||||
isEmpty(): boolean {
|
||||
return this.sections.isEmpty()
|
||||
&& this.contexts.isEmpty()
|
||||
&& this.runtimeContextSuppressors.isEmpty()
|
||||
&& this.toolProviders.isEmpty()
|
||||
&& this.variables.isEmpty()
|
||||
}
|
||||
@@ -334,6 +338,7 @@ class PromptLayer implements ScopeLayer {
|
||||
export class SystemPrompt extends Service {
|
||||
static Config: z<Config> = z.object({
|
||||
includeHarnessIdentity: z.boolean().default(true),
|
||||
includeRuntimeContext: z.boolean().default(true),
|
||||
persona: z.string().default(''),
|
||||
// Preserve omission because an explicit empty order lacks the rest marker.
|
||||
toolOrder: z.array(z.string()).default(undefined as unknown as string[]),
|
||||
@@ -362,6 +367,7 @@ export class SystemPrompt extends Service {
|
||||
// The fallback narrows the optional input type; the schema already defaults it.
|
||||
text: config.persona ?? '',
|
||||
})
|
||||
if (!(config.includeRuntimeContext ?? true)) this.suppressRuntimeContext()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,6 +406,20 @@ export class SystemPrompt extends Service {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress every dynamic runtime-context contribution in the calling
|
||||
* context's scope without changing the services that own or enforce those
|
||||
* facts. Multiple suppressors remain independently disposable.
|
||||
* @returns the exact Cordis effect disposer.
|
||||
*/
|
||||
suppressRuntimeContext(): () => void {
|
||||
return this.layers.effect(
|
||||
this.ctx,
|
||||
layer => layer.runtimeContextSuppressors.append(true),
|
||||
{ label: 'systemPrompt.suppressRuntimeContext()' },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a tool-schema provider in the calling context's scope. Global and
|
||||
* matching scoped providers both contribute; returning the reserved
|
||||
@@ -446,13 +466,16 @@ export class SystemPrompt extends Service {
|
||||
// Keep configuration failures on the declared asynchronous error path.
|
||||
async assemble(context: AssembleContext = {}): Promise<PromptAssembly> {
|
||||
const scope = context.scope
|
||||
const scopeLayers = this.layers.chainLayers(scope)
|
||||
const runtimeContextSuppressed = !this.layers.global.runtimeContextSuppressors.isEmpty()
|
||||
|| scopeLayers.some(layer => !layer.runtimeContextSuppressors.isEmpty())
|
||||
// Scoped variables shadow globals.
|
||||
const variables: Record<string, string | undefined> = {}
|
||||
for (const [name, provider] of this.layers.global.variables.entries()) {
|
||||
variables[name] = provider(context)
|
||||
}
|
||||
// Scope-chain variables, farthest first, so the nearest scope wins a name.
|
||||
for (const layer of this.layers.chainLayers(scope)) {
|
||||
for (const layer of scopeLayers) {
|
||||
for (const [name, provider] of layer.variables.entries()) {
|
||||
variables[name] = provider(context)
|
||||
}
|
||||
@@ -463,7 +486,7 @@ export class SystemPrompt extends Service {
|
||||
// Validate order against pre-restriction names while collecting visible schemas.
|
||||
const providers = [
|
||||
...this.layers.global.toolProviders.values(),
|
||||
...this.layers.chainLayers(scope).flatMap(layer => [...layer.toolProviders.values()]),
|
||||
...scopeLayers.flatMap(layer => [...layer.toolProviders.values()]),
|
||||
]
|
||||
const collected: ToolSchema[] = []
|
||||
const knownNames = new Set<string>()
|
||||
@@ -495,12 +518,14 @@ export class SystemPrompt extends Service {
|
||||
})
|
||||
const assembly: PromptAssembly = {
|
||||
sections,
|
||||
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,
|
||||
})),
|
||||
contexts: runtimeContextSuppressed
|
||||
? []
|
||||
: [...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),
|
||||
variables,
|
||||
}
|
||||
@@ -508,8 +533,12 @@ export class SystemPrompt extends Service {
|
||||
scopeTarget(this, scope), 'system-prompt/assemble', assembly, context,
|
||||
() => Promise.resolve(assembly),
|
||||
)
|
||||
if (completeSection === undefined) return transformed
|
||||
return { ...transformed, sections: [completeSection] }
|
||||
if (completeSection === undefined && !runtimeContextSuppressed) return transformed
|
||||
return {
|
||||
...transformed,
|
||||
sections: completeSection === undefined ? transformed.sections : [completeSection],
|
||||
contexts: runtimeContextSuppressed ? [] : transformed.contexts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,23 @@ describe('scoped cache-safe context', () => {
|
||||
expect(renderContextSnapshot(await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) })))
|
||||
.toContain('global policy')
|
||||
})
|
||||
|
||||
it('suppresses all context for one scope and restores it when disposed', async () => {
|
||||
const ctx = await mount()
|
||||
const scope = await mintScope(ctx, 'suppressed-context')
|
||||
const key = scopeKeyOf(scope)
|
||||
ctx.systemPrompt.context({ name: 'policy', order: 1, text: 'global policy' })
|
||||
const dispose = scope.ctx.systemPrompt.suppressRuntimeContext()
|
||||
|
||||
const suppressed = await ctx.systemPrompt.assemble({ scope: key })
|
||||
expect(suppressed.contexts).toEqual([])
|
||||
const global = await ctx.systemPrompt.assemble()
|
||||
expect(renderContextSnapshot(global)).toContain('global policy')
|
||||
|
||||
dispose()
|
||||
expect(renderContextSnapshot(await ctx.systemPrompt.assemble({ scope: key })))
|
||||
.toContain('global policy')
|
||||
})
|
||||
})
|
||||
|
||||
describe('scoped tool providers and toolOrder × restriction', () => {
|
||||
|
||||
@@ -49,6 +49,25 @@ describe('SystemPrompt', () => {
|
||||
expect(renderPrompt(assembly)).toBe('You are a helpful software engineer assistant.')
|
||||
})
|
||||
|
||||
it('can suppress runtime context without evaluating providers or accepting waterfall additions', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt, { includeRuntimeContext: false })
|
||||
let providerCalls = 0
|
||||
ctx.systemPrompt.context({
|
||||
name: 'policy',
|
||||
order: 0,
|
||||
text: () => `policy ${++providerCalls}`,
|
||||
})
|
||||
ctx.on('system-prompt/assemble', async (assembly, _context, next) => {
|
||||
assembly.contexts.push({ name: 'late', text: 'late context' })
|
||||
return next()
|
||||
})
|
||||
|
||||
const assembly = await ctx.systemPrompt.assemble()
|
||||
expect(assembly.contexts).toEqual([])
|
||||
expect(providerCalls).toBe(0)
|
||||
})
|
||||
|
||||
it('tolerates a schema-bypassing direct construction (persona omitted)', async () => {
|
||||
// ctx.plugin validates + defaults the config first; a direct construction
|
||||
// skips the schema, so the ctor's `?? ''` narrowing is what fires.
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/examples/agent-spine-demo/README.md
|
||||
README.md: 789715e53038f610d1e2db79cf56f9aabd681fac
|
||||
README.zh.md: 7a861297d76d18d5e55539334ca8e7ee5ffef640
|
||||
README.md: d45a128e786da18d2e2a41c1e4c048270a9c5cc7
|
||||
README.zh.md: ae9c523b53a13eae89a670b9177ed996b4fbb928
|
||||
|
||||
@@ -55,11 +55,11 @@ This applies the [Service Definition / Service provider / Consumer separation](.
|
||||
|
||||
```ts
|
||||
import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
|
||||
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, goals?, invariants? }
|
||||
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, goals?, invariants? }
|
||||
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
|
||||
```
|
||||
|
||||
The bundle forwards each field to the child that owns it. App packages supply any pre-created agents: headless and JSON-RPC compositions create `main`, while the ACP app creates agents on demand at `session/new`. Prompt, tool, title, skill, workspace-context, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `tasks.maxConcurrentTasksPerOwner` configures the local provider independently of the model-facing `toolTasks` controls. `pickSpineConfig()` copies only fields owned by this bundle, and conflicting `dshHome` values fail during composition.
|
||||
The bundle forwards each field to the child that owns it. App packages supply any pre-created agents: headless and JSON-RPC compositions create `main`, while the ACP app creates agents on demand at `session/new`. `includeRuntimeContext: false` is forwarded to `dsh-system-prompt` and suppresses all dynamic context snapshots for fresh sessions without disabling their policy services. Prompt, tool, title, skill, workspace-context, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `tasks.maxConcurrentTasksPerOwner` configures the local provider independently of the model-facing `toolTasks` controls. `pickSpineConfig()` copies only fields owned by this bundle, and conflicting `dshHome` values fail during composition.
|
||||
|
||||
For example, `{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` keeps the package-owned companions mounted but suppresses the blocked owner. Blocklist matches override allowlist matches; see [`dsh-invariants`](../../support/invariants/README.md) for regex and lifecycle rules.
|
||||
|
||||
|
||||
@@ -55,11 +55,11 @@
|
||||
|
||||
```ts
|
||||
import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
|
||||
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, goals?, invariants? }
|
||||
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, goals?, invariants? }
|
||||
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
|
||||
```
|
||||
|
||||
组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent:无头和 JSON-RPC 组合会创建 `main`,ACP 应用则在 `session/new` 按需创建 agent。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值;`tasks.maxConcurrentTasksPerOwner` 配置本地 Service provider,并与面向模型的 `toolTasks` 控制工具相互独立。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。
|
||||
组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent:无头和 JSON-RPC 组合会创建 `main`,ACP 应用则在 `session/new` 按需创建 agent。`includeRuntimeContext: false` 会转发给 `dsh-system-prompt`,为新建会话抑制所有动态上下文快照,但不禁用其策略服务。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值;`tasks.maxConcurrentTasksPerOwner` 配置本地 Service provider,并与面向模型的 `toolTasks` 控制工具相互独立。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。
|
||||
|
||||
例如,`{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` 会让包拥有的配套插件保持挂载,但抑制被阻止的拥有者。Blocklist 匹配优先于 allowlist 匹配;正则表达式与生命周期规则见 [`dsh-invariants`](../../support/invariants/README.md)。
|
||||
|
||||
|
||||
@@ -69,9 +69,10 @@ export interface GoalConfig {
|
||||
/**
|
||||
* Bundle config: each field forwarded verbatim to the child that owns it —
|
||||
* `agents` to the agent loop (an app that pre-creates no agents, like the ACP
|
||||
* bridge, simply omits it), `includeHarnessIdentity`, `persona`, and `toolOrder`
|
||||
* to the system-prompt plugin (the fixed opener, deployment persona, and explicit
|
||||
* model-facing tool order), the `tools` object to the tool registry (its presentation `mode`),
|
||||
* bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`,
|
||||
* `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener,
|
||||
* dynamic-context policy, deployment persona, and explicit model-facing tool
|
||||
* order), the `tools` object to the tool registry (its presentation `mode`),
|
||||
* `dshHome` to bash environment and local skill discovery, `sessionTitle` to
|
||||
* the fallback title service, `skills` to the
|
||||
* skill registry/local provider/tool consumer, `workspaceContext` to the
|
||||
@@ -95,6 +96,8 @@ export interface Config {
|
||||
maxParallelToolCalls?: AgentLoopConfig['maxParallelToolCalls']
|
||||
/** Whether the system prompt includes the fixed Harness identity (default true). */
|
||||
includeHarnessIdentity?: SystemPromptConfig['includeHarnessIdentity']
|
||||
/** Whether model history includes dynamic runtime-context snapshots (default true). */
|
||||
includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext']
|
||||
/** The deployment persona (see dsh-system-prompt's `Config`). */
|
||||
persona?: SystemPromptConfig['persona']
|
||||
/** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */
|
||||
@@ -180,6 +183,7 @@ export function pickSpineConfig(config: Omit<Config, 'agents'>): Omit<Config, 'a
|
||||
return {
|
||||
...config.maxParallelToolCalls !== undefined ? { maxParallelToolCalls: config.maxParallelToolCalls } : {},
|
||||
...config.includeHarnessIdentity !== undefined ? { includeHarnessIdentity: config.includeHarnessIdentity } : {},
|
||||
...config.includeRuntimeContext !== undefined ? { includeRuntimeContext: config.includeRuntimeContext } : {},
|
||||
...config.persona !== undefined ? { persona: config.persona } : {},
|
||||
...config.toolOrder !== undefined ? { toolOrder: config.toolOrder } : {},
|
||||
...config.tools !== undefined ? { tools: config.tools } : {},
|
||||
@@ -220,6 +224,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
// Owner schemas resolve defaults; forward toolOrder only when explicitly set.
|
||||
ctx.plugin(SystemPrompt, {
|
||||
includeHarnessIdentity: config.includeHarnessIdentity ?? true,
|
||||
includeRuntimeContext: config.includeRuntimeContext ?? true,
|
||||
persona: config.persona ?? '',
|
||||
...config.toolOrder !== undefined ? { toolOrder: config.toolOrder } : {},
|
||||
})
|
||||
|
||||
@@ -712,6 +712,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
it('can omit the bundled bash tool and Harness identity for a compatibility deployment', async () => {
|
||||
const ctx = await mount({
|
||||
includeHarnessIdentity: false,
|
||||
includeRuntimeContext: false,
|
||||
persona: 'You are a helpful software engineer assistant.',
|
||||
workspaceContext: false,
|
||||
skills: { enabled: false },
|
||||
@@ -720,6 +721,8 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
}, true)
|
||||
|
||||
expect(ctx.tools.schemas()).toEqual([])
|
||||
ctx.systemPrompt.context({ name: 'policy', order: 0, text: 'hidden policy' })
|
||||
expect((await ctx.systemPrompt.assemble()).contexts).toEqual([])
|
||||
expect(renderPrompt(await ctx.systemPrompt.assemble()))
|
||||
.toBe('You are a helpful software engineer assistant.')
|
||||
|
||||
@@ -729,7 +732,9 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
it('picks shared spine config without leaking entry-point fields', () => {
|
||||
const appConfig = {
|
||||
model: 'entrypoint-only',
|
||||
maxParallelToolCalls: 3,
|
||||
includeHarnessIdentity: false,
|
||||
includeRuntimeContext: false,
|
||||
persona: 'You are merged.',
|
||||
toolOrder: ['zulu'],
|
||||
tools: { mode: 'native' as const },
|
||||
@@ -741,10 +746,13 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
tasks: { maxConcurrentTasksPerOwner: 4 },
|
||||
toolTasks: false as const,
|
||||
invariants: { enabled: false },
|
||||
goals: false as const,
|
||||
}
|
||||
|
||||
expect(agentCore.pickSpineConfig(appConfig)).toEqual({
|
||||
maxParallelToolCalls: appConfig.maxParallelToolCalls,
|
||||
includeHarnessIdentity: appConfig.includeHarnessIdentity,
|
||||
includeRuntimeContext: appConfig.includeRuntimeContext,
|
||||
persona: appConfig.persona,
|
||||
toolOrder: appConfig.toolOrder,
|
||||
tools: appConfig.tools,
|
||||
@@ -756,6 +764,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
tasks: appConfig.tasks,
|
||||
toolTasks: appConfig.toolTasks,
|
||||
invariants: appConfig.invariants,
|
||||
goals: appConfig.goals,
|
||||
})
|
||||
expect(agentCore.pickSpineConfig({ workspaceContext: false })).toEqual({ workspaceContext: false })
|
||||
})
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/preset/persona/README.md
|
||||
README.md: 742141e65fa8d50b89e6b74e6d21aa8c5bfe98cd
|
||||
README.zh.md: 61112d1091e428d8c92aa056b7aaa272d3bbbc1d
|
||||
README.md: 978d01ddbd52095bc2486d27d08fde2d4a8fa79b
|
||||
README.zh.md: d36af6c028f1258d044608535c6da4eaf2fb92e8
|
||||
|
||||
@@ -16,8 +16,9 @@ Mounting this row outside an agent scope collides with the registry's own `deplo
|
||||
|---|---|---|
|
||||
| `text` | required | Persona prose rendered as the `deployment:persona` section |
|
||||
| `complete` | `false` | Restore this persona after assembly as the only system-prompt section |
|
||||
| `includeRuntimeContext` | `true` | Include dynamic runtime-context snapshots for this agent scope; false suppresses every context contribution without disabling its owning services |
|
||||
|
||||
`text` is a template, like any prompt section: complete `{{…}}` groups resolve strictly against registered prompt variables when the prompt renders, not when it assembles. Empty text still occupies the slot, so it shadows the deployment persona away entirely and then disappears at render. With `complete: true`, assembly still resolves contexts, tools, variables, and cooperative listeners, then the prompt registry restores this exact persona as the sole section; no identity, tool guidance, or listener can append prompt text.
|
||||
`text` is a template, like any prompt section: complete `{{…}}` groups resolve strictly against registered prompt variables when the prompt renders, not when it assembles. Empty text still occupies the slot, so it shadows the deployment persona away entirely and then disappears at render. With `complete: true`, assembly still resolves contexts, tools, variables, and cooperative listeners, then the prompt registry restores this exact persona as the sole section; no identity, tool guidance, or listener can append prompt text. With `includeRuntimeContext: false`, context providers are not evaluated for this scope and contexts added by assembly listeners are discarded.
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -25,7 +26,7 @@ Mounting this row outside an agent scope collides with the registry's own `deplo
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The `deployment:persona` section at order 0, immediately after the harness identity opener, carrying exactly this row's configured `text` with prompt variables resolved. For an agent whose preset mounts this row, it replaces whatever persona the deployment configured. In complete mode, the model sees only this rendered section as its system prompt.
|
||||
The `deployment:persona` section at order 0, immediately after the harness identity opener, carrying exactly this row's configured `text` with prompt variables resolved. For an agent whose preset mounts this row, it replaces whatever persona the deployment configured. In complete mode, the model sees only this rendered section as its system prompt. Runtime context remains enabled by default. When disabled, a fresh agent receives no runtime-context snapshot from sandbox policy, approval policy, delegation, or another system-prompt context provider.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
|---|---|---|
|
||||
| `text` | 必填 | 作为 `deployment:persona` 段落渲染的人设文本 |
|
||||
| `complete` | `false` | 组装后将此人设恢复为唯一的系统提示词段落 |
|
||||
| `includeRuntimeContext` | `true` | 是否为此 agent 作用域包含动态 runtime-context 快照;false 会抑制所有上下文贡献,但不禁用拥有它们的服务 |
|
||||
|
||||
`text` 与任何提示词段落一样是模板:完整的 `{{…}}` 组在提示词**渲染**时(而非组装时)严格解析为已注册的提示词变量。空文本同样占据该槽位,因此会把部署级人设整个遮蔽掉,然后在渲染时消失。启用 `complete: true` 时,组装仍会解析上下文、工具、变量和协作式监听器,之后提示词注册表将这份确切人设恢复为唯一段落;身份、工具引导或监听器都无法追加提示词文本。
|
||||
`text` 与任何提示词段落一样是模板:完整的 `{{…}}` 组在提示词**渲染**时(而非组装时)严格解析为已注册的提示词变量。空文本同样占据该槽位,因此会把部署级人设整个遮蔽掉,然后在渲染时消失。启用 `complete: true` 时,组装仍会解析上下文、工具、变量和协作式监听器,之后提示词注册表将这份确切人设恢复为唯一段落;身份、工具引导或监听器都无法追加提示词文本。启用 `includeRuntimeContext: false` 时,此作用域的上下文提供方不会被求值,组装监听器添加的上下文也会被丢弃。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -25,7 +26,7 @@
|
||||
|
||||
#### What the model sees
|
||||
|
||||
位于 order 0 的 `deployment:persona` 段落,紧随 harness 身份开场白之后,携带本行配置的 `text`,其中的提示词变量已解析。对于其 preset 挂载了本行的 agent,它会替换部署所配置的任何人设。在完整模式下,模型只会看到这个渲染后的段落作为系统提示词。
|
||||
位于 order 0 的 `deployment:persona` 段落,紧随 harness 身份开场白之后,携带本行配置的 `text`,其中的提示词变量已解析。对于其 preset 挂载了本行的 agent,它会替换部署所配置的任何人设。在完整模式下,模型只会看到这个渲染后的段落作为系统提示词。Runtime context 默认保持启用。禁用后,新建 agent 不会收到来自沙箱策略、批准策略、委派或其他 system-prompt 上下文提供方的 runtime-context 快照。
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -40,12 +40,15 @@ export interface Config {
|
||||
text: string
|
||||
/** Make this persona the complete system prompt, suppressing every other section. */
|
||||
complete?: boolean
|
||||
/** Suppress dynamic runtime-context snapshots for this persona's agent scope. */
|
||||
includeRuntimeContext?: boolean
|
||||
}
|
||||
|
||||
/** Runtime schema for the persona row. */
|
||||
export const Config: z<Config> = z.object({
|
||||
text: z.string().required(),
|
||||
complete: z.boolean().default(false),
|
||||
includeRuntimeContext: z.boolean().default(true),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -61,4 +64,5 @@ export function apply(ctx: Context, config: Config): void {
|
||||
text: config.text,
|
||||
...(config.complete ? { complete: true } : {}),
|
||||
}), 'persona.section()')
|
||||
if (!(config.includeRuntimeContext ?? true)) ctx.systemPrompt.suppressRuntimeContext()
|
||||
}
|
||||
|
||||
@@ -102,4 +102,41 @@ describe('the persona row', () => {
|
||||
expect(assembly.sections).toEqual([{ name: PERSONA_SECTION, text: 'Only this.' }])
|
||||
expect(renderPrompt(assembly)).toBe('Only this.')
|
||||
})
|
||||
|
||||
it('can suppress runtime context for its scope without changing the global assembly', async () => {
|
||||
const ctx = await harness('deployment identity')
|
||||
const key: ScopeKey = { agent: 'a1' }
|
||||
const scope = createScope(ctx, key)
|
||||
ctx.systemPrompt.context({ name: 'policy', order: 1, text: 'global policy' })
|
||||
|
||||
const fiber = await scope.ctx.plugin(Persona, {
|
||||
text: 'Only this.',
|
||||
includeRuntimeContext: false,
|
||||
})
|
||||
const suppressed = await ctx.systemPrompt.assemble({ scope: key })
|
||||
expect(suppressed.contexts).toEqual([])
|
||||
const global = await ctx.systemPrompt.assemble()
|
||||
expect(global.contexts).toEqual([
|
||||
{ name: 'policy', text: 'global policy' },
|
||||
])
|
||||
|
||||
await fiber.dispose()
|
||||
expect((await ctx.systemPrompt.assemble({ scope: key })).contexts).toEqual([
|
||||
{ name: 'policy', text: 'global policy' },
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps runtime context by default when apply bypasses schema defaults', async () => {
|
||||
const ctx = await harness('deployment identity')
|
||||
const key: ScopeKey = { agent: 'a1' }
|
||||
ctx.systemPrompt.context({ name: 'policy', order: 1, text: 'global policy' })
|
||||
|
||||
await ctx.plugin(Object.assign((inner: Context) => {
|
||||
Persona.apply(createScope(inner, key).ctx, { text: 'Scoped identity.' })
|
||||
}, { inject: ['systemPrompt'] }))
|
||||
|
||||
expect((await ctx.systemPrompt.assemble({ scope: key })).contexts).toEqual([
|
||||
{ name: 'policy', text: 'global policy' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1130,6 +1130,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
signature: 'context(context: PromptContext): () => void',
|
||||
jsDoc: '/**\n * Register ordered dynamic context in the calling context\'s scope. Scoped\n * entries shadow global entries with the same name.\n * @param context - the context contribution to register.\n * @returns the exact Cordis effect disposer.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'suppressRuntimeContext(): () => void',
|
||||
jsDoc: '/**\n * Suppress every dynamic runtime-context contribution in the calling\n * context\'s scope without changing the services that own or enforce those\n * facts. Multiple suppressors remain independently disposable.\n * @returns the exact Cordis effect disposer.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'tools(provider: (context: AssembleContext) => ToolProviderResult): () => void',
|
||||
jsDoc: '/**\n * Register a tool-schema provider in the calling context\'s scope. Global and\n * matching scoped providers both contribute; returning the reserved\n * {@link TOOL_ORDER_REST} name makes assembly fail.\n * @param provider - evaluated for each assembly with its context.\n * @returns the exact Cordis effect disposer.\n */',
|
||||
|
||||
Reference in New Issue
Block a user