Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

500 lines
20 KiB
Go

package agent
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log/slog"
"strings"
runtimeprep "github.com/tutti-os/tutti/packages/agent/runtimeprep"
"github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
"github.com/tutti-os/tutti/services/tuttid/biz/agentprovider"
modelbindingbiz "github.com/tutti-os/tutti/services/tuttid/biz/modelbinding"
modelplanbiz "github.com/tutti-os/tutti/services/tuttid/biz/modelplan"
)
// AgentModelBindingSource resolves the per-workspace agent target binding.
type AgentModelBindingSource interface {
GetAgentModelBinding(ctx context.Context, workspaceID string, agentTargetID string) (modelbindingbiz.Binding, error)
}
// AgentModelPlanSource resolves workspace model access plans with their
// stored credentials. Only the daemon runtime boundary may read the key.
type AgentModelPlanSource interface {
GetModelPlan(ctx context.Context, workspaceID string, planID string) (modelplanbiz.Plan, error)
}
// AgentModelPlanRevisionSource resolves an immutable historical plan revision.
// The concrete workspace store implements this optional extension; keeping it
// separate preserves compatibility for tests and integrations that only need
// current-plan reads.
type AgentModelPlanRevisionSource interface {
GetModelPlanRevision(ctx context.Context, workspaceID string, planID string, revision uint64) (modelplanbiz.Plan, error)
}
// modelPlanBindingRuntime holds the optional model plan integration wiring.
type modelPlanBindingRuntime struct {
Bindings AgentModelBindingSource
Plans AgentModelPlanSource
}
const (
modelConfigurationSourceModelPlan = "model-plan"
modelConfigurationSourceProviderNative = "provider-native"
)
// modelConfigurationRuntimeContext is the redaction-safe, authoritative
// description of the model source currently effective for one agent target.
// It deliberately excludes endpoint URLs and credentials.
type modelConfigurationRuntimeContext struct {
AgentTargetID string
Source string
Fingerprint string
DefaultModel string
ModelPlanID string
ModelPlanRevision uint64
}
func (configuration modelConfigurationRuntimeContext) runtimeContext() map[string]any {
result := map[string]any{
"agentTargetId": strings.TrimSpace(configuration.AgentTargetID),
"source": configuration.Source,
"fingerprint": configuration.Fingerprint,
"defaultModel": nullableString(configuration.DefaultModel),
}
if strings.TrimSpace(configuration.ModelPlanID) != "" {
result["modelPlanId"] = strings.TrimSpace(configuration.ModelPlanID)
result["modelPlanRevision"] = configuration.ModelPlanRevision
}
return result
}
// modelPlanResolution keeps endpoint selection and the redaction-safe
// composer configuration derived from the same binding/plan read.
type modelPlanResolution struct {
Endpoint *runtimeprep.ModelEndpointConfig
Models []modelplanbiz.Model
ModelConfiguration modelConfigurationRuntimeContext
}
// applyRequestedModelPlan makes an explicit task/run assignment authoritative
// over the Agent's default Plan. Credentials remain daemon-owned, and the
// ordinary Plan resolution path below still validates protocol, enabled state,
// revision, and the selected model before runtime launch.
func (s *Service) applyRequestedModelPlan(
ctx context.Context,
workspaceID string,
input *CreateSessionInput,
) error {
if input == nil {
return ErrInvalidArgument
}
planID := strings.TrimSpace(value(input.ModelPlanID))
if planID == "" {
return nil
}
runtime := s.modelPlanRuntime()
if runtime.Plans == nil {
return fmt.Errorf("%w: model plan resolver is unavailable", ErrInvalidArgument)
}
plan, err := runtime.Plans.GetModelPlan(
ctx,
strings.TrimSpace(workspaceID),
planID,
)
if err != nil {
return fmt.Errorf("%w: resolve requested model plan: %v", ErrInvalidArgument, err)
}
input.ResolvedModelPlan = &plan
return nil
}
type modelConfigurationFingerprintPayload struct {
Provider string `json:"provider"`
AgentTargetID string `json:"agentTargetId"`
Source string `json:"source"`
ModelPlanID string `json:"modelPlanId,omitempty"`
ModelPlanRevision uint64 `json:"modelPlanRevision,omitempty"`
Protocol string `json:"protocol,omitempty"`
BindingDefaultModel string `json:"bindingDefaultModel,omitempty"`
PlanDefaultModel string `json:"planDefaultModel,omitempty"`
ModelIDs []string `json:"modelIds,omitempty"`
}
func newProviderNativeModelConfiguration(provider string, agentTargetID string) modelConfigurationRuntimeContext {
payload := modelConfigurationFingerprintPayload{
Provider: agentprovider.NormalizeOpen(provider),
AgentTargetID: strings.TrimSpace(agentTargetID),
Source: modelConfigurationSourceProviderNative,
}
return modelConfigurationRuntimeContext{
AgentTargetID: payload.AgentTargetID,
Source: payload.Source,
Fingerprint: fingerprintModelConfiguration(payload),
}
}
func newModelPlanModelConfiguration(provider string, agentTargetID string, binding modelbindingbiz.Binding, plan modelplanbiz.Plan) modelConfigurationRuntimeContext {
modelIDs := make([]string, 0, len(plan.Models))
for _, model := range plan.Models {
modelIDs = append(modelIDs, strings.TrimSpace(model.ID))
}
bindingDefaultModel := strings.TrimSpace(binding.DefaultModel)
if !modelplanbiz.ModelsContain(plan.Models, bindingDefaultModel) {
bindingDefaultModel = ""
}
payload := modelConfigurationFingerprintPayload{
Provider: agentprovider.Normalize(provider),
AgentTargetID: strings.TrimSpace(agentTargetID),
Source: modelConfigurationSourceModelPlan,
ModelPlanID: strings.TrimSpace(plan.ID),
ModelPlanRevision: plan.Revision,
Protocol: string(plan.Protocol),
BindingDefaultModel: bindingDefaultModel,
PlanDefaultModel: strings.TrimSpace(plan.DefaultModel),
ModelIDs: modelIDs,
}
return modelConfigurationRuntimeContext{
AgentTargetID: payload.AgentTargetID,
Source: payload.Source,
Fingerprint: fingerprintModelConfiguration(payload),
DefaultModel: resolvePlanDefaultModel(plan, binding),
ModelPlanID: payload.ModelPlanID,
ModelPlanRevision: payload.ModelPlanRevision,
}
}
func fingerprintModelConfiguration(payload modelConfigurationFingerprintPayload) string {
// A struct (rather than a map) makes the serialized field order stable. All
// fields are already redaction-safe; notably BaseURL and APIKey are absent.
encoded, _ := json.Marshal(payload)
sum := sha256.Sum256(encoded)
return "sha256:" + hex.EncodeToString(sum[:])
}
func (s *Service) modelPlanRuntime() *modelPlanBindingRuntime {
return &s.modelPlanBinding
}
// ConfigureModelPlanBinding wires the optional model plan integration.
func (s *Service) ConfigureModelPlanBinding(bindings AgentModelBindingSource, plans AgentModelPlanSource) {
s.modelPlanBinding.Bindings = bindings
s.modelPlanBinding.Plans = plans
}
// modelPlanProtocolForProvider reads the endpoint-injection strategy declared
// by the canonical provider registry. Providers without that strategy keep
// their native credential source.
func modelPlanProtocolForProvider(provider string) (modelplanbiz.Protocol, bool) {
protocol, ok := agentprovider.ModelPlanProtocol(provider)
return modelplanbiz.Protocol(protocol), ok
}
// planModelComposerValue renders a plan-domain model id as the composer and
// session-settings value for one provider, dispatching on the registry-declared
// model addressing strategy. Provider-prefixed runtimes (OpenCode) resolve
// models against the session-scoped provider config injected by runtimeprep,
// so their values carry the injected provider namespace; every other provider
// consumes raw plan model ids.
func planModelComposerValue(provider string, modelID string) string {
if agentprovider.ModelPlanModelAddressingProviderPrefixed(provider) {
return runtimeprep.OpenCodePlanModelValue(modelID)
}
return strings.TrimSpace(modelID)
}
// planModelIDFromComposerValue maps a composer/settings model value back to
// the plan-domain model id (inverse of planModelComposerValue). Values without
// the injected namespace pass through unchanged, so raw ids stay valid.
func planModelIDFromComposerValue(provider string, value string) string {
if agentprovider.ModelPlanModelAddressingProviderPrefixed(provider) {
return runtimeprep.OpenCodePlanModelID(value)
}
return strings.TrimSpace(value)
}
// modelEndpointModels projects the plan's model list into the redaction-safe
// endpoint DTO so provider preparers can materialize a full session catalog.
func modelEndpointModels(models []modelplanbiz.Model) []runtimeprep.ModelEndpointModel {
if len(models) == 0 {
return nil
}
endpointModels := make([]runtimeprep.ModelEndpointModel, 0, len(models))
for _, model := range models {
endpointModels = append(endpointModels, runtimeprep.ModelEndpointModel{
ID: model.ID,
Name: model.Name,
})
}
return endpointModels
}
// resolveModelPlanEndpoint resolves the injected endpoint for one session
// launch plus the plan's model list for validation. It returns nil when the
// target has no usable plan binding.
func (s *Service) resolveModelPlanEndpoint(ctx context.Context, workspaceID string, agentTargetID string, provider string, requestedModel string) (*runtimeprep.ModelEndpointConfig, []modelplanbiz.Model) {
resolution := s.resolveModelPlan(ctx, workspaceID, agentTargetID, provider, requestedModel)
return resolution.Endpoint, resolution.Models
}
// resolveModelPlan returns both the secret-bearing runtime endpoint (when a
// usable plan is bound) and a redaction-safe model configuration fingerprint
// for composer reconciliation. Unsupported, missing, disabled, and
// protocol-mismatched bindings all resolve to provider-native configuration.
func (s *Service) resolveModelPlan(ctx context.Context, workspaceID string, agentTargetID string, provider string, requestedModel string) modelPlanResolution {
provider = agentprovider.NormalizeOpen(provider)
agentTargetID = strings.TrimSpace(agentTargetID)
providerNative := modelPlanResolution{
ModelConfiguration: newProviderNativeModelConfiguration(provider, agentTargetID),
}
runtime := s.modelPlanRuntime()
if runtime.Bindings == nil || runtime.Plans == nil {
return providerNative
}
workspaceID = strings.TrimSpace(workspaceID)
if workspaceID == "" || agentTargetID == "" {
return providerNative
}
requiredProtocol, supported := modelPlanProtocolForProvider(provider)
if !supported {
return providerNative
}
binding, err := runtime.Bindings.GetAgentModelBinding(ctx, workspaceID, agentTargetID)
if err != nil || strings.TrimSpace(binding.ModelPlanID) == "" {
return providerNative
}
plan, err := runtime.Plans.GetModelPlan(ctx, workspaceID, binding.ModelPlanID)
if err != nil {
slog.Warn("agent model binding references missing plan",
"event", "agent.model_plan.binding_plan_missing",
"workspace_id", workspaceID,
"agent_target_id", agentTargetID,
"model_plan_id", binding.ModelPlanID,
)
return providerNative
}
if !plan.Enabled {
slog.Info("agent model binding plan is disabled; using provider-native credentials",
"event", "agent.model_plan.binding_plan_disabled",
"workspace_id", workspaceID,
"agent_target_id", agentTargetID,
"model_plan_id", plan.ID,
)
return providerNative
}
if plan.Protocol != requiredProtocol {
slog.Warn("agent model binding plan protocol does not match provider",
"event", "agent.model_plan.binding_protocol_mismatch",
"workspace_id", workspaceID,
"agent_target_id", agentTargetID,
"model_plan_id", plan.ID,
"plan_protocol", string(plan.Protocol),
"provider", provider,
)
return providerNative
}
model := resolvePlanSessionModel(plan, binding, planModelIDFromComposerValue(provider, requestedModel))
return modelPlanResolution{
Endpoint: &runtimeprep.ModelEndpointConfig{
PlanID: plan.ID,
PlanName: plan.Name,
Protocol: string(plan.Protocol),
BaseURL: plan.BaseURL,
APIKey: plan.APIKey,
Model: planModelComposerValue(provider, model),
Models: modelEndpointModels(plan.Models),
PlanUpdatedAtUnixMS: plan.UpdatedAt.UnixMilli(),
},
Models: modelplanbiz.CloneModels(plan.Models),
ModelConfiguration: newModelPlanModelConfiguration(provider, agentTargetID, binding, plan),
}
}
// validateModelAgainstPlan rejects an explicitly requested model that the
// bound plan does not expose. An empty request always resolves to the plan
// default.
func validateModelAgainstPlan(provider string, requestedModel string, planModels []modelplanbiz.Model) error {
requestedModel = planModelIDFromComposerValue(provider, requestedModel)
if requestedModel == "" || modelplanbiz.ModelsContain(planModels, requestedModel) {
return nil
}
available := make([]string, 0, len(planModels))
for _, model := range planModels {
available = append(available, model.ID)
}
return &InvalidModelError{
Provider: agentprovider.Normalize(provider),
Model: requestedModel,
AvailableModels: available,
}
}
func resolvePlanSessionModel(plan modelplanbiz.Plan, binding modelbindingbiz.Binding, requestedModel string) string {
requestedModel = strings.TrimSpace(requestedModel)
if requestedModel != "" && modelplanbiz.ModelsContain(plan.Models, requestedModel) {
return requestedModel
}
if defaultModel := resolvePlanDefaultModel(plan, binding); defaultModel != "" {
return defaultModel
}
return requestedModel
}
func resolvePlanDefaultModel(plan modelplanbiz.Plan, binding modelbindingbiz.Binding) string {
if binding.DefaultModel != "" && modelplanbiz.ModelsContain(plan.Models, binding.DefaultModel) {
return strings.TrimSpace(binding.DefaultModel)
}
if plan.DefaultModel != "" {
return strings.TrimSpace(plan.DefaultModel)
}
if len(plan.Models) > 0 {
return strings.TrimSpace(plan.Models[0].ID)
}
return ""
}
// applyModelPlanComposerOverlay replaces the provider-native model options
// with the bound plan's models so the composer selector shows exactly what
// the plan authorizes, labeled with the source plan. Targets without an
// active plan binding keep their native options.
func (s *Service) applyModelPlanComposerOverlay(ctx context.Context, input ComposerOptionsInput, options ComposerOptions) ComposerOptions {
resolution := s.resolveModelPlan(
ctx,
input.WorkspaceID,
input.AgentTargetID,
options.Provider,
strings.TrimSpace(input.Settings.Model),
)
return applyResolvedModelPlanComposerOverlay(options, resolution)
}
func applyResolvedModelPlanComposerOverlay(options ComposerOptions, resolution modelPlanResolution) ComposerOptions {
endpoint := resolution.Endpoint
if endpoint == nil {
return options
}
planModels := resolution.Models
modelOptions := make([]ComposerConfigOptionValue, 0, len(planModels))
for _, model := range planModels {
value := planModelComposerValue(options.Provider, model.ID)
modelOptions = append(modelOptions, ComposerConfigOptionValue{
ID: value,
Label: model.Name,
Value: value,
Description: endpoint.PlanName,
})
}
options.EffectiveSettings.Model = endpoint.Model
options.ModelConfig = ComposerConfigOption{
Configurable: len(modelOptions) > 0,
CurrentValue: endpoint.Model,
DefaultValue: endpoint.Model,
Options: modelOptions,
}
if options.RuntimeContext == nil {
options.RuntimeContext = map[string]any{}
}
options.RuntimeContext["model"] = nullableString(endpoint.Model)
options.RuntimeContext["configOptions"] = composerConfigOptions(
options.Provider,
options.EffectiveSettings,
modelOptions,
options.ReasoningConfig.Options,
nil,
)
options.RuntimeContext["modelPlan"] = map[string]any{
"id": resolution.ModelConfiguration.ModelPlanID,
"name": endpoint.PlanName,
"protocol": endpoint.Protocol,
}
return options
}
// SessionStateObservers fans one projection observer slot out to several
// observers.
type SessionStateObservers []SessionStateObserver
func (observers SessionStateObservers) ObserveAgentSessionState(ctx context.Context, input canonical.ReportSessionStateInput, reply canonical.ReportSessionStateReply) {
for _, observer := range observers {
if observer != nil {
observer.ObserveAgentSessionState(ctx, input, reply)
}
}
}
// resolveCreateSessionModelForPlanOrProvider resolves and validates the
// session model for Create. A bound plan owns the model catalog: the request
// validates against the plan's model list and defaults to the plan-resolved
// model. Without a plan the provider-native resolution and validation apply.
func (s *Service) resolveCreateSessionModelForPlanOrProvider(ctx context.Context, workspaceID string, provider string, requestedModel string, input *CreateSessionInput) (modelPlanResolution, error) {
resolution := modelPlanResolution{}
if input.IgnoreModelPlanBinding {
resolution.ModelConfiguration = newProviderNativeModelConfiguration(provider, input.AgentTargetID)
} else if input.ResolvedModelPlan != nil {
var err error
resolution, err = resolveProvidedModelPlan(provider, input.AgentTargetID, *input.ResolvedModelPlan, input.AgentDefaultModel, requestedModel)
if err != nil {
return modelPlanResolution{}, err
}
} else {
resolution = s.resolveModelPlan(ctx, workspaceID, input.AgentTargetID, provider, requestedModel)
}
if resolution.Endpoint != nil {
if err := validateModelAgainstPlan(provider, requestedModel, resolution.Models); err != nil {
return modelPlanResolution{}, err
}
if strings.TrimSpace(resolution.Endpoint.Model) != "" {
resolvedModel := resolution.Endpoint.Model
input.Model = &resolvedModel
}
return resolution, nil
}
input.Model = s.resolveCreateSessionModel(ctx, provider, input.ProviderTargetRef, value(input.Cwd), input.Model)
if providerTargetRefKind(input.ProviderTargetRef) != "agent_extension" {
if err := s.validateComposerModelForCreate(ctx, provider, workspaceID, value(input.Cwd), requestedModel); err != nil {
return modelPlanResolution{}, err
}
}
return resolution, nil
}
// resolveProvidedModelPlan consumes the exact secret-bearing plan resolved by
// a WorkspaceAgent. It deliberately bypasses legacy AgentTarget bindings.
func resolveProvidedModelPlan(provider string, agentTargetID string, plan modelplanbiz.Plan, configuredDefaultModel string, requestedModel string) (modelPlanResolution, error) {
if strings.TrimSpace(plan.ID) == "" {
return modelPlanResolution{}, fmt.Errorf("%w: workspace agent model plan id is missing", ErrInvalidArgument)
}
requiredProtocol, supported := modelPlanProtocolForProvider(provider)
if !supported || plan.Protocol != requiredProtocol {
return modelPlanResolution{}, fmt.Errorf("%w: workspace agent model plan protocol does not match provider", ErrInvalidArgument)
}
if !plan.Enabled {
return modelPlanResolution{}, fmt.Errorf("%w: workspace agent model plan is disabled", ErrInvalidArgument)
}
if plan.Revision == 0 {
return modelPlanResolution{}, fmt.Errorf("%w: workspace agent model plan revision is missing", ErrInvalidArgument)
}
if err := validateModelAgainstPlan(provider, requestedModel, plan.Models); err != nil {
return modelPlanResolution{}, err
}
binding := modelbindingbiz.Binding{DefaultModel: strings.TrimSpace(configuredDefaultModel)}
model := resolvePlanSessionModel(plan, binding, planModelIDFromComposerValue(provider, requestedModel))
return modelPlanResolution{
Endpoint: &runtimeprep.ModelEndpointConfig{
PlanID: plan.ID,
PlanName: plan.Name,
Protocol: string(plan.Protocol),
BaseURL: plan.BaseURL,
APIKey: plan.APIKey,
Model: planModelComposerValue(provider, model),
Models: modelEndpointModels(plan.Models),
PlanUpdatedAtUnixMS: plan.UpdatedAt.UnixMilli(),
},
Models: modelplanbiz.CloneModels(plan.Models),
ModelConfiguration: newModelPlanModelConfiguration(provider, agentTargetID, binding, plan),
}, nil
}