From c8d4946d70c9ee2e621be6fb0de226785c996b3e Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 11 Mar 2026 03:07:02 +0100 Subject: [PATCH] feat(coding-agent): refactored eager todo messaging for cleaner categorization - Changed eager todo reminder message role from 'developer' to 'custom' with customType field for better message categorization. - Removed userRequest parameter from eager todo prelude generation to simplify prompt template rendering. - Updated eager todo prompt to avoid redundant todo_write calls unless task state materially changed. - Modified eager todo reminder message to use string content with display: false property instead of array format. --- packages/coding-agent/CHANGELOG.md | 3 ++- .../src/prompts/system/eager-todo.md | 6 +---- .../coding-agent/src/session/agent-session.ts | 14 +++++------ .../test/agent-session-eager-todo.test.ts | 23 +++++++++++++------ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 44b472c2c..b11926ffa 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,7 +1,6 @@ # Changelog ## [Unreleased] - ### Added - Added `assignment` field to task progress and result objects to track the raw per-task assignment text separately from the full templated task @@ -14,6 +13,8 @@ ### Changed +- Modified eager todo prelude to use hidden custom message type instead of visible developer message, preventing duplicate prompt text in session history +- Updated eager todo prompt to remove dynamic user request injection, simplifying the template and preventing request repetition in displayed messages - Modified eager todo enforcement to prepend the todo reminder to the first user turn instead of executing it as a separate synthetic turn, reducing unnecessary prompt calls - Updated task rendering to display assignment text instead of full task template when available, reducing noise in progress and result displays - Modified task section rendering to show trimmed assignment text without stripping context blocks, simplifying the display logic diff --git a/packages/coding-agent/src/prompts/system/eager-todo.md b/packages/coding-agent/src/prompts/system/eager-todo.md index 977369af4..350a30709 100644 --- a/packages/coding-agent/src/prompts/system/eager-todo.md +++ b/packages/coding-agent/src/prompts/system/eager-todo.md @@ -1,11 +1,6 @@ Before doing substantive work on the upcoming user request, create a comprehensive phased todo first. -The todo **MUST** cover this request: - -{{userRequest}} - - You **MUST** call `todo_write` first in this turn. You **MUST** initialize the todo list with a single `replace` op. You **MUST** cover the entire request from investigation through implementation and verification — not just the next immediate step. @@ -14,4 +9,5 @@ You **MUST** keep task `content` to a short label (5-10 words). Put file paths, You **MUST** keep exactly one task `in_progress` and all later tasks `pending`. After the initial `todo_write` call succeeds, continue with the user's request in the same turn. +Do not emit another `todo_write` call unless task state materially changed. \ No newline at end of file diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 2138c8bb5..c5d120285 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -1959,7 +1959,7 @@ export class AgentSession { return; } - const eagerTodoPrelude = !options?.synthetic ? this.#createEagerTodoPrelude(expandedText) : undefined; + const eagerTodoPrelude = !options?.synthetic ? this.#createEagerTodoPrelude() : undefined; const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }]; if (options?.images) { @@ -3515,7 +3515,7 @@ export class AgentSession { } } - #createEagerTodoPrelude(userRequest: string): { message: AgentMessage; toolChoice: ToolChoice } | undefined { + #createEagerTodoPrelude(): { message: AgentMessage; toolChoice: ToolChoice } | undefined { const eagerTodosEnabled = this.settings.get("todo.eager"); const todosEnabled = this.settings.get("todo.enabled"); if (!eagerTodosEnabled || !todosEnabled) { @@ -3545,14 +3545,14 @@ export class AgentSession { return undefined; } - const eagerTodoReminder = renderPromptTemplate(eagerTodoPrompt, { - userRequest, - }); + const eagerTodoReminder = renderPromptTemplate(eagerTodoPrompt); return { message: { - role: "developer", - content: [{ type: "text", text: eagerTodoReminder }], + role: "custom", + customType: "eager-todo-prelude", + content: eagerTodoReminder, + display: false, attribution: "agent", timestamp: Date.now(), }, diff --git a/packages/coding-agent/test/agent-session-eager-todo.test.ts b/packages/coding-agent/test/agent-session-eager-todo.test.ts index efe6b0389..e9d7341ad 100644 --- a/packages/coding-agent/test/agent-session-eager-todo.test.ts +++ b/packages/coding-agent/test/agent-session-eager-todo.test.ts @@ -20,6 +20,7 @@ type ObservedPromptCall = { toolChoice: string | undefined; toolNames: string[]; messageRoles: AgentMessage["role"][]; + messageTexts: string[]; lastMessageRole: AgentMessage["role"]; lastMessageText: string; }; @@ -164,6 +165,7 @@ describe("AgentSession eager todo enforcement", () => { toolChoice: getToolChoiceName(options?.toolChoice), toolNames: (context.tools ?? []).map(tool => tool.name), messageRoles: context.messages.map(message => message.role), + messageTexts: context.messages.map(message => getMessageText(message)), lastMessageRole: lastMessage.role, lastMessageText: getMessageText(lastMessage), }); @@ -200,21 +202,24 @@ describe("AgentSession eager todo enforcement", () => { tempDir.removeSync(); }); - it("prepends the eager todo reminder to the first real user turn", async () => { + it("prepends a hidden eager todo reminder without repeating the prompt text", async () => { await session.prompt("list all work trees"); - const dumpText = session.formatSessionAsText(); - expect(observedCalls).toHaveLength(1); expect(observedCalls[0]).toEqual({ toolChoice: "todo_write", toolNames: ["todo_write", "bash"], - messageRoles: ["developer", "user"], + messageRoles: ["user", "user"], + messageTexts: [ + expect.stringContaining("Before doing substantive work on the upcoming user request"), + "list all work trees", + ], lastMessageRole: "user", lastMessageText: "list all work trees", }); - expect(dumpText).toContain("## Developer"); - expect(dumpText).toContain("Before doing substantive work on the upcoming user request"); + expect(observedCalls[0]?.messageTexts.filter(text => text.includes("list all work trees"))).toHaveLength(1); + expect(observedCalls[0]?.messageTexts[0]).not.toContain("list all work trees"); + expect(session.formatSessionAsText()).not.toContain(""); }); it("initializes todos once, then continues within the same user turn", async () => { @@ -242,7 +247,11 @@ describe("AgentSession eager todo enforcement", () => { expect(observedCalls[0]).toEqual({ toolChoice: "todo_write", toolNames: ["todo_write", "bash"], - messageRoles: ["developer", "user"], + messageRoles: ["user", "user"], + messageTexts: [ + expect.stringContaining("Before doing substantive work on the upcoming user request"), + "list all work trees", + ], lastMessageRole: "user", lastMessageText: "list all work trees", });