Skip to content

Commit c9a6fd7

Browse files
committed
perf: move task store to storage
1 parent 808092a commit c9a6fd7

15 files changed

Lines changed: 692 additions & 668 deletions

File tree

internal/agent/session.go

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/voocel/codebot/internal/provider"
1111
"github.com/voocel/codebot/internal/skill"
1212
"github.com/voocel/codebot/internal/storage"
13-
localtools "github.com/voocel/codebot/internal/tools"
1413
)
1514

1615
// ModelFactory creates a chat model instance for a provider/model tuple.
@@ -25,7 +24,7 @@ type SessionConfig struct {
2524
Registry *provider.ModelRegistry
2625
Settings config.Resolved
2726
Cwd string
28-
TaskStore *localtools.TaskStore
27+
TaskStore *storage.TaskStore
2928
// CreateModel allows tests/integrations to override model construction.
3029
// Defaults to provider.CreateModel when nil.
3130
CreateModel ModelFactory
@@ -82,10 +81,10 @@ type Session struct {
8281
skills []skill.Spec
8382
skillCatalog *skill.Catalog
8483
skillUsage *skill.UsageTracker
85-
overlays promptOverlays
84+
overlays overlayStore
8685
beforePrompt func()
8786
hookRunner *hooks.Runner
88-
taskStore *localtools.TaskStore
87+
taskStore *storage.TaskStore
8988
skillAllowsSetter func([]string)
9089
skillRuntime skillRuntimeState
9190

@@ -145,10 +144,39 @@ type invokedSkillSnapshot struct {
145144
Timestamp time.Time
146145
}
147146

148-
type promptOverlays struct {
149-
MCP string
150-
PlanMode string
151-
ApprovedPlan string
147+
type overlayStore struct {
148+
order []string
149+
byKey map[string]string
150+
}
151+
152+
func (o *overlayStore) set(key, text string) {
153+
if o.byKey == nil {
154+
o.byKey = make(map[string]string)
155+
}
156+
if text == "" {
157+
delete(o.byKey, key)
158+
for i, k := range o.order {
159+
if k == key {
160+
o.order = append(o.order[:i], o.order[i+1:]...)
161+
break
162+
}
163+
}
164+
return
165+
}
166+
if _, exists := o.byKey[key]; !exists {
167+
o.order = append(o.order, key)
168+
}
169+
o.byKey[key] = text
170+
}
171+
172+
func (o *overlayStore) texts() []string {
173+
out := make([]string, 0, len(o.order))
174+
for _, k := range o.order {
175+
if v := o.byKey[k]; v != "" {
176+
out = append(out, v)
177+
}
178+
}
179+
return out
152180
}
153181

154182
// NewSession creates a Session and wires auto-persist to the agent.
@@ -201,16 +229,16 @@ func NewSession(cfg SessionConfig) *Session {
201229
return s
202230
}
203231

204-
func (s *Session) SetTaskNotifyFn(fn localtools.TaskNotifyFn) {
232+
func (s *Session) SetTaskNotifyFn(fn storage.TaskNotifyFn) {
205233
if s.taskStore == nil {
206234
return
207235
}
208236
s.taskStore.SetNotifyFn(fn)
209237
}
210238

211-
func (s *Session) TaskSnapshot() localtools.TaskSnapshot {
239+
func (s *Session) TaskSnapshot() storage.TaskSnapshot {
212240
if s.taskStore == nil {
213-
return localtools.TaskSnapshot{}
241+
return storage.TaskSnapshot{}
214242
}
215243
return s.taskStore.Snapshot()
216244
}

internal/agent/session_prompt.go

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,14 @@ func (s *Session) ReplaceMCPTools(tools []agentcore.Tool) {
3737
s.prompts.replaceMCPTools(tools)
3838
}
3939

40-
func (s *Session) SetSystemSuffix(suffix string) {
41-
s.prompts.setApprovedPlanPrompt(suffix)
42-
}
43-
4440
func (s *Session) SetMCPInstructions(text string) {
45-
s.prompts.setMCPInstructions(text)
46-
}
47-
48-
func (s *Session) SetPlanModePrompt(text string) {
49-
s.prompts.setPlanModePrompt(text)
50-
}
51-
52-
func (s *Session) ClearPlanModePrompt() {
53-
s.prompts.setPlanModePrompt("")
54-
}
55-
56-
func (s *Session) SetApprovedPlanPrompt(text string) {
57-
s.prompts.setApprovedPlanPrompt(text)
41+
s.prompts.overlayPrompt("mcp", text)
5842
}
5943

60-
func (s *Session) ClearApprovedPlanPrompt() {
61-
s.prompts.setApprovedPlanPrompt("")
44+
// OverlayPrompt registers or removes a named instructions overlay.
45+
// Pass empty text to remove. Overlays are rendered in insertion order.
46+
func (s *Session) OverlayPrompt(key, text string) {
47+
s.prompts.overlayPrompt(key, text)
6248
}
6349

6450
func (s *Session) Skills() []skill.Spec {
@@ -152,18 +138,8 @@ func (m *sessionPromptManager) replaceMCPTools(tools []agentcore.Tool) {
152138
m.rebuildPrompt()
153139
}
154140

155-
func (m *sessionPromptManager) setMCPInstructions(text string) {
156-
m.session.overlays.MCP = text
157-
m.rebuildPrompt()
158-
}
159-
160-
func (m *sessionPromptManager) setPlanModePrompt(text string) {
161-
m.session.overlays.PlanMode = text
162-
m.rebuildPrompt()
163-
}
164-
165-
func (m *sessionPromptManager) setApprovedPlanPrompt(text string) {
166-
m.session.overlays.ApprovedPlan = text
141+
func (m *sessionPromptManager) overlayPrompt(key, text string) {
142+
m.session.overlays.set(key, text)
167143
m.rebuildPrompt()
168144
}
169145

@@ -208,14 +184,8 @@ func (m *sessionPromptManager) rebuildPrompt() {
208184

209185
// Build two-block system prompt (identity + instructions) for cache stability.
210186
identity, instructions := config.BuildSystemBlockTexts(m.session.cwd, m.session.contextFiles, visibleInfos)
211-
for _, overlay := range []string{
212-
m.session.overlays.MCP,
213-
m.session.overlays.PlanMode,
214-
m.session.overlays.ApprovedPlan,
215-
} {
216-
if strings.TrimSpace(overlay) != "" {
217-
instructions += "\n\n" + overlay
218-
}
187+
for _, overlay := range m.session.overlays.texts() {
188+
instructions += "\n\n" + overlay
219189
}
220190
blocks := []agentcore.SystemBlock{
221191
{Text: identity, CacheControl: "ephemeral"},

internal/agent/session_runtime.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,19 @@ func (s *Session) providerType(prov string) (string, error) {
499499
return config.ResolveProviderType(prov, "")
500500
}
501501

502-
func (s *Session) ResolveAndSetModel(pattern string) (string, error) {
502+
func (s *Session) ResolveAndSetModel(pattern string) (resolved string, err error) {
503+
defer func() {
504+
if err == nil {
505+
prov, model := s.Provider(), s.ModelName()
506+
if e := config.PatchGlobalSettings(config.Settings{
507+
Provider: &prov,
508+
Model: &model,
509+
}); e != nil {
510+
fmt.Fprintf(os.Stderr, "warning: persist model setting: %v\n", e)
511+
}
512+
}
513+
}()
514+
503515
// Extract :thinking suffix (e.g. "model:high").
504516
thinkingLevel := agentcore.ThinkingLevel("")
505517
if idx := strings.LastIndex(pattern, ":"); idx > 0 {

internal/agent/session_test.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ func TestSetModelDoesNotRewriteGlobalSettings(t *testing.T) {
524524
})
525525
t.Cleanup(s.Close)
526526

527+
// SetModel (internal) should NOT persist to global settings.
527528
if err := s.SetModel("anthropic", "claude-sonnet-4-5"); err != nil {
528529
t.Fatalf("set model: %v", err)
529530
}
@@ -538,9 +539,6 @@ func TestSetModelDoesNotRewriteGlobalSettings(t *testing.T) {
538539
if got.Model != "gpt-5.4" {
539540
t.Fatalf("model rewritten: got %q want %q", got.Model, "gpt-5.4")
540541
}
541-
if got.SmallModel != "gpt-5.4" {
542-
t.Fatalf("small model rewritten: got %q want %q", got.SmallModel, "gpt-5.4")
543-
}
544542
if s.Provider() != "anthropic" {
545543
t.Fatalf("session provider not switched: got %q want %q", s.Provider(), "anthropic")
546544
}
@@ -787,8 +785,8 @@ func TestPromptOverlaysAppendInFixedOrder(t *testing.T) {
787785
t.Cleanup(s.Close)
788786

789787
s.SetMCPInstructions("mcp overlay")
790-
s.SetPlanModePrompt("plan overlay")
791-
s.SetApprovedPlanPrompt("approved overlay")
788+
s.OverlayPrompt("plan.mode", "plan overlay")
789+
s.OverlayPrompt("plan.approved", "approved overlay")
792790

793791
systemPrompt := ag.State().SystemPrompt
794792
mcpIdx := strings.Index(systemPrompt, "mcp overlay")
@@ -987,7 +985,7 @@ func TestPromptDoesNotQueueTaskManagementReminderFromUserText(t *testing.T) {
987985
Agent: ag,
988986
Settings: config.Resolved{MaxTurns: 30},
989987
Cwd: t.TempDir(),
990-
TaskStore: localtools.NewTaskStore(),
988+
TaskStore: storage.NewTaskStore(),
991989
})
992990
t.Cleanup(s.Close)
993991

@@ -1008,10 +1006,10 @@ func TestPromptDoesNotQueueTaskManagementReminderFromUserText(t *testing.T) {
10081006
func TestTaskManagementReminderSteersBeforeStopWithOpenInProgressTask(t *testing.T) {
10091007
t.Parallel()
10101008

1011-
store := localtools.NewTaskStore()
1009+
store := storage.NewTaskStore()
10121010
task := store.Create("Summarize project state", "Write the final analysis summary", "Summarizing project state", nil)
1013-
inProgress := localtools.TaskInProgress
1014-
if _, err := store.Update(task.ID, localtools.TaskUpdateOpts{Status: &inProgress}); err != nil {
1011+
inProgress := storage.TaskInProgress
1012+
if _, err := store.Update(task.ID, storage.TaskUpdateOpts{Status: &inProgress}); err != nil {
10151013
t.Fatalf("set task in_progress: %v", err)
10161014
}
10171015

internal/agent/task_reminders.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"strings"
66

77
"github.com/voocel/agentcore"
8-
localtools "github.com/voocel/codebot/internal/tools"
8+
"github.com/voocel/codebot/internal/storage"
99
)
1010

1111
const (
@@ -15,7 +15,7 @@ const (
1515
taskReminderTurnsBetweenReminders = 10
1616
)
1717

18-
func taskManagementReminderForNextPrompt(msgs []agentcore.AgentMessage, snap localtools.TaskSnapshot) (key, reminder string, ok bool) {
18+
func taskManagementReminderForNextPrompt(msgs []agentcore.AgentMessage, snap storage.TaskSnapshot) (key, reminder string, ok bool) {
1919
if snap.Total == 0 {
2020
return "", "", false
2121
}
@@ -42,7 +42,7 @@ func taskManagementReminderForNextPrompt(msgs []agentcore.AgentMessage, snap loc
4242
return "task_management:stale", sb.String(), true
4343
}
4444

45-
func taskManagementReminderBeforeStop(msg agentcore.Message, snap localtools.TaskSnapshot) (key, reminder string, ok bool) {
45+
func taskManagementReminderBeforeStop(msg agentcore.Message, snap storage.TaskSnapshot) (key, reminder string, ok bool) {
4646
if msg.Role != agentcore.RoleAssistant || msg.StopReason != agentcore.StopReasonStop {
4747
return "", "", false
4848
}
@@ -66,14 +66,14 @@ func taskManagementReminderBeforeStop(msg agentcore.Message, snap localtools.Tas
6666
return "", "", false
6767
}
6868

69-
func inProgressTasks(snap localtools.TaskSnapshot) []localtools.Task {
69+
func inProgressTasks(snap storage.TaskSnapshot) []storage.Task {
7070
if len(snap.Items) == 0 || snap.InProgress == 0 {
7171
return nil
7272
}
7373

74-
out := make([]localtools.Task, 0, snap.InProgress)
74+
out := make([]storage.Task, 0, snap.InProgress)
7575
for _, task := range snap.Items {
76-
if task.Status == localtools.TaskInProgress {
76+
if task.Status == storage.TaskInProgress {
7777
out = append(out, task)
7878
}
7979
}

internal/bootstrap/services.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
mcpclient "github.com/voocel/codebot/internal/mcp"
1414
"github.com/voocel/codebot/internal/plugin"
1515
"github.com/voocel/codebot/internal/skill"
16-
localtools "github.com/voocel/codebot/internal/tools"
16+
"github.com/voocel/codebot/internal/storage"
1717
)
1818

1919
type bootServices struct {
@@ -24,7 +24,7 @@ type bootServices struct {
2424
skillUsage *skill.UsageTracker
2525
mcpManager *mcpclient.Manager
2626
mcpServers map[string]mcpclient.ServerConfig
27-
taskStore *localtools.TaskStore
27+
taskStore *storage.TaskStore
2828
}
2929

3030
func buildServices(input *resolvedInput) (*bootServices, error) {
@@ -103,8 +103,8 @@ func buildMCPServices(cwd string, contrib plugin.Contributions) (*mcpclient.Mana
103103
return mcpclient.NewManager(), mcpServers
104104
}
105105

106-
func newTaskStore(input *resolvedInput) *localtools.TaskStore {
107-
taskStore := localtools.NewTaskStore()
106+
func newTaskStore(input *resolvedInput) *storage.TaskStore {
107+
taskStore := storage.NewTaskStore()
108108
taskDir := filepath.Join(config.TasksDir(), input.sessionStore.Header().SessionID)
109109
if err := taskStore.SetDir(taskDir); err != nil {
110110
fmt.Fprintf(os.Stderr, "warning: task persistence: %v\n", err)

internal/plan/manager.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,26 +190,31 @@ func (c *Manager) applyState(state State) {
190190
}
191191
c.wireValidators()
192192

193+
const (
194+
planModeOverlay = "plan.mode"
195+
planApprovedOverlay = "plan.approved"
196+
)
197+
193198
switch state.Phase {
194199
case PhasePlanning:
195200
readOnly := c.session.ToolsByName("read", "glob", "grep", "ls", "ask_user")
196201
c.session.SetTools(append(readOnly, c.newExitTool())...)
197-
c.session.SetPlanModePrompt(modePrompt)
198-
c.session.ClearApprovedPlanPrompt()
202+
c.session.OverlayPrompt(planModeOverlay, modePrompt)
203+
c.session.OverlayPrompt(planApprovedOverlay, "")
199204
if c.approval != nil {
200205
c.approval.SetPlanMode(true)
201206
c.approval.SetPlanAllowedCommands(nil)
202207
}
203208
case PhaseReview:
204-
c.session.SetPlanModePrompt(modePrompt)
205-
c.session.ClearApprovedPlanPrompt()
209+
c.session.OverlayPrompt(planModeOverlay, modePrompt)
210+
c.session.OverlayPrompt(planApprovedOverlay, "")
206211
if c.approval != nil {
207212
c.approval.SetPlanMode(true)
208213
c.approval.SetPlanAllowedCommands(nil)
209214
}
210215
default:
211216
c.session.RestoreAllTools()
212-
c.session.ClearPlanModePrompt()
217+
c.session.OverlayPrompt(planModeOverlay, "")
213218
if c.approval != nil {
214219
c.approval.SetPlanMode(false)
215220
if mode, err := approval.ParseMode(state.PreMode); err == nil && state.PreMode != "" {
@@ -219,11 +224,11 @@ func (c *Manager) applyState(state State) {
219224
}
220225
if state.Slug != "" && state.Title != "" && c.planStore != nil {
221226
if content, err := c.planStore.Load(state.Slug); err == nil && strings.TrimSpace(content) != "" {
222-
c.session.SetApprovedPlanPrompt(BuildApprovedPlanPrompt(state.Title, content, state.AllowedCommands))
227+
c.session.OverlayPrompt(planApprovedOverlay, BuildApprovedPlanPrompt(state.Title, content, state.AllowedCommands))
223228
return
224229
}
225230
}
226-
c.session.ClearApprovedPlanPrompt()
231+
c.session.OverlayPrompt(planApprovedOverlay, "")
227232
}
228233
}
229234

0 commit comments

Comments
 (0)