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.
114 lines
4.3 KiB
Go
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{}
|