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:
3 files changed
+24
No files matched your search
@@ -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<N>-goals` / `exp<N>-compactor`):既能正常带上该头,也让 llmrec 能把这两条路径的 token 用量正确归因到对应探索(此前完全记不到)。
|
||||
|
||||
## [0.3.14] - 2026-09-24
|
||||
|
||||
### 任务列表
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user