{"version":3,"file":"todo-write.d.ts","sourceRoot":"","sources":["../../../src/core/tools/todo-write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,qCAAqC;AACrC,QAAA,MAAM,eAAe;;;;;;;;EAoBnB,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAE5D,wCAAwC;AACxC,MAAM,WAAW,QAAQ;IACxB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,WAAW,CAAC;CAChD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,CAqB/C;AAED,MAAM,WAAW,WAAW;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,IAAI,OAAO,CAAC;IACpB,wBAAwB,IAAI,OAAO,CAAC;IACpC,uBAAuB,IAAI,IAAI,CAAC;IAChC,kBAAkB,IAAI,IAAI,CAAC;CAC3B;AAED,qDAAqD;AACrD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAelD;AAqBD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAClC,eAAe,EAAE,MAAM,QAAQ,EAAE,EACjC,eAAe,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,EAC5C,SAAS,GAAE,aAAmC,EAC9C,WAAW,CAAC,EAAE,MAAM,MAAM,EAC1B,WAAW,CAAC,EAAE,WAAW,GACvB,SAAS,CAAC,OAAO,eAAe,CAAC,CAuLnC;AAED,8EAA8E;AAC9E,eAAO,MAAM,aAAa,EAAE,SAAS,CAAC,OAAO,eAAe,CAG3D,CAAC","sourcesContent":["import type { AgentTool } from \"@apholdings/jensen-agent-core\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport { generateTodoId } from \"../memory.js\";\nimport { TodoLoopGuard } from \"./todo-loop-guard.js\";\n\n/** Schema for the todo_write tool */\nconst todoWriteSchema = Type.Object({\n\ttodos: Type.Array(\n\t\tType.Object({\n\t\t\tid: Type.Optional(\n\t\t\t\tType.String({\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Stable identifier from a prior todo_read or write. Omit for new items; preserved on replacement.\",\n\t\t\t\t}),\n\t\t\t),\n\t\t\tcontent: Type.String({ description: \"Imperative task description (what needs to be done)\" }),\n\t\t\tactiveForm: Type.String({ description: \"Present continuous form shown during execution\" }),\n\t\t\tstatus: Type.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")], {\n\t\t\t\tdescription: \"Task status: pending, in_progress, or completed\",\n\t\t\t}),\n\t\t}),\n\t\t{ description: \"Full replacement list of all tasks\" },\n\t),\n\tconfirmClear: Type.Optional(\n\t\tType.Boolean({ description: \"Set to true explicitly when passing empty todos to clear the list\" }),\n\t),\n});\n\nexport type TodoWriteInput = Static<typeof todoWriteSchema>;\n\n/** Todo item stored in session state */\nexport interface TodoItem {\n\tid?: string;\n\tcontent: string;\n\tactiveForm: string;\n\tstatus: \"pending\" | \"in_progress\" | \"completed\";\n}\n\n/**\n * Per-user-turn lock marker.\n * Prevents duplicate todo_write within the same user turn.\n */\nexport function createPerTurnLock(): PerTurnLock {\n\treturn {\n\t\tcurrentTurn: 0,\n\t\tlockedUntil: -1,\n\t\treservedUntil: -1,\n\t\tisActive() {\n\t\t\treturn this.lockedUntil === this.currentTurn || this.reservedUntil === this.currentTurn;\n\t\t},\n\t\ttryReserveForCurrentTurn() {\n\t\t\tif (this.isActive()) return false;\n\t\t\tthis.reservedUntil = this.currentTurn;\n\t\t\treturn true;\n\t\t},\n\t\tsetLockedForCurrentTurn() {\n\t\t\tthis.lockedUntil = this.currentTurn;\n\t\t\tthis.reservedUntil = -1;\n\t\t},\n\t\treleaseReservation() {\n\t\t\tif (this.reservedUntil === this.currentTurn) this.reservedUntil = -1;\n\t\t},\n\t};\n}\n\nexport interface PerTurnLock {\n\tcurrentTurn: number;\n\tlockedUntil: number;\n\treservedUntil: number;\n\tisActive(): boolean;\n\ttryReserveForCurrentTurn(): boolean;\n\tsetLockedForCurrentTurn(): void;\n\treleaseReservation(): void;\n}\n\n/** Redact sensitive credentials from text string. */\nexport function redactSecrets(text: string): string {\n\tif (!text) return text;\n\tlet result = text;\n\tresult = result.replace(/EXAMPLE_SECRET_DO_NOT_LOG[^\\s]*/g, \"[REDACTED_SECRET]\");\n\tresult = result.replace(/(Bearer\\s+)[A-Za-z0-9\\-._~+/]+=*/gi, \"$1[REDACTED_SECRET]\");\n\tresult = result.replace(\n\t\t/(password|passwd|secret|api_key|apikey|access_token|auth_token)\\s*[:=]\\s*['\"]?([^'\"]\\S+)['\"]?/gi,\n\t\t\"$1=[REDACTED_SECRET]\",\n\t);\n\tresult = result.replace(/\\b(sk|ghp|gho|glpat|aws_secret|xoxb|xoxp)-[A-Za-z0-9_]{16,}\\b/g, \"[REDACTED_SECRET]\");\n\tresult = result.replace(\n\t\t/-----BEGIN [A-Z ]+ PRIVATE KEY-----[\\s\\S]*?-----END [A-Z ]+ PRIVATE KEY-----/g,\n\t\t\"[REDACTED_SECRET]\",\n\t);\n\treturn result;\n}\n\nfunction normalizeTodoItem(item: TodoItem): TodoItem {\n\treturn {\n\t\tid: item.id || generateTodoId(),\n\t\tcontent: redactSecrets(item.content.trim()),\n\t\tactiveForm: redactSecrets(item.activeForm.trim()),\n\t\tstatus: item.status,\n\t};\n}\n\nfunction areTodosEqual(a: TodoItem[], b: TodoItem[]): boolean {\n\tif (a.length !== b.length) return false;\n\tfor (let i = 0; i < a.length; i++) {\n\t\tif (a[i].content !== b[i].content || a[i].activeForm !== b[i].activeForm || a[i].status !== b[i].status) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n/**\n * Create the todo_write tool.\n * @param getSessionTodos - Callback to get the current todos from session\n * @param setSessionTodos - Callback to set todos in session and trigger update event\n * @param loopGuard - Guard instance to track consecutive calls\n * @param getRevision - Callback to get the current todo revision number\n * @param perTurnLock - Optional lock to prevent duplicate writes within a single user turn\n */\nexport function createTodoWriteTool(\n\tgetSessionTodos: () => TodoItem[],\n\tsetSessionTodos: (todos: TodoItem[]) => void,\n\tloopGuard: TodoLoopGuard = new TodoLoopGuard(),\n\tgetRevision?: () => number,\n\tperTurnLock?: PerTurnLock,\n): AgentTool<typeof todoWriteSchema> {\n\tconst execute = async (\n\t\t_toolCallId: string,\n\t\t{ todos, confirmClear }: TodoWriteInput,\n\t\t_signal?: AbortSignal,\n\t): Promise<{ content: Array<{ type: \"text\"; text: string }>; details: Record<string, unknown> }> => {\n\t\t// Per-user-turn lock: reserve the write before validation and mutation.\n\t\tif (perTurnLock && !perTurnLock.tryReserveForCurrentTurn()) {\n\t\t\tconst firstDuplicate = loopGuard.recordDuplicate();\n\t\t\tconst list = [...getSessionTodos()];\n\t\t\tconst pending = list.filter((t) => t.status === \"pending\").length;\n\t\t\tconst inProgress = list.filter((t) => t.status === \"in_progress\").length;\n\t\t\tconst completedCount = list.filter((t) => t.status === \"completed\").length;\n\n\t\t\tif (!firstDuplicate) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"TODO_TOOL_TEMPORARILY_DEGRADED: todo_write is blocked for this user turn after repeated equivalent writes. Execution continues; use todo_update or another authorized tool.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\tchanged: false,\n\t\t\t\t\t\ttodoWriteAlreadyApplied: true,\n\t\t\t\t\t\terrorCode: \"TODO_TOOL_TEMPORARILY_DEGRADED\",\n\t\t\t\t\t\trecoverable: true,\n\t\t\t\t\t\trunMustContinue: true,\n\t\t\t\t\t\tloopBlocked: true,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: `${list.length} todo(s) already exist (${pending} pending, ${inProgress} in progress, ${completedCount} completed). The plan already exists. Do not call todo_write again during this user turn.\\n\\nContinue using another available tool or execute the active task.`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: {\n\t\t\t\t\tchanged: false,\n\t\t\t\t\ttotal: list.length,\n\t\t\t\t\tpending,\n\t\t\t\t\tinProgress,\n\t\t\t\t\tcompleted: completedCount,\n\t\t\t\t\ttodoWriteAlreadyApplied: true,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\ttry {\n\t\t\t// Validate input\n\t\t\tif (!Array.isArray(todos)) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Error: todos must be an array\" }],\n\t\t\t\t\tdetails: {},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Validate each todo item\n\t\t\tconst normalized: TodoItem[] = [];\n\t\t\tfor (const todo of todos) {\n\t\t\t\tif (typeof todo.content !== \"string\" || !todo.content.trim()) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: \"Error: each todo must have a non-empty content field\" }],\n\t\t\t\t\t\tdetails: {},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (typeof todo.activeForm !== \"string\" || !todo.activeForm.trim()) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: \"Error: each todo must have a non-empty activeForm field\" }],\n\t\t\t\t\t\tdetails: {},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (![\"pending\", \"in_progress\", \"completed\"].includes(todo.status)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext: \"Error: each todo must have status of 'pending', 'in_progress', or 'completed'\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: {},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tnormalized.push(normalizeTodoItem(todo as TodoItem));\n\t\t\t}\n\n\t\t\t// Empty list requires explicit confirmation\n\t\t\tif (normalized.length === 0 && confirmClear !== true) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Error: Clearing all todos requires explicit confirmation (set confirmClear: true). To view current todos without modifying them, call todo_read.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: {},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst current = getSessionTodos().map(normalizeTodoItem);\n\t\t\tconst isNoOp = areTodosEqual(normalized, current);\n\n\t\t\tif (isNoOp) {\n\t\t\t\tperTurnLock?.releaseReservation();\n\t\t\t\tconst pending = current.filter((t) => t.status === \"pending\").length;\n\t\t\t\tconst inProgress = current.filter((t) => t.status === \"in_progress\").length;\n\t\t\t\tconst completedCount = current.filter((t) => t.status === \"completed\").length;\n\t\t\t\tconst total = current.length;\n\t\t\t\tconst lockNote =\n\t\t\t\t\t\"\\n\\nTodo list is locked for this user turn. Next permitted operations: todo_read, todo_update.\";\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Todo list unchanged (${total} total: ${pending} pending, ${inProgress} in progress, ${completedCount} completed). Continue executing the active task.${lockNote}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\tchanged: false,\n\t\t\t\t\t\ttotal,\n\t\t\t\t\t\tpending,\n\t\t\t\t\t\tinProgress,\n\t\t\t\t\t\tcompleted: completedCount,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Update session state\n\t\t\tsetSessionTodos(normalized);\n\n\t\t\tconst pending = normalized.filter((t) => t.status === \"pending\").length;\n\t\t\tconst inProgress = normalized.filter((t) => t.status === \"in_progress\").length;\n\t\t\tconst completedCount = normalized.filter((t) => t.status === \"completed\").length;\n\t\t\tconst total = normalized.length;\n\n\t\t\t// Apply per-user-turn lock (only on successful mutations)\n\t\t\tif (perTurnLock) perTurnLock.setLockedForCurrentTurn();\n\n\t\t\tconst revision = getRevision?.();\n\n\t\t\tconst activeTask = normalized.find((t) => t.status === \"in_progress\");\n\t\t\tconst activeText = activeTask ? ` Active task: ${activeTask.activeForm || activeTask.content}.` : \"\";\n\t\t\tconst summary =\n\t\t\t\t`Todo list updated (${total} total: ${pending} pending, ${inProgress} in progress, ${completedCount} completed). Revision ${revision ?? \"new\"}.` +\n\t\t\t\tactiveText +\n\t\t\t\t\"\\n\\ntodo_write is unavailable for the rest of this user turn. todo_read, todo_update, and other authorized tools remain available.\";\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: summary }],\n\t\t\t\tdetails: {\n\t\t\t\t\tchanged: true,\n\t\t\t\t\ttotal,\n\t\t\t\t\tpending,\n\t\t\t\t\tinProgress,\n\t\t\t\t\tcompleted: completedCount,\n\t\t\t\t\trevision,\n\t\t\t\t},\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tperTurnLock?.releaseReservation();\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tperTurnLock?.releaseReservation();\n\t\t}\n\t};\n\n\treturn {\n\t\tname: \"todo_write\",\n\t\tlabel: \"todo_write\",\n\t\tdescription:\n\t\t\t\"Update the session's structured task/todo list for multi-step workflows. \" +\n\t\t\t\"Call this for initial creation, deliberate complete replacement, or explicit confirmed clear. \" +\n\t\t\t\"Use todo_update for status or progress transitions. \" +\n\t\t\t\"Use todo_read when the current IDs or revision are not available. \" +\n\t\t\t\"Do not reconstruct or replace the entire list merely to complete one item. \" +\n\t\t\t\"To view the current todo list without modifying it, call todo_read.\",\n\t\tparameters: todoWriteSchema,\n\t\texecute,\n\t};\n}\n\n/** Default todo_write tool - requires session binding for state management */\nexport const todoWriteTool: AgentTool<typeof todoWriteSchema> = createTodoWriteTool(\n\t() => [],\n\t() => {},\n);\n"]}