package loop import ( "context" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/capacity" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // buildRequest 从该 turn 的 agent 公开表面组装 model.Request, // 镜像 agent.buildRequest,使流式路径的行为与 // RunStep 一致(前导指令消息、工具 schema、模型路由)。agent // 与推理旋钮从 ctx 解析,因此切换了角色的会话 // 在其下一步就拿到新角色的提示词与预算(design/22 §4.2)。 // // 模型路由按该 turn 的角色(persona)解析(裁决 03.4.1)。agent.buildRequest // (非流式路径,internal/runtime/agent/request.go)仍只按消息 role 路由—— // 那条路径的 persona 接线不在本批次文件面内,已在回报中登记。 func (l *Loop) buildRequest(ctx context.Context, messages []model.Message, ag *agent.Agent, knobs LoopKnobs) model.Request { req := model.Request{ Model: ag.Model(), Messages: assembleMessages(ag, messages), Temperature: temperatureOf(ag), MaxTokens: ag.MaxTokens(), Purpose: ag.Purpose(), Tools: l.toolSchemas(ctx, messages), Thinking: knobs.Thinking, ReasoningEffort: knobs.ReasoningEffort, } if ag.Router() != nil { // 按 turn 的角色(persona)路由(裁决 03.4.1:per-role 的「角色」= 人格, // 不是 LLM 消息 role)。路由器的 persona 感知是可选的扩展接口 // (model.PersonaRouter),因此只按消息 role 路由的注入方行为不变。 if pr, ok := ag.Router().(model.PersonaRouter); ok { if m, _ := pr.RouteRequestForPersona(l.personaFor(ctx), req); m != "" { req.Model = m } return req } if m, _ := ag.Router().RouteRequest(req); m != "" { req.Model = m } } return req } // assembleMessages 在 agent 指令非空时把它们作为前导系统消息 // 前置,否则原样返回输入切片。 func assembleMessages(ag *agent.Agent, messages []model.Message) []model.Message { instr := ag.Instructions() if instr == "" { return messages } out := make([]model.Message, 0, len(messages)+1) out = append(out, model.TextMessage(model.RoleSystem, instr)) out = append(out, messages...) return out } // temperatureOf 返回指向 agent 所配置温度的指针,把 // 零映射为 nil,使 provider 默认值生效(与 agent 包一致)。 func temperatureOf(ag *agent.Agent) *float64 { t := ag.Temperature() if t == 0 { return nil } return &t } // toolSchemas 向模型广告已注册的工具 schema;未配置 // dispatcher 时为 nil。接上并启用了 capacity 判定器时,它 // 判定当前轮次并只返回该状态的允许集 // (dispatcher 之上的一个视图,见 design/14 §4):模型只看见/调用 // 允许的工具,而 Invoke 本身不被拦截。nil 或被禁用的 // 判定器保持既有行为(每个工具都被广告)。 // // design/11 §3 安全门最后在 capacity 视图留下的结果之上运行: // 群聊/陌生人环境中只广告其可编辑标记允许该 // 环境的工具(默认关闭),而 owner 环境保留 // 全集。安全是硬红线而非便利提示,因此它 // 只能收缩暴露面 —— 绝不重新放行被 capacity 排除的工具。 func (l *Loop) toolSchemas(ctx context.Context, messages []model.Message) []model.ToolSpec { d := l.dispatcher() if d == nil { return nil } schemas := d.ToolSchemas() if l.capacity == nil || !l.capacity.Enabled() { return l.narrowToolSchemas(ctx, l.restrictSchemas(ctx, d, schemas)) } names := make([]string, 0, len(schemas)) for _, s := range schemas { names = append(names, s.Name) } acc := capState(ctx) dec := l.capacity.Decide(ctx, capacity.Input{ Messages: capacityTexts(messages), CurrentState: acc.state, // 该 turn 所属角色的档位(裁决 14.4.3 的后半):没有它,绑定非默认 // 角色的会话只能吃到默认角色的全局兜底,别的角色写在档案里的 // `awakening` 会被解析、被校验、却静默不生效。 Awakening: l.awakeningFor(ctx), AllToolNames: names, Recall: l.capacityRecall(ctx, messages), }) acc.state = dec.State if l.onCapacity != nil && !isMutedBroadcast(ctx) { sessionID := loopSession(ctx) for _, ev := range dec.Events { ev.SessionID = sessionID l.onCapacity(ev) } } allowed := make(map[string]bool, len(dec.AllowedTools)) for _, n := range dec.AllowedTools { allowed[n] = true } out := make([]model.ToolSpec, 0, len(dec.AllowedTools)) for _, s := range schemas { if allowed[s.Name] { out = append(out, s) } } return l.narrowToolSchemas(ctx, l.restrictSchemas(ctx, d, out)) } // capacityTexts 为 capacity 判定器提取近期对话文本: // 最后几个非 system/非 tool 的轮次,按时间顺序(最早的在前), // 使判定器的当前轮窗口最后看到最新的用户消息。 func capacityTexts(messages []model.Message) []string { texts := make([]string, 0, 6) for i := len(messages) - 1; i >= 0 && len(texts) < 6; i-- { m := messages[i] if m.Role == model.RoleSystem || m.Role == model.RoleTool { continue } if t := messageText(m); t != "" { texts = append(texts, t) } } for i, j := 0, len(texts)-1; i < j; i, j = i+1, j-1 { texts[i], texts[j] = texts[j], texts[i] } return texts } // capacityRecallTopK 限制 capacity L4 层每轮考虑多少条 // 召回的习惯记忆。 const capacityRecallTopK = 5 // capacityRecall 为 capacity L4 习惯对齐层(design §2.2 L4) // 检索主人的相关长期记忆,这是让判定器学到用户任务表达 // 习惯的记忆输入。它返回 nil(使 L4 保持静默)的情形包括: // 未配置 retriever/persona —— 向后兼容 —— 或处于受限 // (群聊 / 陌生人)上下文,依据隔离红线:L4 学习主人的 // 习惯,绝不把主人敏感记忆读进群聊或陌生人的意图 // 判定(design §2.2 红线: 不读陌生人敏感)。 func (l *Loop) capacityRecall(ctx context.Context, messages []model.Message) []capacity.RecallItem { personaID := l.personaFor(ctx) if l.retriever == nil || personaID == "" { return nil } env := sessionEnvFrom(ctx) if env.Restricted() { return nil } results, err := l.retriever.Retrieve(ctx, personaID, queryFromMessages(messages), capacityRecallTopK) if err != nil { l.log.Warn("capacity recall", "error", err, "env", env.ActorEnv) return nil } items := make([]capacity.RecallItem, 0, len(results)) for _, r := range results { items = append(items, capacity.RecallItem{ Content: r.Item.Content, Scope: string(r.Item.Scope), Kind: string(r.Item.Kind), Tags: r.Item.Tags, Score: r.Score, }) } return items } // decodeToolCalls 经 agent 的共享解码器从 model.Response 解码工具 // 调用,使流式与非流式路径对适配器发出的 provider 原生 // 工具调用(Anthropic tool_use / OpenAI tool_calls)以及 // 旧式 JSON 信封约定保持一致。解码出调用时 // 报告 ok=true(切片非空)。 func decodeToolCalls(resp model.Response) ([]agent.ToolCall, bool) { return agent.ExtractToolCalls(resp) }