diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b76ad0..4fc48ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ## [Unreleased] +### LLM + +#### 修复的问题 + +- **修复自定义会话头在「一次性 LLM 调用」路径上不生效**:会话头的头值取自请求 context 上的 session id,而该 id 由 agentcore 仅在挂载了 transcript store 时才写入 context。目标拆解(第 0 轮)与冷节点压缩(§4 body 调用)都是不挂 store 的一次性调用,context 上没有 session id,网关侧读不到该头——对 opencode zen 这类「缺 `x-opencode-session` 直接 400 MissingSessionID」的端点,表现为「第 0 轮目标拆解 400 失败、后续 planner 轮次却完全正常」,且压缩失败只留一行日志、极难关联。现为这两条路径显式挂上按探索稳定的 session id(`exp-goals` / `exp-compactor`):既能正常带上该头,也让 llmrec 能把这两条路径的 token 用量正确归因到对应探索(此前完全记不到)。 + ## [0.3.14] - 2026-09-24 ### 任务列表 diff --git a/agent/compaction.go b/agent/compaction.go index 2dd5dd0..2e2e9e8 100644 --- a/agent/compaction.go +++ b/agent/compaction.go @@ -28,6 +28,7 @@ import ( "github.com/Autumn-27/artex/db" "github.com/Autumn-27/norma/llm" + "github.com/Autumn-27/norma/transcript" ) // Compactor performs background cold-node compaction for many explorations. @@ -85,6 +86,12 @@ func (c *Compactor) OnPlannerRound(ctx context.Context, ts *db.ExplorationStore) defer c.finish(ts.ID()) bg, cancel := context.WithTimeout(context.WithoutCancel(ctx), c.maxDur) defer cancel() + // 压缩是裸 provider 调用(compress 里直接 prov.Complete),不经过 agentcore + // 的会话循环,所以 ctx 上没有 session id;按 session-id 头做提示缓存/粘性 + // 路由的网关(opencode zen 缺 x-opencode-session 直接 400)就收不到该头。 + // 这里补一个按探索稳定的 id:同一探索的所有压缩请求共享它,既能带上头, + // 也让 llmrec 能把这次调用的 token 归因回该探索(此前记不到)。 + bg = transcript.WithSessionID(bg, fmt.Sprintf("exp%d-compactor", ts.ID())) if needMajor { c.major(bg, ts) } else { diff --git a/agent/goals.go b/agent/goals.go index e2c11b9..9bf9e17 100644 --- a/agent/goals.go +++ b/agent/goals.go @@ -3,6 +3,7 @@ package agent import ( "context" "encoding/json" + "fmt" "strings" "github.com/Autumn-27/artex/db" @@ -10,6 +11,7 @@ import ( "github.com/Autumn-27/norma/llm" acperm "github.com/Autumn-27/norma/permission" actool "github.com/Autumn-27/norma/tool" + "github.com/Autumn-27/norma/transcript" ) // goalsDefaultTmpl is the built-in EDITABLE body (段 [A]) of the goals-decomposer @@ -109,6 +111,15 @@ func DecomposeGoalsWithProvider(ctx context.Context, prov llm.Provider, dataDir, if prov == nil { return nil } + // 目标拆解是一次性调用:不挂 transcript store,所以 agentcore 不会往 ctx 上挂 + // session id(它只在有 writer 时才挂,见 agentcore.Prompt)。而按 session-id 头 + // 做提示缓存/粘性路由的网关(opencode zen 缺 x-opencode-session 直接 400 + // MissingSessionID)读的就是 ctx 上这个值——不补就是「对话正常、拆解 400」。 + // 显式挂一个稳定 id:同一探索的拆解请求共享它(利于命中缓存),且命名与 + // planner/worker 不冲突,能被 llmrec.parseSession 正确归因。 + if ts != nil { + ctx = transcript.WithSessionID(ctx, fmt.Sprintf("exp%d-goals", ts.ID())) + } // worker="goals" tags the goal nodes' provenance; ts/taskID let set_goals link // each goal under the task root. This is the catalog's real set_goals tool, so a // web-edited description/schema on it applies here too.