package app import ( "albedo/cli/internal/daemon" "albedo/cli/internal/daemon/protocol" "context" "encoding/json" "slices" ) // ACPConnection owns HTTP workflows; the protocol controller owns observation policy. type ACPConnection struct { connection *daemon.Connection token string } type ACPModels struct { Current string Available []string } func (a *ACPConnection) Models(ctx context.Context) (ACPModels, error) { profiles, err := daemon.ProviderProfiles(ctx, a.connection) if err != nil { return ACPModels{}, err } models := ACPModels{} for _, catalog := range configuredModels(ctx, a.connection, profiles) { for _, model := range catalog.models { models.Available = append(models.Available, catalog.provider+"/"+model) } } return models, nil } func (models ACPModels) WithCurrent(current string) ACPModels { models.Current = current if !slices.Contains(models.Available, current) { models.Available = append(models.Available, current) } return models } func (a *ACPConnection) CurrentModel(ctx context.Context, id string) (string, error) { session, err := daemon.GetSession(ctx, a.connection, id) return session.Provider + "/" + session.Model, err } // SetModel returns the selected model; nil Available keeps the adapter's cached choices. func (a *ACPConnection) SetModel(ctx context.Context, id, value string) (ACPModels, error) { profiles, err := daemon.ProviderProfiles(ctx, a.connection) if err != nil { return ACPModels{}, err } provider, model, err := daemon.ResolveSessionModel(profiles, value) if err != nil { return ACPModels{}, &daemon.APIError{StatusCode: 400, Code: "model_unavailable", Message: err.Error()} } if provider+"/"+model != value { return ACPModels{}, &daemon.APIError{StatusCode: 400, Code: "model_unavailable", Message: "The selected provider is no longer configured; resume to refresh models"} } if err = switchModel(ctx, a.connection, id, modelChoice{provider, model}, a.token); err != nil { return ACPModels{}, err } models, err := a.Models(ctx) if err != nil { return ACPModels{Current: value}, nil } return models.WithCurrent(value), nil } func (s *Service) ConnectACP(ctx context.Context, token string) (*ACPConnection, error) { conn, err := s.Connect(ctx) if err != nil { return nil, err } info, err := daemon.ProbeServer(ctx, conn) if err != nil { return nil, err } if info.Capabilities["acp_attachment"] < 3 { return nil, &daemon.APIError{Code: "daemon_incompatible", Message: "Run `albedo daemon --restart` in a terminal to enable ACP."} } return &ACPConnection{connection: conn, token: token}, nil } func (a *ACPConnection) New(ctx context.Context, cwd string, servers []protocol.ACPMCPServer) (protocol.ACPAttachment, error) { profiles, err := daemon.ProviderProfiles(ctx, a.connection) if err != nil { return protocol.ACPAttachment{}, err } _, configured := profiles.Providers[profiles.Active] if profiles.Active == "" || !configured { return protocol.ACPAttachment{}, &daemon.APIError{Code: "provider_missing", Message: "Run `albedo login` in a terminal, then try again."} } id, err := daemon.CreateACPSession(ctx, a.connection, a.token, protocol.ACPSessionRequest{Kind: "acp", Workspace: cwd, MCPServers: servers}) if err != nil { return protocol.ACPAttachment{}, err } return a.Resume(ctx, id, cwd, servers) } func (a *ACPConnection) Resume(ctx context.Context, id, cwd string, servers []protocol.ACPMCPServer) (protocol.ACPAttachment, error) { return daemon.AttachACP(ctx, a.connection, id, a.token, protocol.ACPAttachRequest{Workspace: cwd, MCPServers: servers}) } func (a *ACPConnection) Prepare(id string, blocks []json.RawMessage) (*daemon.OperationHandle, error) { return daemon.NewACPInput(id, a.token, blocks) } func (a *ACPConnection) Submit(ctx context.Context, handle *daemon.OperationHandle) error { _, err := daemon.SubmitOperation(ctx, a.connection, handle) return err } func (a *ACPConnection) Renew(ctx context.Context, id string) error { return daemon.RenewACP(ctx, a.connection, id, a.token) } func (a *ACPConnection) Release(ctx context.Context, id string) error { return daemon.ReleaseACP(ctx, a.connection, id, a.token) } func (a *ACPConnection) Observer(id string, cursor protocol.Cursor) *daemon.ChatClient { client := daemon.NewChatClient(a.connection, id) client.SeedCursor(cursor) return client } func (a *ACPConnection) Cancel(ctx context.Context, session, id string) (protocol.InputCancellation, error) { return daemon.NewChatClient(a.connection, session).CancelInput(ctx, id, a.token) } func (a *ACPConnection) List(ctx context.Context, cwd, cursor *string) (protocol.SessionPage, error) { return daemon.ListACPSessions(ctx, a.connection, cwd, cursor) } func (a *ACPConnection) History(ctx context.Context, id string, cursor daemon.ACPHistoryCursor, cutoff int64) ([]daemon.StreamEvent, daemon.ACPHistoryCursor, error) { return daemon.ACPHistory(ctx, a.connection, id, cursor, cutoff) }