Merge remote-tracking branch 'origin/master' into docs/readme-human-polish-2

This commit is contained in:
j-xiang
2026-08-13 11:28:25 +08:00
66 changed files with 482 additions and 284 deletions

View File

@@ -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: 01af5d3efd9233172dc251072fd2494cc80cbe51
README.zh.md: e5e8cc25c4f84d88608c30c75050832caf262e8f
README.md: d750a507e628e7609af542227e4528d4d4934ce8
README.zh.md: 36ee94349b63660ed52eedf7ba098334a6db07fb

View File

@@ -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 DeepSeek Harness.` 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

View File

@@ -9,6 +9,7 @@
| 键 | 默认值 | 含义 |
|---|---|---|
| `includeHarnessIdentity` | `true` | 是否包含顺序为 100 的固定开场白 `You are an AI agent powered by DeepSeek Harness.`。仅当兼容部署拥有完整系统提示词时设为 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 身份

View File

@@ -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,
}
}
}

View File

@@ -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', () => {

View File

@@ -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.

View File

@@ -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/tools/README.md
README.md: 120931c9f4b4f5e1c39c3ddd8da4b8cae42dbe69
README.zh.md: 92ea290e5f1fc2e66630a1ba63744d3b380d340a
README.md: 60841513ad5ad439ae8851dffc34e8d250acaa67
README.zh.md: 6e26a802c2c16a9d48e29236d358b15eafddcc07

View File

@@ -155,7 +155,7 @@ Code Mode exposes the generated [`run_code` schema](../../../docs/tool-catalog.m
```markdown
## Writing code for run_code
Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:
- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.

View File

@@ -155,7 +155,7 @@ Code Mode 会公开生成的 [`run_code` schema](../../../docs/tool-catalog.md#d
```markdown
## Writing code for run_code
Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:
- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.

View File

@@ -45,10 +45,11 @@ interface RunCodeFlavor {
*/
const TYPESCRIPT_FLAVOR: RunCodeFlavor = {
description:
'Execute a TypeScript program against the available tools. Write the BODY of an '
+ 'async function (erasable syntax only; top-level `await` and `return` work) and '
+ 'call tools as `await tools.name(args)` per the declarations in the system prompt. '
+ 'Only what you print or return comes back — curate it.',
'Execute a TypeScript program against the available tools. Takes two required '
+ 'arguments: `code`, the BODY of an async function (erasable syntax only; top-level '
+ '`await` and `return` work), and `description`, a short summary of what the program '
+ 'does. Call tools as `await tools.name(args)` per the declarations in the system '
+ 'prompt. Only what you print or return comes back — curate it.',
codeDescription: 'The program: the body of an async TypeScript function.',
}
@@ -59,8 +60,9 @@ const TYPESCRIPT_FLAVOR: RunCodeFlavor = {
*/
const PYTHON_FLAVOR: RunCodeFlavor = {
description:
'Execute a Python program against the available tools. Write the BODY of an '
+ 'async function (top-level `await` and `return` work) and call tools as '
'Execute a Python program against the available tools. Takes two required '
+ 'arguments: `code`, the BODY of an async function (top-level `await` and `return` '
+ 'work), and `description`, a short summary of what the program does. Call tools as '
+ '`await tools.name(args)` per the declarations in the system prompt. Answer '
+ 'with `print(...)` and/or `return <value>` — only that comes back, so curate it.',
codeDescription: 'The program: the body of an async Python function.',

View File

@@ -733,7 +733,7 @@ export function jsonSchemaToPy(schema: unknown): string {
/** The fixed model-facing usage contract rendered above the declarations. */
const SDK_INSTRUCTIONS = `## Writing code for run_code
Pass \`run_code\` the body of an async Python function (top-level \`await\` and \`return\` both work). At run time exactly two of the names declared below are bound: \`tools\` and \`ToolCallError\`. Everything else is a STATIC STUB describing argument and return types — in particular the \`TypedDict\` classes do NOT exist at run time, so build arguments as plain \`dict\`/\`list\` JSON values: \`await tools.name({"field": 1})\`, never \`FooArgs(field=1)\`, which raises \`NameError\`. Inside the program:
\`run_code\` takes two required arguments: \`code\` the body of an async Python function (top-level \`await\` and \`return\` both work) — and \`description\`, a short summary of what the program does. At run time exactly two of the names declared below are bound: \`tools\` and \`ToolCallError\`. Everything else is a STATIC STUB describing argument and return types — in particular the \`TypedDict\` classes do NOT exist at run time, so build arguments as plain \`dict\`/\`list\` JSON values: \`await tools.name({"field": 1})\`, never \`FooArgs(field=1)\`, which raises \`NameError\`. Inside the program:
- Call tools as \`await tools.name(args)\` — subscript access for exotic, reserved, or underscore-leading names: \`await tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value (each method's return type below). Tool arguments must be lossless JSON.
- A FAILED tool call raises \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose message is human-readable — wrap in \`try/except\` to handle and continue.

View File

@@ -249,7 +249,7 @@ export function jsonSchemaToTs(schema: unknown, indent = 0): string {
/** The fixed model-facing usage contract rendered above the declarations (see the Code Mode Agent Note's "What the model sees"). */
const SDK_INSTRUCTIONS = `## Writing code for run_code
Pass \`run_code\` the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
\`run_code\` takes two required arguments: \`code\` the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped) — and \`description\`, a short summary of what the program does. Inside the program:
- Call tools as \`await tools.name(args)\` — quoted access for exotic names: \`tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose \`message\` is human-readable — \`try/catch\` it to handle and continue.

View File

@@ -399,6 +399,10 @@ describe('mode-aware wire contribution', () => {
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
expect(runCodeSchema?.description).toContain('Execute a TypeScript program')
expect(runCodeSchema?.description).toContain('BODY of an')
// Both required arguments are named here, not only in the parameter
// schema: prose that describes the call as "pass the program" is what
// leads a model to emit `{code}` alone and fail INVALID_ARGS.
expect(runCodeSchema?.description).toContain('`description`')
const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code
expect(codeParam.description).toBe('The program: the body of an async TypeScript function.')
})
@@ -410,6 +414,7 @@ describe('mode-aware wire contribution', () => {
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
expect(runCodeSchema?.description).toContain('Execute a Python program')
expect(runCodeSchema?.description).toContain('`return <value>`')
expect(runCodeSchema?.description).toContain('`description`')
expect(runCodeSchema?.description).not.toContain('TypeScript')
const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code
expect(codeParam.description).toBe('The program: the body of an async Python function.')

View File

@@ -166,6 +166,15 @@ describe('renderToolsSdkPy', () => {
expect(text).toContain('tools: Tools')
})
it('names both required call arguments, not just the program', () => {
// The schema requires `code` AND `description`; instructions that mention
// only the program let a model emit `{code}` alone and fail INVALID_ARGS.
const text = renderToolsSdkPy([bash])
expect(text).toContain('`code`')
expect(text).toContain('`description`')
expect(text).toContain('two required arguments')
})
it('renders required as plain fields and optional as NotRequired, with per-field description comments', () => {
const tool: ToolSdkSchema = {
name: 'search',

View File

@@ -148,6 +148,15 @@ describe('renderToolsSdk', () => {
expect(text).toContain('lossless JSON')
})
it('names both required call arguments, not just the program', () => {
// The schema requires `code` AND `description`; instructions that mention
// only the program let a model emit `{code}` alone and fail INVALID_ARGS.
const text = renderToolsSdk([bash])
expect(text).toContain('`code`')
expect(text).toContain('`description`')
expect(text).toContain('two required arguments')
})
it('is deterministic: same tool set, byte-identical text regardless of input order', () => {
expect(renderToolsSdk([bash, exotic])).toBe(renderToolsSdk([exotic, bash]))
// Equal names sort stably (the comparator's equal arm).

View File

@@ -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: 2d48b65e5b5cf08d62bac8b07967965fe38bdd77
README.zh.md: 83df672b540649f58b112a08ba0792ca2cb72295
README.md: e4e92d398d5f48609bc5af23ece2b5fe3e607b21
README.zh.md: c5f78d53a59f3bd3e377afc2c471d187b73da32e

View File

@@ -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?, jobs?, toolJobs?, goals?, invariants? }
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, jobs?, toolJobs?, 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, agent-instructions, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `jobs.maxConcurrentJobsPerOwner` configures the local provider independently of the model-facing `toolJobs` 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, agent-instructions, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `jobs.maxConcurrentJobsPerOwner` configures the local provider independently of the model-facing `toolJobs` 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`](../../runtime-diagnostics/invariants/README.md) for regex and lifecycle rules.

View File

@@ -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?, jobs?, toolJobs?, goals?, invariants? }
// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, jobs?, toolJobs?, goals?, invariants? }
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
```
组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent无头和 JSON-RPC 组合会创建 `main`ACP 应用则在 `session/new` 按需创建 agent。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值;`jobs.maxConcurrentJobsPerOwner` 配置本地 Service provider并与面向模型的 `toolJobs` 控制工具相互独立。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。
组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent无头和 JSON-RPC 组合会创建 `main`ACP 应用则在 `session/new` 按需创建 agent。`includeRuntimeContext: false` 会转发给 `dsh-system-prompt`,为新建会话抑制所有动态上下文快照,但不禁用其策略服务。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值;`jobs.maxConcurrentJobsPerOwner` 配置本地 Service provider并与面向模型的 `toolJobs` 控制工具相互独立。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。
例如,`{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` 会让包拥有的配套插件保持挂载但抑制被阻止的拥有者。Blocklist 匹配优先于 allowlist 匹配;正则表达式与生命周期规则见 [`dsh-invariants`](../../runtime-diagnostics/invariants/README.md)。

View File

@@ -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 } : {},
})

View File

@@ -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', () => {
jobs: { maxConcurrentJobsPerOwner: 4 },
toolJobs: 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', () => {
jobs: appConfig.jobs,
toolJobs: appConfig.toolJobs,
invariants: appConfig.invariants,
goals: appConfig.goals,
})
expect(agentCore.pickSpineConfig({ workspaceContext: false })).toEqual({ workspaceContext: false })
})

View File

@@ -73,7 +73,7 @@ export interface InheritedApiEntry {
/** One named type declaration referenced by a Service or Event signature. */
export interface TypeApiEntry {
/** The exported type/interface name, e.g. `BashRunResult`. */
/** The exported type/interface name, e.g. `ShellRunResult`. */
name: string
/** The full declaration text, comments stripped. */
declaration: string
@@ -411,7 +411,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'AgentContext',
declaration: 'export type AgentContext = Omit<Context, \'remote\'> & {\n readonly remote: TypeRTClientRemote & TypeRTRemoteScopeApi<\'agent\'>;\n};',
declaration: 'export type AgentContext = Omit<Context, \'remote\'> & {\n readonly remote: TypertClientRemote & TypertRemoteScopeApi<\'agent\'>;\n};',
},
{
name: 'AssistantBlock',

View File

@@ -1710,6 +1710,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
parameters: [{ name: 'context', description: 'the context contribution to register.' }],
returns: 'the exact Cordis effect disposer.',
},
{
signature: 'suppressRuntimeContext(): () => void',
description: '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.',
parameters: [],
returns: 'the exact Cordis effect disposer.',
},
{
signature: 'tools(provider: (context: AssembleContext) => ToolProviderResult): () => void',
description: 'Register a tool-schema provider in the calling context\'s scope. Global and matching scoped providers both contribute; returning the reserved TOOL_ORDER_REST name makes assembly fail.',
@@ -4225,7 +4231,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SystemPrompt',
declaration: 'export class SystemPrompt extends Service {\n static Config: z<Config>;\n constructor(ctx: Context, config: Config);\n section(section: PromptSection): () => void;\n context(context: PromptContext): () => void;\n tools(provider: (context: AssembleContext) => ToolProviderResult): () => void;\n variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void;\n async assemble(context: AssembleContext = {}): Promise<PromptAssembly>;\n}',
declaration: 'export class SystemPrompt extends Service {\n static Config: z<Config>;\n constructor(ctx: Context, config: Config);\n section(section: PromptSection): () => void;\n context(context: PromptContext): () => void;\n suppressRuntimeContext(): () => void;\n tools(provider: (context: AssembleContext) => ToolProviderResult): () => void;\n variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void;\n async assemble(context: AssembleContext = {}): Promise<PromptAssembly>;\n}',
},
{
name: 'TableKeyOf',

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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' },
])
})
})