diff --git a/deps/agent-native b/deps/agent-native index cb21ec4..79e1a8f 160000 --- a/deps/agent-native +++ b/deps/agent-native @@ -1 +1 @@ -Subproject commit cb21ec4e2a0bc50a8b9ef5686b77ea3af19b8b7f +Subproject commit 79e1a8fa85b8195ae27139934e999d5ceee78fe3 diff --git a/deps/agent-squad b/deps/agent-squad index 931e5ae..90077b6 160000 --- a/deps/agent-squad +++ b/deps/agent-squad @@ -1 +1 @@ -Subproject commit 931e5ae410299f29d66d819ef2c83e7b06c8d57b +Subproject commit 90077b660101ffae843d37a28d528bff612d93d0 diff --git a/internal/runtime/llm_direct.go b/internal/runtime/llm_direct.go index 675b424..aaeca00 100644 --- a/internal/runtime/llm_direct.go +++ b/internal/runtime/llm_direct.go @@ -3,6 +3,8 @@ package runtime import ( "context" "encoding/json" + "log" + "path/filepath" "fmt" "strings" "sync" @@ -12,6 +14,7 @@ import ( "agent-native/runtime/gates" "agent-native/runtime/hooks" agentllm "agent-native/runtime/llm" + agentsession "agent-native/runtime/session" agenttools "agent-native/runtime/tools" "agent-system-backend/internal/ctxasm" "agent-system-backend/internal/squadadapter" @@ -158,6 +161,18 @@ func (l *LLMRunner) runDirect(rc RunCtx, stream agentllm.StreamFunc, model strin } window, reserve, buffer := contextWindow(st.eff) + // 会话转录(N3 WP2):产品会话 run 接入意图日志与恢复配对—— + // JournalOnly 模式:journal 只作日志,上下文仍由 ctxasm 组装(产品管线 + // 不冻结);恢复写入的配对摘要以系统消息注入。打开失败降级为无 + // Session(旧行为,不阻断对话)。 + var sessStore *agentsession.Store + if sandbox := conversationSandbox(rc); sandbox != "" { + if opened, oerr := agentsession.Open(filepath.Join(sandbox, ".agentx", "transcript.jsonl")); oerr == nil { + sessStore = opened + } else { + log.Printf("[runtime] open session transcript: %v", oerr) + } + } bus := hooks.New() bus.On(func(c hooks.Call) hooks.Outcome { if c.Event != hooks.Stop { @@ -172,6 +187,8 @@ func (l *LLMRunner) runDirect(rc RunCtx, stream agentllm.StreamFunc, model strin res, err := nruntime.Run(rc.Context(), nruntime.Config{ Model: model, History: history, + Session: sessStore, + JournalOnly: sessStore != nil, Tools: reg, Stream: wrapped, MaxTurns: maxTurns, diff --git a/internal/runtime/llm_direct_n3_test.go b/internal/runtime/llm_direct_n3_test.go new file mode 100644 index 0000000..d1f9bf0 --- /dev/null +++ b/internal/runtime/llm_direct_n3_test.go @@ -0,0 +1,126 @@ +package runtime + +import ( + "context" + "path/filepath" + "strings" + "testing" + + agentllm "agent-native/runtime/llm" + agentsession "agent-native/runtime/session" + core "agent-system-core" +) + +// ---- N3 WP2:产品会话路径持久化(验收① 产品层机器证据)---- + +func textTurns(texts ...string) [][]agentllm.Event { + out := make([][]agentllm.Event, 0, len(texts)) + for _, t := range texts { + out = append(out, []agentllm.Event{ + {Kind: agentllm.KindTextDelta, Text: t}, + {Kind: agentllm.KindMessageEnd, Message: agentllm.Message{Role: agentllm.RoleAssistant, Content: t, StopReason: agentllm.StopEnd}}, + }) + } + return out +} + +// 杀窗口形态的种子转录:新 run 前配对修复成立,且 run 正常产出。 +func TestDirectRunPairsAfterKillSeed(t *testing.T) { + sandbox := t.TempDir() + sess, err := agentsession.Open(filepath.Join(sandbox, ".agentx", "transcript.jsonl")) + if err != nil { + t.Fatal(err) + } + for _, m := range []agentllm.Message{ + {Role: agentllm.RoleSystem, Content: "sys"}, + {Role: agentllm.RoleUser, Content: "早前"}, + {Role: agentllm.RoleAssistant, StopReason: agentllm.StopToolCalls, ToolCalls: []agentllm.ToolCall{{ID: "c1", Name: "run_python", Arguments: `{}`}}}, + } { + if err := sess.Append(m); err != nil { + t.Fatal(err) + } + } + rc := &fakeRunCtx{ + ctx: context.Background(), sandbox: sandbox, + prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, + history: []core.Message{ + {ID: "m1", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "还在吗"}}}, + }, + } + r := &LLMRunner{Stream: scriptStream(textTurns("还在"), nil)} + if err := r.Execute(rc); err != nil { + t.Fatalf("Execute: %v", err) + } + loaded, err := sess.Load() + if err != nil { + t.Fatal(err) + } + var paired bool + for _, m := range loaded { + if m.Role == agentllm.RoleTool && m.ToolCallID == "c1" { + paired = true + if !m.IsError || !strings.Contains(m.Content, "进程中断") { + t.Fatalf("兜底配对形态不对:%+v", m) + } + } + } + if !paired { + t.Fatalf("杀窗口调用未配对:%#v", loaded) + } +} + +// JournalOnly 语义:上下文始终由 store 历史组装(每跑都在、恰好一次、 +// 不丢不重);journal 不吞种子历史。 +func TestDirectRunSeedsStoreHistoryOnce(t *testing.T) { + sandbox := t.TempDir() + rc := &fakeRunCtx{ + ctx: context.Background(), sandbox: sandbox, + prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, + history: []core.Message{ + {ID: "m1", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "历史一"}}}, + {ID: "m2", Role: core.RoleAssistant, Parts: []core.Part{{Type: core.PartText, Text: "回复一"}}}, + {ID: "m3", Role: core.RoleUser, 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) + }} + if err := r.Execute(rc); err != nil { + t.Fatalf("first Execute: %v", err) + } + // 第二跑:同一 rc(新 user 消息由 history 尾部提供)。 + rc.history = append(rc.history, core.Message{ID: "m4", Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "再来"}}}) + if err := r.Execute(rc); err != nil { + t.Fatalf("second Execute: %v", err) + } + if len(requests) < 2 { + t.Fatalf("stream 调用数 = %d", len(requests)) + } + for i, req := range requests { + count := 0 + for _, m := range req { + if m.Content == "历史一" { + count++ + } + } + if count != 1 { + t.Fatalf("第 %d 跑历史中「历史一」必须恰好一次,得到 %d", i+1, count) + } + } + 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) + } + for _, m := range loaded { + if m.Content == "历史一" { + t.Fatalf("journal 不得吞种子历史:%#v", loaded) + } + } +} diff --git a/internal/runtime/llm_test.go b/internal/runtime/llm_test.go index c2086a3..62c89b9 100644 --- a/internal/runtime/llm_test.go +++ b/internal/runtime/llm_test.go @@ -39,6 +39,7 @@ type fakeRunCtx struct { history []core.Message // 非 nil 时覆盖默认历史 bgID string bgErr error + sandbox string // 非空时 ConversationSandbox 返回之(N3 WP2 转录测试) bgCalls int } @@ -94,6 +95,13 @@ func (f *fakeRunCtx) SpawnBackground(agentID, name, task, hint string) (core.ID, } return f.bgID, nil } +func (f *fakeRunCtx) ConversationSandbox() string { + if f.sandbox != "" { + return f.sandbox + } + return "" +} + func (f *fakeRunCtx) Attachments() []core.ParsedDocument { return nil } func (f *fakeRunCtx) Tools() []core.ToolDesc { return nil } func (f *fakeRunCtx) Subtask() string { return "" }