package validator import ( "context" "errors" "fmt" "strings" "git.flytoex.net/yuanwei/flyto-agent/internal/transport/retry" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" ) // FlytoLLMClient wraps a flyto.ModelProvider as a minimal text-in / // text-out client for Validator implementations that delegate verdicts // to an LLM. It is an independent copy of the evolve package's // adapter (same event-aggregation algorithm, different caller label) // so validator and evolve remain siblings without a cross-package // dependency. // // Direction of dependency: // validator consumes flyto.ModelProvider; providers/ subpackages // define concrete providers. The direction is validator -> flyto. // // Temperature limitation (inherited from flyto.Request contract): // flyto.Request is the cross-provider greatest common denominator and // deliberately omits Temperature. Per-provider knobs live in the // provider factory Config (anthropic.Config / minimax.Config / ...). // Callers needing per-call temperature control construct separate // provider instances with different factory Configs. // // Event filtering: // - Aggregated: TextEvent (authoritative), TextDeltaEvent (fallback) // - Error path: ErrorEvent -> wraps ErrValidatorBackend // - Ignored: ToolUse*, Thinking*, Usage, Done, Turn*, SessionInfo, // Permission*, Warning, Compact, Checkpoint* // // Aggregation precedence (CLEVER): // some providers emit both TextDeltaEvent (streaming increments) and // TextEvent (the complete text block on block_stop). A naive "sum // both" strategy double-counts. TextEvent is authoritative: on each // TextEvent arrival we append the final text and reset the delta // accumulator. If the stream closes without any TextEvent (providers // that skip the complete-block emission) we fall back to the delta // accumulator so no text is lost. // // FlytoLLMClient 把 flyto.ModelProvider 包装为文本进 / 文本出的最小 // 客户端, 供把审批决策委托给 LLM 的 Validator 实现使用. 本文件是 // evolve 包 adapter 的独立副本 (同事件聚合算法, 不同调用方标签), 让 // validator 与 evolve 保持平级不互相依赖. // // 依赖方向: validator 消费 flyto.ModelProvider; providers/ 子包定义 // 具体 provider. 方向是 validator -> flyto. // // Temperature 限制 (源自 flyto.Request 契约): flyto.Request 是跨 // provider 最大公约数, 刻意不含 Temperature. Provider 专有旋钮放在 // 工厂 Config (anthropic.Config / minimax.Config 等). 需要按次温度 // 控制的调用方构造不同 Config 的 provider 实例. // // 事件过滤: // - 聚合: TextEvent (权威), TextDeltaEvent (fallback) // - 错误路径: ErrorEvent -> 包装 ErrValidatorBackend // - 忽略: ToolUse* / Thinking* / Usage / Done / Turn* / ... // // 聚合优先级 (CLEVER): 部分 provider 同时推 TextDeltaEvent 和 TextEvent, // 简单 "都加" 会双倍计. TextEvent 为权威: 每次收到 TextEvent 追加完整 // 文本并重置 delta 累计. 流关闭时若无任何 TextEvent (跳过 block_stop // 推送的 provider) 回退到 delta 累计器避免丢文本. type FlytoLLMClient struct { provider flyto.ModelProvider defaultModel string systemPrompt string } // NewFlytoLLMClient builds a FlytoLLMClient. provider is required. // defaultModel is used when Complete's model argument is "". systemPrompt // is injected verbatim into flyto.Request.System on every Complete call // (pass "" to omit). // // NewFlytoLLMClient 构造 FlytoLLMClient. provider 必填. defaultModel 在 // Complete 的 model 参数为 "" 时使用. systemPrompt 每次 Complete 调用 // 原样注入 flyto.Request.System (传 "" 忽略). func NewFlytoLLMClient(provider flyto.ModelProvider, defaultModel, systemPrompt string) (*FlytoLLMClient, error) { if provider == nil { return nil, errors.New("validator: NewFlytoLLMClient requires non-nil provider") } return &FlytoLLMClient{ provider: provider, defaultModel: defaultModel, systemPrompt: systemPrompt, }, nil } // Complete sends prompt as a single user message and drains the // provider event stream into a response string. On backend failure // the returned error wraps ErrValidatorBackend so callers detect the // class via errors.Is. An empty model falls back to defaultModel. // maxTokens = 0 leaves the field at flyto.Request default. // // Complete 以单条 user message 发送 prompt 并聚合 provider 事件流为 // 响应字符串. backend 失败时返回的 error 以 %w 包装 ErrValidatorBackend, // 调用方用 errors.Is 识别. model 为 "" 回退到 defaultModel. maxTokens=0 // 使用 flyto.Request 默认. func (c *FlytoLLMClient) Complete(ctx context.Context, prompt string, model string, maxTokens int) (string, error) { effectiveModel := model if effectiveModel == "" { effectiveModel = c.defaultModel } req := &flyto.Request{ Model: effectiveModel, System: c.systemPrompt, Messages: []flyto.Message{flyto.UserText(prompt)}, MaxTokens: maxTokens, } // Validator: pre-commit gate LLM calls. Tag ctx so retry failures // here don't alias with main-thread user failures. // // Validator: pre-commit gate 的 LLM 调用. 标记 ctx 让此处重试失败 // 不与主线程用户失败混淆. ctx = retry.WithQuerySource(ctx, "validator") ch, err := c.provider.Stream(ctx, req) if err != nil { return "", fmt.Errorf("%w: %v", ErrValidatorBackend, err) } var textBlocks []string var deltaBuf strings.Builder for { select { case <-ctx.Done(): return "", ctx.Err() case evt, ok := <-ch: if !ok { if len(textBlocks) > 0 { return strings.Join(textBlocks, ""), nil } return deltaBuf.String(), nil } switch e := evt.(type) { case *flyto.TextEvent: textBlocks = append(textBlocks, e.Text) deltaBuf.Reset() case *flyto.TextDeltaEvent: deltaBuf.WriteString(e.Text) case *flyto.ErrorEvent: return "", fmt.Errorf("%w: %v", ErrValidatorBackend, e.Err) default: // Ignore ToolUse / Thinking / Usage / Done / Turn* / etc. } } } }