Give conversation runs the intent journal via JournalOnly sessions (N3 WP2) #92
Vendored
+1
-1
Submodule deps/agent-native updated: cb21ec4e2a...79e1a8fa85
Vendored
+1
-1
Submodule deps/agent-squad updated: 931e5ae410...90077b6601
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 "" }
|
||||
|
||||
Reference in New Issue
Block a user