fix(llm): 一次性 LLM 调用补挂 session id,让自定义会话头真正生效 (#175)

自定义会话头(profile 的 session_header_key)的头值取自请求 context 上的
session id,而该 id 由 agentcore 仅在挂载 transcript store 时才写入 context
(agentcore.Prompt 中 `if s.writer != nil` 那一行)。

目标拆解(第 0 轮,DecomposeGoalsWithProvider)与冷节点压缩(§4 body 调用)
都是不挂 store 的一次性调用,context 上没有 session id,quotaAwareTransport
因此跳过该头。对 opencode zen 这类「缺 x-opencode-session 直接 400
MissingSessionID」的端点,表现为「第 0 轮目标拆解 400 失败、后续 planner
轮次却完全正常」;压缩失败则只留一行日志,极难关联到本设置。

改为在这两条路径显式挂上按探索稳定的 session id(exp<N>-goals /
exp<N>-compactor),与旁路问答、连接测试已有的做法一致。附带修好用量归因:
这两个 id 能被 llmrec.parseSession 正确解析,此前这两条路径的 token 完全
记不进 llm_usage。
This commit is contained in:
榆木 authored and GitHub committed 2026-09-29 18:21:30 +08:00
1 parent 69fe9357d3
commit 25b6018818
3 files changed
+24

No files matched your search

+6
View File
@@ -4,6 +4,12 @@
## [Unreleased] ## [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<N>-goals` / `exp<N>-compactor`):既能正常带上该头,也让 llmrec 能把这两条路径的 token 用量正确归因到对应探索(此前完全记不到)。
## [0.3.14] - 2026-09-24 ## [0.3.14] - 2026-09-24
### 任务列表 ### 任务列表
+7
View File
@@ -28,6 +28,7 @@ import (
"github.com/Autumn-27/artex/db" "github.com/Autumn-27/artex/db"
"github.com/Autumn-27/norma/llm" "github.com/Autumn-27/norma/llm"
"github.com/Autumn-27/norma/transcript"
) )
// Compactor performs background cold-node compaction for many explorations. // 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()) defer c.finish(ts.ID())
bg, cancel := context.WithTimeout(context.WithoutCancel(ctx), c.maxDur) bg, cancel := context.WithTimeout(context.WithoutCancel(ctx), c.maxDur)
defer cancel() 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 { if needMajor {
c.major(bg, ts) c.major(bg, ts)
} else { } else {
+11
View File
@@ -3,6 +3,7 @@ package agent
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"github.com/Autumn-27/artex/db" "github.com/Autumn-27/artex/db"
@@ -10,6 +11,7 @@ import (
"github.com/Autumn-27/norma/llm" "github.com/Autumn-27/norma/llm"
acperm "github.com/Autumn-27/norma/permission" acperm "github.com/Autumn-27/norma/permission"
actool "github.com/Autumn-27/norma/tool" 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 // 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 { if prov == nil {
return 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 // 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 // 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. // web-edited description/schema on it applies here too.