perf(agent): avoid reloading bootstrap files on cache hits
This commit is contained in:
parent
9d6a445bb1
commit
72863cf8bc
1 changed files with 42 additions and 10 deletions
|
|
@ -45,6 +45,11 @@ type ContextBuilder struct {
|
||||||
// build time. This catches nested file creations/deletions/mtime changes
|
// build time. This catches nested file creations/deletions/mtime changes
|
||||||
// that may not update the top-level skill root directory mtime.
|
// that may not update the top-level skill root directory mtime.
|
||||||
skillFilesAtCache map[string]time.Time
|
skillFilesAtCache map[string]time.Time
|
||||||
|
|
||||||
|
// sourcePathsAtCache stores the active non-skill tracked paths captured
|
||||||
|
// when the cache was built, so cache hits do not need to re-detect the
|
||||||
|
// bootstrap file family.
|
||||||
|
sourcePathsAtCache []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cb *ContextBuilder) WithToolDiscovery(useBM25, useRegex bool) *ContextBuilder {
|
func (cb *ContextBuilder) WithToolDiscovery(useBM25, useRegex bool) *ContextBuilder {
|
||||||
|
|
@ -209,6 +214,7 @@ func (cb *ContextBuilder) BuildSystemPromptWithCache() string {
|
||||||
cb.cachedAt = baseline.maxMtime
|
cb.cachedAt = baseline.maxMtime
|
||||||
cb.existedAtCache = baseline.existed
|
cb.existedAtCache = baseline.existed
|
||||||
cb.skillFilesAtCache = baseline.skillFiles
|
cb.skillFilesAtCache = baseline.skillFiles
|
||||||
|
cb.sourcePathsAtCache = append(cb.sourcePathsAtCache[:0], baseline.sourcePaths...)
|
||||||
|
|
||||||
logger.DebugCF("agent", "System prompt cached",
|
logger.DebugCF("agent", "System prompt cached",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
|
|
@ -229,6 +235,7 @@ func (cb *ContextBuilder) InvalidateCache() {
|
||||||
cb.cachedAt = time.Time{}
|
cb.cachedAt = time.Time{}
|
||||||
cb.existedAtCache = nil
|
cb.existedAtCache = nil
|
||||||
cb.skillFilesAtCache = nil
|
cb.skillFilesAtCache = nil
|
||||||
|
cb.sourcePathsAtCache = nil
|
||||||
|
|
||||||
logger.DebugCF("agent", "System prompt cache invalidated", nil)
|
logger.DebugCF("agent", "System prompt cache invalidated", nil)
|
||||||
}
|
}
|
||||||
|
|
@ -237,10 +244,25 @@ func (cb *ContextBuilder) InvalidateCache() {
|
||||||
// invalidation (bootstrap files + memory). Skill roots are handled separately
|
// invalidation (bootstrap files + memory). Skill roots are handled separately
|
||||||
// because they require both directory-level and recursive file-level checks.
|
// because they require both directory-level and recursive file-level checks.
|
||||||
func (cb *ContextBuilder) sourcePaths() []string {
|
func (cb *ContextBuilder) sourcePaths() []string {
|
||||||
agentDefinition := cb.LoadAgentDefinition()
|
agentPath := filepath.Join(cb.workspace, string(AgentDefinitionSourceAgent))
|
||||||
paths := agentDefinition.trackedPaths(cb.workspace)
|
paths := []string{
|
||||||
paths = append(paths, filepath.Join(cb.workspace, "memory", "MEMORY.md"))
|
agentPath,
|
||||||
return uniquePaths(paths)
|
filepath.Join(cb.workspace, "SOUL.md"),
|
||||||
|
filepath.Join(cb.workspace, "USER.md"),
|
||||||
|
filepath.Join(cb.workspace, "memory", "MEMORY.md"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache invalidation only needs to know which bootstrap path family is active.
|
||||||
|
// Avoid loading/parsing AGENT.md on every cache hit; existence is sufficient
|
||||||
|
// because the structured format always wins when present.
|
||||||
|
if _, err := os.Stat(agentPath); err == nil {
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(paths,
|
||||||
|
filepath.Join(cb.workspace, string(AgentDefinitionSourceAgents)),
|
||||||
|
filepath.Join(cb.workspace, "IDENTITY.md"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// skillRoots returns all skill root directories that can affect
|
// skillRoots returns all skill root directories that can affect
|
||||||
|
|
@ -260,9 +282,10 @@ func (cb *ContextBuilder) skillRoots() []string {
|
||||||
// cacheBaseline holds the file existence snapshot and the latest observed
|
// cacheBaseline holds the file existence snapshot and the latest observed
|
||||||
// mtime across all tracked paths. Used as the cache reference point.
|
// mtime across all tracked paths. Used as the cache reference point.
|
||||||
type cacheBaseline struct {
|
type cacheBaseline struct {
|
||||||
existed map[string]bool
|
existed map[string]bool
|
||||||
skillFiles map[string]time.Time
|
skillFiles map[string]time.Time
|
||||||
maxMtime time.Time
|
sourcePaths []string
|
||||||
|
maxMtime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildCacheBaseline records which tracked paths currently exist and computes
|
// buildCacheBaseline records which tracked paths currently exist and computes
|
||||||
|
|
@ -270,9 +293,10 @@ type cacheBaseline struct {
|
||||||
// Called under write lock when the cache is built.
|
// Called under write lock when the cache is built.
|
||||||
func (cb *ContextBuilder) buildCacheBaseline() cacheBaseline {
|
func (cb *ContextBuilder) buildCacheBaseline() cacheBaseline {
|
||||||
skillRoots := cb.skillRoots()
|
skillRoots := cb.skillRoots()
|
||||||
|
sourcePaths := cb.sourcePaths()
|
||||||
|
|
||||||
// All paths whose existence we track: source files + all skill roots.
|
// All paths whose existence we track: source files + all skill roots.
|
||||||
allPaths := append(cb.sourcePaths(), skillRoots...)
|
allPaths := append(append([]string(nil), sourcePaths...), skillRoots...)
|
||||||
|
|
||||||
existed := make(map[string]bool, len(allPaths))
|
existed := make(map[string]bool, len(allPaths))
|
||||||
skillFiles := make(map[string]time.Time)
|
skillFiles := make(map[string]time.Time)
|
||||||
|
|
@ -312,7 +336,12 @@ func (cb *ContextBuilder) buildCacheBaseline() cacheBaseline {
|
||||||
maxMtime = time.Unix(1, 0)
|
maxMtime = time.Unix(1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cacheBaseline{existed: existed, skillFiles: skillFiles, maxMtime: maxMtime}
|
return cacheBaseline{
|
||||||
|
existed: existed,
|
||||||
|
skillFiles: skillFiles,
|
||||||
|
sourcePaths: sourcePaths,
|
||||||
|
maxMtime: maxMtime,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceFilesChangedLocked checks whether any workspace source file has been
|
// sourceFilesChangedLocked checks whether any workspace source file has been
|
||||||
|
|
@ -328,7 +357,10 @@ func (cb *ContextBuilder) sourceFilesChangedLocked() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check tracked source files (bootstrap + memory).
|
// Check tracked source files (bootstrap + memory).
|
||||||
if slices.ContainsFunc(cb.sourcePaths(), cb.fileChangedSince) {
|
if len(cb.sourcePathsAtCache) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if slices.ContainsFunc(cb.sourcePathsAtCache, cb.fileChangedSince) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue