package agent import ( "context" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // defaultPurpose labels CallRecords when Options.Purpose is empty. const defaultPurpose = "agent" // stepToolSurface 是本步「模型可见工具面」的 ctx 载体。用结构体(而不是 // 裸切片)承载,使「已挂载但为空」与「未挂载」可区分:前者语义是「本步 // 不广告任何工具」,后者是「无人过滤,按 dispatcher 全量广告」。 type stepToolSurface struct { tools []model.ToolSpec } // stepToolSurfaceKey 是本步工具面的 context key。 type stepToolSurfaceKey struct{} // WithToolSurface 把本步模型可见的工具面挂到 ctx 上。它由驱动 RunStep 的 // 上层(运行时循环)在调用之前写入:工具面在**唯一一处**过滤逻辑里算出 // (loop.toolSchemas:capacity 允许集 + design/11 §3 会话环境安全门), // agent 只负责原样广告。 // // 为什么必须有这个载体(audit P0-12):agent 包不能反向 import loop,所以 // 非流式/静音路径(RunStep,被 orchestration 子代理、automations、backthink // 使用)过去只能自己拿 dispatcher 全量 schema,绕过循环的能力层与安全闸 —— // 「模型只见允许集」在这条路径上不成立。下传同一份已过滤工具面(而不是让 // agent 再实现一遍过滤)使两条路径的广告面**按构造相同**,不存在第二份 // 实现可以漂移。 // // tools 允许为 nil/空:语义是「本步什么工具都不广告」。 func WithToolSurface(ctx context.Context, tools []model.ToolSpec) context.Context { if ctx == nil { return nil } return context.WithValue(ctx, stepToolSurfaceKey{}, stepToolSurface{tools: tools}) } // ToolSurfaceFrom 返回挂到 ctx 上的本步工具面。ok=false 表示未挂载 // (agent 被独立使用、没有循环在过滤),此时按 dispatcher 全量广告 —— // 与挂载前的行为逐字节一致。它是 WithToolSurface 的读侧。 func ToolSurfaceFrom(ctx context.Context) ([]model.ToolSpec, bool) { if ctx == nil { return nil, false } s, ok := ctx.Value(stepToolSurfaceKey{}).(stepToolSurface) if !ok { return nil, false } return s.tools, true } // buildRequest assembles a model.Request for one step: the agent's instructions // prepended as a leading system message, plus the conversation messages, the // configured model/temperature/max-tokens/purpose and the tool schemas // advertised for this step (see WithToolSurface). When a router is injected it // overrides the model from the request contents, which is how vision requests // are routed to the vision model. // // 本路径与流式主路径(loop.buildRequest)共用同一处 persona 钩子(裁决 03.4.1): // 角色由**适配器视图**携带(model.PersonaScoped,见 model/persona_stack.go), // 写成请求上的 PersonaID,模型路由也按它走。为什么角色不在本函数参数里:RunStep // 由 loop 与 automations 调用,角色只存在于 loop 的会话环境 ctx 里,而 agent // 不能反向 import loop;按角色分开注入的适配器视图(一角色一 agent, // design/22 §4.2 方案 A)是唯一不依赖 ctx 的载体。 // // 角色**已知**时才切到 RouteRequestForPersona:未知角色(普通 *model.Router 或 // 未绑角色的视图)保持原来的 RouteRequest —— 后者带 vision 模型的改写规则, // RouteRequestForPersona 刻意不含(图像路由已归 vision.Router,裁决 14.4.1/14.4.5)。 // // 工具面从 ctx 取(见 WithToolSurface):驱动本步的循环已按 capacity 允许集与 // design/11 §3 会话环境安全门过滤好并下传,本函数不再自行过滤、也不自行 // 放宽 —— 流式路径与非流式路径因此广告同一份工具面(audit P0-12)。 func (a *Agent) buildRequest(ctx context.Context, messages []model.Message) model.Request { req := model.Request{ Model: a.opts.Model, Messages: a.assembleMessages(messages), Temperature: a.temperature(), MaxTokens: a.opts.MaxTokens, Purpose: a.purpose(), Tools: a.toolSchemas(ctx), } if ps, ok := a.adapter.(model.PersonaScoped); ok { req.PersonaID = ps.ScopedPersona() } if a.router != nil { if pr, ok := a.router.(model.PersonaRouter); ok && req.PersonaID != "" { if routeModel, _ := pr.RouteRequestForPersona(req.PersonaID, req); routeModel != "" { req.Model = routeModel } return req } if routeModel, _ := a.router.RouteRequest(req); routeModel != "" { req.Model = routeModel } } return req } // assembleMessages returns messages with the agent's instructions prepended as a // leading system message when instructions are non-empty, otherwise the input // slice is returned unchanged. func (a *Agent) assembleMessages(messages []model.Message) []model.Message { if a.instructions == "" { return messages } out := make([]model.Message, 0, len(messages)+1) out = append(out, model.TextMessage(model.RoleSystem, a.instructions)) out = append(out, messages...) return out } // temperature returns the temperature pointer to place in a Request. Zero maps // to nil so the provider default applies. func (a *Agent) temperature() *float64 { if a.opts.Temperature == 0 { return nil } t := a.opts.Temperature return &t } // purpose returns the CallRecord purpose label, defaulting to defaultPurpose. func (a *Agent) purpose() string { if a.opts.Purpose == "" { return defaultPurpose } return a.opts.Purpose } // toolSchemas 返回本步向模型广告的工具 schema。ctx 上带着本步工具面 // (WithToolSurface,由循环按 capacity 允许集 + 会话环境安全门过滤好)时 // 原样返回它 —— nil/空即「本步不广告任何工具」;未挂载时回退到 dispatcher // 注册的全量 schema(agent 被独立使用、没有循环在过滤时的既有行为, // dispatcher 为 nil 则为 nil)。 // // 注意回退分支**不是**过滤逻辑的第二份实现:它只是「没有循环驱动」这一 // 情形下的既有行为,与流式路径的差别仅在于此路径本来就不经循环组装请求。 func (a *Agent) toolSchemas(ctx context.Context) []model.ToolSpec { if tools, ok := ToolSurfaceFrom(ctx); ok { return tools } if a.tools == nil { return nil } return a.tools.ToolSchemas() }