Files
Agent-System-Backend/internal/runtime/transcript_seed_test.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

114 lines
4.3 KiB
Go

package runtime
import (
"context"
"path/filepath"
"strings"
"testing"
agentllm "agent-native/runtime/llm"
agentsession "agent-native/runtime/session"
"agent-system-backend/internal/ctxasm"
core "agent-system-core"
)
func textPart(s string) core.Message {
return core.Message{Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: s}}}
}
// 指纹只随结构改写变化:正常追加不动,排除/变体切换必动。
func TestContextRevisionStableOnAppendAndChangesOnDivergence(t *testing.T) {
base := []core.Message{
{ID: "m1"}, {ID: "m2"},
}
r0 := contextRevision(base)
if r0 != contextRevision(append(append([]core.Message{}, base...), core.Message{ID: "m3"})) {
t.Fatal("append must not change revision")
}
excluded := append([]core.Message{}, base...)
excluded[1].Excluded = true
if contextRevision(excluded) == r0 {
t.Fatal("exclude must change revision")
}
variant := append(append([]core.Message{}, base...), core.Message{ID: "m3", VariantGroup: "g1", VariantIndex: 1})
if contextRevision(variant) == r0 {
t.Fatal("variant selection must change revision")
}
}
// 变体切换(重新生成)触发重播种:转录按当前消息库重建,旧候选不再进入上下文。
func TestTranscriptReseedsOnVariantSwitch(t *testing.T) {
sandbox := t.TempDir()
hist := []core.Message{
{ID: "m1", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "写纪要"}}},
{ID: "m2", Role: core.RoleAssistant, VariantGroup: "g1", VariantIndex: 0, Parts: []core.Part{{Type: core.PartText, Text: "旧候选"}}},
}
var requests [][]agentllm.Message
base := scriptStream(textTurns("新候选", "再来一版"), nil)
r := &LLMRunner{Stream: func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event {
requests = append(requests, append([]agentllm.Message{}, msgs...))
return base(ctx, model, msgs, tools, opts)
}}
rc := &fakeRunCtx{ctx: context.Background(), sandbox: sandbox,
prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, history: hist}
if err := r.Execute(rc); err != nil {
t.Fatalf("first Execute: %v", err)
}
if !strings.Contains(joinReq(requests[0]), "旧候选") {
t.Fatalf("seed run must include the then-current variant: %s", joinReq(requests[0]))
}
// 用户重新生成:新候选成为 g1 的选中变体(index 1),指纹变化 ⇒ 重播种。
rc.history = append(append([]core.Message{}, hist...),
core.Message{ID: "m3", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "换一版"}}},
core.Message{ID: "m4", Role: core.RoleAssistant, VariantGroup: "g1", VariantIndex: 1, Parts: []core.Part{{Type: core.PartText, Text: "新候选"}}},
)
if err := r.Execute(rc); err != nil {
t.Fatalf("second Execute: %v", err)
}
sess, err := agentsession.Open(filepath.Join(sandbox, ".agentx", "transcript.jsonl"))
if err != nil {
t.Fatal(err)
}
loaded, err := sess.Load()
if err != nil {
t.Fatal(err)
}
joined := ""
for _, m := range loaded {
joined += m.Content + "\n"
}
if strings.Contains(joined, "旧候选") {
t.Fatalf("reseed must drop the superseded variant: %s", joined)
}
if !strings.Contains(joined, "新候选") {
t.Fatalf("reseed must keep the selected variant: %s", joined)
}
}
// 非播种 run 的首个请求注入一次环境刷新与新命中模块;播种 run 不注入
// (系统段已含)。用 JSON 兼容方式探请求文本。
func joinReq(msgs []agentllm.Message) string {
var b strings.Builder
for _, m := range msgs {
b.WriteString(m.Content)
b.WriteString("\n")
}
return b.String()
}
func TestBuildCurrentUserCarriesAttachmentsAndKnowledge(t *testing.T) {
doc := core.ParsedDocument{
Meta: core.DocMeta{Filename: "会议.docx"},
Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "结论先入库。"}},
}
kn := []core.RetrievalHit{{Doc: core.KnowledgeDoc{Title: "知识", Source: "upload", Chunk: "片段"}}}
got := ctxasm.BuildCurrentUser(core.EffectiveSettings{}, core.Message{Parts: []core.Part{{Type: core.PartText, Text: "总结"}}}, []core.ParsedDocument{doc}, kn, &ctxasm.Report{})
for _, want := range []string{"总结", "会议.docx", "结论先入库。", "[相关知识]", "片段"} {
if !strings.Contains(got.Content, want) {
t.Fatalf("current user missing %q: %s", want, got.Content)
}
}
}
var _ = agentllm.Message{}