Skip to content

Commit eccad0c

Browse files
committed
perf: runtime task reminders
1 parent 5020b48 commit eccad0c

File tree

16 files changed

+1784
-1593
lines changed

16 files changed

+1784
-1593
lines changed

internal/agent/runtime_policy.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ func (p *sessionRuntimePolicy) handleEvent(ev agentcore.Event) {
6363
p.trackToolStart(ev)
6464
case agentcore.EventToolExecEnd:
6565
p.trackToolEnd(ev)
66+
case agentcore.EventMessageEnd:
67+
if msg, ok := ev.Message.(agentcore.Message); ok {
68+
p.handleMessageEnd(msg)
69+
}
6670
}
6771
}
6872

@@ -161,6 +165,25 @@ func (p *sessionRuntimePolicy) detectTaskManagementGap() {
161165
}
162166
}
163167

168+
func (p *sessionRuntimePolicy) handleMessageEnd(msg agentcore.Message) {
169+
if msg.Role != agentcore.RoleAssistant {
170+
return
171+
}
172+
173+
s := p.session
174+
if s.taskStore == nil {
175+
return
176+
}
177+
snap := s.taskStore.Snapshot()
178+
if key, reminder, ok := taskManagementReminderBeforeStop(msg, snap); ok {
179+
s.deliverRuntimeReminder(
180+
key,
181+
ReminderTaskManagement,
182+
reminder,
183+
)
184+
}
185+
}
186+
164187
func hashToolArgs(raw json.RawMessage) string {
165188
if len(raw) == 0 {
166189
return ""

internal/agent/session_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,78 @@ func (m *scriptedReminderModel) GenerateStream(
137137

138138
func (m *scriptedReminderModel) SupportsTools() bool { return true }
139139

140+
type taskCompletionReminderModel struct {
141+
mu sync.Mutex
142+
callCount int
143+
sawReminder bool
144+
taskID string
145+
}
146+
147+
func (m *taskCompletionReminderModel) Generate(
148+
_ context.Context,
149+
msgs []agentcore.Message,
150+
_ []agentcore.ToolSpec,
151+
_ ...agentcore.CallOption,
152+
) (*agentcore.LLMResponse, error) {
153+
m.mu.Lock()
154+
defer m.mu.Unlock()
155+
156+
sawInjectedReminder := false
157+
for _, msg := range msgs {
158+
if msg.Role == agentcore.RoleUser && strings.Contains(msg.TextContent(), "still in_progress") {
159+
sawInjectedReminder = true
160+
m.sawReminder = true
161+
}
162+
}
163+
164+
m.callCount++
165+
switch m.callCount {
166+
case 1:
167+
return &agentcore.LLMResponse{Message: assistantTextMessage("总结完毕")}, nil
168+
case 2:
169+
if !sawInjectedReminder {
170+
return &agentcore.LLMResponse{Message: assistantTextMessage("no reminder")}, nil
171+
}
172+
return &agentcore.LLMResponse{
173+
Message: toolCallMessage(agentcore.ToolCall{
174+
ID: "task-update-1",
175+
Name: "task_update",
176+
Args: json.RawMessage(fmt.Sprintf(`{"taskId":%q,"status":"completed"}`, m.taskID)),
177+
}),
178+
}, nil
179+
default:
180+
return &agentcore.LLMResponse{Message: assistantTextMessage("已补上任务完成状态")}, nil
181+
}
182+
}
183+
184+
func (m *taskCompletionReminderModel) GenerateStream(
185+
ctx context.Context,
186+
msgs []agentcore.Message,
187+
tools []agentcore.ToolSpec,
188+
opts ...agentcore.CallOption,
189+
) (<-chan agentcore.StreamEvent, error) {
190+
resp, err := m.Generate(ctx, msgs, tools, opts...)
191+
if err != nil {
192+
return nil, err
193+
}
194+
ch := make(chan agentcore.StreamEvent, 1)
195+
ch <- agentcore.StreamEvent{
196+
Type: agentcore.StreamEventDone,
197+
Message: resp.Message,
198+
StopReason: resp.Message.StopReason,
199+
}
200+
close(ch)
201+
return ch, nil
202+
}
203+
204+
func (m *taskCompletionReminderModel) SupportsTools() bool { return true }
205+
206+
func (m *taskCompletionReminderModel) SawReminder() bool {
207+
m.mu.Lock()
208+
defer m.mu.Unlock()
209+
return m.sawReminder
210+
}
211+
140212
type namedChatModel struct {
141213
name string
142214
}
@@ -856,6 +928,42 @@ func TestTaskManagementReminderQueuedForUntrackedOrBroadWork(t *testing.T) {
856928
}
857929
}
858930

931+
func TestTaskManagementReminderSteersBeforeStopWithOpenInProgressTask(t *testing.T) {
932+
t.Parallel()
933+
934+
store := localtools.NewTaskStore()
935+
task := store.Create("Summarize project state", "Write the final analysis summary", "Summarizing project state", nil)
936+
inProgress := localtools.TaskInProgress
937+
if _, err := store.Update(task.ID, localtools.TaskUpdateOpts{Status: &inProgress}); err != nil {
938+
t.Fatalf("set task in_progress: %v", err)
939+
}
940+
941+
model := &taskCompletionReminderModel{taskID: task.ID}
942+
taskTools := localtools.NewTaskTools(store, nil, nil)
943+
ag := agentcore.NewAgent(
944+
agentcore.WithModel(model),
945+
agentcore.WithTools(taskTools...),
946+
agentcore.WithMaxTurns(10),
947+
)
948+
s := NewSession(SessionConfig{
949+
Agent: ag,
950+
Settings: config.Resolved{MaxTurns: 10},
951+
Cwd: t.TempDir(),
952+
TaskStore: store,
953+
Tools: taskTools,
954+
})
955+
t.Cleanup(s.Close)
956+
957+
if err := s.Prompt("分析一下项目"); err != nil {
958+
t.Fatalf("prompt: %v", err)
959+
}
960+
961+
waitFor(t, time.Second, func() bool {
962+
snap := store.Snapshot()
963+
return model.SawReminder() && snap.Completed == 1 && snap.InProgress == 0 && s.LastAssistantText() == "已补上任务完成状态"
964+
})
965+
}
966+
859967
func TestRuntimeMetricsTrackCompactionSavings(t *testing.T) {
860968
t.Parallel()
861969

internal/agent/task_management_policy.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package agent
22

33
import (
4+
"fmt"
45
"strings"
56

67
"github.com/voocel/agentcore"
78
localtools "github.com/voocel/codebot/internal/tools"
89
)
910

1011
const (
11-
taskManagementPromptReminder = "<system-reminder>\nThis is a multi-step implementation task. Create and maintain a task list before going deeper. Break the work into concrete tasks, mark a task in_progress before starting it, mark it completed immediately after finishing it, and keep moving to the next unblocked task.\n</system-reminder>"
12+
taskManagementPromptReminder = "<system-reminder>\nThis is a multi-step implementation task. Create and maintain a task list before going deeper. Break the work into concrete tasks, mark a task in_progress before starting it, mark it completed immediately after finishing it, and keep moving to the next unblocked task. Always mark the current task completed before giving your final answer unless the work is blocked, partial, or still failing verification.\n</system-reminder>"
1213
taskManagementMissingReminder = "<system-reminder>\nYou are doing multi-step work without maintaining a task list. Create concrete tasks now instead of continuing without structure.\n</system-reminder>"
1314
taskManagementExpandSingleReminder = "<system-reminder>\nYour task list is too broad for the current scope. Split the single broad task into multiple more specific tasks and keep their statuses up to date.\n</system-reminder>"
1415
)
@@ -40,6 +41,31 @@ func taskManagementReminderForTurn(turn TurnOutcomeSnapshot, snap localtools.Tas
4041
}
4142
}
4243

44+
func taskManagementReminderBeforeStop(msg agentcore.Message, snap localtools.TaskSnapshot) (key, reminder string, ok bool) {
45+
if msg.Role != agentcore.RoleAssistant || msg.StopReason != agentcore.StopReasonStop || snap.InProgress == 0 {
46+
return "", "", false
47+
}
48+
49+
inProgress := inProgressTasks(snap)
50+
if len(inProgress) == 0 {
51+
return "", "", false
52+
}
53+
54+
taskRefs := make([]string, 0, len(inProgress))
55+
taskIDs := make([]string, 0, len(inProgress))
56+
for _, task := range inProgress {
57+
taskIDs = append(taskIDs, task.ID)
58+
taskRefs = append(taskRefs, fmt.Sprintf("#%s %s", task.ID, task.Subject))
59+
}
60+
61+
return "task_management:before_stop_open:" + strings.Join(taskIDs, ","),
62+
fmt.Sprintf(
63+
"<system-reminder>\nYou are about to stop with task(s) still in_progress: %s. If this work is actually finished, call task_update to mark it completed before your final answer. If it is blocked, partial, or still failing verification, keep the task in_progress and explicitly say that instead of ending as if the work were done.\n</system-reminder>",
64+
strings.Join(taskRefs, ", "),
65+
),
66+
true
67+
}
68+
4369
func textContentFromBlocks(blocks []agentcore.ContentBlock) string {
4470
var parts []string
4571
for _, block := range blocks {
@@ -83,3 +109,17 @@ func looksLikeComplexTaskRequest(text string) bool {
83109
func allTasksCompleted(snap localtools.TaskSnapshot) bool {
84110
return snap.Total > 0 && snap.Pending == 0 && snap.InProgress == 0
85111
}
112+
113+
func inProgressTasks(snap localtools.TaskSnapshot) []localtools.Task {
114+
if len(snap.Items) == 0 || snap.InProgress == 0 {
115+
return nil
116+
}
117+
118+
out := make([]localtools.Task, 0, snap.InProgress)
119+
for _, task := range snap.Items {
120+
if task.Status == localtools.TaskInProgress {
121+
out = append(out, task)
122+
}
123+
}
124+
return out
125+
}

internal/config/prompt.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Break down and manage your work with task_create, task_update, and task_list.
6868
- Break larger requests into multiple specific tasks instead of one broad task
6969
- Mark a task in_progress before starting it
7070
- Mark a task completed immediately after fully finishing it
71+
- IMPORTANT: always mark the current task completed before giving your final answer, unless the work is blocked, partial, or still failing verification
7172
- Do not batch multiple completions together
7273
- Keep at most one task in_progress at a time
7374
- Check task_list before creating more tasks if a relevant task may already exist

internal/plan/actions_test.go

Lines changed: 0 additions & 61 deletions
This file was deleted.

0 commit comments

Comments
 (0)