-
Notifications
You must be signed in to change notification settings - Fork 43
feat(agent): AI 使用中自动沉淀长期记忆 #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| 你是一个记忆整理器。给定一段对话记录,判断其中是否有值得长期记住的信息。 | ||
|
|
||
| 只提取以下三类事实,且必须是对话中明确出现的信号,不要推测、不要脑补: | ||
|
|
||
| 1. 用户偏好:用户明确表达的喜好或习惯(输出风格、沟通方式、常用工具等),跨项目通用。 | ||
| 2. 项目约定:本项目的工作方式(构建命令、目录结构、提交规范、专属配置等)。 | ||
| 3. 踩坑经验:讨论中确认的 bug 根因与修法、验证过的绕行方案。 | ||
|
|
||
| 输出严格的 JSON 数组,没有任何其他文本。每条格式: | ||
|
|
||
| [{"name": "snake_case 短名", "description": "一句话摘要(何时该读它)", "scope": "global 或 project", "content": "Markdown 正文,具体、可执行"}, ...] | ||
|
|
||
| 规则: | ||
| - 没有值得记的就输出 []。 | ||
| - name 是文件名:英文小写加下划线,不含空格和路径分隔符,不超过 64 字符。 | ||
| - content 写具体事实,不要写空话;单条不超过 500 字。 | ||
| - 宁缺毋滥:普通任务执行过程、一次性问题不要记。 | ||
| - 最多 3 条。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # 记忆纪律(何时必须调用 memory 工具) | ||
|
|
||
| 长期记忆不是可选项。以下信号出现时,**当轮立即**用 memory 工具记录,不要等会话结束,不要等用户要求: | ||
|
|
||
| - 用户表达个人偏好(输出风格、沟通方式、习惯做法)→ scope=global; | ||
| - 用户纠正过你的做法、指出你说错的事实 → 把纠正记下来(global 或按内容归 project),避免再犯; | ||
| - 项目约定(构建方式、目录结构、分支/提交规范、专属工具链)→ scope=project; | ||
| - 定位到 bug 根因并验证修复后,把「根因 + 修法」沉淀成踩坑记忆 → scope=project。未经根因确认的猜测不记。 | ||
|
|
||
| 记录方式: | ||
|
|
||
| - 已有相关记忆(见下方清单)→ 用 memory(action=edit) 局部更新正文,不要新建重复文件; | ||
| - 确属新主题 → memory(action=save),description 写清「何时该读它」; | ||
| - 同一事实已经记录过 → 不再重复记录;记忆内容过时 → 用 edit 修正或 delete 清理。 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,121 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package com.aicode.feature.agent.domain.memory | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.aicode.core.util.FileLogger | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.aicode.feature.agent.domain.model.AgentMessage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.aicode.feature.agent.domain.provider.AIProvider | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.aicode.feature.agent.domain.prompt.PromptFileResolver | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.serialization.json.Json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.serialization.json.contentOrNull | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.serialization.json.jsonArray | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.serialization.json.jsonObject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.serialization.json.jsonPrimitive | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import javax.inject.Inject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import javax.inject.Singleton | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private const val TAG = "MemoryCurator" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 单次抽取的上限条数(与提示词约定一致)。 */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private const val MAX_CANDIDATES = 3 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 一轮对话送给整理器的上下文上限(字符):只看最近发生的事,控制成本。 */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private const val MAX_TRANSCRIPT_CHARS = 12_000 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 引擎级记忆兜底:一轮对话结束后,用轻量模型静默抽取值得长期记住的事实, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 直接写入 [MemoryRepository]。主模型忘了调用 memory 工具时由此兜底; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 全程静默失败,绝不影响对话主流程。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Singleton | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class MemoryCurator @Inject constructor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val memoryRepository: MemoryRepository, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val promptFileResolver: PromptFileResolver | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 提示词文件名,与 [PromptFileResolver.resolve] 的路径约定一致。 */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private fun prompt(): String = promptFileResolver.resolve("agent/memory-curator.md") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 抽取并落盘本轮对话的记忆。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param provider 由调用方解析好的轻量 provider(压缩专用模型或当前聊天模型回退)。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param transcript 本轮对话文本("用户: …/助手: …" 行)。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @return 本次实际写入的记忆条数(失败为 0)。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| suspend fun curate( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider: AIProvider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessionId: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| projectRoot: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| transcript: String | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Int = runCatching { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (transcript.isBlank()) return@runCatching 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val systemPrompt = prompt().replace(LEADING_COMMENT, "").trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (systemPrompt.isEmpty()) return@runCatching 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val response = provider.complete( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| systemPrompt = systemPrompt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages = listOf(AgentMessage.UserMessage(content = transcript.take(MAX_TRANSCRIPT_CHARS))), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tools = emptyList() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val candidates = parseCandidates(response.content) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var saved = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (c in candidates) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val ok = memoryRepository.saveMemory( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name = c.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description = c.description, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content = c.content, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scope = c.scope, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| projectRoot = projectRoot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ok) saved++ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (saved > 0) FileLogger.i(TAG, "会话 $sessionId 自动沉淀 $saved 条记忆") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| saved | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }.onFailure { e -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileLogger.w(TAG, "自动记忆整理失败(静默忽略): ${e.message}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }.getOrDefault(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '35,90p' app/src/main/java/com/aicode/feature/agent/domain/memory/MemoryCurator.kt
sed -n '990,1018p' app/src/main/java/com/aicode/feature/agent/domain/workflow/StatefulAgentWorkflow.kt
sed -n '1580,1630p' app/src/main/java/com/aicode/feature/agent/presentation/AIAgentViewModel.ktRepository: jieapi/AiCode Length of output: 6401 🏁 Script executed: #!/bin/bash
printf '%s\n' '--- usages and declarations ---'
rg -n -C 3 'curateMemory|memoryCurator\.curate|suspend fun curate|interface AIProvider|fun complete|saveMemory' app/src/main/java app/src/test 2>/dev/null | head -n 260
printf '%s\n' '--- workflow and caller context ---'
sed -n '970,1025p' app/src/main/java/com/aicode/feature/agent/domain/workflow/StatefulAgentWorkflow.kt
sed -n '1550,1625p' app/src/main/java/com/aicode/feature/agent/presentation/AIAgentViewModel.kt
printf '%s\n' '--- provider declarations ---'
rg -l 'interface AIProvider|class .*Provider|override suspend fun complete|suspend fun complete' app/src/main/java | head -n 40
printf '%s\n' '--- repository declarations ---'
rg -l 'interface MemoryRepository|class .*MemoryRepository|saveMemory' app/src/main/java | head -n 40Repository: jieapi/AiCode Length of output: 36918 🏁 Script executed: #!/bin/bash
printf '%s\n' '--- MemoryCurator imports ---'
sed -n '1,38p' app/src/main/java/com/aicode/feature/agent/domain/memory/MemoryCurator.kt
printf '%s\n' '--- StatefulAgentWorkflow imports ---'
sed -n '1,45p' app/src/main/java/com/aicode/feature/agent/domain/workflow/StatefulAgentWorkflow.kt
printf '%s\n' '--- relevant interface contract ---'
sed -n '115,138p' app/src/main/java/com/aicode/feature/agent/domain/workflow/AgentWorkflow.ktRepository: jieapi/AiCode Length of output: 4812 Rethrow cancellation from both curation boundaries. Both The current caller runs in Suggested fixdiff --git a/app/src/main/java/com/aicode/feature/agent/domain/memory/MemoryCurator.kt b/app/src/main/java/com/aicode/feature/agent/domain/memory/MemoryCurator.kt
@@
}.onFailure { e ->
+ if (e is kotlinx.coroutines.CancellationException) throw e
FileLogger.w(TAG, "自动记忆整理失败(静默忽略): ${e.message}")
}.getOrDefault(0)
diff --git a/app/src/main/java/com/aicode/feature/agent/domain/workflow/StatefulAgentWorkflow.kt b/app/src/main/java/com/aicode/feature/agent/domain/workflow/StatefulAgentWorkflow.kt
@@
}.onFailure { e ->
+ if (e is kotlinx.coroutines.CancellationException) throw e
FileLogger.w(TAG, "记忆兑现跳过: ${e.message}")
}.getOrDefault(0)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 解析整理器输出;格式不合法/越界条目一律丢弃,宁缺毋滥。 */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private fun parseCandidates(content: String): List<Candidate> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val start = content.indexOf('[') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val end = content.lastIndexOf(']') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (start < 0 || end <= start) return emptyList() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val arr = runCatching { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Json.parseToJsonElement(content.substring(start, end + 1)).jsonArray | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }.getOrNull() ?: return emptyList() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return arr.asSequence() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .mapNotNull { el -> runCatching { el.jsonObject }.getOrNull() } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .mapNotNull { obj -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runCatching { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val name = obj["name"]?.jsonPrimitive?.contentOrNull?.trim().orEmpty() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val description = obj["description"]?.jsonPrimitive?.contentOrNull?.trim().orEmpty() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val body = obj["content"]?.jsonPrimitive?.contentOrNull?.trim().orEmpty() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val isProject = obj["scope"]?.jsonPrimitive?.contentOrNull?.trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ?.equals("project", ignoreCase = true) == true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (name.isEmpty() || body.isEmpty() || description.isEmpty()) return@runCatching null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Candidate( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name = name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description = description.take(200), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content = body.take(2000), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scope = if (isProject) MemoryScope.PROJECT else MemoryScope.GLOBAL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }.getOrNull() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter { isValidName(it.name) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .take(MAX_CANDIDATES) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toList() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 与记忆文件名规则对齐:小写英文/数字/下划线/连字符,长度 1..64。 */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private fun isValidName(name: String): Boolean = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name.length <= 64 && name.matches(Regex("[a-z0-9][a-z0-9_-]*")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private data class Candidate( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+79
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '75,122p' app/src/main/java/com/aicode/feature/agent/domain/memory/MemoryCurator.kt
cat app/src/main/assets/prompts/agent/memory-curator.md
sed -n '20,65p' app/src/main/java/com/aicode/feature/agent/domain/memory/MemoryRepository.ktRepository: jieapi/AiCode Length of output: 4542 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- curator outline and flow ---'
ast-grep outline app/src/main/java/com/aicode/feature/agent/domain/memory/MemoryCurator.kt
sed -n '1,170p' app/src/main/java/com/aicode/feature/agent/domain/memory/MemoryCurator.kt
printf '%s\n' '--- curator references ---'
rg -n -C 3 'MemoryCurator|curate\(' app/src/main/java app/src/test || true
printf '%s\n' '--- memory scope and repository consumers ---'
rg -n -C 4 'enum class MemoryScope|sealed class MemoryScope|MemoryScope|listMemories\(|loadContent\(' app/src/main/java/com/aicode/feature/agent/domain/memory
printf '%s\n' '--- memory consumers ---'
rg -n -C 4 'listMemories|loadContent|memory' app/src/main/java/com/aicode/feature/agent | head -240
printf '%s\n' '--- PR diff ---'
git diff --stat 7d40c9d7778a34ea1b1e97dd49a8c98bb93855d6 e6cc0bfb179c078c5f8413672d20ac3d4a1bfc77 -- app/src/main/java/com/aicode/feature/agent/domain/memory/MemoryCurator.kt app/src/main/assets/prompts/agent/memory-curator.md
git diff --unified=20 7d40c9d7778a34ea1b1e97dd49a8c98bb93855d6 e6cc0bfb179c078c5f8413672d20ac3d4a1bfc77 -- app/src/main/java/com/aicode/feature/agent/domain/memory/MemoryCurator.kt app/src/main/assets/prompts/agent/memory-curator.mdRepository: jieapi/AiCode Length of output: 41854 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- workflow curation caller ---'
sed -n '930,1020p' app/src/main/java/com/aicode/feature/agent/domain/workflow/StatefulAgentWorkflow.kt
rg -n -C 5 'projectRoot|runMemoryCuration|curateMemory|memoryCurator' app/src/main/java/com/aicode/feature/agent/domain/workflow/StatefulAgentWorkflow.kt
printf '%s\n' '--- memory list source implementation ---'
rg -n -C 6 'class .*MemoryList|MemoryList|memoryListSource|build\(.*AgentContext|listMemories' app/src/main/java/com/aicode/feature/agent/domain
printf '%s\n' '--- scope/prompt contracts and tests ---'
rg -n -C 4 'scope.*global|scope.*project|MemoryScope|project convention|项目约定|memory-curator|自动记忆|记忆整理' app/src/main app/src/test || trueRepository: jieapi/AiCode Length of output: 43811 🤖 get_repo_knowledge executed:
Length of output: 293 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- curateMemory callers ---'
rg -n -C 5 'curateMemory\(' app/src/main/java
printf '%s\n' '--- memory list implementation ---'
sed -n '185,225p' app/src/main/java/com/aicode/feature/agent/domain/prompt/SystemPromptProvider.kt
sed -n '1,90p' app/src/main/assets/prompts/agent/memory-discipline.md
printf '%s\n' '--- exact scope and save sources ---'
sed -n '45,58p' app/src/main/java/com/aicode/feature/agent/domain/memory/MemoryRepository.kt
sed -n '18,38p' app/src/main/java/com/aicode/feature/agent/domain/memory/GlobalMemorySource.ktRepository: jieapi/AiCode Length of output: 8007 Reject candidates with an absent or invalid scope. The curator prompt requires Validate the scope before constructing Suggested fix val name = obj["name"]?.jsonPrimitive?.contentOrNull?.trim().orEmpty()
val description = obj["description"]?.jsonPrimitive?.contentOrNull?.trim().orEmpty()
val body = obj["content"]?.jsonPrimitive?.contentOrNull?.trim().orEmpty()
- val isProject = obj["scope"]?.jsonPrimitive?.contentOrNull?.trim()
- ?.equals("project", ignoreCase = true) == true
- if (name.isEmpty() || body.isEmpty() || description.isEmpty()) return@runCatching null
+ val scope = obj["scope"]?.jsonPrimitive?.contentOrNull?.trim()?.lowercase()
+ if (name.isEmpty() || body.isEmpty() || description.isEmpty()) return@runCatching null
+ if (scope != "global" && scope != "project") return@runCatching null
Candidate(
name = name,
description = description.take(200),
content = body.take(2000),
- scope = if (isProject) MemoryScope.PROJECT else MemoryScope.GLOBAL
+ scope = if (scope == "project") MemoryScope.PROJECT else MemoryScope.GLOBAL
)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val name: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val description: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val content: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val scope: MemoryScope | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private companion object { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val LEADING_COMMENT = Regex("(?s)^\\s*<!--.*?-->\\s*") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package com.aicode.feature.agent.domain.prompt | ||
|
|
||
| import com.aicode.core.util.FileLogger | ||
| import com.aicode.feature.agent.domain.container.ContainerInstaller | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import android.content.Context | ||
| import java.io.File | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| /** | ||
| * 单个提示词片段的文件解析器,供 [SystemPromptProvider] 与无循环依赖需求的 | ||
| * 其它组件(如 MemoryCurator)共用: | ||
| * | ||
| * - 名字是顶层 `<NN>-*.md`:先按数字身份在 `prompts.custom/` 顶层找覆盖(尾部名称可自由改), | ||
| * - 其余名字(含 `agent/` 子目录):按精确同名在 `prompts.custom/<name>` 找覆盖; | ||
| * - 再落到 `prompts/<name>`(本地默认副本),最后 assets(内置兜底)。 | ||
| * | ||
| * 本地副本由 [ContainerInstaller.extractPrompts] 在启动时全量释放,App 升级后随之更新。 | ||
| */ | ||
| @Singleton | ||
| class PromptFileResolver @Inject constructor( | ||
| @param:ApplicationContext private val context: Context, | ||
| private val containerInstaller: ContainerInstaller | ||
| ) { | ||
| private val customDir: File | ||
| get() = File(containerInstaller.aicodeDir, "prompts.custom") | ||
|
|
||
| private val customFragmentsByNumber: Map<Int, File> by lazy { | ||
| PromptFragmentResolver.numberedFragments(customDir).toMap() | ||
| } | ||
|
|
||
| fun resolve(name: String): String { | ||
| PromptFragmentResolver.parseNumber(name) | ||
| ?.let { number -> readFileOrNull(customFragmentsByNumber[number])?.let { return it } } | ||
| readFileOrNull(File(customDir, name))?.let { return it } | ||
| readFileOrNull(File(File(containerInstaller.aicodeDir, "prompts"), name))?.let { return it } | ||
| return context.assets.open("prompts/$name").bufferedReader().use { it.readText() } | ||
| } | ||
|
|
||
| private fun readFileOrNull(file: File?): String? { | ||
| if (file == null || !file.isFile) return null | ||
| return try { | ||
| file.bufferedReader().use { it.readText() } | ||
| } catch (e: Exception) { | ||
| FileLogger.w(TAG, "读取提示词失败 ${file.name}: ${e.message}", e) | ||
| null | ||
| } | ||
| } | ||
|
|
||
| private companion object { | ||
| const val TAG = "PromptFileResolver" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,7 @@ class SystemPromptProvider @Inject constructor( | |
| @param:ApplicationContext private val context: Context, | ||
| private val skillRepository: SkillRepository, | ||
| private val memoryRepository: MemoryRepository, | ||
| private val promptFileResolver: PromptFileResolver, | ||
| private val containerInstaller: ContainerInstaller, | ||
| private val agentDefinitionRepository: AgentDefinitionRepository | ||
| ) { | ||
|
|
@@ -194,7 +195,8 @@ class SystemPromptProvider @Inject constructor( | |
| val memories = try { memoryRepository.listMemories(ctx.projectRoot) } catch (e: Exception) { return null } | ||
| if (memories.isEmpty()) { | ||
| cachedByKey[key] = "" | ||
| return null | ||
| // 空清单也要注入纪律:首次会话正是建立记忆的起点。 | ||
| return memoryDiscipline() | ||
|
Comment on lines
+198
to
+199
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Cache the discipline text for the empty-list case. On the first call with no memories, Line 196 caches 🐛 Proposed fix if (memories.isEmpty()) {
- cachedByKey[key] = ""
// 空清单也要注入纪律:首次会话正是建立记忆的起点。
- return memoryDiscipline()
+ val discipline = memoryDiscipline().orEmpty()
+ cachedByKey[key] = discipline
+ return discipline.ifEmpty { null }
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| val globalMemories = memories.filter { it.scope == MemoryScope.GLOBAL } | ||
|
|
@@ -212,11 +214,19 @@ class SystemPromptProvider @Inject constructor( | |
| } | ||
| }.trimEnd() | ||
|
|
||
| cachedByKey[key] = content | ||
| // 记忆纪律紧跟清单注入:清单告诉模型「有什么」,纪律告诉它「何时必须写」。 | ||
| val full = listOf(content, memoryDiscipline()).mapNotNull { it }.joinToString("\n\n") | ||
| if (full.isEmpty()) return null | ||
|
|
||
| cachedByKey[key] = full | ||
| trimIfNeeded() | ||
| return content | ||
| return full | ||
| } | ||
|
|
||
| /** 记忆纪律正文(无记忆清单时单独注入)。 */ | ||
| private fun memoryDiscipline(): String? = | ||
| resolvePrompt(MEMORY_DISCIPLINE_FILE).replace(LEADING_COMMENT, "").trim().ifEmpty { null } | ||
|
|
||
| private fun trimIfNeeded() { | ||
| if (cachedByKey.size > SOURCE_CACHE_LIMIT) cachedByKey.clear() | ||
| } | ||
|
|
@@ -397,37 +407,15 @@ class SystemPromptProvider @Inject constructor( | |
| private fun currentDate(): String = | ||
| java.time.ZonedDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")) | ||
|
|
||
| /** | ||
| * 按优先级解析单个提示词片段: | ||
| * - 名字是顶层 `<NN>-*.md`:先按数字身份在 `prompts.custom/` 顶层找覆盖(尾部名称可自由改), | ||
| * - 其余名字(含 `agent/` 子目录):按精确同名在 `prompts.custom/<name>` 找覆盖; | ||
| * 再落到 `prompts/<name>`(本地默认副本),最后 assets(内置兜底)。 | ||
| * | ||
| * 本地副本由 [ContainerInstaller.extractPrompts] 在启动时全量释放,App 升级后随之更新。 | ||
| */ | ||
| fun resolvePrompt(name: String): String { | ||
| PromptFragmentResolver.parseNumber(name) | ||
| ?.let { number -> readFileOrNull(customFragmentsByNumber[number])?.let { return it } } | ||
| readFileOrNull(File(customDir, name))?.let { return it } | ||
| readFileOrNull(File(File(containerInstaller.aicodeDir, "prompts"), name))?.let { return it } | ||
| return context.assets.open("prompts/$name").bufferedReader().use { it.readText() } | ||
| } | ||
|
|
||
| private fun readFileOrNull(file: File?): String? { | ||
| if (file == null || !file.isFile) return null | ||
| return try { | ||
| file.bufferedReader().use { it.readText() } | ||
| } catch (e: Exception) { | ||
| FileLogger.w(TAG, "读取提示词失败 ${file.name}: ${e.message}", e) | ||
| null | ||
| } | ||
| } | ||
| /** 按优先级解析单个提示词片段,见 [PromptFileResolver.resolve]。保留本方法以兼容现有调用点。 */ | ||
| fun resolvePrompt(name: String): String = promptFileResolver.resolve(name) | ||
|
|
||
| private companion object { | ||
| const val TAG = "SystemPromptProvider" | ||
| const val AGENTS_FILE = "AGENTS.md" | ||
| const val CLAUDE_FILE = "CLAUDE.md" | ||
| const val SUBAGENT_BASE_FILE = "agent/subagent-base.md" | ||
| const val MEMORY_DISCIPLINE_FILE = "agent/memory-discipline.md" | ||
| const val MAX_AGENTS_CHARS = 32_000 | ||
| /** 会话级缓存 key 数量上限:超过后整体清空,仅防长期累积;正常会话数远小于此。 */ | ||
| const val SOURCE_CACHE_LIMIT = 32 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: jieapi/AiCode
Length of output: 3829
🏁 Script executed:
Repository: jieapi/AiCode
Length of output: 13181
🏁 Script executed:
Repository: jieapi/AiCode
Length of output: 41388
🏁 Script executed:
Repository: jieapi/AiCode
Length of output: 32211
🏁 Script executed:
Repository: jieapi/AiCode
Length of output: 29968
Keep the latest transcript segment for memory curation.
AIAgentViewModelappends the current request and answer after six history messages.transcript.take(12_000)keeps the older prefix. When history exceeds 12,000 characters, the curator can receive none of the current turn, so it can miss facts introduced by that request and answer.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents