{"version":3,"file":"messages-todo-write-context.test.d.ts","sourceRoot":"","sources":["../../src/core/messages-todo-write-context.test.ts"],"names":[],"mappings":"","sourcesContent":["import {\n\ttype AgentContext,\n\ttype AgentEvent,\n\ttype AgentLoopConfig,\n\tagentLoop,\n\ttype StreamFn,\n} from \"@apholdings/jensen-agent-core\";\nimport {\n\ttype AssistantMessage,\n\ttype AssistantMessageEvent,\n\ttype Context,\n\tEventStream,\n\ttype Model,\n\ttype ToolResultMessage,\n\ttype Usage,\n} from \"@apholdings/jensen-ai\";\nimport { describe, expect, it } from \"vitest\";\nimport { convertToLlm } from \"./messages.js\";\nimport { createTodoReadTool } from \"./tools/todo-read.js\";\nimport { createTodoWriteTool, type TodoItem } from \"./tools/todo-write.js\";\n\nconst usage: Usage = {\n\tinput: 0,\n\toutput: 0,\n\tcacheRead: 0,\n\tcacheWrite: 0,\n\ttotalTokens: 0,\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n};\n\nclass MockAssistantStream extends EventStream<AssistantMessageEvent, AssistantMessage> {\n\tconstructor() {\n\t\tsuper(\n\t\t\t(event) => event.type === \"done\" || event.type === \"error\",\n\t\t\t(event) => {\n\t\t\t\tif (event.type === \"done\") return event.message;\n\t\t\t\tif (event.type === \"error\") return event.error;\n\t\t\t\tthrow new Error(\"Unexpected event type\");\n\t\t\t},\n\t\t);\n\t}\n}\n\nconst model: Model<\"openai-responses\"> = {\n\tid: \"fixture\",\n\tname: \"fixture\",\n\tapi: \"openai-responses\",\n\tprovider: \"openai\",\n\tbaseUrl: \"https://example.invalid\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 200_000,\n\tmaxTokens: 1_000,\n};\n\nfunction makeTodos(count: number, contentLength: number): TodoItem[] {\n\treturn Array.from({ length: count }, (_, index) => ({\n\t\tcontent: `Task ${index} ${\"x\".repeat(contentLength)}`,\n\t\tactiveForm: `Doing ${index} ${\"y\".repeat(contentLength)}`,\n\t\tstatus: index === count - 1 ? \"in_progress\" : \"pending\",\n\t}));\n}\n\nfunction makePendingTodos(count: number, contentLength: number): TodoItem[] {\n\treturn makeTodos(count, contentLength).map((todo) => ({ ...todo, status: \"pending\" }));\n}\n\nfunction makeAssistant(todos: TodoItem[]): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: [\n\t\t\t{\n\t\t\t\ttype: \"toolCall\",\n\t\t\t\tid: \"todo-call\",\n\t\t\t\tname: \"todo_write\",\n\t\t\t\targuments: { todos },\n\t\t\t},\n\t\t],\n\t\tapi: \"anthropic-messages\",\n\t\tprovider: \"anthropic\",\n\t\tmodel: \"fixture\",\n\t\tusage,\n\t\tstopReason: \"toolUse\",\n\t\ttimestamp: 1,\n\t};\n}\n\nfunction byteLength(value: unknown): number {\n\treturn Buffer.byteLength(JSON.stringify(value), \"utf8\");\n}\n\nasync function executeAndCapture(\n\ttodos: TodoItem[],\n\tinitial: TodoItem[] = [],\n): Promise<{\n\tpersisted: TodoItem[];\n\tuiEvent: TodoItem[];\n\ttoolResult: ToolResultMessage;\n\tnextRequest: Context;\n}> {\n\tlet persisted: TodoItem[] = initial;\n\tlet uiEvent: TodoItem[] = [];\n\tconst tool = createTodoWriteTool(\n\t\t() => persisted,\n\t\t(next) => {\n\t\t\tpersisted = next;\n\t\t\tuiEvent = next;\n\t\t},\n\t);\n\tconst context: AgentContext = {\n\t\tsystemPrompt: \"fixed system prompt\",\n\t\tmessages: [],\n\t\ttools: [tool],\n\t};\n\tconst config: AgentLoopConfig = { model, convertToLlm };\n\tconst requests: Context[] = [];\n\tlet callIndex = 0;\n\tconst streamFn: StreamFn = (_model, request) => {\n\t\trequests.push(request);\n\t\tconst stream = new MockAssistantStream();\n\t\tconst currentCall = callIndex++;\n\t\tqueueMicrotask(() => {\n\t\t\tif (currentCall === 0) {\n\t\t\t\tstream.push({ type: \"done\", reason: \"toolUse\", message: makeAssistant(todos) });\n\t\t\t} else {\n\t\t\t\tstream.push({\n\t\t\t\t\ttype: \"done\",\n\t\t\t\t\treason: \"stop\",\n\t\t\t\t\tmessage: { ...makeAssistant([]), content: [{ type: \"text\", text: \"done\" }], stopReason: \"stop\" },\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t\treturn stream;\n\t};\n\tconst events: AgentEvent[] = [];\n\tconst stream = agentLoop(\n\t\t[{ role: \"user\", content: \"Update todos\", timestamp: 0 }],\n\t\tcontext,\n\t\tconfig,\n\t\tundefined,\n\t\tstreamFn,\n\t);\n\tfor await (const event of stream) {\n\t\tevents.push(event);\n\t}\n\tconst toolResultEvent = events.find((event) => event.type === \"message_end\" && event.message.role === \"toolResult\");\n\tif (toolResultEvent?.type !== \"message_end\" || toolResultEvent.message.role !== \"toolResult\") {\n\t\tthrow new Error(\"Expected todo_write tool result\");\n\t}\n\tconst nextRequest = requests[1];\n\tif (!nextRequest) {\n\t\tthrow new Error(\"Expected second provider request\");\n\t}\n\treturn {\n\t\tpersisted,\n\t\tuiEvent,\n\t\ttoolResult: toolResultEvent.message,\n\t\tnextRequest,\n\t};\n}\n\ndescribe(\"todo_write model context\", () => {\n\tit.each([\n\t\t[\"creates a todo\", makeTodos(1, 16), []],\n\t\t[\"updates an existing large todo list\", makeTodos(1_000, 256), makePendingTodos(1_000, 256)],\n\t])(\"%s while preserving full state for persistence and UI\", async (_label, todos, initial) => {\n\t\tconst execution = await executeAndCapture(todos, initial);\n\n\t\texpect(execution.persisted).toMatchObject(todos);\n\t\texpect(execution.uiEvent).toMatchObject(todos);\n\t\texpect(execution.toolResult.content).toEqual([\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: expect.stringMatching(\n\t\t\t\t\t/^Todo list updated \\(.*\\)\\. Revision .*\\. .*todo_write is unavailable for the rest of this user turn\\. todo_read, todo_update, and other authorized tools remain available\\.$/s,\n\t\t\t\t),\n\t\t\t},\n\t\t]);\n\t\texpect(initial).not.toEqual(todos);\n\t});\n\n\tit(\"keeps tool result and next request bounded as unrelated todos grow\", async () => {\n\t\tconst smallTodos = makeTodos(2, 16);\n\t\tconst largeTodos = makeTodos(1_000, 256);\n\t\tconst smallExecution = await executeAndCapture(smallTodos);\n\t\tconst largeExecution = await executeAndCapture(largeTodos);\n\t\tconst smallRequest = smallExecution.nextRequest;\n\t\tconst largeRequest = largeExecution.nextRequest;\n\t\tconst rawArgumentGrowth =\n\t\t\tbyteLength(makeAssistant(largeTodos).content[0]) - byteLength(makeAssistant(smallTodos).content[0]);\n\t\texpect(rawArgumentGrowth).toBeGreaterThan(500_000);\n\t\t// Success result includes the active task, so result size is bounded by the\n\t\t// active task text, not the full todo list.\n\t\texpect(byteLength(largeExecution.toolResult) - byteLength(smallExecution.toolResult)).toBeLessThan(4_096);\n\t\texpect(byteLength(largeRequest) - byteLength(smallRequest)).toBeLessThan(rawArgumentGrowth * 2);\n\n\t\t// todo_write call remains visible (no compaction)\n\t\tconst lastCompletedCall = largeRequest.messages.find((message) => message.role === \"assistant\");\n\t\tif (!lastCompletedCall || lastCompletedCall.role !== \"assistant\") {\n\t\t\tthrow new Error(\"Expected assistant message\");\n\t\t}\n\t\tconst hasToolCall = lastCompletedCall.content.some((b) => b.type === \"toolCall\");\n\t\texpect(hasToolCall).toBe(true);\n\t});\n\n\tit(\"keeps arguments for an unexecuted todo_write call\", () => {\n\t\tconst todos = makeTodos(2, 16);\n\t\tconst assistant = makeAssistant(todos);\n\n\t\texpect(convertToLlm([assistant])[0]).toEqual(assistant);\n\t});\n\n\tit(\"matches reused tool call IDs to the nearest preceding call\", () => {\n\t\tconst orphan = makeAssistant(makeTodos(2, 16));\n\t\tconst completed = makeAssistant(makeTodos(3, 16));\n\t\tconst result: ToolResultMessage = {\n\t\t\trole: \"toolResult\",\n\t\t\ttoolCallId: \"todo-call\",\n\t\t\ttoolName: \"todo_write\",\n\t\t\tcontent: [{ type: \"text\", text: \"done\" }],\n\t\t\tisError: false,\n\t\t\ttimestamp: 2,\n\t\t};\n\t\tconst converted = convertToLlm([orphan, completed, result]);\n\n\t\texpect(converted.length).toBe(3);\n\t\texpect(converted[0]).toEqual(orphan); // orphan stays intact (no matching result)\n\t\tconst lastAssistant = converted[1];\n\t\texpect(lastAssistant).toMatchObject({\n\t\t\trole: \"assistant\",\n\t\t});\n\t\t// Latest completed span preserved — both call and result visible\n\t\texpect((lastAssistant.content as Array<{ type: string }>).some((b) => b.type === \"toolCall\")).toBe(true);\n\t});\n\n\tdescribe(\"todo_read mode\", () => {\n\t\tit(\"returns persisted state without modifying it\", async () => {\n\t\t\tconst existing = [\n\t\t\t\t{ content: \"Task A\", activeForm: \"Doing A\", status: \"pending\" as const },\n\t\t\t\t{ content: \"Task B\", activeForm: \"Doing B\", status: \"in_progress\" as const },\n\t\t\t\t{ content: \"Task C\", activeForm: \"Doing C\", status: \"completed\" as const },\n\t\t\t];\n\t\t\tconst persisted = [...existing];\n\t\t\tconst readTool = createTodoReadTool(() => persisted);\n\t\t\tconst result = await readTool.execute(\"read-call\", {});\n\t\t\texpect(result.content[0]).toMatchObject({\n\t\t\t\ttype: \"text\",\n\t\t\t});\n\t\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\t\texpect(text).toContain(\"Current todo list (3 total\");\n\t\t\texpect(text).toContain(\"1 pending, 1 in progress, 1 completed\");\n\t\t\texpect(text).toContain(\"[ ] Task A\");\n\t\t\texpect(text).toContain(\"[>] Doing B\");\n\t\t\texpect(text).toContain(\"[x] Task C\");\n\t\t\texpect(persisted).toEqual(existing); // state unchanged\n\t\t\texpect(result.details).toMatchObject({ todos: existing });\n\t\t});\n\n\t\tit(\"returns empty message when no todos exist\", async () => {\n\t\t\tconst persisted: TodoItem[] = [];\n\t\t\tconst readTool = createTodoReadTool(() => persisted);\n\t\t\tconst result = await readTool.execute(\"read-call\", {});\n\t\t\texpect(result.content[0]).toMatchObject({ type: \"text\" });\n\t\t\texpect((result.content[0] as { text: string }).text).toContain(\"Todo list is empty\");\n\t\t\texpect(result.details).toMatchObject({ todos: [] });\n\t\t});\n\t});\n\n\tdescribe(\"write-read-write round-trip\", () => {\n\t\tit(\"preserves exact content across write, read, and write-update\", async () => {\n\t\t\tlet persisted: TodoItem[] = [];\n\t\t\tconst writeTool = createTodoWriteTool(\n\t\t\t\t() => persisted,\n\t\t\t\t(next) => {\n\t\t\t\t\tpersisted = next;\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst readTool = createTodoReadTool(() => persisted);\n\n\t\t\t// Step 1: Write initial list\n\t\t\tconst initialList: TodoItem[] = [\n\t\t\t\t{ content: \"Audit codebase\", activeForm: \"Auditing codebase\", status: \"pending\" },\n\t\t\t\t{ content: \"Implement fix\", activeForm: \"Implementing fix\", status: \"pending\" },\n\t\t\t\t{ content: \"Write tests\", activeForm: \"Writing tests\", status: \"pending\" },\n\t\t\t\t{ content: \"Review changes\", activeForm: \"Reviewing changes\", status: \"pending\" },\n\t\t\t\t{ content: \"Deploy\", activeForm: \"Deploying\", status: \"pending\" },\n\t\t\t];\n\t\t\tawait writeTool.execute(\"write-1\", { todos: initialList });\n\t\t\texpect(persisted).toMatchObject(initialList);\n\n\t\t\t// Step 2: Read back via todo_read\n\t\t\tconst readResult = await readTool.execute(\"read-1\", {});\n\t\t\texpect(readResult.details).toMatchObject({ todos: initialList });\n\n\t\t\t// Step 3: Update: mark first as in_progress\n\t\t\tconst updatedList: TodoItem[] = [\n\t\t\t\t{ content: \"Audit codebase\", activeForm: \"Auditing codebase\", status: \"in_progress\" },\n\t\t\t\t{ content: \"Implement fix\", activeForm: \"Implementing fix\", status: \"pending\" },\n\t\t\t\t{ content: \"Write tests\", activeForm: \"Writing tests\", status: \"pending\" },\n\t\t\t\t{ content: \"Review changes\", activeForm: \"Reviewing changes\", status: \"pending\" },\n\t\t\t\t{ content: \"Deploy\", activeForm: \"Deploying\", status: \"pending\" },\n\t\t\t];\n\t\t\tawait writeTool.execute(\"write-2\", { todos: updatedList });\n\t\t\texpect(persisted).toMatchObject(updatedList);\n\n\t\t\t// Step 4: Read again\n\t\t\tconst readResult2 = await readTool.execute(\"read-2\", {});\n\t\t\texpect(readResult2.details).toMatchObject({ todos: updatedList });\n\t\t});\n\n\t\tit(\"adding a task preserves all existing tasks\", async () => {\n\t\t\tlet persisted: TodoItem[] = [];\n\t\t\tconst writeTool = createTodoWriteTool(\n\t\t\t\t() => persisted,\n\t\t\t\t(next) => {\n\t\t\t\t\tpersisted = next;\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst readTool = createTodoReadTool(() => persisted);\n\n\t\t\tconst initial: TodoItem[] = [\n\t\t\t\t{ content: \"Task 1\", activeForm: \"Doing 1\", status: \"pending\" },\n\t\t\t\t{ content: \"Task 2\", activeForm: \"Doing 2\", status: \"pending\" },\n\t\t\t];\n\t\t\tawait writeTool.execute(\"write-1\", { todos: initial });\n\n\t\t\tconst readResult = await readTool.execute(\"read-1\", {});\n\t\t\texpect(readResult.details).toMatchObject({ todos: initial });\n\n\t\t\tconst withNew: TodoItem[] = [...initial, { content: \"Task 3\", activeForm: \"Doing 3\", status: \"pending\" }];\n\t\t\tawait writeTool.execute(\"write-2\", { todos: withNew });\n\t\t\texpect(persisted).toMatchObject(withNew);\n\t\t\texpect(persisted.length).toBe(3);\n\t\t});\n\n\t\tit(\"idempotent writes do not cause duplication\", async () => {\n\t\t\tlet persisted: TodoItem[] = [];\n\t\t\tconst writeTool = createTodoWriteTool(\n\t\t\t\t() => persisted,\n\t\t\t\t(next) => {\n\t\t\t\t\tpersisted = next;\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tconst list: TodoItem[] = [{ content: \"Task A\", activeForm: \"Doing A\", status: \"pending\" }];\n\t\t\tconst res1 = await writeTool.execute(\"write-1\", { todos: list });\n\t\t\texpect((res1.details as { changed?: boolean }).changed).toBe(true);\n\n\t\t\tconst res2 = await writeTool.execute(\"write-2\", { todos: list });\n\t\t\texpect((res2.details as { changed?: boolean }).changed).toBe(false);\n\n\t\t\texpect(persisted).toMatchObject(list);\n\t\t\texpect(persisted.length).toBe(1);\n\t\t});\n\n\t\tit(\"clearing the list requires confirmClear: true\", async () => {\n\t\t\tlet persisted: TodoItem[] = [];\n\t\t\tconst writeTool = createTodoWriteTool(\n\t\t\t\t() => persisted,\n\t\t\t\t(next) => {\n\t\t\t\t\tpersisted = next;\n\t\t\t\t},\n\t\t\t);\n\t\t\tconst readTool = createTodoReadTool(() => persisted);\n\n\t\t\tconst list: TodoItem[] = [{ content: \"Task A\", activeForm: \"Doing A\", status: \"pending\" }];\n\t\t\tawait writeTool.execute(\"write-1\", { todos: list });\n\t\t\texpect(persisted).toMatchObject(list);\n\n\t\t\t// Unconfirmed clear returns error\n\t\t\tconst errRes = await writeTool.execute(\"write-2\", { todos: [] });\n\t\t\texpect((errRes.content[0] as { text: string }).text).toContain(\n\t\t\t\t\"Clearing all todos requires explicit confirmation\",\n\t\t\t);\n\t\t\texpect(persisted).toMatchObject(list);\n\n\t\t\t// Confirmed clear succeeds\n\t\t\tawait writeTool.execute(\"write-3\", { todos: [], confirmClear: true });\n\t\t\texpect(persisted).toEqual([]);\n\n\t\t\tconst readResult = await readTool.execute(\"read-1\", {});\n\t\t\texpect(readResult.content[0]).toMatchObject({ type: \"text\" });\n\t\t\texpect((readResult.content[0] as { text: string }).text).toContain(\"Todo list is empty\");\n\t\t});\n\t});\n});\n"]}