Merge remote-tracking branch 'origin/master' into feat/todo-multi-in-progress

# Conflicts:
#	.agents/notes/implemented/feature/2026-06-29-todo-write-tool.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-23-web-todo-display.i18n.yaml
#	docs/core-data-structures/session.i18n.yaml
#	examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-write/session.jsonl
#	examples/acp-agent/tests/snapshots/todo-write/session.jsonl
#	examples/headless-agent/tests/todo-write.e2e.ts
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/README.zh.md
#	packages/todo/tool-todo/README.i18n.yaml
This commit is contained in:
Chinesezjc
2026-07-28 20:41:55 +08:00
391 changed files with 6129 additions and 3065 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/todo/tool-todo/README.md
README.md: 5e8392686507c996424e8d463c5deda7efee10ab
README.zh.md: 49b75869ac9a52de6832449083cb01d22e857e50
README.md: 48d221b696dc692eeee072ce4a47b845b5fac9c7
README.zh.md: c64727f66dffaf670b4f997718e76b769c3de299

View File

@@ -20,11 +20,11 @@ Beyond the schema's type/required/enum checks, `execute` rejects an empty or dup
## Rendering
The canonical result is `{ todos, counts: { pending, inProgress, completed } }`; its Native renderer returns the compact update acknowledgement. The tool also writes the full `todo/write` session event. UIs subscribe to the event stream and render that durable list themselves: the [TUI app](../../examples/tui-demo) shows it as a persistent plan, and the [web client](../../client/ui-conversation) renders a plan strip plus a dedicated tool row off `ConversationSnapshot.todos` ([Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-web-todo-display.md)).
The canonical result is `{ todos, counts: { pending, inProgress, completed } }`; its Native renderer returns the compact update acknowledgement. The tool also writes the full `todo/write` session event. UIs subscribe to the event stream and render that durable list themselves: the [TUI app](../../examples/tui-demo) and the [web client](../../client/ui-conversation) show a plan strip (plus a dedicated web tool row) off the standing plan — latest `todo/write` with no later `turn/start` ([display](../../../.agents/notes/implemented/feature/2026-07-23-web-todo-display.md), [lifetime](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md)).
## Session projection
When the composition mounts `ctx.sessionProjections` ([`@deepseek-ai/dsh-session-projection`](../../session-projection/session-projection/README.md)), this package registers the `todos` projection unit under an injected child: `init` = `null` (no write yet), `apply` = take the whole list from each `todo/write` (last-wins; every other event returns the same state reference), `view` = identity, `stateVersion` = 1. The key merges into `SessionProjectionMap` here (via the interface package's `/types` outlet); the framework drives the unit and carriers serve the value on the history tail page and the `session/projection` push frame. Compositions without the registry are unaffected.
When the composition mounts `ctx.sessionProjections` ([`@deepseek-ai/dsh-session-projection`](../../session-projection/session-projection/README.md)), this package registers the `todos` projection unit under an injected child: `init` = `null` (no write yet), `apply` = take the whole list from each `todo/write` and clear to `null` on each `turn/start` (standing plan; `turn/end` keeps the finished checklist; every other event returns the same state reference), `view` = identity, `stateVersion` = 2. The key merges into `SessionProjectionMap` here (via the interface package's `/types` outlet); the framework drives the unit and carriers serve the value on the history tail page and the `session/projection` push frame. Compositions without the registry are unaffected. Lifetime rationale: [todo plan clears on next turn](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md).
## Export shape

View File

@@ -20,11 +20,11 @@
## 渲染
规范结果为 `{ todos, counts: { pending, inProgress, completed } }`;其 Native 渲染器返回精简的更新确认。工具还会写入完整 `todo/write` 会话事件。UI 订阅事件流,并自行渲染该持久列表:[TUI 应用](../../examples/tui-demo)将其显示为持久计划,[web 客户端](../../client/ui-conversation)基于 `ConversationSnapshot.todos` 渲染计划横条与专属工具行([Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-web-todo-display.md))。
规范结果为 `{ todos, counts: { pending, inProgress, completed } }`;其 Native 渲染器返回精简的更新确认。工具还会写入完整 `todo/write` 会话事件。UI 订阅事件流,并自行渲染该持久列表:[TUI 应用](../../examples/tui-demo)[web 客户端](../../client/ui-conversation)基于站立计划(其后没有更晚 `turn/start` 的最近一次 `todo/write`显示计划条web 另有专属工具行[展示](../../../.agents/notes/implemented/feature/2026-07-23-web-todo-display.md)、[生命周期](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md))。
## 会话投影
当组合挂载了 `ctx.sessionProjections`[`@deepseek-ai/dsh-session-projection`](../../session-projection/session-projection/README.md))时,本包在一个注入式子插件下注册 `todos` 投影单元:`init` = `null`(尚无写入)、`apply` = 从每个 `todo/write` 取整表last-wins;其余事件都返回同一个状态引用)、`view` = 恒等、`stateVersion` = 1。key 在本包合并进 `SessionProjectionMap`(经接口包的 `/types` 出口);框架驱动该单元,载体在历史尾页与 `session/projection` 推送帧上供给该值。未装注册表的组合不受影响。
当组合挂载了 `ctx.sessionProjections`[`@deepseek-ai/dsh-session-projection`](../../session-projection/session-projection/README.md))时,本包在一个注入式子插件下注册 `todos` 投影单元:`init` = `null`(尚无写入)、`apply` = 从每个 `todo/write` 取整表,并在每个 `turn/start` 清为 `null`(站立计划;`turn/end` 保留刚完成的清单;其余事件都返回同一个状态引用)、`view` = 恒等、`stateVersion` = 2。key 在本包合并进 `SessionProjectionMap`(经接口包的 `/types` 出口);框架驱动该单元,载体在历史尾页与 `session/projection` 推送帧上供给该值。未装注册表的组合不受影响。生命周期理由见 [下一轮清空 todo 计划条](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md)。
## 导出形状

View File

@@ -75,17 +75,23 @@ const todosProjectionSchema: ZodType<TodoItem[] | null> = z.union([
/** Register the `todo_write` tool on `ctx.tools` and, when the session-projection seam is composed, the `todos` unit. */
export function apply(ctx: Context): void {
// The unit child activates only when a projection registry is composed
// (headless assemblies without the seam stay unaffected). Pure last-wins
// fold: state is the latest whole todo/write list, null before the first
// write; every other event returns the same reference (no downstream work).
// (headless assemblies without the seam stay unaffected). Standing-plan fold:
// latest whole todo/write list, cleared by the next turn/start (turn/end keeps
// the finished checklist visible); null before the first write or after a
// later turn begins; every other event returns the same state reference.
ctx.inject(['sessionProjections'], (projectionCtx) => {
projectionCtx.sessionProjections.register<'todos', TodoItem[] | null>({
key: 'todos',
schema: todosProjectionSchema,
init: () => null,
apply: (state, event) => (event.type === 'todo/write' ? event.data.todos : state),
apply: (state, event) => {
if (event.type === 'todo/write') return event.data.todos
if (event.type === 'turn/start') return null
return state
},
view: state => state,
stateVersion: 1,
// Fold semantics changed: turn/start clears the standing plan (was last-write-wins only).
stateVersion: 2,
})
})
ctx.tools.register(defineTool({

View File

@@ -1,3 +1,4 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
@@ -59,12 +60,12 @@ describe('todo_write tool through the agent loop', () => {
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('it-todo'), { provider: 'mock', model: 'mock' })
agent.followup({ content: [{ type: 'text', text: 'plan a two-step task' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'plan a two-step task' }], source: { kind: 'user' } }))
await waitForIdle(ctx, agent)
const log = agent.session.events
expect(findEvent(log, 'tool/call').data.name).toBe('todo_write')
expect(findEvent(log, 'tool/result').data.isError).toBe(false)
expect(findEvent(log, 'tool/result').data.message.content[0].isError).toBe(false)
const todoEvent = findEvent(log, 'todo/write')
expect(todoEvent.data.todos).toEqual([
@@ -87,7 +88,7 @@ describe('todo_write tool through the agent loop', () => {
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('it-todo-2'), { provider: 'mock', model: 'mock' })
agent.followup({ content: [{ type: 'text', text: 'plan then update' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'plan then update' }], source: { kind: 'user' } }))
await waitForIdle(ctx, agent)
const todoEvents = agent.session.events.filter(e => e.type === 'todo/write')

View File

@@ -11,6 +11,7 @@ import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import type { Session, TodoItem } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
@@ -58,7 +59,10 @@ async function harness(withTodoTool: boolean): Promise<Bench> {
/** One paginable message so the tail page is non-degenerate. */
function seedMessage(session: Session): void {
session.append('user/message', { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'hi' }],
source: { kind: 'user' },
}), { surfaceOp: 'append' })
}
describe('todos projection provider', () => {
@@ -87,6 +91,20 @@ describe('todos projection provider', () => {
expect(projections?.asOfSeq).toBe(session.seq - 1)
})
it('clears the standing plan on the next turn/start (turn/end keeps it)', async () => {
const bench = await harness(true)
const session = bench.session
seedMessage(session)
const list: TodoItem[] = [{ content: 'done', status: 'completed' }]
session.append('todo/write', { todos: list })
session.append('turn/end', { turn: 0, reason: { kind: 'completed' } })
expect((await bench.tailProjections())?.values.todos).toEqual(list)
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
const cleared = await bench.tailProjections()
expect(cleared?.values.todos).toBeNull()
expect(cleared?.asOfSeq).toBe(session.seq - 1)
})
it('has no todos key when tool-todo is not composed', async () => {
const bench = await harness(false)
seedMessage(bench.session)