Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions packages/core/src/core/__snapshots__/prompts.test.ts.snap

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions packages/core/src/core/prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ describe('Core System Prompt (prompts.ts)', () => {
expect(prompt).toContain(
'rather than one item per error, file, command, or minor edit',
);
expect(prompt).toContain(
'When an active Todo plan covers work delegated through top-level Agent calls',
);
expect(prompt).not.toContain(
'For complex work delegated through top-level Agent calls, create the relevant todo first',
);
expect(prompt).not.toContain('VERY frequently');
expect(prompt).not.toContain('EXTREMELY helpful');
expect(prompt).not.toContain('write 10 items to the todo list');
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ You have access to the ${ToolNames.TODO_WRITE} tool to keep user-visible progres

When you create a todo list:
- Keep it short and outcome-oriented. Use a few meaningful, logically ordered, verifiable steps rather than one item per error, file, command, or minor edit.
- For complex work delegated through top-level Agent calls, create the relevant todo first and pass its ID as \`todo_id\` so the execution can be associated with the plan node.
- When an active Todo plan covers work delegated through top-level Agent calls, pass the matching Todo ID as \`todo_id\` so the execution can be associated with that plan node. Do not create a Todo solely to wrap a delegation that does not otherwise need task tracking.
- Keep at most one item in_progress. Keep the list current, mark finished work completed, and revise it when the scope or approach changes. When work completes together, update multiple statuses in one tool call rather than making bookkeeping-only calls.
- Do not repeat the full todo list in prose after calling the tool; briefly communicate only important context or the next step.

Expand Down
25 changes: 23 additions & 2 deletions packages/core/src/tools/todoWrite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ describe('TodoWriteTool', () => {

const result = await tool
.build({
todos: [{ id: '1', content: 'Still done', status: 'completed' }],
todos: [{ id: '1', content: 'Done', status: 'completed' }],
})
.execute(mockAbortSignal);

Expand All @@ -348,7 +348,28 @@ describe('TodoWriteTool', () => {
mockAtomicWrite.mockResolvedValue(undefined);

const result = await tool
.build({ todos: [{ id: '2', content: 'New', status: 'pending' }] })
.build({ todos: [{ id: '1', content: 'New', status: 'pending' }] })
.execute(mockAbortSignal);
const display = result.returnDisplay as { planId?: string };

expect(display.planId).toEqual(expect.any(String));
expect(display.planId).not.toBe('finished-plan');
});

it('should start a new plan for a distinct all-completed snapshot', async () => {
mockFs.readFile.mockResolvedValue(
JSON.stringify({
planId: 'finished-plan',
todos: [{ id: '1', content: 'Done', status: 'completed' }],
}),
);
mockFs.mkdir.mockResolvedValue(undefined);
mockAtomicWrite.mockResolvedValue(undefined);

const result = await tool
.build({
todos: [{ id: '1', content: 'Already done', status: 'completed' }],
})
.execute(mockAbortSignal);
const display = result.returnDisplay as { planId?: string };

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/tools/todoWrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import type { FunctionDeclaration } from '@google/genai';
import { randomUUID } from 'node:crypto';
import { isDeepStrictEqual } from 'node:util';
import * as fs from 'fs/promises';
import * as fsSync from 'fs';
import * as path from 'path';
Expand Down Expand Up @@ -370,7 +371,7 @@ class TodoWriteToolInvocation extends BaseToolInvocation<
finalTodos.length > 0 &&
(oldTodos.length === 0 ||
(oldTodos.every((todo) => todo.status === 'completed') &&
finalTodos.some((todo) => todo.status !== 'completed')));
!isDeepStrictEqual(finalTodos, oldTodos)));
const activePlanId =
finalTodos.length === 0
? undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,20 @@ describe('PlanExecutionView', () => {
});
});

it('does not block a todo on an unknown dependency', () => {
const todo: TodoItem = {
id: 'standalone',
content: 'Standalone',
status: 'pending',
blockedBy: ['missing'],
};

expect(getPlanNodeState(todo, new Map([[todo.id, todo]]), [], [])).toEqual({
status: 'ready',
attention: false,
});
});

it('restores cancellation from replay output after the live task leaves', () => {
const cancelled = {
...agentTool('build'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ function getPlanNodeStateFromIndex(
return { status: 'paused', attention };
if (todo.status === 'completed') return { status: 'completed', attention };
const blocked = (todo.blockedBy ?? []).some(
(id) => todosById.get(id)?.status !== 'completed',
(id) => todosById.has(id) && todosById.get(id)?.status !== 'completed',
);
if (blocked) return { status: 'blocked', attention };
if (todo.status === 'in_progress')
Expand Down
Loading