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.
This commit is contained in:
can1357
2026-03-11 03:07:02 +01:00
parent 1932b35062
commit c8d4946d70
4 changed files with 26 additions and 20 deletions
+2 -1
View File
@@ -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
@@ -1,11 +1,6 @@
<system-reminder>
Before doing substantive work on the upcoming user request, create a comprehensive phased todo first.
The todo **MUST** cover this request:
<user-request>
{{userRequest}}
</user-request>
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.
</system-reminder>
@@ -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(),
},
@@ -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("<user-request>");
});
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",
});