docs(runtime): internal/runtime/loop 44 个文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;本模块按长期分批协议(一个模块一次派工)执行。 注释行数 1329 → 1329 逐行 1:1(含中文 33 → 1304);行尾注释 5 条一并译。 校验:机械校验(非注释改动行)= 空;独立 Go AST 程序(剥注释复渲)证明 44/44 文件代码全等; 全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。 gofmt:capacity_loop_test.go / flow.go 改动前即未格式化,按铁律保留原样未动。
This commit is contained in:
1 parent
355f81d04a
commit
91e8d7a029
44 files changed
+1315
-1315
No files matched your search
@@ -9,32 +9,32 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
|
||||
// memoryExtractTimeout bounds one background extraction. It only governs the
|
||||
// detached extraction context, so a hung model call cannot leak a goroutine for
|
||||
// the process lifetime.
|
||||
// memoryExtractTimeout 限制单次后台提取。它只管辖
|
||||
// 脱离出来的提取 context,因此挂起的模型调用不会让一个 goroutine
|
||||
// 泄露到进程生命周期结束。
|
||||
const memoryExtractTimeout = 60 * time.Second
|
||||
|
||||
// asyncExtractor is the extractor capability the loop detaches to a background
|
||||
// goroutine: memory.LLMExtractor implements it through ExtractAsync (design/03 §1
|
||||
// LLM 异步自动提取), returning a channel that delivers exactly one result.
|
||||
// Deterministic extractors (memory.ManualExtractor) do not implement it and stay
|
||||
// on the synchronous path.
|
||||
// asyncExtractor 是循环脱离到后台 goroutine 的提取器能力:
|
||||
// memory.LLMExtractor 经 ExtractAsync 实现它(design/03 §1
|
||||
// LLM 异步自动提取),返回一个恰好投递一个结果的 channel。
|
||||
// 确定性提取器(memory.ManualExtractor)不实现它,留在
|
||||
// 同步路径上。
|
||||
type asyncExtractor interface {
|
||||
ExtractAsync(ctx context.Context, conversation []model.Message) (<-chan memory.AsyncResult, error)
|
||||
}
|
||||
|
||||
// injectMemory retrieves the persona's relevant long-term memories and prepends
|
||||
// them as a leading system message, so the model can ground its answer in recalled
|
||||
// context (design §6 / §2). It is re-run every round via assembleContext so the
|
||||
// retrieval reflects the latest conversation rather than a first-turn snapshot. It
|
||||
// returns the input messages unchanged when no retriever/persona is configured,
|
||||
// retrieval fails, or nothing relevant is found.
|
||||
// injectMemory 检索该角色的相关长期记忆并把它们作为前导
|
||||
// 系统消息前置,使模型能把回答建立在召回出的上下文之上
|
||||
// (design §6 / §2)。它经 assembleContext 每轮重新运行,因此
|
||||
// 检索反映最新对话而非首轮快照。未配置
|
||||
// retriever/persona、检索失败、或没找到相关内容时,
|
||||
// 它原样返回输入消息。
|
||||
//
|
||||
// The session env on ctx gates the retrieval: the owner-full env keeps the full
|
||||
// recall (unchanged behavior); the restricted env (group chat / stranger,
|
||||
// design/02 §3, design/11 §2) is read-only isolation — only non-owner-private
|
||||
// project memory is injected so no owner-sensitive memory leaks to a group or
|
||||
// stranger.
|
||||
// ctx 上的会话环境为检索设门:owner 全权限环境保留完整
|
||||
// 召回(行为不变);受限环境(群聊 / 陌生人,
|
||||
// design/02 §3, design/11 §2)是只读隔离 —— 只注入
|
||||
// 非主人私有的项目记忆,使主人敏感记忆不泄露给群聊或
|
||||
// 陌生人。
|
||||
func (l *Loop) injectMemory(ctx context.Context, messages []model.Message) []model.Message {
|
||||
block := l.memoryBlock(ctx, messages)
|
||||
if block == "" {
|
||||
@@ -46,11 +46,11 @@ func (l *Loop) injectMemory(ctx context.Context, messages []model.Message) []mod
|
||||
return out
|
||||
}
|
||||
|
||||
// memoryBlock renders the round's memory block, or "" when there is nothing to
|
||||
// inject (no retriever/persona, a retrieval error, or no relevant memory). It is
|
||||
// split out from injectMemory because the assembly places other leading blocks
|
||||
// between the memory block and the conversation (the role context of design/22
|
||||
// §7-5), so it needs the block itself rather than a pre-prefixed slice.
|
||||
// memoryBlock 渲染该轮的记忆块;无可注入内容时返回 ""(无
|
||||
// retriever/persona、检索出错、或没有相关记忆)。它
|
||||
// 从 injectMemory 中拆出,因为组装会在记忆块与对话之间
|
||||
// 放置其他前导块(design/22 §7-5 的角色上下文),
|
||||
// 因此它需要块本身而不是已前置好的切片。
|
||||
func (l *Loop) memoryBlock(ctx context.Context, messages []model.Message) string {
|
||||
personaID := l.personaFor(ctx)
|
||||
if l.retriever == nil || personaID == "" {
|
||||
@@ -65,11 +65,11 @@ func (l *Loop) memoryBlock(ctx context.Context, messages []model.Message) string
|
||||
return block
|
||||
}
|
||||
|
||||
// retrieveMemoryBlock returns the memory injection block for env and the turn's
|
||||
// role. The owner-full env keeps the pre-existing Inject behavior. The restricted
|
||||
// env (group chat / stranger) injects only project-scope memory (人人可读, not
|
||||
// owner-private) and marks the block as group-isolated so callers can tell an
|
||||
// isolated read from the owner's full read.
|
||||
// retrieveMemoryBlock 返回 env 与该 turn 角色的记忆注入块。
|
||||
// owner 全权限环境保持既有的 Inject 行为。受限
|
||||
// 环境(群聊 / 陌生人)只注入项目作用域记忆(人人可读,
|
||||
// 非主人私有),并把该块标记为群聊隔离,使调用方能区分
|
||||
// 隔离读取与主人的完整读取。
|
||||
func (l *Loop) retrieveMemoryBlock(ctx context.Context, env SessionEnv, personaID, query string) (string, error) {
|
||||
if !env.Restricted() {
|
||||
return l.retriever.Inject(ctx, personaID, query, 0)
|
||||
@@ -90,9 +90,9 @@ func (l *Loop) retrieveMemoryBlock(ctx context.Context, env SessionEnv, personaI
|
||||
return formatMemoryBlock(shared, "群聊隔离"), nil
|
||||
}
|
||||
|
||||
// formatMemoryBlock builds the leading system block from retrieved results. The
|
||||
// non-empty marker prefixes the block so an isolated group read is distinguishable
|
||||
// from the owner's full read.
|
||||
// formatMemoryBlock 由检索结果构建前导系统块。非空
|
||||
// marker 作为该块的前缀,使隔离的群聊读取可与
|
||||
// 主人的完整读取区分开。
|
||||
func formatMemoryBlock(results []memory.Result, marker string) string {
|
||||
var b strings.Builder
|
||||
if marker != "" {
|
||||
@@ -110,28 +110,28 @@ func formatMemoryBlock(results []memory.Result, marker string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// assembleContext builds the model-visible message list for one round (design
|
||||
// §1.3 每轮 assemble, the loop's equivalent of OpenClaw's transformContext): it
|
||||
// re-injects the persona's relevant long-term memory — retrieval is re-run every
|
||||
// round from the current conversation, not cached from the first turn, so it
|
||||
// supports cross-round refresh — folds in the per-session working-memory summary
|
||||
// when present, renders the role/speaker attribution of the conversation (design/22
|
||||
// §7-5), and applies the optional TransformContext hook last so a caller can
|
||||
// project/filter what the model actually sees. It returns the input unchanged
|
||||
// when nothing is configured.
|
||||
// assembleContext 为一轮构建模型可见的消息列表(design
|
||||
// §1.3 每轮 assemble,循环中与 OpenClaw 的 transformContext 等价之物):它
|
||||
// 重新注入该角色的相关长期记忆 —— 检索每轮都从
|
||||
// 当前对话重新运行,不从首轮缓存,因此
|
||||
// 支持跨轮刷新 —— 在存在时并入每会话的工作记忆摘要,
|
||||
// 渲染对话的角色/发言人归属(design/22
|
||||
// §7-5),最后应用可选的 TransformContext 钩子,使调用方能
|
||||
// 投影/过滤模型实际所见。什么都没配置时
|
||||
// 它原样返回输入。
|
||||
//
|
||||
// The existing order is preserved exactly: the leading blocks are the per-round
|
||||
// time, the working-memory summary and the memory block, in that order; the role
|
||||
// declaration sits right after them (immediately before the conversation it
|
||||
// describes) and the speaker boundary notes are inserted into the conversation
|
||||
// itself. A conversation with no speaker label renders no attribution at all, so
|
||||
// every single-role path keeps the prompt it had before multi-character support.
|
||||
// 既有顺序被精确保留:前导块依次是每轮的
|
||||
// 时间、工作记忆摘要与记忆块;角色
|
||||
// 声明紧接其后(就在它所描述的对话之前),而发言人
|
||||
// 边界说明被插入对话本身。没有发言人标签的对话
|
||||
// 完全不渲染归属,因此每条单角色路径都保持多角色支持
|
||||
// 之前的提示词。
|
||||
func (l *Loop) assembleContext(ctx context.Context, messages []model.Message, round int) []model.Message {
|
||||
var leading []model.Message
|
||||
// Per-round "current time" so the model naturally knows the wall clock (not a
|
||||
// first-turn snapshot). Keyed on a configured timezone so tests and no-TZ runs
|
||||
// keep their exact leading-message counts. It sits before working memory / the
|
||||
// memory block and can still be projected by transformContext.
|
||||
// 每轮「当前时间」,使模型自然知道墙上时钟(而非首轮
|
||||
// 快照)。以已配置的时区为条件,使测试与无时区的运行
|
||||
// 保持其精确的前导消息数。它位于工作记忆 /
|
||||
// 记忆块之前,且仍可被 transformContext 投影。
|
||||
if l.timeLocation != nil {
|
||||
leading = append(leading, l.currentTimeBlock())
|
||||
}
|
||||
@@ -154,11 +154,11 @@ func (l *Loop) assembleContext(ctx context.Context, messages []model.Message, ro
|
||||
return out
|
||||
}
|
||||
|
||||
// compactIfNeeded compacts the conversation when its estimated context exceeds
|
||||
// the configured MaxContextTokens. It is a no-op when compaction is disabled
|
||||
// (non-positive MaxContextTokens), the compactor is nil, the estimate is within
|
||||
// budget, or the compactor returns an error — a compaction failure is logged and
|
||||
// the conversation is kept as-is rather than failing the run.
|
||||
// compactIfNeeded 在估算上下文超出配置的 MaxContextTokens
|
||||
// 时压缩对话。压缩被关闭(MaxContextTokens 非正)、
|
||||
// compactor 为 nil、估算在预算内、或 compactor 返回错误时
|
||||
// 它是空操作 —— 压缩失败会被记录日志并保持对话
|
||||
// 原样,而不是让 run 失败。
|
||||
func (l *Loop) compactIfNeeded(ctx context.Context, messages []model.Message) []model.Message {
|
||||
if l.maxContextTokens <= 0 || l.compactor == nil {
|
||||
return messages
|
||||
@@ -174,14 +174,14 @@ func (l *Loop) compactIfNeeded(ctx context.Context, messages []model.Message) []
|
||||
return compacted
|
||||
}
|
||||
|
||||
// extractMemory runs the extraction hook (design §6) over the current
|
||||
// conversation and persists any derived items. Errors are logged, never fatal, so
|
||||
// a bad extraction cannot abort the run.
|
||||
// extractMemory 在当前对话上运行提取钩子(design §6)
|
||||
// 并持久化导出的条目。错误会被记录日志,绝不致命,因此
|
||||
// 一次糟糕的提取无法中止 run。
|
||||
//
|
||||
// In a restricted context (group chat / stranger, !MemWritable) extraction is
|
||||
// skipped entirely: the conversation may read shared memory read-only and may
|
||||
// never persist anything that could leak owner-sensitive information (design/02
|
||||
// §3, design/11 §2).
|
||||
// 在受限上下文(群聊 / 陌生人,!MemWritable)中提取被
|
||||
// 完全跳过:该对话可以只读地读取共享记忆,但绝不
|
||||
// 持久化任何可能泄露主人敏感信息的东西(design/02
|
||||
// §3, design/11 §2)。
|
||||
func (l *Loop) extractMemory(ctx context.Context, messages []model.Message, step int) {
|
||||
env := sessionEnvFrom(ctx)
|
||||
if env.Restricted() {
|
||||
@@ -199,18 +199,18 @@ func (l *Loop) extractMemory(ctx context.Context, messages []model.Message, step
|
||||
l.persistMemories(ctx, items, step)
|
||||
}
|
||||
|
||||
// extractMemoryAsync triggers the extraction hook at the turn boundary without
|
||||
// blocking the reply (design/03 §1: 每轮对话后后台异步抽取). When the configured
|
||||
// extractor can extract asynchronously (the LLM extractor) the conversation is
|
||||
// handed to a background goroutine and this call returns immediately, so a slow
|
||||
// extraction model never delays the response. A deterministic extractor
|
||||
// (ManualExtractor) does not implement asyncExtractor and is run inline, which
|
||||
// keeps its cheap "记住 X" path observable and unchanged.
|
||||
// extractMemoryAsync 在 turn 边界触发提取钩子而不阻塞
|
||||
// 回复(design/03 §1: 每轮对话后后台异步抽取)。配置的
|
||||
// 提取器能异步提取(LLM extractor)时,对话被交给
|
||||
// 后台 goroutine,本调用立即返回,因此慢速
|
||||
// 提取模型绝不延迟响应。确定性提取器
|
||||
// (ManualExtractor)不实现 asyncExtractor,就地运行,这
|
||||
// 使其廉价的 "记住 X" 路径保持可观测且不变。
|
||||
//
|
||||
// The same gates as extractMemory apply: a restricted conversation is never
|
||||
// extracted (read-only isolation, design/02 §3 / design/11 §2), and a missing
|
||||
// extractor or store is a no-op. Extraction failures and store failures are
|
||||
// logged with the loop's source and never propagated.
|
||||
// 与 extractMemory 相同的门仍然适用:受限对话绝不
|
||||
// 被提取(只读隔离,design/02 §3 / design/11 §2),缺少
|
||||
// extractor 或 store 时为空操作。提取失败与 store 失败会
|
||||
// 带循环的来源记录日志,绝不向上传播。
|
||||
func (l *Loop) extractMemoryAsync(ctx context.Context, messages []model.Message, step int) {
|
||||
env := sessionEnvFrom(ctx)
|
||||
if env.Restricted() {
|
||||
@@ -225,12 +225,12 @@ func (l *Loop) extractMemoryAsync(ctx context.Context, messages []model.Message,
|
||||
l.extractMemory(ctx, messages, step)
|
||||
return
|
||||
}
|
||||
// The turn's context dies with the request while the extraction deliberately
|
||||
// outlives it, so the goroutine gets a detached copy of the context (values —
|
||||
// the session env among them — are kept) under its own timeout.
|
||||
// 该 turn 的 context 随请求消亡,而提取刻意比它活得
|
||||
// 更久,因此 goroutine 拿到一份脱离的 context 副本(值 ——
|
||||
// 其中包括会话环境 —— 被保留),并处于自己的超时之下。
|
||||
dctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), memoryExtractTimeout)
|
||||
// The caller keeps appending to messages as the turn continues, so the
|
||||
// goroutine reads a private snapshot instead of racing on the shared slice.
|
||||
// 随着该 turn 继续,调用方会不断向 messages 追加,因此
|
||||
// goroutine 读取一份私有快照,而不是在共享切片上竞争。
|
||||
snapshot := append([]model.Message(nil), messages...)
|
||||
results, err := async.ExtractAsync(dctx, snapshot)
|
||||
if err != nil {
|
||||
@@ -252,13 +252,13 @@ func (l *Loop) extractMemoryAsync(ctx context.Context, messages []model.Message,
|
||||
}()
|
||||
}
|
||||
|
||||
// persistMemories writes extracted items to the memory store, stamping every item
|
||||
// with the role that produced the turn. The assignment is unconditional on
|
||||
// purpose: the extractor is deliberately role-agnostic (it no longer holds a
|
||||
// persona baked in at construction, cmd/cyrene/extractor.go), so a multi-role
|
||||
// process would otherwise file every memory under one role. A blank role (no
|
||||
// persona configured at all) leaves the item as the extractor produced it. Store
|
||||
// failures are logged and never fatal, so one rejected item cannot abort a turn.
|
||||
// persistMemories 把提取出的条目写入记忆 store,为每个条目
|
||||
// 打上产出该 turn 的角色。该赋值刻意无条件:
|
||||
// extractor 刻意不感知角色(它不再持有一个在构造期
|
||||
// 内嵌的人格,cmd/cyrene/extractor.go),否则多角色
|
||||
// 进程会把每条记忆都归到一个角色下。角色为空(完全未配置
|
||||
// persona)时条目保持 extractor 产出时的样子。store
|
||||
// 失败会被记录日志,绝不致命,因此一条被拒的条目无法中止一个 turn。
|
||||
func (l *Loop) persistMemories(ctx context.Context, items []memory.MemoryItem, step int) {
|
||||
personaID := l.personaFor(ctx)
|
||||
for _, it := range items {
|
||||
@@ -271,8 +271,8 @@ func (l *Loop) persistMemories(ctx context.Context, items []memory.MemoryItem, s
|
||||
}
|
||||
}
|
||||
|
||||
// queryFromMessages derives a retrieval query from the conversation: the last
|
||||
// user turn's text, falling back to the last message's text, then "".
|
||||
// queryFromMessages 从对话导出检索查询:最后一个用户
|
||||
// turn 的文本,回退到最后一条消息的文本,再回退到 ""。
|
||||
func queryFromMessages(messages []model.Message) string {
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
if messages[i].Role == model.RoleUser {
|
||||
@@ -285,8 +285,8 @@ func queryFromMessages(messages []model.Message) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// messageText returns the concatenated plain-text payload of a message's text
|
||||
// parts, which is the shared view used to build a retrieval query.
|
||||
// messageText 返回一条消息各文本部件的拼接纯文本载荷,
|
||||
// 这是用于构建检索查询的共享视图。
|
||||
func messageText(m model.Message) string {
|
||||
var b strings.Builder
|
||||
for _, part := range m.Content {
|
||||
|
||||
Reference in new issue
Block a user