@deepseek-ai/dsh-tool-todo 0.0.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +73 -0
- package/README.zh.md +73 -0
- package/lib/index.js +195 -0
- package/lib/invariant.js +54 -0
- package/lib/types/client.d.ts +10 -0
- package/lib/types/client.js +10 -0
- package/lib/types/index.d.ts +32 -0
- package/lib/types/index.js +192 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/invariant.js +62 -0
- package/lib/types/types.d.ts +22 -0
- package/lib/types/types.js +11 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/todo/tool-todo/README.md
|
|
5
|
+
README.md: 4848758dd8f2049221901744e5d09cef2dd31591
|
|
6
|
+
README.zh.md: 39cb243d1cf59bf51e524a9a778f67e841ae16ff
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-tool-todo
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
The model-facing `todo_write` tool: the agent's whole task list, replaced wholesale on each call.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
Registers one tool, `todo_write(todos: [{ content, status }])`, on `ctx.tools`. The model sends the ENTIRE list every call — there are no partial updates or per-item edits. Each call appends a `todo/write` event (the full list snapshot) to the calling agent's session log via `agent.session.append('todo/write', { todos })`; the current list is the most recent such event (last-write-wins on replay).
|
|
10
|
+
|
|
11
|
+
`status` is one of `pending`, `in_progress`, or `completed`.
|
|
12
|
+
|
|
13
|
+
## Single owner
|
|
14
|
+
|
|
15
|
+
The list belongs to the ONE agent session that called the tool. There is no subagent/shared/swarm scope: a non-agent caller (no `exec.agent`) has nowhere to write the list and is rejected. This is a deliberate scope limit — see the Agent Note.
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
`allowParallelInProgress` is required: every composition must choose whether several todos may be `in_progress` at once. It is a deployment choice, not a fixed rule: whether concurrent active tasks are legitimate depends on runtime concurrency the tool cannot observe. Use `true` for agents that may fan out work and `false` to enforce the single-active discipline.
|
|
20
|
+
|
|
21
|
+
The flag moves the model-facing instruction and the accepted input together — `true` asks the model to mark every actively worked task and accepts any number, `false` asks for exactly one and rejects a call marking more with `Error: invalid todos: at most one task may be in_progress (got <n>)`. The durable-log invariant does NOT follow it: a log written while parallel work was allowed must still replay after a deployment tightens the policy, so the invariant stays silent on the active count.
|
|
22
|
+
|
|
23
|
+
## Validation
|
|
24
|
+
|
|
25
|
+
Beyond the schema's type/required/enum checks, `execute` rejects an empty or duplicate `content`, and any item key beyond `content`/`status` — an extended item shape (ids, nesting) fails loud instead of silently flattening, keeping the logged snapshot equal to what the model believes it wrote. How many tasks may be `in_progress` at once is the deployment's call (§ Configuration): a composition that chooses `true` permits parallel work (concurrent subagents, background commands) to mark several tasks simultaneously. Ordering and the discipline of keeping the list current are left to the model via the tool description.
|
|
26
|
+
|
|
27
|
+
## Rendering
|
|
28
|
+
|
|
29
|
+
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 [web client](../../client/ui-conversation) shows a plan strip plus a dedicated 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)).
|
|
30
|
+
|
|
31
|
+
## Session projection
|
|
32
|
+
|
|
33
|
+
When the composition mounts `ctx.sessionProjections` ([`@deepseek-ai/dsh-session-projection`](../../session/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 Service Definition 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).
|
|
34
|
+
|
|
35
|
+
## Export shape
|
|
36
|
+
|
|
37
|
+
A function/namespace plugin: it exports `name` / `inject` / `apply` and NO default. A stray `export default` would collapse the module via the Loader's `unwrapExports` and drop `inject` (see [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)).
|
|
38
|
+
|
|
39
|
+
## Model Experience
|
|
40
|
+
|
|
41
|
+
### Tool schema
|
|
42
|
+
|
|
43
|
+
#### What the model sees
|
|
44
|
+
|
|
45
|
+
The model sees the generated [`todo_write` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-todo).
|
|
46
|
+
|
|
47
|
+
#### Token effect
|
|
48
|
+
|
|
49
|
+
Fixed schema cost on every request where the tool is visible.
|
|
50
|
+
|
|
51
|
+
#### KV Cache effect
|
|
52
|
+
|
|
53
|
+
Prefix-stable while the definition and visibility are unchanged. Plugin lifecycle or scoped restrictions may invalidate reuse from this schema.
|
|
54
|
+
|
|
55
|
+
### Tool-call history and result
|
|
56
|
+
|
|
57
|
+
#### What the model sees
|
|
58
|
+
|
|
59
|
+
Each assistant tool call retains the entire replacement list in its arguments. Success returns exactly `Updated todo list: <pending> pending, <inProgress> in progress, <completed> completed.` Stable failures are ``Error: invalid todo: `content` must be a non-empty string``, `Error: invalid todos: duplicate content "<content>"`, `Error: todo_write requires an owning agent session`, and — only where the deployment set `allowParallelInProgress: false` — `Error: invalid todos: at most one task may be in_progress (got <n>)`. The full `todo/write` session event is UI and replay state, not a second model message.
|
|
60
|
+
|
|
61
|
+
#### Token effect
|
|
62
|
+
|
|
63
|
+
Token growth scales with every full list the model submits, and those call arguments remain until compaction. The result itself is small and fixed-shape.
|
|
64
|
+
|
|
65
|
+
#### KV Cache effect
|
|
66
|
+
|
|
67
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
68
|
+
|
|
69
|
+
## Known Limitations and Deferred Work
|
|
70
|
+
|
|
71
|
+
- **Single-owner scope only** — the list belongs to the one calling agent session; subagent/shared/swarm scopes are a deliberate cut (see § Single owner), and a non-agent caller is rejected.
|
|
72
|
+
- **The item shape is deliberately minimal** — `content` plus three-state `status`; whole-list replacement needs no stable id, priority, or active-form fields.
|
|
73
|
+
- **Whole-list replacement is the only operation** — no partial updates, no read-back tool; the model must resend the entire list each call.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-tool-todo
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
面向模型的 `todo_write` 工具:agent(智能体)的完整任务列表,每次调用都会整体替换。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
注册一个工具 `todo_write(todos: [{ content, status }])` 到 `ctx.tools`。模型每次调用都会发送完整列表,不存在部分更新或单项编辑。每次调用都会向调用 agent 的会话日志追加 `todo/write` 事件(完整列表快照),具体调用 `agent.session.append('todo/write', { todos })`;当前列表是最新的该类事件(回放时后写覆盖先写)。
|
|
10
|
+
|
|
11
|
+
`status` 是 `pending`、`in_progress` 或 `completed` 之一。
|
|
12
|
+
|
|
13
|
+
## 单一所有者
|
|
14
|
+
|
|
15
|
+
该列表属于调用工具的唯一 agent 会话。不存在 subagent/共享/swarm scope:非 agent 调用方(没有 `exec.agent`)无处写入列表,因此会被拒绝。这是有意设置的 scope 限制,详见 Agent Note(agent 决策记录)。
|
|
16
|
+
|
|
17
|
+
## 配置
|
|
18
|
+
|
|
19
|
+
`allowParallelInProgress` 是必填项:每个组合都必须选择是否允许多个 todo 同时处于 `in_progress`。这是部署层的选择而非固定规则:并发的活跃任务是否合理,取决于工具无法观测的运行时并发情况。可能并行展开工作的 agent 使用 `true`,`false` 则强制执行单活跃项纪律。
|
|
20
|
+
|
|
21
|
+
该开关会同时改变面向模型的指令与接受的输入——`true` 要求模型标记每个正在推进的任务并接受任意数量;`false` 要求恰好一个,并以 `Error: invalid todos: at most one task may be in_progress (got <n>)` 拒绝标记更多的调用。持久日志不变式**不**跟随它:在允许并行时写下的日志,在部署收紧策略之后仍必须可回放,因此不变式对活跃数量保持沉默。
|
|
22
|
+
|
|
23
|
+
## 验证
|
|
24
|
+
|
|
25
|
+
除 schema 的类型/必填/枚举检查外,`execute` 还会拒绝空或重复的 `content`,以及 `content`/`status` 之外的任何条目键——扩展条目形状(id、嵌套)会明确报错而不是被静默压平,保证落日志的快照与模型自认为写入的内容一致。同时可以有多少任务处于 `in_progress` 由部署决定(见 § 配置):选择 `true` 的组合允许并行工作(并发 subagent、后台命令)同时将多个任务标记为 `in_progress`。列表的顺序及及时更新由模型依照工具描述负责。
|
|
26
|
+
|
|
27
|
+
## 渲染
|
|
28
|
+
|
|
29
|
+
规范结果为 `{ todos, counts: { pending, inProgress, completed } }`;其 Native 渲染器返回精简的更新确认。工具还会写入完整 `todo/write` 会话事件。UI 订阅事件流,并自行渲染该持久化列表:[web 客户端](../../client/ui-conversation)基于当前有效计划(其后没有更晚 `turn/start` 的最近一次 `todo/write`)显示计划条和专属工具行([展示](../../../.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))。
|
|
30
|
+
|
|
31
|
+
## 会话投影
|
|
32
|
+
|
|
33
|
+
当组合挂载了 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session/session-projection/README.md))时,本包在一个注入的子插件中注册 `todos` 投影单元:`init` = `null`(尚无写入)、`apply` = 从每个 `todo/write` 取整表,并在每个 `turn/start` 清为 `null`(当前有效计划;`turn/end` 保留刚完成的清单;其余事件都返回同一个状态引用)、`view` = 恒等、`stateVersion` = 2。该键在本包中合并进 `SessionProjectionMap`(经 Service Definition 包的 `/types` 出口);框架驱动该单元,载体通过历史尾页与 `session/projection` 推送帧提供该值。未挂载注册表的组合不受影响。生命周期理由见 [在下一轮次清空 todo 计划](../../../.agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md)。
|
|
34
|
+
|
|
35
|
+
## 导出形状
|
|
36
|
+
|
|
37
|
+
函数/命名空间插件:导出 `name`/`inject`/`apply`,不提供默认导出。意外的 `export default` 会被 Loader 的 `unwrapExports` 折叠为默认导出,并导致 `inject` 丢失(参见 [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md))。
|
|
38
|
+
|
|
39
|
+
## 模型体验
|
|
40
|
+
|
|
41
|
+
### 工具 schema
|
|
42
|
+
|
|
43
|
+
#### 模型看到的内容
|
|
44
|
+
|
|
45
|
+
模型会看到生成的 [`todo_write` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-todo)。
|
|
46
|
+
|
|
47
|
+
#### Token 影响
|
|
48
|
+
|
|
49
|
+
工具可见的每个请求都有固定的 schema token 开销。
|
|
50
|
+
|
|
51
|
+
#### KV Cache 影响
|
|
52
|
+
|
|
53
|
+
只要定义和可见性不变,前缀就保持稳定。插件生命周期或 scope 限制可能会使从此 schema 起的缓存复用失效。
|
|
54
|
+
|
|
55
|
+
### 工具调用历史与结果
|
|
56
|
+
|
|
57
|
+
#### 模型看到的内容
|
|
58
|
+
|
|
59
|
+
每个 assistant 工具调用都会在参数中保留整个替换列表。成功时原样返回 `Updated todo list: <pending> pending, <inProgress> in progress, <completed> completed.`。稳定失败文本为 ``Error: invalid todo: `content` must be a non-empty string``、`Error: invalid todos: duplicate content "<content>"`、`Error: todo_write requires an owning agent session`,以及——仅在部署设置了 `allowParallelInProgress: false` 时——`Error: invalid todos: at most one task may be in_progress (got <n>)`。完整 `todo/write` 会话事件是 UI 与回放状态,而非第二条模型消息。
|
|
60
|
+
|
|
61
|
+
#### Token 影响
|
|
62
|
+
|
|
63
|
+
token 用量会随模型每次提交的完整列表增长,且这些调用参数会保留到压缩(compaction)。结果本身很小,且形状固定。
|
|
64
|
+
|
|
65
|
+
#### KV Cache 影响
|
|
66
|
+
|
|
67
|
+
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。
|
|
68
|
+
|
|
69
|
+
## 已知限制与暂缓事项
|
|
70
|
+
|
|
71
|
+
- **仅单一所有者 scope**:列表属于唯一调用 agent 会话;subagent/共享/swarm scope 是有意设置的限制(参见「单一所有者」一节),非 agent 调用方会被拒绝。
|
|
72
|
+
- **条目形状有意保持最小**:`content` 加三态 `status`;整表替换不需要稳定 id、优先级或 active-form 字段。
|
|
73
|
+
- **整表替换是唯一操作**:没有部分更新,也没有回读工具;模型每次调用都必须重新发送完整列表。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { z as z$1 } from "zod";
|
|
3
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
|
+
//#region lib/types/index.js
|
|
5
|
+
/**
|
|
6
|
+
* Model-facing whole-list replacement. Each call appends a `todo/write` snapshot to the calling
|
|
7
|
+
* agent's session; replay is last-write-wins, and UIs render from session events. A non-agent
|
|
8
|
+
* caller has no owning list and is rejected. Named exports preserve loader injection metadata.
|
|
9
|
+
* @module @deepseek-ai/dsh-tool-todo
|
|
10
|
+
*/
|
|
11
|
+
const name = "tool-todo";
|
|
12
|
+
const inject = ["tools"];
|
|
13
|
+
/** The valid {@link TodoItem} statuses, as a runtime set for input narrowing. */
|
|
14
|
+
const STATUSES = [
|
|
15
|
+
"pending",
|
|
16
|
+
"in_progress",
|
|
17
|
+
"completed"
|
|
18
|
+
];
|
|
19
|
+
/** Schemastery configuration for the todo tool consumer. */
|
|
20
|
+
const Config = z.object({ allowParallelInProgress: z.boolean().required() });
|
|
21
|
+
const DESCRIPTION_HEAD = "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. ";
|
|
22
|
+
const DESCRIPTION_PARALLEL = "Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. ";
|
|
23
|
+
const DESCRIPTION_SINGLE = "Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. ";
|
|
24
|
+
const DESCRIPTION_TAIL = "Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).";
|
|
25
|
+
/**
|
|
26
|
+
* The model-facing description for one activation. The active-status clause is the only part that
|
|
27
|
+
* varies, because it is the only instruction the parallel policy changes.
|
|
28
|
+
* @param allowParallel - whether several todos may be `in_progress` at once.
|
|
29
|
+
* @returns the composed tool description.
|
|
30
|
+
*/
|
|
31
|
+
function describe(allowParallel) {
|
|
32
|
+
return DESCRIPTION_HEAD + (allowParallel ? DESCRIPTION_PARALLEL : DESCRIPTION_SINGLE) + DESCRIPTION_TAIL;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate the value constraints the ParameterSchemaSpec can't express and build the canonical {@link
|
|
36
|
+
* TodoItem}[]: trimmed non-empty unique content, and at most one `in_progress` item unless the
|
|
37
|
+
* deployment allows parallel work. The registry has already enforced the status enum and rejected
|
|
38
|
+
* unknown item keys (`additionalProperties: false` — the logged snapshot must equal what the model
|
|
39
|
+
* believes it wrote, so a nested/extended item shape fails loud at the schema boundary instead of
|
|
40
|
+
* silently flattening); the cast below records that guarantee.
|
|
41
|
+
* @param raw - the model-supplied list, already schema-checked.
|
|
42
|
+
* @param allowParallel - whether several items may be `in_progress` at once.
|
|
43
|
+
* @returns the canonical list.
|
|
44
|
+
*/
|
|
45
|
+
function toTodoList(raw, allowParallel) {
|
|
46
|
+
const todos = [];
|
|
47
|
+
const seen = /* @__PURE__ */ new Set();
|
|
48
|
+
let active = 0;
|
|
49
|
+
for (const item of raw) {
|
|
50
|
+
const content = item.content.trim();
|
|
51
|
+
if (content.length === 0) throw new Error("invalid todo: `content` must be a non-empty string");
|
|
52
|
+
if (seen.has(content)) throw new Error(`invalid todos: duplicate content ${JSON.stringify(content)}`);
|
|
53
|
+
seen.add(content);
|
|
54
|
+
if (item.status === "in_progress") active++;
|
|
55
|
+
todos.push({
|
|
56
|
+
content,
|
|
57
|
+
status: item.status
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (!allowParallel && active > 1) throw new Error(`invalid todos: at most one task may be in_progress (got ${active})`);
|
|
61
|
+
return todos;
|
|
62
|
+
}
|
|
63
|
+
/** Wire payload schema of the `todos` projection (whole list or pre-first-write null). */
|
|
64
|
+
const todosProjectionSchema = z$1.union([z$1.array(z$1.object({
|
|
65
|
+
content: z$1.string(),
|
|
66
|
+
status: z$1.union([
|
|
67
|
+
z$1.literal("pending"),
|
|
68
|
+
z$1.literal("in_progress"),
|
|
69
|
+
z$1.literal("completed")
|
|
70
|
+
])
|
|
71
|
+
})), z$1.null()]);
|
|
72
|
+
/**
|
|
73
|
+
* Register the `todo_write` tool on `ctx.tools` and, when the session-projection seam is composed,
|
|
74
|
+
* the `todos` unit.
|
|
75
|
+
* @param ctx - registrant context carrying the tool registry.
|
|
76
|
+
* @param config - deployment's explicit todo policy.
|
|
77
|
+
*/
|
|
78
|
+
function apply(ctx, config) {
|
|
79
|
+
const allowParallel = config.allowParallelInProgress;
|
|
80
|
+
ctx.inject(["sessionProjections"], (projectionCtx) => {
|
|
81
|
+
projectionCtx.sessionProjections.register({
|
|
82
|
+
key: "todos",
|
|
83
|
+
schema: todosProjectionSchema,
|
|
84
|
+
init: () => null,
|
|
85
|
+
apply: (state, event) => {
|
|
86
|
+
if (event.type === "todo/write") return event.data.todos;
|
|
87
|
+
if (event.type === "turn/start") return null;
|
|
88
|
+
return state;
|
|
89
|
+
},
|
|
90
|
+
view: (state) => state,
|
|
91
|
+
stateVersion: 2
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
ctx.tools.register(defineTool({
|
|
95
|
+
name: "todo_write",
|
|
96
|
+
description: describe(allowParallel),
|
|
97
|
+
parameters: { todos: {
|
|
98
|
+
type: "array",
|
|
99
|
+
required: true,
|
|
100
|
+
description: "The COMPLETE task list, replacing any previous list.",
|
|
101
|
+
items: {
|
|
102
|
+
type: "object",
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
properties: {
|
|
105
|
+
content: {
|
|
106
|
+
type: "string",
|
|
107
|
+
required: true,
|
|
108
|
+
description: "What the task is — a short imperative line."
|
|
109
|
+
},
|
|
110
|
+
status: {
|
|
111
|
+
type: "string",
|
|
112
|
+
required: true,
|
|
113
|
+
enum: [...STATUSES],
|
|
114
|
+
description: "pending (not started) | in_progress (now) | completed (done)."
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} },
|
|
119
|
+
output: {
|
|
120
|
+
schema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
properties: {
|
|
124
|
+
todos: {
|
|
125
|
+
type: "array",
|
|
126
|
+
required: true,
|
|
127
|
+
items: {
|
|
128
|
+
type: "object",
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
properties: {
|
|
131
|
+
content: {
|
|
132
|
+
type: "string",
|
|
133
|
+
required: true
|
|
134
|
+
},
|
|
135
|
+
status: {
|
|
136
|
+
type: "string",
|
|
137
|
+
required: true,
|
|
138
|
+
enum: [...STATUSES]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
counts: {
|
|
144
|
+
type: "object",
|
|
145
|
+
additionalProperties: false,
|
|
146
|
+
required: true,
|
|
147
|
+
properties: {
|
|
148
|
+
pending: {
|
|
149
|
+
type: "integer",
|
|
150
|
+
required: true
|
|
151
|
+
},
|
|
152
|
+
inProgress: {
|
|
153
|
+
type: "integer",
|
|
154
|
+
required: true
|
|
155
|
+
},
|
|
156
|
+
completed: {
|
|
157
|
+
type: "integer",
|
|
158
|
+
required: true
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
render: (_args, value) => [{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: `Updated todo list: ${value.counts.pending} pending, ${value.counts.inProgress} in progress, ${value.counts.completed} completed.`
|
|
167
|
+
}]
|
|
168
|
+
},
|
|
169
|
+
execute(args, exec) {
|
|
170
|
+
const todos = toTodoList(args.todos, allowParallel);
|
|
171
|
+
if (!exec.agent) throw new Error("todo_write requires an owning agent session");
|
|
172
|
+
exec.agent.session.append("todo/write", { todos });
|
|
173
|
+
const count = (status) => todos.filter((t) => t.status === status).length;
|
|
174
|
+
return Promise.resolve({
|
|
175
|
+
todos: todos.map((todo) => ({
|
|
176
|
+
content: todo.content,
|
|
177
|
+
status: todo.status
|
|
178
|
+
})),
|
|
179
|
+
counts: {
|
|
180
|
+
pending: count("pending"),
|
|
181
|
+
inProgress: count("in_progress"),
|
|
182
|
+
completed: count("completed")
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
presentCall: (args) => ({
|
|
187
|
+
card: "generic",
|
|
188
|
+
title: "Update todo list",
|
|
189
|
+
kind: "other",
|
|
190
|
+
rawInput: args.todos
|
|
191
|
+
})
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
export { Config, apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/** Package-owned durable todo-snapshot invariants. @module @deepseek-ai/dsh-tool-todo/invariant */
|
|
3
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-tool-todo";
|
|
4
|
+
const TODO_STATUSES = new Set([
|
|
5
|
+
"pending",
|
|
6
|
+
"in_progress",
|
|
7
|
+
"completed"
|
|
8
|
+
]);
|
|
9
|
+
/** Cordis companion plugin name. */
|
|
10
|
+
const name = "tool-todo-invariant";
|
|
11
|
+
/** Service required before the companion can reserve package ownership. */
|
|
12
|
+
const inject = ["invariants"];
|
|
13
|
+
/**
|
|
14
|
+
* Validate one whole-list todo snapshot before it reaches the durable log.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately silent on how many items are `in_progress`. That is the tool's
|
|
17
|
+
* per-deployment policy (`Config.allowParallelInProgress`), not a durable-shape
|
|
18
|
+
* rule: a log written while parallel work was allowed must still replay after a
|
|
19
|
+
* deployment tightens the policy, so tying the invariant to the current config
|
|
20
|
+
* would reject history that was valid when it was written.
|
|
21
|
+
*/
|
|
22
|
+
function validateTodos(value, fail) {
|
|
23
|
+
if (!Array.isArray(value)) fail("todo/write todos must be an array");
|
|
24
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25
|
+
for (const item of value) {
|
|
26
|
+
if (typeof item !== "object" || item === null) fail("todo/write entries must be objects");
|
|
27
|
+
const { content, status } = item;
|
|
28
|
+
if (typeof content !== "string" || content.length === 0 || content.trim() !== content) fail("todo/write content must be non-empty and already trimmed");
|
|
29
|
+
if (seen.has(content)) fail(`todo/write repeats content ${JSON.stringify(content)}`);
|
|
30
|
+
seen.add(content);
|
|
31
|
+
if (typeof status !== "string" || !TODO_STATUSES.has(status)) fail(`todo/write carries unknown status ${JSON.stringify(status)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Validate the package-owned event fields and ignore unrelated events. */
|
|
35
|
+
function validateEvent(event, fail) {
|
|
36
|
+
if (event.type === "todo/write") validateTodos(event.data.todos, fail);
|
|
37
|
+
}
|
|
38
|
+
/** Install validation for loaded and newly appended whole-list todo snapshots. */
|
|
39
|
+
const install = Object.assign((ctx, fail) => {
|
|
40
|
+
for (const session of ctx.sessions.list()) for (const event of session.events) validateEvent(event, fail);
|
|
41
|
+
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
42
|
+
if (eventName !== "session/event") return;
|
|
43
|
+
const event = args[1];
|
|
44
|
+
validateEvent(event, fail);
|
|
45
|
+
}, { global: true });
|
|
46
|
+
}, { inject: ["sessions"] });
|
|
47
|
+
/**
|
|
48
|
+
* Register the todo invariant companion.
|
|
49
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
50
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
51
|
+
*/
|
|
52
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
53
|
+
//#endregion
|
|
54
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-namespace projection of the todo domain: a pure re-export of the package's
|
|
3
|
+
* types outlet. Client code imports ONLY the client namespace (repo
|
|
4
|
+
* discipline), so `./client` projects the same single-source content
|
|
5
|
+
* `./types` serves to host consumers — zero duplication.
|
|
6
|
+
*
|
|
7
|
+
* @module @deepseek-ai/dsh-tool-todo/client
|
|
8
|
+
*/
|
|
9
|
+
export type * from './types.ts';
|
|
10
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-namespace projection of the todo domain: a pure re-export of the package's
|
|
3
|
+
* types outlet. Client code imports ONLY the client namespace (repo
|
|
4
|
+
* discipline), so `./client` projects the same single-source content
|
|
5
|
+
* `./types` serves to host consumers — zero duplication.
|
|
6
|
+
*
|
|
7
|
+
* @module @deepseek-ai/dsh-tool-todo/client
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing whole-list replacement. Each call appends a `todo/write` snapshot to the calling
|
|
3
|
+
* agent's session; replay is last-write-wins, and UIs render from session events. A non-agent
|
|
4
|
+
* caller has no owning list and is rejected. Named exports preserve loader injection metadata.
|
|
5
|
+
* @module @deepseek-ai/dsh-tool-todo
|
|
6
|
+
*/
|
|
7
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
8
|
+
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
export type * from './types.ts';
|
|
10
|
+
export declare const name = "tool-todo";
|
|
11
|
+
export declare const inject: string[];
|
|
12
|
+
/** Model-facing todo tool configuration. */
|
|
13
|
+
export interface Config {
|
|
14
|
+
/**
|
|
15
|
+
* Required deployment choice for whether several todos may be `in_progress` at once. True suits
|
|
16
|
+
* agents that run work concurrently — subagents, background commands, workflow fan-out — and the
|
|
17
|
+
* description then instructs the model to mark every actively worked task. False restores the
|
|
18
|
+
* single-active discipline: the description asks for exactly one, and a call marking more is
|
|
19
|
+
* rejected.
|
|
20
|
+
*/
|
|
21
|
+
allowParallelInProgress: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Schemastery configuration for the todo tool consumer. */
|
|
24
|
+
export declare const Config: z<Config>;
|
|
25
|
+
/**
|
|
26
|
+
* Register the `todo_write` tool on `ctx.tools` and, when the session-projection seam is composed,
|
|
27
|
+
* the `todos` unit.
|
|
28
|
+
* @param ctx - registrant context carrying the tool registry.
|
|
29
|
+
* @param config - deployment's explicit todo policy.
|
|
30
|
+
*/
|
|
31
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing whole-list replacement. Each call appends a `todo/write` snapshot to the calling
|
|
3
|
+
* agent's session; replay is last-write-wins, and UIs render from session events. A non-agent
|
|
4
|
+
* caller has no owning list and is rejected. Named exports preserve loader injection metadata.
|
|
5
|
+
* @module @deepseek-ai/dsh-tool-todo
|
|
6
|
+
*/
|
|
7
|
+
import z from '@deepseek-ai/schemastery';
|
|
8
|
+
import { z as zod } from 'zod';
|
|
9
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
10
|
+
export const name = 'tool-todo';
|
|
11
|
+
export const inject = ['tools'];
|
|
12
|
+
/** The valid {@link TodoItem} statuses, as a runtime set for input narrowing. */
|
|
13
|
+
const STATUSES = ['pending', 'in_progress', 'completed'];
|
|
14
|
+
/** Schemastery configuration for the todo tool consumer. */
|
|
15
|
+
export const Config = z.object({
|
|
16
|
+
allowParallelInProgress: z.boolean().required(),
|
|
17
|
+
});
|
|
18
|
+
const DESCRIPTION_HEAD = 'Record and update a structured task list for the current work. Send the ENTIRE '
|
|
19
|
+
+ 'list every call — it REPLACES the previous list (there are no partial updates, '
|
|
20
|
+
+ 'no per-item edits). Use it to plan multi-step work and show progress: add one '
|
|
21
|
+
+ 'todo per concrete step before you start. ';
|
|
22
|
+
const DESCRIPTION_PARALLEL = 'Mark every todo being actively worked '
|
|
23
|
+
+ 'on `in_progress` — several at once when work genuinely runs in parallel (e.g. '
|
|
24
|
+
+ 'concurrent subagents or background commands), one for sequential work; while '
|
|
25
|
+
+ 'work remains, at least one task should be `in_progress`. ';
|
|
26
|
+
const DESCRIPTION_SINGLE = 'Keep AT MOST ONE todo `in_progress` at a '
|
|
27
|
+
+ 'time; while work remains, exactly one active task should be `in_progress`. ';
|
|
28
|
+
const DESCRIPTION_TAIL = 'Mark a todo '
|
|
29
|
+
+ '`completed` the moment it is done (do not batch completions), and allow no '
|
|
30
|
+
+ '`in_progress` item only once all work is complete. Skip the list for trivial '
|
|
31
|
+
+ 'single-step tasks. Statuses: `pending` (not started), `in_progress` (being '
|
|
32
|
+
+ 'worked on now), `completed` (finished).';
|
|
33
|
+
/**
|
|
34
|
+
* The model-facing description for one activation. The active-status clause is the only part that
|
|
35
|
+
* varies, because it is the only instruction the parallel policy changes.
|
|
36
|
+
* @param allowParallel - whether several todos may be `in_progress` at once.
|
|
37
|
+
* @returns the composed tool description.
|
|
38
|
+
*/
|
|
39
|
+
function describe(allowParallel) {
|
|
40
|
+
return DESCRIPTION_HEAD
|
|
41
|
+
+ (allowParallel ? DESCRIPTION_PARALLEL : DESCRIPTION_SINGLE)
|
|
42
|
+
+ DESCRIPTION_TAIL;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Validate the value constraints the ParameterSchemaSpec can't express and build the canonical {@link
|
|
46
|
+
* TodoItem}[]: trimmed non-empty unique content, and at most one `in_progress` item unless the
|
|
47
|
+
* deployment allows parallel work. The registry has already enforced the status enum and rejected
|
|
48
|
+
* unknown item keys (`additionalProperties: false` — the logged snapshot must equal what the model
|
|
49
|
+
* believes it wrote, so a nested/extended item shape fails loud at the schema boundary instead of
|
|
50
|
+
* silently flattening); the cast below records that guarantee.
|
|
51
|
+
* @param raw - the model-supplied list, already schema-checked.
|
|
52
|
+
* @param allowParallel - whether several items may be `in_progress` at once.
|
|
53
|
+
* @returns the canonical list.
|
|
54
|
+
*/
|
|
55
|
+
function toTodoList(raw, allowParallel) {
|
|
56
|
+
const todos = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
let active = 0;
|
|
59
|
+
for (const item of raw) {
|
|
60
|
+
const content = item.content.trim();
|
|
61
|
+
if (content.length === 0) {
|
|
62
|
+
throw new Error('invalid todo: `content` must be a non-empty string');
|
|
63
|
+
}
|
|
64
|
+
if (seen.has(content)) {
|
|
65
|
+
throw new Error(`invalid todos: duplicate content ${JSON.stringify(content)}`);
|
|
66
|
+
}
|
|
67
|
+
seen.add(content);
|
|
68
|
+
if (item.status === 'in_progress')
|
|
69
|
+
active++;
|
|
70
|
+
todos.push({ content, status: item.status });
|
|
71
|
+
}
|
|
72
|
+
if (!allowParallel && active > 1) {
|
|
73
|
+
throw new Error(`invalid todos: at most one task may be in_progress (got ${active})`);
|
|
74
|
+
}
|
|
75
|
+
return todos;
|
|
76
|
+
}
|
|
77
|
+
/** Wire payload schema of the `todos` projection (whole list or pre-first-write null). */
|
|
78
|
+
const todosProjectionSchema = zod.union([
|
|
79
|
+
zod.array(zod.object({
|
|
80
|
+
content: zod.string(),
|
|
81
|
+
status: zod.union([zod.literal('pending'), zod.literal('in_progress'), zod.literal('completed')]),
|
|
82
|
+
})),
|
|
83
|
+
zod.null(),
|
|
84
|
+
]);
|
|
85
|
+
/**
|
|
86
|
+
* Register the `todo_write` tool on `ctx.tools` and, when the session-projection seam is composed,
|
|
87
|
+
* the `todos` unit.
|
|
88
|
+
* @param ctx - registrant context carrying the tool registry.
|
|
89
|
+
* @param config - deployment's explicit todo policy.
|
|
90
|
+
*/
|
|
91
|
+
export function apply(ctx, config) {
|
|
92
|
+
const allowParallel = config.allowParallelInProgress;
|
|
93
|
+
// The unit child activates only when a projection registry is composed
|
|
94
|
+
// (headless assemblies without the seam stay unaffected). Standing-plan fold:
|
|
95
|
+
// latest whole todo/write list, cleared by the next turn/start (turn/end keeps
|
|
96
|
+
// the finished checklist visible); null before the first write or after a
|
|
97
|
+
// later turn begins; every other event returns the same state reference.
|
|
98
|
+
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
|
99
|
+
projectionCtx.sessionProjections.register({
|
|
100
|
+
key: 'todos',
|
|
101
|
+
schema: todosProjectionSchema,
|
|
102
|
+
init: () => null,
|
|
103
|
+
apply: (state, event) => {
|
|
104
|
+
if (event.type === 'todo/write')
|
|
105
|
+
return event.data.todos;
|
|
106
|
+
if (event.type === 'turn/start')
|
|
107
|
+
return null;
|
|
108
|
+
return state;
|
|
109
|
+
},
|
|
110
|
+
view: state => state,
|
|
111
|
+
stateVersion: 2,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
ctx.tools.register(defineTool({
|
|
115
|
+
name: 'todo_write',
|
|
116
|
+
description: describe(allowParallel),
|
|
117
|
+
parameters: {
|
|
118
|
+
todos: {
|
|
119
|
+
type: 'array',
|
|
120
|
+
required: true,
|
|
121
|
+
description: 'The COMPLETE task list, replacing any previous list.',
|
|
122
|
+
items: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
additionalProperties: false,
|
|
125
|
+
properties: {
|
|
126
|
+
content: { type: 'string', required: true, description: 'What the task is — a short imperative line.' },
|
|
127
|
+
status: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
required: true,
|
|
130
|
+
enum: [...STATUSES],
|
|
131
|
+
description: 'pending (not started) | in_progress (now) | completed (done).',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
output: {
|
|
138
|
+
schema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
additionalProperties: false,
|
|
141
|
+
properties: {
|
|
142
|
+
todos: {
|
|
143
|
+
type: 'array',
|
|
144
|
+
required: true,
|
|
145
|
+
items: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
additionalProperties: false,
|
|
148
|
+
properties: {
|
|
149
|
+
content: { type: 'string', required: true },
|
|
150
|
+
status: { type: 'string', required: true, enum: [...STATUSES] },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
counts: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
additionalProperties: false,
|
|
157
|
+
required: true,
|
|
158
|
+
properties: {
|
|
159
|
+
pending: { type: 'integer', required: true },
|
|
160
|
+
inProgress: { type: 'integer', required: true },
|
|
161
|
+
completed: { type: 'integer', required: true },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
render: (_args, value) => [{
|
|
167
|
+
type: 'text',
|
|
168
|
+
text: `Updated todo list: ${value.counts.pending} pending, ${value.counts.inProgress} in progress, ${value.counts.completed} completed.`,
|
|
169
|
+
}],
|
|
170
|
+
},
|
|
171
|
+
execute(args, exec) {
|
|
172
|
+
const todos = toTodoList(args.todos, allowParallel);
|
|
173
|
+
if (!exec.agent) {
|
|
174
|
+
// The list is per-agent-session state; a non-agent caller (no owning
|
|
175
|
+
// session) has nowhere to write it. Reject rather than silently no-op.
|
|
176
|
+
throw new Error('todo_write requires an owning agent session');
|
|
177
|
+
}
|
|
178
|
+
exec.agent.session.append('todo/write', { todos });
|
|
179
|
+
const count = (status) => todos.filter(t => t.status === status).length;
|
|
180
|
+
return Promise.resolve({
|
|
181
|
+
todos: todos.map(todo => ({ content: todo.content, status: todo.status })),
|
|
182
|
+
counts: {
|
|
183
|
+
pending: count('pending'),
|
|
184
|
+
inProgress: count('in_progress'),
|
|
185
|
+
completed: count('completed'),
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
presentCall: args => ({ card: 'generic', title: 'Update todo list', kind: 'other', rawInput: args.todos }),
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Package-owned durable todo-snapshot invariants. @module @deepseek-ai/dsh-tool-todo/invariant */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export declare const name = "tool-todo-invariant";
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
export declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Register the todo invariant companion.
|
|
9
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
11
|
+
*/
|
|
12
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Package-owned durable todo-snapshot invariants. @module @deepseek-ai/dsh-tool-todo/invariant */
|
|
2
|
+
const PACKAGE_NAME = '@deepseek-ai/dsh-tool-todo';
|
|
3
|
+
const TODO_STATUSES = new Set(['pending', 'in_progress', 'completed']);
|
|
4
|
+
/** Cordis companion plugin name. */
|
|
5
|
+
export const name = 'tool-todo-invariant';
|
|
6
|
+
/** Service required before the companion can reserve package ownership. */
|
|
7
|
+
export const inject = ['invariants'];
|
|
8
|
+
/**
|
|
9
|
+
* Validate one whole-list todo snapshot before it reaches the durable log.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately silent on how many items are `in_progress`. That is the tool's
|
|
12
|
+
* per-deployment policy (`Config.allowParallelInProgress`), not a durable-shape
|
|
13
|
+
* rule: a log written while parallel work was allowed must still replay after a
|
|
14
|
+
* deployment tightens the policy, so tying the invariant to the current config
|
|
15
|
+
* would reject history that was valid when it was written.
|
|
16
|
+
*/
|
|
17
|
+
function validateTodos(value, fail) {
|
|
18
|
+
if (!Array.isArray(value))
|
|
19
|
+
fail('todo/write todos must be an array');
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
for (const item of value) {
|
|
22
|
+
if (typeof item !== 'object' || item === null)
|
|
23
|
+
fail('todo/write entries must be objects');
|
|
24
|
+
const { content, status } = item;
|
|
25
|
+
if (typeof content !== 'string' || content.length === 0 || content.trim() !== content) {
|
|
26
|
+
fail('todo/write content must be non-empty and already trimmed');
|
|
27
|
+
}
|
|
28
|
+
if (seen.has(content))
|
|
29
|
+
fail(`todo/write repeats content ${JSON.stringify(content)}`);
|
|
30
|
+
seen.add(content);
|
|
31
|
+
if (typeof status !== 'string' || !TODO_STATUSES.has(status)) {
|
|
32
|
+
fail(`todo/write carries unknown status ${JSON.stringify(status)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/* jscpd:ignore-start -- package companions share replay and dispatch plumbing */
|
|
37
|
+
/** Validate the package-owned event fields and ignore unrelated events. */
|
|
38
|
+
function validateEvent(event, fail) {
|
|
39
|
+
if (event.type === 'todo/write')
|
|
40
|
+
validateTodos(event.data.todos, fail);
|
|
41
|
+
}
|
|
42
|
+
/** Install validation for loaded and newly appended whole-list todo snapshots. */
|
|
43
|
+
const install = Object.assign((ctx, fail) => {
|
|
44
|
+
for (const session of ctx.sessions.list()) {
|
|
45
|
+
for (const event of session.events)
|
|
46
|
+
validateEvent(event, fail);
|
|
47
|
+
}
|
|
48
|
+
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
49
|
+
if (eventName !== 'session/event')
|
|
50
|
+
return;
|
|
51
|
+
const event = args[1];
|
|
52
|
+
validateEvent(event, fail);
|
|
53
|
+
}, { global: true });
|
|
54
|
+
}, { inject: ['sessions'] });
|
|
55
|
+
/* jscpd:ignore-end */
|
|
56
|
+
/**
|
|
57
|
+
* Register the todo invariant companion.
|
|
58
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
59
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
60
|
+
*/
|
|
61
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
62
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure types of the todo domain: the ONE home of the `todos` projection-key
|
|
3
|
+
* declaration plus its payload types, free of this package's host-side value
|
|
4
|
+
* imports (dsh-tools, zod). Two namespace projections serve it — `./types`
|
|
5
|
+
* for host consumers, `./client/types` (the browser half-entry's re-export)
|
|
6
|
+
* for client aggregates — with zero content duplication.
|
|
7
|
+
*
|
|
8
|
+
* @module @deepseek-ai/dsh-tool-todo/types
|
|
9
|
+
*/
|
|
10
|
+
import type { TodoItem } from '@deepseek-ai/dsh-session/types';
|
|
11
|
+
export type { TodoItem } from '@deepseek-ai/dsh-session/types';
|
|
12
|
+
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
13
|
+
interface SessionProjectionMap {
|
|
14
|
+
/**
|
|
15
|
+
* The agent's current whole todo list (the latest `todo/write` snapshot),
|
|
16
|
+
* or `null` before the first write. Whole-value rule: every `todo/write`
|
|
17
|
+
* carries the complete replacement list, so the fold is last-wins.
|
|
18
|
+
*/
|
|
19
|
+
todos: TodoItem[] | null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure types of the todo domain: the ONE home of the `todos` projection-key
|
|
3
|
+
* declaration plus its payload types, free of this package's host-side value
|
|
4
|
+
* imports (dsh-tools, zod). Two namespace projections serve it — `./types`
|
|
5
|
+
* for host consumers, `./client/types` (the browser half-entry's re-export)
|
|
6
|
+
* for client aggregates — with zero content duplication.
|
|
7
|
+
*
|
|
8
|
+
* @module @deepseek-ai/dsh-tool-todo/types
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-tool-todo",
|
|
3
|
+
"description": "Model-facing todo_write tool over the DeepSeek Harness event-sourced session log",
|
|
4
|
+
"version": "0.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/todo/tool-todo"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./client": {
|
|
26
|
+
"types": "./lib/types/client.d.ts",
|
|
27
|
+
"default": "./lib/types/client.js"
|
|
28
|
+
},
|
|
29
|
+
"./src/*": "./src/*",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"lib/index.js",
|
|
34
|
+
"lib/invariant.js",
|
|
35
|
+
"lib/types/**/*.js",
|
|
36
|
+
"lib/types/**/*.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"license": "BSD-3-Clause",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"zod": "^4.4.3",
|
|
41
|
+
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
|
|
45
|
+
"@deepseek-ai/dsh-session-projection": "^0.0.1-rc.1",
|
|
46
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
|
|
47
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@deepseek-ai/cordis-plugin-include": "^1.0.5-rc.1",
|
|
53
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.1-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-agent-loop": "^0.0.1-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "^0.0.1-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-host-apiproxy": "^0.0.1-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-session-projection": "^0.0.1-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-user-interaction": "^0.0.1-rc.1",
|
|
65
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
66
|
+
}
|
|
67
|
+
}
|