test(todo): pin configurable policy boundaries
This commit is contained in:
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-29-todo-write-tool.md
|
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-29-todo-write-tool.md
|
||||||
2026-06-29-todo-write-tool.md: f1fd8a6b273e569b752f477352e4877e556fc054
|
2026-06-29-todo-write-tool.md: c4cf64b7876bd8b80df80fe6fc27005715f96b5e
|
||||||
2026-06-29-todo-write-tool.zh.md: f5cf3d96dd179bfba5dcac58b437bfc54214e1f9
|
2026-06-29-todo-write-tool.zh.md: 7c1785cdd525397621c8cdee63e1eb4f793a94f2
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Each list belongs to the calling agent session, and non-agent calls are rejected
|
|||||||
|
|
||||||
### Validation: the cheap middle
|
### Validation: the cheap middle
|
||||||
|
|
||||||
The schema enforces type/required/enum. Beyond that, `execute` rejects empty or duplicate `content`: enforce the cheap invariants that make a plan *coherent* (no blank tasks, no dupes), but leave ordering, active-task discipline, and keeping the list current to the model via the tool description. A rejected write returns an `isError` result so the model self-corrects. The original design also capped the list at one `in_progress` task; that cap was removed for parallel work — the [parallel in-progress Agent Note](2026-07-26-todo-parallel-in-progress.md) owns that decision.
|
The schema enforces type/required/enum. Beyond that, `execute` rejects empty or duplicate `content` and, when `allowParallelInProgress` is `false`, more than one active task. Ordering and keeping the list current remain model disciplines expressed in the tool description. A rejected write returns an `isError` result so the model self-corrects. The required deployment policy and the durable invariant's independence from it are owned by the [parallel in-progress Agent Note](2026-07-26-todo-parallel-in-progress.md).
|
||||||
|
|
||||||
## Why no cordis-catalog entry / no `@mode`
|
## Why no cordis-catalog entry / no `@mode`
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ claude-code V1 的条目是 `{ content, status, activeForm }`;后来(V2)
|
|||||||
|
|
||||||
### 校验:低成本的中间路线
|
### 校验:低成本的中间路线
|
||||||
|
|
||||||
schema 强制 type/required/enum。在此之上,`execute` 拒绝为空或重复的 `content`:强制执行使计划*连贯*的低成本不变式(无空任务、无重复),但将排序、活跃任务纪律和保持列表最新通过工具描述交给模型。被拒绝的写入返回 `isError` 结果,使模型自行修正。原始设计还将列表限制为最多一个 `in_progress` 任务;该上限已为并行工作移除——[并行 in-progress Agent Note](2026-07-26-todo-parallel-in-progress.md) 拥有该决定。
|
schema 强制 type/required/enum。在此之上,`execute` 拒绝为空或重复的 `content`,并在 `allowParallelInProgress` 为 `false` 时拒绝超过一个活跃任务。排序和保持列表最新仍通过工具描述交给模型。被拒绝的写入返回 `isError` 结果,使模型自行修正。必填的部署策略及持久日志不变式独立于该策略这一约定,由[并行 in-progress Agent Note](2026-07-26-todo-parallel-in-progress.md) 负责。
|
||||||
|
|
||||||
## 为何没有 cordis-catalog 条目 / 没有 `@mode`
|
## 为何没有 cordis-catalog 条目 / 没有 `@mode`
|
||||||
|
|
||||||
|
|||||||
@@ -277,8 +277,8 @@ function buildAlphaLog(): SessionEvent[] {
|
|||||||
}
|
}
|
||||||
// Turn 65: todo_write sample — the TodoRow toolview in the flow plus the
|
// Turn 65: todo_write sample — the TodoRow toolview in the flow plus the
|
||||||
// todo/write snapshot event feeding the TodoPanel plan strip. Two items are
|
// todo/write snapshot event feeding the TodoPanel plan strip. Two items are
|
||||||
// in_progress: the tool permits several, so both surfaces must render a
|
// in_progress: this fixture chooses the parallel policy, so both surfaces
|
||||||
// parallel plan rather than the first active item alone.
|
// must render a parallel plan rather than the first active item alone.
|
||||||
const fixtureTodos = [
|
const fixtureTodos = [
|
||||||
{ content: '梳理需求', status: 'completed' },
|
{ content: '梳理需求', status: 'completed' },
|
||||||
{ content: '实现 fixture 样本', status: 'in_progress' },
|
{ content: '实现 fixture 样本', status: 'in_progress' },
|
||||||
|
|||||||
@@ -44,6 +44,8 @@
|
|||||||
"cordis": "^4.0.0-rc.7"
|
"cordis": "^4.0.0-rc.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@cordisjs/plugin-include": "workspace:^",
|
||||||
|
"@cordisjs/plugin-loader": "workspace:^",
|
||||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||||
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
|
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
import SessionStore, { type Session, type SessionEvent } from '@deepseek-ai/dsh-session'
|
import SessionStore, { type Session, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||||
|
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||||
|
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
|
||||||
import * as TodoInvariant from '@deepseek-ai/dsh-tool-todo/invariant'
|
import * as TodoInvariant from '@deepseek-ai/dsh-tool-todo/invariant'
|
||||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||||
|
|
||||||
@@ -17,14 +19,22 @@ function event(todos: unknown): SessionEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('todo snapshot invariants', () => {
|
describe('todo snapshot invariants', () => {
|
||||||
it('accepts a unique whole-list snapshot, including several active items', async () => {
|
it('accepts historical and live parallel snapshots under the single-active tool policy', async () => {
|
||||||
const ctx = await setup()
|
const todos = [
|
||||||
expect(() => { ctx.emit('session/event', {} as Session, event([
|
|
||||||
{ content: 'Inspect state', status: 'completed' },
|
{ content: 'Inspect state', status: 'completed' },
|
||||||
{ content: 'Apply fix', status: 'in_progress' },
|
{ content: 'Apply fix', status: 'in_progress' },
|
||||||
{ content: 'Watch background build', status: 'in_progress' },
|
{ content: 'Watch background build', status: 'in_progress' },
|
||||||
{ content: 'Run checks', status: 'pending' },
|
{ content: 'Run checks', status: 'pending' },
|
||||||
])) }).not.toThrow()
|
] as const
|
||||||
|
const ctx = new Context()
|
||||||
|
await ctx.plugin(SessionStore)
|
||||||
|
await ctx.plugin(ToolRegistry)
|
||||||
|
await ctx.plugin(ToolTodo, { allowParallelInProgress: false })
|
||||||
|
ctx.sessions.create().append('todo/write', { todos: [...todos] })
|
||||||
|
await ctx.plugin(InvariantService, { enabled: true })
|
||||||
|
|
||||||
|
await expect(ctx.plugin(TodoInvariant).then(() => undefined)).resolves.toBeUndefined()
|
||||||
|
expect(() => { ctx.emit('session/event', {} as Session, event(todos)) }).not.toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
|
|||||||
@@ -124,20 +124,23 @@ describe('tool-todo real Loader composition through cordis.yml', () => {
|
|||||||
expect(owner.session.events.findLast(e => e.type === 'todo/write')?.data.todos).toEqual(PARALLEL_TODOS)
|
expect(owner.session.events.findLast(e => e.type === 'todo/write')?.data.todos).toEqual(PARALLEL_TODOS)
|
||||||
}, 30_000)
|
}, 30_000)
|
||||||
|
|
||||||
it('fails loading when allowParallelInProgress is omitted', async () => {
|
it.each([
|
||||||
|
{ label: 'is omitted', configLines: [], failure: '$.allowParallelInProgress missing required value' },
|
||||||
|
{ label: 'is not boolean', configLines: [' allowParallelInProgress: "no"'], failure: '$.allowParallelInProgress' },
|
||||||
|
])('fails loading when allowParallelInProgress $label', async ({ configLines, failure }) => {
|
||||||
// loader.await() is all-settled; configuration failure leaves a FAILED
|
// loader.await() is all-settled; configuration failure leaves a FAILED
|
||||||
// entry and escapes as a late rejection for the host boot to report.
|
// entry and escapes as a late rejection for the host boot to report.
|
||||||
const rejections: unknown[] = []
|
const rejections: unknown[] = []
|
||||||
const onUnhandled = (err: unknown): void => { rejections.push(err) }
|
const onUnhandled = (err: unknown): void => { rejections.push(err) }
|
||||||
process.on('unhandledRejection', onUnhandled)
|
process.on('unhandledRejection', onUnhandled)
|
||||||
try {
|
try {
|
||||||
const ctx = await boot([])
|
const ctx = await boot(configLines)
|
||||||
const entry = [...ctx.loader.entries()].find(e => e.options.name === '@deepseek-ai/dsh-tool-todo')
|
const entry = [...ctx.loader.entries()].find(e => e.options.name === '@deepseek-ai/dsh-tool-todo')
|
||||||
expect(entry?.fiber?.state).toBe(FiberState.FAILED)
|
expect(entry?.fiber?.state).toBe(FiberState.FAILED)
|
||||||
for (let i = 0; i < 100 && rejections.length === 0; i++) {
|
for (let i = 0; i < 100 && rejections.length === 0; i++) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 10))
|
await new Promise(resolve => setTimeout(resolve, 10))
|
||||||
}
|
}
|
||||||
expect(rejections.map(String).join('\n')).toContain('$.allowParallelInProgress missing required value')
|
expect(rejections.map(String).join('\n')).toContain(failure)
|
||||||
} finally {
|
} finally {
|
||||||
process.off('unhandledRejection', onUnhandled)
|
process.off('unhandledRejection', onUnhandled)
|
||||||
}
|
}
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -4779,6 +4779,12 @@ importers:
|
|||||||
specifier: ^4.4.3
|
specifier: ^4.4.3
|
||||||
version: 4.4.3
|
version: 4.4.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@cordisjs/plugin-include':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../../vendor/include
|
||||||
|
'@cordisjs/plugin-loader':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../../vendor/loader
|
||||||
'@deepseek-ai/dsh-agent':
|
'@deepseek-ai/dsh-agent':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../core/agent
|
version: link:../../core/agent
|
||||||
@@ -4814,7 +4820,7 @@ importers:
|
|||||||
version: link:../../ui/user-interaction
|
version: link:../../ui/user-interaction
|
||||||
cordis:
|
cordis:
|
||||||
specifier: ^4.0.0-rc.7
|
specifier: ^4.0.0-rc.7
|
||||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
|
version: 4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader)
|
||||||
|
|
||||||
packages/ui/app-boot:
|
packages/ui/app-boot:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user