- 清单只给 name+description(+when-to-use),单条 ≤1536 字符 + 全局条数上限,超限如实标 truncated - 命中才把正文作为一条消息注入:跨回合保留、按内容指纹缓存不重复读盘、注入 ≤16 KiB 标 .truncated - invocation 三态与清单逐格对齐(model-only 清单不含 description;user-only 清单有但模型不可调) - 激活轮的工具面 = allowed-tools ∩ 安全闸 ∩ 注册表 再减 disallowed-tools(owner 也看不见未允许的工具) - 激活状态进程内按会话(不落库);注入量已界化(压缩器估值未含,超我文件面) 裁定(DECISIONS #3/#4/#5):删掉 paths 启发式(只认显式挂载);多技能并集;收缩作用域=激活的那个回合。
196 lines
7.1 KiB
Go
196 lines
7.1 KiB
Go
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)
|
||
}
|