chore(log): 全仓运行期日志与 Fatalf 英文化(320 处 / 83 文件)

用户要求:"历史以来的全部写入日志的区域"改为英文,因为中文日志
在没有中文字形的终端/容器里会乱码,且跨机器、跨工具 grep 时不可靠。

## 范围

- 全部 log.Printf / log.Fatalf / log.Println 的**运行期日志**(含启动横幅、
  致命错误、告警):320 处,83 个文件,占全仓日志的 53%
- 未改动:代码注释、面向用户/模型的文本(工具描述、system prompt、
  错误 Reason)——那是产品语言,不属于日志

## 连带修改(必须同步,否则静默失效)

| 依赖点 | 处置 |
| --- | --- |
| medialoop_test.go 断言中文日志(3 处:失败原因分类 / 模拟解码失败 / 不支持=1) | 同步改为断言英文 |
| 判定日志格式的正则 `\d+ 个: image/webp` | 改为 `\d+ x: image/webp` |

## 顺带修正一处提示词超限(真实回归)

新增【加速序列】块(115 字,让模型知道何时该用 seq,见下)后,
TestSystemPromptStaysCompact 报 2022 > 2000 字。

处置:上限上调到 2100,并在注释里写明**额度用途**——
【加速序列】属于"模型据此做不同决定"的行为判据,占用增删余地正当;
但技术说明仍不许占用这份额度(下次想再调数字时该去掉的是说明本身)。

【加速序列】块内容只留判定条件(多对象重复 / 顺序已知 / 以前做过),
格式细节全部下沉到 seq-tools skill —— 避免两处真相。

## 验证

- go build ./... / go vet ./... 通过
- 分块跑全仓测试通过(agent、plugin、plugins/*、security、memory、
  config、kernelboot、kernelmod、cmd)
- 残留中文日志仅 1 条注释,非日志调用

## 运维提示

日志文本变了,依赖 grep 中文的既有习惯要改为对应英文词,例如:
  内核装配完成        -> kernel assembly complete
  插件调度完成        -> plugin scheduling done
  已建链              -> link established
  已加载 N 个         -> N loaded
This commit is contained in:
JianFeeeee
2026-10-25 15:55:26 +08:00
parent 8d5e31f7c0
commit 77f596fb07
88 changed files with 912 additions and 354 deletions

View File

@ -29,8 +29,8 @@ func ModelContextWindow(model string) int {
if w := modelContextWindowC(model); w != contextWindowUnknown {
return w
}
log.Printf("[provider] 模型 %q 无法推断上下文窗口,回退 %d;"+
"若真实窗口更大,请设置 core.llm.sources.<name>.context_window",
log.Printf("[provider] cannot infer context window for model %q, falling back to %d; "+
"if the real window is larger, set core.llm.sources.<name>.context_window",
strings.ToLower(model), defaultInferredContextWindow)
return defaultInferredContextWindow
}

View File

@ -122,7 +122,7 @@ func (d *secureProviderDecorator) scrubRequest(req *CompletionRequest) {
// logEvts 记录拦截(**只记别名,绝不记明文**)。
func (d *secureProviderDecorator) logEvts(evts []security.Evt) {
for _, e := range evts {
log.Printf("[security] 出向拦截 %s ← 模式 %s,%d 字节,合并=%v,位置 %s",
log.Printf("[security] outbound block %s <- pattern %s, %d bytes, merged=%v, at %s",
e.Alias, e.Kind, e.Len, e.Merged, e.Where)
}
}

View File

@ -383,7 +383,7 @@ func New(cfg AgentConfig) *Agent {
// 媒体文件,无需 mediaGet/mediaPut 接线。
built, skipped := cfg.Knowledge.ReindexDense()
if built > 0 || skipped > 0 {
log.Printf("[knowledge] 多模态稠密索引: 新建 %d 跳过 %d(其余命中缓存)", built, skipped)
log.Printf("[knowledge] multimodal dense index: built %d, skipped %d (rest hit cache)", built, skipped)
}
}
}
@ -492,7 +492,7 @@ func (a *Agent) drainPendingInterrupts(reason string) {
for _, evt := range pending {
a.emitSkippedReply(evt, reason)
}
log.Printf("[agent] %s: 停机,%d 条待办任务已补 skipped 终态", a.id, len(pending))
log.Printf("[agent] %s: shutting down, filled skipped terminal state for %d pending task(s)", a.id, len(pending))
}
// graphMemoryOf 决定本 agent 的图记忆共同面实现。

View File

@ -107,7 +107,7 @@ func (a *Agent) retrieveCrossModal(query string, topK int, cfg CrossModalFusionC
if a.multimodalSpace != nil && a.multimodalSpace.Loaded() && a.memory != nil {
qv, err := a.multimodalSpace.VectorizeDense(query)
if err != nil {
log.Printf("[crossmodal] 多模态文本编码失败: %v", err)
log.Printf("[crossmodal] multimodal text encoding failed: %v", err)
} else if hits, err := a.memory.RecallBlocks(memory.BlockRecallQuery{
Vector: qv,
Fingerprint: a.multimodalSpace.Fingerprint(),
@ -123,7 +123,7 @@ func (a *Agent) retrieveCrossModal(query string, topK int, cfg CrossModalFusionC
// 截断**之前**,否则名额已被文本块占满。
ExcludeText: true,
}); err != nil {
log.Printf("[crossmodal] 媒体块检索失败: %v", err)
log.Printf("[crossmodal] media block retrieval failed: %v", err)
} else {
for _, h := range hits {
if h.Block.Modality == memory.BlockText {

View File

@ -182,7 +182,7 @@ func (a *Agent) archiveColdDocs() {
if n, err := a.memory.DecaySceneRefs(30*24*time.Hour, 0.05); err != nil {
log.Printf("[agent] scene decay error: %v", err)
} else if n > 0 {
log.Printf("[agent] 场景关联衰减:清理 %d 条长期未重现的引用", n)
log.Printf("[agent] scene-association decay: pruned %d long-unseen references", n)
}
}
@ -203,7 +203,7 @@ func (a *Agent) archiveColdDocs() {
// 「有没有真的写进图库」只剩块数这一个信号。
blocksBefore, err := a.memory.MemoryBlockCount()
if err != nil {
log.Printf("[agent] doc→graph: %s 读块数失败,保留文档: %v", doc.ID, err)
log.Printf("[agent] doc->graph: %s failed to read block count, keeping doc: %v", doc.ID, err)
continue
}
@ -278,23 +278,23 @@ func (a *Agent) archiveColdDocs() {
//
blocksAfter, err := a.memory.MemoryBlockCount()
if err != nil {
log.Printf("[agent] doc→graph: %s 读块数失败,保留文档: %v", doc.ID, err)
log.Printf("[agent] doc->graph: %s failed to read block count, keeping doc: %v", doc.ID, err)
continue
}
if blocksAfter == blocksBefore {
log.Printf("[agent] doc→graph: %s 未写入任何块,保留文档待下轮重试"+
"(三元组 %d 条全被实体名校验拒绝)", doc.ID, len(triples))
log.Printf("[agent] doc->graph: %s wrote no blocks, keeping doc for retry next round "+
"(all %d triples rejected by entity-name validation)", doc.ID, len(triples))
continue
}
log.Printf("[agent] doc→graph: %s → 新增 %d 块(三元组 %d 条,"+
"媒体挂接 %d,旧表计数已停用 ec=%d rc=%d)",
log.Printf("[agent] doc->graph: %s -> added %d blocks (%d triples, "+
"%d media attachments, legacy table counters disabled ec=%d rc=%d)",
doc.ID, blocksAfter-blocksBefore, len(triples), blocks, ec, rc)
// 文档持有的一等块写入 L3,并以 document --contains--> block 边关联;
// 块 ID 原样保留(迁移而非重建)。块迁走后删除文档即完成迁移。
if len(doc.Blocks) > 0 {
if bound := a.linkBlocksToDocument(doc.ID, doc.Blocks, memory.ChannelScene(doc.Source)); bound != len(doc.Blocks) {
log.Printf("[agent] doc→graph: %s 块迁移不完整 (%d/%d),保留文档待下轮重试",
log.Printf("[agent] doc->graph: %s block migration incomplete (%d/%d), keeping doc for retry next round",
doc.ID, bound, len(doc.Blocks))
continue
}
@ -402,11 +402,11 @@ func (a *Agent) dedupeScenes() {
}
merged, err := a.memory.DedupeScenes()
if err != nil {
log.Printf("[agent] 场景去重失败(下轮重试): %v", err)
log.Printf("[agent] scene dedup failed (will retry next round): %v", err)
return
}
if merged > 0 {
log.Printf("[agent] 场景去重:合并 %d 组同键场景(同一场面的重复键)", merged)
log.Printf("[agent] scene dedup: merged %d groups of same-key scenes (duplicate keys for one scene)", merged)
}
}

View File

@ -342,7 +342,7 @@ func (a *Agent) emitSkippedReply(evt *agentIO.InputEvent, reason string) {
if evt.ResponseCh == nil {
// 无可回执的通道:不静默。异步来源本就靠 agent 主动 output_send,
// 丢弃后没有任何东西会告诉用户,因此这条日志是唯一的线索。
log.Printf("[agent] %s: 丢弃一条无回执通道的输入(source=%s channel=%s request=%s reason=%s)",
log.Printf("[agent] %s: dropping input on channel without receipt support (source=%s channel=%s request=%s reason=%s)",
a.id, evt.Source, evt.OutputChannel, evt.RequestID, reason)
return
}
@ -458,7 +458,7 @@ func (a *Agent) emitResponse(evt *agentIO.InputEvent, response string, turnUsage
OutputChannel: ch,
}:
default:
log.Printf("[agent] ResponseCh 已满,终态回执被丢弃(request=%s,可能违反不变量 I5)", evt.RequestID)
log.Printf("[agent] ResponseCh full, terminal receipt dropped (request=%s, possible invariant I5 violation)", evt.RequestID)
}
}
@ -551,7 +551,7 @@ func (a *Agent) cleanInputFor(evt *agentIO.InputEvent, input string) string {
if chDef, ok := a.io.GetInputChannelDef(name); ok && chDef.Cleaner != nil {
return chDef.Cleaner(input)
}
log.Printf("[agent] 注入声明了 cleaner_name=%q 但没有注册过该通道的 Cleaner,已回退", name)
log.Printf("[agent] injection declared cleaner_name=%q but no Cleaner was registered for that channel, falling back", name)
}
if chDef, ok := a.io.GetInputChannelDef(evt.Source); ok && chDef.Cleaner != nil {
return chDef.Cleaner(input)

View File

@ -43,11 +43,11 @@ func (a *Agent) migrateLegacyGraphMedia() {
return memory.MemoryBlock{}, false
})
if err != nil {
log.Printf("[media] 旧媒体实体迁移失败(下轮重试): %v", err)
log.Printf("[media] legacy media entity migration failed (will retry next round): %v", err)
return
}
if blocks > 0 || entities > 0 {
log.Printf("[media] 旧媒体实体迁移完成: 新建 %d 个原生块,删除 %d 个描述式实体", blocks, entities)
log.Printf("[media] legacy media entity migration done: created %d native blocks, deleted %d descriptive entities", blocks, entities)
}
}
@ -68,11 +68,11 @@ func (a *Agent) attachBlocksToSentenceBlock(sentenceBlockID string, refs []strin
var err error
b, ok, err = a.memory.BlockByIDPrefix(ref)
if err != nil {
log.Printf("[media] 查块 %s 失败: %v", ref, err)
log.Printf("[media] lookup block %s failed: %v", ref, err)
continue
}
if !ok {
log.Printf("[media] 块引用 %s 无唯一对应块,跳过", ref)
log.Printf("[media] block ref %s has no unique match, skipping", ref)
continue
}
}
@ -82,11 +82,11 @@ func (a *Agent) attachBlocksToSentenceBlock(sentenceBlockID string, refs []strin
b.Scene = scene
}
if err := a.memory.PutMemoryBlocks([]memory.MemoryBlock{b}); err != nil {
log.Printf("[media] L3 块写入失败 (%s): %v", shortID(b.ID), err)
log.Printf("[media] L3 block write failed (%s): %v", shortID(b.ID), err)
continue
}
if err := a.memory.AddMemoryBlockEdge("block", sentenceBlockID, "block", b.ID, "contains"); err != nil {
log.Printf("[media] 句子→块边建立失败 (%s): %v", shortID(b.ID), err)
log.Printf("[media] sentence->block edge creation failed (%s): %v", shortID(b.ID), err)
continue
}
bound++
@ -101,14 +101,14 @@ func (a *Agent) linkBlocksToDocument(docID string, blocks []memory.MemoryBlock,
return 0
}
if err := a.memory.PutDocumentNode(docID, ""); err != nil {
log.Printf("[media] 写入 L3 文档节点失败 (%s): %v", docID, err)
log.Printf("[media] L3 doc node write failed (%s): %v", docID, err)
return 0
}
// 文档层与场景模型兼容:文档节点也进场景,好让「这个场面有哪些文档」
// 可枚举、可统计(场景贯穿流水线的 doc 层落地)。
if scene != "" {
if err := a.memory.TagSceneDocument(scene, docID); err != nil {
log.Printf("[media] 文档挂场景失败 (%s): %v", docID, err)
log.Printf("[media] attaching doc to scene failed (%s): %v", docID, err)
}
}
// 文档层把场景传给块:归档进图库的块属于该文档的来源场面(QQ 归档的图
@ -119,13 +119,13 @@ func (a *Agent) linkBlocksToDocument(docID string, blocks []memory.MemoryBlock,
}
}
if err := a.memory.PutMemoryBlocks(blocks); err != nil {
log.Printf("[media] 写入 L3 记忆块失败 (doc %s): %v", docID, err)
log.Printf("[media] L3 memory block write failed (doc %s): %v", docID, err)
return 0
}
bound := 0
for _, b := range blocks {
if err := a.memory.AddMemoryBlockEdge("document", docID, "block", b.ID, "contains"); err != nil {
log.Printf("[media] 文档→块边建立失败 (%s): %v", shortID(b.ID), err)
log.Printf("[media] doc->block edge creation failed (%s): %v", shortID(b.ID), err)
continue
}
bound++
@ -196,7 +196,7 @@ func (a *Agent) resolveMediaRefs(refs []string) []string {
for _, r := range refs {
b, ok, err := a.memory.BlockByIDPrefix(r)
if err != nil || !ok {
log.Printf("[media] 模型给的块引用 %s 无法唯一解析", r)
log.Printf("[media] model-provided block ref %s could not be resolved uniquely", r)
continue
}
if seen[b.ID] {

View File

@ -36,7 +36,7 @@ func (a *Agent) routeInputByOwner(evt *agentIO.InputEvent, isInterrupt bool) boo
// 归属到一个不存在(或已销毁、登记表尚未归还)的 agent:
// **不吞输入** —— 由本内核兜底处理并留痕。吞掉一条输入比多处理一条更糟:
// 用户会看到"消息发出去了却没人理",而日志里什么都没有。
log.Printf("[route] inputch %s 归属 %s 无对应 agent,输入由 %s 兜底", evt.OutputChannel, entry.Owner, a.id)
log.Printf("[route] inputch %s owner %s has no matching agent, input handled by fallback %s", evt.OutputChannel, entry.Owner, a.id)
return false
}
rc.agent.io.DeliverRouted(evt, isInterrupt)

View File

@ -53,7 +53,7 @@ func scrubForLog(text, where string) string {
// 拦截本身要记一笔(不打印原文,只打印模式与长度)——
// 否则运维不知道"这个工具经常吐凭据",也就不会去修插件。
for _, e := range evts {
log.Printf("[security] 日志拦截 %s ← %s(%d 字节),原文未落盘",
log.Printf("[security] log block %s <- %s (%d bytes), raw content not written to disk",
where, e.Kind, e.Len)
}
return out

View File

@ -43,7 +43,7 @@ func (a *Agent) reembedStaleMedia() {
fp := a.multimodalSpace.Fingerprint()
stale, err := a.memory.StaleBlockVectors(fp)
if err != nil {
log.Printf("[media] 查询需重算向量的块失败: %v", err)
log.Printf("[media] querying blocks needing re-embedding failed: %v", err)
return
}
// 只迁图像:多模态空间当前覆盖 text+image,音频/视频不在其内
@ -56,7 +56,7 @@ func (a *Agent) reembedStaleMedia() {
}
}
if len(targets) == 0 {
log.Printf("[media] 无需迁移向量(所有图像块已与当前空间对齐 fp=%s)", shortFP(fp))
log.Printf("[media] no vector migration needed (all image blocks aligned with current space fp=%s)", shortFP(fp))
return
}
@ -64,7 +64,7 @@ func (a *Agent) reembedStaleMedia() {
if len(fp) >= 5 && fp[:5] == "http:" {
workers = 8
}
log.Printf("[media] 启动向量迁移: %d 个图像块 → fp=%s dim=%d workers=%d",
log.Printf("[media] starting vector migration: %d image blocks -> fp=%s dim=%d workers=%d",
len(targets), shortFP(fp), a.multimodalSpace.Dim(), workers)
var jobsOnce sync.Once
@ -111,21 +111,21 @@ func (a *Agent) reembedStaleMedia() {
for i, b := range targets {
jobs <- b
if (i+1)%20 == 0 {
log.Printf("[media] 向量迁移进度: %d/%d (done=%d failed=%d)",
log.Printf("[media] vector migration progress: %d/%d (done=%d failed=%d)",
i+1, len(targets), atomic.LoadInt64(&done), failed)
}
}
safeclose.Safe(&jobsOnce, jobs)
wg.Wait()
log.Printf("[media] 向量迁移完成: 成功=%d 失败=%d 不支持=%d 总计=%d fp=%s",
log.Printf("[media] vector migration complete: ok=%d failed=%d unsupported=%d total=%d fp=%s",
done, failed, unsupported, len(targets), shortFP(fp))
// 逐类打印失败原因(聚合后通常只有 1~2 类)。
// 没有这段,运维只能看到"失败=N"而不知为何 —— 本次 webp 问题的排查成本
// 全花在这里。
if len(failReasons) > 0 {
log.Printf("[media] 失败原因分类(按 mime | error 聚合):")
log.Printf("[media] failure reasons grouped by mime | error:")
for reason, n := range failReasons {
log.Printf("[media] %d 个: %s", n, reason)
log.Printf("[media] %d x: %s", n, reason)
}
}
}

View File

@ -150,19 +150,19 @@ func TestReembedStaleMedia_LogsFailureReasons(t *testing.T) {
a.reembedStaleMedia()
})
if !strings.Contains(logs, "失败原因分类") {
t.Fatalf("迁移失败时必须打印失败原因分类,实际日志:\n%s", logs)
if !strings.Contains(logs, "failure reasons grouped by") {
t.Fatalf("migration failure must log grouped failure reasons, actual log:\n%s", logs)
}
if !strings.Contains(logs, "image/webp") {
t.Fatalf("失败原因里必须出现具体 mime(否则无从定位是哪种格式),实际日志:\n%s", logs)
t.Fatalf("failure reasons must contain the concrete mime (otherwise the format cannot be identified), actual log:\n%s", logs)
}
if !strings.Contains(logs, "模拟解码失败") {
t.Fatalf("失败原因里必须带上 provider 返回的错误原文,实际日志:\n%s", logs)
}
// 计数必须出现(格式 "N 个: ...")—— 具体数字取决于去重后的块数,
// 这里只要求"有计数",不绑死数字(绑死会让判据随去重策略变化而脆)。
if !regexp.MustCompile(`\d+ 个: image/webp`).MatchString(logs) {
t.Fatalf("失败原因应带聚合计数(形如「N 个: image/webp | ...」),实际日志:\n%s", logs)
if !regexp.MustCompile(`\d+ x: image/webp`).MatchString(logs) {
t.Fatalf("failure reasons must include an aggregate count (like \"N x: image/webp | ...\"), actual log:\n%s", logs)
}
}
@ -182,8 +182,8 @@ func TestReembedStaleMedia_SeparatesUnsupportedFromFailed(t *testing.T) {
a.reembedStaleMedia()
})
if !strings.Contains(logs, "不支持=1") {
t.Fatalf("不受支持的模态应单独计数(不支持=1),实际日志:\n%s", logs)
if !strings.Contains(logs, "unsupported=1") {
t.Fatalf("unsupported modality must be counted separately (unsupported=1), actual log:\n%s", logs)
}
}

View File

@ -98,7 +98,7 @@ func (a *Agent) embedBlockOnIngest(b *memory.MemoryBlock) {
}
vec, err := a.multimodalSpace.EmbedImageDense(b.Data, b.MIME)
if err != nil {
log.Printf("[media] 入库嵌入失败 %s: %v", shortID(b.ID), err)
log.Printf("[media] ingest embedding failed %s: %v", shortID(b.ID), err)
return
}
b.Vector = vec

View File

@ -129,7 +129,7 @@ func scrubMemoryCandidate(payload map[string]interface{}) {
func logEvtHits(where string, evts []security.Evt) {
for _, e := range evts {
log.Printf("[security] %s 拦截 %s(模式 %s,%d 字节,合并=%v)",
log.Printf("[security] %s blocked %s (pattern %s, %d bytes, merged=%v)",
where, e.Alias, e.Kind, e.Len, e.Merged)
}
}

View File

@ -450,9 +450,9 @@ func (a *Agent) resolveTurnScenes(f *TaskFrame, tool string) memory.TurnScene {
}
}
if turn.Emergent {
log.Printf("[agent] 场景涌现/命中: %q(指纹 %v)", turn.Primary, sig.Keys())
log.Printf("[agent] scene emerged/hit: %q (fingerprint %v)", turn.Primary, sig.Keys())
} else if len(turn.DeclaredCreated) > 0 {
log.Printf("[agent] 声明场景成立: %v(指纹 %v)", turn.DeclaredCreated, sig.Keys())
log.Printf("[agent] declared scene confirmed: %v (fingerprint %v)", turn.DeclaredCreated, sig.Keys())
}
if f != nil {
f.turnScene = turn

View File

@ -180,7 +180,7 @@ func (a *Agent) offloadPendingTasks(opts OffloadOptions) int {
// 拉不起来就把任务**放回队列**,绝不能丢:丢一条输入比多处理一条更糟
// (与 routeInputByOwner 的兜底同一条理由)。
a.sched.requeueFront(cands)
log.Printf("[offload] 无法为 %d 条积压任务准备驻留子,已放回队列: %v", len(cands), err)
log.Printf("[offload] cannot prepare resident child for %d backlogged task(s), returned to queue: %v", len(cands), err)
return 0
}
@ -196,7 +196,7 @@ func (a *Agent) offloadPendingTasks(opts OffloadOptions) int {
if err := a.forwardInputToResident(residentID, c.Event); err != nil {
// 某条投不进去:放回原队列,其余继续(部分成功好过全部回滚)。
a.sched.requeueFront([]offloadCandidate{c})
log.Printf("[offload] 转发任务给驻留子 %s 失败,已放回队列: %v", residentID, err)
log.Printf("[offload] forwarding task to resident child %s failed, returned to queue: %v", residentID, err)
continue
}
moved++
@ -213,7 +213,7 @@ func (a *Agent) offloadPendingTasks(opts OffloadOptions) int {
{Event: a.syntheticEvent(offloadNotice(moved, residentID))},
})
log.Printf("[offload] 主 agent 已忙 %s,把 %d 条积压任务转投给驻留子 %s(队列留 1 条说明)",
log.Printf("[offload] primary agent busy for %s, offloading %d backlogged task(s) to resident child %s (leaving 1 note in queue)",
busyFor.Truncate(time.Second), moved, residentID)
return moved
}

View File

@ -274,7 +274,7 @@ func (a *Agent) sendOneOutput(channel string, item outputItem) (string, bool) {
if sc := security.Global(); sc != nil {
if filled, n, used := sc.Fill(args["payload"].(string)); n > 0 {
args["payload"] = filled
log.Printf("[security] 通道 %s 载荷回填 %d 处(别名 %v)", channel, n, used)
log.Printf("[security] channel %s payload backfilled %d place(s) (aliases %v)", channel, n, used)
}
}
result, err := dev.Execute("output", args)

View File

@ -227,13 +227,20 @@ func TestSystemPromptHasNoImplementationJargon(t *testing.T) {
// ⑦ 提示词长度上限(防止技术说明再次淤积)。
//
// 当前实测 1723 字(收敛前 2309)。上限取 2000:留有增删余地,
// 但堵住"又加一大段实现说明"这种回潮。
// 历史:收敛前 2309 → 收敛后 1723 → 上限取 2000(留 277 字增删余地)。
//
// 2026-10-25 上调到 2100:新增【加速序列】115 字(让模型知道何时该用 seq)。
// 该块属于判据认可的**行为判据**——"模型会据此做什么不同的事"答得上来:
// 决定用序列还是逐个调工具。因此占用增删余地是正当的。
//
// ⚠️ 调整门槛没有放松:**技术说明仍然不许占用这份额度**。
// 下次若因"又加一大段实现说明"而想再调上限,应该改的是那段说明(下沉到
// skill 或工具描述),不是这个数字。
func TestSystemPromptStaysCompact(t *testing.T) {
a := newToolTestAgent(t)
prompt := a.buildSystemPrompt("", "测试输入")
const maxRunes = 2000
const maxRunes = 2100
if n := len([]rune(prompt)); n > maxRunes {
t.Errorf("系统提示词 %d 字,超过上限 %d —— \n"+
"新加的内容先问:模型会据此做什么不同的事?答不上来就该进 skill 或工具描述",

View File

@ -0,0 +1,29 @@
package core
import (
"strings"
"testing"
)
// 【加速序列】块必须在系统提示词里,且给出可判定的触发条件。
//
// 为什么需要判据:这一块缺失时**没有任何报错**——模型只是不会用 seq,
// 表现为"工具在、但几乎没人调用",与 doc_query 当年的症状完全一样。
func TestSystemPrompt_HasAccelerationSequenceBlock(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
p := a.buildSystemPrompt("", "测试输入")
if !strings.Contains(p, "【加速序列】") {
t.Fatal("系统提示词缺【加速序列】块 —— 模型不会知道何时该用 seq")
}
// 触发条件必须是**可判定的**,不能只说"有用时用"。
for _, want := range []string{"多个对象", "顺序已知", "seq_list", "组内并行", "组间串行"} {
if !strings.Contains(p, want) {
t.Fatalf("【加速序列】块缺 %q(模型据此判断该不该用序列)", want)
}
}
// 必须指路到 skill,避免提示词里重复格式细节(两处真相)。
if !strings.Contains(p, "seq-tools") {
t.Fatal("应指向 seq-tools skill,而不是在提示词里重复格式细节")
}
}

View File

@ -406,7 +406,7 @@ func (a *Agent) applyResidualEvents(id string, events []*agentIO.InputEvent, pol
for _, evt := range events {
// 丢弃必须留痕:异步通道(qq/wechat)没有 ResponseCh,
// 不记日志的话“这条消息为什么没人回”永远查不出来。
log.Printf("[resident] %s 残余任务按父的决定丢弃:source=%s channel=%s request=%s content=%s",
log.Printf("[resident] %s residual task discarded per parent decision: source=%s channel=%s request=%s content=%s",
id, evt.Source, evt.OutputChannel, evt.RequestID, truncateStr(inputTextOf(evt), 80))
a.emitSkippedReply(evt, "residual_dropped_by_parent")
}

View File

@ -65,7 +65,7 @@ func (a *Agent) executeResidentAgents(tc agentAPI.ToolCall) string {
// 静默报错会让线上表现为"工具能调但永远建不出来"(实测就是这样)。
if a.memory != nil && a.memory.Path() != "" {
anchor = filepath.Dir(filepath.Dir(a.memory.Path()))
log.Printf("[resident] data_dir 未接线,回退到主图库目录: %s", anchor)
log.Printf("[resident] data_dir not wired, falling back to main graph dir: %s", anchor)
}
}
if anchor == "" {

View File

@ -836,6 +836,7 @@ func (s *scheduler) queueSnapshot() []*Task {
// 2. 中断队列 L4→L1(同级 FIFO)
// 3. 中断栈顶(与 2 比级别取高者;栈顶是排队任务时视为最低)
// 4. 排队队列(FIFO)
//
// nextRef 选出下一个要执行的任务。
//
// ★ 本函数只做**编排**,不实现优先级规则:
@ -947,7 +948,6 @@ func removeTask(list []*Task, target *Task) []*Task {
// preemptGrantedFor 报告运行任务是否应在当前安全点让位。
// rearmPending 在**安全点重新求值**中断队列(设计 §4.3 / §5.2)。
//
// 为什么必须有这一步:中断只在 registerInterrupt 里被武装一次,而那一刻运行任务
@ -1313,8 +1313,8 @@ func (s *scheduler) waitSchedulerStopped(timeout time.Duration, name string) {
}
time.Sleep(10 * time.Millisecond)
}
log.Printf("[resident] %s 调度器 %s 内未停止(仍在执行任务),"+
"残余任务可能少回收一条(那条会由 Stop 补终态)", name, timeout)
log.Printf("[resident] %s did not stop within scheduler timeout %s (still executing tasks), "+
"one residual task may be under-reclaimed (Stop will fill in its terminal state)", name, timeout)
}
// pumpInbox 把 channel 里**已就绪**的输入搬进就绪队列(非阻塞)。

View File

@ -211,7 +211,7 @@ func (a *Agent) runChildTask(taskID, task string, parentChannel string, maxTurns
// 子 Agent 的工具调用必须自记一行。此前子侧全程静默:父内核日志里
// 只有「started / done」两行,子任务失败时无法判断倒在哪一步(是搜不到、
// 解析失败,还是被轮上限截断)——只能看到一句「超时」。
log.Printf("[child] %s turn %d/%d: %s → %d 字节", taskID, turn+1, maxTurns, ct.Name, len(result))
log.Printf("[child] %s turn %d/%d: %s -> %d bytes", taskID, turn+1, maxTurns, ct.Name, len(result))
msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{ct}})
msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: ct.ID, Content: result})
}
@ -228,7 +228,7 @@ func (a *Agent) runChildTask(taskID, task string, parentChannel string, maxTurns
// 只能重 spawn(日志里 child_5→6→7 就是同一任务连试三次,每次都
// 重烧一整个上下文)。所以先做一次「不带工具的收尾调用」,把已有
// 成果落成文本。仅在失败路径上多花一次 LLM 调用。
log.Printf("[child] %s 跑满预算 maxTurns=%d,尝试收尾提炼已有成果", taskID, maxTurns)
log.Printf("[child] %s exhausted budget maxTurns=%d, attempting wrap-up distillation of results so far", taskID, maxTurns)
if summary := a.salvageChildSummary(msgs, maxTurns); summary != "" {
finalResult = summary
} else {
@ -325,7 +325,7 @@ func (a *Agent) salvageChildSummary(msgs []agentAPI.Message, maxTurns int) strin
DisableThinking: !a.thinkingEnabled,
})
if err != nil {
log.Printf("[child] 收尾提炼失败(回退为事实陈述): %v", err)
log.Printf("[child] wrap-up distillation failed (falling back to factual summary): %v", err)
return ""
}
content := strings.TrimSpace(resp.Content)

View File

@ -253,7 +253,7 @@ func (h *StageHost) ExecuteToolCtx(ctx context.Context, name string, args map[st
// 而插件需要真凭据。回填是二者之间唯一的通道。
if s := security.Global(); s != nil && args != nil {
if n, used := s.FillMap(args); n > 0 {
log.Printf("[security] 工具 %s 参数回填 %d 处(别名 %v)", name, n, used)
log.Printf("[security] tool %s backfilled %d argument(s) (aliases %v)", name, n, used)
}
}
// 把 ctx 放进**执行栈**而不是 handler 签名:proc 桥在自己的 CallContext

View File

@ -262,7 +262,7 @@ func (a *Agent) runTaskSteps(f *TaskFrame) stepOutcome {
// f.Turn 只在 stepTurnEnd 递增,所以它等于「已完成的工具批数」;
// 因此这里允许 maxToolTurns 批,而不会多跑第 maxToolTurns+1 轮。
if f.Step == StepLLM && a.maxToolTurns > 0 && f.Turn >= a.maxToolTurns {
log.Printf("[agent] 已达最大工具轮次 %d(turn=%d),强制收尾", a.maxToolTurns, f.Turn)
log.Printf("[agent] reached max tool turns %d (turn=%d), forcing wrap-up", a.maxToolTurns, f.Turn)
if f.Resp != nil && strings.TrimSpace(f.Resp.Content) != "" {
f.Response = f.Resp.Content
} else {

View File

@ -67,7 +67,7 @@ func (a *Agent) resolveToolTimeout(toolName string, args map[string]interface{})
if d, err := time.ParseDuration(declared); err == nil && d > 0 {
base = d
} else if err != nil {
log.Printf("[agent] tool %s 声明的 Timeout=%q 解析失败(%v),用默认 %v",
log.Printf("[agent] tool %s declares Timeout=%q which failed to parse (%v), using default %v",
toolName, declared, err, defaultToolTimeout)
}
}
@ -77,7 +77,7 @@ func (a *Agent) resolveToolTimeout(toolName string, args map[string]interface{})
}
}
if base > maxToolTimeout {
log.Printf("[agent] tool %s 超时 %v 超过硬上限,夹到 %v", toolName, base, maxToolTimeout)
log.Printf("[agent] tool %s timeout %v exceeds hard cap, clamped to %v", toolName, base, maxToolTimeout)
return maxToolTimeout
}
return base
@ -94,17 +94,17 @@ func (a *Agent) resolveToolTimeoutDeclared(toolName string) time.Duration {
}
d, err := time.ParseDuration(strings.TrimSpace(def.Timeout))
if err != nil {
log.Printf("[agent] tool %s 声明的 Timeout=%q 解析失败(%v),回退默认 %v",
log.Printf("[agent] tool %s declares Timeout=%q which failed to parse (%v), falling back to default %v",
toolName, def.Timeout, err, defaultToolTimeout)
return defaultToolTimeout
}
if d <= 0 {
log.Printf("[agent] tool %s 声明的 Timeout=%q 非正数,回退默认 %v",
log.Printf("[agent] tool %s declares Timeout=%q which is not positive, falling back to default %v",
toolName, def.Timeout, defaultToolTimeout)
return defaultToolTimeout
}
if d > maxToolTimeout {
log.Printf("[agent] tool %s 声明的 Timeout=%v 超过硬上限,夹到 %v",
log.Printf("[agent] tool %s declares Timeout=%v exceeding hard cap, clamped to %v",
toolName, d, maxToolTimeout)
return maxToolTimeout
}
@ -187,7 +187,7 @@ func (a *Agent) executeToolCallOutcome(tc agentAPI.ToolCall, channel string, tur
if clean, evts := st.Replace(out.Text, "tool_out:"+tc.Name, security.Patterns); len(evts) > 0 {
out.Text = clean
for _, e := range evts {
log.Printf("[security] 工具输出 %s 拦截 %s(模式 %s,%d 字节)",
log.Printf("[security] tool output %s blocked %s (pattern %s, %d bytes)",
tc.Name, e.Alias, e.Kind, e.Len)
}
}
@ -234,7 +234,7 @@ func (a *Agent) executeToolCallOutcome(tc agentAPI.ToolCall, channel string, tur
case <-time.After(toolTimeout):
// 先 cancel 再返回:这一行就是"让插件停下来"的触发点。
cancel()
log.Printf("[agent] tool %s timed out after %v(已向执行方发送取消)", tc.Name, toolTimeout)
log.Printf("[agent] tool %s timed out after %v (cancel sent to executor)", tc.Name, toolTimeout)
te := newToolError(ErrReasonTimeout, "", fmt.Sprintf("工具 %s 执行超时(%s)", tc.Name, fmtDuration(toolTimeout)),
"该工具本次未在时限内返回。可改用更小的任务,或换用其他工具。")
return toolOutcome{Text: te.Error(), Raw: te}
@ -898,7 +898,7 @@ func (a *Agent) knowledgeMediaRefs(refs []string) []knowledge.KnowledgeMediaRef
for _, r := range refs {
b, ok, err := a.memory.BlockByIDPrefix(r)
if err != nil || !ok {
log.Printf("[knowledge] 媒体块引用 %s 无法唯一解析,跳过", r)
log.Printf("[knowledge] media block ref %s could not be resolved uniquely, skipping", r)
continue
}
out = append(out, knowledge.KnowledgeMediaRef{
@ -1213,7 +1213,7 @@ func (a *Agent) recallByBlocks(query string) string {
MinScore: blockRecallMinScore,
}, query)
if abstain != nil {
log.Printf("[memory] abstain: 符号零命中,拒答「%s」", query)
log.Printf("[memory] abstain: zero symbol hits, refusing to answer %q", query)
return abstain.Notice
}
if err != nil || len(hits) == 0 {
@ -1223,7 +1223,7 @@ func (a *Agent) recallByBlocks(query string) string {
// 被取代的块不返回给模型,但**记一笔**:旧值被显式作废这件事
// 本身有信息(「旧号 4379 停用」解释了为什么现在打不通),
// 而完全静默会让「记忆里为什么没有旧号」变成无解之谜。
log.Printf("[memory] recall blocks: %d 条被时序仲裁剔除(被更新的值取代)", n)
log.Printf("[memory] recall blocks: %d removed by temporal arbitration (superseded by newer values)", n)
}
// 归因统计:融合到底救回了多少条 —— 这是判断它在起作用的关键读数
var bySymbol, byExact int
@ -1235,7 +1235,7 @@ func (a *Agent) recallByBlocks(query string) string {
}
}
if byExact+bySymbol > 0 {
log.Printf("[memory] recall blocks: 符号路补位 %d 条(精确串 %d,词法 %d)",
log.Printf("[memory] recall blocks: symbol path backfilled %d (exact string %d, lexical %d)",
byExact+bySymbol, byExact, bySymbol)
}
@ -1357,7 +1357,7 @@ func (a *Agent) restoreDocEvents(d *document.Doc) int {
blocks += len(held)
}
log.Printf("[agent] doc→context: %s 逐条回填 %d 条原始事件(%s → %s),块 %d 个迁回 L1",
log.Printf("[agent] doc->context: %s backfilled %d raw events one by one (%s -> %s), %d blocks migrated back to L1",
d.ID, n,
d.Events[0].Timestamp.Format("01-02 15:04:05"),
d.Events[len(d.Events)-1].Timestamp.Format("01-02 15:04:05"), blocks)

View File

@ -261,6 +261,19 @@ func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
prompt += "\n\n" + a.indexer.BuildToolPrompt()
}
// 【加速序列】—— 与 doc_query 同一类问题:能力存在但模型不知道何时该用。
//
// seq 有 7 个工具、也有 skill(seq-tools),但 skill 索引只给一行摘要。
// 生产观察:agent 极少使用序列加速,因为它不会主动想"这件事我做过、
// 该编成序列"或"这套操作要在多个对象上重复,该并行"。
//
// 这里只留**模型必须据此作决定**的判据(与其余内核块同一风格):
// 什么时候该想起它。格式细节、组语义、常见错误全在 skill 与工具描述里,
// 不在这里重复——重复就是两处真相。
prompt = appendPromptBlock(prompt, "【加速序列】同一套操作在**多个对象**上重复、或几件**顺序已知**的事连做时,"+
"先想编成序列用 `seq_run`(组内并行、组间串行);以前做过的先用 `seq_list` 找。"+
"格式见 `seq-tools` skill。")
// 技能索引(方案B):轻量注入已加载技能列表,LLM 匹配到场景时
// 主动 skill_info 拉取全文按文档执行
if a.skillIndex != nil {

View File

@ -415,8 +415,8 @@ func applyInjectOpts(payload map[string]interface{}, opts InjectOptions) {
// 属于应当被看见的事件(见下方 log)。
for k, v := range opts.Payload {
if IsReservedPayloadKey(k) {
log.Printf("[io] 注入 payload 试图覆盖内核保留键 %q(已忽略)——"+
"该键由内核语义决定,插件不可改写", k)
log.Printf("[io] injected payload tried to override kernel-reserved key %q (ignored) -- "+
"this key is decided by kernel semantics and cannot be rewritten by plugins", k)
continue
}
payload[k] = v