@hydraharness/harness-tool-todo 0.1.1-rc.6

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @hydraharness/harness-tool-todo
2
+
3
+ The model-facing `todo_write` tool: the agent's whole task list, replaced wholesale on each call.
4
+
5
+ The todo projection follows the selected transcript version and refolds on version creation or selection.
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`, `completed`, `blocked`, `failed`, or `cancelled`. Completion requires a verified outcome. Blocked work lacks a prerequisite, failed work has an unsuccessful attempt, and cancelled work is abandoned; the content states the reason. Unfinished work must agree with result files and the final answer. The [evidence decision](../../../.agents/notes/implemented/bug-fix/2026-10-01-browser-evidence-and-result-validation.md) owns these distinctions.
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, blocked, failed, cancelled } }`; 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)). Non-completion outcomes have distinct text labels and never contribute to the completed count.
30
+
31
+ ## Session projection
32
+
33
+ When [`ctx.sessionProjections`](../../session/session-projection/README.md) is mounted, an injected child registers `todos` with `stateVersion:3` and an identity view. The initial value is `null`; each `todo/write` replaces the list and each `turn/start` clears it. `turn/end` keeps the checklist, and other events retain the same reference. This package merges the key into `SessionProjectionMap` through the Service Definition's `/types` export. The framework serves it on the history tail page and `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#hydraharness-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 `Updated todo list: <pending> pending, <inProgress> in progress, <completed> completed.` When unfinished outcomes exist, it appends `, <blocked> blocked, <failed> failed, <cancelled> cancelled` before the period. 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
+ - **Completion verification is model guidance** — the tool records statuses without proving arbitrary task outcomes. 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/lib/index.js ADDED
@@ -0,0 +1,213 @@
1
+ import z from "@hydraharness/schemastery";
2
+ import { z as z$1 } from "zod";
3
+ import { defineTool } from "@hydraharness/harness-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 @hydraharness/harness-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
+ "blocked",
19
+ "failed",
20
+ "cancelled"
21
+ ];
22
+ /** Schemastery configuration for the todo tool consumer. */
23
+ const Config = z.object({ allowParallelInProgress: z.boolean().required() });
24
+ const DESCRIPTION_HEAD = "Replace the current task list with the ENTIRE todos array on each call. Plan concrete steps before multi-step work; skip trivial tasks. ";
25
+ const DESCRIPTION_PARALLEL = "While work can proceed, mark active tasks `in_progress`; several only for concurrent work. ";
26
+ const DESCRIPTION_SINGLE = "While work can proceed, keep exactly one todo `in_progress`. ";
27
+ const DESCRIPTION_TAIL = "Mark `completed` only after verifying the outcome. Use `blocked` for missing prerequisites, `failed` for unsuccessful attempts, `cancelled` for abandoned work; put the reason in content. Keep statuses consistent with deliverables and the final answer.";
28
+ /**
29
+ * The model-facing description for one activation. The active-status clause is the only part that
30
+ * varies, because it is the only instruction the parallel policy changes.
31
+ * @param allowParallel - whether several todos may be `in_progress` at once.
32
+ * @returns the composed tool description.
33
+ */
34
+ function describe(allowParallel) {
35
+ return DESCRIPTION_HEAD + (allowParallel ? DESCRIPTION_PARALLEL : DESCRIPTION_SINGLE) + DESCRIPTION_TAIL;
36
+ }
37
+ /**
38
+ * Validate the value constraints the ParameterSchemaSpec can't express and build the canonical {@link
39
+ * TodoItem}[]: trimmed non-empty unique content, and at most one `in_progress` item unless the
40
+ * deployment allows parallel work. The registry has already enforced the status enum and rejected
41
+ * unknown item keys (`additionalProperties: false` — the logged snapshot must equal what the model
42
+ * believes it wrote, so a nested/extended item shape fails loud at the schema boundary instead of
43
+ * silently flattening); the cast below records that guarantee.
44
+ * @param raw - the model-supplied list, already schema-checked.
45
+ * @param allowParallel - whether several items may be `in_progress` at once.
46
+ * @returns the canonical list.
47
+ */
48
+ function toTodoList(raw, allowParallel) {
49
+ const todos = [];
50
+ const seen = /* @__PURE__ */ new Set();
51
+ let active = 0;
52
+ for (const item of raw) {
53
+ const content = item.content.trim();
54
+ if (content.length === 0) throw new Error("invalid todo: `content` must be a non-empty string");
55
+ if (seen.has(content)) throw new Error(`invalid todos: duplicate content ${JSON.stringify(content)}`);
56
+ seen.add(content);
57
+ if (item.status === "in_progress") active++;
58
+ todos.push({
59
+ content,
60
+ status: item.status
61
+ });
62
+ }
63
+ if (!allowParallel && active > 1) throw new Error(`invalid todos: at most one task may be in_progress (got ${active})`);
64
+ return todos;
65
+ }
66
+ /** Wire payload schema of the `todos` projection (whole list or pre-first-write null). */
67
+ const todosProjectionSchema = z$1.union([z$1.array(z$1.object({
68
+ content: z$1.string(),
69
+ status: z$1.enum(STATUSES)
70
+ })), z$1.null()]);
71
+ /**
72
+ * Register the `todo_write` tool on `ctx.tools` and, when the session-projection seam is composed,
73
+ * the `todos` unit.
74
+ * @param ctx - registrant context carrying the tool registry.
75
+ * @param config - deployment's explicit todo policy.
76
+ */
77
+ function apply(ctx, config) {
78
+ const allowParallel = config.allowParallelInProgress;
79
+ ctx.inject(["sessionProjections"], (projectionCtx) => {
80
+ projectionCtx.sessionProjections.register({
81
+ key: "todos",
82
+ history: "active-version",
83
+ stateSchema: 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
+ wire: {
91
+ viewSchema: todosProjectionSchema,
92
+ view: (state) => state
93
+ },
94
+ stateVersion: 4
95
+ });
96
+ });
97
+ ctx.tools.register(defineTool({
98
+ name: "todo_write",
99
+ description: describe(allowParallel),
100
+ parameters: { todos: {
101
+ type: "array",
102
+ required: true,
103
+ description: "The COMPLETE task list, replacing any previous list.",
104
+ items: {
105
+ type: "object",
106
+ additionalProperties: false,
107
+ properties: {
108
+ content: {
109
+ type: "string",
110
+ required: true,
111
+ description: "What the task is — a short imperative line."
112
+ },
113
+ status: {
114
+ type: "string",
115
+ required: true,
116
+ enum: [...STATUSES],
117
+ description: "pending (not started) | in_progress (now) | completed (verified done) | blocked (missing prerequisite) | failed (unsuccessful) | cancelled (abandoned)."
118
+ }
119
+ }
120
+ }
121
+ } },
122
+ output: {
123
+ schema: {
124
+ type: "object",
125
+ additionalProperties: false,
126
+ properties: {
127
+ todos: {
128
+ type: "array",
129
+ required: true,
130
+ items: {
131
+ type: "object",
132
+ additionalProperties: false,
133
+ properties: {
134
+ content: {
135
+ type: "string",
136
+ required: true
137
+ },
138
+ status: {
139
+ type: "string",
140
+ required: true,
141
+ enum: [...STATUSES]
142
+ }
143
+ }
144
+ }
145
+ },
146
+ counts: {
147
+ type: "object",
148
+ additionalProperties: false,
149
+ required: true,
150
+ properties: {
151
+ pending: {
152
+ type: "integer",
153
+ required: true
154
+ },
155
+ inProgress: {
156
+ type: "integer",
157
+ required: true
158
+ },
159
+ completed: {
160
+ type: "integer",
161
+ required: true
162
+ },
163
+ blocked: {
164
+ type: "integer",
165
+ required: true
166
+ },
167
+ failed: {
168
+ type: "integer",
169
+ required: true
170
+ },
171
+ cancelled: {
172
+ type: "integer",
173
+ required: true
174
+ }
175
+ }
176
+ }
177
+ }
178
+ },
179
+ render: (_args, value) => [{
180
+ type: "text",
181
+ text: `Updated todo list: ${value.counts.pending} pending, ${value.counts.inProgress} in progress, ${value.counts.completed} completed` + (value.counts.blocked + value.counts.failed + value.counts.cancelled > 0 ? `, ${value.counts.blocked} blocked, ${value.counts.failed} failed, ${value.counts.cancelled} cancelled.` : ".")
182
+ }]
183
+ },
184
+ execute(args, exec) {
185
+ const todos = toTodoList(args.todos, allowParallel);
186
+ if (!exec.agent) throw new Error("todo_write requires an owning agent session");
187
+ exec.agent.session.append("todo/write", { todos });
188
+ const count = (status) => todos.filter((t) => t.status === status).length;
189
+ return Promise.resolve({
190
+ todos: todos.map((todo) => ({
191
+ content: todo.content,
192
+ status: todo.status
193
+ })),
194
+ counts: {
195
+ pending: count("pending"),
196
+ inProgress: count("in_progress"),
197
+ completed: count("completed"),
198
+ blocked: count("blocked"),
199
+ failed: count("failed"),
200
+ cancelled: count("cancelled")
201
+ }
202
+ });
203
+ },
204
+ presentCall: (args) => ({
205
+ card: "generic",
206
+ title: "Update todo list",
207
+ kind: "other",
208
+ rawInput: args.todos
209
+ })
210
+ }));
211
+ }
212
+ //#endregion
213
+ export { Config, apply, inject, name };
@@ -0,0 +1,57 @@
1
+ //#region lib/types/invariant.js
2
+ /** Package-owned durable todo-snapshot invariants. @module @hydraharness/harness-tool-todo/invariant */
3
+ const PACKAGE_NAME = "@hydraharness/harness-tool-todo";
4
+ const TODO_STATUSES = new Set([
5
+ "pending",
6
+ "in_progress",
7
+ "completed",
8
+ "blocked",
9
+ "failed",
10
+ "cancelled"
11
+ ]);
12
+ /** Cordis companion plugin name. */
13
+ const name = "tool-todo-invariant";
14
+ /** Service required before the companion can reserve package ownership. */
15
+ const inject = ["invariants"];
16
+ /**
17
+ * Validate one whole-list todo snapshot before it reaches the durable log.
18
+ *
19
+ * Deliberately silent on how many items are `in_progress`. That is the tool's
20
+ * per-deployment policy (`Config.allowParallelInProgress`), not a durable-shape
21
+ * rule: a log written while parallel work was allowed must still replay after a
22
+ * deployment tightens the policy, so tying the invariant to the current config
23
+ * would reject history that was valid when it was written.
24
+ */
25
+ function validateTodos(value, fail) {
26
+ if (!Array.isArray(value)) fail("todo/write todos must be an array");
27
+ const seen = /* @__PURE__ */ new Set();
28
+ for (const item of value) {
29
+ if (typeof item !== "object" || item === null) fail("todo/write entries must be objects");
30
+ const { content, status } = item;
31
+ if (typeof content !== "string" || content.length === 0 || content.trim() !== content) fail("todo/write content must be non-empty and already trimmed");
32
+ if (seen.has(content)) fail(`todo/write repeats content ${JSON.stringify(content)}`);
33
+ seen.add(content);
34
+ if (typeof status !== "string" || !TODO_STATUSES.has(status)) fail(`todo/write carries unknown status ${JSON.stringify(status)}`);
35
+ }
36
+ }
37
+ /** Validate the package-owned event fields and ignore unrelated events. */
38
+ function validateEvent(event, fail) {
39
+ if (event.type === "todo/write") validateTodos(event.data.todos, fail);
40
+ }
41
+ /** Install validation for loaded and newly appended whole-list todo snapshots. */
42
+ const install = Object.assign((ctx, fail) => {
43
+ for (const session of ctx.sessions.list()) for (const event of session.events) validateEvent(event, fail);
44
+ ctx.on("internal/dispatch", (_mode, eventName, args) => {
45
+ if (eventName !== "session/event") return;
46
+ const event = args[1];
47
+ validateEvent(event, fail);
48
+ }, { global: true });
49
+ }, { inject: ["sessions"] });
50
+ /**
51
+ * Register the todo invariant companion.
52
+ * @param ctx - Cordis context carrying the invariant service.
53
+ * @returns the installed registration's disposer after setup succeeds.
54
+ */
55
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
56
+ //#endregion
57
+ 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 @hydraharness/harness-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 @hydraharness/harness-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 @hydraharness/harness-tool-todo
6
+ */
7
+ import type { Context } from '@hydraharness/cordis';
8
+ import z from '@hydraharness/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,193 @@
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 @hydraharness/harness-tool-todo
6
+ */
7
+ import z from '@hydraharness/schemastery';
8
+ import { z as zod } from 'zod';
9
+ import { defineTool } from '@hydraharness/harness-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', 'blocked', 'failed', 'cancelled'];
14
+ /** Schemastery configuration for the todo tool consumer. */
15
+ export const Config = z.object({
16
+ allowParallelInProgress: z.boolean().required(),
17
+ });
18
+ const DESCRIPTION_HEAD = 'Replace the current task list with the ENTIRE todos array on each call. '
19
+ + 'Plan concrete steps before multi-step work; skip trivial tasks. ';
20
+ const DESCRIPTION_PARALLEL = 'While work can proceed, mark active tasks `in_progress`; several only for concurrent work. ';
21
+ const DESCRIPTION_SINGLE = 'While work can proceed, keep exactly one todo `in_progress`. ';
22
+ const DESCRIPTION_TAIL = 'Mark `completed` only after verifying the outcome. Use `blocked` for missing prerequisites, '
23
+ + '`failed` for unsuccessful attempts, `cancelled` for abandoned work; put the reason in content. '
24
+ + 'Keep statuses consistent with deliverables and the final answer.';
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
33
+ + (allowParallel ? DESCRIPTION_PARALLEL : DESCRIPTION_SINGLE)
34
+ + DESCRIPTION_TAIL;
35
+ }
36
+ /**
37
+ * Validate the value constraints the ParameterSchemaSpec can't express and build the canonical {@link
38
+ * TodoItem}[]: trimmed non-empty unique content, and at most one `in_progress` item unless the
39
+ * deployment allows parallel work. The registry has already enforced the status enum and rejected
40
+ * unknown item keys (`additionalProperties: false` — the logged snapshot must equal what the model
41
+ * believes it wrote, so a nested/extended item shape fails loud at the schema boundary instead of
42
+ * silently flattening); the cast below records that guarantee.
43
+ * @param raw - the model-supplied list, already schema-checked.
44
+ * @param allowParallel - whether several items may be `in_progress` at once.
45
+ * @returns the canonical list.
46
+ */
47
+ function toTodoList(raw, allowParallel) {
48
+ const todos = [];
49
+ const seen = new Set();
50
+ let active = 0;
51
+ for (const item of raw) {
52
+ const content = item.content.trim();
53
+ if (content.length === 0) {
54
+ throw new Error('invalid todo: `content` must be a non-empty string');
55
+ }
56
+ if (seen.has(content)) {
57
+ throw new Error(`invalid todos: duplicate content ${JSON.stringify(content)}`);
58
+ }
59
+ seen.add(content);
60
+ if (item.status === 'in_progress')
61
+ active++;
62
+ todos.push({ content, status: item.status });
63
+ }
64
+ if (!allowParallel && active > 1) {
65
+ throw new Error(`invalid todos: at most one task may be in_progress (got ${active})`);
66
+ }
67
+ return todos;
68
+ }
69
+ /** Wire payload schema of the `todos` projection (whole list or pre-first-write null). */
70
+ const todosProjectionSchema = zod.union([
71
+ zod.array(zod.object({
72
+ content: zod.string(),
73
+ status: zod.enum(STATUSES),
74
+ })),
75
+ zod.null(),
76
+ ]);
77
+ /**
78
+ * Register the `todo_write` tool on `ctx.tools` and, when the session-projection seam is composed,
79
+ * the `todos` unit.
80
+ * @param ctx - registrant context carrying the tool registry.
81
+ * @param config - deployment's explicit todo policy.
82
+ */
83
+ export function apply(ctx, config) {
84
+ const allowParallel = config.allowParallelInProgress;
85
+ // The unit child activates only when a projection registry is composed
86
+ // (headless assemblies without the seam stay unaffected). Standing-plan fold:
87
+ // latest whole todo/write list, cleared by the next turn/start (turn/end keeps
88
+ // the finished checklist visible); null before the first write or after a
89
+ // later turn begins; every other event returns the same state reference.
90
+ ctx.inject(['sessionProjections'], (projectionCtx) => {
91
+ projectionCtx.sessionProjections.register({
92
+ key: 'todos',
93
+ history: 'active-version',
94
+ stateSchema: todosProjectionSchema,
95
+ init: () => null,
96
+ apply: (state, event) => {
97
+ if (event.type === 'todo/write')
98
+ return event.data.todos;
99
+ if (event.type === 'turn/start')
100
+ return null;
101
+ return state;
102
+ },
103
+ wire: { viewSchema: todosProjectionSchema, view: state => state },
104
+ stateVersion: 4,
105
+ });
106
+ });
107
+ ctx.tools.register(defineTool({
108
+ name: 'todo_write',
109
+ description: describe(allowParallel),
110
+ parameters: {
111
+ todos: {
112
+ type: 'array',
113
+ required: true,
114
+ description: 'The COMPLETE task list, replacing any previous list.',
115
+ items: {
116
+ type: 'object',
117
+ additionalProperties: false,
118
+ properties: {
119
+ content: { type: 'string', required: true, description: 'What the task is — a short imperative line.' },
120
+ status: {
121
+ type: 'string',
122
+ required: true,
123
+ enum: [...STATUSES],
124
+ description: 'pending (not started) | in_progress (now) | completed (verified done) | blocked (missing prerequisite) | failed (unsuccessful) | cancelled (abandoned).',
125
+ },
126
+ },
127
+ },
128
+ },
129
+ },
130
+ output: {
131
+ schema: {
132
+ type: 'object',
133
+ additionalProperties: false,
134
+ properties: {
135
+ todos: {
136
+ type: 'array',
137
+ required: true,
138
+ items: {
139
+ type: 'object',
140
+ additionalProperties: false,
141
+ properties: {
142
+ content: { type: 'string', required: true },
143
+ status: { type: 'string', required: true, enum: [...STATUSES] },
144
+ },
145
+ },
146
+ },
147
+ counts: {
148
+ type: 'object',
149
+ additionalProperties: false,
150
+ required: true,
151
+ properties: {
152
+ pending: { type: 'integer', required: true },
153
+ inProgress: { type: 'integer', required: true },
154
+ completed: { type: 'integer', required: true },
155
+ blocked: { type: 'integer', required: true },
156
+ failed: { type: 'integer', required: true },
157
+ cancelled: { type: 'integer', required: true },
158
+ },
159
+ },
160
+ },
161
+ },
162
+ render: (_args, value) => [{
163
+ type: 'text',
164
+ text: `Updated todo list: ${value.counts.pending} pending, ${value.counts.inProgress} in progress, ${value.counts.completed} completed`
165
+ + (value.counts.blocked + value.counts.failed + value.counts.cancelled > 0
166
+ ? `, ${value.counts.blocked} blocked, ${value.counts.failed} failed, ${value.counts.cancelled} cancelled.` : '.'),
167
+ }],
168
+ },
169
+ execute(args, exec) {
170
+ const todos = toTodoList(args.todos, allowParallel);
171
+ if (!exec.agent) {
172
+ // The list is per-agent-session state; a non-agent caller (no owning
173
+ // session) has nowhere to write it. Reject rather than silently no-op.
174
+ throw new Error('todo_write requires an owning agent session');
175
+ }
176
+ exec.agent.session.append('todo/write', { todos });
177
+ const count = (status) => todos.filter(t => t.status === status).length;
178
+ return Promise.resolve({
179
+ todos: todos.map(todo => ({ content: todo.content, status: todo.status })),
180
+ counts: {
181
+ pending: count('pending'),
182
+ inProgress: count('in_progress'),
183
+ completed: count('completed'),
184
+ blocked: count('blocked'),
185
+ failed: count('failed'),
186
+ cancelled: count('cancelled'),
187
+ },
188
+ });
189
+ },
190
+ presentCall: args => ({ card: 'generic', title: 'Update todo list', kind: 'other', rawInput: args.todos }),
191
+ }));
192
+ }
193
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ /** Package-owned durable todo-snapshot invariants. @module @hydraharness/harness-tool-todo/invariant */
2
+ import type { Context } from '@hydraharness/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 @hydraharness/harness-tool-todo/invariant */
2
+ const PACKAGE_NAME = '@hydraharness/harness-tool-todo';
3
+ const TODO_STATUSES = new Set(['pending', 'in_progress', 'completed', 'blocked', 'failed', 'cancelled']);
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,25 @@
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 (@hydraharness/harness-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 @hydraharness/harness-tool-todo/types
9
+ */
10
+ import type { TodoItem } from '@hydraharness/harness-session/types';
11
+ export type { TodoItem } from '@hydraharness/harness-session/types';
12
+ declare module '@hydraharness/harness-session-projection/types' {
13
+ interface SessionProjectionStateMap {
14
+ todos: TodoItem[] | null;
15
+ }
16
+ interface SessionProjectionMap {
17
+ /**
18
+ * The agent's current whole todo list (the latest `todo/write` snapshot),
19
+ * or `null` before the first write. Whole-value rule: every `todo/write`
20
+ * carries the complete replacement list, so the fold is last-wins.
21
+ */
22
+ todos: TodoItem[] | null;
23
+ }
24
+ }
25
+ //# 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 (@hydraharness/harness-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 @hydraharness/harness-tool-todo/types
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@hydraharness/harness-tool-todo",
3
+ "description": "Model-facing todo_write tool over the Hydra harness event-sourced session log",
4
+ "hydra": {
5
+ "plugin": {
6
+ "application": "Let the agent maintain a visible task list within the conversation."
7
+ }
8
+ },
9
+ "version": "0.1.1-rc.6",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/MaiHongPhong1902/Hydra-Harness.git",
16
+ "directory": "packages/todo/tool-todo"
17
+ },
18
+ "type": "module",
19
+ "main": "lib/index.js",
20
+ "types": "lib/types/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./lib/types/index.d.ts",
24
+ "default": "./lib/index.js"
25
+ },
26
+ "./invariant": {
27
+ "types": "./lib/types/invariant.d.ts",
28
+ "default": "./lib/invariant.js"
29
+ },
30
+ "./client": {
31
+ "types": "./lib/types/client.d.ts",
32
+ "default": "./lib/types/client.js"
33
+ },
34
+ "./src/*": "./src/*",
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "lib/index.js",
39
+ "lib/invariant.js",
40
+ "lib/types/**/*.js",
41
+ "lib/types/**/*.d.ts"
42
+ ],
43
+ "license": "MIT",
44
+ "dependencies": {
45
+ "zod": "^4.4.3",
46
+ "@hydraharness/schemastery": "^3.18.2"
47
+ },
48
+ "peerDependencies": {
49
+ "@hydraharness/harness-invariants": "^0.1.1-rc.6",
50
+ "@hydraharness/harness-session-projection": "^0.1.1-rc.6",
51
+ "@hydraharness/harness-session": "^0.1.1-rc.6",
52
+ "@hydraharness/cordis": "^4.0.2",
53
+ "@hydraharness/harness-tools": "^0.1.1-rc.6",
54
+ "@hydraharness/harness-agent": "^0.1.1-rc.6"
55
+ },
56
+ "devDependencies": {
57
+ "@hydraharness/cordis-plugin-include": "^1.0.7",
58
+ "@hydraharness/cordis-plugin-loader": "^1.0.3",
59
+ "@hydraharness/harness-agent": "^0.1.1-rc.6",
60
+ "@hydraharness/harness-agent-loop-testkit": "^0.1.1-rc.6",
61
+ "@hydraharness/harness-agent-loop": "^0.1.1-rc.6",
62
+ "@hydraharness/harness-host-apiproxy": "^0.1.1-rc.6",
63
+ "@hydraharness/harness-invariants": "^0.1.1-rc.6",
64
+ "@hydraharness/harness-llm": "^0.1.1-rc.6",
65
+ "@hydraharness/harness-session-projection": "^0.1.1-rc.6",
66
+ "@hydraharness/harness-system-prompt": "^0.1.1-rc.6",
67
+ "@hydraharness/harness-session": "^0.1.1-rc.6",
68
+ "@hydraharness/harness-tools": "^0.1.1-rc.6",
69
+ "@hydraharness/harness-user-questions": "^0.1.1-rc.6",
70
+ "@hydraharness/cordis": "^4.0.2"
71
+ }
72
+ }