feat(session,runtime): 多角色并存—按会话选角(迁移0011 + 按回合解析 + agent池 + 提取器修正)
按 DevDocs/design/22 批次 1 后端(§2/§3/§4.1-4.5/§4.7/§5 验收 1-6)。
- 迁移 0011:chat_sessions.persona_id + chat_messages.persona_id(均 DEFAULT '',无回填 → 升级零风险);同步包内幂等 DDL;**真实库升级验证**(拷贝 data/cyrene.db 到临时目录跑 MigrateUp:既有 1 会话/109 消息 persona 仍为 '',原库未被触碰)
- history:SessionMeta/Message 加 PersonaID + SetSessionPersona;ListSessions/List/scan/Append 带上
- runtime:SessionEnv.PersonaID + loop.personaFor(ctx) 替换固定 l.personaID(memory/request/turn)+ **agent 池**(Options.Agents,按角色取 instructions,未命中回落)+ **提取器不再把 persona 烤死在构造期**(多角色下记忆不再永远记在 cyrene 名下)+ **thinking 旋钮改为按会话角色解析**(否则角色 Y 会用角色 X 的预算/展示)
- gateway:会话 DTO/POST/PATCH 支持 personaId;ws/rest/voice 的 OwnerEnv 硬编改为**按会话解析角色**(actor 元数据语义不变);admin memories 搜索去掉硬编 cyrene
- Docs/api-client v0.42;**两处有意的行为收紧**已写明:POST /sessions body 非法 JSON → 400(原先完全忽略 body;空/null/{} 仍 201);POST /admin/memories/search 省略 personaId 且无默认角色 → 400(原先默认查 cyrene)
- 测试:design/22 §5 验收 1-6(记忆隔离/提示词首条 system/情绪按角色/提取 persona 归属/迁移兼容)+ 进程内 e2e(断言 adapter 收到的 model.Request.Messages[0] 即该角色的 systemPrompt)
- **已知未做(留批次 2 / 待确认)**:① IM 与渠道级未做 → **§7-3「owner 私聊镜像随主会话角色」尚未生效**;② 请求级覆盖未实现(本批只做会话级);③ **§7-5 的「上下文发送角色标记」只完成存储+DTO 半边**——消息 persona 已落库且 DTO 已带,但模型上下文不从 chat_messages 重建,「第 N 条是谁说的」尚未进上下文(需确认是否属批次 2);④「切角色重建摘要」当前是空操作(压缩未接线);⑤ thinking 旋钮仅在流式路径生效(非流式 agent.RunStep 不带,A 批次既有性质)
- 验收:go build/vet/test 全绿 + linux/amd64 与 darwin/arm64 交叉编译(既有 flake 本轮未触发;同批 session_orchestrator.go 一度 501 行 → 抽出 session_tail.go)
This commit is contained in:
1 parent
66a96d2f93
commit
0624d9ee36
46 files changed
+1908
-259
No files matched your search
@@ -21,8 +21,20 @@ import (
|
||||
// checkpoint skips persistence; a missing tracer skips tracing.
|
||||
type Options struct {
|
||||
// Agent is the loop's execution unit: an instruction string plus a backend
|
||||
// adapter, an optional tool dispatcher and optional guardrails.
|
||||
// adapter, an optional tool dispatcher and optional guardrails. It is the
|
||||
// DEFAULT agent: every turn whose role has no entry in Agents (or every turn
|
||||
// when Agents is unset) runs on it, so its instructions should be the global
|
||||
// default role's.
|
||||
Agent *agent.Agent
|
||||
// Agents is the per-role agent pool (design/22 §4.2 方案 A): persona id → the
|
||||
// agent carrying that role's system prompt. The loop picks the entry for the
|
||||
// turn's resolved role on every step, so switching a session's role switches the
|
||||
// prompt without rebuilding the loop (an Agent's instructions are immutable, so
|
||||
// one agent per role is the way to keep that invariant). All entries normally
|
||||
// share one dispatcher/guardrail pipeline/adapter — only the instructions differ.
|
||||
// A missing or nil entry, an unknown role, or an empty pool all fall back to
|
||||
// Agent. Nil keeps the pre-existing single-role behavior.
|
||||
Agents map[string]*agent.Agent
|
||||
// Tools is the tool dispatcher used to execute model-requested calls. When
|
||||
// nil the loop falls back to the agent's own dispatcher (if any).
|
||||
Tools *tool.Dispatcher
|
||||
@@ -75,8 +87,21 @@ type Options struct {
|
||||
// RunStep is bypassed. On the non-streaming path the agent runs its own
|
||||
// guardrails inside RunStep.
|
||||
Guards *guardrail.Pipeline
|
||||
// PersonaID scopes memory retrieval and extraction. Blank skips both.
|
||||
// PersonaID is the loop's configured role: the global default that every turn
|
||||
// without an explicit SessionEnv.PersonaID runs as (design/22 §2 裁决规则 4). It
|
||||
// scopes memory retrieval and extraction, the system prompt fallback and the
|
||||
// emotion hook. Blank skips memory injection/extraction.
|
||||
PersonaID string
|
||||
// PersonaKnobs, when non-nil, resolves the reasoning knobs for a role at the
|
||||
// start of every request build (design/22 §4.2), so a session bound to role Y
|
||||
// uses Y's thinking budget / effort / broadcast switch rather than the default
|
||||
// role's. The returned ok=false means "role unknown" and keeps the loop's
|
||||
// configured defaults (Options.Thinking / ReasoningEffort / ThinkingShowToUser).
|
||||
// Resolving through the role id — rather than a caller-supplied per-turn value —
|
||||
// is what keeps every path (gateway, channelbridge, backthink, sub-agents) on the
|
||||
// right role's knobs from a single input. Nil keeps the configured defaults for
|
||||
// every turn, i.e. the pre-multi-character behavior.
|
||||
PersonaKnobs func(personaID string) (LoopKnobs, bool)
|
||||
// Timezone is the IANA zone used to render the per-round "current time"
|
||||
// system message (e.g. "Asia/Shanghai"). Empty disables time injection
|
||||
// entirely (backward compatible); a name that fails to load also disables it.
|
||||
@@ -90,10 +115,9 @@ type Options struct {
|
||||
// body text, tool calls and (eventually) reasoning segments independently.
|
||||
OnStream func(model.StreamEvent)
|
||||
// Thinking carries the reasoning knobs (budget / explicit switch) injected
|
||||
// into every model request the loop builds. nil means "do not intervene",
|
||||
// which is the pre-existing behavior. It is a loop option rather than a
|
||||
// persona read inside the loop so the per-turn, per-role resolution a later
|
||||
// multi-character batch adds only changes the wiring-time resolver.
|
||||
// into every model request the loop builds when no per-role value overrides it.
|
||||
// nil means "do not intervene", which is the pre-existing behavior. It is the
|
||||
// DEFAULT for turns whose role has no resolved knobs (Options.PersonaKnobs).
|
||||
Thinking *model.Thinking
|
||||
// ReasoningEffort is the OpenAI-compatible strength knob injected into every
|
||||
// model request. Empty means "do not intervene".
|
||||
|
||||
Reference in new issue
Block a user