Skip to content

Commit e40752a

Browse files
committed
fix(agent): use feat/team context.go version, fix GetMemoryContext signature
1 parent 40b61c6 commit e40752a

1 file changed

Lines changed: 27 additions & 78 deletions

File tree

pkg/agent/context.go

Lines changed: 27 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package agent
33
import (
44
"errors"
55
"fmt"
6+
"io/fs"
67
"os"
78
"path/filepath"
89
"runtime"
@@ -105,6 +106,7 @@ Your workspace is at: %s
105106
4. **Context summaries** - Conversation summaries provided as context are approximate references only. They may be incomplete or outdated. Always defer to explicit user instructions over summary content.
106107
107108
5. **Team delegation** - For any task that is non-trivial, multi-step, or involves distinct concerns (e.g. "convert React to Vue", "build a feature", "analyze and report"), you MUST use the 'team' tool to delegate and parallelize. Do NOT attempt to handle complex tasks inline by calling tools one by one yourself. Decompose first, delegate second, then report the outcome.
109+
108110
%s`,
109111
version, workspacePath, workspacePath, workspacePath, workspacePath, workspacePath, toolDiscovery)
110112
}
@@ -150,8 +152,11 @@ The following skills extend your capabilities. To use a skill, read its SKILL.md
150152
%s`, skillsSummary))
151153
}
152154

153-
// Memory context is no longer injected here. It has moved to buildDynamicContextAndMemory
154-
// so that vector memory search can use the specific user query per-request.
155+
// Memory context
156+
memoryContext := cb.memory.GetMemoryContext("")
157+
if memoryContext != "" {
158+
parts = append(parts, "# Memory\n\n"+memoryContext)
159+
}
155160

156161
// Join with "---" separator
157162
return strings.Join(parts, "\n\n---\n\n")
@@ -224,7 +229,7 @@ func (cb *ContextBuilder) sourcePaths() []string {
224229
filepath.Join(cb.workspace, "SOUL.md"),
225230
filepath.Join(cb.workspace, "USER.md"),
226231
filepath.Join(cb.workspace, "IDENTITY.md"),
227-
// MEMORY.md is no longer cached in the static system prompt
232+
filepath.Join(cb.workspace, "memory", "MEMORY.md"),
228233
}
229234
}
230235

@@ -254,12 +259,10 @@ type cacheBaseline struct {
254259
// the latest mtime across all tracked files + skills directory contents.
255260
// Called under write lock when the cache is built.
256261
func (cb *ContextBuilder) buildCacheBaseline() cacheBaseline {
257-
memoryDir := filepath.Join(cb.workspace, "memory")
262+
skillRoots := cb.skillRoots()
258263

259-
// All paths whose existence we track: source files + skill roots + memory dir.
260-
allPaths := cb.sourcePaths()
261-
allPaths = append(allPaths, cb.skillRoots()...)
262-
allPaths = append(allPaths, memoryDir)
264+
// All paths whose existence we track: source files + all skill roots.
265+
allPaths := append(cb.sourcePaths(), skillRoots...)
263266

264267
existed := make(map[string]bool, len(allPaths))
265268
skillFiles := make(map[string]time.Time)
@@ -273,11 +276,10 @@ func (cb *ContextBuilder) buildCacheBaseline() cacheBaseline {
273276
}
274277
}
275278

276-
// Walk skills files to capture their mtimes too.
277-
// Use os.Stat (not d.Info) to match the stat method used in
278-
// fileChangedSince / skillFilesChangedSince for consistency.
279-
for _, root := range cb.skillRoots() {
280-
_ = filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
279+
// Walk all skill roots recursively to snapshot skill files and mtimes.
280+
// Use os.Stat (not d.Info) for consistency with sourceFilesChanged checks.
281+
for _, root := range skillRoots {
282+
_ = filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error {
281283
if walkErr == nil && !d.IsDir() {
282284
if info, err := os.Stat(path); err == nil {
283285
skillFiles[path] = info.ModTime()
@@ -290,16 +292,6 @@ func (cb *ContextBuilder) buildCacheBaseline() cacheBaseline {
290292
})
291293
}
292294

293-
// Also walk memory files
294-
_ = filepath.WalkDir(memoryDir, func(path string, d os.DirEntry, walkErr error) error {
295-
if walkErr == nil && !d.IsDir() {
296-
if info, err := os.Stat(path); err == nil && info.ModTime().After(maxMtime) {
297-
maxMtime = info.ModTime()
298-
}
299-
}
300-
return nil
301-
})
302-
303295
// If no tracked files exist yet (empty workspace), maxMtime is zero.
304296
// Use a very old non-zero time so that:
305297
// 1. cachedAt.IsZero() won't trigger perpetual rebuilds.
@@ -340,26 +332,10 @@ func (cb *ContextBuilder) sourceFilesChangedLocked() bool {
340332
return true
341333
}
342334
}
343-
344-
// 2. Structural changes (add/remove entries inside the dir) are reflected
345-
// in the directory's own mtime, which fileChangedSince already checks.
346-
//
347-
// 3. Content-only edits to files inside skills/ do NOT update the parent
348-
// directory mtime on most filesystems, so we recursively walk to check
349-
// individual file mtimes at any nesting depth.
350335
if skillFilesChangedSince(cb.skillRoots(), cb.skillFilesAtCache) {
351336
return true
352337
}
353338

354-
// --- Memory directory (handled identically to skills) ---
355-
memoryDir := filepath.Join(cb.workspace, "memory")
356-
if cb.fileChangedSince(memoryDir) {
357-
return true
358-
}
359-
if filesModifiedSince(memoryDir, cb.cachedAt) {
360-
return true
361-
}
362-
363339
return false
364340
}
365341

@@ -397,32 +373,6 @@ func (cb *ContextBuilder) fileChangedSince(path string) bool {
397373
// if the callback returned nil when its err parameter is non-nil.
398374
var errWalkStop = errors.New("walk stop")
399375

400-
// filesModifiedSince recursively checks if any file directly or indirectly
401-
// inside dirPath has been modified since the cached time.
402-
func filesModifiedSince(dirPath string, since time.Time) bool {
403-
changed := false
404-
err := filepath.WalkDir(dirPath, func(path string, d os.DirEntry, walkErr error) error {
405-
if changed || walkErr != nil || d.IsDir() {
406-
return nil
407-
}
408-
if info, err := os.Stat(path); err == nil && info.ModTime().After(since) {
409-
changed = true
410-
return errWalkStop // stop walking
411-
}
412-
return nil
413-
})
414-
415-
if err != nil && !errors.Is(err, errWalkStop) && !os.IsNotExist(err) {
416-
logger.DebugCF("agent", "Failed to walk directory for mtime check",
417-
map[string]any{
418-
"dir": dirPath,
419-
"error": err.Error(),
420-
})
421-
}
422-
423-
return changed
424-
}
425-
426376
// skillFilesChangedSince compares the current recursive skill file tree
427377
// against the cache-time snapshot. Any create/delete/mtime drift invalidates
428378
// the cache.
@@ -452,7 +402,7 @@ func skillFilesChangedSince(skillRoots []string, filesAtCache map[string]time.Ti
452402
continue
453403
}
454404

455-
err := filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
405+
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error {
456406
if walkErr != nil {
457407
// Treat unexpected walk errors as changed to avoid stale cache.
458408
if !os.IsNotExist(walkErr) {
@@ -502,10 +452,15 @@ func (cb *ContextBuilder) LoadBootstrapFiles() string {
502452
return sb.String()
503453
}
504454

505-
// buildDynamicContextAndMemory returns a short dynamic context string with per-request info,
506-
// including semantic memory retrieved based on the current user message.
507-
// This changes every request so it is NOT part of the cached prompt.
508-
func (cb *ContextBuilder) buildDynamicContextAndMemory(channel, chatID, currentMessage string) string {
455+
// buildDynamicContext returns a short dynamic context string with per-request info.
456+
// This changes every request (time, session) so it is NOT part of the cached prompt.
457+
// LLM-side KV cache reuse is achieved by each provider adapter's native mechanism:
458+
// - Anthropic: per-block cache_control (ephemeral) on the static SystemParts block
459+
// - OpenAI / Codex: prompt_cache_key for prefix-based caching
460+
//
461+
// See: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
462+
// See: https://platform.openai.com/docs/guides/prompt-caching
463+
func (cb *ContextBuilder) buildDynamicContext(channel, chatID string) string {
509464
now := time.Now().Format("2006-01-02 15:04 (Monday)")
510465
rt := fmt.Sprintf("%s %s, Go %s", runtime.GOOS, runtime.GOARCH, runtime.Version())
511466

@@ -516,12 +471,6 @@ func (cb *ContextBuilder) buildDynamicContextAndMemory(channel, chatID, currentM
516471
fmt.Fprintf(&sb, "\n\n## Current Session\nChannel: %s\nChat ID: %s", channel, chatID)
517472
}
518473

519-
// Dynamic memory context (retrieving relevant context based on user message)
520-
memoryContext := cb.memory.GetMemoryContext(currentMessage)
521-
if memoryContext != "" {
522-
fmt.Fprintf(&sb, "\n\n# Memory\n\n%s", memoryContext)
523-
}
524-
525474
return sb.String()
526475
}
527476

@@ -545,8 +494,8 @@ func (cb *ContextBuilder) BuildMessages(
545494
// - OpenAI-compat passes messages through as-is.
546495
staticPrompt := cb.BuildSystemPromptWithCache()
547496

548-
// Build short dynamic context (time, runtime, session, dynamic semantic memory)
549-
dynamicCtx := cb.buildDynamicContextAndMemory(channel, chatID, currentMessage)
497+
// Build short dynamic context (time, runtime, session) — changes per request
498+
dynamicCtx := cb.buildDynamicContext(channel, chatID)
550499

551500
// Compose a single system message: static (cached) + dynamic + optional summary.
552501
// Keeping all system content in one message ensures every provider adapter can

0 commit comments

Comments
 (0)