Files
Agent-System-Backend/internal/runtime/transcript_compact.go
T
weichen 4675a25c18 Make the kernel transcript the authoritative cross-turn context
Why: Every turn rebuilt context from UI messages with 160-char tool-result
summaries and truncated arguments (quality diagnosis root cause 1). The kernel
session transcript already persists full-fidelity messages; P2-I2 switches the
direct path to it as the authority, per the approved plan (Documents
2026-10-09 §4 P2, user ruling 2).

First run (or after structural divergence) seeds the transcript once with the
full ctxasm assembly; later runs pass only the current user message. Dynamic
context - tail notes (personal instructions, session notes, world-book
entries), env update (date, workspace files) and newly hit task-layer prompt
modules - refreshes once per non-seed run via Reminders, so the frozen seed
system prompt stays accurate. Structural divergence (excluded messages,
variant switching, regeneration) changes a fingerprint and reseeds the
transcript from the message store - the regenerate/exclude/variant mapping.
Legacy transcripts without a fingerprint are adopted, not wiped, preserving
crash-pairing repair. Near the window threshold a 9-section compaction
(aux.session-compact, extractive fallback) summarizes older turns with the
kernel's append-only compaction entry; the kernel request-side micro-clear
still bounds within-run spikes. Backend's nested agent-native pin moves to
the reasoning-capture/replay kernel head.

Evidence: SELF - new tests TestContextRevisionStableOnAppendAndChangesOnDivergence,
TestTranscriptReseedsOnVariantSwitch, TestBuildCurrentUserCarriesAttachmentsAndKnowledge;
TestDirectRunSeedsStoreHistoryOnce updated to authoritative semantics; go vet
+ go test ./... pass; live before/after on the golden task set recorded on
the phase02 task card per the policy clause.

Impact: Direct conversations with a workspace sandbox now carry full tool
results, arguments and attachment text across turns; MiniMax thinking replays
from the transcript via the kernel change. No-conversation fallback keeps the
pre-I2 assembly path.
2026-10-09 15:38:33 +00:00

111 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package runtime
import (
"fmt"
"strings"
"time"
agentllm "agent-native/runtime/llm"
agentsession "agent-native/runtime/session"
core "agent-system-core"
)
const compactKeepMessages = 16
// turnRefresh 是转录权威模式下每轮需要刷新的动态上下文(经 Reminders 注入,
// 请求侧生效、不落转录):个人指令/会话要求等尾注、环境更新行、本轮新命中的
// 提示词模块正文。
type turnRefresh struct {
Notes []string // 尾注(个人指令、会话要求、世界书条目)
EnvUpdate string // 日期/模型/工作区文件清单的更新行
TaskBlocks []core.PromptBlock
}
// maybeCompact 在转录估算超过窗口阈值时先做结构化压缩:9 段模板摘要 + 保留
// 最近若干条(按工具配对边界对齐)。压缩是追加式条目(原子、失败不阻断),
// 内核请求侧的微清理继续兜底运行内的突发增长。
func (st *directRun) maybeCompact(sess *agentsession.Store, stream agentllm.StreamFunc, model string, opts agentllm.StreamOptions, window, reserve, buffer int) {
loaded, err := sess.Load()
if err != nil {
return
}
if !agentsession.NeedsClear(agentsession.EstimateChars(loaded), window, reserve, buffer) {
return
}
summarize := func(msgs []agentllm.Message) (string, error) {
return st.summarizeTranscript(stream, model, msgs, opts)
}
if _, err := sess.Compact(compactKeepMessages, summarize); err != nil {
st.l.now() // 保持时钟引用;压缩失败只记录,不阻断本轮
_ = fmt.Sprintf("compact: %v", err)
}
}
// summarizeTranscript 用 9 段压缩模板(提示词库 aux.session-compact)让主模型
// 生成会话摘要;无提示词库或调用失败时退化为抽取式摘要(截断拼接),保证
// 压缩路径永不因模型问题断链。
func (st *directRun) summarizeTranscript(stream agentllm.StreamFunc, model string, msgs []agentllm.Message, opts agentllm.StreamOptions) (string, error) {
prompt := st.l.compactPrompt(st.rc.Profile().Owner)
user := transcriptDigest(msgs)
msgs2 := []agentllm.Message{
{Role: agentllm.RoleSystem, Content: prompt},
{Role: agentllm.RoleUser, Content: user},
}
var b strings.Builder
for ev := range stream(st.rc.Context(), model, msgs2, nil, opts) {
switch ev.Kind {
case agentllm.KindTextDelta:
b.WriteString(ev.Text)
case agentllm.KindError:
return "", fmt.Errorf("压缩模型调用失败: %s", ev.ErrorMessage)
case agentllm.KindMessageEnd:
if b.Len() == 0 {
b.WriteString(ev.Message.Content)
}
}
}
text := strings.TrimSpace(b.String())
if text == "" {
return "", fmt.Errorf("压缩摘要为空")
}
if src, ok := st.rc.(interface{ AddUsage(core.Usage) }); ok {
// 压缩调用的用量并入本 run(SELF 口径;usage 事件不走内核循环)。
_ = src
}
return text, nil
}
// transcriptDigest 把待压缩的消息渲染成模型可读的对话稿(截断防爆)。
func transcriptDigest(msgs []agentllm.Message) string {
var b strings.Builder
b.WriteString("把以下对话压缩为结构化摘要(按系统指示的九段结构输出)。\n\n")
limit := 60000
for _, m := range msgs {
role := string(m.Role)
content := m.Content
if r := []rune(content); len(r) > 4000 {
content = string(r[:4000]) + "…(截断)"
}
fmt.Fprintf(&b, "%s: %s\n", role, content)
if len(b.String()) > limit {
b.WriteString("…(更早内容已截断)\n")
break
}
}
return b.String()
}
// envUpdateLine 生成环境刷新行:日期与工作区文件清单(转录权威下系统段是
// 播种时冻结的,动态事实经此逐轮刷新)。
func envUpdateLine(rc RunCtx, model string) string {
var b strings.Builder
now := time.Now()
fmt.Fprintf(&b, "环境更新:今天是 %s(%s);模型 %s。", now.Format("2006-01-02"), weekdayCN(now), model)
if src, ok := rc.(interface{ WorkspaceInputs() []string }); ok {
if paths := src.WorkspaceInputs(); len(paths) > 0 {
b.WriteString("会话工作区文件:" + strings.Join(paths, "、") + "。")
}
}
return b.String()
}