Merge remote-tracking branch 'origin/master' into worktree/code-diff-card
This commit is contained in:
commit
3c632ebf66
318 files changed
+9061
-3253
No files matched your search
@@ -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: 7e4826c4fb51873db02fd80eca632c3d1dd8d550
|
||||
README.zh.md: 30f2a43908513f5fe1e7169bff7ee95c9461ea5d
|
||||
README.md: a76ae5c66f0801cb85cd8cf22d0c3ebbc68e65b8
|
||||
README.zh.md: 98c6e733399c954c5bbc56b905aefcc6b080381d
|
||||
@@ -63,6 +63,8 @@ ctx.systemPrompt.section({
|
||||
})
|
||||
```
|
||||
|
||||
Set `interpolate: false` on a section to preserve its text literally, including `{{…}}` groups in generated tool documentation. Other sections interpolate variables by default.
|
||||
|
||||
### Contribute a prompt variable
|
||||
|
||||
Variables are referenced from section text as `{{name}}` and resolved at each assembly; scoped variables shadow a same-named global for that agent. The loop supplies `model` and `cwd`; any plugin can register the facts it owns.
|
||||
@@ -102,7 +104,7 @@ The package is a registry plus a cooperative assembly pipeline. One `assemble()`
|
||||
|
||||
### Assembly and rendering
|
||||
|
||||
Assembly resolves and renders in two stages: `assemble()` returns sections with resolved-but-uninterpolated text, the ordered tool schemas, and every registered variable resolved against the context, while `renderPrompt()` interpolates `{{variable}}` references, drops empty sections, and joins with blank lines — strictly, an unknown reference, a registered-but-valueless reference, or a malformed complete group throws, because a malformed prompt is worse than a loud failure. `toolOrder` canonicalizes the collected tools before the waterfall (registration order is a plugin-load artifact); a waterfall listener that mutates the list owns the determinism of what it emits.
|
||||
Assembly resolves and renders in two stages: `assemble()` returns sections with resolved-but-uninterpolated text, the ordered tool schemas, and every registered variable resolved against the context, while `renderPrompt()` interpolates `{{variable}}` references unless a section sets `interpolate: false`, drops empty sections, and joins with blank lines — strictly, an unknown reference, a registered-but-valueless reference, or a malformed complete group throws, because a malformed prompt is worse than a loud failure. `toolOrder` canonicalizes the collected tools before the waterfall (registration order is a plugin-load artifact); a waterfall listener that mutates the list owns the determinism of what it emits.
|
||||
|
||||
### Scoping
|
||||
|
||||
@@ -170,7 +172,7 @@ Prefix-stable while the visible schema set, rendering, and order are unchanged.
|
||||
These limits define when prompt assembly needs special care. They are current package constraints, not a task backlog.
|
||||
|
||||
- **Deployment-authored prompt text is config/composition only** — this plugin owns the global persona prefix and suffix defaults, creator plugins may register agent-scoped shadows, and other sections come from the plugin that owns the fact; there is no end-user prompt-editing API.
|
||||
- **No escape syntax for literal `{{…}}` braces** — every complete group is interpolated against registered variables; an escape is deferred until a real prompt needs one.
|
||||
- **No inline escape syntax in interpolated text** — use `interpolate: false` when a whole section must preserve literal braces.
|
||||
- **`toolOrder` misconfiguration surfaces at prompt assembly (the first turn), not at boot** — only shape violations throw at config load.
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ ctx.systemPrompt.section({
|
||||
})
|
||||
```
|
||||
|
||||
在段上设置 `interpolate: false` 可原样保留文本,包括生成的工具文档中的 `{{…}}` 组。其他段默认执行变量插值。
|
||||
|
||||
### 贡献提示词变量
|
||||
|
||||
变量在段文本中以 `{{name}}` 引用,并在每次组装时解析;带作用域变量会为该 agent 遮蔽同名全局变量。循环提供 `model` 与 `cwd`;任何插件都可以注册自己拥有的事实。
|
||||
@@ -102,7 +104,7 @@ ctx.systemPrompt.variable('cwd', ({ agent }) => agent?.session.header.cwd)
|
||||
|
||||
### 组装与渲染
|
||||
|
||||
组装分两阶段完成求值与渲染:`assemble()` 返回文本已求值但尚未插值的段、有序工具 schema,以及每个已注册变量按当前上下文求得的值;`renderPrompt()` 插值 `{{variable}}` 引用、删除空段并用空行连接——严格规则:未知引用、已注册但无值的引用或格式错误的完整组都会抛出,因为格式错误的提示词比明确失败更糟。`toolOrder` 在 waterfall 分发前规范化收集到的工具(注册顺序只是插件加载产物);修改列表的 waterfall 监听器对其输出的确定性负责。
|
||||
组装分两阶段完成求值与渲染:`assemble()` 返回文本已求值但尚未插值的段、有序工具 schema,以及每个已注册变量按当前上下文求得的值;`renderPrompt()` 对未设置 `interpolate: false` 的段插值 `{{variable}}` 引用、删除空段并用空行连接——严格规则:未知引用、已注册但无值的引用或格式错误的完整组都会抛出,因为格式错误的提示词比明确失败更糟。`toolOrder` 在 waterfall 分发前规范化收集到的工具(注册顺序只是插件加载产物);修改列表的 waterfall 监听器对其输出的确定性负责。
|
||||
|
||||
### 作用域
|
||||
|
||||
@@ -170,7 +172,7 @@ schema token 在每次请求中重复。限制工具会为该 agent 移除其全
|
||||
这些限制说明提示词组装何时需要特别留意。它们是当前包约束,不是待办事项清单。
|
||||
|
||||
- **部署方编写的提示词文本只来自配置/组合**:此插件拥有全局 persona 前缀与后缀默认值;创建方插件可以注册 agent 作用域的遮蔽项;其他段来自拥有相应事实的插件。不存在终端用户提示词编辑 API。
|
||||
- **没有表示字面量 `{{…}}` 花括号的转义语法**:每个完整组都会按已注册变量插值;只有实际提示词需要转义时才会实现。
|
||||
- **插值文本不支持行内转义语法**:整段需要保留字面花括号时,使用 `interpolate: false`。
|
||||
- **`toolOrder` 配置错误在提示词组装(首轮)时出现,而不是启动时**:只有形状违规会在配置加载时抛出。
|
||||
|
||||
|
||||
|
||||
@@ -61,9 +61,11 @@ export interface PromptSection {
|
||||
/**
|
||||
* Static text or a provider evaluated at each assembly with that assembly's
|
||||
* {@link AssembleContext}. The text may reference `{{variable}}`s — they are
|
||||
* interpolated later, by {@link renderPrompt}.
|
||||
* interpolated later, by {@link renderPrompt}, unless `interpolate` is false.
|
||||
*/
|
||||
readonly text: string | ((context: AssembleContext) => string)
|
||||
/** Whether to interpolate prompt variables. Defaults to true; false preserves literal text. */
|
||||
readonly interpolate?: boolean
|
||||
/**
|
||||
* Treat this contribution as the complete system prompt. Assembly still
|
||||
* runs the cooperative waterfall so tools, contexts, and variables can be
|
||||
@@ -89,6 +91,8 @@ export interface AssembledSection {
|
||||
name: string
|
||||
/** The resolved (but not yet interpolated) section text. */
|
||||
text: string
|
||||
/** Whether to interpolate prompt variables. Defaults to true; false preserves literal text. */
|
||||
interpolate?: boolean
|
||||
}
|
||||
|
||||
/** One resolved dynamic context contribution. */
|
||||
@@ -264,7 +268,8 @@ export interface Config {
|
||||
|
||||
/**
|
||||
* Interpolate strict `{{variable}}` references, drop empty sections, and join
|
||||
* the rest with blank lines. Malformed, unknown, or undefined references throw;
|
||||
* the rest with blank lines. Sections with `interpolate: false` retain literal
|
||||
* text. Malformed, unknown, or undefined references in other sections throw;
|
||||
* a lone `{{` without any later `}}` is literal prose, and substituted values
|
||||
* are not scanned again.
|
||||
* @param assembly - the assembly whose sections and variables to render.
|
||||
@@ -272,7 +277,7 @@ export interface Config {
|
||||
*/
|
||||
export function renderPrompt(assembly: PromptAssembly): string {
|
||||
return assembly.sections
|
||||
.map(section => interpolate(section, assembly.variables, 'section'))
|
||||
.map(section => section.interpolate === false ? section.text : interpolate(section, assembly.variables, 'section'))
|
||||
.filter(text => text.length > 0)
|
||||
.join('\n\n')
|
||||
}
|
||||
@@ -597,6 +602,7 @@ export class SystemPrompt extends Service {
|
||||
const assembled = {
|
||||
name: section.name,
|
||||
text: typeof section.text === 'function' ? section.text(context) : section.text,
|
||||
...section.interpolate !== undefined ? { interpolate: section.interpolate } : {},
|
||||
}
|
||||
if (section.complete === true) completeSection = { ...assembled }
|
||||
return assembled
|
||||
|
||||
@@ -579,6 +579,25 @@ describe('SystemPrompt', () => {
|
||||
expect(renderPrompt(await ctx.systemPrompt.assemble())).toBe(`${IDENTITY}\n\nYou run on deepseek-v4 in /work.`)
|
||||
})
|
||||
|
||||
it.each([
|
||||
[false, false],
|
||||
[false, true],
|
||||
[true, false],
|
||||
[true, true],
|
||||
])('preserves literal section text with complete=%s and dynamic=%s', async (complete, dynamic) => {
|
||||
const ctx = new Context()
|
||||
try {
|
||||
await ctx.plugin(SystemPrompt, { includeHarnessIdentity: false, personaPrefix: '{{model}}' })
|
||||
ctx.systemPrompt.variable('model', () => 'actual-model')
|
||||
const text = '{{item}} {{model}} {{ model }} {{nested{{item}}}}'
|
||||
ctx.systemPrompt.section({ name: 'literal', order: 1, text: dynamic ? () => text : text, interpolate: false, complete })
|
||||
expect(renderPrompt(await ctx.systemPrompt.assemble()))
|
||||
.toBe(complete ? text : `actual-model\n\n${text}`)
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('lets a waterfall listener add or override variables before render', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
|
||||
@@ -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: a62e89c1bde0b96d5ba6b0e7c8f27d2af87f4340
|
||||
README.zh.md: d10ce3913cc9eb24e417f61c5b9f081a40b50cc3
|
||||
README.md: 17e2eb05bac152c75f7a906bf6e15f26517d38dc
|
||||
README.zh.md: cd08e9282e2ac6e75c1694d9989a4079d65985b5
|
||||
@@ -170,7 +170,7 @@ Prefix-stable while visible definitions and their order are unchanged. Registrat
|
||||
|
||||
#### What the model sees
|
||||
|
||||
PTC mode exposes the generated [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools), the SDK instructions below, and the generated exact SDK block for the loaded runtime's language. The TypeScript instructions identify generated declarations as program-only bindings. When the current `bash` parameter schema accepts the example arguments, they also show a complete `run_code` call around `tools.bash(...)`. The `tools:sdk` section uses first-party order 5000. `both` exposes normal schemas and this PTC mode API; under `ptc` the prompt also carries the `tools:ptc-only` rule earlier in the first-party order, so the model reads which tools it may call before it reads what each one is for.
|
||||
PTC mode exposes the generated [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools), the SDK instructions below, and the generated exact SDK block for the loaded runtime's language. The TypeScript instructions identify generated declarations as program-only bindings. When the current `bash` parameter schema accepts the example arguments, they also show a complete `run_code` call around `tools.bash(...)`. The `tools:sdk` section uses first-party order 5000 and disables prompt-variable interpolation, preserving literal `{{…}}` text in tool descriptions and schemas for both runtime languages. `both` exposes normal schemas and this PTC mode API; under `ptc` the prompt also carries the `tools:ptc-only` rule earlier in the first-party order, so the model reads which tools it may call before it reads what each one is for.
|
||||
|
||||
##### TypeScript PTC mode SDK instructions with bash
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ ctx.tools.register(defineTool({
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
PTC mode 会公开生成的 [`run_code` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tools)、下方 SDK 说明,以及按所加载运行时语言生成的精确 SDK 块。TypeScript 说明会把生成声明明确标为只能在程序内使用的绑定。当当前 `bash` 参数 schema 接受示例参数时,说明还会给出以 `run_code` 包住 `tools.bash(...)` 的完整调用。`tools:sdk` 段使用 first-party 顺序 5000。`both` 会同时公开普通 schema 与此 PTC mode API;在 `ptc` 下,提示词还会带上处于更早 first-party 顺序的 `tools:ptc-only` 规则,让模型先读到「可以调用哪些工具」再读「每个工具做什么」。
|
||||
PTC mode 会公开生成的 [`run_code` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tools)、下方 SDK 说明,以及按所加载运行时语言生成的精确 SDK 块。TypeScript 说明会把生成声明明确标为只能在程序内使用的绑定。当当前 `bash` 参数 schema 接受示例参数时,说明还会给出以 `run_code` 包住 `tools.bash(...)` 的完整调用。`tools:sdk` 段使用 first-party 顺序 5000,并关闭提示词变量插值,使两种运行时语言都原样保留工具描述和 schema 中的 `{{…}}` 文本。`both` 会同时公开普通 schema 与此 PTC mode API;在 `ptc` 下,提示词还会带上处于更早 first-party 顺序的 `tools:ptc-only` 规则,让模型先读到「可以调用哪些工具」再读「每个工具做什么」。
|
||||
|
||||
##### 带 bash 的 TypeScript PTC mode SDK 说明
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { assertNever, deepFreeze, snapshotJsonValue, type JsonValue } from '@deepseek-ai/dsh-util-values'
|
||||
import type { ToolProviderResult } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { PromptSection, ToolProviderResult } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
|
||||
// Type-only: makes `ctx.get('approval')` resolve to the ApprovalService
|
||||
// augmentation. The seam stays optional at runtime — see `serviceAsk`.
|
||||
@@ -844,7 +844,7 @@ export class ToolRuntime extends Service {
|
||||
* `both` renders empty: native calls do execute there, so the rule is false.
|
||||
* @returns the section registration.
|
||||
*/
|
||||
private collapseSection(): { name: string; order: number; text: (context: { scope?: ScopeKey }) => string } {
|
||||
private collapseSection(): PromptSection {
|
||||
return {
|
||||
name: 'tools:ptc-only',
|
||||
order: this.ctx.systemPrompt.getSectionOrder('PTC_ONLY'),
|
||||
@@ -864,10 +864,11 @@ export class ToolRuntime extends Service {
|
||||
* dropped from the rendered prompt.
|
||||
* @returns the section registration.
|
||||
*/
|
||||
private sdkSection(): { name: string; order: number; text: (context: { scope?: ScopeKey }) => string } {
|
||||
private sdkSection(): PromptSection {
|
||||
return {
|
||||
name: 'tools:sdk',
|
||||
order: this.ctx.systemPrompt.getSectionOrder('TOOLS_SDK'),
|
||||
interpolate: false,
|
||||
// Regenerate from the calling scope's visible tools in stable order.
|
||||
text: (context) => {
|
||||
const mode = this.modeFor(context.scope)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/** Tool documentation containing literal template syntax for recorded PTC replay. */
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'literal-sdk'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'template_echo',
|
||||
description: 'Echo template text such as {{item}}, {{model}}, or {{ item }} without substitution.',
|
||||
parameters: { text: { type: 'string', required: true } },
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
execute: args => Promise.resolve(args.text),
|
||||
}))
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
import { createScope } from '@deepseek-ai/dsh-scope'
|
||||
import type { Scope } from '@deepseek-ai/dsh-scope'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
|
||||
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
|
||||
import ToolRuntime, { CodeRunFailedError, RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DISPATCH, defineContentToolFixture, defineTool } from '@deepseek-ai/dsh-tools'
|
||||
@@ -117,6 +117,38 @@ async function runCode(
|
||||
}
|
||||
|
||||
describe('mode-aware wire contribution', () => {
|
||||
it.each([
|
||||
{ mode: 'ptc', language: 'typescript' },
|
||||
{ mode: 'both', language: 'typescript' },
|
||||
{ mode: 'ptc', language: 'python' },
|
||||
{ mode: 'both', language: 'python' },
|
||||
] as const)('preserves literal braces in the $language SDK under $mode', async ({ mode, language }) => {
|
||||
const { ctx, systemPrompt } = await setup({ mode, runtime: { language } })
|
||||
try {
|
||||
systemPrompt.variable('model', () => 'actual-model')
|
||||
const description = 'Expand {{item}} with {{model}} or {{ model }}.'
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'template',
|
||||
description,
|
||||
parameters: { value: { type: 'string', description, enum: ['{{item}}', '{{model}}'], required: true } },
|
||||
output: {
|
||||
schema: { type: 'string', enum: ['{{item}}', '{{model}}'] },
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
},
|
||||
execute: args => Promise.resolve(args.value),
|
||||
}))
|
||||
const assembly = await systemPrompt.assemble()
|
||||
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')
|
||||
expect(sdk).toBeDefined()
|
||||
const prompt = renderPrompt(assembly)
|
||||
expect(prompt).toContain(description)
|
||||
expect(prompt).toContain(sdk!.text)
|
||||
expect(prompt).not.toContain('actual-model')
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it("mode 'native' contributes every schema, no run_code, no SDK section — and needs no runtime", async () => {
|
||||
const { ctx, systemPrompt } = await setup({ mode: 'native', runtime: false })
|
||||
registerEcho(ctx)
|
||||
|
||||
Reference in new issue
Block a user