refactor(system-prompt): rename TOOL_ORDER_REST from '...' to '<unlisted-tools>'

A three-dot rest entry reads as elision in a cordis.yml; the spelled-out
sentinel says what lands there. The literal now appears once in code (the
constant) and once in the value-pinning test; every other reference — the
forwarding tests included — imports TOOL_ORDER_REST, which adds the
dsh-system-prompt devDependency to the two app packages.

Review follow-up on #196.
This commit is contained in:
imccyu
2026-07-07 20:18:01 +08:00
parent e930dd328a
commit 72933ec558
13 changed files with 48 additions and 26 deletions

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import * as agentCore from '../src/index.ts'
import { AgentId } from '@deepseek-ai/dsh-agent'
@@ -68,7 +69,7 @@ describe('dsh-agent-core bundle', () => {
})
it('forwards toolOrder to the system-prompt assembly', async () => {
const ctx = await mount({ toolOrder: ['zulu', '...'] })
const ctx = await mount({ toolOrder: ['zulu', TOOL_ORDER_REST] })
// The bundle's own bash tools pend on the absent `ctx.bash` executor in
// this providerless mount, so register two plain tools to order.
for (const name of ['alpha', 'zulu']) {

View File

@@ -7,7 +7,7 @@ System prompt assembly registry. Plugins contribute ordered text sections, tool-
| Key | Default | Meaning |
|---|---|---|
| `persona` | `''` | The deployment persona: the ONE deployment-authored prompt fragment, rendered as the order-0 `deployment:persona` section and shared by every agent in the context (subagents included). 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 `'...'` rest entry (`TOOL_ORDER_REST`): listed tools take their listed position, names with no registered tool are ignored, unlisted tools land at `'...'` 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. A list without exactly one `'...'`, or with duplicates, throws at load. Why a central list and not per-plugin weights: [Explicit model-facing tool order](../../../docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.md). |
| `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, names with no registered tool are ignored, 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. A list without exactly one rest entry, or with duplicates, throws at load. Why a central list and not per-plugin weights: [Explicit model-facing tool order](../../../docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.md). |
## Service: `SystemPrompt` (ctx key: `systemPrompt`)

View File

@@ -117,11 +117,11 @@ const GROUP_AT = /^\{\{([^{}]*)\}\}/
* Deliberately not a valid model-facing tool name, so it can never collide
* with a real tool.
*/
export const TOOL_ORDER_REST = '...'
export const TOOL_ORDER_REST = '<unlisted-tools>'
/**
* Validate a configured tool-order list at service construction: `'...'`
* ({@link TOOL_ORDER_REST}) exactly once, no duplicate names. Returns the list
* Validate a configured tool-order list at service construction: the
* {@link TOOL_ORDER_REST} rest entry exactly once, no duplicate names. Returns the list
* (or undefined when unconfigured); throws otherwise, failing the service at
* load — a bad order config must never reach an assembly.
*/
@@ -141,7 +141,7 @@ function validateToolOrder(toolOrder: string[] | undefined): string[] | undefine
/**
* Order collected tool schemas by the validated policy: with no configured
* list, plain lexicographic name order; with one, listed names take their
* listed position and every unlisted tool lands at the `'...'` entry in
* listed position and every unlisted tool lands at the {@link TOOL_ORDER_REST} rest entry in
* lexicographic name order. Never drops a tool, and both sorts are stable, so
* tools sharing a name keep their collection order.
*/
@@ -176,8 +176,8 @@ export interface Config {
* Explicit model-facing tool order, as a list of `ToolSchema.name`s: listed
* tools take their listed position, names with no registered tool are
* ignored, and tools absent from the list are inserted at the
* {@link TOOL_ORDER_REST} (`'...'`) entry in lexicographic name order. A
* configured list must contain `'...'` exactly once and no duplicate names —
* {@link TOOL_ORDER_REST} (`'<unlisted-tools>'`) entry in lexicographic name order. A
* configured list must contain the rest entry exactly once and no duplicate names —
* anything else throws at load; a bad order config must never reach a
* model request. When omitted, tools are ordered lexicographically by name.
* Applied to the tools {@link SystemPrompt.assemble} collects, BEFORE the
@@ -264,8 +264,8 @@ export class SystemPrompt extends Service {
persona: z.string().default(''),
// A schemastery array defaults to [] when omitted, but an omitted
// toolOrder must stay absent ("lexicographic order"), not become an
// explicitly-configured empty list (which is invalid — it lacks the '...'
// entry). Forcing the default to undefined keeps the key out of the
// explicitly-configured empty list (which is invalid — it lacks the
// rest entry). Forcing the default to undefined keeps the key out of the
// validated config; the cast is needed because .default() expects the
// array type.
toolOrder: z.array(z.string()).default(undefined as unknown as string[]),

View File

@@ -18,8 +18,8 @@ function names(assembly: PromptAssembly): string[] {
}
describe('SystemPrompt tool order', () => {
it('exports the rest entry as "..."', () => {
expect(TOOL_ORDER_REST).toBe('...')
it('exports the rest entry as "<unlisted-tools>"', () => {
expect(TOOL_ORDER_REST).toBe('<unlisted-tools>')
})
it('assembles tools in lexicographic name order when no toolOrder is configured', async () => {
@@ -40,7 +40,7 @@ describe('SystemPrompt tool order', () => {
expect(names(await backward.systemPrompt.assemble())).toEqual(['alpha', 'zulu'])
})
it('applies a configured toolOrder: listed positions, rest at "..." lexicographically, absent names ignored', async () => {
it('applies a configured toolOrder: listed positions, rest at the rest entry lexicographically, absent names ignored', async () => {
const ctx = await mount({ toolOrder: ['todo_write', 'ghost', TOOL_ORDER_REST, 'bash'] })
ctx.systemPrompt.tools(() => [tool('bash'), tool('echo_b'), tool('todo_write'), tool('echo_a')])
expect(names(await ctx.systemPrompt.assemble())).toEqual(['todo_write', 'echo_a', 'echo_b', 'bash'])
@@ -73,8 +73,8 @@ describe('SystemPrompt tool order', () => {
it.each([
['an empty list', []],
['a list without the rest entry', ['bash', 'todo_write']],
])('rejects %s at load (the "..." rest entry is required)', async (_case, toolOrder) => {
await expect(new Context().plugin(SystemPrompt, { toolOrder })).rejects.toThrow('must contain the "..." rest entry')
])('rejects %s at load (the rest entry is required)', async (_case, toolOrder) => {
await expect(new Context().plugin(SystemPrompt, { toolOrder })).rejects.toThrow(`must contain the "${TOOL_ORDER_REST}" rest entry`)
})
it.each([